Qosmo

Qemu Calypso Firmware

Auteur·rice

Bastien Baranoff

Date de publication

28 octobre 2026

1 Synthèse

run élément valeur
(pas de verdict.txt)
dossier de run contenu
Verdict et couverture échelle des barreaux et tableau de couverture couche 1
Résultats des tests captures VTY, diagnostics, rapports pytest, grafcets (.txt .md .mmd)
Fichiers scripts du banc et des tests (.sh .py)
Logs journaux (.log), en dernier

Dossiers : /opt/GSM/qosmo-overlay. Extensions : log sh md mmd py txt c h. Entrée 2929 Ko, sortie 2910 Ko.

Compactage sans perte : fichiers identiques cités une fois ; lignes consécutives identiques « ×N » ; lignes ne différant que par des nombres : gabarit avec ⟨k⟩ puis la liste ordonnée des valeurs (k-uplets), chaque ligne se reconstruit en remettant les valeurs dans l’ordre. Seuls les codes couleur, la négociation telnet des captures VTY et les caractères de commande sont retirés. Les .md sont inclus tels quels (titres rétrogradés) et les .mmd rendus en diagrammes Mermaid, sans compactage.

2 Resultats des tests

2.1 https://github.com/bbaranoff/qosmo/blob/main/README.md

9888 octets, 189 lignes

2.1.1 qosmo - QEMU model of the TI Calypso baseband

QEMU machine for the TI Calypso GSM baseband (Compal E88 / Motorola C1xx). The unmodified osmocom-bb firmware boots and runs on it. One platform, with the layer 1 selected at configure time:

../configure --target-list=arm-softmmu                      # bare platform, no L1
../configure --target-list=arm-softmmu --enable-l1-grgsm    # gr-gsm demodulation
../configure --target-list=arm-softmmu --enable-l1-dsp      # TI mask ROM on an emulated C54x

The two --enable-l1-* options are mutually exclusive; meson rejects the combination.

2.1.1.1 Build and install
./install.sh --check          # prerequisites and state, changes nothing
sudo ./install.sh --with-deps # missing apt packages, then every step
./install.sh --list           # steps (venv configure build install bin verify) and values

Main options (and their environment variables): --prefix (PREFIX, default $GSM_ROOT/qemu-install), --build-dir (BUILD_DIR), --venv (VENV), --bin-dir (BIN_DIR, default /usr/local/bin), --destdir (DESTDIR), --cc (QOSMO_CC). ./install.sh --help lists them all.

Manual build:

mkdir build && cd build
../configure --target-list=arm-softmmu --disable-werror
ninja qemu-system-arm
./qemu-system-arm -M help | grep calypso   # names the linked L1: none, grgsm or c54x

Without an L1 the firmware boots and scans but never finds a cell. This is the expected way to test the platform alone: boot, IRQs, TDMA, UART and SIM.

2.1.1.2 Running the bench

run.sh starts the full bench (Osmocom core network, Asterisk, QEMU, osmocon, mobile, BTS side-cars) through the numbered steps in run_modules/, and lays out the panes with tmux_modules/. Its environment comes from environnement/bench.env (see environnement/README.md).

2.1.1.3 Layer-1 interface (CalypsoL1Ops)

calypso_l1_ops.h defines nine callbacks from the platform to the L1:

static const CalypsoL1Ops my_ops = {
    .name              = "grgsm",
    .init              = my_l1_init,           /* end of machine_init */
    .frame_tick        = my_l1_frame_tick,     /* once per TDMA frame */
    .api_read_override = my_l1_read_override,  /* API RAM read */
    .si_valid          = my_l1_si_valid,
    .l1s_fn            = my_l1s_fn,
    .burst_written     = my_l1_burst_written,  /* d_burst_d */
    .rach_written      = my_l1_rach_written,   /* d_rach */
    .page_written      = my_l1_page_written,   /* d_dsp_page */
    .uart_tx_byte      = my_tap_l1ctl,         /* optional */
};
static void reg(void) { calypso_l1_register(&my_ops); }
type_init(reg)

The L1 calls the platform directly (calypso_api_ram(), calypso_trx_get_fn(), calypso_trx_autosync_fn(), calypso_tsp_move(), calypso_tpu_run_scenario()). calypso_l1_disable(why) turns the linked L1 off when CALYPSO_DSP_EXTERN is set; the DCCH tap (calypso_dcch_tap.c) then publishes the dedicated channel.

2.1.1.4 External DSP bridge

This is the mode used on the bench: the TI mask ROM runs in c54x_exe, outside QEMU, and calypso_trx.c talks to it through calypso_dsp_pont.h.

  • Start c54x_exe --arm first, then QEMU with CALYPSO_DSP_EXTERN=1 (or a socket path). QEMU exits if the shared memory does not exist yet.
  • API RAM shared through /dev/shm/calypso_api_ram (16 KiB); frame-by-frame synchronisation on /tmp/calypso_dsp.sock (SOCK_SEQPACKET).
  • Messages: HELLO/HELLO_OK (magic 'C54X'), RESET, TICK (a = fn, b = page and flags), DONE (IRQ/IDLE/INIT flags), GO, DCCH.
  • TICK.b bit 16: frame IRQ armed by the ARM (TPU_CTRL_DSP_EN). Bit 17: two-phase tick (ROM ISR, DONE|PONT_DONE_PHASE_A, ARM frame IRQ and l1_sync(), then PONT_GO and the burst).
  • PONT_DCCH carries TN, type and sub-channel of SDCCH/4 (0), SDCCH/8 (1) or released (0xFF). calypso_dcch_tap.c learns it from the L1CTL stream and also publishes /dev/shm/calypso_dcch_cfg, which announces TCH/F (2) and TCH/H (3); the TCH switch itself follows the firmware task in c54x_exe.

The l1-dsp/ sources also build outside QEMU with contrib/hors-qemu/cales-qemu.c and the headers in contrib/hors-qemu/doublures/; c54x_exe and grgsm_exe compile them from this tree (QOSMO ?= /opt/GSM/qosmo).

2.1.1.4.1 QEMU-side environment variables
Variable (on qemu-system-arm) Effect
CALYPSO_DSP_EXTERN 1 or socket path: bridge to c54x_exe
CALYPSO_PONT_LOCKSTEP=1 QEMU waits for DONE each frame (required: the emulated C54x is slower than real time)
CALYPSO_PONT_ARM_FIRST=0 single-phase tick
CALYPSO_PONT_RETRY_DIV resend to the DSP every frame/N (default 16; c54x_exe/run.sh sets 64)
CALYPSO_CPU_KICK_NS CPU kick period (default 5 ms)
CALYPSO_PACER_RATTRAPAGE late frames the TDMA pacer catches up (default 20; 0 = strict grid)

CALYPSO_BSP_STREAM, CALYPSO_A5, CALYPSO_SONDES, CALYPSO_C54X_OVM, CALYPSO_MVKD_DMAD_AVANT and CALYPSO_HACK_SOFT_SCALE are read by l1-dsp/: with the external DSP, set them on c54x_exe, not on QEMU.

2.1.1.6 Status
Part State
Platform (SoC, TPU, TSP, SIM, ULPD, UART, timers, SPI/I2C, INTH, TRF6151, L1 dispatch, DCCH tap) Builds and boots alone
CalypsoL1Ops interface 9 callbacks, used by the gr-gsm L1
--enable-l1-grgsm Working
--enable-l1-dsp (C54x inside QEMU) l1-dsp/ compiles and links; the ops table is empty except .name = "c54x"
External DSP (CALYPSO_DSP_EXTERN) SCH, BCCH SI1-4, location update, SMS and TCH/F calls end to end; A5 ciphering on the XIO ports
E88 board devices (calypso_iota.c, calypso_rf3166.c, calypso_asm4532.c, calypso_xio.c, calypso_debug.c, calypso_invariants.c, fw_console.c, sercomm_gate.c, l1ctl_sock.c) Built only with --enable-l1-dsp; not wired: their entry point calypso_pcb_init() (in l1-dsp/calypso_full_pcb.c) is not called from calypso_soc_realize()
2.1.1.7 Known issues
  1. B_BFI on every speech frame with the external DSP. The ROM reports roughly 3 to 20 % bit errors on the 456 coded bits; FR speech stays intelligible because the firmware does not forward the BFI. Unknown whether the cause is the signal path (BSP, I/Q, equalisation) or the C54x core (Viterbi, re-encoding); a bit-by-bit comparison with the BTS frames is needed.
  2. Occasional LOS during a call: the SACCH downlink stops being accepted, the ACCH counter drops to 0 and the mobile reports “LOS during dedicated mode”. Not reproducible on every run.
  3. SDCCH/8 SACCH downlink: about half of the blocks are dropped. Suspect: the 45.002 table of the BSP for SACCH/8 over 102 frames.
  4. Real-time margin: in TCH the DSP work takes about 4.2 to 4.6 ms of the 4.62 ms frame, which leaves almost no headroom.
  5. Pacer catch-up (CALYPSO_PACER_RATTRAPAGE) is not validated for speech.
  6. The “kept in qosmo-dsp” block in hw/arm/calypso/meson.build is outdated.
2.1.1.8 Repository layout
Path Content
hw/arm/calypso/ Calypso platform devices, L1 dispatch, DCCH tap
hw/arm/calypso/l1-grgsm/ gr-gsm layer 1 and its registration
hw/arm/calypso/l1-dsp/ C54x core (exec, decode, mem, irq, probes), BSP, A5, mailbox, full PCB
hw/arm/calypso/doc/SONDES.md C54x core probes
include/hw/arm/calypso/ Public headers (calypso_l1_ops.h, calypso_dsp_pont.h, …)
hw/intc/calypso_inth.c Interrupt controller
contrib/hors-qemu/ POSIX shims to build l1-dsp/ outside QEMU
run.sh, run_modules/ Bench launcher and its numbered steps
tmux_modules/ tmux layout of the bench panes
environnement/ Bench environment file (bench.env)
cfgs/ mobile configuration for group runs
opt-gsm-scripts/ Helper scripts installed into /opt/GSM
tools/ injecteur_bruit.py, rapport-run.sh (run report), qosmo-launch, gdb helpers
install.sh Build and install script
make-overlay.sh, tout-en-un.py Source overlay and single-file dump helpers
everything else Upstream QEMU 9.2.4
2.1.1.9 License

Derived from QEMU 9.2.4: GPL-2.0-or-later (SPDX headers, COPYING). The repository also carries a GPLv3 LICENSE (“qosmo Copyright (C) 2026 Bastien Baranoff”).

2.2 https://github.com/bbaranoff/qosmo/blob/main/meson_options.txt

20174 octets, 393 lignes → 393 lignes

# These options do not correspond to a --enable/--disable-* option
# on the configure script command line.  If you add more, list them in
# scripts/meson-buildoptions.py's SKIP_OPTIONS constant too.

option('qemu_suffix', type : 'string', value: 'qemu',
       description: 'Suffix for QEMU data/modules/config directories (can be empty)')
option('docdir', type : 'string', value : 'share/doc',
       description: 'Base directory for documentation installation (can be empty)')
option('qemu_firmwarepath', type : 'array', value : ['share/qemu-firmware'],
       description: 'search PATH for firmware files')
option('pkgversion', type : 'string', value : '',
       description: 'use specified string as sub-version of the package')
option('smbd', type : 'string', value : '',
       description: 'Path to smbd for slirp networking')
option('iasl', type : 'string', value : '',
       description: 'Path to ACPI disassembler')
option('tls_priority', type : 'string', value : 'NORMAL',
       description: 'Default TLS protocol/cipher priority string')
option('default_devices', type : 'boolean', value : true,
       description: 'Include a default selection of devices in emulators')
option('audio_drv_list', type: 'array', value: ['default'],
       choices: ['alsa', 'coreaudio', 'default', 'dsound', 'jack', 'oss', 'pa', 'pipewire', 'sdl', 'sndio'],
       description: 'Set audio driver list')
option('block_drv_rw_whitelist', type : 'string', value : '',
       description: 'set block driver read-write whitelist (by default affects only QEMU, not tools like qemu-img)')
option('block_drv_ro_whitelist', type : 'string', value : '',
       description: 'set block driver read-only whitelist (by default affects only QEMU, not tools like qemu-img)')
option('interp_prefix', type : 'string', value : '/usr/gnemul/qemu-%M',
       description: 'where to find shared libraries etc., use %M for cpu name')
option('rtsig_map', type : 'string', value : 'NULL',
       description: 'default value of QEMU_RTSIG_MAP')
option('fuzzing_engine', type : 'string', value : '',
       description: 'fuzzing engine library for OSS-Fuzz')
option('trace_file', type: 'string', value: 'trace',
       description: 'Trace file prefix for simple backend')
option('coroutine_backend', type: 'combo',
       choices: ['ucontext', 'sigaltstack', 'windows', 'auto'],
       value: 'auto', description: 'coroutine backend to use')

# Everything else can be set via --enable/--disable-* option
# on the configure script command line.  After adding an option
# here make sure to run "make update-buildoptions".

option('docs', type : 'feature', value : 'auto',
       description: 'Documentations build support')
option('fuzzing', type : 'boolean', value: false,
       description: 'build fuzzing targets')
option('gettext', type : 'feature', value : 'auto',
       description: 'Localization of the GTK+ user interface')
option('modules', type : 'feature', value : 'disabled',
       description: 'modules support (non Windows)')
option('module_upgrades', type : 'boolean', value : false,
       description: 'try to load modules from alternate paths for upgrades')
option('install_blobs', type : 'boolean', value : true,
       description: 'install provided firmware blobs')
option('sparse', type : 'feature', value : 'auto',
       description: 'sparse checker')
option('guest_agent', type : 'feature', value : 'auto',
       description: 'Build QEMU Guest Agent')
option('guest_agent_msi', type : 'feature', value : 'auto',
       description: 'Build MSI package for the QEMU Guest Agent')
option('tools', type : 'feature', value : 'auto',
       description: 'build support utilities that come with QEMU')
option('qga_vss', type : 'feature', value: 'auto',
       description: 'build QGA VSS support (broken with MinGW)')

option('malloc_trim', type : 'feature', value : 'auto',
       description: 'enable libc malloc_trim() for memory optimization')
option('malloc', type : 'combo', choices : ['system', 'tcmalloc', 'jemalloc'],
       value: 'system', description: 'choose memory allocator to use')

option('kvm', type: 'feature', value: 'auto',
       description: 'KVM acceleration support')
option('whpx', type: 'feature', value: 'auto',
       description: 'WHPX acceleration support')
option('hvf', type: 'feature', value: 'auto',
       description: 'HVF acceleration support')
option('nvmm', type: 'feature', value: 'auto',
       description: 'NVMM acceleration support')
option('xen', type: 'feature', value: 'auto',
       description: 'Xen backend support')
option('xen_pci_passthrough', type: 'feature', value: 'auto',
       description: 'Xen PCI passthrough support')
option('tcg', type: 'feature', value: 'enabled',
       description: 'TCG support')
option('plugins', type: 'boolean', value: false,
       description: 'TCG plugins via shared library loading')
option('debug_tcg', type: 'boolean', value: false,
       description: 'TCG debugging')
option('debug_remap', type: 'boolean', value: false,
       description: 'syscall buffer debugging support')
option('tcg_interpreter', type: 'boolean', value: false,
       description: 'TCG with bytecode interpreter (slow)')
option('safe_stack', type: 'boolean', value: false,
       description: 'SafeStack Stack Smash Protection (requires clang/llvm and coroutine backend ucontext)')
option('asan', type: 'boolean', value: false,
       description: 'enable address sanitizer')
option('ubsan', type: 'boolean', value: false,
       description: 'enable undefined behaviour sanitizer')
option('tsan', type: 'boolean', value: false,
       description: 'enable thread sanitizer')
option('stack_protector', type: 'feature', value: 'auto',
       description: 'compiler-provided stack protection')
option('cfi', type: 'boolean', value: false,
       description: 'Control-Flow Integrity (CFI)')
option('cfi_debug', type: 'boolean', value: false,
       description: 'Verbose errors in case of CFI violation')
option('multiprocess', type: 'feature', value: 'auto',
       description: 'Out of process device emulation support')
option('relocatable', type : 'boolean', value : true,
       description: 'toggle relocatable install')
option('vfio_user_server', type: 'feature', value: 'disabled',
       description: 'vfio-user server support')
option('dbus_display', type: 'feature', value: 'auto',
       description: '-display dbus support')
option('tpm', type : 'feature', value : 'auto',
       description: 'TPM support')

# Do not enable it by default even for Mingw32, because it doesn't
# work on Wine.
option('membarrier', type: 'feature', value: 'disabled',
       description: 'membarrier system call (for Linux 4.14+ or Windows')

option('avx2', type: 'feature', value: 'auto',
       description: 'AVX2 optimizations')
option('avx512bw', type: 'feature', value: 'auto',
       description: 'AVX512BW optimizations')
option('keyring', type: 'feature', value: 'auto',
       description: 'Linux keyring support')
option('libkeyutils', type: 'feature', value: 'auto',
       description: 'Linux keyutils support')

option('af_xdp', type : 'feature', value : 'auto',
       description: 'AF_XDP network backend support')
option('attr', type : 'feature', value : 'auto',
       description: 'attr/xattr support')
option('auth_pam', type : 'feature', value : 'auto',
       description: 'PAM access control')
option('brlapi', type : 'feature', value : 'auto',
       description: 'brlapi character device driver')
option('bzip2', type : 'feature', value : 'auto',
       description: 'bzip2 support for DMG images')
option('cap_ng', type : 'feature', value : 'auto',
       description: 'cap_ng support')
option('blkio', type : 'feature', value : 'auto',
       description: 'libblkio block device driver')
option('bpf', type : 'feature', value : 'auto',
        description: 'eBPF support')
option('cocoa', type : 'feature', value : 'auto',
       description: 'Cocoa user interface (macOS only)')
option('curl', type : 'feature', value : 'auto',
       description: 'CURL block device driver')
option('gio', type : 'feature', value : 'auto',
       description: 'use libgio for D-Bus support')
option('glusterfs', type : 'feature', value : 'auto',
       description: 'Glusterfs block device driver')
option('hv_balloon', type : 'feature', value : 'auto',
       description: 'hv-balloon driver (requires Glib 2.68+ GTree API)')
option('libdw', type : 'feature', value : 'auto',
       description: 'debuginfo support')
option('libiscsi', type : 'feature', value : 'auto',
       description: 'libiscsi userspace initiator')
option('libnfs', type : 'feature', value : 'auto',
       description: 'libnfs block device driver')
option('mpath', type : 'feature', value : 'auto',
       description: 'Multipath persistent reservation passthrough')
option('numa', type : 'feature', value : 'auto',
       description: 'libnuma support')
option('iconv', type : 'feature', value : 'auto',
       description: 'Font glyph conversion support')
option('curses', type : 'feature', value : 'auto',
       description: 'curses UI')
option('libcbor', type : 'feature', value : 'auto',
       description: 'libcbor support')
option('gnutls', type : 'feature', value : 'auto',
       description: 'GNUTLS cryptography support')
option('nettle', type : 'feature', value : 'auto',
       description: 'nettle cryptography support')
option('gcrypt', type : 'feature', value : 'auto',
       description: 'libgcrypt cryptography support')
option('crypto_afalg', type : 'feature', value : 'disabled',
       description: 'Linux AF_ALG crypto backend driver')
option('libdaxctl', type : 'feature', value : 'auto',
       description: 'libdaxctl support')
option('libpmem', type : 'feature', value : 'auto',
       description: 'libpmem support')
option('libssh', type : 'feature', value : 'auto',
       description: 'ssh block device support')
option('libudev', type : 'feature', value : 'auto',
       description: 'Use libudev to enumerate host devices')
option('libusb', type : 'feature', value : 'auto',
       description: 'libusb support for USB passthrough')
option('linux_aio', type : 'feature', value : 'auto',
       description: 'Linux AIO support')
option('linux_io_uring', type : 'feature', value : 'auto',
       description: 'Linux io_uring support')
option('lzfse', type : 'feature', value : 'auto',
       description: 'lzfse support for DMG images')
option('lzo', type : 'feature', value : 'auto',
       description: 'lzo compression support')
option('rbd', type : 'feature', value : 'auto',
       description: 'Ceph block device driver')
option('opengl', type : 'feature', value : 'auto',
       description: 'OpenGL support')
option('rdma', type : 'feature', value : 'auto',
       description: 'Enable RDMA-based migration')
option('gtk', type : 'feature', value : 'auto',
       description: 'GTK+ user interface')
option('sdl', type : 'feature', value : 'auto',
       description: 'SDL user interface')
option('sdl_image', type : 'feature', value : 'auto',
       description: 'SDL Image support for icons')
option('seccomp', type : 'feature', value : 'auto',
       description: 'seccomp support')
option('smartcard', type : 'feature', value : 'auto',
       description: 'CA smartcard emulation support')
option('snappy', type : 'feature', value : 'auto',
       description: 'snappy compression support')
option('spice', type : 'feature', value : 'auto',
       description: 'Spice server support')
option('spice_protocol', type : 'feature', value : 'auto',
       description: 'Spice protocol support')
option('u2f', type : 'feature', value : 'auto',
       description: 'U2F emulation support')
option('canokey', type : 'feature', value : 'auto',
       description: 'CanoKey support')
option('usb_redir', type : 'feature', value : 'auto',
       description: 'libusbredir support')
option('l2tpv3', type : 'feature', value : 'auto',
       description: 'l2tpv3 network backend support')
option('netmap', type : 'feature', value : 'auto',
       description: 'netmap network backend support')
option('pixman', type : 'feature', value : 'auto',
       description: 'pixman support')
option('slirp', type: 'feature', value: 'auto',
       description: 'libslirp user mode network backend support')
option('vde', type : 'feature', value : 'auto',
       description: 'vde network backend support')
option('vmnet', type : 'feature', value : 'auto',
       description: 'vmnet.framework network backend support')
option('virglrenderer', type : 'feature', value : 'auto',
       description: 'virgl rendering support')
option('rutabaga_gfx', type : 'feature', value : 'auto',
       description: 'rutabaga_gfx support')
option('png', type : 'feature', value : 'auto',
       description: 'PNG support with libpng')
option('vnc', type : 'feature', value : 'auto',
       description: 'VNC server')
option('vnc_jpeg', type : 'feature', value : 'auto',
       description: 'JPEG lossy compression for VNC server')
option('vnc_sasl', type : 'feature', value : 'auto',
       description: 'SASL authentication for VNC server')
option('vte', type : 'feature', value : 'auto',
       description: 'vte support for the gtk UI')

# GTK Clipboard implementation is disabled by default, since it may cause hangs
# of the guest VCPUs. See gitlab issue 1150:
# https://gitlab.com/qemu-project/qemu/-/issues/1150

option('gtk_clipboard', type: 'feature', value : 'disabled',
       description: 'clipboard support for the gtk UI (EXPERIMENTAL, MAY HANG)')
option('xkbcommon', type : 'feature', value : 'auto',
       description: 'xkbcommon support')
option('zstd', type : 'feature', value : 'auto',
       description: 'zstd compression support')
option('qpl', type : 'feature', value : 'auto',
       description: 'Query Processing Library support')
option('uadk', type : 'feature', value : 'auto',
       description: 'UADK Library support')
option('qatzip', type: 'feature', value: 'auto',
       description: 'QATzip compression support')
option('fuse', type: 'feature', value: 'auto',
       description: 'FUSE block device export')
option('fuse_lseek', type : 'feature', value : 'auto',
       description: 'SEEK_HOLE/SEEK_DATA support for FUSE exports')

option('trace_backends', type: 'array', value: ['log'],
       choices: ['dtrace', 'ftrace', 'log', 'nop', 'simple', 'syslog', 'ust'],
       description: 'Set available tracing backends')

option('alsa', type: 'feature', value: 'auto',
       description: 'ALSA sound support')
option('coreaudio', type: 'feature', value: 'auto',
       description: 'CoreAudio sound support')
option('dsound', type: 'feature', value: 'auto',
       description: 'DirectSound sound support')
option('jack', type: 'feature', value: 'auto',
       description: 'JACK sound support')
option('oss', type: 'feature', value: 'auto',
       description: 'OSS sound support')
option('pa', type: 'feature', value: 'auto',
       description: 'PulseAudio sound support')
option('pipewire', type: 'feature', value: 'auto',
       description: 'PipeWire sound support')
option('sndio', type: 'feature', value: 'auto',
       description: 'sndio sound support')

option('vhost_kernel', type: 'feature', value: 'auto',
       description: 'vhost kernel backend support')
option('vhost_net', type: 'feature', value: 'auto',
       description: 'vhost-net kernel acceleration support')
option('vhost_user', type: 'feature', value: 'auto',
       description: 'vhost-user backend support')
option('vhost_crypto', type: 'feature', value: 'auto',
       description: 'vhost-user crypto backend support')
option('vhost_vdpa', type: 'feature', value: 'auto',
       description: 'vhost-vdpa kernel backend support')
option('vhost_user_blk_server', type: 'feature', value: 'auto',
       description: 'build vhost-user-blk server')
option('virtfs', type: 'feature', value: 'auto',
       description: 'virtio-9p support')
option('libvduse', type: 'feature', value: 'auto',
       description: 'build VDUSE Library')
option('vduse_blk_export', type: 'feature', value: 'auto',
       description: 'VDUSE block export support')

option('capstone', type: 'feature', value: 'auto',
       description: 'Whether and how to find the capstone library')
option('fdt', type: 'combo', value: 'auto',
       choices: ['disabled', 'enabled', 'auto', 'system', 'internal'],
       description: 'Whether and how to find the libfdt library')

option('selinux', type: 'feature', value: 'auto',
       description: 'SELinux support in qemu-nbd')
option('replication', type: 'feature', value: 'auto',
       description: 'replication support')
option('colo_proxy', type: 'feature', value: 'auto',
       description: 'colo-proxy support')
option('bochs', type: 'feature', value: 'auto',
       description: 'bochs image format support')
option('cloop', type: 'feature', value: 'auto',
       description: 'cloop image format support')
option('dmg', type: 'feature', value: 'auto',
       description: 'dmg image format support')
option('qcow1', type: 'feature', value: 'auto',
       description: 'qcow1 image format support')
option('vdi', type: 'feature', value: 'auto',
       description: 'vdi image format support')
option('vhdx', type: 'feature', value: 'auto',
       description: 'vhdx image format support')
option('vmdk', type: 'feature', value: 'auto',
       description: 'vmdk image format support')
option('vpc', type: 'feature', value: 'auto',
       description: 'vpc image format support')
option('vvfat', type: 'feature', value: 'auto',
       description: 'vvfat image format support')
option('qed', type: 'feature', value: 'auto',
       description: 'qed image format support')
option('parallels', type: 'feature', value: 'auto',
       description: 'parallels image format support')
option('block_drv_whitelist_in_tools', type: 'boolean', value: false,
       description: 'use block whitelist also in tools instead of only QEMU')
option('rng_none', type: 'boolean', value: false,
       description: 'dummy RNG, avoid using /dev/(u)random and getrandom()')
option('coroutine_pool', type: 'boolean', value: true,
       description: 'coroutine freelist (better performance)')
option('debug_graph_lock', type: 'boolean', value: false,
       description: 'graph lock debugging support')
option('debug_mutex', type: 'boolean', value: false,
       description: 'mutex debugging support')
option('debug_stack_usage', type: 'boolean', value: false,
       description: 'measure coroutine stack usage')
option('qom_cast_debug', type: 'boolean', value: true,
       description: 'cast debugging support')
option('slirp_smbd', type : 'feature', value : 'auto',
       description: 'use smbd (at path --smbd=*) in slirp networking')

option('qemu_ga_manufacturer', type: 'string', value: 'QEMU',
       description: '"manufacturer" name for qemu-ga registry entries')
option('qemu_ga_distro', type: 'string', value: 'Linux',
       description: 'second path element in qemu-ga registry entries')
option('qemu_ga_version', type: 'string', value: '',
       description: 'version number for qemu-ga installer')

option('hexagon_idef_parser', type : 'boolean', value : true,
       description: 'use idef-parser to automatically generate TCG code for the Hexagon frontend')

option('x86_version', type : 'combo', choices : ['0', '1', '2', '3', '4'], value: '1',
       description: 'tweak required x86_64 architecture version beyond compiler default')

option('rust', type: 'feature', value: 'disabled',
       description: 'Rust support')
option('strict_rust_lints', type: 'boolean', value: false,
       description: 'Enable stricter set of Rust warnings')

# [2026-09-16] Le choix de la couche 1 de la machine Calypso. Voir
# include/hw/arm/calypso/calypso_l1_ops.h. Les deux sont exclusives : deux L1
# liees dans le meme binaire, c'est deux appels a calypso_l1_register(), donc
# la derniere qui gagne en silence. meson.build refuse la combinaison.
option('l1_grgsm', type: 'feature', value: 'disabled',
       description: 'Calypso: couche 1 gr-gsm (demodulation cote hote)')
option('l1_dsp', type: 'feature', value: 'disabled',
       description: 'Calypso: couche 1 DSP C54x (mask-ROM TI reelle)')

2.3 https://github.com/bbaranoff/qosmo/blob/main/environnement/README.md

1771 octets, 41 lignes

2.3.1 environnement/ - bench configuration

run.sh sources bench.env with set -a before running the steps in run_modules/. Every value uses : "${VAR:=default}", so a value given on the command line or in the caller’s environment always wins:

LOG_DIR=/var/tmp/calypso ENCRYPTION="a5 1" ./run.sh
./run.sh --check-paths        # check the paths below on this machine
2.3.1.1 Variables in bench.env
Group Variables
Paths GSM_ROOT, QEMU_TREE, QEMU_BIN, QEMU_CFGS, FIRMWARE_DIR, FIRMWARE_ELF, FIRMWARE_BIN, OSMOCOM_BB, OSMOCON, OSMOCOM_CFG, OSMOCOM_HOME, NITB_ROOT
Run and logs RUN_DIR (default $XDG_RUNTIME_DIR/calypso), LOG_DIR, CAPTURE_DIR, TMUX_SESSION
Core network ENCRYPTION (default a5 0)
Mobiles CALYPSO_MOBILE_DEBUG (log categories), CALYPSO_PULSE_LATENCY_MSEC
Display CALYPSO_LANG (en), CALYPSO_NO_ATTACH (1 = do not attach tmux)

FIRMWARE_ELF defaults to layer1.highram_patched.elf when it exists (B_BFI frames, FACCH steals included, passed at zero gain and masked by the GAPK ECU), otherwise to layer1.highram.elf.

The noise injector variables (BRUIT_*) are not set here; see the “Downlink noise injector” section of the repository README.

2.3.1.2 Gate idioms in the C code

A CALYPSO_* variable does not always turn off with =0:

Idiom Active when How to turn off
getenv("X") ? 1 : 0 the variable exists, even as 0 unset X
atoi(...) > 0 value > 0 X=0
*e == '1' exactly "1" any other value
default ON + X_OFF by default set X_OFF

QEMU logs the effective configuration as calypso-manifest lines in qemu.log; check there rather than trusting the command line.

2.4 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/doc/SONDES.md

3857 octets, 72 lignes

2.4.1 Probing the emulated Calypso DSP

First hypothesis to test: the probe is wrong, not the emulated DSP. A broken probe usually produces plausible numbers; check the measurement before building a hypothesis on it.

2.4.1.1 Rules
  1. Prove the probe is active in the mode being run. Never print a counter that is filled in only one branch without printing “not measured” in the other.
  2. Locate a buffer by RAM difference (snapshot before, diff after), never by assuming an address.
  3. Respect types. DSP memory is a uint16_t array holding signed int16_t values. Cast for every magnitude comparison, and for a signed quantity print the maximum absolute value NEXT TO the signed maximum.
  4. Go through the core’s address translation, not the raw arrays. prog[] and data[] are not what the DSP reads: the OVLY alias (program reads in 0x0060-0x27FF go to data[] when PMST_OVLY is set), XPC banking and MMRs sit in between. Reproduce the translation or call the core function.
  5. Read compiler warnings, not only errors (-Wformat catches argument shifts in long trace lines).
  6. Qualify success in code, not by reading the log. On this bench a CRC OK counts as a decode only if it returns the INJECTED BSIC, T3 <= 50 and an FN equal to the frame: print both numbers (CRC OK: N, dont VRAIES: M).
2.4.1.2 Zero witness

Define a region known to weigh zero and measure it in every campaign. For the SB, the 21 samples before and after the burst carry no information: perturbing one must not change the output. If a margin sample starts to weigh, the probe or the protocol is broken. Measure in max |delta| of value (not sign changes), and check that the witness floor is separate from the useful-region floor.

2.4.1.3 PC reported by memory probes

The dispatcher advances the PC before the probe sees it, so memory-write probes in c54x_mem.c usually report the PC of the NEXT instruction (the writer is pc-1), but not for every instruction family. Never name a faulty instruction from a memory probe’s PC: confirm with a single-step trace that prints the PC BEFORE execution with the real opcode (CALYPSO_PISTE_LO/HI), and decode the opcode against doc/opcodes/tic54x-opc.c.

2.4.1.4 Verbosity filter of c54x_exe

c54x_exe (src/verbosite.c) pipes stderr and classifies each line by a keyword of the probe name: WATCH, DUMP, SCAN, PROBE, MAP are level 4, TRACE, LOOP, CYCLE level 5. Without -vvvv / -vvvvv those probes print nothing. Before concluding from silence, rerun with -vvvvvv; for an unconditional probe, choose a name without these keywords.

2.4.1.5 Bench pitfalls
  • getenv("X") is TRUE for X=0: use a helper that treats 0, "", off, non as false.
  • REJEU_SCH_PARTOUT=1 corrupts the reference: cellule.c computes t3p = p51 / 10, only correct for p51 in {1, 11, 21, 31, 41}. FN cannot be validated with this flag.
  • The GMSK sampling offset must be 0.5 (symbol centre); at 0.0 the offset becomes undetermined and everything downstream is unstable.
  • The SB window is 380 words (190 complex samples). Follow the length the DSP programs (ALGTH) instead of hard-coding it.
2.4.1.6 Probes
Variable Effect
CALYPSO_WATCH_WR_LO/HI/N chronological log of every write in a data range (index, value, old value, PC, insn)
CALYPSO_PISTE_LO/HI/N single-step over a PC range: PC BEFORE execution, real opcode, next word, A/B/T/AR0-7/BRC, words pointed to by AR2..AR5, DP/CPL/SP and candidate direct addresses
CALYPSO_PISTE_DUMP_LO/HI dump of a data range at each pass through the PC range
CALYPSO_T_LO/HI within an instruction window: every change of T (PISTE-T) and every clearing of a non-zero accumulator (PISTE-NUL)

All of them need -vvvv / -vvvvv and stay inert until their bounds are set.

2.5 https://github.com/bbaranoff/qosmo/blob/main/tests/detail.mmd

13556 octets, 188 lignes

graph TD
  classDef pass    fill:#bef5b1,stroke:#1f7a1f,color:#0a3d0a;
  classDef partial fill:#fde79c,stroke:#b07a00,color:#3a2c00;
  classDef fail    fill:#fbb4b4,stroke:#a31a1a,color:#5a0000;
  classDef skip    fill:#e0e0e0,stroke:#6a6a6a,color:#333;
  classDef xfail   fill:#fde79c,stroke:#b07a00,color:#3a2c00;
  C_Injection["Injection<br/>9/21"]
  class C_Injection partial;
  C_Injection_L_ARM_feedback["ARM-feedback<br/>0/2"]
  class C_Injection_L_ARM_feedback fail;
  C_Injection --> C_Injection_L_ARM_feedback
  C_Injection_L_ARM_feedback --> T_001_test_efficacy_arm_reads_d_fb_det_1["test_efficacy_arm_reads_d_fb_det_1<br/>inject_efficacy<br/>3.15s"]
  class T_001_test_efficacy_arm_reads_d_fb_det_1 fail;
  C_Injection_L_ARM_feedback --> T_002_test_efficacy_arm_reads_a_cd["test_efficacy_arm_reads_a_cd<br/>inject_efficacy<br/>3.11s"]
  class T_002_test_efficacy_arm_reads_a_cd xfail;
  C_Injection_L_NDB["NDB<br/>9/19"]
  class C_Injection_L_NDB partial;
  C_Injection --> C_Injection_L_NDB
  C_Injection_L_NDB --> T_003_test_probe_ndb["test_probe_ndb<br/>inject_frames<br/>0.29s"]
  class T_003_test_probe_ndb pass;
  C_Injection_L_NDB --> T_004_test_clear_ndb["test_clear_ndb<br/>inject_frames<br/>0.29s"]
  class T_004_test_clear_ndb pass;
  C_Injection_L_NDB --> T_005_test_inject_fbsb_fb_found["test_inject_fbsb_fb_found<br/>inject_frames<br/>0.62s"]
  class T_005_test_inject_fbsb_fb_found fail;
  C_Injection_L_NDB --> T_006_test_inject_fbsb_sb_found["test_inject_fbsb_sb_found<br/>inject_frames<br/>0.54s"]
  class T_006_test_inject_fbsb_sb_found pass;
  C_Injection_L_NDB --> T_007_test_inject_si_1_["test_inject_si(1)<br/>inject_frames<br/>0.33s"]
  class T_007_test_inject_si_1_ fail;
  C_Injection_L_NDB --> T_008_test_inject_si_2_["test_inject_si(2)<br/>inject_frames<br/>0.33s"]
  class T_008_test_inject_si_2_ fail;
  C_Injection_L_NDB --> T_009_test_inject_si_3_["test_inject_si(3)<br/>inject_frames<br/>0.33s"]
  class T_009_test_inject_si_3_ fail;
  C_Injection_L_NDB --> T_010_test_inject_si_4_["test_inject_si(4)<br/>inject_frames<br/>0.33s"]
  class T_010_test_inject_si_4_ fail;
  C_Injection_L_NDB --> T_011_test_inject_si_5_["test_inject_si(5)<br/>inject_frames<br/>0.37s"]
  class T_011_test_inject_si_5_ fail;
  C_Injection_L_NDB --> T_012_test_inject_si_6_["test_inject_si(6)<br/>inject_frames<br/>0.33s"]
  class T_012_test_inject_si_6_ fail;
  C_Injection_L_NDB --> T_013_test_inject_a_cd_pattern_23B["test_inject_a_cd_pattern_23B<br/>inject_frames<br/>0.33s"]
  class T_013_test_inject_a_cd_pattern_23B fail;
  C_Injection_L_NDB --> T_014_test_inject_a_cd_pattern_30B["test_inject_a_cd_pattern_30B<br/>inject_frames<br/>0.33s"]
  class T_014_test_inject_a_cd_pattern_30B fail;
  C_Injection_L_NDB --> T_015_test_inject_a_cd_invalid_length["test_inject_a_cd_invalid_length<br/>inject_frames<br/>0.00s"]
  class T_015_test_inject_a_cd_invalid_length pass;
  C_Injection_L_NDB --> T_016_test_inject_d_task_5_0_["test_inject_d_task(5-0)<br/>inject_frames<br/>0.04s"]
  class T_016_test_inject_d_task_5_0_ pass;
  C_Injection_L_NDB --> T_017_test_inject_d_task_6_1_["test_inject_d_task(6-1)<br/>inject_frames<br/>0.04s"]
  class T_017_test_inject_d_task_6_1_ pass;
  C_Injection_L_NDB --> T_018_test_inject_d_task_24_0_["test_inject_d_task(24-0)<br/>inject_frames<br/>0.04s"]
  class T_018_test_inject_d_task_24_0_ pass;
  C_Injection_L_NDB --> T_019_test_inject_d_task_28_1_["test_inject_d_task(28-1)<br/>inject_frames<br/>0.04s"]
  class T_019_test_inject_d_task_28_1_ fail;
  C_Injection_L_NDB --> T_020_test_inject_d_burst_0_["test_inject_d_burst(0)<br/>inject_frames<br/>0.04s"]
  class T_020_test_inject_d_burst_0_ pass;
  C_Injection_L_NDB --> T_021_test_inject_d_burst_1_["test_inject_d_burst(1)<br/>inject_frames<br/>0.04s"]
  class T_021_test_inject_d_burst_1_ pass;
  C_Milestone["Milestone<br/>6/21"]
  class C_Milestone partial;
  C_Milestone_L_DSP["DSP<br/>4/10"]
  class C_Milestone_L_DSP partial;
  C_Milestone --> C_Milestone_L_DSP
  C_Milestone_L_DSP --> T_022_test_popm_decoder_active["test_popm_decoder_active<br/>milestone_dsp_decoder<br/>0.00s"]
  class T_022_test_popm_decoder_active pass;
  C_Milestone_L_DSP --> T_023_test_tier_a_decoder_fixes_present["test_tier_a_decoder_fixes_present<br/>milestone_dsp_decoder<br/>0.00s"]
  class T_023_test_tier_a_decoder_fixes_present pass;
  C_Milestone_L_DSP --> T_024_test_intm_dwell_no_regression["test_intm_dwell_no_regression<br/>milestone_dsp_decoder<br/>30.57s"]
  class T_024_test_intm_dwell_no_regression pass;
  C_Milestone_L_DSP --> T_025_test_dsp_throughput_5x["test_dsp_throughput_5x<br/>milestone_dsp_decoder<br/>0.13s"]
  class T_025_test_dsp_throughput_5x pass;
  C_Milestone_L_DSP --> T_026_test_fb0_att_nonzero["test_fb0_att_nonzero<br/>milestone_fb_det<br/>0.00s"]
  class T_026_test_fb0_att_nonzero xfail;
  C_Milestone_L_DSP --> T_027_test_synth_zero_path_active["test_synth_zero_path_active<br/>milestone_fb_det<br/>0.11s"]
  class T_027_test_synth_zero_path_active skip;
  C_Milestone_L_DSP --> T_028_test_c54x_interrupt_ex_called_nonzero["test_c54x_interrupt_ex_called_nonzero<br/>milestone_irq<br/>0.00s"]
  class T_028_test_c54x_interrupt_ex_called_nonzero xfail;
  C_Milestone_L_DSP --> T_029_test_isr_entered_implies_rete["test_isr_entered_implies_rete<br/>milestone_irq<br/>0.00s"]
  class T_029_test_isr_entered_implies_rete xfail;
  C_Milestone_L_DSP --> T_030_test_no_pending_irq_gated["test_no_pending_irq_gated<br/>milestone_irq<br/>0.00s"]
  class T_030_test_no_pending_irq_gated xfail;
  C_Milestone_L_DSP --> T_031_test_rxdoneflag_no_longer_blocks["test_rxdoneflag_no_longer_blocks<br/>milestone_dsp_decoder<br/>0.00s"]
  class T_031_test_rxdoneflag_no_longer_blocks xfail;
  C_Milestone_L_L1_ctrl["L1-ctrl<br/>0/2"]
  class C_Milestone_L_L1_ctrl fail;
  C_Milestone --> C_Milestone_L_L1_ctrl
  C_Milestone_L_L1_ctrl --> T_032_test_l1ctl_data_ind_received["test_l1ctl_data_ind_received<br/>milestone_l1ctl<br/>0.16s"]
  class T_032_test_l1ctl_data_ind_received fail;
  C_Milestone_L_L1_ctrl --> T_033_test_neigh_pm_req_response_unchanged["test_neigh_pm_req_response_unchanged<br/>milestone_l1ctl<br/>0.00s"]
  class T_033_test_neigh_pm_req_response_unchanged xfail;
  C_Milestone_L_L1_data["L1-data<br/>2/4"]
  class C_Milestone_L_L1_data partial;
  C_Milestone --> C_Milestone_L_L1_data
  C_Milestone_L_L1_data --> T_034_test_ar3_init_source_identified["test_ar3_init_source_identified<br/>milestone_bsp_dma<br/>0.01s"]
  class T_034_test_ar3_init_source_identified skip;
  C_Milestone_L_L1_data --> T_035_test_ar4_init_source_identified["test_ar4_init_source_identified<br/>milestone_bsp_dma<br/>0.01s"]
  class T_035_test_ar4_init_source_identified skip;
  C_Milestone_L_L1_data --> T_036_test_bsp_dma_target_matches_correlator_read_zone["test_bsp_dma_target_matches_correlator<br/>milestone_bsp_dma<br/>0.06s"]
  class T_036_test_bsp_dma_target_matches_correlator_read_zone pass;
  C_Milestone_L_L1_data --> T_037_test_no_d_fb_det_wr_site_anomaly["test_no_d_fb_det_wr_site_anomaly<br/>milestone_bsp_dma<br/>0.17s"]
  class T_037_test_no_d_fb_det_wr_site_anomaly pass;
  C_Milestone_L_L3_MM["L3-MM<br/>0/5"]
  class C_Milestone_L_L3_MM fail;
  C_Milestone --> C_Milestone_L_L3_MM
  C_Milestone_L_L3_MM --> T_038_test_rach_emitted["test_rach_emitted<br/>milestone_mm_lu<br/>0.00s"]
  class T_038_test_rach_emitted xfail;
  C_Milestone_L_L3_MM --> T_039_test_immediate_assignment_decoded["test_immediate_assignment_decoded<br/>milestone_mm_lu<br/>0.00s"]
  class T_039_test_immediate_assignment_decoded xfail;
  C_Milestone_L_L3_MM --> T_040_test_rr_sdcch_established["test_rr_sdcch_established<br/>milestone_mm_lu<br/>0.00s"]
  class T_040_test_rr_sdcch_established xfail;
  C_Milestone_L_L3_MM --> T_041_test_location_updating_request_sent["test_location_updating_request_sent<br/>milestone_mm_lu<br/>0.00s"]
  class T_041_test_location_updating_request_sent xfail;
  C_Milestone_L_L3_MM --> T_042_test_location_updating_accept_received["test_location_updating_accept_received<br/>milestone_mm_lu<br/>0.00s"]
  class T_042_test_location_updating_accept_received xfail;
  C_Runtime["Runtime<br/>19/28"]
  class C_Runtime partial;
  C_Runtime_L_DSP["DSP<br/>6/10"]
  class C_Runtime_L_DSP partial;
  C_Runtime --> C_Runtime_L_DSP
  C_Runtime_L_DSP --> T_043_test_no_wait_a21a_on_window["test_no_wait_a21a_on_window<br/>runtime_dsp<br/>0.07s"]
  class T_043_test_no_wait_a21a_on_window pass;
  C_Runtime_L_DSP --> T_044_test_no_enter_7740_dwell["test_no_enter_7740_dwell<br/>runtime_dsp<br/>0.08s"]
  class T_044_test_no_enter_7740_dwell pass;
  C_Runtime_L_DSP --> T_045_test_intm_reaches_zero["test_intm_reaches_zero<br/>runtime_dsp<br/>0.07s"]
  class T_045_test_intm_reaches_zero pass;
  C_Runtime_L_DSP --> T_046_test_dsp_throughput_above_threshold["test_dsp_throughput_above_threshold<br/>runtime_dsp<br/>0.07s"]
  class T_046_test_dsp_throughput_above_threshold pass;
  C_Runtime_L_DSP --> T_047_test_d_fb_det_pattern_unchanged["test_d_fb_det_pattern_unchanged<br/>runtime_dsp<br/>0.06s"]
  class T_047_test_d_fb_det_pattern_unchanged xfail;
  C_Runtime_L_DSP --> T_048_test_bsp_daram_write_distribution["test_bsp_daram_write_distribution<br/>runtime_dsp<br/>0.08s"]
  class T_048_test_bsp_daram_write_distribution pass;
  C_Runtime_L_DSP --> T_049_test_d_fb_det_data_no_longer_zero["test_d_fb_det_data_no_longer_zero<br/>runtime_dsp<br/>0.08s"]
  class T_049_test_d_fb_det_data_no_longer_zero pass;
  C_Runtime_L_DSP --> T_050_test_interrupt_ex_called_counter_exposed["test_interrupt_ex_called_counter_expos<br/>runtime_irq<br/>0.06s"]
  class T_050_test_interrupt_ex_called_counter_exposed xfail;
  C_Runtime_L_DSP --> T_051_test_isr_entered_matches_rete["test_isr_entered_matches_rete<br/>runtime_irq<br/>0.13s"]
  class T_051_test_isr_entered_matches_rete xfail;
  C_Runtime_L_DSP --> T_052_test_no_pending_irq_gating["test_no_pending_irq_gating<br/>runtime_irq<br/>0.16s"]
  class T_052_test_no_pending_irq_gating xfail;
  C_Runtime_L_Infra["Infra<br/>5/5"]
  class C_Runtime_L_Infra pass;
  C_Runtime --> C_Runtime_L_Infra
  C_Runtime_L_Infra --> T_053_test_all_expected_processes_present["test_all_expected_processes_present<br/>runtime_health<br/>0.12s"]
  class T_053_test_all_expected_processes_present pass;
  C_Runtime_L_Infra --> T_054_test_no_zombie_or_defunct["test_no_zombie_or_defunct<br/>runtime_health<br/>0.05s"]
  class T_054_test_no_zombie_or_defunct pass;
  C_Runtime_L_Infra --> T_055_test_qemu_log_is_fresh["test_qemu_log_is_fresh<br/>runtime_health<br/>3.11s"]
  class T_055_test_qemu_log_is_fresh pass;
  C_Runtime_L_Infra --> T_056_test_mobile_pcap_growing["test_mobile_pcap_growing<br/>runtime_health<br/>10.00s"]
  class T_056_test_mobile_pcap_growing pass;
  C_Runtime_L_Infra --> T_057_test_volumes_mounted["test_volumes_mounted<br/>runtime_health<br/>0.08s"]
  class T_057_test_volumes_mounted pass;
  C_Runtime_L_L1_ctrl["L1-ctrl<br/>2/6"]
  class C_Runtime_L_L1_ctrl partial;
  C_Runtime --> C_Runtime_L_L1_ctrl
  C_Runtime_L_L1_ctrl --> T_058_test_neigh_pm_req_loop_alive["test_neigh_pm_req_loop_alive<br/>runtime_l1ctl<br/>0.19s"]
  class T_058_test_neigh_pm_req_loop_alive pass;
  C_Runtime_L_L1_ctrl --> T_059_test_l1ctl_data_ind_received["test_l1ctl_data_ind_received<br/>runtime_l1ctl<br/>0.17s"]
  class T_059_test_l1ctl_data_ind_received fail;
  C_Runtime_L_L1_ctrl --> T_060_test_a_cd_writes_nonzero["test_a_cd_writes_nonzero<br/>runtime_l1ctl<br/>0.09s"]
  class T_060_test_a_cd_writes_nonzero pass;
  C_Runtime_L_L1_ctrl --> T_061_test_a_cd_write_pc_includes_ccch_demod["test_a_cd_write_pc_includes_ccch_demod<br/>runtime_l1ctl<br/>0.19s"]
  class T_061_test_a_cd_write_pc_includes_ccch_demod xfail;
  C_Runtime_L_L1_ctrl --> T_062_test_l1ctl_data_ind_rate_vs_alc["test_l1ctl_data_ind_rate_vs_alc<br/>runtime_l1ctl<br/>0.24s"]
  class T_062_test_l1ctl_data_ind_rate_vs_alc fail;
  C_Runtime_L_L1_ctrl --> T_063_test_rach_attempted["test_rach_attempted<br/>runtime_l1ctl<br/>0.00s"]
  class T_063_test_rach_attempted xfail;
  C_Runtime_L_L1_data["L1-data<br/>2/3"]
  class C_Runtime_L_L1_data partial;
  C_Runtime --> C_Runtime_L_L1_data
  C_Runtime_L_L1_data --> T_064_test_bridge_log_shows_traffic["test_bridge_log_shows_traffic<br/>runtime_bridge<br/>0.05s"]
  class T_064_test_bridge_log_shows_traffic pass;
  C_Runtime_L_L1_data --> T_065_test_bridge_fn_drift_under_threshold["test_bridge_fn_drift_under_threshold<br/>runtime_bridge<br/>0.00s"]
  class T_065_test_bridge_fn_drift_under_threshold xfail;
  C_Runtime_L_L1_data --> T_066_test_bridge_dl_lookahead_respected["test_bridge_dl_lookahead_respected<br/>runtime_bridge<br/>0.05s"]
  class T_066_test_bridge_dl_lookahead_respected pass;
  C_Runtime_L_Mgmt["Mgmt<br/>3/3"]
  class C_Runtime_L_Mgmt pass;
  C_Runtime --> C_Runtime_L_Mgmt
  C_Runtime_L_Mgmt --> T_067_test_mobile_vty_reachable["test_mobile_vty_reachable<br/>runtime_vty<br/>1.61s"]
  class T_067_test_mobile_vty_reachable pass;
  C_Runtime_L_Mgmt --> T_068_test_mobile_imsi_loaded["test_mobile_imsi_loaded<br/>runtime_vty<br/>1.62s"]
  class T_068_test_mobile_imsi_loaded pass;
  C_Runtime_L_Mgmt --> T_069_test_mobile_mm_state_is_null_or_idle["test_mobile_mm_state_is_null_or_idle<br/>runtime_vty<br/>1.73s"]
  class T_069_test_mobile_mm_state_is_null_or_idle pass;
  C_Runtime_L_Summary["Summary<br/>1/1"]
  class C_Runtime_L_Summary pass;
  C_Runtime --> C_Runtime_L_Summary
  C_Runtime_L_Summary --> T_070_test_run_summary_snapshot["test_run_summary_snapshot<br/>runtime_summary<br/>30.68s"]
  class T_070_test_run_summary_snapshot pass;

2.6 https://github.com/bbaranoff/qosmo/blob/main/tests/detail.qmd.mmd

26127 octets, 393 lignes

flowchart TB
  classDef pass    fill:#bef5b1,stroke:#1f7a1f,color:#0a3d0a;
  classDef partial fill:#fde79c,stroke:#b07a00,color:#3a2c00;
  classDef fail    fill:#fbb4b4,stroke:#a31a1a,color:#5a0000;
  classDef skip    fill:#e0e0e0,stroke:#6a6a6a,color:#333;
  classDef xfail   fill:#fde79c,stroke:#b07a00,color:#3a2c00;
  subgraph SG_C_Ar ["Ar 4/8"]
    direction TB
    C_Ar_L__["— 4/8"]
    C_Ar_L__ --> T_001_pass_grp__["PASS 4 0.0s"]
    C_Ar_L__ --> T_002_test_reset_sp_silicon["test_reset_sp_silicon ar_imr_invariant 0.01s"]
    C_Ar_L__ --> T_003_test_reset_pmst_silicon["test_reset_pmst_silicon ar_imr_invariant 0.01s"]
    C_Ar_L__ --> T_004_test_irq_servicing_imr_nonzero["test_irq_servicing_imr_nonzero ar_imr_invariant 0.01s"]
    C_Ar_L__ --> T_005_test_tpu_frame_irq_delivered["test_tpu_frame_irq_delivered ar_imr_invariant 0.01s"]
  end

  subgraph SG_C_Injection ["Injection 19/21"]
    direction TB
    C_Injection_L_ARM_feedback["ARM-feedback 0/2"]
    C_Injection_L_ARM_feedback --> T_006_test_efficacy_arm_reads_d_fb_det_1["test_efficacy_arm_reads_d_fb_det_1 inject_efficacy 3.01s"]
    C_Injection_L_ARM_feedback --> T_007_test_efficacy_arm_reads_a_cd["test_efficacy_arm_reads_a_cd inject_efficacy 2.59s"]
    C_Injection_L_NDB["NDB 19/19"]
    C_Injection_L_ARM_feedback -.-> C_Injection_L_NDB
    C_Injection_L_NDB --> T_008_pass_grp_NDB["PASS 19 5.4s"]
  end
  SG_C_Ar -.-> SG_C_Injection

  subgraph SG_C_Milestone ["Milestone 3/21"]
    direction TB
    C_Milestone_L_DSP["DSP 3/10"]
    C_Milestone_L_DSP --> T_009_pass_grp_DSP["PASS 3 30.3s"]
    C_Milestone_L_DSP --> T_010_test_dsp_throughput_5x["test_dsp_throughput_5x milestone_dsp_decoder 0.01s"]
    C_Milestone_L_DSP --> T_011_test_fb0_att_nonzero["test_fb0_att_nonzero milestone_fb_det 0.00s"]
    C_Milestone_L_DSP --> T_012_test_synth_zero_path_active["test_synth_zero_path_active milestone_fb_det 0.01s"]
    C_Milestone_L_DSP --> T_013_test_c54x_interrupt_ex_called_nonzero["test_c54x_interrupt_ex_called_nonzero milestone_irq 0.00s"]
    C_Milestone_L_DSP --> T_014_test_isr_entered_implies_rete["test_isr_entered_implies_rete milestone_irq 0.00s"]
    C_Milestone_L_DSP --> T_015_test_no_pending_irq_gated["test_no_pending_irq_gated milestone_irq 0.00s"]
    C_Milestone_L_DSP --> T_016_test_rxdoneflag_no_longer_blocks["test_rxdoneflag_no_longer_blocks milestone_dsp_decoder 0.00s"]
    C_Milestone_L_L1_ctrl["L1-ctrl 0/2"]
    C_Milestone_L_DSP -.-> C_Milestone_L_L1_ctrl
    C_Milestone_L_L1_ctrl --> T_017_test_l1ctl_data_ind_received["test_l1ctl_data_ind_received milestone_l1ctl 0.02s"]
    C_Milestone_L_L1_ctrl --> T_018_test_neigh_pm_req_response_unchanged["test_neigh_pm_req_response_unchanged milestone_l1ctl 0.00s"]
    C_Milestone_L_L1_data["L1-data 0/4"]
    C_Milestone_L_L1_ctrl -.-> C_Milestone_L_L1_data
    C_Milestone_L_L1_data --> T_019_test_ar3_init_source_identified["test_ar3_init_source_identified milestone_bsp_dma 0.01s"]
    C_Milestone_L_L1_data --> T_020_test_ar4_init_source_identified["test_ar4_init_source_identified milestone_bsp_dma 0.01s"]
    C_Milestone_L_L1_data --> T_021_test_bsp_dma_target_matches_correlator_read_zone["test_bsp_dma_target_matches_correlator milestone_bsp_dma 0.01s"]
    C_Milestone_L_L1_data --> T_022_test_no_d_fb_det_wr_site_anomaly["test_no_d_fb_det_wr_site_anomaly milestone_bsp_dma 0.01s"]
    C_Milestone_L_L3_MM["L3-MM 0/5"]
    C_Milestone_L_L1_data -.-> C_Milestone_L_L3_MM
    C_Milestone_L_L3_MM --> T_023_test_rach_emitted["test_rach_emitted milestone_mm_lu 0.00s"]
    C_Milestone_L_L3_MM --> T_024_test_immediate_assignment_decoded["test_immediate_assignment_decoded milestone_mm_lu 0.00s"]
    C_Milestone_L_L3_MM --> T_025_test_rr_sdcch_established["test_rr_sdcch_established milestone_mm_lu 0.00s"]
    C_Milestone_L_L3_MM --> T_026_test_location_updating_request_sent["test_location_updating_request_sent milestone_mm_lu 0.00s"]
    C_Milestone_L_L3_MM --> T_027_test_location_updating_accept_received["test_location_updating_accept_received milestone_mm_lu 0.00s"]
  end
  SG_C_Injection -.-> SG_C_Milestone

  subgraph SG_C_Osmocom ["Osmocom 12/13"]
    direction TB
    C_Osmocom_L__["— 12/13"]
    C_Osmocom_L__ --> T_028_pass_grp__["PASS 12 0.0s"]
    C_Osmocom_L__ --> T_029_test_bridge_clock_from_qemu_defaultX["test_bridge_clock_from_qemu_default osmocom_bridge 0.00s"]
  end
  SG_C_Milestone -.-> SG_C_Osmocom

  subgraph SG_C_Other ["Other 3/18"]
    direction TB
    C_Other_L_unmarked["unmarked 3/18"]
    C_Other_L_unmarked --> T_030_pass_grp_unmarked["PASS 3 0.0s"]
    C_Other_L_unmarked --> T_031_test_verdict_shunt_works["test_verdict_shunt_works  0.00s"]
    C_Other_L_unmarked --> T_032_test_verdict_shunt_silent["test_verdict_shunt_silent  0.00s"]
    C_Other_L_unmarked --> T_033_test_verdict_bts_dead["test_verdict_bts_dead  0.00s"]
    C_Other_L_unmarked --> T_034_test_verdict_ipc_chan_mismatch["test_verdict_ipc_chan_mismatch  0.00s"]
    C_Other_L_unmarked --> T_035_test_bissection_fbsb_shunt_vs_full["test_bissection_fbsb_shunt_vs_full  0.00s"]
    C_Other_L_unmarked --> T_036_test_bridge_vs_ipc_radio_chain["test_bridge_vs_ipc_radio_chain  0.00s"]
    C_Other_L_unmarked --> T_037_test_lost_frames_consistency["test_lost_frames_consistency  0.00s"]
    C_Other_L_unmarked --> T_038_test_errors_quiet["test_errors_quiet  0.00s"]
    C_Other_L_unmarked --> T_039_test_verdict_dsp_wall_confirmed["test_verdict_dsp_wall_confirmed  0.00s"]
    C_Other_L_unmarked --> T_040_test_verdict_arm_bug["test_verdict_arm_bug  0.00s"]
    C_Other_L_unmarked --> T_041_test_verdict_pipeline_works["test_verdict_pipeline_works  0.00s"]
    C_Other_L_unmarked --> T_042_test_verdict_radio_chain_broken["test_verdict_radio_chain_broken  0.00s"]
    C_Other_L_unmarked --> T_043_test_verdict_instability["test_verdict_instability  0.00s"]
    C_Other_L_unmarked --> T_044_test_verdict_bridge_vs_ipc["test_verdict_bridge_vs_ipc  0.00s"]
    C_Other_L_unmarked --> T_045_test_zz_final_report["test_zz_final_report  0.00s"]
  end
  SG_C_Osmocom -.-> SG_C_Other

  subgraph SG_C_Parametrize ["Parametrize 1/34"]
    direction TB
    C_Parametrize_L__["— 1/34"]
    C_Parametrize_L__ --> T_046_pass_grp__["PASS 1 0.1s"]
    C_Parametrize_L__ --> T_047_test_mode_executed_full_["test_mode_executed(full) parametrize 0.00s"]
    C_Parametrize_L__ --> T_048_test_mode_executed_shunt_["test_mode_executed(shunt) parametrize 0.00s"]
    C_Parametrize_L__ --> T_049_test_mode_executed_shunt_ipc_["test_mode_executed(shunt-ipc) parametrize 0.00s"]
    C_Parametrize_L__ --> T_050_test_mode_executed_bridge_["test_mode_executed(bridge) parametrize 0.00s"]
    C_Parametrize_L__ --> T_051_test_mode_executed_bare_["test_mode_executed(bare) parametrize 0.00s"]
    C_Parametrize_L__ --> T_052_test_mode_invariant_full_bts_ok___0_osmo_bts_trx["test_mode_invariant(full-bts_ok->-0-os parametrize 0.00s"]
    C_Parametrize_L__ --> T_053_test_mode_invariant_full_ipc_ok___0_calypso_ipc_["test_mode_invariant(full-ipc_ok->-0-ca parametrize 0.00s"]
    C_Parametrize_L__ --> T_054_test_mode_invariant_full_err_q____0_qemu_log_san["test_mode_invariant(full-err_q-==-0-qe parametrize 0.00s"]
    C_Parametrize_L__ --> T_055_test_mode_invariant_shunt_shunt_latch___0_ARM_do["test_mode_invariant(shunt-shunt_latch- parametrize 0.00s"]
    C_Parametrize_L__ --> T_056_test_mode_invariant_shunt_shunt_disp___0_le_mock["test_mode_invariant(shunt-shunt_disp-> parametrize 0.00s"]
    C_Parametrize_L__ --> T_057_test_mode_invariant_shunt_err_q____0_qemu_log_sa["test_mode_invariant(shunt-err_q-==-0-q parametrize 0.00s"]
    C_Parametrize_L__ --> T_058_test_mode_invariant_shunt_bts_ok____0_BTS_doit__["test_mode_invariant(shunt-bts_ok-==-0- parametrize 0.00s"]
    C_Parametrize_L__ --> T_059_test_mode_invariant_shunt_ipc_ok____0_IPC_doit__["test_mode_invariant(shunt-ipc_ok-==-0- parametrize 0.00s"]
    C_Parametrize_L__ --> T_060_test_mode_invariant_shunt_ipc_shunt_latch___0_AR["test_mode_invariant(shunt-ipc-shunt_la parametrize 0.00s"]
    C_Parametrize_L__ --> T_061_test_mode_invariant_shunt_ipc_shunt_disp___0_le_["test_mode_invariant(shunt-ipc-shunt_di parametrize 0.00s"]
    C_Parametrize_L__ --> T_062_test_mode_invariant_shunt_ipc_bts_ok___0_BTS_ali["test_mode_invariant(shunt-ipc-bts_ok-> parametrize 0.00s"]
    C_Parametrize_L__ --> T_063_test_mode_invariant_shunt_ipc_ipc_ok___0_IPC_han["test_mode_invariant(shunt-ipc-ipc_ok-> parametrize 0.00s"]
    C_Parametrize_L__ --> T_064_test_mode_invariant_bridge_bts_ok___0_BTS_alive_["test_mode_invariant(bridge-bts_ok->-0- parametrize 0.00s"]
    C_Parametrize_L__ --> T_065_test_mode_invariant_bridge_ipc_ok____0_IPC_skipp["test_mode_invariant(bridge-ipc_ok-==-0 parametrize 0.00s"]
    C_Parametrize_L__ --> T_066_test_mode_invariant_bare_bts_ok____0_pas_de_BTS_["test_mode_invariant(bare-bts_ok-==-0-p parametrize 0.00s"]
    C_Parametrize_L__ --> T_067_test_mode_invariant_bare_ipc_ok____0_pas_d_IPC_e["test_mode_invariant(bare-ipc_ok-==-0-p parametrize 0.00s"]
    C_Parametrize_L__ --> T_068_test_mode_invariant_bare_rr_est____0_pas_de_mobi["test_mode_invariant(bare-rr_est-==-0-p parametrize 0.00s"]
    C_Parametrize_L__ --> T_069_test_payload_coverage_FB_detect_["test_payload_coverage(FB_detect) parametrize 0.00s"]
    C_Parametrize_L__ --> T_070_test_payload_coverage_SB_decode_["test_payload_coverage(SB_decode) parametrize 0.00s"]
    C_Parametrize_L__ --> T_071_test_payload_coverage_Mock_LATCH_["test_payload_coverage(Mock_LATCH) parametrize 0.00s"]
    C_Parametrize_L__ --> T_072_test_payload_coverage_Mock_DISP_["test_payload_coverage(Mock_DISP) parametrize 0.00s"]
    C_Parametrize_L__ --> T_073_test_payload_coverage_RR_EST_REQ_["test_payload_coverage(RR_EST_REQ) parametrize 0.00s"]
    C_Parametrize_L__ --> T_074_test_payload_coverage_BTS_alive_["test_payload_coverage(BTS_alive) parametrize 0.00s"]
    C_Parametrize_L__ --> T_075_test_payload_coverage_IPC_handsh_["test_payload_coverage(IPC_handsh) parametrize 0.00s"]
    C_Parametrize_L__ --> T_076_test_timer_runtime_period_kick_["test_timer_runtime_period(kick) parametrize 0.10s"]
    C_Parametrize_L__ --> T_077_test_timer_runtime_period_bridge_clk_ind_["test_timer_runtime_period(bridge_clk_i parametrize 0.00s"]
    C_Parametrize_L__ --> T_078_test_timer_runtime_period_bts_clk_ind_["test_timer_runtime_period(bts_clk_ind) parametrize 0.00s"]
    C_Parametrize_L__ --> T_079_test_timer_runtime_period_bsp_burst_["test_timer_runtime_period(bsp_burst) parametrize 0.10s"]
  end
  SG_C_Other -.-> SG_C_Parametrize

  subgraph SG_C_Runtime ["Runtime 64/96"]
    direction TB
    C_Runtime_L_DSP["DSP 2/10"]
    C_Runtime_L_DSP --> T_080_pass_grp_DSP["PASS 2 0.0s"]
    C_Runtime_L_DSP --> T_081_test_intm_reaches_zero["test_intm_reaches_zero runtime_dsp 0.02s"]
    C_Runtime_L_DSP --> T_082_test_dsp_throughput_above_threshold["test_dsp_throughput_above_threshold runtime_dsp 0.02s"]
    C_Runtime_L_DSP --> T_083_test_d_fb_det_pattern_unchanged["test_d_fb_det_pattern_unchanged runtime_dsp 0.03s"]
    C_Runtime_L_DSP --> T_084_test_bsp_daram_write_distribution["test_bsp_daram_write_distribution runtime_dsp 0.02s"]
    C_Runtime_L_DSP --> T_085_test_d_fb_det_data_no_longer_zero["test_d_fb_det_data_no_longer_zero runtime_dsp 0.01s"]
    C_Runtime_L_DSP --> T_086_test_interrupt_ex_called_counter_exposed["test_interrupt_ex_called_counter_expos runtime_irq 0.00s"]
    C_Runtime_L_DSP --> T_087_test_isr_entered_matches_rete["test_isr_entered_matches_rete runtime_irq 0.00s"]
    C_Runtime_L_DSP --> T_088_test_no_pending_irq_gating["test_no_pending_irq_gating runtime_irq 0.00s"]
    C_Runtime_L_FS["FS 3/3"]
    C_Runtime_L_DSP -.-> C_Runtime_L_FS
    C_Runtime_L_FS --> T_089_pass_grp_FS["PASS 3 0.0s"]
    C_Runtime_L_Firmware_state["Firmware-state 5/5"]
    C_Runtime_L_FS -.-> C_Runtime_L_Firmware_state
    C_Runtime_L_Firmware_state --> T_090_pass_grp_Firmware_state["PASS 5 0.2s"]
    C_Runtime_L_GDB_introspect["GDB-introspect 8/8"]
    C_Runtime_L_Firmware_state -.-> C_Runtime_L_GDB_introspect
    C_Runtime_L_GDB_introspect --> T_091_pass_grp_GDB_introspect["PASS 8 0.3s"]
    C_Runtime_L_Infra["Infra 5/5"]
    C_Runtime_L_GDB_introspect -.-> C_Runtime_L_Infra
    C_Runtime_L_Infra --> T_092_pass_grp_Infra["PASS 5 13.0s"]
    C_Runtime_L_IrDA_channel["IrDA-channel 3/8"]
    C_Runtime_L_Infra -.-> C_Runtime_L_IrDA_channel
    C_Runtime_L_IrDA_channel --> T_093_pass_grp_IrDA_channel["PASS 3 0.1s"]
    C_Runtime_L_IrDA_channel --> T_094_test_irda_boot_marker_present["test_irda_boot_marker_present runtime_irda 0.00s"]
    C_Runtime_L_IrDA_channel --> T_095_test_irda_channel_produces_bytes["test_irda_channel_produces_bytes runtime_irda 5.03s"]
    C_Runtime_L_IrDA_channel --> T_096_test_irda_log_has_timestamp_prefix["test_irda_log_has_timestamp_prefix runtime_irda 0.00s"]
    C_Runtime_L_IrDA_channel --> T_097_test_irda_capture_process_alive["test_irda_capture_process_alive runtime_irda 0.00s"]
    C_Runtime_L_IrDA_channel --> T_098_test_irda_throughput_below_saturation["test_irda_throughput_below_saturation runtime_irda 10.07s"]
    C_Runtime_L_L1_ctrl["L1-ctrl 1/6"]
    C_Runtime_L_IrDA_channel -.-> C_Runtime_L_L1_ctrl
    C_Runtime_L_L1_ctrl --> T_099_pass_grp_L1_ctrl["PASS 1 0.1s"]
    C_Runtime_L_L1_ctrl --> T_100_test_l1ctl_data_ind_received["test_l1ctl_data_ind_received runtime_l1ctl 0.01s"]
    C_Runtime_L_L1_ctrl --> T_101_test_a_cd_writes_nonzero["test_a_cd_writes_nonzero runtime_l1ctl 0.01s"]
    C_Runtime_L_L1_ctrl --> T_102_test_a_cd_write_pc_includes_ccch_demod["test_a_cd_write_pc_includes_ccch_demod runtime_l1ctl 0.02s"]
    C_Runtime_L_L1_ctrl --> T_103_test_l1ctl_data_ind_rate_vs_alc["test_l1ctl_data_ind_rate_vs_alc runtime_l1ctl 0.02s"]
    C_Runtime_L_L1_ctrl --> T_104_test_rach_attempted["test_rach_attempted runtime_l1ctl 0.00s"]
    C_Runtime_L_L1_data["L1-data 1/3"]
    C_Runtime_L_L1_ctrl -.-> C_Runtime_L_L1_data
    C_Runtime_L_L1_data --> T_105_pass_grp_L1_data["PASS 1 0.0s"]
    C_Runtime_L_L1_data --> T_106_test_bridge_fn_drift_under_threshold["test_bridge_fn_drift_under_threshold runtime_bridge 0.00s"]
    C_Runtime_L_L1_data --> T_107_test_bridge_dl_lookahead_respected["test_bridge_dl_lookahead_respected runtime_bridge 0.00s"]
    C_Runtime_L_Log_grep["Log-grep 19/30"]
    C_Runtime_L_L1_data -.-> C_Runtime_L_Log_grep
    C_Runtime_L_Log_grep --> T_108_pass_grp_Log_grep["PASS 19 0.2s"]
    C_Runtime_L_Log_grep --> T_109_test_grep_qemu_task24_dispatched["test_grep_qemu_task24_dispatched runtime_log_grep 0.01s"]
    C_Runtime_L_Log_grep --> T_110_test_grep_bridge_dl_bursts_received["test_grep_bridge_dl_bursts_received runtime_log_grep 0.00s"]
    C_Runtime_L_Log_grep --> T_111_test_grep_bridge_fb_pattern_dominant["test_grep_bridge_fb_pattern_dominant runtime_log_grep 0.00s"]
    C_Runtime_L_Log_grep --> T_112_test_grep_bridge_no_clock_skew_shutdown["test_grep_bridge_no_clock_skew_shutdow runtime_log_grep 0.00s"]
    C_Runtime_L_Log_grep --> T_113_test_grep_fwirda_log_present_or_skip["test_grep_fwirda_log_present_or_skip runtime_log_grep 0.00s"]
    C_Runtime_L_Log_grep --> T_114_test_grep_fwirda_boot_marker["test_grep_fwirda_boot_marker runtime_log_grep 0.00s"]
    C_Runtime_L_Log_grep --> T_115_test_blocker_bridge_no_bts_shutdown["test_blocker_bridge_no_bts_shutdown runtime_log_grep 0.00s"]
    C_Runtime_L_Log_grep --> T_116_test_blocker_bridge_no_rach_parity["test_blocker_bridge_no_rach_parity runtime_log_grep 0.00s"]
    C_Runtime_L_Log_grep --> T_117_test_blocker_bridge_no_socket_error["test_blocker_bridge_no_socket_error runtime_log_grep 0.00s"]
    C_Runtime_L_Log_grep --> T_118_test_blocker_fwirda_no_panic["test_blocker_fwirda_no_panic runtime_log_grep 0.00s"]
    C_Runtime_L_Log_grep --> T_119_test_grep_qemu_a_cd_wr_vs_task24["test_grep_qemu_a_cd_wr_vs_task24 runtime_log_grep 0.02s"]
    C_Runtime_L_Mgmt["Mgmt 3/3"]
    C_Runtime_L_Log_grep -.-> C_Runtime_L_Mgmt
    C_Runtime_L_Mgmt --> T_120_pass_grp_Mgmt["PASS 3 4.5s"]
    C_Runtime_L_Monitor_extended["Monitor-extended 9/9"]
    C_Runtime_L_Mgmt -.-> C_Runtime_L_Monitor_extended
    C_Runtime_L_Monitor_extended --> T_121_pass_grp_Monitor_extended["PASS 9 27.0s"]
    C_Runtime_L_Net["Net 2/2"]
    C_Runtime_L_Monitor_extended -.-> C_Runtime_L_Net
    C_Runtime_L_Net --> T_122_pass_grp_Net["PASS 2 0.0s"]
    C_Runtime_L_Osmocon["Osmocon 2/3"]
    C_Runtime_L_Net -.-> C_Runtime_L_Osmocon
    C_Runtime_L_Osmocon --> T_123_pass_grp_Osmocon["PASS 2 0.1s"]
    C_Runtime_L_Osmocon --> T_124_test_osmocon_no_recent_lost_spam["test_osmocon_no_recent_lost_spam runtime_osmocon 0.03s"]
    C_Runtime_L_Summary["Summary 1/1"]
    C_Runtime_L_Osmocon -.-> C_Runtime_L_Summary
    C_Runtime_L_Summary --> T_125_pass_grp_Summary["PASS 1 30.2s"]
  end
  SG_C_Parametrize -.-> SG_C_Runtime

  subgraph SG_C_Timer ["Timer 2/2"]
    direction TB
    C_Timer_L__["— 2/2"]
    C_Timer_L__ --> T_126_pass_grp__["PASS 2 0.5s"]
  end
  SG_C_Runtime -.-> SG_C_Timer

  subgraph SG_C_Timing ["Timing 11/19"]
    direction TB
    C_Timing_L_Drift["Drift 4/10"]
    C_Timing_L_Drift --> T_127_pass_grp_Drift["PASS 4 0.1s"]
    C_Timing_L_Drift --> T_128_test_qemu_insn_rate_cv_below_0_4["test_qemu_insn_rate_cv_below_0_4 drift 0.10s"]
    C_Timing_L_Drift --> T_129_test_qemu_insn_rate_p1_above_1m["test_qemu_insn_rate_p1_above_1m drift 0.06s"]
    C_Timing_L_Drift --> T_130_test_bridge_qfn_tracks_qemu_fn["test_bridge_qfn_tracks_qemu_fn drift 0.04s"]
    C_Timing_L_Drift --> T_131_test_bridge_qfn_advances_steadily["test_bridge_qfn_advances_steadily drift 0.00s"]
    C_Timing_L_Drift --> T_132_test_log_still_growing_bridge__root_bridge_py_lo["test_log_still_growing(bridge-/root/br drift 0.00s"]
    C_Timing_L_Drift --> T_133_test_no_long_gap_bridge__root_bridge_py_log_10_0["test_no_long_gap(bridge-/root/bridge.p drift 0.00s"]
    C_Timing_L_Timers["Timers 7/9"]
    C_Timing_L_Drift -.-> C_Timing_L_Timers
    C_Timing_L_Timers --> T_134_pass_grp_Timers["PASS 7 0.0s"]
    C_Timing_L_Timers --> T_135_test_frame_irq_per_tdma_ratio["test_frame_irq_per_tdma_ratio timer_invariant 0.05s"]
    C_Timing_L_Timers --> T_136_test_kick_realtime_cadence["test_kick_realtime_cadence timer_invariant 0.04s"]
  end
  SG_C_Timer -.-> SG_C_Timing
  class C_Ar_L__ partial;
  class T_001_pass_grp__ pass;
  class T_002_test_reset_sp_silicon skip;
  class T_003_test_reset_pmst_silicon skip;
  class T_004_test_irq_servicing_imr_nonzero skip;
  class T_005_test_tpu_frame_irq_delivered skip;
  class C_Injection_L_ARM_feedback fail;
  class T_006_test_efficacy_arm_reads_d_fb_det_1 xfail;
  class T_007_test_efficacy_arm_reads_a_cd xfail;
  class C_Injection_L_NDB pass;
  class T_008_pass_grp_NDB pass;
  class C_Milestone_L_DSP partial;
  class T_009_pass_grp_DSP pass;
  class T_010_test_dsp_throughput_5x skip;
  class T_011_test_fb0_att_nonzero xfail;
  class T_012_test_synth_zero_path_active xfail;
  class T_013_test_c54x_interrupt_ex_called_nonzero xfail;
  class T_014_test_isr_entered_implies_rete xfail;
  class T_015_test_no_pending_irq_gated xfail;
  class T_016_test_rxdoneflag_no_longer_blocks xfail;
  class C_Milestone_L_L1_ctrl fail;
  class T_017_test_l1ctl_data_ind_received xfail;
  class T_018_test_neigh_pm_req_response_unchanged xfail;
  class C_Milestone_L_L1_data fail;
  class T_019_test_ar3_init_source_identified skip;
  class T_020_test_ar4_init_source_identified skip;
  class T_021_test_bsp_dma_target_matches_correlator_read_zone skip;
  class T_022_test_no_d_fb_det_wr_site_anomaly xfail;
  class C_Milestone_L_L3_MM fail;
  class T_023_test_rach_emitted xfail;
  class T_024_test_immediate_assignment_decoded xfail;
  class T_025_test_rr_sdcch_established xfail;
  class T_026_test_location_updating_request_sent xfail;
  class T_027_test_location_updating_accept_received xfail;
  class C_Osmocom_L__ partial;
  class T_028_pass_grp__ pass;
  class T_029_test_bridge_clock_from_qemu_defaultX xfail;
  class C_Other_L_unmarked partial;
  class T_030_pass_grp_unmarked pass;
  class T_031_test_verdict_shunt_works skip;
  class T_032_test_verdict_shunt_silent skip;
  class T_033_test_verdict_bts_dead skip;
  class T_034_test_verdict_ipc_chan_mismatch skip;
  class T_035_test_bissection_fbsb_shunt_vs_full skip;
  class T_036_test_bridge_vs_ipc_radio_chain skip;
  class T_037_test_lost_frames_consistency skip;
  class T_038_test_errors_quiet skip;
  class T_039_test_verdict_dsp_wall_confirmed skip;
  class T_040_test_verdict_arm_bug skip;
  class T_041_test_verdict_pipeline_works skip;
  class T_042_test_verdict_radio_chain_broken skip;
  class T_043_test_verdict_instability skip;
  class T_044_test_verdict_bridge_vs_ipc skip;
  class T_045_test_zz_final_report skip;
  class C_Parametrize_L__ partial;
  class T_046_pass_grp__ pass;
  class T_047_test_mode_executed_full_ skip;
  class T_048_test_mode_executed_shunt_ skip;
  class T_049_test_mode_executed_shunt_ipc_ skip;
  class T_050_test_mode_executed_bridge_ skip;
  class T_051_test_mode_executed_bare_ skip;
  class T_052_test_mode_invariant_full_bts_ok___0_osmo_bts_trx skip;
  class T_053_test_mode_invariant_full_ipc_ok___0_calypso_ipc_ skip;
  class T_054_test_mode_invariant_full_err_q____0_qemu_log_san skip;
  class T_055_test_mode_invariant_shunt_shunt_latch___0_ARM_do skip;
  class T_056_test_mode_invariant_shunt_shunt_disp___0_le_mock skip;
  class T_057_test_mode_invariant_shunt_err_q____0_qemu_log_sa skip;
  class T_058_test_mode_invariant_shunt_bts_ok____0_BTS_doit__ skip;
  class T_059_test_mode_invariant_shunt_ipc_ok____0_IPC_doit__ skip;
  class T_060_test_mode_invariant_shunt_ipc_shunt_latch___0_AR skip;
  class T_061_test_mode_invariant_shunt_ipc_shunt_disp___0_le_ skip;
  class T_062_test_mode_invariant_shunt_ipc_bts_ok___0_BTS_ali skip;
  class T_063_test_mode_invariant_shunt_ipc_ipc_ok___0_IPC_han skip;
  class T_064_test_mode_invariant_bridge_bts_ok___0_BTS_alive_ skip;
  class T_065_test_mode_invariant_bridge_ipc_ok____0_IPC_skipp skip;
  class T_066_test_mode_invariant_bare_bts_ok____0_pas_de_BTS_ skip;
  class T_067_test_mode_invariant_bare_ipc_ok____0_pas_d_IPC_e skip;
  class T_068_test_mode_invariant_bare_rr_est____0_pas_de_mobi skip;
  class T_069_test_payload_coverage_FB_detect_ skip;
  class T_070_test_payload_coverage_SB_decode_ skip;
  class T_071_test_payload_coverage_Mock_LATCH_ skip;
  class T_072_test_payload_coverage_Mock_DISP_ skip;
  class T_073_test_payload_coverage_RR_EST_REQ_ skip;
  class T_074_test_payload_coverage_BTS_alive_ skip;
  class T_075_test_payload_coverage_IPC_handsh_ skip;
  class T_076_test_timer_runtime_period_kick_ skip;
  class T_077_test_timer_runtime_period_bridge_clk_ind_ skip;
  class T_078_test_timer_runtime_period_bts_clk_ind_ skip;
  class T_079_test_timer_runtime_period_bsp_burst_ skip;
  class C_Runtime_L_DSP partial;
  class T_080_pass_grp_DSP pass;
  class T_081_test_intm_reaches_zero xfail;
  class T_082_test_dsp_throughput_above_threshold skip;
  class T_083_test_d_fb_det_pattern_unchanged skip;
  class T_084_test_bsp_daram_write_distribution skip;
  class T_085_test_d_fb_det_data_no_longer_zero skip;
  class T_086_test_interrupt_ex_called_counter_exposed xfail;
  class T_087_test_isr_entered_matches_rete xfail;
  class T_088_test_no_pending_irq_gating xfail;
  class C_Runtime_L_FS pass;
  class T_089_pass_grp_FS pass;
  class C_Runtime_L_Firmware_state pass;
  class T_090_pass_grp_Firmware_state pass;
  class C_Runtime_L_GDB_introspect pass;
  class T_091_pass_grp_GDB_introspect pass;
  class C_Runtime_L_Infra pass;
  class T_092_pass_grp_Infra pass;
  class C_Runtime_L_IrDA_channel partial;
  class T_093_pass_grp_IrDA_channel pass;
  class T_094_test_irda_boot_marker_present xfail;
  class T_095_test_irda_channel_produces_bytes xfail;
  class T_096_test_irda_log_has_timestamp_prefix skip;
  class T_097_test_irda_capture_process_alive xfail;
  class T_098_test_irda_throughput_below_saturation skip;
  class C_Runtime_L_L1_ctrl partial;
  class T_099_pass_grp_L1_ctrl pass;
  class T_100_test_l1ctl_data_ind_received xfail;
  class T_101_test_a_cd_writes_nonzero skip;
  class T_102_test_a_cd_write_pc_includes_ccch_demod xfail;
  class T_103_test_l1ctl_data_ind_rate_vs_alc skip;
  class T_104_test_rach_attempted xfail;
  class C_Runtime_L_L1_data partial;
  class T_105_pass_grp_L1_data pass;
  class T_106_test_bridge_fn_drift_under_threshold xfail;
  class T_107_test_bridge_dl_lookahead_respected skip;
  class C_Runtime_L_Log_grep partial;
  class T_108_pass_grp_Log_grep pass;
  class T_109_test_grep_qemu_task24_dispatched xfail;
  class T_110_test_grep_bridge_dl_bursts_received skip;
  class T_111_test_grep_bridge_fb_pattern_dominant skip;
  class T_112_test_grep_bridge_no_clock_skew_shutdown skip;
  class T_113_test_grep_fwirda_log_present_or_skip skip;
  class T_114_test_grep_fwirda_boot_marker skip;
  class T_115_test_blocker_bridge_no_bts_shutdown skip;
  class T_116_test_blocker_bridge_no_rach_parity skip;
  class T_117_test_blocker_bridge_no_socket_error skip;
  class T_118_test_blocker_fwirda_no_panic skip;
  class T_119_test_grep_qemu_a_cd_wr_vs_task24 skip;
  class C_Runtime_L_Mgmt pass;
  class T_120_pass_grp_Mgmt pass;
  class C_Runtime_L_Monitor_extended pass;
  class T_121_pass_grp_Monitor_extended pass;
  class C_Runtime_L_Net pass;
  class T_122_pass_grp_Net pass;
  class C_Runtime_L_Osmocon partial;
  class T_123_pass_grp_Osmocon pass;
  class T_124_test_osmocon_no_recent_lost_spam xfail;
  class C_Runtime_L_Summary pass;
  class T_125_pass_grp_Summary pass;
  class C_Timer_L__ pass;
  class T_126_pass_grp__ pass;
  class C_Timing_L_Drift partial;
  class T_127_pass_grp_Drift pass;
  class T_128_test_qemu_insn_rate_cv_below_0_4 skip;
  class T_129_test_qemu_insn_rate_p1_above_1m skip;
  class T_130_test_bridge_qfn_tracks_qemu_fn skip;
  class T_131_test_bridge_qfn_advances_steadily skip;
  class T_132_test_log_still_growing_bridge__root_bridge_py_lo skip;
  class T_133_test_no_long_gap_bridge__root_bridge_py_log_10_0 skip;
  class C_Timing_L_Timers partial;
  class T_134_pass_grp_Timers pass;
  class T_135_test_frame_irq_per_tdma_ratio skip;
  class T_136_test_kick_realtime_cadence skip;

2.7 https://github.com/bbaranoff/qosmo/blob/main/tests/full.mmd

15096 octets, 226 lignes

%% Generated by tests/conftest.py at 2026-05-15T23:28:30
graph TD
  classDef pass    fill:#bef5b1,stroke:#1f7a1f,color:#0a3d0a;
  classDef partial fill:#fde79c,stroke:#b07a00,color:#3a2c00;
  classDef fail    fill:#fbb4b4,stroke:#a31a1a,color:#5a0000;
  classDef skip    fill:#e0e0e0,stroke:#6a6a6a,color:#333;
  classDef xfail   fill:#fde79c,stroke:#b07a00,color:#3a2c00;
  classDef break_  fill:#ff5252,stroke:#990000,color:#fff,stroke-width:3px;
  ROOT[Test Run]
  subgraph PIPELINE ["Pipeline — où ça casse"]
    direction LR
    P_Infra["Infra<br/>5/5"]
    class P_Infra pass;
    P_DSP["DSP<br/>10/20"]
    class P_DSP partial;
    P_Infra --> P_DSP
    P_L1_data["L1-data<br/>4/7"]
    class P_L1_data partial;
    P_DSP --> P_L1_data
    P_L1_ctrl["L1-ctrl<br/>2/8"]
    class P_L1_ctrl partial;
    P_L1_data --> P_L1_ctrl
    P_NDB["NDB<br/>9/19"]
    class P_NDB partial;
    P_L1_ctrl --> P_NDB
    P_ARM_feedback["ARM-feedback<br/>0/2"]
    class P_ARM_feedback fail;
    P_NDB --> P_ARM_feedback
    P_ARM_feedback -.->|🛑| BREAK_ARM_feedback["BREAK HERE<br/>ARM-feedback 0% pass"]
    class BREAK_ARM_feedback break_;
    P_L3_MM["L3-MM<br/>0/5"]
    class P_L3_MM fail;
    P_ARM_feedback --> P_L3_MM
    P_Mgmt["Mgmt<br/>3/3"]
    class P_Mgmt pass;
    P_L3_MM --> P_Mgmt
    P_Summary["Summary<br/>1/1"]
    class P_Summary pass;
    P_Mgmt --> P_Summary
  end
  ROOT --> PIPELINE
  subgraph DETAIL ["Détail — catégorie / couche / test"]
    C_Injection["Injection<br/>9/21"]
    class C_Injection partial;
    C_Injection_L_ARM_feedback["ARM-feedback<br/>0/2"]
    class C_Injection_L_ARM_feedback fail;
    C_Injection --> C_Injection_L_ARM_feedback
    C_Injection_L_ARM_feedback --> T_001_test_efficacy_arm_reads_d_fb_det_1["test_efficacy_arm_reads_d_fb_det_1<br/>inject_efficacy<br/>3.15s"]
    class T_001_test_efficacy_arm_reads_d_fb_det_1 fail;
    C_Injection_L_ARM_feedback --> T_002_test_efficacy_arm_reads_a_cd["test_efficacy_arm_reads_a_cd<br/>inject_efficacy<br/>3.11s"]
    class T_002_test_efficacy_arm_reads_a_cd xfail;
    C_Injection_L_NDB["NDB<br/>9/19"]
    class C_Injection_L_NDB partial;
    C_Injection --> C_Injection_L_NDB
    C_Injection_L_NDB --> T_003_test_probe_ndb["test_probe_ndb<br/>inject_frames<br/>0.29s"]
    class T_003_test_probe_ndb pass;
    C_Injection_L_NDB --> T_004_test_clear_ndb["test_clear_ndb<br/>inject_frames<br/>0.29s"]
    class T_004_test_clear_ndb pass;
    C_Injection_L_NDB --> T_005_test_inject_fbsb_fb_found["test_inject_fbsb_fb_found<br/>inject_frames<br/>0.62s"]
    class T_005_test_inject_fbsb_fb_found fail;
    C_Injection_L_NDB --> T_006_test_inject_fbsb_sb_found["test_inject_fbsb_sb_found<br/>inject_frames<br/>0.54s"]
    class T_006_test_inject_fbsb_sb_found pass;
    C_Injection_L_NDB --> T_007_test_inject_si_1_["test_inject_si(1)<br/>inject_frames<br/>0.33s"]
    class T_007_test_inject_si_1_ fail;
    C_Injection_L_NDB --> T_008_test_inject_si_2_["test_inject_si(2)<br/>inject_frames<br/>0.33s"]
    class T_008_test_inject_si_2_ fail;
    C_Injection_L_NDB --> T_009_test_inject_si_3_["test_inject_si(3)<br/>inject_frames<br/>0.33s"]
    class T_009_test_inject_si_3_ fail;
    C_Injection_L_NDB --> T_010_test_inject_si_4_["test_inject_si(4)<br/>inject_frames<br/>0.33s"]
    class T_010_test_inject_si_4_ fail;
    C_Injection_L_NDB --> T_011_test_inject_si_5_["test_inject_si(5)<br/>inject_frames<br/>0.37s"]
    class T_011_test_inject_si_5_ fail;
    C_Injection_L_NDB --> T_012_test_inject_si_6_["test_inject_si(6)<br/>inject_frames<br/>0.33s"]
    class T_012_test_inject_si_6_ fail;
    C_Injection_L_NDB --> T_013_test_inject_a_cd_pattern_23B["test_inject_a_cd_pattern_23B<br/>inject_frames<br/>0.33s"]
    class T_013_test_inject_a_cd_pattern_23B fail;
    C_Injection_L_NDB --> T_014_test_inject_a_cd_pattern_30B["test_inject_a_cd_pattern_30B<br/>inject_frames<br/>0.33s"]
    class T_014_test_inject_a_cd_pattern_30B fail;
    C_Injection_L_NDB --> T_015_test_inject_a_cd_invalid_length["test_inject_a_cd_invalid_length<br/>inject_frames<br/>0.00s"]
    class T_015_test_inject_a_cd_invalid_length pass;
    C_Injection_L_NDB --> T_016_test_inject_d_task_5_0_["test_inject_d_task(5-0)<br/>inject_frames<br/>0.04s"]
    class T_016_test_inject_d_task_5_0_ pass;
    C_Injection_L_NDB --> T_017_test_inject_d_task_6_1_["test_inject_d_task(6-1)<br/>inject_frames<br/>0.04s"]
    class T_017_test_inject_d_task_6_1_ pass;
    C_Injection_L_NDB --> T_018_test_inject_d_task_24_0_["test_inject_d_task(24-0)<br/>inject_frames<br/>0.04s"]
    class T_018_test_inject_d_task_24_0_ pass;
    C_Injection_L_NDB --> T_019_test_inject_d_task_28_1_["test_inject_d_task(28-1)<br/>inject_frames<br/>0.04s"]
    class T_019_test_inject_d_task_28_1_ fail;
    C_Injection_L_NDB --> T_020_test_inject_d_burst_0_["test_inject_d_burst(0)<br/>inject_frames<br/>0.04s"]
    class T_020_test_inject_d_burst_0_ pass;
    C_Injection_L_NDB --> T_021_test_inject_d_burst_1_["test_inject_d_burst(1)<br/>inject_frames<br/>0.04s"]
    class T_021_test_inject_d_burst_1_ pass;
    C_Milestone["Milestone<br/>6/21"]
    class C_Milestone partial;
    C_Milestone_L_DSP["DSP<br/>4/10"]
    class C_Milestone_L_DSP partial;
    C_Milestone --> C_Milestone_L_DSP
    C_Milestone_L_DSP --> T_022_test_popm_decoder_active["test_popm_decoder_active<br/>milestone_dsp_decoder<br/>0.00s"]
    class T_022_test_popm_decoder_active pass;
    C_Milestone_L_DSP --> T_023_test_tier_a_decoder_fixes_present["test_tier_a_decoder_fixes_present<br/>milestone_dsp_decoder<br/>0.00s"]
    class T_023_test_tier_a_decoder_fixes_present pass;
    C_Milestone_L_DSP --> T_024_test_intm_dwell_no_regression["test_intm_dwell_no_regression<br/>milestone_dsp_decoder<br/>30.57s"]
    class T_024_test_intm_dwell_no_regression pass;
    C_Milestone_L_DSP --> T_025_test_dsp_throughput_5x["test_dsp_throughput_5x<br/>milestone_dsp_decoder<br/>0.13s"]
    class T_025_test_dsp_throughput_5x pass;
    C_Milestone_L_DSP --> T_026_test_fb0_att_nonzero["test_fb0_att_nonzero<br/>milestone_fb_det<br/>0.00s"]
    class T_026_test_fb0_att_nonzero xfail;
    C_Milestone_L_DSP --> T_027_test_synth_zero_path_active["test_synth_zero_path_active<br/>milestone_fb_det<br/>0.11s"]
    class T_027_test_synth_zero_path_active skip;
    C_Milestone_L_DSP --> T_028_test_c54x_interrupt_ex_called_nonzero["test_c54x_interrupt_ex_called_nonzero<br/>milestone_irq<br/>0.00s"]
    class T_028_test_c54x_interrupt_ex_called_nonzero xfail;
    C_Milestone_L_DSP --> T_029_test_isr_entered_implies_rete["test_isr_entered_implies_rete<br/>milestone_irq<br/>0.00s"]
    class T_029_test_isr_entered_implies_rete xfail;
    C_Milestone_L_DSP --> T_030_test_no_pending_irq_gated["test_no_pending_irq_gated<br/>milestone_irq<br/>0.00s"]
    class T_030_test_no_pending_irq_gated xfail;
    C_Milestone_L_DSP --> T_031_test_rxdoneflag_no_longer_blocks["test_rxdoneflag_no_longer_blocks<br/>milestone_dsp_decoder<br/>0.00s"]
    class T_031_test_rxdoneflag_no_longer_blocks xfail;
    C_Milestone_L_L1_ctrl["L1-ctrl<br/>0/2"]
    class C_Milestone_L_L1_ctrl fail;
    C_Milestone --> C_Milestone_L_L1_ctrl
    C_Milestone_L_L1_ctrl --> T_032_test_l1ctl_data_ind_received["test_l1ctl_data_ind_received<br/>milestone_l1ctl<br/>0.16s"]
    class T_032_test_l1ctl_data_ind_received fail;
    C_Milestone_L_L1_ctrl --> T_033_test_neigh_pm_req_response_unchanged["test_neigh_pm_req_response_unchanged<br/>milestone_l1ctl<br/>0.00s"]
    class T_033_test_neigh_pm_req_response_unchanged xfail;
    C_Milestone_L_L1_data["L1-data<br/>2/4"]
    class C_Milestone_L_L1_data partial;
    C_Milestone --> C_Milestone_L_L1_data
    C_Milestone_L_L1_data --> T_034_test_ar3_init_source_identified["test_ar3_init_source_identified<br/>milestone_bsp_dma<br/>0.01s"]
    class T_034_test_ar3_init_source_identified skip;
    C_Milestone_L_L1_data --> T_035_test_ar4_init_source_identified["test_ar4_init_source_identified<br/>milestone_bsp_dma<br/>0.01s"]
    class T_035_test_ar4_init_source_identified skip;
    C_Milestone_L_L1_data --> T_036_test_bsp_dma_target_matches_correlator_read_zone["test_bsp_dma_target_matches_correlator<br/>milestone_bsp_dma<br/>0.06s"]
    class T_036_test_bsp_dma_target_matches_correlator_read_zone pass;
    C_Milestone_L_L1_data --> T_037_test_no_d_fb_det_wr_site_anomaly["test_no_d_fb_det_wr_site_anomaly<br/>milestone_bsp_dma<br/>0.17s"]
    class T_037_test_no_d_fb_det_wr_site_anomaly pass;
    C_Milestone_L_L3_MM["L3-MM<br/>0/5"]
    class C_Milestone_L_L3_MM fail;
    C_Milestone --> C_Milestone_L_L3_MM
    C_Milestone_L_L3_MM --> T_038_test_rach_emitted["test_rach_emitted<br/>milestone_mm_lu<br/>0.00s"]
    class T_038_test_rach_emitted xfail;
    C_Milestone_L_L3_MM --> T_039_test_immediate_assignment_decoded["test_immediate_assignment_decoded<br/>milestone_mm_lu<br/>0.00s"]
    class T_039_test_immediate_assignment_decoded xfail;
    C_Milestone_L_L3_MM --> T_040_test_rr_sdcch_established["test_rr_sdcch_established<br/>milestone_mm_lu<br/>0.00s"]
    class T_040_test_rr_sdcch_established xfail;
    C_Milestone_L_L3_MM --> T_041_test_location_updating_request_sent["test_location_updating_request_sent<br/>milestone_mm_lu<br/>0.00s"]
    class T_041_test_location_updating_request_sent xfail;
    C_Milestone_L_L3_MM --> T_042_test_location_updating_accept_received["test_location_updating_accept_received<br/>milestone_mm_lu<br/>0.00s"]
    class T_042_test_location_updating_accept_received xfail;
    C_Runtime["Runtime<br/>19/28"]
    class C_Runtime partial;
    C_Runtime_L_DSP["DSP<br/>6/10"]
    class C_Runtime_L_DSP partial;
    C_Runtime --> C_Runtime_L_DSP
    C_Runtime_L_DSP --> T_043_test_no_wait_a21a_on_window["test_no_wait_a21a_on_window<br/>runtime_dsp<br/>0.07s"]
    class T_043_test_no_wait_a21a_on_window pass;
    C_Runtime_L_DSP --> T_044_test_no_enter_7740_dwell["test_no_enter_7740_dwell<br/>runtime_dsp<br/>0.08s"]
    class T_044_test_no_enter_7740_dwell pass;
    C_Runtime_L_DSP --> T_045_test_intm_reaches_zero["test_intm_reaches_zero<br/>runtime_dsp<br/>0.07s"]
    class T_045_test_intm_reaches_zero pass;
    C_Runtime_L_DSP --> T_046_test_dsp_throughput_above_threshold["test_dsp_throughput_above_threshold<br/>runtime_dsp<br/>0.07s"]
    class T_046_test_dsp_throughput_above_threshold pass;
    C_Runtime_L_DSP --> T_047_test_d_fb_det_pattern_unchanged["test_d_fb_det_pattern_unchanged<br/>runtime_dsp<br/>0.06s"]
    class T_047_test_d_fb_det_pattern_unchanged xfail;
    C_Runtime_L_DSP --> T_048_test_bsp_daram_write_distribution["test_bsp_daram_write_distribution<br/>runtime_dsp<br/>0.08s"]
    class T_048_test_bsp_daram_write_distribution pass;
    C_Runtime_L_DSP --> T_049_test_d_fb_det_data_no_longer_zero["test_d_fb_det_data_no_longer_zero<br/>runtime_dsp<br/>0.08s"]
    class T_049_test_d_fb_det_data_no_longer_zero pass;
    C_Runtime_L_DSP --> T_050_test_interrupt_ex_called_counter_exposed["test_interrupt_ex_called_counter_expos<br/>runtime_irq<br/>0.06s"]
    class T_050_test_interrupt_ex_called_counter_exposed xfail;
    C_Runtime_L_DSP --> T_051_test_isr_entered_matches_rete["test_isr_entered_matches_rete<br/>runtime_irq<br/>0.13s"]
    class T_051_test_isr_entered_matches_rete xfail;
    C_Runtime_L_DSP --> T_052_test_no_pending_irq_gating["test_no_pending_irq_gating<br/>runtime_irq<br/>0.16s"]
    class T_052_test_no_pending_irq_gating xfail;
    C_Runtime_L_Infra["Infra<br/>5/5"]
    class C_Runtime_L_Infra pass;
    C_Runtime --> C_Runtime_L_Infra
    C_Runtime_L_Infra --> T_053_test_all_expected_processes_present["test_all_expected_processes_present<br/>runtime_health<br/>0.12s"]
    class T_053_test_all_expected_processes_present pass;
    C_Runtime_L_Infra --> T_054_test_no_zombie_or_defunct["test_no_zombie_or_defunct<br/>runtime_health<br/>0.05s"]
    class T_054_test_no_zombie_or_defunct pass;
    C_Runtime_L_Infra --> T_055_test_qemu_log_is_fresh["test_qemu_log_is_fresh<br/>runtime_health<br/>3.11s"]
    class T_055_test_qemu_log_is_fresh pass;
    C_Runtime_L_Infra --> T_056_test_mobile_pcap_growing["test_mobile_pcap_growing<br/>runtime_health<br/>10.00s"]
    class T_056_test_mobile_pcap_growing pass;
    C_Runtime_L_Infra --> T_057_test_volumes_mounted["test_volumes_mounted<br/>runtime_health<br/>0.08s"]
    class T_057_test_volumes_mounted pass;
    C_Runtime_L_L1_ctrl["L1-ctrl<br/>2/6"]
    class C_Runtime_L_L1_ctrl partial;
    C_Runtime --> C_Runtime_L_L1_ctrl
    C_Runtime_L_L1_ctrl --> T_058_test_neigh_pm_req_loop_alive["test_neigh_pm_req_loop_alive<br/>runtime_l1ctl<br/>0.19s"]
    class T_058_test_neigh_pm_req_loop_alive pass;
    C_Runtime_L_L1_ctrl --> T_059_test_l1ctl_data_ind_received["test_l1ctl_data_ind_received<br/>runtime_l1ctl<br/>0.17s"]
    class T_059_test_l1ctl_data_ind_received fail;
    C_Runtime_L_L1_ctrl --> T_060_test_a_cd_writes_nonzero["test_a_cd_writes_nonzero<br/>runtime_l1ctl<br/>0.09s"]
    class T_060_test_a_cd_writes_nonzero pass;
    C_Runtime_L_L1_ctrl --> T_061_test_a_cd_write_pc_includes_ccch_demod["test_a_cd_write_pc_includes_ccch_demod<br/>runtime_l1ctl<br/>0.19s"]
    class T_061_test_a_cd_write_pc_includes_ccch_demod xfail;
    C_Runtime_L_L1_ctrl --> T_062_test_l1ctl_data_ind_rate_vs_alc["test_l1ctl_data_ind_rate_vs_alc<br/>runtime_l1ctl<br/>0.24s"]
    class T_062_test_l1ctl_data_ind_rate_vs_alc fail;
    C_Runtime_L_L1_ctrl --> T_063_test_rach_attempted["test_rach_attempted<br/>runtime_l1ctl<br/>0.00s"]
    class T_063_test_rach_attempted xfail;
    C_Runtime_L_L1_data["L1-data<br/>2/3"]
    class C_Runtime_L_L1_data partial;
    C_Runtime --> C_Runtime_L_L1_data
    C_Runtime_L_L1_data --> T_064_test_bridge_log_shows_traffic["test_bridge_log_shows_traffic<br/>runtime_bridge<br/>0.05s"]
    class T_064_test_bridge_log_shows_traffic pass;
    C_Runtime_L_L1_data --> T_065_test_bridge_fn_drift_under_threshold["test_bridge_fn_drift_under_threshold<br/>runtime_bridge<br/>0.00s"]
    class T_065_test_bridge_fn_drift_under_threshold xfail;
    C_Runtime_L_L1_data --> T_066_test_bridge_dl_lookahead_respected["test_bridge_dl_lookahead_respected<br/>runtime_bridge<br/>0.05s"]
    class T_066_test_bridge_dl_lookahead_respected pass;
    C_Runtime_L_Mgmt["Mgmt<br/>3/3"]
    class C_Runtime_L_Mgmt pass;
    C_Runtime --> C_Runtime_L_Mgmt
    C_Runtime_L_Mgmt --> T_067_test_mobile_vty_reachable["test_mobile_vty_reachable<br/>runtime_vty<br/>1.61s"]
    class T_067_test_mobile_vty_reachable pass;
    C_Runtime_L_Mgmt --> T_068_test_mobile_imsi_loaded["test_mobile_imsi_loaded<br/>runtime_vty<br/>1.62s"]
    class T_068_test_mobile_imsi_loaded pass;
    C_Runtime_L_Mgmt --> T_069_test_mobile_mm_state_is_null_or_idle["test_mobile_mm_state_is_null_or_idle<br/>runtime_vty<br/>1.73s"]
    class T_069_test_mobile_mm_state_is_null_or_idle pass;
    C_Runtime_L_Summary["Summary<br/>1/1"]
    class C_Runtime_L_Summary pass;
    C_Runtime --> C_Runtime_L_Summary
    C_Runtime_L_Summary --> T_070_test_run_summary_snapshot["test_run_summary_snapshot<br/>runtime_summary<br/>30.68s"]
    class T_070_test_run_summary_snapshot pass;
  end
  ROOT --> DETAIL

2.8 https://github.com/bbaranoff/qosmo/blob/main/tests/pipeline.mmd

1120 octets, 34 lignes

graph LR
  classDef pass    fill:#bef5b1,stroke:#1f7a1f,color:#0a3d0a;
  classDef partial fill:#fde79c,stroke:#b07a00,color:#3a2c00;
  classDef fail    fill:#fbb4b4,stroke:#a31a1a,color:#5a0000;
  classDef skip    fill:#e0e0e0,stroke:#6a6a6a,color:#333;
  classDef break_  fill:#ff5252,stroke:#990000,color:#fff,stroke-width:3px;
  P_Infra["Infra<br/>5/5"]
  class P_Infra pass;
  P_DSP["DSP<br/>10/20"]
  class P_DSP partial;
  P_Infra --> P_DSP
  P_L1_data["L1-data<br/>4/7"]
  class P_L1_data partial;
  P_DSP --> P_L1_data
  P_L1_ctrl["L1-ctrl<br/>2/8"]
  class P_L1_ctrl partial;
  P_L1_data --> P_L1_ctrl
  P_NDB["NDB<br/>9/19"]
  class P_NDB partial;
  P_L1_ctrl --> P_NDB
  P_ARM_feedback["ARM-feedback<br/>0/2"]
  class P_ARM_feedback fail;
  P_NDB --> P_ARM_feedback
  P_ARM_feedback -.->|🛑| BREAK_ARM_feedback["BREAK HERE<br/>ARM-feedback 0% pass"]
  class BREAK_ARM_feedback break_;
  P_L3_MM["L3-MM<br/>0/5"]
  class P_L3_MM fail;
  P_ARM_feedback --> P_L3_MM
  P_Mgmt["Mgmt<br/>3/3"]
  class P_Mgmt pass;
  P_L3_MM --> P_Mgmt
  P_Summary["Summary<br/>1/1"]
  class P_Summary pass;
  P_Mgmt --> P_Summary

2.9 https://github.com/bbaranoff/qosmo/blob/main/tests/pipeline.qmd.mmd

1945 octets, 64 lignes

graph LR
  classDef pass    fill:#bef5b1,stroke:#1f7a1f,color:#0a3d0a;
  classDef partial fill:#fde79c,stroke:#b07a00,color:#3a2c00;
  classDef fail    fill:#fbb4b4,stroke:#a31a1a,color:#5a0000;
  classDef skip    fill:#e0e0e0,stroke:#6a6a6a,color:#333;
  classDef brk  fill:#ff5252,stroke:#990000,color:#fff,stroke-width:3px;
  P_Infra["Infra 5/5"]
  class P_Infra pass;
  P_GDB_introspect["GDB-introspect 8/8"]
  class P_GDB_introspect pass;
  P_Infra --> P_GDB_introspect
  P_Monitor_extended["Monitor-extended 9/9"]
  class P_Monitor_extended pass;
  P_GDB_introspect --> P_Monitor_extended
  P_Firmware_state["Firmware-state 5/5"]
  class P_Firmware_state pass;
  P_Monitor_extended --> P_Firmware_state
  P_Osmocon["Osmocon 2/3"]
  class P_Osmocon partial;
  P_Firmware_state --> P_Osmocon
  P_DSP["DSP 5/20"]
  class P_DSP partial;
  P_Osmocon --> P_DSP
  P_L1_data["L1-data 1/7"]
  class P_L1_data partial;
  P_DSP --> P_L1_data
  P_L1_ctrl["L1-ctrl 1/8"]
  class P_L1_ctrl partial;
  P_L1_data --> P_L1_ctrl
  P_Timers["Timers 7/9"]
  class P_Timers partial;
  P_L1_ctrl --> P_Timers
  P_Drift["Drift 4/10"]
  class P_Drift partial;
  P_Timers --> P_Drift
  P_NDB["NDB 19/19"]
  class P_NDB pass;
  P_Drift --> P_NDB
  P_ARM_feedback["ARM-feedback 0/2"]
  class P_ARM_feedback fail;
  P_NDB --> P_ARM_feedback
  P_ARM_feedback -.->|BREAK| BREAK_ARM_feedback["BREAK HERE ARM-feedback 0% pass"]
  class BREAK_ARM_feedback brk;
  P_IrDA_channel["IrDA-channel 3/8"]
  class P_IrDA_channel partial;
  P_ARM_feedback --> P_IrDA_channel
  P_L3_MM["L3-MM 0/5"]
  class P_L3_MM fail;
  P_IrDA_channel --> P_L3_MM
  P_Mgmt["Mgmt 3/3"]
  class P_Mgmt pass;
  P_L3_MM --> P_Mgmt
  P_Net["Net 2/2"]
  class P_Net pass;
  P_Mgmt --> P_Net
  P_FS["FS 3/3"]
  class P_FS pass;
  P_Net --> P_FS
  P_Log_grep["Log-grep 19/30"]
  class P_Log_grep partial;
  P_FS --> P_Log_grep
  P_Summary["Summary 1/1"]
  class P_Summary pass;
  P_Log_grep --> P_Summary

2.10 https://github.com/bbaranoff/qosmo/blob/main/tmux_modules/README.md

1209 octets, 31 lignes

2.10.1 tmux_modules - tmux layout of the bench

run_modules/35-tmux.sh sources _commun.sh, then bench.sh, and calls tmux_layout. The session (TMUX_SESSION, default calypso) has six windows: radio, core, ms1, ms2 (side-car), voice, shell.

_commun.sh    colours, _w / _split / _tail helpers, core/voice/shell windows, styling
bench.sh      the bench layout (TMUX_FENETRE_PREMIERE, tmux_layout_premiere, tmux_layout)
2.10.1.1 Writing a layout

A layout file defines:

Name Purpose
TMUX_FENETRE_PREMIERE name of the window created with the session
tmux_layout_premiere() command run in that window
tmux_layout() every other window, built with _w / _split
2.10.1.2 Rules
  • Colour carries the role, not the position (see the header of _commun.sh).
  • Core panes follow /var/log/osmocom/osmo-*.log, the service logs, not the module start-up traces in $LOG_DIR/mod/.
  • tail -F, never -f: the log may not exist yet, and 40-qemu truncates qemu.log beyond its size cap.
  • || sleep infinity as fallback, so a missing log does not kill the pane.
  • A layout starts no service: tmux is only for watching; the bench runs without it.

3 Fichiers

3.1 https://github.com/bbaranoff/qosmo/blob/main/install.sh

28907 octets, 544 lignes → 544 lignes

#!/bin/bash
# =============================================================================
#  install.sh - builds and installs qosmo (QEMU, Calypso machine)
# =============================================================================
#
#  Standalone: this repository and a few apt packages are enough, osmo-operator
#  is not required. This is also the script called by the osmo-operator
#  Dockerfile (qemu stage), its Dockerfile.run, start-docker.sh and the native
#  install (install_modules/45-calypso.sh): a single list of commands.
#
#      ./install.sh                 everything, in order (see --list)
#      ./install.sh --check         prerequisites and state, CHANGES NOTHING
#      ./install.sh --list          the steps and the values in effect
#      ./install.sh --print-deps    the apt packages, on one line
#      sudo ./install.sh --deps     install the missing apt packages, nothing else
#      sudo ./install.sh --with-deps   the missing packages, then everything
#      ./install.sh --only build    one or more steps (comma-separated list)
#      ./install.sh --skip bin      all steps except those
#      ./install.sh --reinstall     replay even what is already done (reconfigure)
#      ./install.sh -v              command output on screen (otherwise: log file)
#
#  Steps: venv configure build install bin verify
#
#  Options, and the variable that does the same (the option wins):
#      --prefix DIR     PREFIX     $GSM_ROOT/qemu-install  where `make install` puts QEMU
#      --build-dir DIR  BUILD_DIR  $QOSMO/build            build out of tree
#      --venv DIR       VENV       $HOME/.venv-qemu        venv for configure (tomli); none = system python
#      --bin-dir DIR    BIN_DIR    /usr/local/bin          copy of qemu-system-arm and qosmo; none = no copy
#      --destdir DIR    DESTDIR    (empty)                 install under another root (ISO rootfs)
#      --cc NAME        QOSMO_CC   (empty)                 forced compiler, e.g. gcc-13 (build/ configured with it)
#      -j N             JOBS       nproc
#                       GSM_ROOT   /opt/GSM     QOSMO = the tree to build, this repository by default
#                       LOG_DIR    /tmp/qosmo-install (one log per step)
#
#  The launchers read $QOSMO/build/qemu-system-arm (c54x_exe/run.sh,
#  environnement/bench.env) and c54x_exe / grgsm_exe compile the sources of this
#  tree: keep the tree AND build/.
#
#  Each step cites the osmo-operator/Dockerfile lines it mirrors (qemu stage,
#  691-705; packages 156-205).
# -----------------------------------------------------------------------------
set -uo pipefail
ICI="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ═════════════════════════════════════════════════════════════════════════════
#  THE CONTRACT - the one of osmo-operator/install_modules/_lib/inst.sh
# ═════════════════════════════════════════════════════════════════════════════
#  IDENTICAL copy in qosmo/install.sh, c54x_exe/install.sh and
#  grgsm_exe/install.sh: each repository must install on its own, without
#  osmo-operator. A fix here is carried over to the other two.
#
#  Four functions per step, only `run` is mandatory:
#     inst_<step>_check     prerequisites, READ ONLY        0 ok · 1 missing · 4 not applicable
#     inst_<step>_done      already done?                   0 yes · 1 no
#     inst_<step>_run       does the work                   0 ok · 1 failure · 3 already done
#     inst_<step>_verify    check AFTERWARDS                0 ok · 1 failure
# -----------------------------------------------------------------------------
readonly INST_RC_OK=0 INST_RC_FAIL=1 INST_RC_DONE=3 INST_RC_NA=4
declare -a INST_ORDER=()
declare -A INST_DESC=()
INST_REGISTER() { INST_ORDER+=("$1"); INST_DESC[$1]="$2"; }

_INST_REASON=""
_INST_HINT=""
inst_ok()   { _INST_REASON=""   ; return $INST_RC_OK; }
inst_fail() { _INST_REASON="$*" ; return $INST_RC_FAIL; }
inst_done() { _INST_REASON="$*" ; return $INST_RC_DONE; }
inst_na()   { _INST_REASON="$*" ; return $INST_RC_NA; }
inst_hint() { _INST_HINT="$*"; }
inst_say()  { printf '%s\n' "$*"; }

have_cmd()  { command -v "$1" >/dev/null 2>&1; }
have_file() { [ -f "$1" ]; }
have_pkg()  { dpkg -s "$1" >/dev/null 2>&1; }
# A path is writable if it is, or if its closest existing parent is (it can
# then be created).
ecrivable() { local d="$1"; while [ ! -e "$d" ] && [ "$d" != / ]; do d="$(dirname "$d")"; done; [ -w "$d" ]; }
# Absolute path without requiring it to exist (configure, make -C and the --dest
# options change directory: a relative path would mean nothing there).
absolu() { case "$1" in /*) printf '%s\n' "$1" ;; *) printf '%s\n' "$PWD/$1" ;; esac; }

# exige "what is needed" "advice if missing" command...: one prerequisite
# condition. With --check it is printed (ok / MANQUE + advice); otherwise it is
# recorded, and the first missing one gives the failure reason.
_MANQUE=()
exige() {
    local quoi="$1" conseil="$2"; shift 2
    if "$@" >/dev/null 2>&1; then
        [ "$ACTION" = check ] && printf '      ok      %s\n' "$quoi"
        return 0
    fi
    if [ "$ACTION" = check ]; then
        printf '      %sMANQUE%s  %s\n' "$C_KO" "$C_Z" "$quoi"
        [ -n "$conseil" ] && printf '              → %s\n' "$conseil"
    fi
    [ ${#_MANQUE[@]} -eq 0 ] && [ -n "$conseil" ] && inst_hint "$conseil"
    _MANQUE+=("$quoi")
    return 1
}
# Verdict of a check function that called exige.
bilan_exige() {
    [ ${#_MANQUE[@]} -eq 0 ] && { inst_ok; return $INST_RC_OK; }
    local plus=""; [ ${#_MANQUE[@]} -gt 1 ] && plus=" (+$(( ${#_MANQUE[@]} - 1 )) autre(s), --check)"
    inst_fail "manque : ${_MANQUE[0]}$plus"
}
# ═════════════════════════════════════════════════════════════════════════════
#  THE COMPONENT - qosmo
# ═════════════════════════════════════════════════════════════════════════════
COMPOSANT=qosmo
: "${GSM_ROOT:=/opt/GSM}"
: "${QOSMO:=$ICI}"
: "${PREFIX:=$GSM_ROOT/qemu-install}"
: "${BUILD_DIR:=}"
: "${VENV:=${HOME:-/root}/.venv-qemu}"
: "${BIN_DIR:=/usr/local/bin}"
: "${DESTDIR:=}"
: "${QOSMO_CC:=}"
: "${JOBS:=$(nproc 2>/dev/null || echo 2)}"
: "${LOG_DIR:=/tmp/qosmo-install}"

# apt packages, taken from the single list in osmo-operator/Dockerfile:
#   :159      build-essential git pkg-config        build tools
#   :166      python3
#   :189      python3-venv python3-pip              the configure venv (tomli)
#   :190      libglib2.0-dev libpixman-1-dev libslirp-dev ninja-build
#                                                   the arm-softmmu target, meson
#   :190      socat                                 run.sh / 40-qemu.sh talk to the QEMU monitor
PAQUETS=(build-essential git pkg-config python3 python3-venv python3-pip
         libglib2.0-dev libpixman-1-dev libslirp-dev ninja-build socat)

options_composant() {
    case "$1" in
        --prefix)    _val "$@"; PREFIX="$2";    _n=2 ;;
        --build-dir) _val "$@"; BUILD_DIR="$2"; _n=2 ;;
        --venv)      _val "$@"; VENV="$2";      _n=2 ;;
        --bin-dir)   _val "$@"; BIN_DIR="$2";   _n=2 ;;
        --destdir)   _val "$@"; DESTDIR="$2";   _n=2 ;;
        --cc)        _val "$@"; QOSMO_CC="$2";  _n=2 ;;
        -j)          _val "$@"; JOBS="$2";      _n=2 ;;
        -j[0-9]*)    JOBS="${1#-j}";            _n=1 ;;
    esac
}

apres_options() {
    QOSMO="$(absolu "$QOSMO")"
    [ -n "$BUILD_DIR" ] || BUILD_DIR="$QOSMO/build"
    BUILD_DIR="$(absolu "$BUILD_DIR")"; PREFIX="$(absolu "$PREFIX")"
    [ "$VENV" = none ]    || VENV="$(absolu "$VENV")"
    [ "$BIN_DIR" = none ] || BIN_DIR="$(absolu "$BIN_DIR")"
    [ -z "$DESTDIR" ]     || DESTDIR="$(absolu "$DESTDIR")"
    # Dockerfile:698-699 - the configure options, identical.
    CONFIGURE_ARGS=(--target-list=arm-softmmu --enable-l1-grgsm "--prefix=$PREFIX"
                    --disable-werror --disable-docs)
}

resume_composant() {
    printf 'QOSMO=%s\nBUILD_DIR=%s\nPREFIX=%s%s\nVENV=%s\nBIN_DIR=%s\nJOBS=%s%s\n' \
        "$QOSMO" "$BUILD_DIR" "$PREFIX" "${DESTDIR:+   (sous DESTDIR=$DESTDIR)}" "$VENV" "$BIN_DIR" \
        "$JOBS" "${QOSMO_CC:+   QOSMO_CC=$QOSMO_CC}"
    printf 'configure %s\n' "${CONFIGURE_ARGS[*]}"
}

# --cc: the local PATH of Dockerfile.run:183-185. build/ keeps the compiler
# used by configure (meson records "cc"); if that cc is not the same any more,
# ninja fails on the first unit. So a directory where cc/gcc (and c++/g++)
# point to the requested compiler is put in front of PATH.
_CCDIR=""
avant_etapes() {
    [ -n "$QOSMO_CC" ] || return 0
    local cc cxx
    cc="$(command -v "$QOSMO_CC")" || { printf -- '--cc %s : introuvable\n' "$QOSMO_CC" >&2; exit 1; }
    cxx="$(command -v "${QOSMO_CC/gcc/g++}" 2>/dev/null || true)"
    _CCDIR="$(mktemp -d)" || exit 1
    trap 'rm -rf "$_CCDIR"' EXIT
    ln -s "$cc" "$_CCDIR/cc"; ln -s "$cc" "$_CCDIR/gcc"
    [ -n "$cxx" ] && { ln -s "$cxx" "$_CCDIR/c++"; ln -s "$cxx" "$_CCDIR/g++"; }
    return 0
}
# avec_env COMMAND...: the venv activated (Dockerfile:695) and the optional --cc.
avec_env() {
    local p="$PATH"
    [ "$VENV" != none ] && [ -d "$VENV/bin" ] && p="$VENV/bin:$p"
    [ -n "$_CCDIR" ] && p="$_CCDIR:$p"
    if [ "$VENV" != none ] && [ -d "$VENV/bin" ]; then
        PATH="$p" VIRTUAL_ENV="$VENV" "$@"
    else
        PATH="$p" "$@"
    fi
}

apres_etapes() {
    [ -x "$BUILD_DIR/qemu-system-arm" ] && printf '  qemu-system-arm : %s\n' "$BUILD_DIR/qemu-system-arm"
    printf '  ensuite         : c54x_exe/install.sh (le DSP) - wiki osmo-operator, Telephone-emule\n'
}

# ── venv: python for configure ────────────────────────────────────────────────
INST_REGISTER venv "Venv python de configure (tomli)"
inst_venv_done() { [ "$VENV" != none ] && "$VENV/bin/python3" -c 'import tomli' 2>/dev/null; }
inst_venv_check() {
    [ "$VENV" = none ] && { inst_na "VENV=none : python du systeme"; return $INST_RC_NA; }
    _MANQUE=()
    exige "python3" "apt-get install python3" have_cmd python3
    exige "module venv + ensurepip (python3-venv)" "apt-get install python3-venv" python3 -c 'import venv, ensurepip'
    exige "$VENV inscriptible" "--venv DIR vers un dossier a vous, ou --venv none" ecrivable "$VENV"
    bilan_exige
}
inst_venv_run() {
    # Dockerfile:694-696: python3 -m venv /root/.venv-qemu, activate, pip install tomli
    python3 -m venv "$VENV" \
        && "$VENV/bin/pip" install --no-cache-dir tomli \
        || { inst_hint "pip a besoin du reseau (PyPI)"; inst_fail "venv $VENV ou pip install tomli en echec"; return $INST_RC_FAIL; }
    inst_ok
}
inst_venv_verify() { inst_venv_done && inst_ok || inst_fail "tomli introuvable dans $VENV"; }

# ── configure ─────────────────────────────────────────────────────────────────
INST_REGISTER configure "configure --target-list=arm-softmmu --enable-l1-grgsm"
# config.log keeps the invocation line: "# Configured with: '../configure' '--...'".
# Already done = tree configured WITH the same options (the first word, the
# path of configure, does not count).
_cfg_attendu() { printf "'%s' " "${CONFIGURE_ARGS[@]}" | sed 's/ $//'; }
_cfg_actuel()  { sed -n "s/^# Configured with: '[^']*' //p" "$BUILD_DIR/config.log" 2>/dev/null | head -1; }
inst_configure_done() { [ -f "$BUILD_DIR/build.ninja" ] && [ "$(_cfg_actuel)" = "$(_cfg_attendu)" ]; }
inst_configure_check() {
    _MANQUE=()
    exige "sources qosmo ($QOSMO/configure, hw/arm/calypso/)" "QOSMO=/chemin/vers/qosmo, ou git clone https://github.com/bbaranoff/qosmo" \
        test -x "$QOSMO/configure" -a -d "$QOSMO/hw/arm/calypso"
    exige "compilateur C (${QOSMO_CC:-cc})" "apt-get install build-essential" have_cmd "${QOSMO_CC:-cc}"
    exige "make" "apt-get install build-essential" have_cmd make
    exige "ninja (ninja-build)" "apt-get install ninja-build" have_cmd ninja
    exige "pkg-config" "apt-get install pkg-config" have_cmd pkg-config
    exige "glib-2.0 vu de pkg-config (libglib2.0-dev)" "apt-get install libglib2.0-dev" pkg-config --exists glib-2.0
    exige "pixman-1 vu de pkg-config (libpixman-1-dev)" "apt-get install libpixman-1-dev" pkg-config --exists pixman-1
    exige "$BUILD_DIR inscriptible" "--build-dir DIR, ou les droits sur $QOSMO" ecrivable "$BUILD_DIR"
    # configure run FROM the source tree creates ./build and DELETES a build/ it
    # created itself (configure:16-45): never build inside the source tree.
    exige "BUILD_DIR distinct de l arbre source" "--build-dir $QOSMO/build" test "$BUILD_DIR" != "$QOSMO"
    bilan_exige
}
inst_configure_run() {
    # Dockerfile:697-699: mkdir -p build && cd build && ../configure <options>
    local conf="$QOSMO/configure"
    [ "$BUILD_DIR" = "$QOSMO/build" ] && conf=../configure
    mkdir -p "$BUILD_DIR" || { inst_fail "impossible de creer $BUILD_DIR"; return $INST_RC_FAIL; }
    ( cd "$BUILD_DIR" && avec_env "$conf" "${CONFIGURE_ARGS[@]}" ) \
        || { inst_hint "$BUILD_DIR/config.log et $BUILD_DIR/meson-logs/"; inst_fail "configure en echec"; return $INST_RC_FAIL; }
    inst_ok
}
inst_configure_verify() {
    [ -f "$BUILD_DIR/build.ninja" ] || { inst_fail "$BUILD_DIR/build.ninja absent apres configure"; return $INST_RC_FAIL; }
    grep -q '^TARGET_DIRS=arm-softmmu' "$BUILD_DIR/config-host.mak" 2>/dev/null \
        || { inst_fail "config-host.mak ne vise pas arm-softmmu"; return $INST_RC_FAIL; }
    inst_ok
}

# ── build ─────────────────────────────────────────────────────────────────────
# No "already done": make is incremental, it only redoes what changed.
INST_REGISTER build "Compilation (make)"
inst_build_check() {
    _MANQUE=()
    etape_retenue configure || exige "arbre configure ($BUILD_DIR/build.ninja)" "./install.sh --only configure" \
        test -f "$BUILD_DIR/build.ninja"
    exige "make" "apt-get install build-essential" have_cmd make
    exige "ninja (ninja-build)" "apt-get install ninja-build" have_cmd ninja
    [ -n "$QOSMO_CC" ] && exige "compilateur $QOSMO_CC (--cc)" "" have_cmd "$QOSMO_CC"
    exige "$BUILD_DIR inscriptible" "les droits sur $BUILD_DIR" ecrivable "$BUILD_DIR"
    bilan_exige
}
inst_build_run() {
    # Dockerfile:700: make -j$(nproc) in build/ (the QEMU Makefile drives ninja).
    # Dockerfile.run:185 runs `ninja` with the --cc PATH: same target.
    avec_env make -C "$BUILD_DIR" -j"$JOBS" || { inst_fail "echec de compilation"; return $INST_RC_FAIL; }
    inst_ok
}
inst_build_verify() {
    [ -x "$BUILD_DIR/qemu-system-arm" ] && inst_ok || inst_fail "$BUILD_DIR/qemu-system-arm absent apres make"
}

# ── install: make install into the prefix ────────────────────────────────────
INST_REGISTER install "make install (prefixe, lanceur qosmo compris)"
inst_install_done() {
    cmp -s "$BUILD_DIR/qemu-system-arm" "$DESTDIR$PREFIX/bin/qemu-system-arm" \
        && [ -x "$DESTDIR$PREFIX/bin/qosmo" ]
}
inst_install_check() {
    _MANQUE=()
    etape_retenue configure || exige "arbre configure ($BUILD_DIR/build.ninja)" "./install.sh --only configure" \
        test -f "$BUILD_DIR/build.ninja"
    exige "$DESTDIR$PREFIX inscriptible" "--prefix DIR, ou sudo" ecrivable "$DESTDIR$PREFIX"
    bilan_exige
}
inst_install_run() {
    # Dockerfile:701: make install (meson install; DESTDIR honoured, cf. ISO)
    if [ -n "$DESTDIR" ]; then
        avec_env env DESTDIR="$DESTDIR" make -C "$BUILD_DIR" install
    else
        avec_env make -C "$BUILD_DIR" install
    fi || { inst_fail "make install en echec"; return $INST_RC_FAIL; }
    inst_ok
}
inst_install_verify() {
    [ -x "$DESTDIR$PREFIX/bin/qemu-system-arm" ] || { inst_fail "$DESTDIR$PREFIX/bin/qemu-system-arm absent"; return $INST_RC_FAIL; }
    [ -x "$DESTDIR$PREFIX/bin/qosmo" ] || { inst_fail "lanceur $DESTDIR$PREFIX/bin/qosmo absent (tools/qosmo/meson.build)"; return $INST_RC_FAIL; }
    inst_ok
}

# ── bin: copy into the PATH ───────────────────────────────────────────────────
INST_REGISTER bin "qemu-system-arm et qosmo dans le PATH"
inst_bin_done() {
    [ "$BIN_DIR" != none ] || return 1
    local b; for b in qemu-system-arm qosmo; do
        cmp -s "$DESTDIR$PREFIX/bin/$b" "$DESTDIR$BIN_DIR/$b" || return 1
    done
}
inst_bin_check() {
    [ "$BIN_DIR" = none ] && { inst_na "--bin-dir none"; return $INST_RC_NA; }
    ecrivable "$DESTDIR$BIN_DIR" \
        || { inst_na "$DESTDIR$BIN_DIR non inscriptible : binaires laisses dans $PREFIX/bin (sudo, ou --bin-dir DIR)"; return $INST_RC_NA; }
    [ "$ACTION" = check ] && printf '      ok      %s inscriptible\n' "$DESTDIR$BIN_DIR"
    inst_ok
}
inst_bin_run() {
    # Dockerfile:702-703: cp qemu-install/bin/{qemu-system-arm,qosmo} /usr/local/bin/
    # --remove-destination: a running qemu-system-arm keeps its inode, the copy
    # does not fail with "Text file busy".
    local b
    mkdir -p "$DESTDIR$BIN_DIR" || { inst_fail "impossible de creer $DESTDIR$BIN_DIR"; return $INST_RC_FAIL; }
    for b in qemu-system-arm qosmo; do
        [ -x "$DESTDIR$PREFIX/bin/$b" ] || { inst_fail "$DESTDIR$PREFIX/bin/$b absent (etape install)"; return $INST_RC_FAIL; }
        cp --remove-destination "$DESTDIR$PREFIX/bin/$b" "$DESTDIR$BIN_DIR/$b" \
            || { inst_fail "copie de $b vers $DESTDIR$BIN_DIR impossible"; return $INST_RC_FAIL; }
        inst_say "$DESTDIR$BIN_DIR/$b"
    done
    inst_ok
}
inst_bin_verify() { inst_bin_done && inst_ok || inst_fail "$DESTDIR$BIN_DIR differe de $DESTDIR$PREFIX/bin"; }

# ── verify: the calypso machine, with the gr-gsm layer 1 ──────────────────────
# The same binary serves both setups: built with --enable-l1-grgsm, run with
# CALYPSO_DSP_EXTERN=1 for the DSP (Dockerfile:679-682). Without the gr-gsm
# layer 1, the machine description says so (hw/arm/calypso/calypso_mb.c).
INST_REGISTER verify "Controle : -M help liste calypso (couche 1 : grgsm)"
inst_verify_run() {
    local q="$BUILD_DIR/qemu-system-arm" l
    [ -x "$q" ] || { inst_hint "./install.sh --only build"; inst_fail "$q absent"; return $INST_RC_FAIL; }
    l="$(timeout 30 "$q" -M help 2>/dev/null | grep -E '^calypso[[:space:]]')"
    inst_say "$q -M help : ${l:-<pas de machine calypso>}"
    [ -n "$l" ] || { inst_hint "le configure a-t-il garde --enable-l1-grgsm ? ($BUILD_DIR/config.log)"
                     inst_fail "machine calypso absente de -M help"; return $INST_RC_FAIL; }
    case "$l" in
        *"couche 1 : grgsm"*) ;;
        *) inst_fail "machine calypso sans la couche 1 gr-gsm : $l"; return $INST_RC_FAIL ;;
    esac
    if [ -z "$DESTDIR" ] && [ -x "$PREFIX/bin/qosmo" ]; then
        inst_say "lanceur : $("$PREFIX/bin/qosmo" -V 2>&1 | head -1)"
    fi
    inst_ok
}
# ═════════════════════════════════════════════════════════════════════════════
#  THE ENGINE - IDENTICAL copy in the three repositories (see THE CONTRACT)
# ═════════════════════════════════════════════════════════════════════════════
#  Same flow as osmo-operator/install.sh: for each selected step,
#  "already done?" (unless --reinstall), prerequisites, work, check. The first
#  failure stops everything, says why and shows the end of the step's log.
#  The component provides: COMPOSANT, PAQUETS, LOG_DIR, the steps, and the
#  hooks options_composant / apres_options / resume_composant /
#  avant_etapes / apres_etapes.
# -----------------------------------------------------------------------------
ACTION=install ONLY="" SKIP="" WITH_DEPS=0
: "${REINSTALL:=0}" "${VERBOSE:=0}"
_ARGS_ORIG=("$@")
# _val OPTION [VALUE]: an option that expects a value must have one.
_val() { [ $# -ge 2 ] && [ -n "$2" ] || { printf '%s : valeur attendue   (--help)\n' "$1" >&2; exit 2; }; }
while [ $# -gt 0 ]; do
    case "$1" in
        -h|--help)    ACTION=help ;;
        --check)      ACTION=check ;;
        --list)       ACTION=list ;;
        --deps)       ACTION=deps ;;
        --print-deps) ACTION=print-deps ;;
        --with-deps)  WITH_DEPS=1 ;;
        --only)       _val "$@"; ONLY="$2"; shift ;;
        --skip)       _val "$@"; SKIP="$2"; shift ;;
        --reinstall)  REINSTALL=1 ;;
        -v|--verbose) VERBOSE=1 ;;
        *)  _n=0; options_composant "$@"
            [ "$_n" -gt 0 ] || { printf 'option inconnue : %s   (--help)\n' "$1" >&2; exit 2; }
            shift $((_n - 1)) ;;
    esac
    shift
done

if [ "$ACTION" = help ]; then
    awk 'NR > 2 && /^# -{10}/ { exit } NR > 2 && !/^# =+$/ { sub(/^# ?/, ""); print }' "$0"
    exit 0
fi
apres_options

if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
    TTY=1; C_OK=$'\033[32m'; C_KO=$'\033[31m'; C_SK=$'\033[33m'; C_DIM=$'\033[2m'; C_Z=$'\033[0m'
else
    TTY=0; C_OK=""; C_KO=""; C_SK=""; C_DIM=""; C_Z=""
fi
begin() { if [ $TTY -eq 1 ] && [ "$VERBOSE" != 1 ]; then printf '[ %s..%s ] %s' "$C_DIM" "$C_Z" "$1"; else printf '[ .. ] %s\n' "$1"; fi; }
end()   { [ $TTY -eq 1 ] && [ "$VERBOSE" != 1 ] && printf '\r\033[K'
          printf '[%s%s%s] %s' "$2" "$1" "$C_Z" "$3"
          [ -n "${4:-}" ] && printf ' %s(%s)%s' "$C_DIM" "$4" "$C_Z"; printf '\n'; }

# --- step selection (--only / --skip, comma-separated lists) -----------------
in_csv() { case ",$1," in *",$2,"*) return 0;; esac; return 1; }
for _s in ${ONLY//,/ } ${SKIP//,/ }; do
    [ -n "${INST_DESC[$_s]+x}" ] || { printf 'etape inconnue : %s   (etapes : %s)\n' "$_s" "${INST_ORDER[*]}" >&2; exit 2; }
done
SELECTED=()
for _s in "${INST_ORDER[@]}"; do
    [ -n "$ONLY" ] && ! in_csv "$ONLY" "$_s" && continue
    [ -n "$SKIP" ] && in_csv "$SKIP" "$_s" && continue
    SELECTED+=("$_s")
done
# A step that produces what another one requires: with --check, the second is
# not blamed for the absence of what the first one is about to build.
etape_retenue() { in_csv "$(IFS=,; echo "${SELECTED[*]}")" "$1"; }

# --- apt packages -------------------------------------------------------------
# prints the list of missing ones; code 2 without dpkg (not a Debian/Ubuntu)
paquets_absents() {
    have_cmd dpkg || return 2
    local p; for p in "${PAQUETS[@]}"; do have_pkg "$p" || printf '%s ' "$p"; done
    return 0
}
installer_paquets() {
    local manque rc
    manque="$(paquets_absents)"; rc=$?
    if [ $rc -eq 2 ]; then
        printf 'dpkg absent : installez l equivalent de : %s\n' "${PAQUETS[*]}" >&2; return 1
    fi
    if [ -z "${manque// }" ]; then
        printf 'paquets apt : les %d sont presents\n' "${#PAQUETS[@]}"; return 0
    fi
    if [ "$(id -u)" -ne 0 ]; then
        printf 'paquets apt manquants : %s\n  sudo apt-get install -y --no-install-recommends %s\n' "$manque" "$manque" >&2
        return 1
    fi
    printf 'apt-get install : %s\n' "$manque"
    apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends $manque || return 1
    manque="$(paquets_absents)"
    [ -z "${manque// }" ] || { printf 'toujours absents apres apt-get : %s\n' "$manque" >&2; return 1; }
}

case "$ACTION" in
    print-deps) printf '%s\n' "${PAQUETS[*]}"; exit 0 ;;
    deps)       installer_paquets; exit $? ;;
    list)
        printf '%s - %d etape(s), dans cet ordre\n\n' "$COMPOSANT" "${#SELECTED[@]}"
        for _s in "${SELECTED[@]}"; do printf '  %-10s %s\n' "$_s" "${INST_DESC[$_s]}"; done
        printf '\n'; resume_composant | sed 's/^/  /'
        exit 0 ;;
    check)
        printf '%s : prerequis et etat (rien n est modifie)\n\n' "$COMPOSANT"
        resume_composant | sed 's/^/  /'
        _manque="$(paquets_absents)"; _rc=$?
        if [ $_rc -eq 2 ]; then printf '\n  paquets apt : dpkg absent, non verifies (%s)\n' "${PAQUETS[*]}"
        elif [ -n "${_manque// }" ]; then printf '\n  paquets apt absents (installes autrement ? sinon --deps) : %s\n' "$_manque"
        else printf '\n  paquets apt : les %d sont presents\n' "${#PAQUETS[@]}"; fi
        _rc=0
        for _s in "${SELECTED[@]}"; do
            _p="inst_${_s//-/_}"
            if ! declare -F "${_p}_done" >/dev/null; then _etat="pas de controle"
            elif "${_p}_done" >/dev/null 2>&1; then _etat="deja fait"
            else _etat="a faire"; fi
            printf '\n  %-10s %s  %s[%s]%s\n' "$_s" "${INST_DESC[$_s]}" "$C_DIM" "$_etat" "$C_Z"
            declare -F "${_p}_check" >/dev/null || continue
            _INST_REASON=""; _INST_HINT=""; _MANQUE=()
            "${_p}_check"; _r=$?
            case $_r in
                "$INST_RC_FAIL") _rc=1 ;;
                "$INST_RC_NA")   printf '      sans objet : %s\n' "$_INST_REASON" ;;
            esac
        done
        printf '\n'
        if [ $_rc -eq 0 ]; then printf '%sprerequis satisfaits%s\n' "$C_OK" "$C_Z"
        else printf '%sprerequis manquants%s (voir MANQUE ci-dessus)\n' "$C_KO" "$C_Z"; fi
        exit $_rc ;;
esac

# --- installation ---------------------------------------------------------------
if [ "$WITH_DEPS" = 1 ]; then
    installer_paquets || { printf 'paquets apt : echec - rien n a ete construit\n' >&2; exit 1; }
else
    _manque="$(paquets_absents 2>/dev/null)"
    [ -n "${_manque// }" ] && printf '%snote : paquets apt absents (installes autrement ? sinon --with-deps) : %s%s\n' "$C_DIM" "$_manque" "$C_Z"
fi
if [ "$VERBOSE" != 1 ]; then
    mkdir -p "$LOG_DIR" || { printf 'journal impossible : %s (LOG_DIR=...)\n' "$LOG_DIR" >&2; exit 1; }
fi
# _jouer FUNCTION: output goes to the step's log, or to the screen with -v.
# No pipe: the function runs in THIS shell (its failure reason survives).
_jouer() { if [ "$VERBOSE" = 1 ]; then "$1"; else "$1" >>"$_log" 2>&1; fi; }
_echec() {   # _echec STEP REASON [journal]: says everything known, and stops
    end FAIL "$C_KO" "${INST_DESC[$1]}" "$2"
    [ -n "$_INST_HINT" ] && printf '       → %s\n' "$_INST_HINT"
    # the end of the log, when the work itself (run, verify) failed
    if [ -n "${3:-}" ] && [ "$VERBOSE" != 1 ] && [ -s "$_log" ]; then
        printf '       %sjournal : %s (fin ci-dessous)%s\n' "$C_DIM" "$_log" "$C_Z"
        tail -n 25 "$_log" | sed 's/^/       | /'
    fi
    printf '\n%s : installation interrompue a l etape %s\n' "$COMPOSANT" "$1"
    exit 1
}
avant_etapes
_nb_ok=0; _nb_skip=0
for _s in "${SELECTED[@]}"; do
    _p="inst_${_s//-/_}"; _log="$LOG_DIR/$_s.log"
    _INST_REASON=""; _INST_HINT=""; _MANQUE=()
    [ "$VERBOSE" = 1 ] || printf '\n===== %s  %s =====\n' "$(date '+%F %T')" "$_s" >>"$_log"
    begin "${INST_DESC[$_s]}"
    if [ "$REINSTALL" != 1 ] && declare -F "${_p}_done" >/dev/null && _jouer "${_p}_done"; then
        end SKIP "$C_SK" "${INST_DESC[$_s]}" "deja fait"; _nb_skip=$((_nb_skip + 1)); continue
    fi
    if declare -F "${_p}_check" >/dev/null; then
        _jouer "${_p}_check"; _r=$?
        case $_r in
            "$INST_RC_OK")   ;;
            "$INST_RC_NA")   end SKIP "$C_SK" "${INST_DESC[$_s]}" "${_INST_REASON:-sans objet}"
                             _nb_skip=$((_nb_skip + 1)); continue ;;
            *)               _echec "$_s" "${_INST_REASON:-prerequis non satisfait}" ;;
        esac
    fi
    _jouer "${_p}_run"; _r=$?
    case $_r in
        "$INST_RC_OK")   ;;
        "$INST_RC_DONE"|"$INST_RC_NA")
                         end SKIP "$C_SK" "${INST_DESC[$_s]}" "${_INST_REASON:-rien a faire}"
                         _nb_skip=$((_nb_skip + 1)); continue ;;
        *)               _echec "$_s" "${_INST_REASON:-echec}" journal ;;
    esac
    # installing is not having installed: check afterwards
    if declare -F "${_p}_verify" >/dev/null && ! _jouer "${_p}_verify"; then
        _echec "$_s" "faite, mais le controle echoue : ${_INST_REASON:-}" journal
    fi
    end " OK " "$C_OK" "${INST_DESC[$_s]}" "${_INST_REASON:-}"; _nb_ok=$((_nb_ok + 1))
done
printf '\n%s : %d ok · %d ignoree(s) · 0 echec\n' "$COMPOSANT" "$_nb_ok" "$_nb_skip"
apres_etapes
exit 0

3.2 https://github.com/bbaranoff/qosmo/blob/main/make-overlay.sh

3670 octets, 90 lignes → 90 lignes

#!/bin/bash
# make-overlay.sh — collects the files specific to qosmo into a directory.
#
#     vanilla QEMU (v$(cat VERSION))  +  overlay  =  qosmo
#
# "Specific to qosmo" = tracked by git here AND (absent from vanilla QEMU OR
# different from it). Everything else is original QEMU and stays out. Symbolic
# links only count if their target differs.
#
# The directory is rebuilt on each run (no stale file) and carries:
#   MANIFEST.ajoutes   files absent from vanilla
#   MANIFEST.modifies  vanilla files modified by qosmo
#   MANIFEST.retires   vanilla files qosmo does not track (gitlab CI, roms/...),
#                      for information: the overlay does not delete them
#
# Rebuilding the tree:  cp -a <vanilla> tree && cp -a <overlay>/. tree/
#                       (then rm tree/MANIFEST.*)
#
# Vanilla is taken from $QEMU_GENUINE (default ../qemu-<VERSION>); if absent, it
# is cloned (depth 1, tag v<VERSION>) from gitlab.com/qemu-project/qemu.
#
# Usage: ./make-overlay.sh [--dry-run] [DIR]   (default ../qosmo-overlay)
set -euo pipefail
DRY=0; [ "${1:-}" = "--dry-run" ] && { DRY=1; shift; }
SRC="$(cd "$(dirname "$0")" && pwd)"
VER="$(cat "$SRC/VERSION")"
OUT="${1:-$SRC/../qosmo-overlay}"
GEN="${QEMU_GENUINE:-$SRC/../qemu-$VER}"

if [ ! -f "$GEN/VERSION" ]; then
    echo "vanilla absent : clone de v$VER dans $GEN"
    git clone -q --depth 1 --branch "v$VER" https://gitlab.com/qemu-project/qemu.git "$GEN"
fi
[ "$(cat "$GEN/VERSION")" = "$VER" ] || {
    echo "!! $GEN est en $(cat "$GEN/VERSION"), qosmo en $VER" >&2; exit 1; }

# Artifacts that do not belong in an overlay, even if tracked by mistake.
exclu() {
    case "$1" in
        */__pycache__/*|*.pyc|*.o|*.d|*.orig|*.rej|*~|*.swp) return 0;;
    esac
    return 1
}

# Same content? A link is compared on its target string, not on what it points to.
pareil() {
    local a="$SRC/$1" b="$GEN/$1"
    if [ -L "$a" ] || [ -L "$b" ]; then
        [ -L "$a" ] && [ -L "$b" ] && [ "$(readlink "$a")" = "$(readlink "$b")" ]
    else
        [ -f "$b" ] && cmp -s "$a" "$b"
    fi
}

tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
(cd "$GEN" && git ls-files) | sort > "$tmp/vanilla"
(cd "$SRC" && git ls-files) | sort > "$tmp/qosmo"
: > "$tmp/ajoutes"; : > "$tmp/modifies"; skip=0
while IFS= read -r f; do
    if exclu "$f"; then skip=$((skip+1)); continue; fi
    [ -e "$SRC/$f" ] || [ -L "$SRC/$f" ] || continue   # tracked but removed from disk
    echo "$f" >> "$tmp/ajoutes"
done < <(comm -13 "$tmp/vanilla" "$tmp/qosmo")
while IFS= read -r f; do
    pareil "$f" || echo "$f" >> "$tmp/modifies"
done < <(comm -12 "$tmp/vanilla" "$tmp/qosmo")
comm -23 "$tmp/vanilla" "$tmp/qosmo" > "$tmp/retires"

na=$(wc -l < "$tmp/ajoutes"); nm=$(wc -l < "$tmp/modifies"); nr=$(wc -l < "$tmp/retires")
echo "qosmo    : $SRC ($VER)"
echo "vanilla  : $GEN"
echo "overlay  : $OUT"
echo "----"
sed 's/^/  ajoute  /' "$tmp/ajoutes"
sed 's/^/  modifie /' "$tmp/modifies"
echo "----"
echo "$na ajoute(s), $nm modifie(s), $nr vanilla non suivi(s), $skip artefact(s) ecarte(s)"
[ "$DRY" = 1 ] && { echo "DRY-RUN : rien d'ecrit."; exit 0; }

# Only wipe what this script produced itself.
if [ -e "$OUT" ] && [ -n "$(ls -A "$OUT")" ] && [ ! -f "$OUT/MANIFEST.ajoutes" ]; then
    echo "!! $OUT existe, n'est pas vide et n'est pas un overlay qosmo : je n'y touche pas" >&2
    exit 1
fi
rm -rf "$OUT"; mkdir -p "$OUT"
(cd "$SRC" && cat "$tmp/ajoutes" "$tmp/modifies" | xargs -r -d "\n" cp -a --parents -t "$OUT")
cp "$tmp/ajoutes" "$OUT/MANIFEST.ajoutes"
cp "$tmp/modifies" "$OUT/MANIFEST.modifies"
cp "$tmp/retires" "$OUT/MANIFEST.retires"
echo "ecrit : $(find "$OUT" -type f -o -type l | wc -l) entree(s) dans $OUT"

3.3 https://github.com/bbaranoff/qosmo/blob/main/run.sh

12847 octets, 311 lignes → 311 lignes

#!/bin/bash
set -uo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MODDIR="$HERE/run_modules"

DRY=0 ONLY="" SKIP="" ACTION=start FORCE=0
NO_ATTACH="${CALYPSO_NO_ATTACH:-0}"

usage() {
    cat <<'USAGE'
Usage : ./run.sh [options]
  --list              affiche le plan et sort, sans rien lancer
  --dry-run           déroule le plan sans effet de bord
  --only  <slugs>     ne joue que ces modules (séparés par des virgules)
  --skip  <slugs>     saute ces modules
  --stop              arrête la pile (plan en ordre inverse)
  --status            interroge l'état de chaque module
  --force             relance même les modules déjà démarrés
  --no-attach         ne pas s'attacher à tmux à la fin
  --check-paths       vérifie que les dépendances déclarées existent
  --restart           = --reset puis démarrage — la relance sûre au quotidien
  --reset             repart d'un état propre : arrête la pile, tue le serveur
                      tmux, archive les journaux, purge sockets et fichiers partagés
  --lang <code>       langue de l'affichage (fr de en es pt it hi zh ar)
  --icount[=SPEC]     QEMU avec -icount SPEC (défaut : auto) ; = QEMU_ICOUNT
  --asm[=FICHIER]     QEMU avec -d in_asm -D FICHIER (défaut : $LOG_DIR/qemu-asm.log) ; = QEMU_ASM
  --profile <nom>     accepté pour compatibilité, sans effet (un seul banc)
  --verbose           accepté pour compatibilité, sans effet
  -h, --help          cette aide
USAGE
}

_reset() {
    local horodat rep n=0
    printf '\nRemise à zéro — rien ne sera relancé.\n\n' >&2
    printf '  arrêt de la pile…\n' >&2
    "$0" --stop >/dev/null 2>&1 || true
    local logdir="$LOG_DIR"
    if [ -d "$logdir" ] && [ -n "$(ls -A "$logdir" 2>/dev/null)" ]; then
        horodat="$(date +%Y%m%d-%H%M%S)"
        rep="$logdir/../archives/$horodat"
        mkdir -p "$rep" 2>/dev/null && \
        if cp -a "$logdir/." "$rep/" 2>/dev/null; then
            printf '  journaux archivés     %s\n' "$rep" >&2
        fi
        local arcdir="$logdir/../archives"
        local keep="${CALYPSO_ARCHIVES_KEEP:-3}"
        local maxmb="${CALYPSO_ARCHIVE_MAX_MB:-32}"
        local f taille avant apres
        for f in "$rep"/*; do
            [ -f "$f" ] || continue
            taille=$(stat -c %s "$f" 2>/dev/null || echo 0)
            if [ "$taille" -gt $(( maxmb * 1024 * 1024 )) ]; then
                head -c $(( maxmb * 1024 * 1024 )) "$f" > "$f.tete" 2>/dev/null &&
                mv -f "$f.tete" "$f" 2>/dev/null &&
                printf '  archive tronquée      %s (%s Mo -> %s Mo, tête conservée)\n' \
                       "$(basename "$f")" "$(( taille / 1048576 ))" "$maxmb" >&2
            fi
        done
        if [ -d "$arcdir" ]; then
            avant=$(du -sm "$arcdir" 2>/dev/null | cut -f1)
            ls -1t "$arcdir" 2>/dev/null | tail -n +$(( keep + 1 )) | while read -r vieux; do
                [ -n "$vieux" ] && rm -rf -- "$arcdir/$vieux"
            done
            apres=$(du -sm "$arcdir" 2>/dev/null | cut -f1)
            [ "${avant:-0}" -gt "${apres:-0}" ] 2>/dev/null &&
                printf '  archives purgées      %s gardées (%s Mo -> %s Mo)\n' \
                       "$keep" "$avant" "$apres" >&2
        fi
        rm -f "$logdir"/*.log 2>/dev/null
        printf '  journaux remis à zéro %s\n' "$logdir" >&2
        df -h "$logdir" 2>/dev/null | tail -1 | \
            awk '{printf "  espace disponible     %s (%s utilisé)\n", $4, $5}' >&2
    fi
    if command -v tmux >/dev/null 2>&1 && tmux ls >/dev/null 2>&1; then
        tmux kill-server 2>/dev/null || true
        printf '  serveur tmux tué      (environnement global purgé)\n' >&2
    fi
    local motifs="qemu-system-arm osmocon osmo-bts-trx fake_trx trxcon"
    local m p pids
    for m in $motifs; do
        pids="$(pgrep -f -- "$m" 2>/dev/null || true)"
        for p in $pids; do
            [ "$p" = "$$" ] && continue
            [ "$p" = "${PPID:-0}" ] && continue
            kill -TERM "$p" 2>/dev/null && n=$((n + 1))
        done
    done
    [ "$n" -gt 0 ] && printf '  processus arrêtés     %d\n' "$n" >&2
    local art
    for art in /tmp/osmocom_l2 /tmp/ms2_l2 /dev/shm/calypso_*; do
        [ -e "$art" ] && rm -f "$art" 2>/dev/null && printf '  supprimé              %s\n' "$art" >&2
    done
    printf '\nÉtat propre. Relancez :\n' >&2
    printf '   ./run.sh\n\n' >&2
}

while [ $# -gt 0 ]; do
    case "$1" in
        --list)        ACTION=list ;;
        --dry-run)     DRY=1 ;;
        --only)        ONLY="${2:-}"; shift ;;
        --skip)        SKIP="${2:-}"; shift ;;
        --profile)     shift ;;
        --stop)        ACTION=stop ;;
        --status)      ACTION=status ;;
        --force)       FORCE=1 ;;
        --no-attach)   NO_ATTACH=1 ;;
        --verbose)     ;;
        --check-paths) ACTION=checkpaths ;;
        --reset)       ACTION=reset ;;
        --restart)     ACTION=restart ;;
        --lang)        CALYPSO_LANG="${2:-}"; export CALYPSO_LANG; shift ;;
        --icount)      export QEMU_ICOUNT=auto ;;
        --icount=*)    export QEMU_ICOUNT="${1#--icount=}" ;;
        --asm)         export QEMU_ASM=1 ;;
        --asm=*)       export QEMU_ASM="${1#--asm=}" ;;
        -h|--help)     usage; exit 0 ;;
        *) printf 'option inconnue : %s\n\n' "$1" >&2; usage >&2; exit 2 ;;
    esac
    shift
done

if [ -f "$HERE/environnement/bench.env" ]; then
    set -a; . "$HERE/environnement/bench.env"; set +a
else
    printf 'configuration introuvable : %s\n' "$HERE/environnement/bench.env" >&2
    exit 2
fi

if [ "$ACTION" = reset ]; then _reset; exit 0; fi
if [ "$ACTION" = restart ]; then _reset; ACTION=start; fi

LOGDIR="$LOG_DIR"
mkdir -p "$LOGDIR/mod" 2>/dev/null || true

if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
    TTY=1; C_OK=$'\033[32m'; C_KO=$'\033[31m'; C_SK=$'\033[33m'; C_DIM=$'\033[2m'; C_Z=$'\033[0m'
else
    TTY=0; C_OK=""; C_KO=""; C_SK=""; C_DIM=""; C_Z=""
fi
say_begin() { if [ $TTY -eq 1 ]; then printf '[ %s.. %s] %s' "$C_DIM" "$C_Z" "$1"; else printf '[ .. ] %s\n' "$1"; fi; }
say_end()   {
    if [ $TTY -eq 1 ]; then printf '\r\033[K'; fi
    printf '[%s%-4s%s] %s' "$2" "$1" "$C_Z" "$3"
    [ -n "${4:-}" ] && printf ' %s(%s)%s' "$C_DIM" "$4" "$C_Z"
    printf '\n'
}

. "$MODDIR/_lib/mod.sh"
shopt -s nullglob
for f in "$MODDIR"/[0-9][0-9]-*.sh; do . "$f"; done
shopt -u nullglob

if [ ${#MOD_ORDER[@]} -eq 0 ]; then
    printf '%s\n' "$(t aucun_mod "$MODDIR")" >&2
    exit 2
fi

in_csv() { case ",$1," in *",$2,"*) return 0;; esac; return 1; }
selected=()
for m in "${MOD_ORDER[@]}"; do
    [ -n "$ONLY" ] && { in_csv "$ONLY" "$m" || continue; }
    [ -n "$SKIP" ] && { in_csv "$SKIP" "$m" && continue; }
    selected+=("$m")
done

if [ "$ACTION" = list ]; then
    printf '%s\n\n' "$(t plan "${#selected[@]}")"
    for m in "${selected[@]}"; do
        printf '  %-18s %-42s %s%s\n' "$m" "$(t_mod "$m")" \
            "$([ "${MOD_REQUIRED[$m]}" = 1 ] && t obligatoire || t optionnel)" \
            "$([ -n "${MOD_DEPS[$m]}" ] && printf '  %s' "$(t apres "${MOD_DEPS[$m]}")")"
    done
    exit 0
fi

if [ "$ACTION" = checkpaths ]; then
    rc=0
    for v in QEMU_BIN FIRMWARE_ELF FIRMWARE_BIN OSMOCON OSMOCOM_CFG; do
        p="${!v:-}"
        if [ -z "$p" ];      then say_end "$(t fail)" "$C_KO" "$v" "non défini"; rc=1
        elif [ -e "$p" ];    then say_end " $(t ok) " "$C_OK" "$v" "$p"
        else                      say_end "$(t fail)" "$C_KO" "$v" "introuvable : $p"; rc=1; fi
    done
    exit $rc
fi

declare -A STATE=()
nb_ok=0 nb_fail=0 nb_skip=0 nb_warn=0
[ "$ACTION" = stop ] && { tmp=(); for ((i=${#selected[@]}-1;i>=0;i--)); do tmp+=("${selected[$i]}"); done; selected=("${tmp[@]}"); }

for m in "${selected[@]}"; do
    p="$(mod_prefix "$m")"
    log="${MOD_LOG[$m]:-$LOGDIR/mod/$m.log}"
    _MOD_REASON=""; _MOD_HINT=""

    if [ "$ACTION" = stop ]; then
        say_begin "$(t arret "$(t_mod "$m")")"
        declare -F "${p}_stop" >/dev/null && "${p}_stop" >>"$log" 2>&1
        say_end " $(t ok) " "$C_OK" "$(t arret "$(t_mod "$m")")"
        continue
    fi

    if [ "$ACTION" = status ]; then
        if declare -F "${p}_status" >/dev/null && "${p}_status" >>"$log" 2>&1; then
            say_end "$(t up)" "$C_OK" "$(t_mod "$m")"
        else
            say_end "$(t down)" "$C_SK" "$(t_mod "$m")"
        fi
        continue
    fi

    if ! eval "${MOD_ENABLED_IF[$m]}" 2>/dev/null; then
        say_end "$(t skip)" "$C_SK" "$(t_mod "$m")" "$(t desactive "${MOD_ENABLED_IF[$m]}")"
        STATE[$m]=skip; nb_skip=$((nb_skip+1)); continue
    fi

    dep_ko=""
    for d in ${MOD_DEPS[$m]}; do
        case "${STATE[$d]:-absent}" in ok|skip) ;; *) dep_ko="$d"; break;; esac
    done
    if [ -n "$dep_ko" ]; then
        say_end "$(t skip)" "$C_SK" "$(t_mod "$m")" "$(t dep_ko "$dep_ko")"
        STATE[$m]=skip; nb_skip=$((nb_skip+1)); continue
    fi

    say_begin "$(t_mod "$m")"

    if [ $FORCE -eq 0 ] && declare -F "${p}_status" >/dev/null && "${p}_status" >>"$log" 2>&1; then
        say_end "$(t skip)" "$C_SK" "$(t_mod "$m")" "$(t deja)"
        STATE[$m]=skip; nb_skip=$((nb_skip+1)); continue
    fi

    if declare -F "${p}_check" >/dev/null; then
        "${p}_check" >>"$log" 2>&1; rc=$?
        if [ $rc -eq $MOD_RC_FAIL ] && [ $DRY -eq 1 ]; then
            say_end "$(t dry)" "$C_DIM" "$(t_mod "$m")" \
                    "$(t prereq_sim "${_MOD_REASON:-}")"
            STATE[$m]=skip; nb_skip=$((nb_skip+1)); continue
        fi
        if [ $rc -eq $MOD_RC_FAIL ]; then
            say_end "$(t fail)" "$C_KO" "$(t_mod "$m")" "${_MOD_REASON:-$(t sans_raison)}"
            [ -n "$_MOD_HINT" ] && printf '       %s→ %s%s\n' "$C_DIM" "$_MOD_HINT" "$C_Z"
            printf '       %s%s%s\n' "$C_DIM" "$(t journal "$log")" "$C_Z"
            STATE[$m]=fail; nb_fail=$((nb_fail+1))
            [ "${MOD_REQUIRED[$m]}" = 1 ] && { printf '\n%s\n' "$(t interrompu)"; exit 1; }
            nb_warn=$((nb_warn+1)); continue
        elif [ $rc -eq $MOD_RC_SKIP ]; then
            say_end "$(t skip)" "$C_SK" "$(t_mod "$m")" "${_MOD_REASON:-}"
            STATE[$m]=skip; nb_skip=$((nb_skip+1)); continue
        fi
    fi

    if [ $DRY -eq 1 ] && [ "${MOD_PURE[$m]}" != 1 ]; then
        say_end " -- " "$C_DIM" "$(t_mod "$m")" "$(t simule)"
        STATE[$m]=ok; continue
    fi

    "${p}_start" >>"$log" 2>&1; rc=$?
    case $rc in
        $MOD_RC_ALREADY) say_end "$(t skip)" "$C_SK" "$(t_mod "$m")" "${_MOD_REASON:-$(t deja)}"
                         STATE[$m]=skip; nb_skip=$((nb_skip+1)); continue ;;
        $MOD_RC_SKIP)    say_end "$(t skip)" "$C_SK" "$(t_mod "$m")" "${_MOD_REASON:-}"
                         STATE[$m]=skip; nb_skip=$((nb_skip+1)); continue ;;
        $MOD_RC_FAIL)    say_end "$(t fail)" "$C_KO" "$(t_mod "$m")" "${_MOD_REASON:-$(t sans_raison)}"
                         [ -n "$_MOD_HINT" ] && printf '       %s→ %s%s\n' "$C_DIM" "$_MOD_HINT" "$C_Z"
                         printf '       %s%s%s\n' "$C_DIM" "$(t journal "$log")" "$C_Z"
                         STATE[$m]=fail; nb_fail=$((nb_fail+1))
                         [ "${MOD_REQUIRED[$m]}" = 1 ] && { printf '\n%s\n' "$(t interrompu)"; exit 1; }
                         nb_warn=$((nb_warn+1)); continue ;;
    esac

    if declare -F "${p}_wait" >/dev/null; then
        if ! "${p}_wait" >>"$log" 2>&1; then
            say_end "$(t fail)" "$C_KO" "$(t_mod "$m")" "$(t pas_pret "${_MOD_REASON:-délai dépassé}")"
            printf '       %s%s%s\n' "$C_DIM" "$(t journal "$log")" "$C_Z"
            STATE[$m]=fail; nb_fail=$((nb_fail+1))
            [ "${MOD_REQUIRED[$m]}" = 1 ] && { printf '\n%s\n' "$(t interrompu)"; exit 1; }
            nb_warn=$((nb_warn+1)); continue
        fi
    fi

    say_end " $(t ok) " "$C_OK" "$(t_mod "$m")"
    STATE[$m]=ok; nb_ok=$((nb_ok+1))
done

printf '\n%s\n' "$(t bilan "$nb_ok" "$nb_skip" "$nb_fail")"

if [ "$ACTION" = start ] && [ $DRY -eq 0 ] && [ $nb_fail -eq 0 ]; then
    _sess="${TMUX_SESSION:-calypso}"
    printf '\n'
    if tmux has-session -t "$_sess" 2>/dev/null; then
        printf '  %s%s%s   tmux attach -t %s\n' "$C_OK" "$(t connecter)" "$C_Z" "$_sess"
    else
        printf '  %s%s%s   %s\n' "$C_DIM" "$(t connecter)" "$C_Z" "$(t pas_session "$_sess")"
    fi
    printf '  %s%s%s       %s\n' "$C_DIM" "$(t journaux)" "$C_Z" "$LOGDIR"
    printf '  %s%s%s           ./run.sh --status\n' "$C_DIM" "$(t etat)" "$C_Z"
    printf '  %s%s%s        ./run.sh --stop\n' "$C_DIM" "$(t arreter)" "$C_Z"
    printf '\n'
    if [ $TTY -eq 1 ] && [ "$NO_ATTACH" != 1 ] && [ -z "${TMUX:-}" ] \
       && tmux has-session -t "$_sess" 2>/dev/null; then
        printf '  %sconnexion a la session…  (Ctrl-b d pour detacher)%s\n\n' "$C_DIM" "$C_Z"
        exec tmux attach -t "$_sess"
    fi
fi

[ $nb_fail -gt 0 ] && exit 1
exit 0

3.4 https://github.com/bbaranoff/qosmo/blob/main/tout-en-un.py

19308 octets, 412 lignes → 409 lignes (1 groupes compactés)

#!/usr/bin/env python3
"""tout-en-un.py — gathers the .log .sh .md .mmd .py .txt of one or more
directories into ONE Markdown file, shortened WITHOUT LOSS:

  * identical files (same content): a single copy, the others refer to the
    first one;
  * identical consecutive lines: "line  ×N";
  * consecutive lines that only differ by numbers (timestamp, counter, fn...):
    a template where the constant numbers stay in place and the variable
    numbers become ⟨1⟩ ⟨2⟩..., followed by the ordered list of values. Each
    line is rebuilt by putting the values back in order.
  * ANSI colour codes and carriage returns removed (the only loss, deliberate).

    ./tout-en-un.py [dir...]      default: the qosmo overlay (rebuilt by make-overlay.sh
                                  unless run from the overlay)
    SORTIE=/path.md  EXT="log sh md mmd py txt c h"  SEUIL=3 (minimum group size)
"""
import hashlib
import os
import re
import subprocess
import sys
import time

EXT = os.environ.get("EXT", "log sh md mmd py txt c h").split()
SEUIL = int(os.environ.get("SEUIL", "3"))
FORMAT = os.environ.get("FORMAT", "qmd")           # qmd (Quarto) or md
SORTIE = os.environ.get("SORTIE") or "/root/qosmo-%s.%s" % (time.strftime("%Y%m%d-%H%M%S"), FORMAT)
AUTEUR = os.environ.get("AUTEUR", "Banc GSM émulé — banc-max")
IGNORES = {".git", "__pycache__", ".pytest_cache", "node_modules"}
DSP_DUMP = re.compile(r"(^calypso_dsp.*\.txt|^dsp-rom-.*\.txt|\.dis)$")   # DSP ROM dumps (not ours, megabytes of hex)
HTML_SANS_COULEUR = 2_500_000                      # bytes: larger bundles get no syntax highlighting in html
ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
TELNET = re.compile(rb"\xff[\xfb-\xfe].|\xff.")           # telnet negotiation (IAC) of the VTY captures
CONTROLE = re.compile("[\x00-\x08\x0b\x0c\x0e-\x1f\x7f\ufffd]")  # control characters, undecodable bytes
NUM = re.compile(r"\d+")
LANG = {"sh": "bash", "py": "python", "md": "markdown", "mmd": "mermaid", "txt": "text", "log": "text",
        "c": "c", "h": "c"}


def dossiers_par_defaut():
    """The qosmo overlay (only the files specific to qosmo, not vanilla QEMU).
    Run from an overlay, the overlay is this directory; run from the qosmo
    tree, make-overlay.sh rebuilds ../qosmo-overlay first."""
    ici = os.path.dirname(os.path.realpath(__file__))
    if os.path.exists(os.path.join(ici, "MANIFEST.ajoutes")):
        overlay = ici
    else:
        overlay = os.path.join(os.path.dirname(ici), "qosmo-overlay")
        subprocess.run([os.path.join(ici, "make-overlay.sh"), overlay], check=True, stdout=subprocess.DEVNULL)
    return [overlay]
  ×4
def rendu_quarto(racine, d):
    """d is the resource directory of a page rendered by Quarto (<name>_files next to <name>.html)."""
    return d.endswith("_files") and os.path.exists(os.path.join(racine, d[:-len("_files")] + ".html"))


def fichiers(dossier):
    for racine, dirs, noms in os.walk(dossier):
        dirs[:] = sorted(d for d in dirs if d not in IGNORES and not rendu_quarto(racine, d))
        for n in sorted(noms):
            if n.startswith("tout-en-un-"):      # our own outputs
                continue
            if DSP_DUMP.search(n):
                continue
            if n.rsplit(".", 1)[-1].lower() in EXT and "." in n:
                yield os.path.join(racine, n)


def compacter(lignes):
    """Returns (output lines, number of compacted groups)."""
    out, groupes, i, n = [], 0, 0, len(lignes)
    while i < n:
        cle = NUM.sub("\0", lignes[i])
        j = i + 1
        while j < n and NUM.sub("\0", lignes[j]) == cle:
            j += 1
        taille = j - i
        if taille < SEUIL:
            out.extend(lignes[i:j])
            i = j
            continue
        groupes += 1
        if "\0" not in cle:                      # exact repetition
            out.append("%s  ×%d" % (lignes[i], taille))
            i = j
            continue
        valeurs = [NUM.findall(l) for l in lignes[i:j]]
        nb = len(valeurs[0])
        varie = [len({v[k] for v in valeurs}) > 1 for k in range(nb)]
        # template: constant numbers put back, variable ones numbered ⟨k⟩
        morceaux, k, idx = cle.split("\0"), 0, 0
        gab = morceaux[0]
        for m in morceaux[1:]:
            if varie[k]:
                idx += 1
                gab += "⟨%d⟩" % idx
            else:
                gab += valeurs[0][k]
            gab += m
            k += 1
        out.append("%s  ×%d" % (gab, taille))
        if idx:
            tuples = [",".join(v[k] for k in range(nb) if varie[k]) for v in valeurs]
            # value lines of ~100 columns
            ligne, courant = [], "    ⟨⟩ ="
            for t in tuples:
                if len(courant) + len(t) + 3 > 110:
                    ligne.append(courant)
                    courant = "       "
                courant += " (" + t + ")"
            ligne.append(courant)
            out.extend(ligne)
        i = j
    return out, groupes


def synthese(tous):
    """Run, scale, failures, coverage summary: read from verdict.txt / couverture.txt."""
    lignes, vus = [], set()
    for f in tous:
        nom = os.path.basename(f)
        if nom not in ("verdict.txt", "couverture.txt"):
            continue
        run = os.path.basename(os.path.dirname(f))
        if (run, nom) in vus:                 # same run copied in two places
            continue
        vus.add((run, nom))
        try:
            txt = ANSI.sub("", open(f, "rb").read().decode("utf-8", "replace"))
        except OSError:
            continue
        for l in txt.splitlines():
            l = re.sub(r"\s+", " ", l.strip())
            if l.startswith("ELEMENT MAX"):
                lignes.append((0, "| %s | échelle | %s |" % (run, l)))
            elif l.startswith("bilan :"):
                lignes.append((2, "| %s | couverture | %s |" % (run, l[8:])))
            elif l.startswith("couverture couche 1"):
                lignes.append((1, "| %s | mode, date | %s |" % (run, l.split("—", 1)[-1].strip())))
            else:
                m = re.match(r"(\d+) (\S+) (ECHEC|SAUTE) ?(.*)", l)
                if m:
                    lignes.append((3, "| %s | barreau %s %s | %s %s |" % (run, m.group(1), m.group(2), m.group(3), m.group(4))))
    return [l for _, l in sorted(lignes, key=lambda x: x[0])]


MERMAID_OUVRE = "```{mermaid}" if FORMAT == "qmd" else "```mermaid"


def rendre_mmd(lignes):
    return "%s\n%s\n```" % (MERMAID_OUVRE, "\n".join(lignes))


_DEPOTS = {}
QOSMO_GITHUB = "https://github.com/bbaranoff/qosmo"   # overlay without its source tree next to it


def depot_git(chemin):
    """(root, GitHub base URL, branch, name) of the repository holding chemin,
    or None. A qosmo overlay (directory with MANIFEST.ajoutes, made by
    make-overlay.sh) stands for the qosmo repository: same relative paths."""
    d = os.path.abspath(chemin)
    if not os.path.isdir(d):
        d = os.path.dirname(d)
    while not (os.path.exists(os.path.join(d, ".git")) or os.path.exists(os.path.join(d, "MANIFEST.ajoutes"))):
        if d == "/":
            return None
        d = os.path.dirname(d)
    if d not in _DEPOTS:
        source = d
        if not os.path.exists(os.path.join(d, ".git")):         # overlay: its source tree sits next to it
            source = os.path.join(os.path.dirname(d), "qosmo")
        def git(*a):
            r = subprocess.run(["git", "-C", source] + list(a), capture_output=True, text=True)
            return r.stdout.strip() if r.returncode == 0 else ""
        url = git("config", "--get", "remote.origin.url") or (QOSMO_GITHUB if source != d else "")
        url = re.sub(r"^git@([^:]+):", r"https://\1/", url)
        url = re.sub(r"\.git$", "", url).rstrip("/")
        branche = git("rev-parse", "--abbrev-ref", "HEAD")
        _DEPOTS[d] = (d, url, branche if branche and branche != "HEAD" else "main",
                      os.path.basename(source)) if url else None
    return _DEPOTS[d]


def adresse(chemin):
    """GitHub address of chemin (blob/<branch>/<path>), or the local path outside a repository."""
    g = depot_git(chemin)
    if not g:
        return chemin
    rel = os.path.relpath(os.path.abspath(chemin), g[0])
    return "%s/blob/%s/%s" % (g[1], g[2], "" if rel == "." else rel)


def reference(chemin):
    """File reference used in titles: a GitHub link when the file lives in a repository."""
    a = adresse(chemin)
    if not a.startswith("http"):
        return a
    # Quarto opens external links in a new tab (link-external-newwindow);
    # plain Markdown needs an HTML anchor for that
    return "<%s>" % a if FORMAT == "qmd" else '<a href="%s" target="_blank">%s</a>' % (a, a)


def nom_depot():
    """Name of the repository this script belongs to (title of the bundle)."""
    ici = os.path.dirname(os.path.realpath(__file__))
    g = depot_git(ici)
    return g[3] if g else os.path.basename(ici)


LIEN = re.compile(r"(?<!!)(\[[^\]]*\]\()([^)\s]+)((?:\s+\"[^\"]*\")?\))")
LIEN_REF = re.compile(r"^(\s*\[[^\]]+\]:\s*)(\S+)(.*)$")


def lien_github(cible, md):
    """Rewrite a link target of the .md file md to its GitHub address; others stay as they are."""
    if re.match(r"^([a-zA-Z][\w+.-]*:|#|<)", cible):
        return cible
    chemin, _, ancre = cible.partition("#")
    if not chemin:
        return cible
    if not chemin.startswith("/"):
        chemin = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(md)), chemin))
    a = adresse(chemin)
    if not a.startswith("http"):
        return cible
    return a + ("#" + ancre if ancre else "")


def lien_md(m, md):
    cible = lien_github(m.group(2), md)
    if FORMAT != "qmd" and cible.startswith("http"):  # plain Markdown: new tab via an HTML anchor
        return '<a href="%s" target="_blank">%s</a>' % (cible, m.group(1)[1:-2])
    return m.group(1) + cible + m.group(3)


IMAGE = re.compile(r"(!\[[^\]]*\]\()([^)\s]+)((?:\s+\"[^\"]*\")?\))")


def image_locale(cible, md):
    """Relative image of the .md file md: path relative to the output file, so the page
    embeds it offline (Quarto reads absolute paths as project-relative)."""
    if re.match(r"^([a-zA-Z][\w+.-]*:|/|#)", cible):
        return cible
    chemin = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(md)), cible))
    return os.path.relpath(chemin, os.path.dirname(os.path.abspath(SORTIE))) if os.path.exists(chemin) else cible


def liens_github(l, md):
    l = IMAGE.sub(lambda m: m.group(1) + image_locale(m.group(2), md) + m.group(3), l)
    l = LIEN.sub(lambda m: lien_md(m, md), l)
    return LIEN_REF.sub(lambda m: m.group(1) + lien_github(m.group(2), md) + m.group(3), l)


def rendre_md(lignes, md):
    out, dans_bloc, cloture = [], False, ""
    i = 0
    if lignes and lignes[0].strip() == "---":           # YAML front matter: shown as a block, not interpreted
        j = next((k for k in range(1, len(lignes)) if lignes[k].strip() in ("---", "...")), None)
        if j:
            out.append("```yaml"); out.extend(lignes[1:j]); out.append("```")
            i = j + 1
    while i < len(lignes):
        l = lignes[i]; i += 1
        m = re.match(r"^(\s*)(`{3,}|~{3,})(.*)$", l)
        if m and not dans_bloc:
            dans_bloc, cloture = True, m.group(2)
            info = m.group(3).strip()
            if info.startswith("{mermaid}") or info.startswith("mermaid"):
                l = m.group(1) + MERMAID_OUVRE
            elif info.startswith("{"):                     # ```{r ...}: never executed
                l = m.group(1) + m.group(2) + info.strip("{}").split(" ")[0].split(",")[0]
            out.append(l); continue
        if m and dans_bloc and m.group(2)[0] == cloture[0] and len(m.group(2)) >= len(cloture) and not m.group(3).strip():
            dans_bloc = False; out.append(l); continue
        if not dans_bloc:
            l = liens_github(l, md)
            h = re.match(r"^(#{1,6})\s", l)
            if h:
                l = "#" * min(6, len(h.group(1)) + 3) + l[len(h.group(1)):]
            elif l.strip() in ("---", "..."):             # no YAML block nor rule in the middle of the dossier
                l = "* * *"
        out.append(l)
    if dans_bloc:
        out.append(cloture)
    return "\n".join(out)


def main():
    dossiers = sys.argv[1:] or dossiers_par_defaut()
    vus, sections, table = {}, [], []
    total_in = total_out = 0
    # Order: test results (.txt .md .mmd: verdicts, coverage, reports, grafcets)
    # first, then the bench files (.sh .py), the .log last
    CATEGORIES = (("Verdict et couverture", ()), ("Resultats des tests", ("txt", "md", "mmd")),
                  ("Fichiers", ("sh", "py")), ("Logs", ("log",)))
    EN_TETE = ("verdict.txt", "couverture.txt")     # at the very top, in this order
    tous = []
    for d in dossiers:
        if not os.path.isdir(d):
            table.append("| (absent) %s | | | | |" % d)
            continue
        tous.extend(fichiers(d))
    def rang(f):
        if os.path.basename(f) in EN_TETE:
            return 0
        e = f.rsplit(".", 1)[-1].lower()
        for i, (_, exts) in enumerate(CATEGORIES):
            if e in exts:
                return i
        return len(CATEGORIES)
    categorie_courante = None
    def cle(f):
        r = rang(f)
        return (r, EN_TETE.index(os.path.basename(f)) if r == 0 else 0, tous.index(f))
    for f in sorted(tous, key=cle):
        if rang(f) != categorie_courante:
            categorie_courante = rang(f)
            nom = CATEGORIES[categorie_courante][0] if categorie_courante < len(CATEGORIES) else "Autres"
            sections.append("## %s\n" % nom)
        if True:
            try:
                brut = open(f, "rb").read()
            except OSError as e:
                table.append("| %s | | | | illisible : %s |" % (f, e))
                continue
            texte = ANSI.sub("", TELNET.sub(b"", brut).decode("utf-8", "replace")).replace("\r", "")
            texte = CONTROLE.sub("", texte)
            lignes = texte.split("\n")
            if lignes and lignes[-1] == "":
                lignes.pop()
            h = hashlib.sha1(texte.encode()).hexdigest()
            total_in += len(brut)
            if h in vus:
                sections.append("### %s\n\nidentique à %s\n" % (reference(f), reference(vus[h])))
                continue
            vus[h] = f
            ext = f.rsplit(".", 1)[-1].lower()
            if ext in ("md", "mmd"):
                # Rendered, not quoted: .mmd become Mermaid diagrams, .md are
                # included as is (headings demoted under the file title, mermaid
                # blocks rendered, {r}/{python} blocks neutralised). No compaction.
                total_out += len(texte) + 1
                sections.append("### %s\n\n%d octets, %d lignes\n\n%s\n"
                                % (reference(f), len(brut), len(lignes), rendre_md(lignes, f) if ext == "md" else rendre_mmd(lignes)))
                continue
            compact, groupes = compacter(lignes)
            total_out += sum(len(l) + 1 for l in compact)
            note = "%d groupes compactés" % groupes if groupes else ""
            # fence longer than any run of ` in the content: a ``` in a .py or
            # a .md no longer closes the block (pandoc would otherwise read what
            # follows as Markdown: "Could not fetch resource")
            plus_long = max((len(m) for m in re.findall(r"`+", "\n".join(compact))), default=0)
            cloture = "`" * max(3, plus_long + 1)
            sections.append("### %s\n\n%d octets, %d lignes → %d lignes%s\n\n%s%s\n%s\n%s\n"
                            % (reference(f), len(brut), len(lignes), len(compact), (" (%s)" % note) if note else "",
                               cloture, LANG.get(ext, "text"), "\n".join(compact), cloture))
    runs = sorted({os.path.basename(os.path.dirname(f)) for f in tous if os.path.basename(f) == "verdict.txt"})
    with open(SORTIE, "w") as o:
        o.write("---\n")
        o.write("title: \"%s\"\n" % nom_depot())
        o.write("generator: tout-en-un.py\n")
        o.write("subtitle: \"%s\"\n" % (", ".join(runs) if runs else ", ".join(dossiers)))
        o.write("author: \"%s\"\n" % AUTEUR)
        o.write("date: \"%s\"\n" % time.strftime("%Y-%m-%d %H:%M"))
        o.write("lang: fr\n")
        if FORMAT == "qmd":
            o.write("engine: markdown\n")     # pandoc only: knitr would see ```{r} in the included reports and stop
        if FORMAT == "qmd":
            o.write("format:\n  html:\n    toc: true\n    toc-depth: 3\n    toc-location: left\n"
                    "    link-external-newwindow: true\n"
                    "    number-sections: true\n    embed-resources: true\n    theme: cosmo\n"
                    "    code-overflow: wrap\n    fontsize: 0.9em\n")
            # Highlighting wraps every token in a <span>; past ~2.5 MB the DOM is
            # too large for Quarto's HTML post-processing (RangeError: Maximum call
            # stack size exceeded), so large bundles are rendered without it
            if total_out > HTML_SANS_COULEUR:
                o.write("    highlight-style: none\n")
        o.write("---\n\n")
        o.write("## Synthèse\n\n")
        o.write("| run | élément | valeur |\n|---|---|---|\n")
        lignes_syn = synthese(tous)
        o.write("\n".join(lignes_syn) + "\n\n" if lignes_syn else "| (pas de verdict.txt) | | |\n\n")
        o.write("| dossier de run | contenu | |\n|---|---|---|\n")
        o.write("| Verdict et couverture | échelle des barreaux et tableau de couverture couche 1 | |\n")
        o.write("| Résultats des tests | captures VTY, diagnostics, rapports pytest, grafcets (.txt .md .mmd) | |\n")
        o.write("| Fichiers | scripts du banc et des tests (.sh .py) | |\n")
        o.write("| Logs | journaux (.log), en dernier | |\n\n")
        o.write("::: {.callout-note collapse=\"true\"}\n## Méthode\n\n" if FORMAT == "qmd" else "### Méthode\n\n")
        o.write("Dossiers : %s. Extensions : %s. Entrée %d Ko, sortie %d Ko.\n\n"
                % (", ".join(dossiers), " ".join(EXT), total_in // 1024, total_out // 1024))
        o.write("Compactage sans perte : fichiers identiques cités une fois ; lignes consécutives identiques "
                "« ×N » ; lignes ne différant que par des nombres : gabarit avec ⟨k⟩ puis la liste ordonnée "
                "des valeurs (k-uplets), chaque ligne se reconstruit en remettant les valeurs dans l'ordre. "
                "Seuls les codes couleur, la négociation telnet des captures VTY et les caractères de commande "
                "sont retirés. Les .md sont inclus tels quels (titres rétrogradés) et les .mmd rendus en "
                "diagrammes Mermaid, sans compactage.\n")
        o.write(":::\n\n" if FORMAT == "qmd" else "\n")
        for t in table:                      # only absent directories / unreadable files remain
            o.write(t.strip("| ").split(" |")[0] + "\n")
        o.write("\n")
        o.write("\n".join(sections))
    print("%s : %d fichiers (%d uniques), %d Ko -> %d Ko" % (SORTIE, len(tous), len(vus), total_in // 1024,
                                                            os.path.getsize(SORTIE) // 1024))


if __name__ == "__main__":
    main()

3.5 https://github.com/bbaranoff/qosmo/blob/main/opt-gsm-scripts/gsm_sniff.py

5520 octets, 129 lignes → 129 lignes

#!/usr/bin/env python3
# gsm_sniff.py — PASSIVE sniff (raw socket) of the GSMTAP/SCH/TRXD UDP sockets,
# TURNED into decoded SI / decoded bursts. Binds NO port (AF_PACKET) -> steals
# nothing from the real recipients (grgsm/qemu), touches NO fifo.
#
# Usage: gsm_sniff.py si      # GSMTAP -> SI number+type, BCCH/CCCH channel, FN (4729,4730)
#        gsm_sniff.py burst   # SCH (4731) + TRXD v1 (5700-5702/5800-5802) -> burst
import socket, struct, sys, os

MODE = sys.argv[1] if len(sys.argv) > 1 else "si"
IFACE = os.environ.get("SNIFF_IFACE", "lo")
ETH_P_IP = 0x0800
GSMTAP_HDR_LEN = 16                         # GSMTAP v2 header (bytes)

# --- GSM 04.08 / 44.018 RR message types (PD = 0x06), 10.4 -------------------
# key = RR message-type octet (L2[2]); value = (name, is_si, expected DL channel)
RR_MT = {
    0x19: ("SI1",       True,  "BCCH"),
    0x1a: ("SI2",       True,  "BCCH"),
    0x1b: ("SI3",       True,  "BCCH"),
    0x1c: ("SI4",       True,  "BCCH"),
    0x1d: ("SI2bis",    True,  "BCCH"),
    0x1e: ("SI2ter",    True,  "BCCH"),
    0x02: ("SI2quater", True,  "BCCH"),
    0x03: ("SI9",       True,  "BCCH"),
    0x00: ("SI13",      True,  "CCCH"),   # SI13 = mt 0x00 (44.018 tbl 10.4.1)
    # --- CCCH downlink non-SI ---
    0x3f: ("IMM-ASSIGN",     False, "CCCH"),   # Immediate Assignment (AGCH) <- the LU
    0x39: ("IMM-ASSIGN-EXT", False, "CCCH"),
    0x3a: ("IMM-ASSIGN-REJ", False, "CCCH"),
    0x21: ("PAGING-REQ-1",   False, "CCCH"),   # 0x21 = PAGING type 1 (NOT SI13)
    0x22: ("PAGING-REQ-2",   False, "CCCH"),
    0x24: ("PAGING-REQ-3",   False, "CCCH"),
    0x06: ("CHAN-RELEASE",   False, "*"),
    0x2e: ("ASSIGN-CMD",     False, "*"),
    0x10: ("CIPHER-MODE-CMD",False, "*"),
    0x12: ("RR-STATUS",      False, "*"),
}

GSMTAP_CHAN = {0x00:"UNKNOWN",0x01:"BCCH",0x02:"CCCH",0x03:"RACH",0x04:"AGCH",
               0x05:"PCH",0x06:"SDCCH",0x07:"SDCCH4",0x08:"SDCCH8",0x09:"TCH/F",
               0x0a:"TCH/H",0x0b:"PACCH",0x0c:"CBCH52",0x0d:"PDTCH",0x0e:"PTCCH",0x0f:"CBCH51"}
def gsmtap_chan_name(st):
    base = GSMTAP_CHAN.get(st & 0x7f, "0x%02x" % (st & 0x7f))
    return base + ("+ACCH" if (st & 0x80) else "")
def gsmtap_chan_class(st):
    b = st & 0x7f
    if b == 0x01: return "BCCH"
    if b in (0x02,0x04,0x05): return "CCCH"
    if b == 0x03: return "RACH"
    if b in (0x06,0x07,0x08): return "SDCCH"
    return "OTHER"

def fn51_role(fn):
    m = fn % 51
    if m in (0,10,20,30,40): return "FCCH"
    if m in (1,11,21,31,41): return "SCH"
    if m in (2,3,4,5):       return "BCCH"
    if m == 50:              return "idle"
    return "CCCH"
def fn51_class(fn):
    r = fn51_role(fn)
    return r if r in ("BCCH","CCCH") else "—"

if MODE == "si":
    PORTS = {int(p) for p in os.environ.get("SNIFF_PORTS","4729,4730").split(",")}
else:
    PORTS = {int(p) for p in os.environ.get("SNIFF_PORTS","4731,5700,5701,5702,5800,5801,5802").split(",")}

s = socket.socket(socket.AF_PACKET, socket.SOCK_DGRAM, socket.htons(ETH_P_IP))
s.bind((IFACE, 0))
print("[gsm-sniff:%s] iface=%s ports=%s (passif, aucune fifo)"
      % (MODE, IFACE, sorted(PORTS)), flush=True)

def udp_payload(ip):
    if len(ip) < 20 or (ip[0] >> 4) != 4: return None, None
    ihl = (ip[0] & 0x0f) * 4
    if ip[9] != 17: return None, None
    u = ip[ihl:]
    if len(u) < 8: return None, None
    return struct.unpack(">H", u[2:4])[0], u[8:]

def decode_si(pl):
    if len(pl) < GSMTAP_HDR_LEN: return None
    ts  = pl[3]
    fn  = struct.unpack(">I", pl[8:12])[0]     # real frame_number (BE @ offset 8)
    sub = pl[12]                               # GSMTAP sub_type / channel
    l2  = pl[16:]
    if len(l2) >= 3 and l2[1] == 0x06:
        mt = l2[2]
        name, is_si, exp = RR_MT.get(mt, ("RR/mt=0x%02x" % mt, False, "*"))
        name = "%s(0x%02x)" % (name, mt)
    else:
        name, is_si, exp = "?", False, "*"
    tag_cls = gsmtap_chan_class(sub)
    fn_role = fn51_role(fn); fn_cls = fn51_class(fn)
    flags = []
    if tag_cls != fn_cls and fn_cls != "—": flags.append("CHAN!=FN(%s/%s)" % (tag_cls, fn_cls))
    if fn_role in ("FCCH","SCH","idle"):      flags.append("BAD-FN(sur %s)" % fn_role)
    if is_si and exp == "BCCH" and fn_cls == "CCCH": flags.append("SI-sur-CCCH")
    fl = ("  [" + " ".join(flags) + "]") if flags else ""
    return "FN=%d ts%d (%%51=%d %s)  CHAN=%-12s  %-18s  L2: %s%s" % (
        fn, ts, fn % 51, fn_role, gsmtap_chan_name(sub), name, l2[:23].hex(" "), fl)

def decode_burst(dport, pl):
    if dport == 4731 and len(pl) >= 16 and pl[:4] == b"SCH1":
        bsic, fn, toa = struct.unpack("<iii", pl[4:16])
        return "SCH   bsic=%d (ncc=%d bcc=%d)  fn=%d (%%51=%d %s)  toa=%d" % (
            bsic,(bsic>>3)&7,bsic&7,fn,fn%51,fn51_role(fn),toa)
    if dport in (5700,5701,5702,5800,5801,5802) and len(pl) >= 6:
        ver = pl[0] >> 4; tn = pl[0] & 0x07
        fn  = int.from_bytes(pl[1:5], "big")
        if len(pl) >= 6 + 148:
            b = "".join("1" if x else "0" for x in pl[-148:])
            return "TRXDv%d ts%d fn=%d (%%51=%d %s) 148b %s" % (ver,tn,fn,fn%51,fn51_role(fn),b)
        return "TRXDv%d ts%d fn=%d (%%51=%d %s) NOPE(len=%d, burst vide)" % (
            ver,tn,fn,fn%51,fn51_role(fn),len(pl))
    return None

n = 0
while True:
    pkt = s.recv(65535)
    dport, pl = udp_payload(pkt)
    if dport not in PORTS or pl is None:
        continue
    line = decode_si(pl) if MODE == "si" else decode_burst(dport, pl)
    if line:
        n += 1
        print("%6d  :%-4d  %s" % (n, dport, line), flush=True)

3.6 https://github.com/bbaranoff/qosmo/blob/main/run_modules/00-prereqs.sh

862 octets, 29 lignes → 29 lignes

MOD_REGISTER prereqs "Vérification des prérequis"
MOD_REQUIRED[prereqs]=1
MOD_PURE[prereqs]=1

mod_prereqs_check() {
    local missing="" v p
    for v in QEMU_BIN FIRMWARE_ELF FIRMWARE_BIN OSMOCON; do
        p="${!v:-}"
        [ -n "$p" ] && [ -e "$p" ] || missing="$missing $v"
    done
    if [ -n "$missing" ]; then
        mod_hint "réglez les chemins dans environnement/bench.env, puis ./run.sh --check-paths"
        mod_fail "dépendances introuvables :$missing"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_prereqs_start() {
    mod_say "arbre         : ${QEMU_TREE}"
    mod_say "binaire QEMU  : ${QEMU_BIN}"
    mod_say "firmware      : ${FIRMWARE_ELF}"
    mod_say "osmocon       : ${OSMOCON}"
    mod_say "exécution     : ${RUN_DIR}"
    mod_say "journaux      : ${LOG_DIR}"
    mod_ok
}

mod_prereqs_status() { return $MOD_RC_FAIL; }

3.7 https://github.com/bbaranoff/qosmo/blob/main/run_modules/02-outils.sh

2151 octets, 62 lignes → 62 lignes

MOD_REGISTER outils "Outillage de l'hôte"
MOD_REQUIRED[outils]=1
MOD_PURE[outils]=1
MOD_TIMEOUT[outils]=10

: "${OUTILS_REQUIS:=bash sleep pgrep pkill kill stat grep sed awk ip mkfifo rm}"

if ! declare -p OUTILS_OPTIONNELS >/dev/null 2>&1; then
    OUTILS_OPTIONNELS=(
        "ss|sonde des ports d'écoute (repli : /dev/tcp, sans nom de tenant)"
        "tmux|fenêtres de la pile et épilogue « se connecter »"
        "setsid|détachement des processus lancés en arrière-plan"
        "flock|verrou d'unicité du démon audio"
        "socat|passerelles série et sockets de test"
        "nc|alimentation du HLR (repli telnet : fermeture différente, échec silencieux)"
        "python3|outils d'analyse, gr-gsm, transceiver de test"
    )
fi

mod_outils_check() {
    local c manquants=""
    for c in $OUTILS_REQUIS; do
        command -v "$c" >/dev/null 2>&1 || manquants="$manquants $c"
    done
    if [ -n "$manquants" ]; then
        mod_hint "installez-les :  apt-get install -y coreutils procps iproute2 grep sed gawk"
        mod_fail "commandes indispensables absentes du PATH :$manquants"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_outils_status() { return $MOD_RC_FAIL; }

mod_outils_start() {
    local c n d
    for c in $OUTILS_REQUIS; do
        mod_say "requis     $c -> $(command -v "$c" 2>/dev/null)"
    done
    for c in "${OUTILS_OPTIONNELS[@]}"; do
        n="${c%%|*}"; d="${c#*|}"
        if command -v "$n" >/dev/null 2>&1; then
            mod_say "optionnel  $n -> $(command -v "$n")"
        else
            mod_say "ABSENT     $n — dégrade : $d"
        fi
    done
    mod_ok
}

_outils_enfant_resout() {
    bash -c 'command -v ip >/dev/null 2>&1 && command -v pgrep >/dev/null 2>&1 && command -v stat >/dev/null 2>&1'
}

mod_outils_wait() {
    if ! wait_until "${MOD_TIMEOUT[outils]}" "outillage visible d'un enfant" _outils_enfant_resout; then
        mod_hint "PATH courant : $PATH — exportez-le, ou donnez les chemins absolus dans environnement/paths.env"
        mod_fail "un shell enfant ne résout pas l'outillage (PATH non hérité)"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

3.8 https://github.com/bbaranoff/qosmo/blob/main/run_modules/03-rundir.sh

2239 octets, 66 lignes → 66 lignes

MOD_REGISTER rundir "Répertoire d'exécution"
MOD_REQUIRED[rundir]=1
MOD_DEPS[rundir]="profil"
MOD_TIMEOUT[rundir]=15

mod_rundir_check() {
    local d="${RUN_DIR:-}" parent
    [ -n "$d" ] || { mod_hint "RUN_DIR est posé par environnement/paths.env"
                     mod_fail "RUN_DIR non défini"
                     return $MOD_RC_FAIL; }
    if [ -e "$d" ] && [ ! -d "$d" ]; then
        mod_hint "rm -f $d   (ou choisissez un autre RUN_DIR)"
        mod_fail "$d existe et n'est pas un répertoire"
        return $MOD_RC_FAIL
    fi
    if [ ! -d "$d" ]; then
        parent="$(dirname "$d")"
        while [ ! -d "$parent" ] && [ "$parent" != "/" ]; do parent="$(dirname "$parent")"; done
        if [ ! -w "$parent" ]; then
            mod_hint "choisissez un emplacement inscriptible :  RUN_DIR=\$HOME/calypso ./start-oqc.sh"
            mod_fail "impossible de créer $d : $parent n'est pas inscriptible"
            return $MOD_RC_FAIL
        fi
    fi
    mod_ok
}

mod_rundir_status() { [ -d "${RUN_DIR:-}" ] && [ -w "${RUN_DIR:-}" ]; }

mod_rundir_start() {
    mkdir -p "${RUN_DIR}" 2>/dev/null || { mod_fail "création impossible : ${RUN_DIR}"; return $MOD_RC_FAIL; }
    chmod 0755 "${RUN_DIR}" 2>/dev/null
    mod_say "répertoire d'exécution : ${RUN_DIR}"
    local f n=0
    for f in "${RUN_DIR}"/*.pid "${RUN_DIR}"/*.sock; do
        [ -e "$f" ] && { mod_say "reste du run précédent : $f"; n=$((n+1)); }
    done
    [ "$n" -gt 0 ] && mod_say "$n reste(s) — 04-restes puis 10-teardown s'en chargent"
    mod_ok
}

_rundir_ecriture_ok() {
    local t="${RUN_DIR}/.temoin.$$"
    : > "$t" 2>/dev/null || return 1
    rm -f "$t" 2>/dev/null
    return 0
}

mod_rundir_wait() {
    if ! wait_until "${MOD_TIMEOUT[rundir]}" "écriture dans ${RUN_DIR}" _rundir_ecriture_ok; then
        mod_hint "montage en lecture seule ou volume plein :  df -h ${RUN_DIR}"
        mod_fail "${RUN_DIR} existe mais l'écriture y échoue"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_rundir_stop() {
    local f pid
    for f in "${RUN_DIR:-/nonexistent}"/*.pid; do
        [ -f "$f" ] || continue
        pid="$(cat "$f" 2>/dev/null || echo 0)"
        kill -0 "$pid" 2>/dev/null || rm -f "$f"
    done
    return 0
}

3.9 https://github.com/bbaranoff/qosmo/blob/main/run_modules/04-restes.sh

2518 octets, 82 lignes → 82 lignes

MOD_REGISTER restes "Extinction des processus enregistrés"
MOD_REQUIRED[restes]=0
MOD_DEPS[restes]="rundir"
MOD_TIMEOUT[restes]=20
MOD_ENABLED_IF[restes]='[ "${NO_STARTUP_STOP:-0}" != 1 ]'

_res_pids() {
    local f pid
    for f in "${RUN_DIR:-/nonexistent}"/*.pid; do
        [ -f "$f" ] || continue
        pid="$(cat "$f" 2>/dev/null || echo 0)"
        [ "$pid" -gt 0 ] 2>/dev/null || continue
        [ "$pid" = "$$" ] && continue
        kill -0 "$pid" 2>/dev/null && printf '%s\n' "$pid"
    done
}
_res_vide() { [ -z "$(_res_pids)" ]; }

_res_cmdline() { tr '\0' ' ' < "/proc/$1/cmdline" 2>/dev/null | cut -c1-140; }

_res_signal() {
    local sig="$1" p
    for p in $(_res_pids); do
        kill "-$sig" -- "-$p" 2>/dev/null || kill "-$sig" "$p" 2>/dev/null
    done
    return 0
}

_res_menage() {
    local f pid
    for f in "${RUN_DIR:-/nonexistent}"/*.pid; do
        [ -f "$f" ] || continue
        pid="$(cat "$f" 2>/dev/null || echo 0)"
        kill -0 "$pid" 2>/dev/null || { rm -f "$f"; mod_say "fichier PID retiré : $f"; }
    done
    return 0
}

mod_restes_check() {
    command -v kill >/dev/null 2>&1 || { mod_fail "commande kill introuvable"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_restes_status() { _res_vide; }

mod_restes_start() {
    local liste p
    liste="$(_res_pids | tr '\n' ' ')"
    if [ -z "${liste// /}" ]; then
        _res_menage
        mod_already "aucun processus enregistré n'a survécu au run précédent"
        return $MOD_RC_ALREADY
    fi
    for p in $liste; do mod_say "encore vivant : $p  $(_res_cmdline "$p")"; done
    _res_signal TERM
    mod_ok
}

mod_restes_wait() {
    local moitie=$(( ${MOD_TIMEOUT[restes]} / 2 ))
    [ "$moitie" -lt 2 ] && moitie=2

    if ! wait_until "$moitie" "extinction douce" _res_vide; then
        mod_say "toujours vivants après TERM : $(_res_pids | tr '\n' ' ') — passage en KILL"
        _res_signal KILL
        if ! wait_until "$moitie" "extinction forcée" _res_vide; then
            mod_hint "identifiez le tenace :  ps -o pid,ppid,stat,cmd -p \$(cat ${RUN_DIR:-/tmp/calypso}/*.pid | tr '\\n' ',')"
            mod_fail "processus enregistrés impossibles à arrêter : $(_res_pids | tr '\n' ' ')"
            return $MOD_RC_FAIL
        fi
    fi
    _res_menage
    mod_ok
}

mod_restes_stop() {
    _res_vide && { _res_menage; return 0; }
    _res_signal TERM
    wait_until 5 "extinction douce" _res_vide || { _res_signal KILL; wait_until 5 "extinction forcée" _res_vide; }
    _res_menage
    return 0
}

3.10 https://github.com/bbaranoff/qosmo/blob/main/run_modules/05-config.sh

2431 octets, 64 lignes → 64 lignes

MOD_REGISTER config "Résolution des chemins et des ports"
MOD_REQUIRED[config]=1
MOD_DEPS[config]="prereqs"
MOD_PURE[config]=1
MOD_TIMEOUT[config]=5

mod_config_check() {
    if [ ! -d "${QEMU_TREE:-}/run_modules" ]; then
        mod_hint "posez QEMU_TREE sur la racine du dépôt, ou lancez ./run.sh depuis cette racine"
        mod_fail "QEMU_TREE ne désigne pas ce dépôt : ${QEMU_TREE:-<non défini>}"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_config_status() { return $MOD_RC_FAIL; }

mod_config_start() {
    : "${BTS_CFG:=${OSMOCOM_CFG:-/etc/osmocom}/osmo-bts-trx.cfg}"
    : "${MOBILE_CFG_SRC:=${QEMU_CFGS:-$QEMU_TREE/cfgs}/mobile_group1.cfg}"
    : "${MOBILE_CFG:=${OSMOCOM_HOME:-$HOME/.osmocom}/bb/mobile_group1.cfg}"
    : "${QEMU_MON_SOCK:=${RUN_DIR:-/tmp/calypso}/qemu-monitor.sock}"
    : "${L1CTL_SOCK_PATH:=/tmp/osmocom_l2}"
    : "${TMUX_SESSION:=calypso}"
    : "${PORT_TRXD_CLOCK:=5700}"
    : "${PORT_TRXD_CTRL:=5701}"
    : "${PORT_TRXD_DATA:=5702}"
    : "${PORT_GSMTAP:=4729}"
    : "${PORT_GSMTAP_SI:=4730}"
    : "${PORT_GSMTAP_SCH:=4731}"
    mod_say "BTS_CFG          = $BTS_CFG"
    mod_say "MOBILE_CFG_SRC   = $MOBILE_CFG_SRC"
    mod_say "MOBILE_CFG       = $MOBILE_CFG"
    mod_say "QEMU_MON_SOCK    = $QEMU_MON_SOCK"
    mod_say "L1CTL_SOCK_PATH  = $L1CTL_SOCK_PATH (non exportée)"
    mod_say "ports            : TRXD $PORT_TRXD_CLOCK-$PORT_TRXD_DATA (pont) · GSMTAP $PORT_GSMTAP · QEMU $PORT_GSMTAP_SI/$PORT_GSMTAP_SCH"
    local f
    for f in "$BTS_CFG" "$MOBILE_CFG_SRC"; do
        [ -r "$f" ] && mod_say "lisible  : $f" || mod_say "ABSENT   : $f (le module qui l'utilise le signalera)"
    done
    mod_ok
}

_config_vars() {
    printf '%s\n' BTS_CFG MOBILE_CFG MOBILE_CFG_SRC QEMU_MON_SOCK L1CTL_SOCK_PATH TMUX_SESSION \
                  PORT_TRXD_CLOCK PORT_GSMTAP PORT_GSMTAP_SI PORT_GSMTAP_SCH
}
_config_missing() {
    local v out=""
    while read -r v; do [ -n "${!v:-}" ] || out="$out $v"; done < <(_config_vars)
    printf '%s' "${out# }"
}
_config_all_set() { [ -z "$(_config_missing)" ]; }

mod_config_wait() {
    if ! wait_until "${MOD_TIMEOUT[config]}" "variables de configuration" _config_all_set; then
        mod_hint "vérifiez environnement/bench.env (QEMU_TREE, OSMOCOM_CFG, OSMOCOM_HOME, RUN_DIR)"
        mod_fail "variables de configuration vides : $(_config_missing)"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_config_stop() { return 0; }

3.11 https://github.com/bbaranoff/qosmo/blob/main/run_modules/08-gabarits.sh

5374 octets, 130 lignes → 130 lignes

MOD_REGISTER gabarits "Gabarits de configuration du cœur"
MOD_REQUIRED[gabarits]=0
MOD_DEPS[gabarits]="config"
MOD_TIMEOUT[gabarits]=20

MOD_ENABLED_IF[gabarits]='[ "${NO_OSMO_START:-0}" != 1 ] && [ "${NO_GABARITS:-0}" != 1 ]'

: "${EGPRS_DIR:=${NITB_ROOT}}"
: "${EGPRS_LIB:=$EGPRS_DIR/lib/gabarits.sh}"
: "${OPERATOR_ID:=1}"
: "${MCC:=001}"
: "${MNC:=01}"
: "${OPERATOR_NAME:=OsmoDirect}"
: "${ENCRYPTION:=a5 0}"

_gab_installes() {
    printf '%s\n' "${OSMOCOM_CFG:-/etc/osmocom}/osmo-bsc.cfg" \
                  "${OSMOCOM_CFG:-/etc/osmocom}/osmo-msc.cfg"
}

mod_gabarits_check() {
    if [ ! -d "$EGPRS_DIR/configs" ]; then
        mod_hint "posez EGPRS_DIR=<racine osmo_egprs> si l'arborescence a bougé"
        mod_skip "osmo_egprs introuvable ($EGPRS_DIR) : les gabarits ne sont pas de ce dépôt"
        return $MOD_RC_SKIP
    fi
    if [ ! -r "$EGPRS_LIB" ]; then
        mod_hint "la bibliothèque est l'extraction de start-direct.sh.legacy L174-258 ; elle doit accompagner osmo_egprs"
        mod_fail "bibliothèque de gabarits absente : $EGPRS_LIB"
        return $MOD_RC_FAIL
    fi
    if [ "$(id -u)" -ne 0 ]; then
        mod_fail "root requis pour écrire dans ${OSMOCOM_CFG:-/etc/osmocom} et /etc/asterisk"
        return $MOD_RC_FAIL
    fi
    if [ "${N_OPERATORS:-1}" -gt 1 ]; then
        mod_hint "le multi-opérateur pose une configuration par netns ; aucun module du cœur ne démarre encore dans un netns"
        mod_fail "N_OPERATORS=${N_OPERATORS} : ce module ne sait poser que le mono-opérateur (cf. POURQUOI 4)"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_gabarits_status() { return $MOD_RC_FAIL; }

mod_gabarits_start() {
    local retour="$PWD" rc=0
    cd "$EGPRS_DIR" || { mod_fail "impossible d'entrer dans $EGPRS_DIR"; return $MOD_RC_FAIL; }

    . "$EGPRS_LIB" || { cd "$retour"; mod_fail "bibliothèque illisible : $EGPRS_LIB"; return $MOD_RC_FAIL; }

    local tmpdir; tmpdir="$(mktemp -d)" || { cd "$retour"; mod_fail "mktemp -d a échoué"; return $MOD_RC_FAIL; }

    local cip gw
    cip="$(op_private_ip "$OPERATOR_ID")"
    gw="$(op_private_gw "$OPERATOR_ID")"

    mod_say "opérateur $OPERATOR_ID · MCC/MNC ${MCC}/${MNC} · chiffrement « ${ENCRYPTION} »"
    mod_say "gabarits  : $EGPRS_DIR/configs -> ${OSMOCOM_CFG:-/etc/osmocom}, /etc/asterisk, $HOME/.osmocom/bb"

    local _inter_shutdown="shutdown" _multi="/etc/osmocom/osmo-multi.conf"
    if [ -r "$_multi" ] && grep -q '^MULTI_HUB_IP=' "$_multi" 2>/dev/null; then
        _inter_shutdown=""
        mod_say "inter-STP déclaré (osmo-multi.conf) : ASP as-inter laissé ACTIF"
    fi
    if apply_config_templates "$tmpdir" "$cip" "$gw" "$OPERATOR_ID" \
            "1.1.1" "1.1.2" "1.1.3" "$MCC" "$MNC" "$OPERATOR_NAME" \
            "${INTER_STP_IP:-127.0.0.1}" "$_inter_shutdown" "1"; then
        install_configs_native "$tmpdir" "" || rc=1
        # The templates overwrite pjsip/extensions/rtp/sms-routing: without WAN
        # in those files, no call to another node (503 on the SIP side). The mesh
        # is replayed AFTER them, if the WAN table has several nodes.
        _wan_rejoue() {
            local t="${WAN_CONF_FILE:-/etc/osmo-wan.conf}"
            local sh="${WAN_MESH_SH:-/opt/GSM/osmo-operator/network/setup-wan-mesh.sh}"
            local n
            [ -r "$t" ] && [ -r "$sh" ] || return 0
            n="$(sed -n 's/^WAN_NODE_COUNT=//p' "$t" | tail -1)"
            [ "${n:-1}" -ge 2 ] 2>/dev/null || return 0
            if WAN_AUTO=1 bash "$sh" --native --config "$t" --no-restart </dev/null >/dev/null 2>&1; then
                mod_say "WAN rejoue apres les gabarits (table $t, $n noeuds)"
            else
                mod_say "WAN NON rejoue apres les gabarits (voir $sh)"
                return 1
            fi
        }
        _wan_rejoue || rc=1
    else
        rc=1
    fi

    rm -rf "$tmpdir"
    cd "$retour"
    [ $rc -eq 0 ] || { mod_fail "la pose des gabarits a échoué (détail dans le journal du module)"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_gabarits_wait() {
    local f restants=""
    while IFS= read -r f; do
        if [ ! -f "$f" ]; then
            mod_hint "vérifiez que $EGPRS_DIR/configs contient bien le gabarit correspondant"
            mod_fail "configuration non installée : $f"
            return $MOD_RC_FAIL
        fi
        if grep -qE '__[A-Z0-9_]+__' "$f" 2>/dev/null; then
            restants="$restants $(basename "$f"):$(grep -oE '__[A-Z0-9_]+__' "$f" | sort -u | tr '\n' ',' )"
        fi
    done < <(_gab_installes)

    if [ -n "$restants" ]; then
        mod_hint "ces jetons n'ont pas de valeur dans apply_config_templates (lib/gabarits.sh)"
        mod_fail "jetons non substitués :$restants"
        return $MOD_RC_FAIL
    fi

    local cfg
    for cfg in "${OSMOCOM_CFG:-/etc/osmocom}/osmo-bsc.cfg" \
               "${OSMOCOM_CFG:-/etc/osmocom}/osmo-msc.cfg"; do
        if grep -q '__ENCRYPTION__' "$cfg" 2>/dev/null || ! grep -qF "$ENCRYPTION" "$cfg" 2>/dev/null; then
            mod_hint "attendu « $ENCRYPTION » dans $cfg ; vérifiez que le gabarit porte bien __ENCRYPTION__"
            mod_fail "le chiffrement demandé n'est pas dans la configuration installée ($cfg)"
            return $MOD_RC_FAIL
        fi
        mod_say "chiffrement « $ENCRYPTION » confirmé dans $cfg"
    done
    mod_ok
}

mod_gabarits_stop() { return 0; }

3.12 https://github.com/bbaranoff/qosmo/blob/main/run_modules/09-logs.sh

3037 octets, 81 lignes → 81 lignes

MOD_REGISTER logs "Journaux et espace de travail"
MOD_REQUIRED[logs]=1
MOD_DEPS[logs]="config"
MOD_TIMEOUT[logs]=10

: "${LOG_ARCHIVES_KEEP:=10}"
: "${OSMO_LOG_DIR:=/var/log/osmocom}"
: "${OSMO_LOG_USER:=osmocom}"

mod_logs_check() {
    local d="${LOG_DIR:-}" p
    [ -n "$d" ] || { mod_fail "LOG_DIR non défini"; return $MOD_RC_FAIL; }
    p="$d"
    while [ ! -d "$p" ] && [ "$p" != "/" ] && [ "$p" != "." ]; do p="$(dirname "$p")"; done
    if [ ! -w "$p" ]; then
        mod_hint "posez LOG_DIR sur un chemin inscriptible (environnement/bench.env, LOG_DIR)"
        mod_fail "aucun parent inscriptible pour LOG_DIR=$d (bloqué à $p)"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_logs_status() { return $MOD_RC_FAIL; }

mod_logs_start() {
    local _n _cible _lien _faits=0
    if [ "${CALYPSO_LIENS_ROOT:-1}" = 1 ] && [ -w /root ]; then
        for _n in qemu osmocon mobile bts sidecar-mobile; do
            _cible="$LOG_DIR/$_n.log"; _lien="/root/$_n.log"
            if [ -e "$_lien" ] && [ ! -L "$_lien" ]; then
                mod_say "⚠ /root/$_n.log est un vrai fichier — laissé en place"
                continue
            fi
            ln -sfn "$_cible" "$_lien" 2>/dev/null && _faits=$(( _faits + 1 ))
        done
        [ "$_faits" -gt 0 ] && mod_say "$_faits liens de compatibilité dans /root (→ $LOG_DIR)"
    fi
    [ -d "$OSMO_LOG_DIR" ] || mkdir -p "$OSMO_LOG_DIR" 2>/dev/null || true
    if id "$OSMO_LOG_USER" >/dev/null 2>&1; then
        chown "$OSMO_LOG_USER":"$OSMO_LOG_USER" "$OSMO_LOG_DIR" 2>/dev/null || true
    fi
    if [ -d "$OSMO_LOG_DIR" ]; then
        mod_say "journaux des services : $OSMO_LOG_DIR (propriétaire $OSMO_LOG_USER)"
    else
        mod_say "⚠ $OSMO_LOG_DIR absent — les services n'écriront que sur journald"
    fi
    mkdir -p "$LOG_DIR" "$LOG_DIR/mod" "${RUN_DIR:-/tmp/calypso}" "${CAPTURE_DIR:-$RUN_DIR/captures}" 2>/dev/null
    local osmocon_log="$LOG_DIR/osmocon.log"
    if [ -s "$osmocon_log" ]; then
        local arch="${osmocon_log%.log}.$(date +%Y%m%d_%H%M%S).log"
        if mv "$osmocon_log" "$arch" 2>/dev/null; then
            mod_say "osmocon.log du run précédent archivé -> $arch"
        fi
    fi
    ls -1t "${osmocon_log%.log}".[0-9]*.log 2>/dev/null \
        | tail -n +$((LOG_ARCHIVES_KEEP + 1)) | xargs -r rm -f 2>/dev/null || true
    for _n in qemu osmocon mobile bts; do
        : > "$LOG_DIR/$_n.log" 2>/dev/null
    done
    mod_say "LOG_DIR    = $LOG_DIR"
    mod_say "RUN_DIR    = ${RUN_DIR:-/tmp/calypso}"
    mod_ok
}

_logs_writable() {
    local probe="$LOG_DIR/.probe.$$"
    : > "$probe" 2>/dev/null || return 1
    rm -f "$probe" 2>/dev/null
    return 0
}

mod_logs_wait() {
    if ! wait_until "${MOD_TIMEOUT[logs]}" "espace de journaux" _logs_writable; then
        mod_hint "df -h $LOG_DIR — volume plein ou monté en lecture seule ?"
        mod_fail "écriture impossible dans $LOG_DIR (test réel, pas un test de droits)"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_logs_stop() { return 0; }

3.13 https://github.com/bbaranoff/qosmo/blob/main/run_modules/09-teardown.sh

4618 octets, 147 lignes → 147 lignes

MOD_REGISTER teardown "Nettoyage du run précédent"
MOD_REQUIRED[teardown]=1
MOD_DEPS[teardown]="logs"
MOD_TIMEOUT[teardown]=25

_td_patterns() {
    cat <<'EOF'
f:qemu-system-arm
f:gardien-bts
x:osmo-bts-trx
x:osmocon
x:mobile
x:trxcon
f:pont/pont.py
f:pont/pont_dsp.py
f:tools/injecteur_bruit.py
EOF
}

_td_port_busy() {
    ss -ln 2>/dev/null | grep -qE "[:.]${1}[[:space:]]"
}

_td_ports() {
    printf '%s\n' "${PORT_TRXD_CLOCK:-5700}" "${PORT_TRXD_CTRL:-5701}" "${PORT_TRXD_DATA:-5702}" \
                  "${PORT_GSMTAP_SI:-4730}" "${PORT_GSMTAP_SCH:-4731}"
}

_td_sockets() {
    printf '%s\n' "${L1CTL_SOCK_PATH:-/tmp/osmocom_l2}" \
                  "${SC_L2_SOCK:-/tmp/ms2_l2}" \
                  "${QEMU_MON_SOCK:-${RUN_DIR:-/tmp/calypso}/qemu-monitor.sock}"
}

_td_live() {
    local _p _st
    for _p in $(pgrep "$1" "$2" 2>/dev/null); do
        _st="$(ps -o stat= -p "$_p" 2>/dev/null | tr -d ' ')"
        case "$_st" in
            ''|Z*) continue ;;
            *)     return 0 ;;
        esac
    done
    return 1
}

_td_leftovers() {
    local line kind pat out="" p
    while IFS= read -r line; do
        kind="${line%%:*}"; pat="${line#*:}"
        case "$kind" in
            x) _td_live -x "$pat" && out="$out $pat" ;;
            f) _td_live -f "$pat" && out="$out $pat" ;;
        esac
    done < <(_td_patterns)
    while IFS= read -r p; do
        _td_port_busy "$p" && out="$out port:$p"
    done < <(_td_ports)
    while IFS= read -r p; do
        [ -e "$p" ] && out="$out socket:$p"
    done < <(_td_sockets)
    printf '%s' "${out# }"
}
_td_clean() { [ -z "$(_td_leftovers)" ]; }

mod_teardown_check() {
    local t
    for t in pgrep pkill; do
        command -v "$t" >/dev/null 2>&1 || {
            mod_hint "installez procps (paquet procps / procps-ng)"
            mod_fail "$t introuvable : impossible de garantir une machine propre"
            return $MOD_RC_FAIL
        }
    done
    command -v ss >/dev/null 2>&1 || mod_say "ss absent : la libération des ports UDP ne sera pas vérifiée"
    mod_ok
}

mod_teardown_status() { _td_clean; }

_td_kill_all() {
    local line kind pat
    while IFS= read -r line; do
        kind="${line%%:*}"; pat="${line#*:}"
        case "$kind" in
            x) _td_live -x "$pat" && { mod_say "kill -x $pat"; pkill -9 -x "$pat" 2>/dev/null; } ;;
            f) _td_live -f "$pat" && { mod_say "kill -f $pat"; pkill -9 -f "$pat" 2>/dev/null; } ;;
        esac
    done < <(_td_patterns)
    return 0
}

_td_kill_ports() {
    command -v ss >/dev/null 2>&1 || { mod_say "ss absent : passe par port impossible"; return 0; }
    local p pid cmd busy=0 named=0
    while IFS= read -r p; do
        _td_port_busy "$p" || continue
        busy=1
        for pid in $(ss -lnp "sport = :$p" 2>/dev/null \
                     | grep -oE 'pid=[0-9]+' | cut -d= -f2 | sort -u); do
            named=1
            [ "$pid" -gt 1 ] 2>/dev/null || continue
            [ "$pid" = "$$" ] && continue
            cmd=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null)
            mod_say "port $p tenu par PID $pid (${cmd:-cmdline illisible}) — kill -9"
            kill -9 "$pid" 2>/dev/null
        done
    done < <(_td_ports)
    if [ "$busy" = 1 ] && [ "$named" = 0 ]; then
        mod_say "ports occupes mais ss n'a rendu aucun pid= (ss sans -p, ou proprietaire hors de notre namespace) — inspectez : ss -lnp"
    fi
    return 0
}

mod_teardown_start() {
    if [ -n "${TMUX:-}" ]; then
        mod_say "lancé depuis tmux : kill-server évité, on ne touche qu'à la session ${TMUX_SESSION:-calypso}"
        tmux kill-session -t "${TMUX_SESSION:-calypso}" 2>/dev/null
    else
        tmux kill-server 2>/dev/null
    fi
    _td_kill_all
    local p
    while IFS= read -r p; do rm -f "$p" 2>/dev/null; done < <(_td_sockets)
    rm -f /dev/shm/calypso_kc /dev/shm/calypso_kc_l1 2>/dev/null
    rm -f "${RUN_DIR:-/tmp/calypso}/qemu.pid" 2>/dev/null
    mod_ok
}

mod_teardown_wait() {
    local half=$(( ${MOD_TIMEOUT[teardown]} / 2 ))
    if wait_until "$half" "machine propre" _td_clean; then mod_ok; return $MOD_RC_OK; fi
    mod_say "restes après $half s : $(_td_leftovers) — deuxième passe"
    _td_kill_all
    _td_kill_ports
    if wait_until "$half" "machine propre" _td_clean; then mod_ok; return $MOD_RC_OK; fi
    mod_hint "identifiez le tenace : pgrep -a -f <motif> ; ss -lnp | grep <port>"
    mod_fail "restes du run précédent : $(_td_leftovers)"
    return $MOD_RC_FAIL
}

mod_teardown_stop() {
    _td_kill_all
    local p
    while IFS= read -r p; do rm -f "$p" 2>/dev/null; done < <(_td_sockets)
    return 0
}

3.14 https://github.com/bbaranoff/qosmo/blob/main/run_modules/10-reseau.sh

4158 octets, 108 lignes → 108 lignes

MOD_REGISTER reseau "Interfaces et plan d'adressage"
MOD_REQUIRED[reseau]=1
MOD_DEPS[reseau]="rundir"
MOD_TIMEOUT[reseau]=15

: "${RESEAU_PORTS_LIBRES:=${PORT_GSMTAP_SI:-4730} ${PORT_GSMTAP_SCH:-4731}}"
: "${NEED_TUN:=0}"

_net_tenant() {
    local port="$1" out=""
    if command -v ss >/dev/null 2>&1; then
        out="$(ss -lntp "sport = :$port" 2>/dev/null | awk 'NR>1{print $0}')"
    fi
    [ -n "$out" ] && printf '%s' "$out" || printf 'tenant inconnu (ss absent ou sans droits)'
}
_net_lo_up() { ip link show lo 2>/dev/null | grep -q 'state UNKNOWN\|state UP\|<.*UP'; }
_net_route_loopback() { ip route get 127.0.0.2 >/dev/null 2>&1; }
_net_port_occupe() {
    if command -v ss >/dev/null 2>&1; then
        ss -ln 2>/dev/null | grep -qE "[:.]${1}[[:space:]]" && return 0
        return 1
    fi
    have_port "$1"
}
_net_ports_libres() {
    local p
    for p in $RESEAU_PORTS_LIBRES; do
        _net_port_occupe "$p" && return 1
    done
    return 0
}
_net_tun_ok() { [ -c /dev/net/tun ]; }

mod_reseau_check() {
    command -v ip >/dev/null 2>&1 || {
        mod_hint "apt-get install -y iproute2"
        mod_fail "commande « ip » absente : le réseau ne peut pas être constaté"
        return $MOD_RC_FAIL; }
    local p occupes=""
    for p in $RESEAU_PORTS_LIBRES; do
        _net_port_occupe "$p" && occupes="$occupes $p"
    done
    if [ -n "$occupes" ]; then
        for p in $occupes; do mod_say "port $p occupé : $(_net_tenant "$p")"; done
        mod_hint "arrêtez le run précédent :  ./run.sh --stop   (le module teardown balaie au démarrage)"
        mod_fail "ports déjà occupés :$occupes — le plan ne pourra pas les ouvrir"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_reseau_status() {
    _net_lo_up || return $MOD_RC_FAIL
    _net_route_loopback || return $MOD_RC_FAIL
    if [ "${NEED_TUN}" = 1 ]; then _net_tun_ok || return $MOD_RC_FAIL; fi
    return $MOD_RC_OK
}

mod_reseau_start() {
    _net_lo_up || ip link set lo up 2>/dev/null
    if ! _net_tun_ok; then
        if [ "$(id -u)" != 0 ]; then
            mod_say "/dev/net/tun absent et nous ne sommes pas root : création impossible"
        else
            modprobe tun 2>/dev/null
            if ! _net_tun_ok; then
                mkdir -p /dev/net 2>/dev/null
                mknod /dev/net/tun c 10 200 2>/dev/null && chmod 0666 /dev/net/tun 2>/dev/null
            fi
        fi
        if _net_tun_ok; then
            mod_say "/dev/net/tun : créé"
        elif [ "${NEED_TUN}" = 1 ]; then
            mod_hint "relancez le conteneur avec --device /dev/net/tun (ou --privileged), ou posez NEED_TUN=0"
            mod_fail "/dev/net/tun indisponible alors que NEED_TUN=1"
            return $MOD_RC_FAIL
        else
            mod_say "/dev/net/tun indisponible — sans effet (NEED_TUN=0)"
        fi
    fi
    mod_say "lo        : $(ip -o -4 addr show dev lo 2>/dev/null | awk '{print $4}' | tr '\n' ' ')"
    mod_say "ports     : $RESEAU_PORTS_LIBRES (attendus libres)"
    mod_ok
}

mod_reseau_wait() {
    wait_until "${MOD_TIMEOUT[reseau]}" "interface lo active" _net_lo_up || {
        mod_hint "ip link set lo up"
        mod_fail "la boucle locale n'est pas active"
        return $MOD_RC_FAIL; }
    wait_until "${MOD_TIMEOUT[reseau]}" "route vers 127.0.0.2" _net_route_loopback || {
        mod_hint "la pile écrit en dur sur 127.0.0.2 (HLR, contrôle BTS) : vérifiez  ip route get 127.0.0.2"
        mod_fail "127.0.0.2 n'est pas routable"
        return $MOD_RC_FAIL; }
    wait_until "${MOD_TIMEOUT[reseau]}" "libération des ports" _net_ports_libres || {
        mod_hint "un processus tient encore un port :  ss -lnp | grep -E '$(printf '%s' "$RESEAU_PORTS_LIBRES" | tr ' ' '|')'"
        mod_fail "ports toujours occupés parmi : $RESEAU_PORTS_LIBRES"
        return $MOD_RC_FAIL; }
    if [ "${NEED_TUN}" = 1 ]; then
        wait_until "${MOD_TIMEOUT[reseau]}" "périphérique TUN" _net_tun_ok || {
            mod_hint "--device /dev/net/tun au lancement du conteneur"
            mod_fail "/dev/net/tun absent (NEED_TUN=1)"
            return $MOD_RC_FAIL; }
    fi
    mod_ok
}

mod_reseau_stop() { return 0; }

3.15 https://github.com/bbaranoff/qosmo/blob/main/run_modules/10-stp.sh

2475 octets, 61 lignes → 61 lignes

: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER stp "Cœur — OsmoSTP (SS7/M3UA)"
MOD_REQUIRED[stp]=1
MOD_JOURNAL[stp]="osmo-stp"
MOD_TIMEOUT[stp]=25
MOD_ENABLED_IF[stp]='[ "${NO_OSMO_START:-0}" != 1 ]'

: "${STP_UNIT:=osmo-stp}"
: "${STP_VTY_PORT:=4239}"

_stp_cfg()  { core_cfg osmo-stp; }
_stp_m3ua() { core_cfg_field "$(_stp_cfg)" '^[[:space:]]*listen[[:space:]]+m3ua[[:space:]]' 3 2905; }

mod_stp_check() {
    core_bin osmo-stp >/dev/null || {
        mod_hint "paquet osmo-stp absent — vérifiez l'image, ou réglez OSMOCOM_CFG/PATH"
        mod_fail "binaire osmo-stp introuvable"; return $MOD_RC_FAIL; }
    [ -r "$(_stp_cfg)" ] || {
        mod_hint "déployez la configuration : cp configs/osmo-stp.cfg $OSMOCOM_CFG/"
        mod_fail "configuration illisible : $(_stp_cfg)"; return $MOD_RC_FAIL; }
    if [ ! -e /proc/net/sctp/snmp ] && ! lsmod 2>/dev/null | grep -q '^sctp'; then
        mod_hint "chargez le module côté hôte : modprobe sctp (un conteneur ne peut pas le faire)"
        mod_fail "SCTP indisponible dans ce noyau — l'écoute M3UA est impossible"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_stp_status() { core_unit_active "$STP_UNIT" || core_vty_listen "$STP_VTY_PORT"; }

mod_stp_start() {
    core_svc_start "$STP_UNIT" "$(core_bin osmo-stp)" -c "$(_stp_cfg)" \
        || { mod_fail "systemctl start $STP_UNIT a échoué"
             mod_hint "journalctl -u $STP_UNIT -n 30"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_stp_wait() {
    local to="${MOD_TIMEOUT[stp]}" m3ua; m3ua="$(_stp_m3ua)"

    if ! wait_until "$to" "VTY OsmoSTP ($STP_VTY_PORT)" core_vty_listen "$STP_VTY_PORT"; then
        mod_hint "journalctl -u $STP_UNIT -n 30 ; port $STP_VTY_PORT déjà pris ?"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "$to" "écoute M3UA SCTP ($m3ua)" core_sctp_listen "$m3ua"; then
        mod_hint "vérifiez « listen m3ua $m3ua » dans $(_stp_cfg) et le support SCTP (ss -ln --sctp)"
        return $MOD_RC_FAIL
    fi
    if core_restarted_since "$STP_UNIT"; then
        mod_hint "journalctl -u $STP_UNIT -n 50 : le service redémarre en boucle"
        mod_fail "OsmoSTP a redémarré depuis le lancement"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

# Exact pattern: "osmo-stp -c" alone would also kill the hub
# (osmo-stp-interop.cfg), started by start-native.sh --wan on node 1.
mod_stp_stop() { core_svc_stop "$STP_UNIT" "osmo-stp -c $(_stp_cfg)"; }

3.16 https://github.com/bbaranoff/qosmo/blob/main/run_modules/11-hlr.sh

2466 octets, 62 lignes → 62 lignes

: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER hlr "Cœur — OsmoHLR (registre GSUP)"
MOD_REQUIRED[hlr]=1
MOD_JOURNAL[hlr]="osmo-hlr"
MOD_TIMEOUT[hlr]=30
MOD_ENABLED_IF[hlr]='[ "${NO_OSMO_START:-0}" != 1 ]'

: "${HLR_UNIT:=osmo-hlr}"
: "${HLR_VTY_PORT:=4258}"
: "${HLR_GSUP_PORT:=4222}"
: "${HLR_DB:=/var/lib/osmocom/hlr.db}"

_hlr_cfg()     { core_cfg osmo-hlr; }
_hlr_gsup_ip() { core_cfg_field "$(_hlr_cfg)" '^[[:space:]]*bind[[:space:]]+ip[[:space:]]' 3 127.0.0.2; }

mod_hlr_check() {
    core_bin osmo-hlr >/dev/null || {
        mod_fail "binaire osmo-hlr introuvable"; return $MOD_RC_FAIL; }
    [ -r "$(_hlr_cfg)" ] || {
        mod_hint "déployez la configuration : cp configs/osmo-hlr.cfg $OSMOCOM_CFG/"
        mod_fail "configuration illisible : $(_hlr_cfg)"; return $MOD_RC_FAIL; }
    local dbdir; dbdir="$(dirname "$HLR_DB")"
    [ -d "$dbdir" ] || {
        mod_hint "mkdir -p $dbdir && chown osmocom:osmocom $dbdir"
        mod_fail "répertoire de base absent : $dbdir"; return $MOD_RC_FAIL; }
    core_ip_local "$(_hlr_gsup_ip)" || {
        mod_hint "ip addr add $(_hlr_gsup_ip)/24 dev lo, ou changez « gsup / bind ip » dans $(_hlr_cfg)"
        mod_fail "adresse GSUP $(_hlr_gsup_ip) portée par aucune interface"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_hlr_status() { core_unit_active "$HLR_UNIT" || core_vty_listen "$HLR_VTY_PORT"; }

mod_hlr_start() {
    core_svc_start "$HLR_UNIT" "$(core_bin osmo-hlr)" -c "$(_hlr_cfg)" -l "$HLR_DB" \
        || { mod_fail "systemctl start $HLR_UNIT a échoué"
             mod_hint "journalctl -u $HLR_UNIT -n 30"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_hlr_wait() {
    local to="${MOD_TIMEOUT[hlr]}" gip; gip="$(_hlr_gsup_ip)"

    if ! wait_until "$to" "VTY OsmoHLR ($HLR_VTY_PORT)" core_vty_listen "$HLR_VTY_PORT"; then
        mod_hint "journalctl -u $HLR_UNIT -n 30"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "$to" "GSUP ($gip:$HLR_GSUP_PORT)" core_tcp_listen "$HLR_GSUP_PORT" "$gip"; then
        mod_hint "base verrouillée ou illisible ? ls -l $HLR_DB ; journalctl -u $HLR_UNIT -n 30"
        return $MOD_RC_FAIL
    fi
    if core_restarted_since "$HLR_UNIT"; then
        mod_hint "journalctl -u $HLR_UNIT -n 50 : le service redémarre en boucle"
        mod_fail "OsmoHLR a redémarré depuis le lancement"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_hlr_stop() { core_svc_stop "$HLR_UNIT" "osmo-hlr -c"; }

3.17 https://github.com/bbaranoff/qosmo/blob/main/run_modules/12-mgw.sh

2227 octets, 57 lignes → 57 lignes

: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER mgw "Cœur — OsmoMGW (média MGCP/RTP)"
MOD_REQUIRED[mgw]=1
MOD_JOURNAL[mgw]="osmo-mgw"
MOD_TIMEOUT[mgw]=25
MOD_ENABLED_IF[mgw]='[ "${NO_OSMO_START:-0}" != 1 ]'

: "${MGW_UNIT:=osmo-mgw}"
: "${MGW_VTY_PORT:=4243}"

_mgw_cfg()  { core_cfg osmo-mgw; }
_mgw_ip()   { core_cfg_field "$(_mgw_cfg)" '^[[:space:]]*bind[[:space:]]+ip[[:space:]]'   3 127.0.0.1; }
_mgw_port() { core_cfg_field "$(_mgw_cfg)" '^[[:space:]]*bind[[:space:]]+port[[:space:]]' 3 2427; }

mod_mgw_check() {
    core_bin osmo-mgw >/dev/null || { mod_fail "binaire osmo-mgw introuvable"; return $MOD_RC_FAIL; }
    [ -r "$(_mgw_cfg)" ] || {
        mod_hint "déployez la configuration : cp configs/osmo-mgw.cfg $OSMOCOM_CFG/"
        mod_fail "configuration illisible : $(_mgw_cfg)"; return $MOD_RC_FAIL; }
    core_ip_local "$(_mgw_ip)" || {
        mod_hint "corrigez « mgcp / bind ip » dans $(_mgw_cfg), ou ajoutez l'adresse à une interface"
        mod_fail "adresse MGCP $(_mgw_ip) portée par aucune interface"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_mgw_status() { core_unit_active "$MGW_UNIT" || core_vty_listen "$MGW_VTY_PORT"; }

mod_mgw_start() {
    core_svc_start "$MGW_UNIT" "$(core_bin osmo-mgw)" -s -c "$(_mgw_cfg)" \
        || { mod_fail "systemctl start $MGW_UNIT a échoué"
             mod_hint "journalctl -u $MGW_UNIT -n 30"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_mgw_wait() {
    local to="${MOD_TIMEOUT[mgw]}" ip port
    ip="$(_mgw_ip)"; port="$(_mgw_port)"

    if ! wait_until "$to" "VTY OsmoMGW ($MGW_VTY_PORT)" core_vty_listen "$MGW_VTY_PORT"; then
        mod_hint "journalctl -u $MGW_UNIT -n 30"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "$to" "MGCP UDP ($ip:$port)" core_udp_listen "$port" "$ip"; then
        mod_hint "port $port déjà pris ? ss -lun | grep $port"
        return $MOD_RC_FAIL
    fi
    if core_restarted_since "$MGW_UNIT"; then
        mod_hint "journalctl -u $MGW_UNIT -n 50 : le service redémarre en boucle"
        mod_fail "OsmoMGW a redémarré depuis le lancement"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_mgw_stop() { core_svc_stop "$MGW_UNIT" "osmo-mgw -s -c"; }

3.18 https://github.com/bbaranoff/qosmo/blob/main/run_modules/13-msc.sh

2516 octets, 65 lignes → 65 lignes

: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER msc "Cœur — OsmoMSC (commutation CS)"
MOD_REQUIRED[msc]=1
MOD_DEPS[msc]="stp hlr mgw"
MOD_JOURNAL[msc]="osmo-msc"
MOD_TIMEOUT[msc]=40
MOD_ENABLED_IF[msc]='[ "${NO_OSMO_START:-0}" != 1 ]'

: "${MSC_UNIT:=osmo-msc}"
: "${MSC_VTY_PORT:=4254}"

_msc_cfg()      { core_cfg osmo-msc; }
_msc_asp_port() { core_cfg_field "$(_msc_cfg)" '^[[:space:]]*asp[[:space:]]+.*[[:space:]]m3ua[[:space:]]*$' 4 ""; }
_msc_mncc()     { core_cfg_field "$(_msc_cfg)" '^[[:space:]]*mncc[[:space:]]+external[[:space:]]' 3 ""; }

mod_msc_check() {
    core_bin osmo-msc >/dev/null || { mod_fail "binaire osmo-msc introuvable"; return $MOD_RC_FAIL; }
    [ -r "$(_msc_cfg)" ] || {
        mod_hint "déployez la configuration : cp configs/osmo-msc.cfg $OSMOCOM_CFG/"
        mod_fail "configuration illisible : $(_msc_cfg)"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_msc_status() { core_unit_active "$MSC_UNIT" || core_vty_listen "$MSC_VTY_PORT"; }

mod_msc_start() {
    core_svc_start "$MSC_UNIT" "$(core_bin osmo-msc)" -c "$(_msc_cfg)" \
        || { mod_fail "systemctl start $MSC_UNIT a échoué"
             mod_hint "journalctl -u $MSC_UNIT -n 30"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_msc_wait() {
    local to="${MOD_TIMEOUT[msc]}" asp mncc
    asp="$(_msc_asp_port)"; mncc="$(_msc_mncc)"

    if ! wait_until "$to" "VTY OsmoMSC ($MSC_VTY_PORT)" core_vty_listen "$MSC_VTY_PORT"; then
        mod_hint "journalctl -u $MSC_UNIT -n 30"
        return $MOD_RC_FAIL
    fi
    if [ -n "$asp" ]; then
        if ! wait_until "$to" "lien M3UA vers le STP (SCTP local :$asp)" core_sctp_estab "$asp"; then
            mod_hint "STP joignable ? ss -an --sctp | grep $asp ; vérifiez « asp ... m3ua » dans $(_msc_cfg)"
            return $MOD_RC_FAIL
        fi
    else
        mod_say "aucun ASP m3ua dans $(_msc_cfg) — barrière SS7 non applicable"
    fi
    if [ -n "$mncc" ]; then
        if ! wait_until "$to" "socket MNCC ($mncc)" core_unix_listen "$mncc"; then
            mod_hint "« mncc external $mncc » configuré mais la socket n'apparaît pas : ss -xl | grep msc_mncc"
            return $MOD_RC_FAIL
        fi
    fi
    if core_restarted_since "$MSC_UNIT"; then
        mod_hint "journalctl -u $MSC_UNIT -n 50 : le service redémarre en boucle"
        mod_fail "OsmoMSC a redémarré depuis le lancement"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_msc_stop() { core_svc_stop "$MSC_UNIT" "osmo-msc -c"; }

3.19 https://github.com/bbaranoff/qosmo/blob/main/run_modules/13-sidecar-cfg.sh

5084 octets, 140 lignes → 140 lignes

. "$(dirname "${BASH_SOURCE[0]}")/_lib/radio.sh"
. "$(dirname "${BASH_SOURCE[0]}")/_lib/modem.sh"

MOD_REGISTER sidecar-cfg "Configurations du BTS#1 (side-car)"
MOD_REQUIRED[sidecar-cfg]=1
MOD_DEPS[sidecar-cfg]="config"
MOD_PURE[sidecar-cfg]=1
MOD_TIMEOUT[sidecar-cfg]=10

: "${BSC_CFG:=${OSMOCOM_CFG:-/etc/osmocom}/osmo-bsc.cfg}"

_sc_bsc_declare() { grep -qE "^[[:space:]]*ipa unit-id ${SC_UNIT_ID} 0" "$BSC_CFG" 2>/dev/null; }

mod_sidecar_cfg_check() {
    [ -r "$BSC_CFG" ] || {
        mod_hint "posez BSC_CFG, ou déployez /etc/osmocom/osmo-bsc.cfg"
        mod_fail "configuration BSC illisible : $BSC_CFG"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_sidecar_cfg_status() {
    _sc_bsc_declare && [ -r "$SC_BTS_CFG" ] && [ -r "$SC_MOBILE_CFG" ]
}

# The Gb of BTS#1: same block as the main BTS (nsei 10, bvci 12, udp 23001),
# with its own identifiers (_lib/modem.sh) so that the SGSN sees a second NSE.
# Without it, "gprs mode none": no PDCH served, the second PCU has nothing.
_sc_gprs_block() {
    if ! modem_enabled; then printf '  gprs mode none\n'; return; fi
    cat <<GPRS
  gprs mode egprs
  gprs routing area 0
  gprs network-control-order nc0
  gprs power-control alpha 0
  gprs cell bvci ${SC_GPRS_BVCI}
  gprs cell timer blocking-timer 3
  gprs cell timer blocking-retries 3
  gprs cell timer unblocking-retries 3
  gprs cell timer reset-timer 3
  gprs cell timer reset-retries 3
  gprs cell timer suspend-timer 10
  gprs cell timer suspend-retries 3
  gprs cell timer resume-timer 10
  gprs cell timer resume-retries 3
  gprs cell timer capability-update-timer 10
  gprs cell timer capability-update-retries 3
  gprs nsei ${SC_GPRS_NSEI}
  gprs ns timer tns-block 3
  gprs ns timer tns-block-retries 3
  gprs ns timer tns-reset 3
  gprs ns timer tns-reset-retries 3
  gprs ns timer tns-test 30
  gprs ns timer tns-alive 3
  gprs ns timer tns-alive-retries 10
  gprs nsvc 0 nsvci ${SC_GPRS_NSVCI}
  gprs nsvc 0 local udp port ${SC_GPRS_NS_PORT}
  gprs nsvc 0 remote ip ${SC_SGSN_IP}
  gprs nsvc 0 remote udp port ${SC_SGSN_PORT}
  gprs nsvc 1 nsvci 0
  gprs nsvc 1 local udp port 0
  gprs nsvc 1 remote ip 0.0.0.0
  gprs nsvc 1 remote udp port 0
GPRS
}

_sc_bts_block() {
    cat <<BLOC
 bts 1
  type osmo-bts
  band DCS1800
  cell_identity ${SC_UNIT_ID}
  location_area_code 0x0001
  base_station_id_code 8
  ms max power 15
  cell reselection hysteresis 4
  rxlev access min 0
  radio-link-timeout 32
  channel allocator mode chan-req descending
  channel allocator mode assignment ascending
  channel allocator mode handover ascending
  rach tx integer 9
  rach max transmission 7
  ipa unit-id ${SC_UNIT_ID} 0
  oml ipa stream-id 255 line 0
  neighbor-list mode automatic
  codec-support fr
$(_sc_gprs_block)
  trx 0
   rf_locked 0
   arfcn ${SC_ARFCN}
   nominal power 23
   max_power_red 0
   rsl e1 tei 0
$(for t in 0 1 2 3 4 5 6 7; do
    case "$t" in
      0) pchan='CCCH+SDCCH4' ;;
      1) pchan="${SC_TS1_PCHAN:-SDCCH8}" ;;
      5|6|7) pchan="${SC_TS_PDCH_PCHAN:-PDCH}" ;;   # packet channels of the GPRS modem (as in configs/osmo-bsc.cfg)
      *) pchan='TCH/F' ;;
    esac
    printf '   timeslot %s\n    phys_chan_config %s\n    hopping enabled 0\n' "$t" "$pchan"
  done)
BLOC
}

mod_sidecar_cfg_start() {
    if _sc_bsc_declare; then
        mod_say "bloc « bts 1 » (unit-id $SC_UNIT_ID) déjà déclaré dans $BSC_CFG"
    else
        grep -qE '^msc 0' "$BSC_CFG" || {
            mod_hint "le bloc « bts 1 » s'insère avant « msc 0 » ; cette ancre est absente"
            mod_fail "structure inattendue dans $BSC_CFG"; return $MOD_RC_FAIL; }
        cp -f "$BSC_CFG" "${BSC_CFG}.avant-sidecar" 2>/dev/null
        local tmp; tmp="$(mktemp)"
        awk -v bloc="$(_sc_bts_block)" '
            /^msc 0/ && !fait { print bloc; fait = 1 }
            { print }
        ' "$BSC_CFG" > "$tmp" || { rm -f "$tmp"; mod_fail "réécriture de $BSC_CFG impossible"; return $MOD_RC_FAIL; }
        cat "$tmp" > "$BSC_CFG" && rm -f "$tmp"
        _sc_bsc_declare || { mod_fail "insertion du bloc « bts 1 » sans effet"; return $MOD_RC_FAIL; }
        mod_say "bloc « bts 1 » inséré (sauvegarde : ${BSC_CFG}.avant-sidecar)"
    fi

    [ -r "$SC_BTS_CFG" ] || {
        mod_hint "attendu : $SC_BTS_CFG (VTY $SC_BTS_VTY_PORT, base-port remote $SC_TRX_PORT)"
        mod_fail "configuration du BTS#1 absente"; return $MOD_RC_FAIL; }
    local port; port="$(radio_cfg_val "$SC_BTS_CFG" "osmotrx base-port remote")"
    [ "${port:-$SC_TRX_PORT}" = "$SC_TRX_PORT" ] && mod_say "BTS#1 : $SC_BTS_CFG (TRXD $SC_TRX_PORT)" \
        || mod_say "⚠ $SC_BTS_CFG annonce base-port remote $port, le side-car attend $SC_TRX_PORT"

    if [ ! -r "$SC_MOBILE_CFG" ]; then
        mod_hint "attendu : $SC_MOBILE_CFG (layer2-socket $SC_L2_SOCK, VTY $SC_MOBILE_VTY_PORT)"
        mod_fail "configuration du MS#2 absente"; return $MOD_RC_FAIL
    fi
    local imsi; imsi="$(awk '$1=="imsi"{print $2; exit}' "$SC_MOBILE_CFG" 2>/dev/null)"
    mod_say "MS#2 : $SC_MOBILE_CFG (imsi ${imsi:-?}, socket $SC_L2_SOCK)"
    mod_ok
}

mod_sidecar_cfg_stop() { return 0; }

3.20 https://github.com/bbaranoff/qosmo/blob/main/run_modules/14-bsc.sh

2850 octets, 71 lignes → 71 lignes

: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER bsc "Cœur — OsmoBSC (contrôleur BTS)"
MOD_REQUIRED[bsc]=1
MOD_DEPS[bsc]="stp mgw msc"
MOD_JOURNAL[bsc]="osmo-bsc"
MOD_TIMEOUT[bsc]=40
MOD_ENABLED_IF[bsc]='[ "${NO_OSMO_START:-0}" != 1 ]'

: "${BSC_UNIT:=osmo-bsc}"
: "${BSC_VTY_PORT:=4242}"
: "${BSC_OML_PORT:=3002}"
: "${BSC_RSL_PORT:=3003}"

_bsc_cfg()      { core_cfg osmo-bsc; }
_bsc_asp_port() { core_cfg_field "$(_bsc_cfg)" '^[[:space:]]*asp[[:space:]]+.*[[:space:]]m3ua[[:space:]]*$' 4 ""; }

mod_bsc_check() {
    core_bin osmo-bsc >/dev/null || { mod_fail "binaire osmo-bsc introuvable"; return $MOD_RC_FAIL; }
    [ -r "$(_bsc_cfg)" ] || {
        mod_hint "déployez la configuration : cp configs/osmo-bsc.cfg $OSMOCOM_CFG/"
        mod_fail "configuration illisible : $(_bsc_cfg)"; return $MOD_RC_FAIL; }
    if grep -qa '__[A-Z_]\+__' "$(_bsc_cfg)" 2>/dev/null; then
        mod_hint "la configuration contient encore des jetons de gabarit — régénérez-la"
        mod_fail "$(_bsc_cfg) : gabarit non substitué"; return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_bsc_status() { core_unit_active "$BSC_UNIT" || core_vty_listen "$BSC_VTY_PORT"; }

mod_bsc_start() {
    core_svc_start "$BSC_UNIT" "$(core_bin osmo-bsc)" -c "$(_bsc_cfg)" -s \
        || { mod_fail "systemctl start $BSC_UNIT a échoué"
             mod_hint "journalctl -u $BSC_UNIT -n 30"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_bsc_wait() {
    local to="${MOD_TIMEOUT[bsc]}" asp; asp="$(_bsc_asp_port)"

    if ! wait_until "$to" "VTY OsmoBSC ($BSC_VTY_PORT)" core_vty_listen "$BSC_VTY_PORT"; then
        mod_hint "journalctl -u $BSC_UNIT -n 30"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "$to" "écoute A-bis OML ($BSC_OML_PORT)" core_tcp_listen "$BSC_OML_PORT"; then
        mod_hint "port $BSC_OML_PORT déjà pris par un autre BSC ? ss -ltn | grep $BSC_OML_PORT"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "$to" "écoute A-bis RSL ($BSC_RSL_PORT)" core_tcp_listen "$BSC_RSL_PORT"; then
        mod_hint "port $BSC_RSL_PORT déjà pris ? ss -ltn | grep $BSC_RSL_PORT"
        return $MOD_RC_FAIL
    fi
    if [ -n "$asp" ]; then
        if ! wait_until "$to" "lien M3UA vers le STP (SCTP local :$asp)" core_sctp_estab "$asp"; then
            mod_hint "STP joignable ? ss -an --sctp | grep $asp ; vérifiez « asp ... m3ua » dans $(_bsc_cfg)"
            return $MOD_RC_FAIL
        fi
    else
        mod_say "aucun ASP m3ua dans $(_bsc_cfg) — barrière SS7 non applicable"
    fi
    if core_restarted_since "$BSC_UNIT"; then
        mod_hint "journalctl -u $BSC_UNIT -n 50 : le service redémarre en boucle"
        mod_fail "OsmoBSC a redémarré depuis le lancement"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_bsc_stop() { core_svc_stop "$BSC_UNIT" "osmo-bsc -c"; }

3.21 https://github.com/bbaranoff/qosmo/blob/main/run_modules/15-apn0.sh

1833 octets, 48 lignes → 48 lignes

: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER apn0 "Cœur — interface TUN de l'APN"
MOD_REQUIRED[apn0]=0
MOD_TIMEOUT[apn0]=15
MOD_ENABLED_IF[apn0]='[ "${NO_OSMO_START:-0}" != 1 ] && [ "${CORE_GPRS:-1}" = 1 ]'

: "${APN_DEV:=apn0}"
: "${APN_ADDR:=176.16.32.0/24}"

_apn_up()   { ip -o link show "$APN_DEV" 2>/dev/null | grep -qE '<[^>]*(^|<|,)UP(,|>)'; }
_apn_addr() { ip -o -4 addr show dev "$APN_DEV" 2>/dev/null | grep -q "${APN_ADDR%%/*}"; }

mod_apn0_check() {
    command -v ip >/dev/null 2>&1 || { mod_fail "commande ip absente (paquet iproute2)"; return $MOD_RC_FAIL; }
    [ -c /dev/net/tun ] || {
        mod_hint "conteneur sans TUN : ajoutez --device /dev/net/tun et --cap-add NET_ADMIN"
        mod_fail "/dev/net/tun absent — impossible de créer une interface TUN"
        return $MOD_RC_FAIL; }
    mod_ok
}

mod_apn0_status() { _apn_up && _apn_addr; }

mod_apn0_start() {
    ip link show "$APN_DEV" >/dev/null 2>&1 && ip link del dev "$APN_DEV" 2>/dev/null
    ip tuntap add dev "$APN_DEV" mode tun 2>&1 || {
        mod_hint "droits insuffisants : lancez le conteneur avec --cap-add NET_ADMIN"
        mod_fail "création de $APN_DEV refusée"; return $MOD_RC_FAIL; }
    ip addr add "$APN_ADDR" dev "$APN_DEV" 2>&1
    ip link set dev "$APN_DEV" up 2>&1
    mod_ok
}

mod_apn0_wait() {
    if ! wait_until "${MOD_TIMEOUT[apn0]}" "interface $APN_DEV UP" _apn_up; then
        mod_hint "ip link show $APN_DEV"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "${MOD_TIMEOUT[apn0]}" "adresse $APN_ADDR sur $APN_DEV" _apn_addr; then
        mod_hint "adresse déjà portée par une autre interface ? ip -4 addr | grep ${APN_ADDR%%/*}"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_apn0_stop() { ip link del dev "$APN_DEV" 2>/dev/null; return 0; }

3.22 https://github.com/bbaranoff/qosmo/blob/main/run_modules/16-ggsn.sh

2634 octets, 65 lignes → 65 lignes

: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER ggsn "Cœur — OsmoGGSN (data GTP)"
MOD_REQUIRED[ggsn]=0
MOD_DEPS[ggsn]="apn0"
MOD_JOURNAL[ggsn]="osmo-ggsn"
MOD_TIMEOUT[ggsn]=25
MOD_ENABLED_IF[ggsn]='[ "${NO_OSMO_START:-0}" != 1 ] && [ "${CORE_GPRS:-1}" = 1 ]'

: "${GGSN_UNIT:=osmo-ggsn}"
: "${GGSN_VTY_PORT:=4260}"
: "${GGSN_GTPC_PORT:=2123}"
: "${GGSN_GTPU_PORT:=2152}"

_ggsn_cfg() { core_cfg osmo-ggsn; }
_ggsn_ip()  { core_cfg_field "$(_ggsn_cfg)" '^[[:space:]]*gtp[[:space:]]+bind-ip[[:space:]]' 3 ""; }

mod_ggsn_check() {
    core_bin osmo-ggsn >/dev/null || { mod_fail "binaire osmo-ggsn introuvable"; return $MOD_RC_FAIL; }
    [ -r "$(_ggsn_cfg)" ] || {
        mod_hint "déployez la configuration : cp configs/osmo-ggsn.cfg $OSMOCOM_CFG/"
        mod_fail "configuration illisible : $(_ggsn_cfg)"; return $MOD_RC_FAIL; }
    local ip; ip="$(_ggsn_ip)"
    [ -n "$ip" ] || { mod_fail "« gtp bind-ip » absent de $(_ggsn_cfg)"; return $MOD_RC_FAIL; }
    core_ip_local "$ip" || {
        mod_hint "corrigez « gtp bind-ip » dans $(_ggsn_cfg) : aucune interface ne porte $ip"
        mod_fail "adresse GTP $ip introuvable localement — le GGSN refusera de démarrer"
        return $MOD_RC_FAIL; }
    mod_ok
}

mod_ggsn_status() { core_unit_active "$GGSN_UNIT" || core_vty_listen "$GGSN_VTY_PORT"; }

mod_ggsn_start() {
    core_svc_start "$GGSN_UNIT" "$(core_bin osmo-ggsn)" -c "$(_ggsn_cfg)" \
        || { mod_fail "systemctl start $GGSN_UNIT a échoué"
             mod_hint "journalctl -u $GGSN_UNIT -n 30"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_ggsn_wait() {
    local to="${MOD_TIMEOUT[ggsn]}" ip; ip="$(_ggsn_ip)"

    if ! wait_until "$to" "VTY OsmoGGSN ($GGSN_VTY_PORT)" core_vty_listen "$GGSN_VTY_PORT"; then
        mod_hint "journalctl -u $GGSN_UNIT -n 30"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "$to" "GTP-C ($ip:$GGSN_GTPC_PORT)" core_udp_listen "$GGSN_GTPC_PORT" "$ip"; then
        mod_hint "l'interface TUN de l'APN est-elle montée ? ip link show ${APN_DEV:-apn0}"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "$to" "GTP-U ($ip:$GGSN_GTPU_PORT)" core_udp_listen "$GGSN_GTPU_PORT" "$ip"; then
        mod_hint "port $GGSN_GTPU_PORT déjà pris ? ss -lun | grep $GGSN_GTPU_PORT"
        return $MOD_RC_FAIL
    fi
    if core_restarted_since "$GGSN_UNIT"; then
        mod_hint "journalctl -u $GGSN_UNIT -n 50 : le service redémarre en boucle"
        mod_fail "OsmoGGSN a redémarré depuis le lancement"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_ggsn_stop() { core_svc_stop "$GGSN_UNIT" "osmo-ggsn -c"; }

3.23 https://github.com/bbaranoff/qosmo/blob/main/run_modules/17-sgsn.sh

2351 octets, 60 lignes → 60 lignes

: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER sgsn "Cœur — OsmoSGSN (rattachement paquet)"
MOD_REQUIRED[sgsn]=0
MOD_DEPS[sgsn]="hlr"
MOD_JOURNAL[sgsn]="osmo-sgsn"
MOD_TIMEOUT[sgsn]=25
MOD_ENABLED_IF[sgsn]='[ "${NO_OSMO_START:-0}" != 1 ] && [ "${CORE_GPRS:-1}" = 1 ]'

: "${SGSN_UNIT:=osmo-sgsn}"
: "${SGSN_VTY_PORT:=4245}"

_sgsn_cfg()  { core_cfg osmo-sgsn; }
_sgsn_ns_ip()   { core_cfg_field "$(_sgsn_cfg)" '^[[:space:]]*listen[[:space:]]+[0-9]' 2 ""; }
_sgsn_ns_port() { core_cfg_field "$(_sgsn_cfg)" '^[[:space:]]*listen[[:space:]]+[0-9]' 3 23000; }

mod_sgsn_check() {
    core_bin osmo-sgsn >/dev/null || { mod_fail "binaire osmo-sgsn introuvable"; return $MOD_RC_FAIL; }
    [ -r "$(_sgsn_cfg)" ] || {
        mod_hint "déployez la configuration : cp configs/osmo-sgsn.cfg $OSMOCOM_CFG/"
        mod_fail "configuration illisible : $(_sgsn_cfg)"; return $MOD_RC_FAIL; }
    local ip; ip="$(_sgsn_ns_ip)"
    if [ -n "$ip" ] && ! core_ip_local "$ip"; then
        mod_hint "corrigez « ns / bind udp local / listen » dans $(_sgsn_cfg)"
        mod_fail "adresse NS $ip portée par aucune interface"; return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_sgsn_status() { core_unit_active "$SGSN_UNIT" || core_vty_listen "$SGSN_VTY_PORT"; }

mod_sgsn_start() {
    core_svc_start "$SGSN_UNIT" "$(core_bin osmo-sgsn)" -c "$(_sgsn_cfg)" \
        || { mod_fail "systemctl start $SGSN_UNIT a échoué"
             mod_hint "journalctl -u $SGSN_UNIT -n 30"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_sgsn_wait() {
    local to="${MOD_TIMEOUT[sgsn]}" ip port
    ip="$(_sgsn_ns_ip)"; port="$(_sgsn_ns_port)"

    if ! wait_until "$to" "VTY OsmoSGSN ($SGSN_VTY_PORT)" core_vty_listen "$SGSN_VTY_PORT"; then
        mod_hint "journalctl -u $SGSN_UNIT -n 30"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "$to" "NS UDP ($ip:$port)" core_udp_listen "$port" "$ip"; then
        mod_hint "adresse $ip absente ou port $port déjà pris : ss -lun | grep $port"
        return $MOD_RC_FAIL
    fi
    if core_restarted_since "$SGSN_UNIT"; then
        mod_hint "journalctl -u $SGSN_UNIT -n 50 : le service redémarre en boucle"
        mod_fail "OsmoSGSN a redémarré depuis le lancement"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_sgsn_stop() { core_svc_stop "$SGSN_UNIT" "osmo-sgsn -c"; }

3.24 https://github.com/bbaranoff/qosmo/blob/main/run_modules/18-pcu.sh

2008 octets, 53 lignes → 53 lignes

: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER pcu "Cœur — OsmoPCU (ordonnanceur paquet)"
MOD_REQUIRED[pcu]=0
MOD_DEPS[pcu]="sgsn"
MOD_JOURNAL[pcu]="osmo-pcu"
MOD_TIMEOUT[pcu]=20
MOD_ENABLED_IF[pcu]='[ "${NO_OSMO_START:-0}" != 1 ] && [ "${CORE_GPRS:-1}" = 1 ]'

: "${PCU_UNIT:=osmo-pcu}"
: "${PCU_VTY_PORT:=4240}"
: "${PCU_SOCKET:=/tmp/pcu_bts}"

_pcu_cfg() { core_cfg osmo-pcu; }

mod_pcu_check() {
    core_bin osmo-pcu >/dev/null || { mod_fail "binaire osmo-pcu introuvable"; return $MOD_RC_FAIL; }
    [ -r "$(_pcu_cfg)" ] || {
        mod_hint "déployez la configuration : cp configs/osmo-pcu.cfg $OSMOCOM_CFG/"
        mod_fail "configuration illisible : $(_pcu_cfg)"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_pcu_status() { core_unit_active "$PCU_UNIT" || core_vty_listen "$PCU_VTY_PORT"; }

mod_pcu_start() {
    core_svc_start "$PCU_UNIT" "$(core_bin osmo-pcu)" -c "$(_pcu_cfg)" \
        || { mod_fail "systemctl start $PCU_UNIT a échoué"
             mod_hint "journalctl -u $PCU_UNIT -n 30"; return $MOD_RC_FAIL; }
    [ -e "$PCU_SOCKET" ] && chmod 777 "$PCU_SOCKET" 2>/dev/null
    mod_ok
}

mod_pcu_wait() {
    if ! core_alive "$PCU_UNIT"; then
        mod_hint "journalctl -u $PCU_UNIT -n 30"
        mod_fail "osmo-pcu n'est pas resté en vie après le lancement"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "${MOD_TIMEOUT[pcu]}" "VTY OsmoPCU ($PCU_VTY_PORT)" core_vty_listen "$PCU_VTY_PORT"; then
        mod_say "VTY $PCU_VTY_PORT non observé — port non déclaré dans $(_pcu_cfg), repli sur la sonde de vie"
    fi
    if core_restarted_since "$PCU_UNIT"; then
        mod_hint "journalctl -u $PCU_UNIT -n 50 : conf invalide ou socket PCU inaccessible"
        mod_fail "OsmoPCU redémarre en boucle"
        return $MOD_RC_FAIL
    fi
    core_unix_listen "$PCU_SOCKET" && mod_say "socket PCU $PCU_SOCKET présente (BTS déjà démarrée)"
    mod_ok
}

mod_pcu_stop() { core_svc_stop "$PCU_UNIT" "osmo-pcu -c"; }

3.25 https://github.com/bbaranoff/qosmo/blob/main/run_modules/19-asterisk.sh

2241 octets, 66 lignes → 66 lignes

: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER asterisk "Cœur — Asterisk (PBX SIP)"
MOD_REQUIRED[asterisk]=0
MOD_JOURNAL[asterisk]="asterisk"
MOD_TIMEOUT[asterisk]=45
MOD_ENABLED_IF[asterisk]='[ "${NO_OSMO_START:-0}" != 1 ] && [ "${CORE_VOICE:-1}" = 1 ]'

: "${ASTERISK_UNIT:=asterisk}"
: "${ASTERISK_CFG:=/etc/asterisk/asterisk.conf}"

_ast_cli() { asterisk -rx "$1" 2>/dev/null; }
_ast_ready() { _ast_cli "core show uptime" | grep -qi 'uptime'; }

mod_asterisk_check() {
    command -v asterisk >/dev/null 2>&1 || {
        mod_hint "installez asterisk, ou désactivez la voix : CORE_VOICE=0"
        mod_fail "binaire asterisk introuvable"; return $MOD_RC_FAIL; }
    [ -r "$ASTERISK_CFG" ] || {
        mod_fail "configuration illisible : $ASTERISK_CFG"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_asterisk_status() { _ast_ready; }

mod_asterisk_start() {
    if _ast_ready; then mod_say "déjà actif — on ne relance pas"; mod_ok; return 0; fi

    local bin pf log
    bin="$(command -v asterisk)"
    pf="$(core_pidfile "$ASTERISK_UNIT")"
    log="${LOG_DIR}/${ASTERISK_UNIT}.log"
    mkdir -p "$RUN_DIR" "$LOG_DIR" 2>/dev/null || true

    mod_say "lancement direct : $bin -f -U asterisk"
    setsid "$bin" -f -U asterisk >>"$log" 2>&1 </dev/null &
    printf '%s\n' "$!" > "$pf"

    sleep 1
    if ! kill -0 "$(cat "$pf" 2>/dev/null)" 2>/dev/null; then
        mod_hint "tail -30 $log"
        mod_fail "asterisk est mort dans la seconde qui a suivi le lancement"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_asterisk_wait() {
    if ! wait_until "${MOD_TIMEOUT[asterisk]}" "console Asterisk" _ast_ready; then
        mod_hint "asterisk -rx 'core show uptime' ; journalctl -u $ASTERISK_UNIT -n 40"
        return $MOD_RC_FAIL
    fi
    local pf; pf="$(core_pidfile "$ASTERISK_UNIT")"
    if [ -f "$pf" ] && ! kill -0 "$(cat "$pf" 2>/dev/null)" 2>/dev/null; then
        mod_hint "tail -50 ${LOG_DIR}/${ASTERISK_UNIT}.log"
        mod_fail "le PID lancé n'existe plus : Asterisk est mort ou a été remplacé"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_asterisk_stop() {
    _ast_cli "core stop now" >/dev/null 2>&1
    core_svc_stop "$ASTERISK_UNIT" ""
}

3.26 https://github.com/bbaranoff/qosmo/blob/main/run_modules/20-mobile-cfg.sh

3917 octets, 89 lignes → 89 lignes

MOD_REGISTER mobile-cfg "Configuration du mobile"
MOD_REQUIRED[mobile-cfg]=0
MOD_DEPS[mobile-cfg]="config"
MOD_TIMEOUT[mobile-cfg]=10

_mcfg_imsi() { grep -oE '^[[:space:]]*imsi [0-9]{15}' "${MOBILE_CFG:-}" 2>/dev/null | head -1 | grep -oE '[0-9]{15}'; }
_mcfg_ki()   { sed -n 's/^[[:space:]]*ki comp128 \([0-9a-fA-F ]*\)$/\1/p' "${MOBILE_CFG:-}" 2>/dev/null | head -1 | tr -d ' '; }
_mcfg_stick(){ sed -n 's/^[[:space:]]*stick \([0-9]\+\).*/\1/p' "${MOBILE_CFG:-}" 2>/dev/null | head -1; }

mod_mobile_cfg_check() {
    if [ "${CALYPSO_SYNC_MOBILE_CFG:-1}" != "1" ]; then
        [ -r "${MOBILE_CFG:-}" ] && { mod_say "synchronisation désactivée, fichier existant conservé"; mod_ok; return $MOD_RC_OK; }
        mod_hint "CALYPSO_SYNC_MOBILE_CFG=1 pour réinstaller la version du dépôt"
        mod_fail "synchronisation désactivée et ${MOBILE_CFG:-<non défini>} absent"
        return $MOD_RC_FAIL
    fi
    if [ ! -r "${MOBILE_CFG_SRC:-}" ]; then
        mod_hint "attendu : \$QEMU_TREE/cfgs/mobile_group1.cfg — vérifiez QEMU_CFGS dans paths.env"
        mod_skip "configuration versionnée absente : ${MOBILE_CFG_SRC:-<non défini>}"
        return $MOD_RC_SKIP
    fi
    mod_ok
}

mod_mobile_cfg_status() {
    [ -r "${MOBILE_CFG:-}" ] || return 1
    [ "${CALYPSO_SYNC_MOBILE_CFG:-1}" = "1" ] || return 0
    cmp -s "${MOBILE_CFG_SRC:-}" "$MOBILE_CFG" 2>/dev/null || return 1
    [ -z "${CALYPSO_STICK_ARFCN:-}" ] || [ "${CALYPSO_STICK_ARFCN}" = "0" ]
}

mod_mobile_cfg_start() {
    if [ "${CALYPSO_SYNC_MOBILE_CFG:-1}" = "1" ]; then
        mkdir -p "$(dirname "$MOBILE_CFG")" 2>/dev/null
        if ! cp "$MOBILE_CFG_SRC" "$MOBILE_CFG" 2>/dev/null; then
            mod_hint "vérifiez les droits sur $(dirname "$MOBILE_CFG")"
            mod_fail "copie impossible : $MOBILE_CFG_SRC -> $MOBILE_CFG"
            return $MOD_RC_FAIL
        fi
        mod_say "installée : $MOBILE_CFG_SRC -> $MOBILE_CFG"
    fi

    local a="${CALYPSO_STICK_ARFCN:-}"
    if [ -n "$a" ] && [ "$a" != "0" ]; then
        if grep -qE '^[[:space:]]*stick [0-9]+' "$MOBILE_CFG" 2>/dev/null; then
            sed -i "s/^\([[:space:]]*\)stick [0-9]\+/\1stick $a/" "$MOBILE_CFG"
        elif grep -qE '^[[:space:]]*no stick' "$MOBILE_CFG" 2>/dev/null; then
            sed -i "s/^\([[:space:]]*\)no stick/\1stick $a/" "$MOBILE_CFG"
        else
            mod_say "aucune directive stick/no stick dans $MOBILE_CFG : rien à surcharger"
        fi
        mod_say "stick demandé = $a, lu = $(_mcfg_stick)"
    fi

    MOBILE_IMSI="$(_mcfg_imsi)"
    MOBILE_KI="$(_mcfg_ki)"
    mod_say "IMSI = ${MOBILE_IMSI:-<non trouvé>}"
    mod_say "Ki   = ${MOBILE_KI:+<présent, ${#MOBILE_KI} caractères>}${MOBILE_KI:-<non trouvé>}"
    mod_ok
}

_mcfg_ready() {
    [ -s "${MOBILE_CFG:-}" ] || return 1
    [ -n "$(_mcfg_imsi)" ] || return 1
    [ -n "$(_mcfg_ki)" ]   || return 1
    local a="${CALYPSO_STICK_ARFCN:-}"
    if [ -n "$a" ] && [ "$a" != "0" ] && grep -qE '^[[:space:]]*stick ' "$MOBILE_CFG" 2>/dev/null; then
        [ "$(_mcfg_stick)" = "$a" ] || return 1
    fi
    return 0
}

mod_mobile_cfg_wait() {
    if ! wait_until "${MOD_TIMEOUT[mobile-cfg]}" "configuration du mobile" _mcfg_ready; then
        if [ ! -s "${MOBILE_CFG:-}" ]; then
            mod_fail "$MOBILE_CFG absent ou vide après la copie"
        elif [ -n "${CALYPSO_STICK_ARFCN:-}" ] && [ "$(_mcfg_stick)" != "${CALYPSO_STICK_ARFCN}" ]; then
            mod_hint "vérifiez la ligne 'stick' de $MOBILE_CFG_SRC"
            mod_fail "ARFCN non appliqué : demandé ${CALYPSO_STICK_ARFCN}, lu $(_mcfg_stick)"
        else
            mod_hint "attendu dans $MOBILE_CFG : une ligne 'imsi <15 chiffres>' et une ligne 'ki comp128 <16 octets>'"
            mod_fail "IMSI ou Ki introuvable dans $MOBILE_CFG — le HLR ne pourra pas être alimenté"
        fi
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_mobile_cfg_stop() { return 0; }

3.27 https://github.com/bbaranoff/qosmo/blob/main/run_modules/20-sip-connector.sh

2710 octets, 64 lignes → 64 lignes

: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER sip-connector "Cœur — passerelle MNCC↔SIP"
MOD_REQUIRED[sip-connector]=0
MOD_DEPS[sip-connector]="msc asterisk"
MOD_JOURNAL[sip-connector]="osmo-sip-connector"
MOD_TIMEOUT[sip-connector]=25
MOD_ENABLED_IF[sip-connector]='[ "${NO_OSMO_START:-0}" != 1 ] && [ "${CORE_VOICE:-1}" = 1 ]'

: "${SIPC_UNIT:=osmo-sip-connector}"

_sipc_cfg()  { core_cfg osmo-sip-connector; }
_sipc_ip()   { core_cfg_field "$(_sipc_cfg)" '^[[:space:]]*local[[:space:]]+[0-9]' 2 127.0.0.1; }
_sipc_port() { core_cfg_field "$(_sipc_cfg)" '^[[:space:]]*local[[:space:]]+[0-9]' 3 5060; }
_sipc_mncc() { core_cfg_field "$(_sipc_cfg)" '^[[:space:]]*socket-path[[:space:]]' 2 /tmp/msc_mncc; }

_sipc_mncc_connected() {
    local p; p="$(_sipc_mncc)"
    ss -xH 2>/dev/null | grep -F -- "$p" | grep -q 'ESTAB\|CONNECTED'
}

mod_sip_connector_check() {
    core_bin osmo-sip-connector >/dev/null || {
        mod_hint "installez osmo-sip-connector, ou désactivez la voix : CORE_VOICE=0"
        mod_fail "binaire osmo-sip-connector introuvable"; return $MOD_RC_FAIL; }
    [ -r "$(_sipc_cfg)" ] || {
        mod_fail "configuration illisible : $(_sipc_cfg)"; return $MOD_RC_FAIL; }
    core_ip_local "$(_sipc_ip)" || {
        mod_hint "corrigez « sip / local » dans $(_sipc_cfg)"
        mod_fail "adresse SIP $(_sipc_ip) portée par aucune interface"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_sip_connector_status() { core_unit_active "$SIPC_UNIT" || have_proc "osmo-sip-connector -c"; }

mod_sip_connector_start() {
    core_svc_start "$SIPC_UNIT" "$(core_bin osmo-sip-connector)" -c "$(_sipc_cfg)" \
        || { mod_fail "systemctl start $SIPC_UNIT a échoué"
             mod_hint "journalctl -u $SIPC_UNIT -n 30"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_sip_connector_wait() {
    local to="${MOD_TIMEOUT[sip-connector]}" ip port
    ip="$(_sipc_ip)"; port="$(_sipc_port)"

    if ! wait_until "$to" "bind SIP ($ip:$port)" core_udp_listen "$port" "$ip"; then
        mod_hint "port $port déjà pris (Asterisk ?) : ss -lun | grep $port"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "$to" "connexion MNCC ($(_sipc_mncc))" _sipc_mncc_connected; then
        mod_hint "le MSC expose-t-il « mncc external $(_sipc_mncc) » ? ss -x | grep $(_sipc_mncc)"
        return $MOD_RC_FAIL
    fi
    if core_restarted_since "$SIPC_UNIT"; then
        mod_hint "journalctl -u $SIPC_UNIT -n 50 : le service redémarre en boucle"
        mod_fail "osmo-sip-connector a redémarré depuis le lancement"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_sip_connector_stop() { core_svc_stop "$SIPC_UNIT" "osmo-sip-connector -c"; }

3.28 https://github.com/bbaranoff/qosmo/blob/main/run_modules/21-abonnes-hlr.sh

4107 octets, 94 lignes → 94 lignes

: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER abonnes-hlr "Cœur — abonnés provisionnés dans le HLR"
MOD_REQUIRED[abonnes-hlr]=0
MOD_DEPS[abonnes-hlr]="hlr"
MOD_TIMEOUT[abonnes-hlr]=30
MOD_ENABLED_IF[abonnes-hlr]='[ "${NO_OSMO_START:-0}" != 1 ]'

: "${HLR_VTY_PORT:=4258}"
: "${HLR_DB:=/var/lib/osmocom/hlr.db}"
: "${OPERATOR_ID:=1}"
: "${N_MS:=1}"

_abo_mcc() { printf '%s\n' "${MCC:-$(core_cfg_field "$(core_cfg osmo-msc)" '^[[:space:]]*network country code[[:space:]]' 4 001)}"; }
_abo_mnc() { printf '%s\n' "${MNC:-$(core_cfg_field "$(core_cfg osmo-msc)" '^[[:space:]]*mobile network code[[:space:]]' 4 01)}"; }

_abo_imsi() { printf '%s%s%04d%06d\n' "$(_abo_mcc)" "$(_abo_mnc)" "$OPERATOR_ID" "$1"; }
_abo_ki()   { printf '00112233445566778899aabbccdd%02x%02x\n' "$1" "$OPERATOR_ID"; }

_abo_node() {
    local n="${OSMO_WAN_NODE:-${WAN_NODE_ID:-}}"
    [ -n "$n" ] || n="$(sed -n 's/^PLAN_NODE=//p' "${OSMOCOM_CFG:-/etc/osmocom}/radio-plan.env" 2>/dev/null | tail -1)"
    case "$n" in [1-9]) ;; *) n=1 ;; esac
    printf '%s' "$n"
}
_abo_msisdn() { echo $(( $(_abo_node) * 100000 + OPERATOR_ID * 100 + $1 )); }

_abo_present() {
    local imsi="$1" n=""
    if [ -r "$HLR_DB" ] && command -v sqlite3 >/dev/null 2>&1; then
        n="$(sqlite3 -readonly "$HLR_DB" "select count(*) from subscriber where imsi='$imsi';" 2>/dev/null)"
        case "$n" in ''|*[!0-9]*) n="" ;; esac
        if [ -n "$n" ]; then [ "$n" -ge 1 ]; return $?; fi
    fi
    core_vty_ask "$HLR_VTY_PORT" "enable" "subscriber imsi $imsi show" 2>/dev/null | grep -q "IMSI: $imsi"
}

mod_abonnes_hlr_check() {
    case "$N_MS" in ''|*[!0-9]*) mod_fail "N_MS invalide : « $N_MS »"; return $MOD_RC_FAIL ;; esac
    [ "$N_MS" -ge 1 ] || { mod_skip "N_MS=$N_MS : aucun abonné à créer"; return $MOD_RC_SKIP; }
    core_vty_listen "$HLR_VTY_PORT" || {
        mod_hint "le HLR doit être prêt : ./run.sh --only hlr"
        mod_fail "VTY HLR ($HLR_VTY_PORT) injoignable"; return $MOD_RC_FAIL; }
    command -v socat >/dev/null 2>&1 || command -v telnet >/dev/null 2>&1 || {
        mod_hint "installez socat (ou telnet) : le provisionnement passe par le VTY"
        mod_fail "ni socat ni telnet — impossible d'écrire dans le HLR"; return $MOD_RC_FAIL; }
    mod_ok
}

_abo_msisdn_now() {
    local imsi="$1" v=""
    if [ -r "$HLR_DB" ] && command -v sqlite3 >/dev/null 2>&1; then
        v="$(sqlite3 -readonly "$HLR_DB" "select msisdn from subscriber where imsi='$imsi';" 2>/dev/null)"
        [ -n "$v" ] && { printf '%s' "$v"; return 0; }
    fi
    core_vty_ask "$HLR_VTY_PORT" "enable" "subscriber imsi $imsi show" 2>/dev/null \
        | sed -n 's/.*MSISDN: *\([0-9][0-9]*\).*/\1/p' | head -1
}

mod_abonnes_hlr_status() {
    local imsi; imsi="$(_abo_imsi "$N_MS")"
    _abo_present "$imsi" || return 1
    [ "$(_abo_msisdn_now "$imsi")" = "$(_abo_msisdn "$N_MS")" ]
}

mod_abonnes_hlr_start() {
    local m cmds=() imsi msisdn
    cmds+=("enable")
    for m in $(seq 1 "$N_MS"); do
        imsi="$(_abo_imsi "$m")"
        msisdn="$(_abo_msisdn "$m")"
        cmds+=("subscriber imsi $imsi create")
        cmds+=("subscriber imsi $imsi update msisdn $msisdn")
        cmds+=("subscriber imsi $imsi update aud2g comp128v1 ki $(_abo_ki "$m")")
    done
    cmds+=("exit")
    mod_say "provisionnement de $N_MS abonné(s), nœud $(_abo_node), opérateur $OPERATOR_ID, PLMN $(_abo_mcc)-$(_abo_mnc) — MSISDN $(_abo_msisdn 1)..$(_abo_msisdn "$N_MS")"
    core_vty_ask "$HLR_VTY_PORT" "${cmds[@]}" || {
        mod_hint "vérifiez le VTY : socat STDIO TCP:127.0.0.1:$HLR_VTY_PORT,crlf"
        mod_fail "le dialogue VTY avec le HLR n'a rien retourné"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_abonnes_hlr_wait() {
    local imsi; imsi="$(_abo_imsi "$N_MS")"
    if ! wait_until "${MOD_TIMEOUT[abonnes-hlr]}" "abonné $imsi présent dans le HLR" _abo_present "$imsi"; then
        mod_hint "essayez à la main : subscriber imsi $imsi show (VTY $HLR_VTY_PORT) ; base = $HLR_DB"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_abonnes_hlr_stop() { return 0; }

3.29 https://github.com/bbaranoff/qosmo/blob/main/run_modules/22-smsc.sh

2866 octets, 68 lignes → 68 lignes

: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER smsc "Cœur — passerelle SMS (proto-SMSC)"
MOD_REQUIRED[smsc]=0
MOD_DEPS[smsc]="hlr"
MOD_TIMEOUT[smsc]=60
MOD_ENABLED_IF[smsc]='[ "${NO_OSMO_START:-0}" != 1 ] && [ "${CORE_SMS:-1}" = 1 ]'

: "${OPERATOR_ID:=1}"
: "${SMSC_SCRIPT:=/etc/osmocom/smsc-start.sh}"
: "${SMSC_RELAY_SCRIPT:=/etc/osmocom/sms-interop-relay.py}"
: "${SMSC_RELAY_PORT:=7890}"
: "${SMSC_SENDMT_SOCKET:=/tmp/sendmt_socket}"
: "${SMSC_HLR_IP:=127.0.0.2}"

mod_smsc_check() {
    command -v proto-smsc-daemon >/dev/null 2>&1 || {
        mod_hint "sans proto-smsc-daemon les SMS passent uniquement par le VTY du MSC ; CORE_SMS=0 pour ne plus l'essayer"
        mod_skip "proto-smsc-daemon absent"; return $MOD_RC_SKIP; }
    [ -x "$SMSC_SCRIPT" ] || {
        mod_hint "déployez les scripts : cp scripts/smsc-start.sh $OSMOCOM_CFG/"
        mod_fail "script de démarrage absent : $SMSC_SCRIPT"; return $MOD_RC_FAIL; }
    core_tcp_listen 4222 "$SMSC_HLR_IP" || {
        mod_hint "le HLR doit écouter en GSUP : ./run.sh --only hlr"
        mod_fail "GSUP HLR ($SMSC_HLR_IP:4222) injoignable"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_smsc_status() { have_proc 'proto-smsc-daemon'; }

mod_smsc_start() {
    mkdir -p "$RUN_DIR" "$LOG_DIR" /var/log/osmocom 2>/dev/null || true
    pkill -f 'proto-smsc-daemon' 2>/dev/null
    local log="$LOG_DIR/smsc-op${OPERATOR_ID}.log"
    mod_say "journal : $log ; SMS MO reçus : /var/log/osmocom/mo-sms-op${OPERATOR_ID}.log"
    setsid env OPERATOR_ID="$OPERATOR_ID" HLR_IP="$SMSC_HLR_IP" RELAY_PORT="$SMSC_RELAY_PORT" \
        stdbuf -oL -eL bash "$SMSC_SCRIPT" >"$log" 2>&1 </dev/null &
    printf '%s\n' "$!" > "$RUN_DIR/smsc-op${OPERATOR_ID}.pid"
    mod_ok
}

mod_smsc_wait() {
    local to="${MOD_TIMEOUT[smsc]}"

    if ! wait_until "$to" "socket d'envoi MT ($SMSC_SENDMT_SOCKET)" core_unix_listen "$SMSC_SENDMT_SOCKET"; then
        mod_hint "voir $LOG_DIR/smsc-op${OPERATOR_ID}.log : le daemon n'a pas pu se connecter au HLR ($SMSC_HLR_IP:4222)"
        return $MOD_RC_FAIL
    fi
    if [ -f "$SMSC_RELAY_SCRIPT" ]; then
        if ! wait_until "$to" "relay SMS interop (TCP $SMSC_RELAY_PORT)" core_tcp_listen "$SMSC_RELAY_PORT"; then
            mod_hint "port $SMSC_RELAY_PORT déjà pris, ou sms-routing.conf illisible : voir $LOG_DIR/smsc-op${OPERATOR_ID}.log"
            return $MOD_RC_FAIL
        fi
    else
        mod_say "$SMSC_RELAY_SCRIPT absent — pas de relay inter-opérateurs, barrière non applicable"
    fi
    mod_ok
}

mod_smsc_stop() {
    local pf="$RUN_DIR/smsc-op${OPERATOR_ID}.pid"
    [ -f "$pf" ] && { kill "$(cat "$pf" 2>/dev/null)" 2>/dev/null; rm -f "$pf"; }
    pkill -f 'proto-smsc-daemon' 2>/dev/null
    pkill -f 'sms-interop-relay.py' 2>/dev/null
    rm -f "$SMSC_SENDMT_SOCKET" 2>/dev/null
    return 0
}

3.30 https://github.com/bbaranoff/qosmo/blob/main/run_modules/25-audio.sh

2720 octets, 75 lignes → 75 lignes

MOD_REGISTER audio "Pont audio (PulseAudio + GAPK)"
MOD_REQUIRED[audio]=0
MOD_TIMEOUT[audio]=25

MOD_ENABLED_IF[audio]='[ "${AUDIO:-1}" = 1 ]'

: "${EGPRS_DIR:=${NITB_ROOT}}"
: "${AUDIO_LIB:=$EGPRS_DIR/lib/audio.sh}"
: "${AUDIO_SINK:=gsm_audio}"
: "${AUDIO_TMUX_SESSION:=gapk}"

_audio_sink_present() {
    pactl list short sinks 2>/dev/null | grep -q "$AUDIO_SINK"
}
_audio_gapk_vivant() {
    pgrep --ns $$ --nslist mnt -f "[g]apk-start\.sh auto" >/dev/null 2>&1 \
        || tmux has-session -t "${AUDIO_TMUX:-gapk}" 2>/dev/null
}

mod_audio_check() {
    if [ ! -r "$AUDIO_LIB" ]; then
        mod_hint "la bibliothèque est l'extraction de start-direct.sh.legacy L566-722 ; elle accompagne osmo_egprs"
        mod_skip "bibliothèque audio absente ($AUDIO_LIB) : pont audio non monté"
        return $MOD_RC_SKIP
    fi
    local manquant=""
    command -v tmux      >/dev/null 2>&1 || manquant="$manquant tmux"
    command -v osmo-gapk >/dev/null 2>&1 || manquant="$manquant osmo-gapk"
    if [ -n "$manquant" ]; then
        mod_hint "installez-les pour avoir le son :$manquant — ou AUDIO=0 pour ne plus l'essayer"
        mod_skip "outillage audio absent :$manquant"
        return $MOD_RC_SKIP
    fi
    mod_ok
}

mod_audio_status() { _audio_gapk_vivant && _audio_sink_present; }

mod_audio_start() {
    local retour="$PWD" rc=0
    cd "$EGPRS_DIR" || { mod_fail "impossible d'entrer dans $EGPRS_DIR"; return $MOD_RC_FAIL; }
    HERE="$EGPRS_DIR"
    . "$AUDIO_LIB" || { cd "$retour"; mod_fail "bibliothèque illisible : $AUDIO_LIB"; return $MOD_RC_FAIL; }

    mod_say "sink visé : $AUDIO_SINK · session tmux : $AUDIO_TMUX_SESSION"
    ensure_gapk || rc=$?
    cd "$retour"
    [ "$rc" -eq 0 ] || { mod_fail "ensure_gapk a rendu $rc"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_audio_wait() {
    wait_until "${MOD_TIMEOUT[audio]}" "démon PulseAudio" pactl info || {
        mod_hint "regardez $LOG_DIR/pulse-system.log : le démon système n'a pas démarré"
        mod_fail "PulseAudio ne répond pas"
        return $MOD_RC_FAIL
    }
    wait_until "${MOD_TIMEOUT[audio]}" "sink $AUDIO_SINK" _audio_sink_present || {
        mod_hint "pactl load-module module-null-sink sink_name=$AUDIO_SINK"
        mod_fail "le sink $AUDIO_SINK n'est pas enregistré"
        return $MOD_RC_FAIL
    }
    wait_until "${MOD_TIMEOUT[audio]}" "superviseur gapk" _audio_gapk_vivant || {
        mod_hint "regardez $LOG_DIR/gapk-auto.log : le superviseur s'est arrêté aussitôt lancé"
        mod_fail "le superviseur gapk ne tourne pas"
        return $MOD_RC_FAIL
    }
    mod_ok
}

mod_audio_stop() {
    tmux kill-session -t "$AUDIO_TMUX_SESSION" 2>/dev/null
    pkill -x osmo-gapk 2>/dev/null
    return 0
}

3.31 https://github.com/bbaranoff/qosmo/blob/main/run_modules/35-tmux.sh

2009 octets, 53 lignes → 53 lignes

MOD_REGISTER tmux "Session d'observation tmux"
MOD_REQUIRED[tmux]=0
MOD_DEPS[tmux]="logs"
MOD_TIMEOUT[tmux]=10
MOD_ENABLED_IF[tmux]='command -v tmux >/dev/null 2>&1'

: "${TMUX_SESSION:=calypso}"
: "${TMUX_MODULES_DIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")/../tmux_modules" 2>/dev/null && pwd)}"

mod_tmux_check() {
    command -v tmux >/dev/null 2>&1 || {
        mod_hint "apt-get install tmux — ou lancez sans : les services tournent quand même"
        mod_fail "tmux introuvable"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_tmux_status() { tmux has-session -t "$TMUX_SESSION" 2>/dev/null; }

_tmux_charger() {
    local d="${TMUX_MODULES_DIR:-}"
    [ -n "$d" ] && [ -r "$d/_commun.sh" ] && [ -r "$d/bench.sh" ] || {
        mod_hint "attendu : <dépôt>/tmux_modules/_commun.sh et bench.sh"
        mod_fail "disposition tmux introuvable (TMUX_MODULES_DIR=${d:-<vide>})"
        return $MOD_RC_FAIL; }
    . "$d/_commun.sh"
    . "$d/bench.sh"
    mod_say "disposition : bench.sh"
    return 0
}

mod_tmux_start() {
    _tmux_charger || return $MOD_RC_FAIL
    tmux new-session -d -s "$TMUX_SESSION" -n "${TMUX_FENETRE_PREMIERE:-radio}" \
        "$(tmux_layout_premiere)" 2>/dev/null || {
        mod_hint "un serveur tmux périmé peut refuser la session : tmux kill-server puis relancez"
        mod_fail "création de la session « $TMUX_SESSION » refusée"; return $MOD_RC_FAIL; }
    _tmux_env_repropre
    tmux_layout
    _tmux_style
    tmux select-window -t "$TMUX_SESSION:${TMUX_FENETRE_PREMIERE:-radio}" 2>/dev/null
    mod_say "${TMUX_RESUME:-session créée}"
    mod_ok
}

mod_tmux_wait() {
    wait_until "${MOD_TIMEOUT[tmux]}" "session tmux « $TMUX_SESSION »" \
        tmux has-session -t "$TMUX_SESSION" || return $MOD_RC_FAIL
    [ "$(tmux list-windows -t "$TMUX_SESSION" 2>/dev/null | wc -l)" -ge 1 ] || {
        mod_fail "session créée mais aucune fenêtre"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_tmux_stop() { tmux kill-session -t "$TMUX_SESSION" 2>/dev/null; return 0; }

3.32 https://github.com/bbaranoff/qosmo/blob/main/run_modules/38-bruit.sh

9837 octets, 202 lignes → 202 lignes

# 38-bruit.sh - noise injector on the DOWNLINK direction. OFF BY DEFAULT.
#
# Inserts tools/injecteur_bruit.py between the bridge and the layer 1 of THIS
# plan's QEMU, when the operator sets BRUIT_MODE (or BRUIT_SNR_DB alone).
# Numbered 38: before 40-qemu, because this module puts into the environment
# variables that QEMU must inherit (CALYPSO_BSP_PORT, CALYPSO_BSP_SNR_DB).
#
#   BRUIT_MODE     ber | souple | iq | relais (empty = module disabled)
#                    ber     flipped bits, rate BRUIT_BER (BRUIT_RAFALES=L:
#                            in bursts of L bits, same average rate)
#                    souple  AWGN at BRUIT_SNR_DB then hard decision (the stream
#                            has no soft values: hard bits or L2 bytes)
#                    iq      EXPERIMENTAL: bits -> GMSK I/Q + AWGN (BRUIT_SNR_DB)
#                    relais  nothing altered: checks the insertion alone
#   BRUIT_BER      flip rate, 0..1                   (ber mode)
#   BRUIT_RAFALES  average burst length, bits (ber mode, optional)
#   BRUIT_SNR_DB   SNR in dB, P/(2 sigma^2) per sample (souple, iq)
#                  SET WITHOUT BRUIT_MODE: no injector; the module exports
#                  CALYPSO_BSP_SNR_DB (AWGN of the C54x core, calypso_c54x.c
#                  c54x_bsp_load), which only acts on a QEMU built with
#                  --enable-l1-dsp (the external DSP, c54x_exe, gets it from
#                  c54x_exe/run.sh).
#   BRUIT_PERTE    probability of erasing a burst (dsp) / a block (grgsm)
#   BRUIT_TN       timeslots to degrade, e.g. 0 or 1,2 (default all)
#   BRUIT_GRAINE   seed: same seed + same frames = same errors; without
#                  BRUIT_MODE, becomes CALYPSO_BSP_BRUIT_GRAINE
#   BRUIT_STATS    statistics period in bruit.log (10 s)
#   BRUIT_OPTS     extra options passed as is
#   BRUIT_CIBLE    auto (default) | dsp | grgsm: QEMU layer 1; auto reads
#                  build/meson-info (l1_dsp / l1_grgsm)
#   BRUIT_PORT_GRGSM  port the injector listens on in grgsm (14730)
#   BRUIT_PORT_DSP    port the BSP is moved to in dsp (16702)
#   BRUIT_PY       path of the injector (default <QEMU_TREE>/tools/injecteur_bruit.py)
#
# WHERE IT IS INSERTED
#   gr-gsm L1 (the build of this bench): QEMU listens for GSMTAP on
#     127.0.0.1:4730, hard-coded (calypso_l1_grgsm.c). The bridge is what
#     changes target: pont/pont.py aims at PONT_GSMTAP_PORT, which it sets
#     itself to BRUIT_PORT_GRGSM (14730) when BRUIT_MODE is ber or relais. The
#     bridge is started by start-native.sh, not by this plan: BRUIT_MODE must
#     therefore be in the environment of start-native.sh
#     (BRUIT_MODE=ber BRUIT_BER=0.01 ./start-native.sh --grgsm).
#     Modes ber and relais only (L2 blocks already decoded, cf. injector).
#   DSP L1 inside QEMU (--enable-l1-dsp): the QEMU BSP listens on BRUIT_PORT_DSP
#     (CALYPSO_BSP_PORT, exported here before 40-qemu), the injector takes
#     127.0.0.1:6702 and receives there what the bridge sends to --dsp-port 6702.
#   DSP setup of start-native.sh (--dsp): qemu is removed from this plan, the
#     module skips itself; c54x_exe/run.sh inserts the injector.
# Injector log: $LOG_DIR/bruit.log. Details:
# osmo-operator/wiki/Injecteur-bruit.md.
MOD_REGISTER bruit "Injecteur de bruit (sens descendant)"
MOD_REQUIRED[bruit]=0
MOD_DEPS[bruit]="logs"
MOD_TIMEOUT[bruit]=10
MOD_ENABLED_IF[bruit]='[ -n "${BRUIT_MODE:-}${BRUIT_SNR_DB:-}" ]'

_TM[fr:bruit]="Injecteur de bruit (sens descendant)"
_TM[en:bruit]="Noise injector (downlink)"

: "${BRUIT_PY:=${QEMU_TREE:-/opt/GSM/qosmo}/tools/injecteur_bruit.py}"
: "${BRUIT_PORT_DSP:=16702}"
: "${BRUIT_PORT_GRGSM:=14730}"
: "${BRUIT_STATS:=10}"

_bruit_log() { printf '%s/bruit.log' "${LOG_DIR:-/tmp/calypso/logs}"; }
_bruit_pidf() { printf '%s/bruit.pid' "${RUN_DIR:-/tmp/calypso}"; }

# The layer 1 of this plan's QEMU: BRUIT_CIBLE, otherwise the build options.
_bruit_l1() {
    case "${BRUIT_CIBLE:-auto}" in dsp|grgsm) printf '%s' "$BRUIT_CIBLE"; return 0 ;; esac
    local j
    j="$(dirname "${QEMU_BIN:-/nonexistent}")/meson-info/intro-buildoptions.json"
    python3 -c 'import json, sys
d = {o["name"]: o["value"] for o in json.load(open(sys.argv[1]))}
print("dsp" if d.get("l1_dsp") == "enabled" else "grgsm", end="")' "$j" 2>/dev/null || printf 'grgsm'
}

# Is qemu in the plan being run? (start-native.sh --dsp removes it: --skip qemu,...)
_bruit_qemu_au_plan() {
    local m
    for m in "${selected[@]:-}"; do [ "$m" = qemu ] && return 0; done
    return 1
}

_bruit_args() {
    BRUIT_ARGS=(--mode "$BRUIT_MODE" --etiquette qosmo --stats "$BRUIT_STATS")
    if [ "$(_bruit_l1)" = dsp ]; then
        BRUIT_ARGS+=(--cible dsp --ecoute 127.0.0.1:6702 --vers "127.0.0.1:$BRUIT_PORT_DSP")
    else
        BRUIT_ARGS+=(--cible grgsm --ecoute "127.0.0.1:${PONT_GSMTAP_PORT:-$BRUIT_PORT_GRGSM}"
                     --vers "127.0.0.1:${PORT_GSMTAP_SI:-4730}")
    fi
    [ -n "${BRUIT_BER:-}" ]     && BRUIT_ARGS+=(--ber "$BRUIT_BER")
    [ -n "${BRUIT_RAFALES:-}" ] && BRUIT_ARGS+=(--rafales "$BRUIT_RAFALES")
    [ -n "${BRUIT_SNR_DB:-}" ]  && BRUIT_ARGS+=(--snr-db "$BRUIT_SNR_DB")
    [ -n "${BRUIT_PERTE:-}" ]   && BRUIT_ARGS+=(--perte "$BRUIT_PERTE")
    [ -n "${BRUIT_TN:-}" ]      && BRUIT_ARGS+=(--tn "$BRUIT_TN")
    [ -n "${BRUIT_GRAINE:-}" ]  && BRUIT_ARGS+=(--graine "$BRUIT_GRAINE")
    # shellcheck disable=SC2206 # BRUIT_OPTS: word splitting intended
    [ -n "${BRUIT_OPTS:-}" ]    && BRUIT_ARGS+=($BRUIT_OPTS)
    return 0
}

_bruit_vivant() {
    [ -n "${BRUIT_MODE:-}" ] || return 1
    local pid; pid="$(cat "$(_bruit_pidf)" 2>/dev/null || echo 0)"
    [ "$pid" != 0 ] && kill -0 "$pid" 2>/dev/null \
        && tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null | grep -q 'injecteur_bruit\.py'
}

mod_bruit_check() {
    if ! _bruit_qemu_au_plan; then
        mod_skip "qemu hors du plan (montage DSP de start-native.sh) : c54x_exe/run.sh intercale l'injecteur"
        return $MOD_RC_SKIP
    fi
    [ -n "${BRUIT_MODE:-}" ] || { mod_ok; return $MOD_RC_OK; }      # BRUIT_SNR_DB alone: nothing to start
    command -v python3 >/dev/null 2>&1 || { mod_fail "python3 introuvable"; return $MOD_RC_FAIL; }
    [ -r "$BRUIT_PY" ] || {
        mod_hint "posez BRUIT_PY=<chemin de injecteur_bruit.py>"
        mod_fail "injecteur introuvable : $BRUIT_PY"; return $MOD_RC_FAIL; }
    local l1; l1="$(_bruit_l1)"
    if [ "$l1" = grgsm ]; then
        case "$BRUIT_MODE" in
            ber|relais) ;;
            *) mod_hint "en gr-gsm, QEMU recoit des blocs L2 deja decodes : BRUIT_MODE=ber (ou relais), BRUIT_PERTE pour des blocs perdus"
               mod_fail "BRUIT_MODE=$BRUIT_MODE impossible avec la L1 gr-gsm (le pont n'est pas redirige, cellule intacte)"
               return $MOD_RC_FAIL ;;
        esac
        if [ "${PONT_GSMTAP_PORT:-$BRUIT_PORT_GRGSM}" = "${PORT_GSMTAP_SI:-4730}" ]; then
            mod_hint "laissez PONT_GSMTAP_PORT vide (14730 par defaut) ou posez BRUIT_PORT_GRGSM"
            mod_fail "l'injecteur ecouterait le port de QEMU (${PORT_GSMTAP_SI:-4730})"
            return $MOD_RC_FAIL
        fi
    fi
    _bruit_args
    mod_say "verification : python3 $BRUIT_PY ${BRUIT_ARGS[*]} --verifier"
    local out
    if ! out="$(python3 "$BRUIT_PY" "${BRUIT_ARGS[@]}" --verifier 2>&1)"; then
        mod_say "$out"
        mod_fail "$(printf '%s\n' "$out" | tail -1)"
        return $MOD_RC_FAIL
    fi
    mod_say "$out"
    mod_ok
}

mod_bruit_status() { _bruit_vivant; }

mod_bruit_start() {
    local l1; l1="$(_bruit_l1)"
    if [ -z "${BRUIT_MODE:-}" ]; then
        # BRUIT_SNR_DB alone: the "physical" noise of the C54x core, in C.
        export CALYPSO_BSP_SNR_DB="${CALYPSO_BSP_SNR_DB:-$BRUIT_SNR_DB}"
        [ -n "${BRUIT_GRAINE:-}" ] && export CALYPSO_BSP_BRUIT_GRAINE="${CALYPSO_BSP_BRUIT_GRAINE:-$BRUIT_GRAINE}"
        if [ "$l1" = dsp ]; then
            mod_say "CALYPSO_BSP_SNR_DB=$CALYPSO_BSP_SNR_DB exporte pour QEMU (L1 DSP) : AWGN sur tous les I/Q descendants"
        else
            mod_say "CALYPSO_BSP_SNR_DB=$CALYPSO_BSP_SNR_DB exporte, mais la L1 de ce QEMU est gr-gsm : SANS EFFET"
            mod_say "  (bruit physique = coeur C54x : montage DSP, c54x_exe/run.sh ; ici : BRUIT_MODE=ber)"
        fi
        mod_ok; return $MOD_RC_OK
    fi
    local log; log="$(_bruit_log)"
    mkdir -p "$(dirname "$log")" "$(dirname "$(_bruit_pidf)")" 2>/dev/null
    : > "$log"
    _bruit_args
    if [ "$l1" = dsp ]; then
        export CALYPSO_BSP_PORT="$BRUIT_PORT_DSP"
        mod_say "CALYPSO_BSP_PORT=$CALYPSO_BSP_PORT exporte : le BSP de QEMU y ecoutera, l'injecteur prend 6702"
    else
        mod_say "L1 gr-gsm : pont -> 127.0.0.1:${PONT_GSMTAP_PORT:-$BRUIT_PORT_GRGSM} (injecteur) -> QEMU ${PORT_GSMTAP_SI:-4730}"
        mod_say "  le pont doit viser ce port : BRUIT_MODE dans l'environnement de start-native.sh (pont/pont.py)"
    fi
    mod_say "commande : python3 -u $BRUIT_PY ${BRUIT_ARGS[*]}"
    mod_say "journal  : $log"
    setsid python3 -u "$BRUIT_PY" "${BRUIT_ARGS[@]}" >>"$log" 2>&1 </dev/null &
    printf '%s\n' "$!" > "$(_bruit_pidf)"
    mod_ok
}

mod_bruit_wait() {
    [ -n "${BRUIT_MODE:-}" ] || { mod_ok; return $MOD_RC_OK; }
    local log; log="$(_bruit_log)"
    wait_until "${MOD_TIMEOUT[bruit]}" "injecteur pret" log_has "$log" "PRET ecoute=" || {
        modb_tail "$log" 15
        mod_hint "lisez $log"
        return $MOD_RC_FAIL; }
    _bruit_vivant || {
        modb_tail "$log" 15
        mod_fail "l'injecteur s'est arrete aussitot lance"
        return $MOD_RC_FAIL; }
    mod_say "$(grep -a 'PRET ecoute=' "$log" | tail -1)"
    mod_ok
}

mod_bruit_stop() {
    local pid; pid="$(cat "$(_bruit_pidf)" 2>/dev/null || echo 0)"
    [ "$pid" != 0 ] && kill "$pid" 2>/dev/null
    pkill -f -- "injecteur_bruit\.py .*--etiquette qosmo" 2>/dev/null
    rm -f "$(_bruit_pidf)"
    return 0
}

3.33 https://github.com/bbaranoff/qosmo/blob/main/run_modules/40-qemu.sh

6811 octets, 145 lignes → 145 lignes

MOD_REGISTER qemu "Émulateur Calypso (QEMU)"
MOD_REQUIRED[qemu]=1
MOD_DEPS[qemu]="prereqs"
MOD_TIMEOUT[qemu]=30

: "${QEMU_LOG_MAX:=$((64 * 1024 * 1024))}"
: "${QEMU_LOG_HEAD:=$((8 * 1024 * 1024))}"

mod_qemu_check() {
    [ -x "${QEMU_BIN:-}" ] || { mod_fail "binaire QEMU absent : ${QEMU_BIN:-<non défini>}"
                                mod_hint "compilez-le : ./configure --target-list=arm-softmmu && ninja -C build qemu-system-arm"
                                return $MOD_RC_FAIL; }
    [ -r "${FIRMWARE_ELF:-}" ] || { mod_fail "firmware ARM introuvable : ${FIRMWARE_ELF:-<non défini>}"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_qemu_status() { have_proc "qemu-system-arm.*calypso"; }

_qemu_save_head() {
    local src="$1" dst="$2" taille="$3" i=0
    while [ "$i" -lt 120 ]; do
        if [ -s "$src" ] && [ "$(stat -c %s "$src" 2>/dev/null || echo 0)" -ge "$taille" ]; then
            head -c "$taille" "$src" > "$dst" 2>/dev/null
            return 0
        fi
        sleep 1; i=$(( i + 1 ))
    done
    [ -s "$src" ] && head -c "$taille" "$src" > "$dst" 2>/dev/null
    return 0
}

_qemu_log_guard() {
    local f="$1" max="$2" qpid="$3" n=0
    while kill -0 "$qpid" 2>/dev/null; do
        if [ -f "$f" ] && [ "$(stat -c %s "$f" 2>/dev/null || echo 0)" -gt "$max" ]; then
            n=$((n+1))
            printf '\n--- journal remis à zéro (%d fois) : plafond de %s octets atteint.\n--- La tête du journal est conservée dans qemu-tete.log ---\n' \
                   "$n" "$max" > "$f"
        fi
        sleep 5
    done
}

mod_qemu_start() {
    local qlog="${LOG_DIR}/qemu.log" qpid
    mod_say "machine  : calypso"
    mod_say "journal  : $qlog (plafond ${QEMU_LOG_MAX} o)"
    # C LAUNCHER `qosmo-grgsm` (tools/qosmo-launch, installed into
    # /usr/local/bin by `make install`). Same command line as below
    # (-M calypso, -cpu arm946, -serial pty x2, -monitor, -kernel), reads
    # l1s/last_rach from the ELF, relays QEMU stdout+stderr into qemu.log
    # (41-pty keeps reading "redirected to /dev/pts/N"), and two stable links:
    # $RUN_DIR/modem.pty (serial0, the one of osmocon) and $RUN_DIR/irda.pty.
    # It forwards SIGTERM to QEMU and dies with it. Without the launcher: the
    # direct qemu-system-arm line.
    # ARM gdbstub ACTIVE here too (tcp::$CALYPSO_GDB_PORT, 1234): QEMU only
    # stops when a client connects, so no cost as long as it is unused. This
    # is what 44-gdb-telnet.sh attaches on demand (telnet localhost 44444,
    # target running). CALYPSO_GDB_PORT=off to disable.
    : "${CALYPSO_GDB_PORT:=1234}"
    # -display none -parallel none, HERE AS IN THE LAUNCHER.
    # The calypso machine has no screen. Without -display, QEMU takes its
    # graphical default (SDL is compiled in) and wires its default devices to
    # virtual consoles; both serial ports being taken (-serial pty) and the
    # monitor too, only the parallel port is left: an empty SDL window titled
    # "parallel0", which grabs the keyboard, while the boot happens on the
    # ptys. -parallel none also removes the chardev (`info chardev` still shows
    # it as `vc` with -display none alone).
    # icount / in_asm trace, off by default (./run.sh --icount / --asm):
    #   QEMU_ICOUNT=auto|shift=N,…   -> -icount
    #   QEMU_ASM=1|FILE              -> -d in_asm -D FILE (default $LOG_DIR/qemu-asm.log)
    #   QEMU_ASM_FLAGS=…             replaces in_asm (in_asm,exec,nochain: every execution, too slow for attach)
    #   QEMU_ASM_FILTRE=…            adds -dfilter
    # Passed after `--` to the launcher: also works with an older installed launcher.
    local -a qx=()
    case "${QEMU_ICOUNT:-}" in ''|0|off) ;; *) qx+=(-icount "$QEMU_ICOUNT") ;; esac
    case "${QEMU_ASM:-}" in
        ''|0|off) ;;
        1) qx+=(-d "${QEMU_ASM_FLAGS:-in_asm}" -D "${LOG_DIR}/qemu-asm.log") ;;
        *) qx+=(-d "${QEMU_ASM_FLAGS:-in_asm}" -D "$QEMU_ASM") ;;
    esac
    [ -n "${QEMU_ASM:-}" ] && [ -n "${QEMU_ASM_FILTRE:-}" ] && qx+=(-dfilter "$QEMU_ASM_FILTRE")
    [ "${#qx[@]}" -gt 0 ] && mod_say "extra    : ${qx[*]}"
    local launcher="${QOSMO_LAUNCHER:-/usr/local/bin/qosmo}"
    if [ -x "$launcher" ]; then
        mod_say "lanceur  : $launcher (QOSMO_LAUNCHER) ; gdbstub ARM : $CALYPSO_GDB_PORT"
        setsid "$launcher" --qemu "$QEMU_BIN" -k "$FIRMWARE_ELF" --bin "$FIRMWARE_BIN" \
            --cpu arm946 --gdb "$CALYPSO_GDB_PORT" --rundir "$RUN_DIR" \
            --monitor "${RUN_DIR}/qemu-monitor.sock" -- "${qx[@]}" >>"$qlog" 2>&1 </dev/null &
        qpid=$!
    else
        mod_say "lanceur  : absent ($launcher) — ligne qemu-system-arm directe ; installez-le : make -C ${QEMU_TREE:-.}/tools/qosmo-launch install"
        setsid "$QEMU_BIN" -M calypso -cpu arm946 \
            -display none -parallel none \
            -serial pty -serial pty \
            -monitor "unix:${RUN_DIR}/qemu-monitor.sock,server,nowait" \
            -kernel "$FIRMWARE_ELF" "${qx[@]}" >>"$qlog" 2>&1 </dev/null &
        qpid=$!
    fi
    printf '%s\n' "$qpid" > "${RUN_DIR}/qemu.pid"
    ( trap '' HUP; _qemu_save_head "$qlog" "${LOG_DIR}/qemu-tete.log" "${QEMU_LOG_HEAD}" ) >/dev/null 2>&1 </dev/null &
    ( trap '' HUP; _qemu_log_guard "$qlog" "$QEMU_LOG_MAX" "$qpid" ) >/dev/null 2>&1 </dev/null &
    mod_say "tête     : ${LOG_DIR}/qemu-tete.log"
    mod_ok
}

_qemu_running() {
    local sock="$1"
    printf 'info status\n' \
        | timeout 3 socat -t 1 - "UNIX-CONNECT:$sock" 2>/dev/null \
        | grep -q 'VM status: running'
}

mod_qemu_wait() {
    local sock="${RUN_DIR}/qemu-monitor.sock" qlog="${LOG_DIR}/qemu.log"
    wait_until "${MOD_TIMEOUT[qemu]}" "socket du moniteur QEMU" have_unix "$sock" || return $MOD_RC_FAIL
    local qpid; qpid="$(cat "${RUN_DIR}/qemu.pid" 2>/dev/null || echo 0)"
    if ! kill -0 "$qpid" 2>/dev/null; then
        modb_tail "$qlog" 20
        mod_hint "cause typique : machine type ou firmware invalide"
        mod_fail "QEMU a démarré puis s'est arrêté"
        return $MOD_RC_FAIL
    fi
    if command -v socat >/dev/null 2>&1; then
        wait_until "${MOD_TIMEOUT[qemu]}" "machine virtuelle en marche" _qemu_running "$sock" || {
            modb_tail "$qlog" 20
            mod_hint "socat - UNIX-CONNECT:$sock puis « info status » pour voir l'état de la machine"
            return $MOD_RC_FAIL
        }
    else
        wait_until "${MOD_TIMEOUT[qemu]}" "PTY série alloués" grep -aq 'label serial1' "$qlog" || {
            modb_tail "$qlog" 20
            return $MOD_RC_FAIL
        }
    fi
    mod_ok
}

mod_qemu_stop() {
    local qpid; qpid="$(cat "${RUN_DIR}/qemu.pid" 2>/dev/null || echo 0)"
    [ "$qpid" != 0 ] && kill "$qpid" 2>/dev/null
    pkill -f "qemu-system-arm.*calypso" 2>/dev/null
    rm -f "${RUN_DIR}/qemu.pid" "${RUN_DIR}/qemu-monitor.sock"
    return 0
}

3.34 https://github.com/bbaranoff/qosmo/blob/main/run_modules/41-pty.sh

2592 octets, 75 lignes → 75 lignes

MOD_REGISTER pty "PTY série de l'émulateur"
MOD_REQUIRED[pty]=1
MOD_DEPS[pty]="qemu"
MOD_TIMEOUT[pty]=60

: "${QEMU_MON_SOCK:=${RUN_DIR:-/tmp/calypso}/qemu-monitor.sock}"

_pty_from_monitor() {
    local sock="$1" label="$2"
    [ -S "$sock" ] || return 1
    command -v socat >/dev/null 2>&1 || return 1
    printf 'info chardev\n' \
        | timeout 3 socat -t 1 - "UNIX-CONNECT:$sock" 2>/dev/null \
        | grep -E "^${label}:" \
        | grep -oE '/dev/pts/[0-9]+' \
        | head -1
}

_pty_from_log() {
    local log="$1" label="$2"
    [ -r "$log" ] || return 1
    grep -a "redirected to /dev/pts/.* (label ${label})" "$log" 2>/dev/null \
        | grep -oE '/dev/pts/[0-9]+' \
        | head -1
}

_pty_resolve() {
    local log="${LOG_DIR}/qemu.log" p
    if [ -z "${OSMOCON_PTY:-}" ] || [ ! -c "${OSMOCON_PTY:-/nonexistent}" ]; then
        p="$(_pty_from_monitor "$QEMU_MON_SOCK" serial0)"
        [ -n "$p" ] || p="$(_pty_from_log "$log" serial0)"
        [ -n "$p" ] && OSMOCON_PTY="$p"
    fi
    [ -n "${OSMOCON_PTY:-}" ] && [ -c "$OSMOCON_PTY" ]
}

mod_pty_check() {
    if ! command -v socat >/dev/null 2>&1; then
        mod_say "socat absent : lecture du moniteur impossible, secours par le journal seulement"
    fi
    [ -S "$QEMU_MON_SOCK" ] || [ -r "${LOG_DIR}/qemu.log" ] || {
        mod_hint "le module qemu doit avoir démarré : ./run.sh --only qemu"
        mod_fail "ni moniteur ($QEMU_MON_SOCK) ni journal QEMU : rien à interroger"
        return $MOD_RC_FAIL
    }
    mod_ok
}

mod_pty_status() { return 1; }
mod_pty_start() { _pty_resolve; mod_ok; }

mod_pty_wait() {
    local deadline=$(( SECONDS + ${MOD_TIMEOUT[pty]} ))
    local qpid; qpid="$(cat "${RUN_DIR:-/tmp/calypso}/qemu.pid" 2>/dev/null || echo 0)"
    while (( SECONDS < deadline )); do
        if _pty_resolve; then
            export OSMOCON_PTY
            mod_say "serial0=$OSMOCON_PTY"
            mod_ok
            return $MOD_RC_OK
        fi
        if [ "$qpid" != 0 ] && ! kill -0 "$qpid" 2>/dev/null; then
            modb_tail "${LOG_DIR}/qemu.log" 20
            mod_hint "cause typique : machine type ou firmware invalide"
            mod_fail "QEMU (pid $qpid) s'est arrêté avant d'allouer son PTY"
            return $MOD_RC_FAIL
        fi
        sleep 0.5
    done
    mod_hint "vérifiez que QEMU est lancé avec « -serial pty » ; sinon : socat - UNIX-CONNECT:$QEMU_MON_SOCK puis « info chardev »"
    mod_fail "aucun PTY serial0 après ${MOD_TIMEOUT[pty]}s (moniteur $QEMU_MON_SOCK et journal muets)"
    return $MOD_RC_FAIL
}

mod_pty_stop() { return 0; }

3.35 https://github.com/bbaranoff/qosmo/blob/main/run_modules/44-gdb-telnet.sh

3792 octets, 71 lignes → 71 lignes

# =============================================================================
#  44-gdb-telnet — gdb console of the ARM, served over telnet, non-blocking
# =============================================================================
#  ROLE      Keep a server (tools/gdb-telnet.py) which, on each
#            `telnet localhost 44444` connection, attaches a gdb-multiarch to
#            the QEMU gdbstub (-gdb tcp::$CALYPSO_GDB_PORT) with the target
#            RUNNING (`continue &`). Ctrl-C stops the ARM, `continue &` resumes,
#            quit closes the session: gdb is killed and QEMU resumes the ARM.
#            As long as nobody is connected, nothing touches the target: it is
#            used on demand.
#  REQUIRES  QEMU started with its gdbstub (qemu module; the qosmo-dsp launcher
#            exports CALYPSO_GDB_PORT and CALYPSO_GDB_ELF).
#  SUCCESS   The telnet port is listening.
#  LOG       $LOG_DIR/gdb-telnet.log
#  SETTINGS  CALYPSO_GDB_TELNET_PORT (default 44444, 0 = no server)
#            CALYPSO_GDB_TELNET_BIND (default 127.0.0.1: a gdb console
#            controls the machine, it is not exposed to the network unwittingly)
# -----------------------------------------------------------------------------
MOD_REGISTER gdbtelnet "Console gdb ARM en telnet (non bloquante)"
MOD_REQUIRED[gdbtelnet]=0
MOD_DEPS[gdbtelnet]="qemu"
MOD_TIMEOUT[gdbtelnet]=10
MOD_ENABLED_IF[gdbtelnet]='[ "${CALYPSO_GDB_TELNET_PORT:-44444}" != 0 ] && [ -n "${CALYPSO_GDB_PORT:-}" ]'

# The QEMU gdbstub is itself non-blocking: QEMU runs normally as long as nobody
# is connected to it. 1234 = the port the launcher (qosmo-dsp/qosmo-grgsm)
# passes to -gdb; setting it here guarantees 40-qemu and this module agree.
: "${CALYPSO_GDB_PORT:=1234}"
: "${CALYPSO_GDB_TELNET_PORT:=44444}"
: "${CALYPSO_GDB_TELNET_BIND:=127.0.0.1}"
: "${GDB_TELNET_PY:=${QEMU_TREE:-.}/tools/gdb-telnet.py}"

_gdbtel_log() { printf '%s\n' "${LOG_DIR:-/root/calypso/logs}/gdb-telnet.log"; }

mod_gdbtelnet_check() {
    [ -r "$GDB_TELNET_PY" ] || { mod_skip "gdb-telnet.py absent ($GDB_TELNET_PY)"; return $MOD_RC_SKIP; }
    command -v gdb-multiarch >/dev/null 2>&1 || {
        mod_hint "apt-get install gdb-multiarch — ou CALYPSO_GDB_TELNET_PORT=0"
        mod_fail "gdb-multiarch introuvable"; return $MOD_RC_FAIL; }
    command -v python3 >/dev/null 2>&1 || { mod_fail "python3 introuvable"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_gdbtelnet_status() { have_proc "gdb-telnet.py" && have_port "$CALYPSO_GDB_TELNET_PORT"; }

mod_gdbtelnet_start() {
    local log; log="$(_gdbtel_log)"
    mod_say "telnet   : $CALYPSO_GDB_TELNET_BIND:$CALYPSO_GDB_TELNET_PORT   (telnet localhost $CALYPSO_GDB_TELNET_PORT)"
    mod_say "gdbstub  : 127.0.0.1:$CALYPSO_GDB_PORT"
    mod_say "elf      : ${CALYPSO_GDB_ELF:-${FIRMWARE_ELF:-}}"
    setsid python3 "$GDB_TELNET_PY" --port "$CALYPSO_GDB_TELNET_PORT" --bind "$CALYPSO_GDB_TELNET_BIND" \
        --stub "$CALYPSO_GDB_PORT" --elf "${CALYPSO_GDB_ELF:-${FIRMWARE_ELF:-}}" \
        </dev/null >>"$log" 2>&1 &
    printf '%s\n' "$!" > "${RUN_DIR:-/tmp/calypso}/gdb-telnet.pid"
    mod_ok
}

mod_gdbtelnet_wait() {
    wait_until "${MOD_TIMEOUT[gdbtelnet]}" "port telnet $CALYPSO_GDB_TELNET_PORT" have_port "$CALYPSO_GDB_TELNET_PORT" || {
        mod_hint "regardez $(_gdbtel_log) — port deja pris ? CALYPSO_GDB_TELNET_PORT=autre"
        mod_fail "le serveur gdb-telnet n'ecoute pas"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_gdbtelnet_stop() {
    local pid; pid="$(cat "${RUN_DIR:-/tmp/calypso}/gdb-telnet.pid" 2>/dev/null || echo 0)"
    [ "$pid" != 0 ] && kill "$pid" 2>/dev/null
    for p in $(pgrep -f 'tools/gdb-telnet.py' 2>/dev/null); do [ "$p" = "$$" ] || kill "$p" 2>/dev/null; done
    rm -f "${RUN_DIR:-/tmp/calypso}/gdb-telnet.pid"
    return 0
}

3.36 https://github.com/bbaranoff/qosmo/blob/main/run_modules/50-osmocon.sh

3011 octets, 77 lignes → 77 lignes

MOD_REGISTER osmocon "Passerelle osmocon"
MOD_REQUIRED[osmocon]=1
MOD_DEPS[osmocon]="qemu pty"
MOD_TIMEOUT[osmocon]=30

: "${OSMOCON_MODEL:=romload}"
: "${OSMOCON_INTER_BYTE_DELAY:=100}"
: "${OSMOCON_DEBUG:=tr}"
: "${L1CTL_SOCK_PATH:=/tmp/osmocom_l2}"

_osmocon_log() { printf '%s\n' "${LOG_DIR}/osmocon.log"; }
_osmocon_vivant() { pgrep -f "osmocon .*-p " >/dev/null 2>&1; }

mod_osmocon_check() {
    [ -x "${OSMOCON:-}" ] || {
        mod_hint "réglez OSMOCOM_BB dans environnement/bench.env, ou compilez osmocon"
        mod_fail "osmocon introuvable : ${OSMOCON:-<non défini>}"; return $MOD_RC_FAIL; }
    [ -r "${FIRMWARE_BIN:-}" ] || {
        mod_hint "réglez FIRMWARE_BIN dans environnement/bench.env"
        mod_fail "image firmware illisible : ${FIRMWARE_BIN:-<non défini>}"; return $MOD_RC_FAIL; }
    [ -n "${OSMOCON_PTY:-}" ] && [ -c "$OSMOCON_PTY" ] || {
        mod_hint "le module « pty » doit avoir résolu le port série de QEMU ; regardez son journal"
        mod_fail "PTY série indisponible : ${OSMOCON_PTY:-<non résolu>}"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_osmocon_status() { _osmocon_vivant && have_unix "$L1CTL_SOCK_PATH"; }

mod_osmocon_start() {
    local log; log="$(_osmocon_log)"
    rm -f "$L1CTL_SOCK_PATH" 2>/dev/null
    mod_say "PTY      : $OSMOCON_PTY"
    mod_say "firmware : $FIRMWARE_BIN"
    mod_say "modèle   : $OSMOCON_MODEL"
    mod_say "L1CTL    : $L1CTL_SOCK_PATH"
    setsid stdbuf -oL -eL "$OSMOCON" \
        -m "$OSMOCON_MODEL" \
        -i "$OSMOCON_INTER_BYTE_DELAY" \
        -p "$OSMOCON_PTY" \
        -s "$L1CTL_SOCK_PATH" \
        "$FIRMWARE_BIN" \
        -d "$OSMOCON_DEBUG" >>"$log" 2>&1 </dev/null &
    printf '%s\n' "$!" > "${RUN_DIR:-/tmp/calypso}/osmocon.pid"
    mod_ok
}

mod_osmocon_wait() {
    local log; log="$(_osmocon_log)"
    wait_until 5 "processus osmocon" _osmocon_vivant || {
        modb_tail "$log" 15
        mod_hint "PTY $OSMOCON_PTY refusé ? regardez $log"
        mod_fail "osmocon s'est arrêté aussitôt lancé"
        return $MOD_RC_FAIL
    }
    if ! wait_until "${MOD_TIMEOUT[osmocon]}" "socket L1CTL $L1CTL_SOCK_PATH" \
         have_unix "$L1CTL_SOCK_PATH"; then
        modb_tail "$log" 15
        if _osmocon_vivant; then
            mod_hint "osmocon vit mais n'a pas fini son romload : modèle inadapté (OSMOCON_MODEL=$OSMOCON_MODEL — « romload » attendu sous QEMU) ou PTY qui ne mène pas au baseband"
            mod_fail "aucune socket L1CTL : le firmware n'a pas été chargé"
        else
            mod_hint "regardez $log"
            mod_fail "osmocon est mort pendant le chargement"
        fi
        return $MOD_RC_FAIL
    fi
    mod_say "firmware chargé, socket L1CTL ouverte"
    mod_ok
}

mod_osmocon_stop() {
    local pid; pid="$(cat "${RUN_DIR:-/tmp/calypso}/osmocon.pid" 2>/dev/null || echo 0)"
    [ "$pid" != 0 ] && kill "$pid" 2>/dev/null
    pkill -f "osmocon .*-p " 2>/dev/null
    rm -f "${RUN_DIR:-/tmp/calypso}/osmocon.pid" "$L1CTL_SOCK_PATH"
    return 0
}

3.37 https://github.com/bbaranoff/qosmo/blob/main/run_modules/58-sidecar-faketrx.sh

2717 octets, 64 lignes → 64 lignes

. "$(dirname "${BASH_SOURCE[0]}")/_lib/radio.sh"
. "$(dirname "${BASH_SOURCE[0]}")/_lib/modem.sh"

MOD_REGISTER sidecar-faketrx "Transceiver simulé du BTS#1"
MOD_REQUIRED[sidecar-faketrx]=1
MOD_DEPS[sidecar-faketrx]="sidecar-cfg"
MOD_TIMEOUT[sidecar-faketrx]=20

: "${FAKETRX_PY:=${OSMOCOM_BB:-${GSM_ROOT}/osmocom-bb}/src/target/trx_toolkit/fake_trx.py}"
: "${FAKETRX_PYTHON:=python3}"

mod_sidecar_faketrx_check() {
    command -v "$FAKETRX_PYTHON" >/dev/null 2>&1 || {
        mod_fail "interpréteur introuvable : $FAKETRX_PYTHON"; return $MOD_RC_FAIL; }
    [ -r "$FAKETRX_PY" ] || {
        mod_hint "posez FAKETRX_PY (trx_toolkit/fake_trx.py d'osmocom-bb)"
        mod_fail "fake_trx.py illisible : $FAKETRX_PY"; return $MOD_RC_FAIL; }
    local p ports="$SC_TRX_PORT $SC_BB_PORT"
    modem_enabled && ports="$ports $MODEM_BB_PORT"
    for p in $ports; do
        if radio_udp_bound "$p" && ! radio_alive sidecar-faketrx; then
            mod_hint "ss -uanp | grep :$p — arrêtez le tenant, ou décalez SC_TRX_PORT/SC_BB_PORT"
            mod_fail "port UDP $p déjà pris par un autre processus"; return $MOD_RC_FAIL
        fi
    done
    mod_ok
}

mod_sidecar_faketrx_status() { radio_alive sidecar-faketrx; }

mod_sidecar_faketrx_start() {
    radio_dirs
    local log; log="$(radio_log sidecar-faketrx)"
    mod_say "fake_trx -P $SC_TRX_PORT (BTS#1) -p $SC_BB_PORT (bande de base)"
    # Second MS on the same BTS#1: the GPRS modem. fake_trx multiplexes itself
    # (--trx NAME@ADDR:PORT, independent transceiver); the modem's trxcon on that port.
    local -a extra=()
    if modem_enabled; then
        extra=(--trx "MODEM@${TRX_BB_IP}:${MODEM_BB_PORT}")
        mod_say "second MS (modem GPRS) : --trx ${TRX_BB_IP}:${MODEM_BB_PORT}"
    fi
    mod_say "journal  : $log"
    setsid stdbuf -oL -eL "$FAKETRX_PYTHON" "$FAKETRX_PY" \
        -b "$TRX_BIND_IP" -R "$TRX_BTS_IP" -r "$TRX_BB_IP" \
        -P "$SC_TRX_PORT" -p "$SC_BB_PORT" "${extra[@]}" >>"$log" 2>&1 </dev/null &
    radio_save_pid sidecar-faketrx $!
    mod_ok
}

mod_sidecar_faketrx_wait() {
    local log; log="$(radio_log sidecar-faketrx)"
    wait_until "${MOD_TIMEOUT[sidecar-faketrx]}" "port UDP $SC_TRX_PORT (côté BTS#1)" \
        radio_udp_bound "$SC_TRX_PORT" || {
        modb_tail "$log" 20
        mod_hint "fake_trx a-t-il démarré ? $log"
        mod_fail "port $SC_TRX_PORT jamais lié"; return $MOD_RC_FAIL; }
    wait_until 10 "port UDP $SC_BB_PORT (côté bande de base)" \
        radio_udp_bound "$SC_BB_PORT" || {
        mod_fail "côté BTS lié mais pas le côté bande de base ($SC_BB_PORT)"
        return $MOD_RC_FAIL; }
    mod_ok
}

mod_sidecar_faketrx_stop() { radio_kill sidecar-faketrx; return 0; }

3.38 https://github.com/bbaranoff/qosmo/blob/main/run_modules/59-sidecar-bts.sh

4023 octets, 98 lignes → 98 lignes

. "$(dirname "${BASH_SOURCE[0]}")/_lib/radio.sh"
. "$(dirname "${BASH_SOURCE[0]}")/_lib/temps_reel.sh"

MOD_REGISTER sidecar-bts "Station de base BTS#1 (side-car)"
MOD_REQUIRED[sidecar-bts]=1
MOD_DEPS[sidecar-bts]="sidecar-faketrx bsc"
MOD_TIMEOUT[sidecar-bts]=30

: "${SC_BTS_BIN:=osmo-bts-trx}"
: "${SC_BTS_STAB_SECS:=8}"
: "${SC_BTS_RETRIES:=3}"

_sc_bts_mort() {
    grep -qE "No clock since TRX|BTS_SHUTDOWN.*Shutting down" "$(radio_log sidecar-bts)" 2>/dev/null
}

mod_sidecar_bts_check() {
    command -v "$SC_BTS_BIN" >/dev/null 2>&1 || {
        mod_fail "binaire introuvable : $SC_BTS_BIN"; return $MOD_RC_FAIL; }
    [ -r "$SC_BTS_CFG" ] || {
        mod_hint "le module « sidecar-cfg » doit avoir posé ce fichier"
        mod_fail "configuration illisible : $SC_BTS_CFG"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_sidecar_bts_status() { radio_alive sidecar-bts; }

# osmo-bts-trx exits (exit 1) on "PC clock skew too high" (an osmo-bts guard, not
# tunable); the BSC then loses the OML, fake_trx receives POWEROFF and MS#2 loses
# its cell and stays in "No service" (C6, sync timeout). The retries in _start
# only apply at startup. This watchdog restarts osmo-bts-trx as long as the stop
# marker does not exist (set by _stop). The pid in sidecar-bts.pid follows the
# current BTS (radio_alive, status). SC_BTS_RESPAWN=0 disables the watchdog.
_sc_bts_watchdog() {
    local log="$1" marqueur="$RUN_DIR/sidecar-bts.stop"
    rm -f "$marqueur"
    (
        local p
        while [ ! -e "$marqueur" ]; do
            p="$(radio_pid sidecar-bts)"
            if [ "$p" != 0 ] && kill -0 "$p" 2>/dev/null; then sleep 2; continue; fi
            [ -e "$marqueur" ] && break
            printf '%s [watchdog] BTS#1 absente (%s) : relance\n' "$(date +%T)" \
                "$(grep -o 'reason: [^(]*' "$log" 2>/dev/null | tail -1 | tr -d '\n')" >> "$log"
            setsid stdbuf -oL -eL $(rt_prefixe) "$SC_BTS_BIN" -c "$SC_BTS_CFG" >>"$log" 2>&1 </dev/null &
            radio_save_pid sidecar-bts $!
            sleep "$SC_BTS_STAB_SECS"
        done
    ) >/dev/null 2>&1 &
    radio_save_pid sidecar-bts-watchdog $!
    mod_say "chien de garde BTS#1 (pid $!) : relance automatique si osmo-bts-trx s'arrête (SC_BTS_RESPAWN=0 pour l'enlever)"
}

mod_sidecar_bts_start() {
    radio_dirs
    local log; log="$(radio_log sidecar-bts)"
    local essai=0 reste
    while :; do
        essai=$(( essai + 1 ))
        : > "$log"
        setsid stdbuf -oL -eL $(rt_prefixe) "$SC_BTS_BIN" -c "$SC_BTS_CFG" >>"$log" 2>&1 </dev/null &
        radio_save_pid sidecar-bts $!
        reste="$SC_BTS_STAB_SECS"
        while [ "$reste" -gt 0 ]; do
            radio_alive sidecar-bts || break
            _sc_bts_mort && break
            sleep 1; reste=$(( reste - 1 ))
        done
        if [ "$reste" -eq 0 ] && radio_alive sidecar-bts && ! _sc_bts_mort; then
            mod_say "BTS#1 stable après ${SC_BTS_STAB_SECS}s (essai $essai, $(rt_etat "$(radio_pid sidecar-bts)"))"
            [ "${SC_BTS_RESPAWN:-1}" = 1 ] && _sc_bts_watchdog "$log"
            mod_ok; return $MOD_RC_OK
        fi
        radio_kill sidecar-bts
        if [ "$essai" -ge "$SC_BTS_RETRIES" ]; then
            modb_tail "$log" 20
            mod_hint "fake_trx publie-t-il son horloge ? voir $(radio_log sidecar-faketrx)"
            mod_fail "BTS#1 instable après $essai tentatives (course « No clock »)"
            return $MOD_RC_FAIL
        fi
        mod_say "BTS#1 tombé (« No clock ») — nouvelle tentative $essai/$SC_BTS_RETRIES"
    done
}

mod_sidecar_bts_wait() {
    wait_until "${MOD_TIMEOUT[sidecar-bts]}" "VTY du BTS#1 ($SC_BTS_VTY_PORT)" \
        have_port "$SC_BTS_VTY_PORT" || {
        modb_tail "$(radio_log sidecar-bts)" 20
        mod_fail "VTY $SC_BTS_VTY_PORT jamais en écoute"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_sidecar_bts_stop() {
    touch "$RUN_DIR/sidecar-bts.stop" 2>/dev/null    # the watchdog stops restarting
    radio_kill sidecar-bts-watchdog
    radio_kill sidecar-bts
    return 0
}

3.39 https://github.com/bbaranoff/qosmo/blob/main/run_modules/60-bts.sh

4904 octets, 114 lignes → 114 lignes

. "$(dirname "${BASH_SOURCE[0]}")/_lib/temps_reel.sh"

MOD_REGISTER bts "Station de base BTS#0 (osmo-bts-trx, pont)"
MOD_REQUIRED[bts]=0
MOD_DEPS[bts]="bsc"
MOD_TIMEOUT[bts]=30

: "${OSMO_BTS_TRX:=}"
: "${BTS_CFG:=${OSMOCOM_CFG:-/etc/osmocom}/osmo-bts-trx.cfg}"
: "${BTS_LOG:=${LOG_DIR}/bts.log}"
: "${BTS_VTY_PORT:=4241}"

mod_bts_check() {
    if [ -z "$OSMO_BTS_TRX" ] || [ ! -x "$OSMO_BTS_TRX" ]; then
        OSMO_BTS_TRX="$(command -v osmo-bts-trx 2>/dev/null)"
    fi
    [ -n "$OSMO_BTS_TRX" ] && [ -x "$OSMO_BTS_TRX" ] || {
        mod_hint "installez osmo-bts (cible trx) ou posez OSMO_BTS_TRX=<chemin>"
        mod_fail "osmo-bts-trx introuvable"
        return $MOD_RC_FAIL
    }
    [ -r "$BTS_CFG" ] || {
        mod_hint "attendu : <OSMOCOM_CFG>/osmo-bts-trx.cfg — ou posez BTS_CFG=<chemin>"
        mod_fail "configuration de la BTS illisible : $BTS_CFG"
        return $MOD_RC_FAIL
    }
    have_port "${BSC_VTY_PORT:-4242}" || mod_say "AVERTISSEMENT : le VTY du BSC (${BSC_VTY_PORT:-4242}) ne répond pas — l'OML risque de boucler"
    mod_say "binaire=$OSMO_BTS_TRX cfg=$BTS_CFG"
    mod_ok
}

_bts_est_le_notre() {
    tr '\0' ' ' < "/proc/$1/cmdline" 2>/dev/null | grep -qF "$BTS_CFG"
}

mod_bts_status() {
    local pid; pid="$(cat "${RUN_DIR}/bts.pid" 2>/dev/null || echo 0)"
    if [ "$pid" != 0 ] && kill -0 "$pid" 2>/dev/null && _bts_est_le_notre "$pid"; then
        return 0
    fi
    local p
    for p in $(pgrep -x osmo-bts-trx 2>/dev/null); do
        _bts_est_le_notre "$p" && return 0
    done
    return 1
}

mod_bts_start() {
    mkdir -p "${RUN_DIR:-/tmp/calypso}" "$(dirname "$BTS_LOG")" 2>/dev/null || true
    : > "$BTS_LOG" 2>/dev/null || true
    # GUARD (BTS_GARDIEN=1 by default): osmo-bts-trx EXITS (exit 1) on "PC clock skew too high"
    # when the bridge clock, paced by the DSP, stalls for more than 50 frames; without the guard
    # BTS0 is gone, no more downlink, network lost until the bench is restarted.
    # The loop restarts it after BTS_GARDIEN_PAUSE s; it waits if another BTS with the same config
    # is already running (c54x_exe/run.sh also restarts it with --bts). The pid in the file is the
    # loop's: mod_bts_stop kills the loop first, then the BTS.
    if [ "${BTS_GARDIEN:-1}" = 1 ]; then
        setsid bash -c '
            bin="$1" cfg="$2" log="$3" pause="$4"; shift 4
            while :; do
                if pgrep -f -- "osmo-bts-trx -c $cfg\$" >/dev/null 2>&1; then sleep 2; continue; fi
                stdbuf -oL -eL "$@" "$bin" -c "$cfg" </dev/null; rc=$?
                printf "\n[gardien bts] %s : osmo-bts-trx sorti (code %s), relance dans %s s\n" "$(date +%T)" "$rc" "$pause"
                sleep "$pause"
            done' gardien-bts "$OSMO_BTS_TRX" "$BTS_CFG" "$BTS_LOG" "${BTS_GARDIEN_PAUSE:-3}" $(rt_prefixe) \
            >>"$BTS_LOG" 2>&1 </dev/null &
    else
        setsid stdbuf -oL -eL $(rt_prefixe) "$OSMO_BTS_TRX" -c "$BTS_CFG" >>"$BTS_LOG" 2>&1 </dev/null &
    fi
    printf '%s\n' "$!" > "${RUN_DIR:-/tmp/calypso}/bts.pid"
    mod_ok
}

mod_bts_wait() {
    local pid; pid="$(cat "${RUN_DIR:-/tmp/calypso}/bts.pid" 2>/dev/null || echo 0)"
    if ! wait_until "${MOD_TIMEOUT[bts]}" "VTY de la BTS ($BTS_VTY_PORT)" have_port "$BTS_VTY_PORT"; then
        modb_tail "$BTS_LOG" 25
        if [ "$pid" != 0 ] && ! kill -0 "$pid" 2>/dev/null; then
            mod_hint "causes fréquentes : phy/instance absente de $BTS_CFG, ou unit-id déjà pris par une autre BTS"
            mod_fail "osmo-bts-trx s'est arrêté pendant son initialisation"
        else
            mod_hint "détail : $BTS_LOG"
            mod_fail "osmo-bts-trx tourne mais n'expose pas son VTY après ${MOD_TIMEOUT[bts]}s"
        fi
        return $MOD_RC_FAIL
    fi
    if [ "$pid" != 0 ] && ! kill -0 "$pid" 2>/dev/null; then
        modb_tail "$BTS_LOG" 25
        mod_hint "une BTS résiduelle (unité systemd ?) tient le VTY : systemctl stop osmo-bts-trx"
        mod_fail "le VTY $BTS_VTY_PORT répond, mais ce n'est PAS notre processus (il est mort)"
        return $MOD_RC_FAIL
    fi
    if grep -qi 'rsl' "$BTS_LOG" 2>/dev/null; then
        mod_say "RSL mentionné dans $BTS_LOG (lien vers le BSC en cours d'établissement)"
    else
        mod_say "aucune mention de RSL dans $BTS_LOG pour l'instant — surveillez l'OML côté BSC"
    fi
    mod_say "ordonnancement : $(rt_etat "$pid")"
    mod_ok
}

mod_bts_stop() {
    local pidf="${RUN_DIR:-/tmp/calypso}/bts.pid" pid _p
    pid="$(cat "$pidf" 2>/dev/null || echo 0)"
    [ "$pid" != 0 ] && kill "$pid" 2>/dev/null
    for _p in $(pgrep -f '^bash -c .*gardien-bts' 2>/dev/null) $(pgrep -f "gardien-bts .*$BTS_CFG" 2>/dev/null); do
        _bts_est_le_notre "$_p" && kill "$_p" 2>/dev/null
    done
    for _p in $(pgrep -x osmo-bts-trx 2>/dev/null); do
        _bts_est_le_notre "$_p" && kill "$_p" 2>/dev/null
    done
    rm -f "$pidf"
    return 0
}

3.40 https://github.com/bbaranoff/qosmo/blob/main/run_modules/61-sidecar-pcu.sh

3226 octets, 62 lignes → 62 lignes

: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"
. "$MODDIR/_lib/radio.sh"
. "$MODDIR/_lib/modem.sh"

MOD_REGISTER sidecar-pcu "Second OsmoPCU (BTS#1, GPRS du modem)"
MOD_REQUIRED[sidecar-pcu]=0
MOD_DEPS[sidecar-pcu]="sgsn sidecar-bts"
MOD_TIMEOUT[sidecar-pcu]=30
MOD_ENABLED_IF[sidecar-pcu]='[ "${NO_OSMO_START:-0}" != 1 ] && [ "${CORE_GPRS:-1}" = 1 ] && [ "${MODEM_GPRS:-1}" = 1 ]'

# One osmo-pcu serves only ONE BTS socket. The first one (18-pcu.sh) is on
# /tmp/pcu_bts, the main BTS's; this one takes /tmp/pcu_bts2, the socket that
# BTS#1 declares (osmo-bts-trx-bts1.cfg). Its config is derived from the first
# one's: same policy, other socket, other log, VTY on 127.0.0.2.
_scpcu_gen_cfg() {
    local src; src="$(core_cfg osmo-pcu)"
    [ -r "$src" ] || return 1
    {
        sed -e "s|^log file .*|log file /var/log/osmocom/${SC_PCU_UNIT}.log|" \
            -e "/^ *pcu-socket /d" "$src"
        printf ' pcu-socket %s\n' "$SC_PCU_SOCKET"
        printf 'line vty\n no login\n bind %s\n' "$SC_PCU_VTY_HOST"
    } > "$SC_PCU_CFG.tmp" && mv -f "$SC_PCU_CFG.tmp" "$SC_PCU_CFG"
}

mod_sidecar_pcu_check() {
    core_bin osmo-pcu >/dev/null || { mod_fail "binaire osmo-pcu introuvable"; return $MOD_RC_FAIL; }
    [ -r "$(core_cfg osmo-pcu)" ] || { mod_fail "configuration du premier PCU illisible : $(core_cfg osmo-pcu)"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_sidecar_pcu_status() { core_alive "$SC_PCU_UNIT"; }

mod_sidecar_pcu_start() {
    _scpcu_gen_cfg || { mod_fail "generation de $SC_PCU_CFG impossible"; return $MOD_RC_FAIL; }
    # The "pcu" block must end with pcu-socket: sed removed it if present, and
    # it is rewritten at the end of the pcu block (last node of the source file).
    grep -q "^ pcu-socket $SC_PCU_SOCKET" "$SC_PCU_CFG" || {
        mod_fail "pcu-socket absent de $SC_PCU_CFG"; return $MOD_RC_FAIL; }
    mod_say "osmo-pcu -c $SC_PCU_CFG (socket $SC_PCU_SOCKET, VTY $SC_PCU_VTY_HOST:$SC_PCU_VTY_PORT, nsei $SC_GPRS_NSEI)"
    core_svc_start "$SC_PCU_UNIT" "$(core_bin osmo-pcu)" -c "$SC_PCU_CFG" \
        || { mod_fail "lancement de $SC_PCU_UNIT impossible"; return $MOD_RC_FAIL; }
    [ -e "$SC_PCU_SOCKET" ] && chmod 777 "$SC_PCU_SOCKET" 2>/dev/null
    mod_ok
}

mod_sidecar_pcu_wait() {
    wait_until 5 "processus $SC_PCU_UNIT" core_alive "$SC_PCU_UNIT" || {
        modb_tail "$LOG_DIR/$SC_PCU_UNIT.log" 20
        mod_fail "le second osmo-pcu s'est arrete aussitot lance"; return $MOD_RC_FAIL; }
    wait_until "${MOD_TIMEOUT[sidecar-pcu]}" "liaison PCU <-> BTS#1 ($SC_PCU_SOCKET)" \
        grep -aq "PCU socket $SC_PCU_SOCKET has been connected" "/var/log/osmocom/${SC_PCU_UNIT}.log" || {
        modb_tail "/var/log/osmocom/${SC_PCU_UNIT}.log" 15
        mod_hint "le BTS#1 cree $SC_PCU_SOCKET (osmo-bts-trx-bts1.cfg : pcu-socket) ; bts 1 en « gprs mode egprs » ?"
        mod_fail "le PCU ne s'est pas raccorde au BTS#1"; return $MOD_RC_FAIL; }
    mod_say "pret - Gb : nsei $SC_GPRS_NSEI, bvci $SC_GPRS_BVCI, udp $SC_GPRS_NS_PORT -> $SC_SGSN_IP:$SC_SGSN_PORT"
    mod_ok
}

# By pid only: "osmo-pcu -c" as a pattern would also kill the first PCU.
mod_sidecar_pcu_stop() { core_svc_stop "$SC_PCU_UNIT" "osmo-pcu -c $SC_PCU_CFG"; }

3.41 https://github.com/bbaranoff/qosmo/blob/main/run_modules/67-sidecar-trxcon.sh

1680 octets, 44 lignes → 44 lignes

. "$(dirname "${BASH_SOURCE[0]}")/_lib/radio.sh"

MOD_REGISTER sidecar-trxcon "Couche 1 du MS#2 (trxcon)"
MOD_REQUIRED[sidecar-trxcon]=1
MOD_DEPS[sidecar-trxcon]="sidecar-faketrx"
MOD_TIMEOUT[sidecar-trxcon]=20

: "${SC_TRXCON_BIN:=trxcon}"
: "${SC_TRXCON_FN_ADVANCE:=100}"

mod_sidecar_trxcon_check() {
    command -v "$SC_TRXCON_BIN" >/dev/null 2>&1 || {
        mod_fail "binaire introuvable : $SC_TRXCON_BIN"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_sidecar_trxcon_status() { radio_alive sidecar-trxcon && have_unix "$SC_L2_SOCK"; }

mod_sidecar_trxcon_start() {
    radio_dirs
    local log; log="$(radio_log sidecar-trxcon)"
    rm -f "$SC_L2_SOCK" 2>/dev/null
    mod_say "trxcon -p $SC_BB_PORT -s $SC_L2_SOCK (avance de trame $SC_TRXCON_FN_ADVANCE)"
    mod_say "journal : $log"
    setsid stdbuf -oL -eL "$SC_TRXCON_BIN" -i "$TRX_BIND_IP" -b "$TRX_BB_IP" \
        -p "$SC_BB_PORT" -s "$SC_L2_SOCK" -C 1 -F "$SC_TRXCON_FN_ADVANCE" >>"$log" 2>&1 </dev/null &
    radio_save_pid sidecar-trxcon $!
    mod_ok
}

mod_sidecar_trxcon_wait() {
    local log; log="$(radio_log sidecar-trxcon)"
    wait_until 5 "processus trxcon" radio_alive sidecar-trxcon || {
        modb_tail "$log" 20
        mod_fail "trxcon s'est arrêté aussitôt lancé"; return $MOD_RC_FAIL; }
    wait_until "${MOD_TIMEOUT[sidecar-trxcon]}" "socket L1CTL $SC_L2_SOCK" \
        have_unix "$SC_L2_SOCK" || {
        modb_tail "$log" 20
        mod_hint "fake_trx répond-il sur $SC_BB_PORT ? voir $(radio_log sidecar-faketrx)"
        mod_fail "socket L1CTL jamais créée"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_sidecar_trxcon_stop() { radio_kill sidecar-trxcon; rm -f "$SC_L2_SOCK"; return 0; }

3.42 https://github.com/bbaranoff/qosmo/blob/main/run_modules/68-sidecar-mobile.sh

2124 octets, 53 lignes → 53 lignes

. "$(dirname "${BASH_SOURCE[0]}")/_lib/radio.sh"

MOD_REGISTER sidecar-mobile "Second abonné (MS#2)"
MOD_REQUIRED[sidecar-mobile]=0
MOD_DEPS[sidecar-mobile]="sidecar-trxcon"
MOD_TIMEOUT[sidecar-mobile]=30

: "${SC_MOBILE_BIN:=mobile}"
: "${SC_MOBILE_DELAY:=3}"
: "${CALYPSO_PULSE_LATENCY_MSEC:=320}"

mod_sidecar_mobile_check() {
    command -v "$SC_MOBILE_BIN" >/dev/null 2>&1 || {
        mod_fail "binaire introuvable : $SC_MOBILE_BIN"; return $MOD_RC_FAIL; }
    [ -r "$SC_MOBILE_CFG" ] || {
        mod_hint "le module « sidecar-cfg » doit avoir posé ce fichier"
        mod_fail "configuration illisible : $SC_MOBILE_CFG"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_sidecar_mobile_status() { radio_alive sidecar-mobile; }

mod_sidecar_mobile_start() {
    radio_dirs
    local log; log="$(radio_log sidecar-mobile)"
    wait_until 30 "socket L1CTL $SC_L2_SOCK" have_unix "$SC_L2_SOCK" || {
        mod_fail "socket L1CTL absente : $SC_L2_SOCK"; return $MOD_RC_FAIL; }
    if [ "${SC_MOBILE_DELAY}" != 0 ]; then
        mod_say "attente de ${SC_MOBILE_DELAY}s avant lancement"
        sleep "$SC_MOBILE_DELAY"
    fi
    mod_say "mobile -c $SC_MOBILE_CFG (VTY $SC_MOBILE_VTY_PORT)"
    mod_say "journal : $log"
    mod_say "latence : PULSE_LATENCY_MSEC=${CALYPSO_PULSE_LATENCY_MSEC:-<defaut greffon>}"
    mod_say "debug    : $CALYPSO_MOBILE_DEBUG"
    PULSE_LATENCY_MSEC="$CALYPSO_PULSE_LATENCY_MSEC" \
        setsid stdbuf -oL -eL "$SC_MOBILE_BIN" -c "$SC_MOBILE_CFG" \
        -d "$CALYPSO_MOBILE_DEBUG" >>"$log" 2>&1 </dev/null &
    radio_save_pid sidecar-mobile $!
    mod_ok
}

mod_sidecar_mobile_wait() {
    wait_until "${MOD_TIMEOUT[sidecar-mobile]}" "VTY du MS#2 ($SC_MOBILE_VTY_PORT)" \
        have_port "$SC_MOBILE_VTY_PORT" || {
        modb_tail "$(radio_log sidecar-mobile)" 20
        mod_hint "port déjà pris ? ss -tlnp | grep :$SC_MOBILE_VTY_PORT"
        mod_fail "VTY $SC_MOBILE_VTY_PORT jamais en écoute"; return $MOD_RC_FAIL; }
    mod_say "prêt — telnet 127.0.0.1 $SC_MOBILE_VTY_PORT puis « show ms »"
    mod_ok
}

mod_sidecar_mobile_stop() { radio_kill sidecar-mobile; return 0; }

3.43 https://github.com/bbaranoff/qosmo/blob/main/run_modules/69-modem-trxcon.sh

2555 octets, 55 lignes → 55 lignes

. "$(dirname "${BASH_SOURCE[0]}")/_lib/radio.sh"
. "$(dirname "${BASH_SOURCE[0]}")/_lib/modem.sh"

MOD_REGISTER modem-trxcon "Couche 1 du MS GPRS (trxcon, second MS du fake_trx)"
MOD_REQUIRED[modem-trxcon]=0
MOD_DEPS[modem-trxcon]="sidecar-faketrx"
MOD_TIMEOUT[modem-trxcon]=20
MOD_ENABLED_IF[modem-trxcon]='[ "${MODEM_GPRS:-1}" = 1 ]'

: "${SC_TRXCON_BIN:=trxcon}"

mod_modem_trxcon_check() {
    command -v "$SC_TRXCON_BIN" >/dev/null 2>&1 || {
        mod_fail "binaire introuvable : $SC_TRXCON_BIN"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_modem_trxcon_status() { radio_alive modem-trxcon && have_unix "$MODEM_L2_SOCK"; }

# Same launch as the trxcon of MS#2 (67-sidecar-trxcon.sh), on the TRXD base
# that fake_trx opened for this second MS (--trx ...:MODEM_BB_PORT).
mod_modem_trxcon_start() {
    radio_dirs
    local log; log="$(radio_log modem-trxcon)"
    rm -f "$MODEM_L2_SOCK" 2>/dev/null
    # -f 0 (uplink burst pull advance): on a PDCH, trxcon asks the modem for
    # uplink block X when the downlink block is DECODED (burst X-1,
    # trxcon_fsm.c TRXCON_EV_RX_DATA_IND) but, with the default advance (2),
    # it PULLS that block when burst X-2 arrives: the primitive always comes
    # after the pull and is discarded ("dropping stale Tx prim", one block
    # late every time; N3101 on the PCU side). At 0, the pull happens at burst
    # X: one frame to answer. MS#2 (voice) keeps its own trxcon and advance.
    mod_say "trxcon -p $MODEM_BB_PORT -s $MODEM_L2_SOCK (fbsb +$MODEM_TRXCON_FN_ADVANCE, avance UL $MODEM_TRXCON_TRX_ADVANCE)"
    mod_say "journal : $log"
    setsid stdbuf -oL -eL "$SC_TRXCON_BIN" -i "$TRX_BIND_IP" -b "$TRX_BB_IP" \
        -p "$MODEM_BB_PORT" -s "$MODEM_L2_SOCK" -C 1 -F "$MODEM_TRXCON_FN_ADVANCE" \
        -f "$MODEM_TRXCON_TRX_ADVANCE" >>"$log" 2>&1 </dev/null &
    radio_save_pid modem-trxcon $!
    mod_ok
}

mod_modem_trxcon_wait() {
    local log; log="$(radio_log modem-trxcon)"
    wait_until 5 "processus trxcon (modem)" radio_alive modem-trxcon || {
        modb_tail "$log" 20
        mod_fail "trxcon (modem) s'est arrete aussitot lance"; return $MOD_RC_FAIL; }
    wait_until "${MOD_TIMEOUT[modem-trxcon]}" "socket L1CTL $MODEM_L2_SOCK" \
        have_unix "$MODEM_L2_SOCK" || {
        modb_tail "$log" 20
        mod_hint "fake_trx a-t-il ete lance avec --trx ...:$MODEM_BB_PORT ? voir $(radio_log sidecar-faketrx)"
        mod_fail "socket L1CTL jamais creee"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_modem_trxcon_stop() { radio_kill modem-trxcon; rm -f "$MODEM_L2_SOCK"; return 0; }

3.44 https://github.com/bbaranoff/qosmo/blob/main/run_modules/70-l2.sh

4027 octets, 97 lignes → 97 lignes

MOD_REGISTER l2 "Client de couche 2 (mobile, MS#1)"
MOD_REQUIRED[l2]=0
MOD_DEPS[l2]="osmocon"
MOD_TIMEOUT[l2]=30

: "${L1CTL_SOCK_PATH:=/tmp/osmocom_l2}"
: "${CALYPSO_MOBILE_DEBUG:=DCS:DNB:DPLMN:DRR:DMM:DSIM:DCC:DMNCC:DSS:DLSMS:DPAG:DSUM:DSAP:DGPS:DMOB:DPRIM:DLUA:DGAPK:DLLAPD}"
: "${CALYPSO_PULSE_LATENCY_MSEC:=320}"
: "${CALYPSO_L1_READY_MARKER:=your code is running now}"
: "${CALYPSO_L1_READY_TIMEOUT:=20}"

_l2_log() { printf '%s' "${LOG_DIR}/mobile.log"; }
_l2_cfg() {
    if [ -n "${MOBILE_CFG:-}" ]; then printf '%s' "$MOBILE_CFG"
    elif [ -r "${OSMOCOM_HOME:-$HOME/.osmocom}/bb/mobile_group1.cfg" ]; then
        printf '%s' "${OSMOCOM_HOME:-$HOME/.osmocom}/bb/mobile_group1.cfg"
    else printf '%s' "${QEMU_CFGS:-${QEMU_TREE}/cfgs}/mobile_group1.cfg"; fi
}
_l2_pat() { printf '%s' "mobile -c $(_l2_cfg)"; }

mod_l2_check() {
    command -v mobile >/dev/null 2>&1 || {
        mod_hint "compilez osmocom-bb (layer23) ou ajoutez son répertoire au PATH"
        mod_fail "client L2 introuvable dans le PATH : mobile"
        return $MOD_RC_FAIL; }
    local cfg; cfg="$(_l2_cfg)"
    [ -r "$cfg" ] || {
        mod_hint "le module mobile-cfg doit avoir copié cfgs/mobile_group1.cfg vers \$OSMOCOM_HOME/bb/"
        mod_fail "configuration du mobile illisible : $cfg"
        return $MOD_RC_FAIL; }
    if [ "${DRY:-0}" = 1 ]; then
        mod_say "simulation : attente de la socket L1CTL et du marqueur L1 non jouées"
        mod_ok; return $MOD_RC_OK
    fi
    wait_until "${MOD_TIMEOUT[l2]}" "socket L1CTL $L1CTL_SOCK_PATH" \
               have_unix "$L1CTL_SOCK_PATH" || {
        mod_hint "c'est osmocon qui crée cette socket (option -s) : regardez le journal du module osmocon"
        mod_fail "socket L1CTL absente : $L1CTL_SOCK_PATH"
        return $MOD_RC_FAIL; }
    if ! wait_until "$CALYPSO_L1_READY_TIMEOUT" "L1 prête" \
                    log_has "${LOG_DIR}/osmocon.log" "$CALYPSO_L1_READY_MARKER"; then
        mod_say "AVERTISSEMENT : marqueur « $CALYPSO_L1_READY_MARKER » absent d'osmocon.log — on lance quand même"
    fi
    mod_ok
}

mod_l2_status() {
    local pid; pid="$(cat "${RUN_DIR}/l2.pid" 2>/dev/null || echo 0)"
    if [ "$pid" != 0 ] && kill -0 "$pid" 2>/dev/null; then
        tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null | grep -qF "$(_l2_cfg)" && return 0
    fi
    have_proc "$(_l2_pat)"
}

mod_l2_start() {
    local log cfg
    log="$(_l2_log)"; cfg="$(_l2_cfg)"
    mkdir -p "$LOG_DIR" "$RUN_DIR" 2>/dev/null
    : > "$log"
    mod_say "journal  : $log"
    mod_say "cfg      : $cfg"
    mod_say "debug    : $CALYPSO_MOBILE_DEBUG"
    mod_say "latence  : PULSE_LATENCY_MSEC=$CALYPSO_PULSE_LATENCY_MSEC"
    PULSE_LATENCY_MSEC="$CALYPSO_PULSE_LATENCY_MSEC" \
        setsid mobile -c "$cfg" -d "$CALYPSO_MOBILE_DEBUG" >>"$log" 2>&1 </dev/null &
    printf '%s\n' "$!" > "${RUN_DIR}/l2.pid"
    mod_ok
}

mod_l2_wait() {
    local pid log; pid="$(cat "${RUN_DIR}/l2.pid" 2>/dev/null || echo 0)"
    log="$(_l2_log)"
    wait_until "${MOD_TIMEOUT[l2]}" "première trace du mobile" test -s "$log" || {
        mod_hint "lisez $log ; « same layer2-socket » = un autre client tient déjà $L1CTL_SOCK_PATH"
        mod_fail "mobile n'a produit aucune trace"
        return $MOD_RC_FAIL; }
    kill -0 "$pid" 2>/dev/null || {
        mod_hint "lisez la fin de $log : la cause est presque toujours dans les 10 dernières lignes"
        mod_fail "mobile a démarré puis s'est arrêté"
        return $MOD_RC_FAIL; }
    if command -v ss >/dev/null 2>&1; then
        if ss -x 2>/dev/null | grep -q "$L1CTL_SOCK_PATH"; then
            mod_say "socket L1CTL $L1CTL_SOCK_PATH : connexion observée"
        else
            mod_say "socket L1CTL $L1CTL_SOCK_PATH : pas de connexion visible via ss (indice, pas verdict)"
        fi
    fi
    mod_ok
}

mod_l2_stop() {
    local pid; pid="$(cat "${RUN_DIR}/l2.pid" 2>/dev/null || echo 0)"
    [ "$pid" != 0 ] && kill "$pid" 2>/dev/null
    pkill -f "$(_l2_pat)" 2>/dev/null
    rm -f "${RUN_DIR}/l2.pid"
    return 0
}

3.45 https://github.com/bbaranoff/qosmo/blob/main/run_modules/71-modem-gprs.sh

5890 octets, 135 lignes → 135 lignes

: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"
. "$MODDIR/_lib/radio.sh"
. "$MODDIR/_lib/modem.sh"

MOD_REGISTER modem-gprs "MS GPRS (modem osmocom-bb : attache, PDP, tun)"
MOD_REQUIRED[modem-gprs]=0
MOD_DEPS[modem-gprs]="modem-trxcon sidecar-pcu abonnes-hlr"
MOD_TIMEOUT[modem-gprs]=90
MOD_ENABLED_IF[modem-gprs]='[ "${MODEM_GPRS:-1}" = 1 ]'

: "${HLR_VTY_PORT:=4258}"
: "${OPERATOR_ID:=1}"
: "${BSC_CFG:=${OSMOCOM_CFG:-/etc/osmocom}/osmo-bsc.cfg}"

# Same formulas as 21-abonnes-hlr.sh, for the 3rd subscriber (MODEM_MS_IDX).
_mg_mcc() { printf '%s\n' "${MCC:-$(core_cfg_field "$(core_cfg osmo-msc)" '^[[:space:]]*network country code[[:space:]]' 4 001)}"; }
_mg_mnc() { printf '%s\n' "${MNC:-$(core_cfg_field "$(core_cfg osmo-msc)" '^[[:space:]]*mobile network code[[:space:]]' 4 01)}"; }
_mg_imsi() { printf '%s%s%04d%06d\n' "$(_mg_mcc)" "$(_mg_mnc)" "$OPERATOR_ID" "$MODEM_MS_IDX"; }
_mg_ki()   { printf '00112233445566778899aabbccdd%02x%02x\n' "$MODEM_MS_IDX" "$OPERATOR_ID"; }
_mg_ki_sp() { _mg_ki | sed 's/../& /g; s/ $//'; }
_mg_node() {
    local n="${OSMO_WAN_NODE:-${WAN_NODE_ID:-}}"
    [ -n "$n" ] || n="$(sed -n 's/^PLAN_NODE=//p' "${OSMOCOM_CFG:-/etc/osmocom}/radio-plan.env" 2>/dev/null | tail -1)"
    case "$n" in [1-9]) ;; *) n=1 ;; esac
    printf '%s' "$n"
}
_mg_msisdn() { echo $(( $(_mg_node) * 100000 + OPERATOR_ID * 100 + MODEM_MS_IDX )); }
# The ARFCN of BTS#1 as the BSC declares it (the radio plan may have changed it).
_mg_arfcn() {
    local a; a="$(awk '/^ bts 1/{f=1} f && /^ *arfcn /{print $2; exit}' "$BSC_CFG" 2>/dev/null)"
    printf '%s' "${a:-$SC_ARFCN}"
}

_mg_write_cfg() {
    cat > "$MODEM_CFG.tmp" <<EOF
! modem-gprs.cfg - MS GPRS du banc (osmocom-bb modem), ecrit par 71-modem-gprs.sh
! IMSI $(_mg_imsi) · MSISDN $(_mg_msisdn) · APN $MODEM_APN -> $MODEM_TUN · ARFCN $(_mg_arfcn)
line vty
 no login
 bind 127.0.0.1 $MODEM_VTY_PORT
log stderr
 logging filter all 1
 logging color 0
 logging timestamp 1
 logging print category 1
 logging print level 1
 logging level all notice
ms 1
 layer2-socket $MODEM_L2_SOCK
 sim test
 imei 35892500865948$MODEM_MS_IDX 0
 imei-fixed
 test-sim
  imsi $(_mg_imsi)
  ki comp128 $(_mg_ki_sp)
  no barred-access
  rplmn $(_mg_mcc) $(_mg_mnc)
 apn $MODEM_APN
  tun-device $MODEM_TUN
  type-support v4
  no shutdown
 no shutdown
EOF
    mv -f "$MODEM_CFG.tmp" "$MODEM_CFG"
}

mod_modem_gprs_check() {
    [ -x "$MODEM_BIN" ] || {
        mod_hint "compilez layer23 d'osmocom-bb (src/host/layer23/src/modem/modem), ou posez MODEM_BIN"
        mod_fail "binaire modem introuvable : $MODEM_BIN"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_modem_gprs_status() { radio_alive modem-gprs; }

mod_modem_gprs_start() {
    radio_dirs
    local log imsi; log="$(radio_log modem-gprs)"; imsi="$(_mg_imsi)"

    # 1. The subscriber in the HLR (idempotent: "create" of an existing IMSI is refused, no effect).
    core_vty_ask "$HLR_VTY_PORT" "enable" \
        "subscriber imsi $imsi create" \
        "subscriber imsi $imsi update msisdn $(_mg_msisdn)" \
        "subscriber imsi $imsi update aud2g comp128v1 ki $(_mg_ki)" "exit" >/dev/null 2>&1
    # Presence: the database first (as in 21-abonnes-hlr.sh), the VTY as a
    # fallback - the VTY dialogue may return nothing (socat missing, delays)
    # while the subscriber is there.
    _mg_present() {
        local n=""
        if command -v sqlite3 >/dev/null 2>&1; then
            n="$(sqlite3 -readonly "${HLR_DB:-/var/lib/osmocom/hlr.db}" "select count(*) from subscriber where imsi='$1';" 2>/dev/null)"
            [ "${n:-0}" -ge 1 ] 2>/dev/null && return 0
        fi
        core_vty_ask "$HLR_VTY_PORT" "enable" "subscriber imsi $1 show" "exit" 2>/dev/null | grep -q "IMSI: $1"
    }
    _mg_present "$imsi" \
        || { mod_fail "abonne $imsi absent du HLR apres provisionnement (VTY $HLR_VTY_PORT)"; return $MOD_RC_FAIL; }
    mod_say "abonne GPRS : IMSI $imsi, MSISDN $(_mg_msisdn)"

    # 2. The modem config, rewritten on each launch (it follows the radio plan).
    _mg_write_cfg || { mod_fail "ecriture de $MODEM_CFG impossible"; return $MOD_RC_FAIL; }

    # 3. The modem. It creates $MODEM_TUN itself (root) once the PDP is active.
    wait_until 30 "socket L1CTL $MODEM_L2_SOCK" have_unix "$MODEM_L2_SOCK" || {
        mod_fail "socket L1CTL absente : $MODEM_L2_SOCK"; return $MOD_RC_FAIL; }
    ip link del "$MODEM_TUN" 2>/dev/null
    mod_say "modem -c $MODEM_CFG -a $(_mg_arfcn) (VTY $MODEM_VTY_PORT, tun $MODEM_TUN)"
    mod_say "journal : $log"
    setsid stdbuf -oL -eL "$MODEM_BIN" -c "$MODEM_CFG" -s "$MODEM_L2_SOCK" -a "$(_mg_arfcn)" >>"$log" 2>&1 </dev/null &
    radio_save_pid modem-gprs $!
    mod_ok
}

mod_modem_gprs_wait() {
    local log; log="$(radio_log modem-gprs)"
    wait_until 5 "processus modem" radio_alive modem-gprs || {
        modb_tail "$log" 20
        mod_fail "le modem s'est arrete aussitot lance"; return $MOD_RC_FAIL; }
    wait_until 20 "VTY du modem ($MODEM_VTY_PORT)" have_port "$MODEM_VTY_PORT" || {
        modb_tail "$log" 20
        mod_fail "VTY $MODEM_VTY_PORT jamais en ecoute"; return $MOD_RC_FAIL; }
    # GPRS attach then PDP: visible when the tun carries an address (the GGSN's, 176.16.32.x).
    if wait_until "${MOD_TIMEOUT[modem-gprs]}" "PDP actif ($MODEM_TUN avec adresse)" \
            sh -c "ip -4 -o addr show '$MODEM_TUN' 2>/dev/null | grep -q inet"; then
        mod_say "pret - $(ip -4 -o addr show "$MODEM_TUN" | awk '{print $4}' | head -1) sur $MODEM_TUN (APN $MODEM_APN)"
    else
        modb_tail "$log" 25
        mod_hint "attache GMM / PDP non abouti : journal du modem, puis osmo-sgsn.log (attach, PDP) et ${SC_PCU_UNIT}.log (TBF)"
        mod_say "modem lance mais pas de PDP dans le delai - la voix n'est pas concernee"
    fi
    mod_ok
}

mod_modem_gprs_stop() { radio_kill modem-gprs; ip link del "$MODEM_TUN" 2>/dev/null; return 0; }

3.46 https://github.com/bbaranoff/qosmo/blob/main/run_modules/75-gsmtap.sh

2319 octets, 63 lignes → 63 lignes

MOD_REGISTER gsmtap "Capture GSMTAP (pcap)"
MOD_REQUIRED[gsmtap]=0
MOD_DEPS[gsmtap]="l2"
MOD_TIMEOUT[gsmtap]=15

: "${PORT_GSMTAP:=4729}"
: "${CALYPSO_GSMTAP_IFACE:=any}"
: "${CAPTURE_DIR:=${RUN_DIR:-/tmp/calypso}/captures}"

_gsmtap_pat()  { printf '%s' "tcpdump .*udp port ${PORT_GSMTAP}"; }
_gsmtap_file() { cat "${RUN_DIR}/gsmtap.pcap.path" 2>/dev/null; }

mod_gsmtap_check() {
    command -v tcpdump >/dev/null 2>&1 || {
        mod_hint "installez-le vous-même : apt-get install tcpdump — ou lancez avec --skip gsmtap"
        mod_fail "tcpdump absent : aucune capture GSMTAP possible"
        return $MOD_RC_FAIL; }
    local d="$CAPTURE_DIR"
    [ -d "$d" ] || d="$(dirname "$CAPTURE_DIR")"
    [ -w "$d" ] || {
        mod_hint "posez CAPTURE_DIR vers un répertoire inscriptible"
        mod_fail "répertoire de capture non inscriptible : $d"
        return $MOD_RC_FAIL; }
    mod_ok
}

mod_gsmtap_status() { have_proc "$(_gsmtap_pat)"; }

mod_gsmtap_start() {
    local pcap log
    pcap="$CAPTURE_DIR/mobile-gsmtap-$(date +%Y%m%d_%H%M%S).pcap"
    log="${LOG_DIR}/gsmtap.log"
    mkdir -p "$LOG_DIR" "$RUN_DIR" "$CAPTURE_DIR" 2>/dev/null
    printf '%s\n' "$pcap" > "${RUN_DIR}/gsmtap.pcap.path"
    mod_say "capture  : $pcap"
    mod_say "filtre   : udp port $PORT_GSMTAP sur $CALYPSO_GSMTAP_IFACE"
    setsid tcpdump -i "$CALYPSO_GSMTAP_IFACE" -U \
            -w "$pcap" "udp port $PORT_GSMTAP" >>"$log" 2>&1 </dev/null &
    printf '%s\n' "$!" > "${RUN_DIR}/gsmtap.pid"
    mod_ok
}

mod_gsmtap_wait() {
    local pid pcap; pid="$(cat "${RUN_DIR}/gsmtap.pid" 2>/dev/null || echo 0)"
    pcap="$(_gsmtap_file)"
    wait_until "${MOD_TIMEOUT[gsmtap]}" "création du pcap" test -s "$pcap" || {
        mod_hint "lisez ${LOG_DIR}/gsmtap.log ; « permission denied » = capture sans droits (CAP_NET_RAW)"
        mod_fail "tcpdump n'a pas créé $pcap"
        return $MOD_RC_FAIL; }
    kill -0 "$pid" 2>/dev/null || {
        mod_fail "tcpdump a démarré puis s'est arrêté"
        mod_hint "lisez ${LOG_DIR}/gsmtap.log"
        return $MOD_RC_FAIL; }
    mod_ok
}

mod_gsmtap_stop() {
    local pid; pid="$(cat "${RUN_DIR}/gsmtap.pid" 2>/dev/null || echo 0)"
    [ "$pid" != 0 ] && kill "$pid" 2>/dev/null
    pkill -f "$(_gsmtap_pat)" 2>/dev/null
    rm -f "${RUN_DIR}/gsmtap.pid"
    return 0
}

3.47 https://github.com/bbaranoff/qosmo/blob/main/run_modules/80-panes.sh

2649 octets, 67 lignes → 67 lignes

MOD_REGISTER panes "Vue de supervision (tmux)"
MOD_REQUIRED[panes]=0
MOD_DEPS[panes]="tmux"
MOD_TIMEOUT[panes]=15

: "${TMUX_SESSION:=calypso}"
: "${CALYPSO_GSM_SNIFF:=${QEMU_TREE}/opt-gsm-scripts/gsm_sniff.py}"

_panes_count() { tmux list-panes -t "$TMUX_SESSION:all" 2>/dev/null | wc -l; }
_panes_enough() { [ "$(_panes_count)" -ge 2 ]; }

mod_panes_check() {
    command -v tmux >/dev/null 2>&1 || { mod_skip "tmux absent — pas de vue de supervision"; return $MOD_RC_SKIP; }
    if [ "${DRY:-0}" = 1 ]; then
        mod_say "simulation : existence de la session tmux non vérifiée"
        mod_ok; return $MOD_RC_OK
    fi
    tmux has-session -t "$TMUX_SESSION" 2>/dev/null || {
        mod_hint "le module tmux crée la session ; ./run.sh --only tmux"
        mod_fail "session tmux « $TMUX_SESSION » inexistante"
        return $MOD_RC_FAIL; }
    mod_ok
}

mod_panes_status() { tmux list-windows -t "$TMUX_SESSION" 2>/dev/null | grep -q '^[0-9]*: all'; }

mod_panes_start() {
    tmux new-window -t "$TMUX_SESSION" -n all \
        "clear; echo '=== osmocon ==='; tail -F ${LOG_DIR}/osmocon.log" || {
        mod_fail "tmux a refusé de créer la fenêtre « all »"
        return $MOD_RC_FAIL; }
    local specs=() spec name what cmd added=0
    specs+=("qemu|${LOG_DIR}/qemu.log")
    [ -r "$CALYPSO_GSM_SNIFF" ] && specs+=("burst|__BURST__")
    specs+=("${MS1_LOG_NOM:-mobile}|${MS1_LOG:-${LOG_DIR}/mobile.log}")   # [2026-10-24] rvinterf.log en Magnetite
    specs+=("pont|/dev/shm/pont.log")
    for spec in "${specs[@]}"; do
        name="${spec%%|*}"; what="${spec##*|}"
        case "$what" in
            __BURST__)
                cmd="clear; echo '=== BURST decode (4731 + 5700-5702) ==='; python3 -u $CALYPSO_GSM_SNIFF burst" ;;
            *)  cmd="clear; echo '=== $name ==='; tail -F $what" ;;
        esac
        if tmux split-window -t "$TMUX_SESSION:all" "$cmd" 2>/dev/null; then
            added=$((added + 1))
        else
            mod_say "pane « $name » refusé par tmux (place insuffisante)"
        fi
        tmux select-layout -t "$TMUX_SESSION:all" tiled >/dev/null 2>&1
    done
    mod_say "fenêtre « all » : $((added + 1)) panes demandés"
    mod_ok
}

mod_panes_wait() {
    wait_until "${MOD_TIMEOUT[panes]}" "panes de la fenêtre « all »" _panes_enough || {
        mod_hint "tmux list-panes -t $TMUX_SESSION:all pour voir ce qui a survécu"
        mod_fail "la fenêtre « all » n'a pas gardé ses panes"
        return $MOD_RC_FAIL; }
    mod_say "panes vivants : $(_panes_count)"
    mod_ok
}

mod_panes_stop() {
    tmux kill-window -t "$TMUX_SESSION:all" 2>/dev/null
    return 0
}

3.48 https://github.com/bbaranoff/qosmo/blob/main/run_modules/95-summary.sh

1284 octets, 25 lignes → 25 lignes

MOD_REGISTER summary "Identité du build et résumé"
MOD_REQUIRED[summary]=0
MOD_PURE[summary]=1
MOD_TIMEOUT[summary]=10

mod_summary_check() { mod_ok; }
mod_summary_status() { return $MOD_RC_FAIL; }

mod_summary_start() {
    mod_say "===== IDENTITÉ DU BUILD ====="
    if command -v git >/dev/null 2>&1 && [ -d "${QEMU_TREE}/.git" ]; then
        mod_say "  révision git    : $(git -C "$QEMU_TREE" rev-parse --short HEAD 2>/dev/null || echo inconnue)"
        mod_say "  arbre modifié   : $(git -C "$QEMU_TREE" diff --stat 2>/dev/null | tail -1)"
    fi
    mod_say "  binaire QEMU    : ${QEMU_BIN:-<non défini>}"
    mod_say "  date du binaire : $(stat -c %y "${QEMU_BIN:-/nonexistent}" 2>/dev/null | cut -d. -f1)"
    mod_say "  firmware        : ${FIRMWARE_ELF:-<non défini>}"
    mod_say "===== RÉSUMÉ D'ENVIRONNEMENT ====="
    mod_say "  couche 1        : gr-gsm (GSMTAP ${PORT_GSMTAP_SI:-4730}, SCH ${PORT_GSMTAP_SCH:-4731}) · pont TRXD ${PORT_TRXD_CLOCK:-5700}-${PORT_TRXD_DATA:-5702}"
    mod_say "  cellule du pont : ARFCN ${PONT_ARFCN:-?} BSIC ${PONT_BSIC:-?}"
    mod_say "  chiffrement     : ${ENCRYPTION:-?}"
    mod_say "  MS#1 / MS#2     : ${MOBILE_CFG:-?} / ${SC_MOBILE_CFG:-?}"
    mod_say "  LOG_DIR / RUN_DIR : ${LOG_DIR:-} / ${RUN_DIR:-}"
    mod_ok
}

3.49 https://github.com/bbaranoff/qosmo/blob/main/run_modules/99-attach.sh

1931 octets, 50 lignes → 50 lignes

MOD_REGISTER attach "Reprise en main de la session tmux"
MOD_REQUIRED[attach]=0
MOD_DEPS[attach]="tmux"
MOD_TIMEOUT[attach]=10
MOD_ENABLED_IF[attach]='[ "${CALYPSO_NO_ATTACH:-0}" != 1 ] && [ -t 1 ]'

: "${TMUX_SESSION:=calypso}"
: "${CALYPSO_DEFAULT_WINDOW:=all}"

_attach_current_window() {
    tmux display-message -p -t "$TMUX_SESSION" '#W' 2>/dev/null
}

mod_attach_check() {
    command -v tmux >/dev/null 2>&1 || { mod_skip "tmux absent — rien à sélectionner"; return $MOD_RC_SKIP; }
    tmux has-session -t "$TMUX_SESSION" 2>/dev/null || {
        mod_hint "le module tmux (35) crée la session ; sans elle la pile tourne quand même, en arrière-plan"
        mod_skip "session tmux « $TMUX_SESSION » inexistante"
        return $MOD_RC_SKIP; }
    mod_ok
}

mod_attach_status() { [ "$(_attach_current_window)" = "$CALYPSO_DEFAULT_WINDOW" ]; }

mod_attach_start() {
    mkdir -p "$RUN_DIR" 2>/dev/null
    tmux select-window -t "$TMUX_SESSION:$CALYPSO_DEFAULT_WINDOW" 2>/dev/null \
        || tmux select-window -t "$TMUX_SESSION:qemu" 2>/dev/null \
        || mod_say "aucune des fenêtres « $CALYPSO_DEFAULT_WINDOW » / « qemu » n'existe"

    printf 'tmux attach -t %s\n' "$TMUX_SESSION" > "${RUN_DIR}/attach.cmd"
    mod_say "reprise en main : tmux attach -t $TMUX_SESSION"
    mod_say "fin de session  : tmux kill-session -t $TMUX_SESSION"
    mod_ok
}

mod_attach_wait() {
    wait_until "${MOD_TIMEOUT[attach]}" "session tmux « $TMUX_SESSION »" \
               tmux has-session -t "$TMUX_SESSION" || {
        mod_fail "la session tmux a disparu"
        return $MOD_RC_FAIL; }
    local w; w="$(_attach_current_window)"
    [ -n "$w" ] || { mod_fail "aucune fenêtre sélectionnable dans « $TMUX_SESSION »"
                     mod_hint "tmux list-windows -t $TMUX_SESSION"
                     return $MOD_RC_FAIL; }
    mod_say "fenêtre courante : $w"
    mod_ok
}

mod_attach_stop() { return 0; }

3.50 https://github.com/bbaranoff/qosmo/blob/main/run_modules/_lib/core.sh

4542 octets, 131 lignes → 131 lignes

[ -n "${CORE_LIB_LOADED:-}" ] && return 0
CORE_LIB_LOADED=1

: "${OSMOCOM_CFG:=/etc/osmocom}"
: "${RUN_DIR:=/tmp/calypso}"
: "${LOG_DIR:=$RUN_DIR/logs}"
: "${CORE_VTY_HOST:=127.0.0.1}"

core_bin() {
    local n="$1" p
    for p in "/usr/bin/$n" "/usr/local/bin/$n"; do
        [ -x "$p" ] && { printf '%s\n' "$p"; return 0; }
    done
    p="$(command -v "$n" 2>/dev/null)" || return 1
    [ -n "$p" ] && { printf '%s\n' "$p"; return 0; }
    return 1
}

core_cfg() { printf '%s/%s.cfg\n' "$OSMOCOM_CFG" "$1"; }

core_cfg_field() {
    local f="$1" re="$2" n="$3" def="${4:-}" v=""
    if [ -r "$f" ]; then
        v="$(grep -aE "$re" "$f" 2>/dev/null | head -n1 | awk -v k="$n" '{print $k}')"
    fi
    printf '%s\n' "${v:-$def}"
}

core_has_systemd() { [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; }
core_unit_exists() { core_has_systemd && systemctl cat "$1" >/dev/null 2>&1; }
core_unit_active() { core_has_systemd && systemctl is-active --quiet "$1"; }
core_nrestarts()   { core_has_systemd && systemctl show -p NRestarts --value "$1" 2>/dev/null || echo 0; }
core_pidfile()     { printf '%s/%s.pid\n' "$RUN_DIR" "$1"; }

core_svc_start() {
    local unit="$1" bin="$2"; shift 2
    mkdir -p "$RUN_DIR" "$LOG_DIR" 2>/dev/null || true
    core_nrestarts "$unit" > "$RUN_DIR/$unit.nrestarts" 2>/dev/null || true
    if core_unit_exists "$unit"; then
        mod_say "systemctl start $unit"
        systemctl start "$unit" || return 1
        return 0
    fi
    mod_say "systemd indisponible — lancement direct : $bin $*"
    setsid "$bin" "$@" >>"$LOG_DIR/$unit.log" 2>&1 </dev/null &
    printf '%s\n' "$!" > "$(core_pidfile "$unit")"
    return 0
}

core_svc_stop() {
    local unit="$1" pat="${2:-}" pf
    core_unit_exists "$unit" && systemctl stop "$unit" 2>/dev/null
    pf="$(core_pidfile "$unit")"
    if [ -f "$pf" ]; then
        kill "$(cat "$pf" 2>/dev/null)" 2>/dev/null
        rm -f "$pf"
    fi
    [ -n "$pat" ] && pkill -f "$pat" 2>/dev/null
    rm -f "$RUN_DIR/$unit.nrestarts" 2>/dev/null
    return 0
}

core_alive() {
    local unit="$1" pf
    if core_unit_exists "$unit"; then core_unit_active "$unit"; return $?; fi
    pf="$(core_pidfile "$unit")"
    [ -f "$pf" ] && kill -0 "$(cat "$pf" 2>/dev/null)" 2>/dev/null
}

core_restarted_since() {
    local unit="$1"
    local f="$RUN_DIR/$unit.nrestarts"
    local before now
    [ -f "$f" ] || return 1
    before="$(cat "$f" 2>/dev/null || echo 0)"
    now="$(core_nrestarts "$unit")"
    [ "${now:-0}" -gt "${before:-0}" ] 2>/dev/null
}

core_healthy() { core_alive "$1" && ! core_restarted_since "$1"; }

_core_listen() {
    local opts="$1" port="$2" ip="${3:-}"
    ss -H $opts 2>/dev/null | awk -v want="$port" -v wip="$ip" '
        { a = $4
          if (a == "") next
          if (match(a, /:[0-9]+$/) == 0) next
          p = substr(a, RSTART + 1); h = substr(a, 1, RSTART - 1)
          if (p != want) next
          if (wip == "" || h == wip || h == "0.0.0.0" || h == "*" || h == "[::]" || h == "::") { f = 1; exit }
        }
        END { exit !f }'
}
core_tcp_listen()  { _core_listen "-ltn"      "$@"; }
core_udp_listen()  { _core_listen "-lun"      "$@"; }
core_sctp_listen() { _core_listen "-ln --sctp" "$@"; }
core_vty_listen()  { core_tcp_listen "$1" "$CORE_VTY_HOST"; }

core_sctp_estab() {
    ss -nH --sctp 2>/dev/null | awk -v want="$1" '
        $1 == "ESTAB" { a = $4
            if (match(a, /:[0-9]+$/)) { p = substr(a, RSTART + 1); if (p == want) { f = 1; exit } } }
        END { exit !f }'
}

core_unix_listen() {
    local p="$1"
    ss -xlH 2>/dev/null | awk -v w="$p" '{ for (i = 1; i <= NF; i++) if ($i == w) { f = 1; exit } } END { exit !f }' \
        || [ -S "$p" ]
}

core_ip_local() {
    local w="$1"
    case "$w" in ""|0.0.0.0|127.*) return 0 ;; esac
    ip -o -4 addr show 2>/dev/null | awk -v w="$w" '{ split($4, a, "/"); if (a[1] == w) { f = 1; exit } } END { exit !f }'
}

core_vty_ask() {
    local port="$1"; shift
    local body="" c t="${CORE_VTY_TIMEOUT:-10}"
    for c in "$@"; do body+="$c"$'\n'; done
    if command -v socat >/dev/null 2>&1; then
        { sleep "${CORE_VTY_OPEN:-1}"; printf '%s' "$body"; sleep "${CORE_VTY_READ:-2}"; } \
            | timeout "$t" socat -T"$t" STDIO "TCP:${CORE_VTY_HOST}:${port},crlf" 2>/dev/null
    elif command -v telnet >/dev/null 2>&1; then
        { sleep "${CORE_VTY_OPEN:-1}"; printf '%s' "$body"; sleep "${CORE_VTY_READ:-2}"; } \
            | timeout "$t" telnet "$CORE_VTY_HOST" "$port" 2>/dev/null
    else
        return 1
    fi
}

3.51 https://github.com/bbaranoff/qosmo/blob/main/run_modules/_lib/i18n.sh

27847 octets, 630 lignes → 630 lignes

: "${CALYPSO_LANG:=en}"

declare -gA _T _TM

_T[fr:plan]="Plan — %d modules"
_T[en:plan]="Plan — %d modules"
_T[es:plan]="Plan — %d módulos"
_T[pt:plan]="Plano — %d módulos"
_T[it:plan]="Piano — %d moduli"
_T[hi:plan]="योजना — %d मॉड्यूल"
_T[zh:plan]="方案 — %d 个模块"
_T[ar:plan]="الخطة — %d وحدة"
_T[fr:ok]="OK"
_T[en:ok]="OK"
_T[es:ok]="OK"
_T[pt:ok]="OK"
_T[it:ok]="OK"
_T[hi:ok]="ठीक"
_T[zh:ok]="完成"
_T[ar:ok]="تم"
_T[fr:fail]="ÉCHC"
_T[en:fail]="FAIL"
_T[es:fail]="FALL"
_T[pt:fail]="FALH"
_T[it:fail]="ERRO"
_T[hi:fail]="विफल"
_T[zh:fail]="失败"
_T[ar:fail]="فشل"
_T[fr:skip]="SAUT"
_T[en:skip]="SKIP"
_T[es:skip]="OMIT"
_T[pt:skip]="OMIT"
_T[it:skip]="SALT"
_T[hi:skip]="छोड़ा"
_T[zh:skip]="跳过"
_T[ar:skip]="تخطٍ"
_T[fr:dry]="SIMU"
_T[en:dry]="DRY "
_T[es:dry]="SIMU"
_T[pt:dry]="SIMU"
_T[it:dry]="SIMU"
_T[hi:dry]="सिमु"
_T[zh:dry]="模拟"
_T[ar:dry]="محاكاة"
_T[fr:simule]="simulé"
_T[en:simule]="simulated"
_T[es:simule]="simulado"
_T[pt:simule]="simulado"
_T[it:simule]="simulato"
_T[hi:simule]="सिमुलेटेड"
_T[zh:simule]="已模拟"
_T[ar:simule]="محاكى"
_T[fr:deja]="déjà démarré"
_T[en:deja]="already running"
_T[es:deja]="ya iniciado"
_T[pt:deja]="já iniciado"
_T[it:deja]="già avviato"
_T[hi:deja]="पहले से चालू"
_T[zh:deja]="已在运行"
_T[ar:deja]="يعمل بالفعل"
_T[fr:desactive]="désactivé par %s"
_T[en:desactive]="disabled by %s"
_T[es:desactive]="desactivado por %s"
_T[pt:desactive]="desativado por %s"
_T[it:desactive]="disattivato da %s"
_T[hi:desactive]="%s द्वारा निष्क्रिय"
_T[zh:desactive]="由 %s 停用"
_T[ar:desactive]="معطّل بواسطة %s"
_T[fr:interrompu]="séquence interrompue (module obligatoire)"
_T[en:interrompu]="sequence aborted (required module)"
_T[es:interrompu]="secuencia interrumpida (módulo obligatorio)"
_T[pt:interrompu]="sequência interrompida (módulo obrigatório)"
_T[it:interrompu]="sequenza interrotta (modulo obbligatorio)"
_T[hi:interrompu]="अनुक्रम रुका (अनिवार्य मॉड्यूल)"
_T[zh:interrompu]="流程中止(必需模块)"
_T[ar:interrompu]="توقف التسلسل (وحدة إلزامية)"
_T[fr:journal]="journal : %s"
_T[en:journal]="log : %s"
_T[es:journal]="registro : %s"
_T[pt:journal]="registo : %s"
_T[it:journal]="registro : %s"
_T[hi:journal]="लॉग : %s"
_T[zh:journal]="日志 : %s"
_T[ar:journal]="السجل : %s"
_T[fr:pas_pret]="démarré mais jamais prêt : %s"
_T[en:pas_pret]="started but never ready: %s"
_T[es:pas_pret]="iniciado pero nunca listo: %s"
_T[pt:pas_pret]="iniciado mas nunca pronto: %s"
_T[it:pas_pret]="avviato ma mai pronto: %s"
_T[hi:pas_pret]="चालू हुआ पर तैयार नहीं: %s"
_T[zh:pas_pret]="已启动但始终未就绪:%s"
_T[ar:pas_pret]="بدأ لكنه لم يجهز: %s"
_T[fr:sans_raison]="raison non fournie par le module"
_T[en:sans_raison]="no reason given by the module"
_T[es:sans_raison]="el módulo no indicó la causa"
_T[pt:sans_raison]="o módulo não indicou a causa"
_T[it:sans_raison]="il modulo non ha indicato la causa"
_T[hi:sans_raison]="मॉड्यूल ने कारण नहीं दिया"
_T[zh:sans_raison]="模块未给出原因"
_T[ar:sans_raison]="الوحدة لم تذكر السبب"
_T[fr:prereq_sim]="prérequis non vérifiable en simulation : %s"
_T[en:prereq_sim]="prerequisite not checkable in simulation: %s"
_T[es:prereq_sim]="requisito no verificable en simulación: %s"
_T[pt:prereq_sim]="requisito não verificável em simulação: %s"
_T[it:prereq_sim]="prerequisito non verificabile in simulazione: %s"
_T[hi:prereq_sim]="सिमुलेशन में पूर्वापेक्षा जाँच योग्य नहीं: %s"
_T[zh:prereq_sim]="模拟模式下无法校验前置条件:%s"
_T[ar:prereq_sim]="لا يمكن التحقق من المتطلب في المحاكاة: %s"
_T[fr:bilan]="%d ok · %d ignorés · %d échecs"
_T[en:bilan]="%d ok · %d skipped · %d failed"
_T[es:bilan]="%d ok · %d omitidos · %d fallos"
_T[pt:bilan]="%d ok · %d omitidos · %d falhas"
_T[it:bilan]="%d ok · %d saltati · %d errori"
_T[hi:bilan]="%d ठीक · %d छोड़े · %d विफल"
_T[zh:bilan]="%d 完成 · %d 跳过 · %d 失败"
_T[ar:bilan]="%d تم · %d تخطٍ · %d فشل"
_T[fr:opt_inconnue]="option inconnue : %s"
_T[en:opt_inconnue]="unknown option: %s"
_T[es:opt_inconnue]="opción desconocida: %s"
_T[pt:opt_inconnue]="opção desconhecida: %s"
_T[it:opt_inconnue]="opzione sconosciuta: %s"
_T[hi:opt_inconnue]="अज्ञात विकल्प: %s"
_T[zh:opt_inconnue]="未知选项:%s"
_T[ar:opt_inconnue]="خيار غير معروف: %s"

_TM[fr:abonnes-hlr]="Cœur — abonnés provisionnés dans le HLR"
_TM[en:abonnes-hlr]="Core — subscribers provisioned in the HLR"
_TM[es:abonnes-hlr]="Núcleo — abonados aprovisionados en el HLR"
_TM[pt:abonnes-hlr]="Núcleo — assinantes provisionados no HLR"
_TM[it:abonnes-hlr]="Core — abbonati registrati nell'HLR"
_TM[hi:abonnes-hlr]="कोर — HLR में पंजीकृत ग्राहक"
_TM[zh:abonnes-hlr]="核心 — HLR 中已开通的用户"
_TM[ar:abonnes-hlr]="النواة — المشتركون المسجلون في HLR"
_TM[fr:apn0]="Cœur — interface TUN de l'APN"
_TM[en:apn0]="Core — APN TUN interface"
_TM[es:apn0]="Núcleo — interfaz TUN del APN"
_TM[pt:apn0]="Núcleo — interface TUN do APN"
_TM[it:apn0]="Core — interfaccia TUN dell'APN"
_TM[hi:apn0]="कोर — APN का TUN इंटरफ़ेस"
_TM[zh:apn0]="核心 — APN 的 TUN 接口"
_TM[ar:apn0]="النواة — واجهة TUN لنقطة APN"
_TM[fr:asterisk]="Cœur — Asterisk (PBX SIP)"
_TM[en:asterisk]="Core — Asterisk (SIP PBX)"
_TM[es:asterisk]="Núcleo — Asterisk (PBX SIP)"
_TM[pt:asterisk]="Núcleo — Asterisk (PBX SIP)"
_TM[it:asterisk]="Core — Asterisk (PBX SIP)"
_TM[hi:asterisk]="कोर — Asterisk (SIP PBX)"
_TM[zh:asterisk]="核心 — Asterisk(SIP 交换机)"
_TM[ar:asterisk]="النواة — Asterisk (مقسم SIP)"
_TM[fr:attach]="Reprise en main de la session tmux"
_TM[en:attach]="Attaching to the tmux session"
_TM[es:attach]="Reconexión a la sesión tmux"
_TM[pt:attach]="Reconexão à sessão tmux"
_TM[it:attach]="Ripresa della sessione tmux"
_TM[hi:attach]="tmux सत्र से पुनः जुड़ाव"
_TM[zh:attach]="接入 tmux 会话"
_TM[ar:attach]="الاتصال بجلسة tmux"
_TM[fr:audio]="Pont audio (PulseAudio + GAPK)"
_TM[en:audio]="Audio bridge (PulseAudio + GAPK)"
_TM[es:audio]="Puente de audio (PulseAudio + GAPK)"
_TM[pt:audio]="Ponte de áudio (PulseAudio + GAPK)"
_TM[it:audio]="Ponte audio (PulseAudio + GAPK)"
_TM[hi:audio]="ऑडियो सेतु (PulseAudio + GAPK)"
_TM[zh:audio]="音频桥(PulseAudio + GAPK)"
_TM[ar:audio]="جسر الصوت (PulseAudio + GAPK)"
_TM[fr:bsc]="Cœur — OsmoBSC (contrôleur BTS)"
_TM[en:bsc]="Core — OsmoBSC (BTS controller)"
_TM[es:bsc]="Núcleo — OsmoBSC (controlador BTS)"
_TM[pt:bsc]="Núcleo — OsmoBSC (controlador BTS)"
_TM[it:bsc]="Core — OsmoBSC (controllore BTS)"
_TM[hi:bsc]="कोर — OsmoBSC (BTS नियंत्रक)"
_TM[zh:bsc]="核心 — OsmoBSC(基站控制器)"
_TM[ar:bsc]="النواة — OsmoBSC (متحكم BTS)"
_TM[fr:bts]="Station de base osmo-bts-trx"
_TM[en:bts]="Base station osmo-bts-trx"
_TM[es:bts]="Estación base osmo-bts-trx"
_TM[pt:bts]="Estação base osmo-bts-trx"
_TM[it:bts]="Stazione base osmo-bts-trx"
_TM[hi:bts]="बेस स्टेशन osmo-bts-trx"
_TM[zh:bts]="基站 osmo-bts-trx"
_TM[ar:bts]="المحطة القاعدية osmo-bts-trx"
_TM[fr:config]="Résolution des chemins et des ports"
_TM[en:config]="Resolving paths and ports"
_TM[es:config]="Resolución de rutas y puertos"
_TM[pt:config]="Resolução de caminhos e portas"
_TM[it:config]="Risoluzione di percorsi e porte"
_TM[hi:config]="पथ और पोर्ट का निर्धारण"
_TM[zh:config]="解析路径与端口"
_TM[ar:config]="تحديد المسارات والمنافذ"
_TM[fr:gabarits]="Gabarits de configuration du cœur"
_TM[en:gabarits]="Core configuration templates"
_TM[es:gabarits]="Plantillas de configuración del núcleo"
_TM[pt:gabarits]="Modelos de configuração do núcleo"
_TM[it:gabarits]="Modelli di configurazione del core"
_TM[hi:gabarits]="कोर विन्यास टेम्पलेट"
_TM[zh:gabarits]="核心配置模板"
_TM[ar:gabarits]="قوالب إعداد النواة"
_TM[fr:ggsn]="Cœur — OsmoGGSN (data GTP)"
_TM[en:ggsn]="Core — OsmoGGSN (GTP data)"
_TM[es:ggsn]="Núcleo — OsmoGGSN (datos GTP)"
_TM[pt:ggsn]="Núcleo — OsmoGGSN (dados GTP)"
_TM[it:ggsn]="Core — OsmoGGSN (dati GTP)"
_TM[hi:ggsn]="कोर — OsmoGGSN (GTP डेटा)"
_TM[zh:ggsn]="核心 — OsmoGGSN(GTP 数据)"
_TM[ar:ggsn]="النواة — OsmoGGSN (بيانات GTP)"
_TM[fr:gsmtap]="Capture GSMTAP (pcap)"
_TM[en:gsmtap]="GSMTAP capture (pcap)"
_TM[es:gsmtap]="Captura GSMTAP (pcap)"
_TM[pt:gsmtap]="Captura GSMTAP (pcap)"
_TM[it:gsmtap]="Cattura GSMTAP (pcap)"
_TM[hi:gsmtap]="GSMTAP कैप्चर (pcap)"
_TM[zh:gsmtap]="GSMTAP 抓包(pcap)"
_TM[ar:gsmtap]="التقاط GSMTAP (pcap)"
_TM[fr:hlr]="Cœur — OsmoHLR (registre GSUP)"
_TM[en:hlr]="Core — OsmoHLR (GSUP registry)"
_TM[es:hlr]="Núcleo — OsmoHLR (registro GSUP)"
_TM[pt:hlr]="Núcleo — OsmoHLR (registo GSUP)"
_TM[it:hlr]="Core — OsmoHLR (registro GSUP)"
_TM[hi:hlr]="कोर — OsmoHLR (GSUP रजिस्टर)"
_TM[zh:hlr]="核心 — OsmoHLR(GSUP 注册库)"
_TM[ar:hlr]="النواة — OsmoHLR (سجل GSUP)"
_TM[fr:l2]="Client de couche 2"
_TM[en:l2]="Layer 2 client"
_TM[es:l2]="Cliente de capa 2"
_TM[pt:l2]="Cliente de camada 2"
_TM[it:l2]="Client di livello 2"
_TM[hi:l2]="लेयर 2 क्लाइंट"
_TM[zh:l2]="第二层客户端"
_TM[ar:l2]="عميل الطبقة الثانية"
_TM[fr:logs]="Journaux et espace de travail"
_TM[en:logs]="Logs and working area"
_TM[es:logs]="Registros y espacio de trabajo"
_TM[pt:logs]="Registos e área de trabalho"
_TM[it:logs]="Registri e area di lavoro"
_TM[hi:logs]="लॉग और कार्यक्षेत्र"
_TM[zh:logs]="日志与工作区"
_TM[ar:logs]="السجلات ومساحة العمل"
_TM[fr:mgw]="Cœur — OsmoMGW (média MGCP/RTP)"
_TM[en:mgw]="Core — OsmoMGW (MGCP/RTP media)"
_TM[es:mgw]="Núcleo — OsmoMGW (medios MGCP/RTP)"
_TM[pt:mgw]="Núcleo — OsmoMGW (média MGCP/RTP)"
_TM[it:mgw]="Core — OsmoMGW (media MGCP/RTP)"
_TM[hi:mgw]="कोर — OsmoMGW (MGCP/RTP मीडिया)"
_TM[zh:mgw]="核心 — OsmoMGW(MGCP/RTP 媒体)"
_TM[ar:mgw]="النواة — OsmoMGW (وسائط MGCP/RTP)"
_TM[fr:mobile-cfg]="Configuration du mobile"
_TM[en:mobile-cfg]="Mobile configuration"
_TM[es:mobile-cfg]="Configuración del móvil"
_TM[pt:mobile-cfg]="Configuração do móvel"
_TM[it:mobile-cfg]="Configurazione del mobile"
_TM[hi:mobile-cfg]="मोबाइल विन्यास"
_TM[zh:mobile-cfg]="手机端配置"
_TM[ar:mobile-cfg]="إعداد الهاتف"
_TM[fr:msc]="Cœur — OsmoMSC (commutation CS)"
_TM[en:msc]="Core — OsmoMSC (CS switching)"
_TM[es:msc]="Núcleo — OsmoMSC (conmutación CS)"
_TM[pt:msc]="Núcleo — OsmoMSC (comutação CS)"
_TM[it:msc]="Core — OsmoMSC (commutazione CS)"
_TM[hi:msc]="कोर — OsmoMSC (CS स्विचिंग)"
_TM[zh:msc]="核心 — OsmoMSC(电路域交换)"
_TM[ar:msc]="النواة — OsmoMSC (تبديل الدارات)"
_TM[fr:osmocon]="Passerelle osmocon"
_TM[en:osmocon]="osmocon gateway"
_TM[es:osmocon]="Pasarela osmocon"
_TM[pt:osmocon]="Gateway osmocon"
_TM[it:osmocon]="Gateway osmocon"
_TM[hi:osmocon]="osmocon गेटवे"
_TM[zh:osmocon]="osmocon 网关"
_TM[ar:osmocon]="بوابة osmocon"
_TM[fr:outils]="Outillage de l'hôte"
_TM[en:outils]="Host tooling"
_TM[es:outils]="Herramientas del anfitrión"
_TM[pt:outils]="Ferramentas do anfitrião"
_TM[it:outils]="Strumenti dell'host"
_TM[hi:outils]="होस्ट उपकरण"
_TM[zh:outils]="主机工具"
_TM[ar:outils]="أدوات المضيف"
_TM[fr:panes]="Vue de supervision (tmux)"
_TM[en:panes]="Supervision view (tmux)"
_TM[es:panes]="Vista de supervisión (tmux)"
_TM[pt:panes]="Vista de supervisão (tmux)"
_TM[it:panes]="Vista di supervisione (tmux)"
_TM[hi:panes]="निगरानी दृश्य (tmux)"
_TM[zh:panes]="监控视图(tmux)"
_TM[ar:panes]="عرض المراقبة (tmux)"
_TM[fr:pcu]="Cœur — OsmoPCU (ordonnanceur paquet)"
_TM[en:pcu]="Core — OsmoPCU (packet scheduler)"
_TM[es:pcu]="Núcleo — OsmoPCU (planificador de paquetes)"
_TM[pt:pcu]="Núcleo — OsmoPCU (escalonador de pacotes)"
_TM[it:pcu]="Core — OsmoPCU (scheduler pacchetti)"
_TM[hi:pcu]="कोर — OsmoPCU (पैकेट अनुसूचक)"
_TM[zh:pcu]="核心 — OsmoPCU(分组调度器)"
_TM[ar:pcu]="النواة — OsmoPCU (مجدول الحزم)"
_TM[fr:prereqs]="Vérification des prérequis"
_TM[en:prereqs]="Prerequisite checks"
_TM[es:prereqs]="Comprobación de requisitos"
_TM[pt:prereqs]="Verificação de requisitos"
_TM[it:prereqs]="Verifica dei prerequisiti"
_TM[hi:prereqs]="पूर्वापेक्षा जाँच"
_TM[zh:prereqs]="前置条件检查"
_TM[ar:prereqs]="التحقق من المتطلبات"
_TM[fr:pty]="PTY série de l'émulateur"
_TM[en:pty]="Emulator serial PTY"
_TM[es:pty]="PTY serie del emulador"
_TM[pt:pty]="PTY série do emulador"
_TM[it:pty]="PTY seriale dell'emulatore"
_TM[hi:pty]="एमुलेटर का सीरियल PTY"
_TM[zh:pty]="仿真器串口 PTY"
_TM[ar:pty]="منفذ PTY التسلسلي للمحاكي"
_TM[fr:qemu]="Émulateur Calypso (QEMU)"
_TM[en:qemu]="Calypso emulator (QEMU)"
_TM[es:qemu]="Emulador Calypso (QEMU)"
_TM[pt:qemu]="Emulador Calypso (QEMU)"
_TM[it:qemu]="Emulatore Calypso (QEMU)"
_TM[hi:qemu]="Calypso एमुलेटर (QEMU)"
_TM[zh:qemu]="Calypso 仿真器(QEMU)"
_TM[ar:qemu]="محاكي Calypso (QEMU)"
_TM[fr:reseau]="Interfaces et plan d'adressage"
_TM[en:reseau]="Interfaces and addressing plan"
_TM[es:reseau]="Interfaces y plan de direccionamiento"
_TM[pt:reseau]="Interfaces e plano de endereçamento"
_TM[it:reseau]="Interfacce e piano di indirizzamento"
_TM[hi:reseau]="इंटरफ़ेस और पता-योजना"
_TM[zh:reseau]="接口与编址方案"
_TM[ar:reseau]="الواجهات وخطة العنونة"
_TM[fr:restes]="Extinction des processus enregistrés"
_TM[en:restes]="Shutting down recorded processes"
_TM[es:restes]="Cierre de los procesos registrados"
_TM[pt:restes]="Encerramento dos processos registados"
_TM[it:restes]="Arresto dei processi registrati"
_TM[hi:restes]="दर्ज प्रक्रियाओं की समाप्ति"
_TM[zh:restes]="关闭已登记的进程"
_TM[ar:restes]="إنهاء العمليات المسجلة"
_TM[fr:rundir]="Répertoire d'exécution"
_TM[en:rundir]="Run directory"
_TM[es:rundir]="Directorio de ejecución"
_TM[pt:rundir]="Diretório de execução"
_TM[it:rundir]="Directory di esecuzione"
_TM[hi:rundir]="रन निर्देशिका"
_TM[zh:rundir]="运行目录"
_TM[ar:rundir]="دليل التشغيل"
_TM[fr:sgsn]="Cœur — OsmoSGSN (rattachement paquet)"
_TM[en:sgsn]="Core — OsmoSGSN (packet attach)"
_TM[es:sgsn]="Núcleo — OsmoSGSN (conexión de paquetes)"
_TM[pt:sgsn]="Núcleo — OsmoSGSN (ligação de pacotes)"
_TM[it:sgsn]="Core — OsmoSGSN (attach pacchetti)"
_TM[hi:sgsn]="कोर — OsmoSGSN (पैकेट अटैच)"
_TM[zh:sgsn]="核心 — OsmoSGSN(分组附着)"
_TM[ar:sgsn]="النواة — OsmoSGSN (ارتباط الحزم)"
_TM[fr:sidecar-bts]="Station de base BTS#1 (side-car)"
_TM[en:sidecar-bts]="Base station BTS#1 (side-car)"
_TM[es:sidecar-bts]="Estación base BTS#1 (side-car)"
_TM[pt:sidecar-bts]="Estação base BTS#1 (side-car)"
_TM[it:sidecar-bts]="Stazione base BTS#1 (side-car)"
_TM[hi:sidecar-bts]="बेस स्टेशन BTS#1 (साइड-कार)"
_TM[zh:sidecar-bts]="基站 BTS#1(边车)"
_TM[ar:sidecar-bts]="المحطة القاعدية BTS#1 (الرديفة)"
_TM[fr:sidecar-cfg]="Configurations du BTS#1 (side-car)"
_TM[en:sidecar-cfg]="BTS#1 configurations (side-car)"
_TM[es:sidecar-cfg]="Configuraciones del BTS#1 (side-car)"
_TM[pt:sidecar-cfg]="Configurações do BTS#1 (side-car)"
_TM[it:sidecar-cfg]="Configurazioni del BTS#1 (side-car)"
_TM[hi:sidecar-cfg]="BTS#1 के विन्यास (साइड-कार)"
_TM[zh:sidecar-cfg]="BTS#1 配置(边车)"
_TM[ar:sidecar-cfg]="إعدادات BTS#1 (الرديفة)"
_TM[fr:sidecar-faketrx]="Transceiver simulé du BTS#1"
_TM[en:sidecar-faketrx]="BTS#1 simulated transceiver"
_TM[es:sidecar-faketrx]="Transceptor simulado del BTS#1"
_TM[pt:sidecar-faketrx]="Transcetor simulado do BTS#1"
_TM[it:sidecar-faketrx]="Ricetrasmettitore simulato del BTS#1"
_TM[hi:sidecar-faketrx]="BTS#1 का सिमुलेटेड ट्रांससीवर"
_TM[zh:sidecar-faketrx]="BTS#1 的模拟收发器"
_TM[ar:sidecar-faketrx]="جهاز الإرسال المحاكى لـ BTS#1"
_TM[fr:sidecar-mobile]="Second abonné (MS#2)"
_TM[en:sidecar-mobile]="Second subscriber (MS#2)"
_TM[es:sidecar-mobile]="Segundo abonado (MS#2)"
_TM[pt:sidecar-mobile]="Segundo assinante (MS#2)"
_TM[it:sidecar-mobile]="Secondo abbonato (MS#2)"
_TM[hi:sidecar-mobile]="दूसरा ग्राहक (MS#2)"
_TM[zh:sidecar-mobile]="第二个用户(MS#2)"
_TM[ar:sidecar-mobile]="المشترك الثاني (MS#2)"
_TM[fr:sidecar-trxcon]="Couche 1 du MS#2 (trxcon)"
_TM[en:sidecar-trxcon]="MS#2 layer 1 (trxcon)"
_TM[es:sidecar-trxcon]="Capa 1 del MS#2 (trxcon)"
_TM[pt:sidecar-trxcon]="Camada 1 do MS#2 (trxcon)"
_TM[it:sidecar-trxcon]="Livello 1 del MS#2 (trxcon)"
_TM[hi:sidecar-trxcon]="MS#2 की लेयर 1 (trxcon)"
_TM[zh:sidecar-trxcon]="MS#2 的第一层(trxcon)"
_TM[ar:sidecar-trxcon]="الطبقة الأولى لـ MS#2 (trxcon)"
_TM[fr:sip-connector]="Cœur — passerelle MNCC↔SIP"
_TM[en:sip-connector]="Core — MNCC↔SIP gateway"
_TM[es:sip-connector]="Núcleo — pasarela MNCC↔SIP"
_TM[pt:sip-connector]="Núcleo — gateway MNCC↔SIP"
_TM[it:sip-connector]="Core — gateway MNCC↔SIP"
_TM[hi:sip-connector]="कोर — MNCC↔SIP गेटवे"
_TM[zh:sip-connector]="核心 — MNCC↔SIP 网关"
_TM[ar:sip-connector]="النواة — بوابة MNCC↔SIP"
_TM[fr:smsc]="Cœur — passerelle SMS (proto-SMSC)"
_TM[en:smsc]="Core — SMS gateway (proto-SMSC)"
_TM[es:smsc]="Núcleo — pasarela SMS (proto-SMSC)"
_TM[pt:smsc]="Núcleo — gateway SMS (proto-SMSC)"
_TM[it:smsc]="Core — gateway SMS (proto-SMSC)"
_TM[hi:smsc]="कोर — SMS गेटवे (proto-SMSC)"
_TM[zh:smsc]="核心 — 短信网关(proto-SMSC)"
_TM[ar:smsc]="النواة — بوابة الرسائل (proto-SMSC)"
_TM[fr:stp]="Cœur — OsmoSTP (SS7/M3UA)"
_TM[en:stp]="Core — OsmoSTP (SS7/M3UA)"
_TM[es:stp]="Núcleo — OsmoSTP (SS7/M3UA)"
_TM[pt:stp]="Núcleo — OsmoSTP (SS7/M3UA)"
_TM[it:stp]="Core — OsmoSTP (SS7/M3UA)"
_TM[hi:stp]="कोर — OsmoSTP (SS7/M3UA)"
_TM[zh:stp]="核心 — OsmoSTP(SS7/M3UA)"
_TM[ar:stp]="النواة — OsmoSTP (SS7/M3UA)"
_TM[fr:summary]="Identité du build et résumé"
_TM[en:summary]="Build identity and summary"
_TM[es:summary]="Identidad de la compilación y resumen"
_TM[pt:summary]="Identidade da compilação e resumo"
_TM[it:summary]="Identità della build e riepilogo"
_TM[hi:summary]="बिल्ड पहचान और सारांश"
_TM[zh:summary]="构建标识与摘要"
_TM[ar:summary]="هوية البناء والملخص"
_TM[fr:teardown]="Nettoyage du run précédent"
_TM[en:teardown]="Cleaning up the previous run"
_TM[es:teardown]="Limpieza de la ejecución anterior"
_TM[pt:teardown]="Limpeza da execução anterior"
_TM[it:teardown]="Pulizia dell'esecuzione precedente"
_TM[hi:teardown]="पिछले रन की सफ़ाई"
_TM[zh:teardown]="清理上一次运行"
_TM[ar:teardown]="تنظيف التشغيل السابق"
_TM[fr:tmux]="Session d'observation tmux"
_TM[en:tmux]="tmux observation session"
_TM[es:tmux]="Sesión de observación tmux"
_TM[pt:tmux]="Sessão de observação tmux"
_TM[it:tmux]="Sessione di osservazione tmux"
_TM[hi:tmux]="tmux अवलोकन सत्र"
_TM[zh:tmux]="tmux 观察会话"
_TM[ar:tmux]="جلسة مراقبة tmux"

_T[fr:arret]="Arrêt de %s"
_T[en:arret]="Stopping %s"
_T[es:arret]="Deteniendo %s"
_T[pt:arret]="A parar %s"
_T[it:arret]="Arresto di %s"
_T[hi:arret]="%s रोका जा रहा है"
_T[zh:arret]="正在停止 %s"
_T[ar:arret]="إيقاف %s"
_T[fr:dep_ko]="dépendance non satisfaite : %s"
_T[en:dep_ko]="unmet dependency: %s"
_T[es:dep_ko]="dependencia no satisfecha: %s"
_T[pt:dep_ko]="dependência não satisfeita: %s"
_T[it:dep_ko]="dipendenza non soddisfatta: %s"
_T[hi:dep_ko]="निर्भरता अपूर्ण: %s"
_T[zh:dep_ko]="依赖未满足:%s"
_T[ar:dep_ko]="تبعية غير مستوفاة: %s"

_T[fr:obligatoire]="obligatoire"
_T[en:obligatoire]="required"
_T[es:obligatoire]="obligatorio"
_T[pt:obligatoire]="obrigatório"
_T[it:obligatoire]="obbligatorio"
_T[hi:obligatoire]="अनिवार्य"
_T[zh:obligatoire]="必需"
_T[ar:obligatoire]="إلزامي"
_T[fr:optionnel]="optionnel"
_T[en:optionnel]="optional"
_T[es:optionnel]="opcional"
_T[pt:optionnel]="opcional"
_T[it:optionnel]="opzionale"
_T[hi:optionnel]="वैकल्पिक"
_T[zh:optionnel]="可选"
_T[ar:optionnel]="اختياري"
_T[fr:apres]="après: %s"
_T[en:apres]="after: %s"
_T[es:apres]="tras: %s"
_T[pt:apres]="após: %s"
_T[it:apres]="dopo: %s"
_T[hi:apres]="के बाद: %s"
_T[zh:apres]="依赖:%s"
_T[ar:apres]="بعد: %s"

_T[de:plan]="Plan — %d Module"
_T[de:ok]="OK"
_T[de:fail]="FEHL"
_T[de:skip]="ÜBER"
_T[de:dry]="PROB"
_T[de:simule]="simuliert"
_T[de:deja]="läuft bereits"
_T[de:desactive]="deaktiviert durch %s"
_T[de:interrompu]="Ablauf abgebrochen (Pflichtmodul)"
_T[de:journal]="Protokoll: %s"
_T[de:pas_pret]="gestartet, aber nie bereit: %s"
_T[de:sans_raison]="kein Grund vom Modul angegeben"
_T[de:prereq_sim]="Voraussetzung im Probelauf nicht prüfbar: %s"
_T[de:bilan]="%d ok · %d übersprungen · %d Fehler"
_T[de:opt_inconnue]="unbekannte Option: %s"
_T[de:arret]="%s wird beendet"
_T[de:dep_ko]="Abhängigkeit nicht erfüllt: %s"
_T[de:obligatoire]="Pflicht"
_T[de:optionnel]="optional"
_T[de:apres]="nach: %s"
_TM[de:abonnes-hlr]="Kern — im HLR eingerichtete Teilnehmer"
_TM[de:apn0]="Kern — TUN-Schnittstelle des APN"
_TM[de:asterisk]="Kern — Asterisk (SIP-Telefonanlage)"
_TM[de:attach]="Rückkehr in die tmux-Sitzung"
_TM[de:audio]="Audiobrücke (PulseAudio + GAPK)"
_TM[de:bsc]="Kern — OsmoBSC (BTS-Steuerung)"
_TM[de:bts]="Basisstation osmo-bts-trx"
_TM[de:config]="Auflösung von Pfaden und Ports"
_TM[de:gabarits]="Konfigurationsvorlagen des Kerns"
_TM[de:ggsn]="Kern — OsmoGGSN (GTP-Daten)"
_TM[de:gsmtap]="GSMTAP-Mitschnitt (pcap)"
_TM[de:hlr]="Kern — OsmoHLR (GSUP-Register)"
_TM[de:l2]="Schicht-2-Client"
_TM[de:logs]="Protokolle und Arbeitsbereich"
_TM[de:mgw]="Kern — OsmoMGW (MGCP/RTP-Medien)"
_TM[de:mobile-cfg]="Konfiguration des Mobilteils"
_TM[de:msc]="Kern — OsmoMSC (leitungsvermittelt)"
_TM[de:osmocon]="osmocon-Übergang"
_TM[de:outils]="Werkzeuge des Wirtssystems"
_TM[de:panes]="Überwachungsansicht (tmux)"
_TM[de:pcu]="Kern — OsmoPCU (Paketzuteilung)"
_TM[de:prereqs]="Prüfung der Voraussetzungen"
_TM[de:pty]="Serielles PTY des Emulators"
_TM[de:qemu]="Calypso-Emulator (QEMU)"
_TM[de:reseau]="Schnittstellen und Adressplan"
_TM[de:restes]="Beenden der erfassten Prozesse"
_TM[de:rundir]="Ausführungsverzeichnis"
_TM[de:sgsn]="Kern — OsmoSGSN (Paketanbindung)"
_TM[de:sidecar-bts]="Basisstation BTS#1 (Beiwagen)"
_TM[de:sidecar-cfg]="Konfigurationen der BTS#1 (Beiwagen)"
_TM[de:sidecar-faketrx]="Simulierter Transceiver der BTS#1"
_TM[de:sidecar-mobile]="Zweiter Teilnehmer (MS#2)"
_TM[de:sidecar-trxcon]="Schicht 1 des MS#2 (trxcon)"
_TM[de:sip-connector]="Kern — MNCC↔SIP-Übergang"
_TM[de:smsc]="Kern — SMS-Übergang (proto-SMSC)"
_TM[de:stp]="Kern — OsmoSTP (SS7/M3UA)"
_TM[de:summary]="Build-Kennung und Zusammenfassung"
_TM[de:teardown]="Aufräumen des vorherigen Laufs"
_TM[de:tmux]="tmux-Beobachtungssitzung"

_T[fr:up]="ACTIF"
_T[de:up]="AKTIV"
_T[en:up]="UP"
_T[es:up]="ACTIVO"
_T[pt:up]="ATIVO"
_T[it:up]="ATTIVO"
_T[hi:up]="चालू"
_T[zh:up]="运行"
_T[ar:up]="يعمل"
_T[fr:down]="ARRÊT"
_T[de:down]="AUS"
_T[en:down]="DOWN"
_T[es:down]="PARADO"
_T[pt:down]="PARADO"
_T[it:down]="FERMO"
_T[hi:down]="बंद"
_T[zh:down]="停止"
_T[ar:down]="متوقف"
_T[fr:connecter]="se connecter"
_T[de:connecter]="verbinden"
_T[en:connecter]="attach"
_T[es:connecter]="conectar"
_T[pt:connecter]="ligar"
_T[it:connecter]="collegarsi"
_T[hi:connecter]="जुड़ें"
_T[zh:connecter]="接入"
_T[ar:connecter]="اتصال"
_T[fr:pas_session]="(aucune session tmux « %s » — la pile tourne en arrière-plan)"
_T[de:pas_session]="(keine tmux-Sitzung „%s“ — der Stapel läuft im Hintergrund)"
_T[en:pas_session]="(no tmux session \"%s\" — the stack runs in the background)"
_T[es:pas_session]="(sin sesión tmux «%s» — la pila corre en segundo plano)"
_T[pt:pas_session]="(sem sessão tmux «%s» — a pilha corre em segundo plano)"
_T[it:pas_session]="(nessuna sessione tmux «%s» — lo stack gira in background)"
_T[hi:pas_session]="(कोई tmux सत्र «%s» नहीं — स्टैक पृष्ठभूमि में चल रहा है)"
_T[zh:pas_session]="(没有 tmux 会话「%s」— 整套服务在后台运行)"
_T[ar:pas_session]="(لا توجد جلسة tmux «%s» — الحزمة تعمل في الخلفية)"
_T[fr:journaux]="journaux"
_T[de:journaux]="Protokolle"
_T[en:journaux]="logs"
_T[es:journaux]="registros"
_T[pt:journaux]="registos"
_T[it:journaux]="registri"
_T[hi:journaux]="लॉग"
_T[zh:journaux]="日志"
_T[ar:journaux]="السجلات"
_T[fr:etat]="état"
_T[de:etat]="Status"
_T[en:etat]="status"
_T[es:etat]="estado"
_T[pt:etat]="estado"
_T[it:etat]="stato"
_T[hi:etat]="स्थिति"
_T[zh:etat]="状态"
_T[ar:etat]="الحالة"
_T[fr:arreter]="arrêter"
_T[de:arreter]="beenden"
_T[en:arreter]="stop"
_T[es:arreter]="detener"
_T[pt:arreter]="parar"
_T[it:arreter]="fermare"
_T[hi:arreter]="रोकें"
_T[zh:arreter]="停止"
_T[ar:arreter]="إيقاف"
_T[fr:aucun_mod]="aucun module dans %s — rien à faire"
_T[de:aucun_mod]="kein Modul in %s — nichts zu tun"
_T[en:aucun_mod]="no module in %s — nothing to do"
_T[es:aucun_mod]="ningún módulo en %s — nada que hacer"
_T[pt:aucun_mod]="nenhum módulo em %s — nada a fazer"
_T[it:aucun_mod]="nessun modulo in %s — niente da fare"
_T[hi:aucun_mod]="%s में कोई मॉड्यूल नहीं — कुछ नहीं करना"
_T[zh:aucun_mod]="%s 中没有模块 — 无事可做"
_T[ar:aucun_mod]="لا توجد وحدات في %s — لا شيء لفعله"

t() {
    local cle="$1"; shift
    local fmt="${_T[${CALYPSO_LANG}:$cle]:-${_T[en:$cle]:-${_T[fr:$cle]:-$cle}}}"
    printf "$fmt" "$@"
}

t_mod() {
    printf '%s' "${_TM[${CALYPSO_LANG}:$1]:-${_TM[en:$1]:-${_TM[fr:$1]:-${MOD_DESC[$1]:-$1}}}}"
}

case " fr de en es pt it hi zh ar " in
    *" $CALYPSO_LANG "*) ;;
    *) printf 'CALYPSO_LANG=%s inconnue — repli sur « en » (fr de en es pt it hi zh ar)\n' \
              "$CALYPSO_LANG" >&2
       CALYPSO_LANG=en ;;
esac

3.52 https://github.com/bbaranoff/qosmo/blob/main/run_modules/_lib/mod.sh

2363 octets, 80 lignes → 80 lignes

. "$(dirname "${BASH_SOURCE[0]}")/i18n.sh"

readonly MOD_RC_OK=0
readonly MOD_RC_FAIL=1
readonly MOD_RC_UNKNOWN=2
readonly MOD_RC_ALREADY=3
readonly MOD_RC_SKIP=4

declare -a MOD_ORDER=()
declare -A MOD_DESC=()
declare -A MOD_DEPS=()
declare -A MOD_REQUIRED=()
declare -A MOD_LOG=()
declare -A MOD_JOURNAL=()
declare -A MOD_PURE=()
declare -A MOD_ENABLED_IF=()
declare -A MOD_TIMEOUT=()

MOD_REGISTER() {
    local slug="$1" desc="$2"
    MOD_ORDER+=("$slug")
    MOD_DESC[$slug]="$desc"
    MOD_DEPS[$slug]=""
    MOD_REQUIRED[$slug]=1
    MOD_LOG[$slug]=""
    MOD_JOURNAL[$slug]=""
    MOD_PURE[$slug]=0
    MOD_ENABLED_IF[$slug]='true'
    MOD_TIMEOUT[$slug]=30
}

mod_prefix() { printf 'mod_%s' "${1//-/_}"; }

_MOD_REASON=""
_MOD_HINT=""

mod_ok()      { _MOD_REASON=""   ; return $MOD_RC_OK; }
mod_fail()    { _MOD_REASON="$*" ; return $MOD_RC_FAIL; }
mod_already() { _MOD_REASON="$*" ; return $MOD_RC_ALREADY; }
mod_skip()    { _MOD_REASON="$*" ; return $MOD_RC_SKIP; }
mod_hint()    { _MOD_HINT="$*"; }
mod_say()     { printf '%s\n' "$*"; }

wait_until() {
    local timeout="$1" what="$2"; shift 2
    local deadline=$(( SECONDS + timeout ))
    while (( SECONDS < deadline )); do
        if "$@" >/dev/null 2>&1; then return $MOD_RC_OK; fi
        sleep 0.2
    done
    mod_fail "$what : toujours pas prêt après ${timeout}s"
}

have_proc() {
    local motif="$1" p ligne ns
    ns="$(readlink /proc/self/ns/pid 2>/dev/null)"
    for p in $(pgrep -f "$motif" 2>/dev/null); do
        [ "$p" = "$$" ] || [ "$p" = "$PPID" ] && continue
        [ -z "$ns" ] || [ "$(readlink "/proc/$p/ns/pid" 2>/dev/null)" = "$ns" ] || continue
        ligne="$( { tr '\0' ' ' < "/proc/$p/cmdline"; } 2>/dev/null )"
        [ -n "$ligne" ] || continue
        case "$ligne" in
            *"tail "*|*"tail -"*|*"less "*|*"multitail "*|*"watch "*|\
            *"sleep infinity"*|*"journalctl "*) continue ;;
        esac
        return 0
    done
    return 1
}
have_port()  { (exec 3<>"/dev/tcp/127.0.0.1/$1") >/dev/null 2>&1; }
have_unix()  { [ -S "$1" ]; }
log_has()    { [ -f "$1" ] && grep -q "$2" "$1" 2>/dev/null; }

modb_tail() {
    local f="$1" n="${2:-20}"
    if [ ! -r "$f" ]; then mod_say "journal absent ou illisible : $f"; return 0; fi
    mod_say "--- $n dernières lignes de $f ---"
    tail -n "$n" "$f" 2>/dev/null
    mod_say "--- fin de $f ---"
}

3.53 https://github.com/bbaranoff/qosmo/blob/main/run_modules/_lib/modem.sh

1979 octets, 35 lignes → 35 lignes

[ -n "${_MODEM_LIB_LOADED:-}" ] && return 0
_MODEM_LIB_LOADED=1
# _lib/modem.sh - the bench's GPRS MS: the osmocom-bb "modem" (GMM/SM/SNDCP/LLC)
# attached as a SECOND MS on the side-car's fake_trx (BTS#1), next to MS#2.
# fake_trx can serve several MS (--trx): no home-made multiplexer.
#   BTS#1 (bts 1, TS5-7 PDCH) --TRXD 5720--> fake_trx --6720--> trxcon MS#2 (voice)
#                                                   \--6740--> trxcon modem (GPRS)
#   BTS#1 --/tmp/pcu_bts2--> osmo-pcu (second, nsei 11) --Gb--> osmo-sgsn --> osmo-ggsn (apn internet)
# MODEM_GPRS=0 disables the whole set (start-native.sh --no-gprs-modem).
: "${MODEM_GPRS:=1}"
: "${MODEM_BIN:=${OSMOCOM_BB:-${GSM_ROOT:-/opt/GSM}/osmocom-bb}/src/host/layer23/src/modem/modem}"
: "${MODEM_BB_PORT:=6740}"            # TRXD base on the modem side (fake_trx --trx ...:6740, trxcon -p 6740)
: "${MODEM_L2_SOCK:=/tmp/modem_l2}"
: "${MODEM_VTY_PORT:=4299}"
: "${MODEM_CFG:=${OSMOCOM_CFG:-/etc/osmocom}/modem-gprs.cfg}"
: "${MODEM_TUN:=tun_modem}"
: "${MODEM_MS_IDX:=3}"                # 3rd subscriber: IMSI ...000003, MSISDN <node>00<op>03
: "${MODEM_APN:=internet}"
: "${MODEM_TRXCON_FN_ADVANCE:=100}"  # -F: FBSB timeout extension (as for MS#2)
: "${MODEM_TRXCON_TRX_ADVANCE:=0}"   # -f: UL pull advance; 0 required on PDCH (see 69-modem-trxcon.sh)
# Second PCU, the one of BTS#1 (the first serves the main BTS on /tmp/pcu_bts).
: "${SC_PCU_UNIT:=osmo-pcu-bts1}"
: "${SC_PCU_SOCKET:=/tmp/pcu_bts2}"
: "${SC_PCU_CFG:=${OSMOCOM_CFG:-/etc/osmocom}/osmo-pcu-bts1.cfg}"
: "${SC_PCU_VTY_HOST:=127.0.0.2}"     # same port 4240 as the first PCU, other address
: "${SC_PCU_VTY_PORT:=4240}"
# Gb of BTS#1 (gprs block of "bts 1", cf. 13-sidecar-cfg.sh): distinct from the main BTS (nsei 10, bvci 12, 23001).
: "${SC_GPRS_NSEI:=11}"
: "${SC_GPRS_NSVCI:=11}"
: "${SC_GPRS_BVCI:=13}"
: "${SC_GPRS_NS_PORT:=23002}"
: "${SC_SGSN_IP:=192.168.2.10}"
: "${SC_SGSN_PORT:=23000}"

modem_enabled() { [ "${MODEM_GPRS:-1}" = 1 ]; }

3.54 https://github.com/bbaranoff/qosmo/blob/main/run_modules/_lib/radio.sh

1986 octets, 59 lignes → 59 lignes

[ -n "${_RADIO_LIB_LOADED:-}" ] && return 0
_RADIO_LIB_LOADED=1

: "${SC_TRX_PORT:=5720}"
: "${SC_BB_PORT:=6720}"
: "${SC_BTS_VTY_PORT:=4250}"
: "${SC_MOBILE_VTY_PORT:=4248}"
: "${SC_UNIT_ID:=6002}"
: "${SC_ARFCN:=516}"
: "${SC_BTS_CFG:=${OSMOCOM_CFG:-/etc/osmocom}/osmo-bts-trx-bts1.cfg}"
: "${SC_MOBILE_CFG:=/root/.osmocom/bb/mobile_faketrx_bts1.cfg}"
: "${SC_L2_SOCK:=/tmp/ms2_l2}"
: "${TRX_BIND_IP:=127.0.0.1}"
: "${TRX_BTS_IP:=127.0.0.1}"
: "${TRX_BB_IP:=127.0.0.1}"
: "${RUN_DIR:=/tmp/calypso}"
: "${LOG_DIR:=$RUN_DIR/logs}"

radio_dirs() { mkdir -p "$RUN_DIR" "$LOG_DIR" 2>/dev/null || true; }
radio_log() { printf '%s/%s.log\n' "$LOG_DIR" "$1"; }
radio_pidfile() { printf '%s/%s.pid\n' "$RUN_DIR" "$1"; }
radio_save_pid() { radio_dirs; printf '%s\n' "$2" > "$(radio_pidfile "$1")"; }
radio_pid()   { cat "$(radio_pidfile "$1")" 2>/dev/null || echo 0; }
radio_alive() { local p; p="$(radio_pid "$1")"; [ "$p" != 0 ] && kill -0 "$p" 2>/dev/null; }

radio_kill() {
    local slug="$1" pattern="${2:-}" p i
    p="$(radio_pid "$slug")"
    [ "$p" != 0 ] && kill "$p" 2>/dev/null
    [ -n "$pattern" ] && pkill -f "$pattern" 2>/dev/null
    for i in 1 2 3 4 5 6 7 8 9 10; do
        if [ "$p" = 0 ] || ! kill -0 "$p" 2>/dev/null; then break; fi
        sleep 0.2
    done
    if [ "$p" != 0 ] && kill -0 "$p" 2>/dev/null; then
        kill -9 "$p" 2>/dev/null
        [ -n "$pattern" ] && pkill -9 -f "$pattern" 2>/dev/null
    fi
    rm -f "$(radio_pidfile "$slug")"
    return 0
}

radio_udp_bound() {
    ss -uan 2>/dev/null | grep -qE "[:.]$1[[:space:]]"
}

radio_cfg_val() {
    local f="$1"; shift
    [ -r "$f" ] || return 1
    awk -v k="$*" '
        {
            line = $0
            sub(/^[[:space:]]+/, "", line); sub(/[[:space:]]+$/, "", line)
            n = split(k, kk, /[[:space:]]+/)
            split(line, ff, /[[:space:]]+/)
            for (i = 1; i <= n; i++) if (ff[i] != kk[i]) next
            if (ff[n+1] != "") { print ff[n+1]; exit }
        }' "$f"
}

3.55 https://github.com/bbaranoff/qosmo/blob/main/run_modules/_lib/temps_reel.sh

1672 octets, 36 lignes → 36 lignes

[ -n "${_TEMPS_REEL_LIB_LOADED:-}" ] && return 0
_TEMPS_REEL_LIB_LOADED=1

# Real-time priority (SCHED_RR) for the processes that have a hard deadline.
#
# osmo-bts-trx shuts itself down ("PC clock skew too high", exit 1) when its
# frame timer (4.615 ms) is served too late; under load a SCHED_OTHER process
# can wait long enough for that to happen, and BTS#0 is close to the same
# limit ("We missed 1 timers" in bts.log).
#
# Done with chrt AT LAUNCH, not with "cpu-sched / policy rr 1" in the .cfg:
# with the .cfg, osmo-bts-trx 1.10 REFUSES TO START when the right is missing
# ("Failed setting SCHED_RR priority 1", exit 1, without CAP_SYS_NICE or
# rtprio). Here it is tried first: without the right, the launch is unchanged.
# The right: root (CAP_SYS_NICE) on the host; "--ulimit rtprio=18" in the
# containers (start-docker.sh, compose.yaml).
#
#   QOSMO_RT_PRIO=<1-99>  SCHED_RR priority (default 1: above everything
#                         SCHED_OTHER, below the rest of real time)
#   QOSMO_RT_PRIO=0       disabled: SCHED_OTHER

# Prefix to put in front of the command: "chrt -r <prio>", or nothing.
# Expands WITHOUT quotes:  setsid stdbuf -oL $(rt_prefixe) "$BIN" ...
# chrt does an exec: the launched pid remains the binary's, and the threads
# it creates inherit the policy.
rt_prefixe() {
    local prio="${QOSMO_RT_PRIO:-1}"
    [ "$prio" -gt 0 ] 2>/dev/null || return 0
    chrt -r "$prio" true 2>/dev/null && printf 'chrt -r %s\n' "$prio"
    return 0
}

# Effective scheduling of a process, for the logs: "SCHED_RR 1".
rt_etat() {   # $1 = pid
    LC_ALL=C chrt -p "$1" 2>/dev/null | awk -F': ' '/policy/ { p = $2 } /priority/ { print p, $2 }'
}

3.56 https://github.com/bbaranoff/qosmo/blob/main/scripts/inject_fb.py

2925 octets, 90 lignes → 90 lignes

#!/usr/bin/env python3
"""
inject_fb.py — diagnostic script: send a clean FB burst (148 zero bits)
straight to QEMU's BSP UDP port (127.0.0.1:6702), bypassing osmo-bts-trx.

Purpose: confirm whether the DSP correlator at PROM0 0x82f6 actually
writes d_fb_det when fed a perfect frequency burst. If yes → the loop
in production runs is failing on amplitude/phase/timing of the bridge's
GMSK simulation. If no → the DSP code path itself is broken.

We tail bridge.log to track QEMU's current FN (the bridge prints
"bridge: QEMU tick #N FN=X" on every TDMA tick) and send each burst
slightly in the future of the live FN so it lands in the BSP match
window (±64).
"""
import os
import re
import socket
import struct
import sys
import time

(removed) = "/tmp/bridge.log"
BSP_ADDR = ("127.0.0.1", 6702)
LOOKAHEAD = 30          # frames ahead of cur_fn (well inside ±64 window)
SEND_PERIOD_S = 0.004   # ~one TDMA frame
DURATION_S = 30

def make_fb_burst(tn, fn, rssi=20, toa=0):
    """TRXDv0 DL: tn(1) fn(4 BE) rssi(1) toa(2 BE) + 148 zero bits."""
    return (
        bytes([tn & 0x07])
        + struct.pack(">I", fn & 0xFFFFFFFF)
        + bytes([rssi])
        + struct.pack(">H", toa & 0xFFFF)
        + bytes(148)
    )

def latest_fn_from_bridge():
    """Walk bridge.log backwards looking for the most recent 'fn=N' tag."""
    try:
        with open((removed), "rb") as f:
            f.seek(0, 2)
            size = f.tell()
            chunk = 8192
            f.seek(max(0, size - chunk))
            tail = f.read().decode("utf-8", errors="replace")
        # Prefer DL bursts (carry bts_fn and our rewritten fn) for freshness
        m = list(re.finditer(r"\bfn=(\d+)", tail))
        if m:
            return int(m[-1].group(1))
    except FileNotFoundError:
        pass
    return None

def main():
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    print(f"inject_fb: target={BSP_ADDR} lookahead={LOOKAHEAD} duration={DURATION_S}s",
          flush=True)

    # Wait until bridge.log has at least one fn= line
    start = time.time()
    while time.time() - start < 10:
        fn = latest_fn_from_bridge()
        if fn is not None:
            break
        time.sleep(0.2)
    else:
        print("inject_fb: no fn= seen in bridge.log within 10s", file=sys.stderr)
        sys.exit(1)
    print(f"inject_fb: starting FN={fn}", flush=True)

    sent = 0
    end = time.time() + DURATION_S
    while time.time() < end:
        cur = latest_fn_from_bridge()
        if cur is None:
            cur = fn
        target_fn = cur + LOOKAHEAD
        sock.sendto(make_fb_burst(tn=0, fn=target_fn), BSP_ADDR)
        sent += 1
        if sent % 250 == 0:
            print(f"inject_fb: sent={sent} cur_fn~={cur} target_fn={target_fn}",
                  flush=True)
        time.sleep(SEND_PERIOD_S)

    print(f"inject_fb: done sent={sent}", flush=True)

if __name__ == "__main__":
    main()

3.57 https://github.com/bbaranoff/qosmo/blob/main/scripts/make_dsp_bin_L1.py

8610 octets, 233 lignes → 227 lignes (3 groupes compactés)

#!/usr/bin/env python3

# --- installation root, resolved without a hard-coded path -------------------
# GSM_ROOT if the environment sets it (the case when going through run.sh),
# otherwise the parent of the repository: both repositories live side by side.
import os as _os
GSM_ROOT = _os.environ.get(
    "GSM_ROOT",
    _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__))))

"""
make_dsp_bin_L1.py — Generate calypso_dsp.PROM0.bin with synthetic L1+L2 publisher.

Scope (L1 → L2):
- FB lock : d_fb_det + a_sync_demod[4] cells (TOA/PM/ANGLE/SNR)
- PM measurement : a_pm[3] WP0/WP1 with canned -85 dBm value
- SB lock : a_sch[5] WP0/WP1 with canned BSIC matching osmo-bsc.cfg
- BCCH SI3 (L2) : a_cd[0..14] with FIRE-OK status + 23-byte SI3 LAPDm frame
  (same canned bytes as CALYPSO_DSP_SHUNT shunt_dispatch_allc, MCC=001/MNC=01)

Entry point : 0x7120 (= SILICON-BOOT-REDIRECT target from c54x.c).
Patches existing PROM0.bin at offset 0x240 (= word 0x120, code addr 0x7120).
Rest of ROM preserved.

Architecture :
- Boot init at 0x7120 : sets SP=0x5AC8, IMR=0xFFFF
- Main loop : tight loop writing all cells continuously
- ARM reads cells anytime, always sees canned "lock detected" values

Strategy : continuous-write rather than dispatch-on-task. Simpler than full
dispatcher, plus ARM cell-read race is auto-resolved (whatever read happens
sees current canned value).

Usage :
    python3 make_dsp_bin_L1.py [input.bin] [output.bin]
    Default input  : /opt/GSM/calypso_dsp.PROM0.bin
    Default output : /opt/GSM/calypso_dsp.PROM0.bin.L1
"""
import sys
import struct

INPUT  = sys.argv[1] if len(sys.argv) > 1 else f'{GSM_ROOT}/calypso_dsp.PROM0.bin'
# *** OUTPUT path EXPLICITLY separate — NEVER overwrites the original ***
OUTPUT = sys.argv[2] if len(sys.argv) > 2 else '/tmp/calypso_dsp_L1stub.PROM0.bin'

# ---- C54x opcode encoders ----

def stm_lk_mmr(lk, mmr):
    """STM #lk, MMR — 2 words (0x7700 | MMR_low7, lk)"""
    return [0x7700 | (mmr & 0x7F), lk & 0xFFFF]

def ld_lk_a(lk):
    """LD #lk, A — 2 words ALU class F0xx (alu_op=2 LD)"""
    # bits 7:4 = 0010 = LD ; bit 8 = SRC ; bit 9 = DST
    # Use src=A dst=A : bit 8 = 0 bit 9 = 0
    # opcode 0xF0 << 8 | (alu_op << 4) | shift = 0xF020 (shift=0)
    return [0xF020, lk & 0xFFFF]

def stl_a_arn(n, mod):
    """STL A, *ARn[mod] — 1 word (0x80xx hi8 = STL A)
       mod ∈ {0=*ARn, 1=*ARn-, 2=*ARn+}"""
    smem = 0x80 | ((mod & 0xF) << 3) | (n & 0x7)
    return [(0x80 << 8) | smem]

def b_unc(target):
    """B pmad (unconditional NEAR branch) — 2 words.
       Use 0xF880 (= FB FAR in c54x.c emulator but sets XPC=0 always,
       which is no-op when staying within PROM0). Same encoding as the
       SILICON-BOOT-REDIRECT vector synthesis in calypso_c54x.c.
       NOT 0xF800 : that's BANZ AR0 (= conditional on AR0!=0) in this emu."""
    return [0xF880, target & 0xFFFF]

# ---- MMR table ----
MMR_IMR = 0x00
MMR_AR0 = 0x10
MMR_SP  = 0x18

# ---- Cell addresses (DSP code space) ----
# API base = 0x0800
# NDB base = 0x08D4 (= byte 0x01A8 / 2)
D_FB_DET     = 0x08F8    # NDB + 0x48 byte
D_FB_MODE    = 0x08F9    # NDB + 0x4A byte
A_SYNC_DEMOD = 0x08FA    # NDB + 0x4C..0x53 byte = 4 words
A_PM_WP0     = 0x0830    # API + 0x60 byte = read page 0 word 8
A_PM_WP1     = 0x0844    # API + 0x88 byte = read page 1 word 8
# a_sch[5] at words 15..19 of read page (after a_pm[3] words 8..10 + a_serv_demod[4] words 11..14)
A_SCH_WP0    = 0x0837    # RP0 + 15 word = 0x0828 + 15
A_SCH_WP1    = 0x084B    # RP1 + 15 word = 0x083C + 15
# a_cd[0..14] = CCCH demod result.
# Per the firmware DWARF: NDB+0x1DC byte = NDB+0xEE word
# = code 0x08D4 + 0xEE = 0x09C2. The shunt uses 0x1FC (probably an alternative
# struct variant). The DWARF, the firmware authority, is followed.
A_CD_BASE    = 0x09C2    # a_cd[0]
A_CD_PAYLOAD = 0x09C5    # a_cd[3] = start of 12-word SI3 packed payload

# ---- Canned values ----
FB_DET_VAL   = 0x4E20     # 20000 = nonzero detection flag
TOA_VAL      = 0x0240     # 576 (mid burst window)
PM_VAL       = 0xFB00     # signed -1280, firmware >>3 = -160, Power = (-160-520)/8 = -85 dBm
ANGLE_VAL    = 0x0000     # AFC error = 0 Hz
SNR_VAL      = 0x4E20     # 20000 = above FB1_SNR_THRESH=3000

# SB encoding for BSIC=7, t1=0, t2=0, t3=1 : sb = (bsic & 0x3f) << 2 = 0x1C
SB_BSIC      = 7
SB_PACKED    = (SB_BSIC & 0x3F) << 2  # = 0x1C
SB_LO        = SB_PACKED & 0xFFFF      # = 0x001C
SB_HI        = (SB_PACKED >> 16) & 0xFFFF  # = 0x0000

# ---- Canned SI3 L2 frame (23 bytes, identical to shunt_dispatch_allc) ----
SI3_BYTES = [
    0x49, 0x06, 0x1B,                    # L2 hdr + RR PD + SI3 mt
    0x00, 0x01,                          # Cell ID = 1
    0x00, 0xF1, 0x10,                    # MCC=001 MNC=01
    0x00, 0x01,                          # LAC = 1
    0x01, 0x00,                          # cell opts + cell select
    0x18, 0xFF, 0xFF,                    # RACH ctrl
    0x2B, 0x2B, 0x2B, 0x2B, 0x2B, 0x2B, 0x2B, 0x2B
]
assert len(SI3_BYTES) == 23

# Pack 23 bytes → 12 LE words (lo + hi<<8); last word pads with 0x2B
SI3_WORDS = []
for i in range(0, 23, 2):
    lo = SI3_BYTES[i]
    hi = SI3_BYTES[i+1] if i+1 < 23 else 0x2B
    SI3_WORDS.append(lo | (hi << 8))
assert len(SI3_WORDS) == 12

# ---- Build code ----
code = []

# Boot init
code += stm_lk_mmr(0x5AC8, MMR_SP)        # STM #0x5AC8, SP
code += stm_lk_mmr(0xFFFF, MMR_IMR)       # STM #0xFFFF, IMR

# Compute main_loop address now that we know boot prefix length
main_loop_addr = 0x7120 + len(code)

# --- d_fb_det = 0x4E20 ---
code += stm_lk_mmr(D_FB_DET, MMR_AR0)
code += ld_lk_a(FB_DET_VAL)
code += stl_a_arn(0, 0)                    # STL A, *AR0 (no mod)

# --- a_sync_demod[0..3] = TOA, PM, ANGLE, SNR ---
code += stm_lk_mmr(A_SYNC_DEMOD, MMR_AR0)
code += ld_lk_a(TOA_VAL)
code += stl_a_arn(0, 2)                    # STL A, *AR0+ (post-inc)
code += ld_lk_a(PM_VAL)
code += stl_a_arn(0, 2)
code += ld_lk_a(ANGLE_VAL)
code += stl_a_arn(0, 2)
code += ld_lk_a(SNR_VAL)
code += stl_a_arn(0, 2)

# --- a_pm[3] WP0 ---
code += stm_lk_mmr(A_PM_WP0, MMR_AR0)
code += ld_lk_a(PM_VAL)
code += stl_a_arn(0, 2)  ×3

# --- a_pm[3] WP1 (reuse A) ---
code += stm_lk_mmr(A_PM_WP1, MMR_AR0)
code += stl_a_arn(0, 2)  ×3

# --- a_sch[5] WP0 : a_sch[0]=0 (CRC OK), [1]=0, [2]=0, [3]=SB_LO, [4]=SB_HI ---
code += stm_lk_mmr(A_SCH_WP0, MMR_AR0)
code += ld_lk_a(0x0000)
code += stl_a_arn(0, 2)                   # a_sch[0] = 0 (CRC OK)
code += stl_a_arn(0, 2)                   # a_sch[1] = 0
code += stl_a_arn(0, 2)                   # a_sch[2] = 0
code += ld_lk_a(SB_LO)
code += stl_a_arn(0, 2)                   # a_sch[3] = SB low
code += ld_lk_a(SB_HI)
code += stl_a_arn(0, 2)                   # a_sch[4] = SB high

# --- a_sch[5] WP1 same ---
code += stm_lk_mmr(A_SCH_WP1, MMR_AR0)
code += ld_lk_a(0x0000)
code += stl_a_arn(0, 2)  ×3
code += ld_lk_a(SB_LO)
code += stl_a_arn(0, 2)
code += ld_lk_a(SB_HI)
code += stl_a_arn(0, 2)

# --- a_cd[0..2] = FIRE-OK status (CRC pass, no biterr) ---
code += stm_lk_mmr(A_CD_BASE, MMR_AR0)
code += ld_lk_a(0x0000)
code += stl_a_arn(0, 2)                    # a_cd[0] = FIRE bits = 0 (CRC OK)
code += stl_a_arn(0, 2)                    # a_cd[1] = reserved = 0
code += stl_a_arn(0, 2)                    # a_cd[2] = num_biterr = 0

# --- a_cd[3..14] = 12 words of SI3 LAPDm packed payload ---
code += stm_lk_mmr(A_CD_PAYLOAD, MMR_AR0)
for w in SI3_WORDS:
    code += ld_lk_a(w)
    code += stl_a_arn(0, 2)

# --- Loop back ---
code += b_unc(main_loop_addr)

# ---- Sanity check ----
print(f"Code length: {len(code)} words ({len(code)*2} bytes)")
print(f"Main loop entry: 0x{main_loop_addr:04x}")
print(f"Code spans 0x7120 - 0x{0x7120 + len(code) - 1:04x}")

# ---- Patch PROM0.bin ----
with open(INPUT, 'rb') as f:
    prom_bytes = bytearray(f.read())

print(f"Input PROM0 size: {len(prom_bytes)} bytes ({len(prom_bytes)//2} words)")

# 0x7120 → byte offset 0x240 from PROM0 start (= 2 × (0x7120 - 0x7000))
PATCH_BYTE_OFFSET = 2 * (0x7120 - 0x7000)
print(f"Patch byte offset: 0x{PATCH_BYTE_OFFSET:04x} ({PATCH_BYTE_OFFSET})")

for i, w in enumerate(code):
    off = PATCH_BYTE_OFFSET + 2*i
    prom_bytes[off]   = w & 0xFF
    prom_bytes[off+1] = (w >> 8) & 0xFF

with open(OUTPUT, 'wb') as f:
    f.write(prom_bytes)

print(f"Wrote {OUTPUT}")
print()
print("Pour utiliser SANS toucher l'original :")
print(f"  Update QEMU launch dsp-prom0=...PROM0.bin → dsp-prom0={OUTPUT}")
print(f"  L'original /opt/GSM/calypso_dsp.PROM0.bin reste INTACT.")

3.58 https://github.com/bbaranoff/qosmo/blob/main/scripts/meson-buildoptions.sh

35406 octets, 589 lignes → 589 lignes

# This file is generated by meson-buildoptions.py, do not edit!
meson_options_help() {
  printf "%s\n" '  --audio-drv-list=CHOICES Set audio driver list [default] (choices: alsa/co'
  printf "%s\n" '                           reaudio/default/dsound/jack/oss/pa/pipewire/sdl/s'
  printf "%s\n" '                           ndio)'
  printf "%s\n" '  --bindir=VALUE           Executable directory [bin]'
  printf "%s\n" '  --block-drv-ro-whitelist=VALUE'
  printf "%s\n" '                           set block driver read-only whitelist (by default'
  printf "%s\n" '                           affects only QEMU, not tools like qemu-img)'
  printf "%s\n" '  --block-drv-rw-whitelist=VALUE'
  printf "%s\n" '                           set block driver read-write whitelist (by default'
  printf "%s\n" '                           affects only QEMU, not tools like qemu-img)'
  printf "%s\n" '  --datadir=VALUE          Data file directory [share]'
  printf "%s\n" '  --disable-coroutine-pool coroutine freelist (better performance)'
  printf "%s\n" '  --disable-debug-info     Enable debug symbols and other information'
  printf "%s\n" '  --disable-hexagon-idef-parser'
  printf "%s\n" '                           use idef-parser to automatically generate TCG'
  printf "%s\n" '                           code for the Hexagon frontend'
  printf "%s\n" '  --disable-install-blobs  install provided firmware blobs'
  printf "%s\n" '  --disable-qom-cast-debug cast debugging support'
  printf "%s\n" '  --disable-relocatable    toggle relocatable install'
  printf "%s\n" '  --docdir=VALUE           Base directory for documentation installation'
  printf "%s\n" '                           (can be empty) [share/doc]'
  printf "%s\n" '  --enable-asan            enable address sanitizer'
  printf "%s\n" '  --enable-block-drv-whitelist-in-tools'
  printf "%s\n" '                           use block whitelist also in tools instead of only'
  printf "%s\n" '                           QEMU'
  printf "%s\n" '  --enable-cfi             Control-Flow Integrity (CFI)'
  printf "%s\n" '  --enable-cfi-debug       Verbose errors in case of CFI violation'
  printf "%s\n" '  --enable-debug-graph-lock'
  printf "%s\n" '                           graph lock debugging support'
  printf "%s\n" '  --enable-debug-mutex     mutex debugging support'
  printf "%s\n" '  --enable-debug-remap     syscall buffer debugging support'
  printf "%s\n" '  --enable-debug-stack-usage'
  printf "%s\n" '                           measure coroutine stack usage'
  printf "%s\n" '  --enable-debug-tcg       TCG debugging'
  printf "%s\n" '  --enable-fdt[=CHOICE]    Whether and how to find the libfdt library'
  printf "%s\n" '                           (choices: auto/disabled/enabled/internal/system)'
  printf "%s\n" '  --enable-fuzzing         build fuzzing targets'
  printf "%s\n" '  --enable-gcov            Enable coverage tracking.'
  printf "%s\n" '  --enable-lto             Use link time optimization'
  printf "%s\n" '  --enable-malloc=CHOICE   choose memory allocator to use [system] (choices:'
  printf "%s\n" '                           jemalloc/system/tcmalloc)'
  printf "%s\n" '  --enable-module-upgrades try to load modules from alternate paths for'
  printf "%s\n" '                           upgrades'
  printf "%s\n" '  --enable-rng-none        dummy RNG, avoid using /dev/(u)random and'
  printf "%s\n" '                           getrandom()'
  printf "%s\n" '  --enable-safe-stack      SafeStack Stack Smash Protection (requires'
  printf "%s\n" '                           clang/llvm and coroutine backend ucontext)'
  printf "%s\n" '  --enable-strict-rust-lints'
  printf "%s\n" '                           Enable stricter set of Rust warnings'
  printf "%s\n" '  --enable-strip           Strip targets on install'
  printf "%s\n" '  --enable-tcg-interpreter TCG with bytecode interpreter (slow)'
  printf "%s\n" '  --enable-trace-backends=CHOICES'
  printf "%s\n" '                           Set available tracing backends [log] (choices:'
  printf "%s\n" '                           dtrace/ftrace/log/nop/simple/syslog/ust)'
  printf "%s\n" '  --enable-tsan            enable thread sanitizer'
  printf "%s\n" '  --enable-ubsan           enable undefined behaviour sanitizer'
  printf "%s\n" '  --firmwarepath=VALUES    search PATH for firmware files [share/qemu-'
  printf "%s\n" '                           firmware]'
  printf "%s\n" '  --iasl=VALUE             Path to ACPI disassembler'
  printf "%s\n" '  --includedir=VALUE       Header file directory [include]'
  printf "%s\n" '  --interp-prefix=VALUE    where to find shared libraries etc., use %M for'
  printf "%s\n" '                           cpu name [/usr/gnemul/qemu-%M]'
  printf "%s\n" '  --libdir=VALUE           Library directory [system default]'
  printf "%s\n" '  --libexecdir=VALUE       Library executable directory [libexec]'
  printf "%s\n" '  --localedir=VALUE        Locale data directory [share/locale]'
  printf "%s\n" '  --localstatedir=VALUE    Localstate data directory [/var/local]'
  printf "%s\n" '  --mandir=VALUE           Manual page directory [share/man]'
  printf "%s\n" '  --prefix=VALUE           Installation prefix [/usr/local]'
  printf "%s\n" '  --qemu-ga-distro=VALUE   second path element in qemu-ga registry entries'
  printf "%s\n" '                           [Linux]'
  printf "%s\n" '  --qemu-ga-manufacturer=VALUE'
  printf "%s\n" '                           "manufacturer" name for qemu-ga registry entries'
  printf "%s\n" '                           [QEMU]'
  printf "%s\n" '  --qemu-ga-version=VALUE  version number for qemu-ga installer'
  printf "%s\n" '  --rtsig-map=VALUE        default value of QEMU_RTSIG_MAP [NULL]'
  printf "%s\n" '  --smbd=VALUE             Path to smbd for slirp networking'
  printf "%s\n" '  --sysconfdir=VALUE       Sysconf data directory [etc]'
  printf "%s\n" '  --tls-priority=VALUE     Default TLS protocol/cipher priority string'
  printf "%s\n" '                           [NORMAL]'
  printf "%s\n" '  --with-coroutine=CHOICE  coroutine backend to use (choices:'
  printf "%s\n" '                           auto/sigaltstack/ucontext/windows)'
  printf "%s\n" '  --with-pkgversion=VALUE  use specified string as sub-version of the'
  printf "%s\n" '                           package'
  printf "%s\n" '  --with-suffix=VALUE      Suffix for QEMU data/modules/config directories'
  printf "%s\n" '                           (can be empty) [qemu]'
  printf "%s\n" '  --with-trace-file=VALUE  Trace file prefix for simple backend [trace]'
  printf "%s\n" '  --x86-version=CHOICE     tweak required x86_64 architecture version beyond'
  printf "%s\n" '                           compiler default [1] (choices: 0/1/2/3/4)'
  printf "%s\n" ''
  printf "%s\n" 'Optional features, enabled with --enable-FEATURE and'
  printf "%s\n" 'disabled with --disable-FEATURE, default is enabled if available'
  printf "%s\n" '(unless built with --without-default-features):'
  printf "%s\n" ''
  printf "%s\n" '  af-xdp          AF_XDP network backend support'
  printf "%s\n" '  alsa            ALSA sound support'
  printf "%s\n" '  attr            attr/xattr support'
  printf "%s\n" '  auth-pam        PAM access control'
  printf "%s\n" '  avx2            AVX2 optimizations'
  printf "%s\n" '  avx512bw        AVX512BW optimizations'
  printf "%s\n" '  blkio           libblkio block device driver'
  printf "%s\n" '  bochs           bochs image format support'
  printf "%s\n" '  bpf             eBPF support'
  printf "%s\n" '  brlapi          brlapi character device driver'
  printf "%s\n" '  bzip2           bzip2 support for DMG images'
  printf "%s\n" '  canokey         CanoKey support'
  printf "%s\n" '  cap-ng          cap_ng support'
  printf "%s\n" '  capstone        Whether and how to find the capstone library'
  printf "%s\n" '  cloop           cloop image format support'
  printf "%s\n" '  cocoa           Cocoa user interface (macOS only)'
  printf "%s\n" '  colo-proxy      colo-proxy support'
  printf "%s\n" '  coreaudio       CoreAudio sound support'
  printf "%s\n" '  crypto-afalg    Linux AF_ALG crypto backend driver'
  printf "%s\n" '  curl            CURL block device driver'
  printf "%s\n" '  curses          curses UI'
  printf "%s\n" '  dbus-display    -display dbus support'
  printf "%s\n" '  dmg             dmg image format support'
  printf "%s\n" '  docs            Documentations build support'
  printf "%s\n" '  dsound          DirectSound sound support'
  printf "%s\n" '  fuse            FUSE block device export'
  printf "%s\n" '  fuse-lseek      SEEK_HOLE/SEEK_DATA support for FUSE exports'
  printf "%s\n" '  gcrypt          libgcrypt cryptography support'
  printf "%s\n" '  gettext         Localization of the GTK+ user interface'
  printf "%s\n" '  gio             use libgio for D-Bus support'
  printf "%s\n" '  glusterfs       Glusterfs block device driver'
  printf "%s\n" '  gnutls          GNUTLS cryptography support'
  printf "%s\n" '  gtk             GTK+ user interface'
  printf "%s\n" '  gtk-clipboard   clipboard support for the gtk UI (EXPERIMENTAL, MAY HANG)'
  printf "%s\n" '  guest-agent     Build QEMU Guest Agent'
  printf "%s\n" '  guest-agent-msi Build MSI package for the QEMU Guest Agent'
  printf "%s\n" '  hv-balloon      hv-balloon driver (requires Glib 2.68+ GTree API)'
  printf "%s\n" '  hvf             HVF acceleration support'
  printf "%s\n" '  iconv           Font glyph conversion support'
  printf "%s\n" '  jack            JACK sound support'
  printf "%s\n" '  keyring         Linux keyring support'
  printf "%s\n" '  kvm             KVM acceleration support'
  printf "%s\n" '  l1-dsp          Calypso: couche 1 DSP C54x (mask-ROM TI reelle)'
  printf "%s\n" '  l1-grgsm        Calypso: couche 1 gr-gsm (demodulation cote hote)'
  printf "%s\n" '  l2tpv3          l2tpv3 network backend support'
  printf "%s\n" '  libcbor         libcbor support'
  printf "%s\n" '  libdaxctl       libdaxctl support'
  printf "%s\n" '  libdw           debuginfo support'
  printf "%s\n" '  libiscsi        libiscsi userspace initiator'
  printf "%s\n" '  libkeyutils     Linux keyutils support'
  printf "%s\n" '  libnfs          libnfs block device driver'
  printf "%s\n" '  libpmem         libpmem support'
  printf "%s\n" '  libssh          ssh block device support'
  printf "%s\n" '  libudev         Use libudev to enumerate host devices'
  printf "%s\n" '  libusb          libusb support for USB passthrough'
  printf "%s\n" '  libvduse        build VDUSE Library'
  printf "%s\n" '  linux-aio       Linux AIO support'
  printf "%s\n" '  linux-io-uring  Linux io_uring support'
  printf "%s\n" '  lzfse           lzfse support for DMG images'
  printf "%s\n" '  lzo             lzo compression support'
  printf "%s\n" '  malloc-trim     enable libc malloc_trim() for memory optimization'
  printf "%s\n" '  membarrier      membarrier system call (for Linux 4.14+ or Windows'
  printf "%s\n" '  modules         modules support (non Windows)'
  printf "%s\n" '  mpath           Multipath persistent reservation passthrough'
  printf "%s\n" '  multiprocess    Out of process device emulation support'
  printf "%s\n" '  netmap          netmap network backend support'
  printf "%s\n" '  nettle          nettle cryptography support'
  printf "%s\n" '  numa            libnuma support'
  printf "%s\n" '  nvmm            NVMM acceleration support'
  printf "%s\n" '  opengl          OpenGL support'
  printf "%s\n" '  oss             OSS sound support'
  printf "%s\n" '  pa              PulseAudio sound support'
  printf "%s\n" '  parallels       parallels image format support'
  printf "%s\n" '  pipewire        PipeWire sound support'
  printf "%s\n" '  pixman          pixman support'
  printf "%s\n" '  plugins         TCG plugins via shared library loading'
  printf "%s\n" '  png             PNG support with libpng'
  printf "%s\n" '  qatzip          QATzip compression support'
  printf "%s\n" '  qcow1           qcow1 image format support'
  printf "%s\n" '  qed             qed image format support'
  printf "%s\n" '  qga-vss         build QGA VSS support (broken with MinGW)'
  printf "%s\n" '  qpl             Query Processing Library support'
  printf "%s\n" '  rbd             Ceph block device driver'
  printf "%s\n" '  rdma            Enable RDMA-based migration'
  printf "%s\n" '  replication     replication support'
  printf "%s\n" '  rust            Rust support'
  printf "%s\n" '  rutabaga-gfx    rutabaga_gfx support'
  printf "%s\n" '  sdl             SDL user interface'
  printf "%s\n" '  sdl-image       SDL Image support for icons'
  printf "%s\n" '  seccomp         seccomp support'
  printf "%s\n" '  selinux         SELinux support in qemu-nbd'
  printf "%s\n" '  slirp           libslirp user mode network backend support'
  printf "%s\n" '  slirp-smbd      use smbd (at path --smbd=*) in slirp networking'
  printf "%s\n" '  smartcard       CA smartcard emulation support'
  printf "%s\n" '  snappy          snappy compression support'
  printf "%s\n" '  sndio           sndio sound support'
  printf "%s\n" '  sparse          sparse checker'
  printf "%s\n" '  spice           Spice server support'
  printf "%s\n" '  spice-protocol  Spice protocol support'
  printf "%s\n" '  stack-protector compiler-provided stack protection'
  printf "%s\n" '  tcg             TCG support'
  printf "%s\n" '  tools           build support utilities that come with QEMU'
  printf "%s\n" '  tpm             TPM support'
  printf "%s\n" '  u2f             U2F emulation support'
  printf "%s\n" '  uadk            UADK Library support'
  printf "%s\n" '  usb-redir       libusbredir support'
  printf "%s\n" '  vde             vde network backend support'
  printf "%s\n" '  vdi             vdi image format support'
  printf "%s\n" '  vduse-blk-export'
  printf "%s\n" '                  VDUSE block export support'
  printf "%s\n" '  vfio-user-server'
  printf "%s\n" '                  vfio-user server support'
  printf "%s\n" '  vhdx            vhdx image format support'
  printf "%s\n" '  vhost-crypto    vhost-user crypto backend support'
  printf "%s\n" '  vhost-kernel    vhost kernel backend support'
  printf "%s\n" '  vhost-net       vhost-net kernel acceleration support'
  printf "%s\n" '  vhost-user      vhost-user backend support'
  printf "%s\n" '  vhost-user-blk-server'
  printf "%s\n" '                  build vhost-user-blk server'
  printf "%s\n" '  vhost-vdpa      vhost-vdpa kernel backend support'
  printf "%s\n" '  virglrenderer   virgl rendering support'
  printf "%s\n" '  virtfs          virtio-9p support'
  printf "%s\n" '  vmdk            vmdk image format support'
  printf "%s\n" '  vmnet           vmnet.framework network backend support'
  printf "%s\n" '  vnc             VNC server'
  printf "%s\n" '  vnc-jpeg        JPEG lossy compression for VNC server'
  printf "%s\n" '  vnc-sasl        SASL authentication for VNC server'
  printf "%s\n" '  vpc             vpc image format support'
  printf "%s\n" '  vte             vte support for the gtk UI'
  printf "%s\n" '  vvfat           vvfat image format support'
  printf "%s\n" '  werror          Treat warnings as errors'
  printf "%s\n" '  whpx            WHPX acceleration support'
  printf "%s\n" '  xen             Xen backend support'
  printf "%s\n" '  xen-pci-passthrough'
  printf "%s\n" '                  Xen PCI passthrough support'
  printf "%s\n" '  xkbcommon       xkbcommon support'
  printf "%s\n" '  zstd            zstd compression support'
}
_meson_option_parse() {
  case $1 in
    --enable-af-xdp) printf "%s" -Daf_xdp=enabled ;;
    --disable-af-xdp) printf "%s" -Daf_xdp=disabled ;;
    --enable-alsa) printf "%s" -Dalsa=enabled ;;
    --disable-alsa) printf "%s" -Dalsa=disabled ;;
    --enable-asan) printf "%s" -Dasan=true ;;
    --disable-asan) printf "%s" -Dasan=false ;;
    --enable-attr) printf "%s" -Dattr=enabled ;;
    --disable-attr) printf "%s" -Dattr=disabled ;;
    --audio-drv-list=*) quote_sh "-Daudio_drv_list=$2" ;;
    --enable-auth-pam) printf "%s" -Dauth_pam=enabled ;;
    --disable-auth-pam) printf "%s" -Dauth_pam=disabled ;;
    --enable-avx2) printf "%s" -Davx2=enabled ;;
    --disable-avx2) printf "%s" -Davx2=disabled ;;
    --enable-avx512bw) printf "%s" -Davx512bw=enabled ;;
    --disable-avx512bw) printf "%s" -Davx512bw=disabled ;;
    --enable-gcov) printf "%s" -Db_coverage=true ;;
    --disable-gcov) printf "%s" -Db_coverage=false ;;
    --enable-lto) printf "%s" -Db_lto=true ;;
    --disable-lto) printf "%s" -Db_lto=false ;;
    --bindir=*) quote_sh "-Dbindir=$2" ;;
    --enable-blkio) printf "%s" -Dblkio=enabled ;;
    --disable-blkio) printf "%s" -Dblkio=disabled ;;
    --block-drv-ro-whitelist=*) quote_sh "-Dblock_drv_ro_whitelist=$2" ;;
    --block-drv-rw-whitelist=*) quote_sh "-Dblock_drv_rw_whitelist=$2" ;;
    --enable-block-drv-whitelist-in-tools) printf "%s" -Dblock_drv_whitelist_in_tools=true ;;
    --disable-block-drv-whitelist-in-tools) printf "%s" -Dblock_drv_whitelist_in_tools=false ;;
    --enable-bochs) printf "%s" -Dbochs=enabled ;;
    --disable-bochs) printf "%s" -Dbochs=disabled ;;
    --enable-bpf) printf "%s" -Dbpf=enabled ;;
    --disable-bpf) printf "%s" -Dbpf=disabled ;;
    --enable-brlapi) printf "%s" -Dbrlapi=enabled ;;
    --disable-brlapi) printf "%s" -Dbrlapi=disabled ;;
    --enable-bzip2) printf "%s" -Dbzip2=enabled ;;
    --disable-bzip2) printf "%s" -Dbzip2=disabled ;;
    --enable-canokey) printf "%s" -Dcanokey=enabled ;;
    --disable-canokey) printf "%s" -Dcanokey=disabled ;;
    --enable-cap-ng) printf "%s" -Dcap_ng=enabled ;;
    --disable-cap-ng) printf "%s" -Dcap_ng=disabled ;;
    --enable-capstone) printf "%s" -Dcapstone=enabled ;;
    --disable-capstone) printf "%s" -Dcapstone=disabled ;;
    --enable-cfi) printf "%s" -Dcfi=true ;;
    --disable-cfi) printf "%s" -Dcfi=false ;;
    --enable-cfi-debug) printf "%s" -Dcfi_debug=true ;;
    --disable-cfi-debug) printf "%s" -Dcfi_debug=false ;;
    --enable-cloop) printf "%s" -Dcloop=enabled ;;
    --disable-cloop) printf "%s" -Dcloop=disabled ;;
    --enable-cocoa) printf "%s" -Dcocoa=enabled ;;
    --disable-cocoa) printf "%s" -Dcocoa=disabled ;;
    --enable-colo-proxy) printf "%s" -Dcolo_proxy=enabled ;;
    --disable-colo-proxy) printf "%s" -Dcolo_proxy=disabled ;;
    --enable-coreaudio) printf "%s" -Dcoreaudio=enabled ;;
    --disable-coreaudio) printf "%s" -Dcoreaudio=disabled ;;
    --with-coroutine=*) quote_sh "-Dcoroutine_backend=$2" ;;
    --enable-coroutine-pool) printf "%s" -Dcoroutine_pool=true ;;
    --disable-coroutine-pool) printf "%s" -Dcoroutine_pool=false ;;
    --enable-crypto-afalg) printf "%s" -Dcrypto_afalg=enabled ;;
    --disable-crypto-afalg) printf "%s" -Dcrypto_afalg=disabled ;;
    --enable-curl) printf "%s" -Dcurl=enabled ;;
    --disable-curl) printf "%s" -Dcurl=disabled ;;
    --enable-curses) printf "%s" -Dcurses=enabled ;;
    --disable-curses) printf "%s" -Dcurses=disabled ;;
    --datadir=*) quote_sh "-Ddatadir=$2" ;;
    --enable-dbus-display) printf "%s" -Ddbus_display=enabled ;;
    --disable-dbus-display) printf "%s" -Ddbus_display=disabled ;;
    --enable-debug-info) printf "%s" -Ddebug=true ;;
    --disable-debug-info) printf "%s" -Ddebug=false ;;
    --enable-debug-graph-lock) printf "%s" -Ddebug_graph_lock=true ;;
    --disable-debug-graph-lock) printf "%s" -Ddebug_graph_lock=false ;;
    --enable-debug-mutex) printf "%s" -Ddebug_mutex=true ;;
    --disable-debug-mutex) printf "%s" -Ddebug_mutex=false ;;
    --enable-debug-remap) printf "%s" -Ddebug_remap=true ;;
    --disable-debug-remap) printf "%s" -Ddebug_remap=false ;;
    --enable-debug-stack-usage) printf "%s" -Ddebug_stack_usage=true ;;
    --disable-debug-stack-usage) printf "%s" -Ddebug_stack_usage=false ;;
    --enable-debug-tcg) printf "%s" -Ddebug_tcg=true ;;
    --disable-debug-tcg) printf "%s" -Ddebug_tcg=false ;;
    --enable-dmg) printf "%s" -Ddmg=enabled ;;
    --disable-dmg) printf "%s" -Ddmg=disabled ;;
    --docdir=*) quote_sh "-Ddocdir=$2" ;;
    --enable-docs) printf "%s" -Ddocs=enabled ;;
    --disable-docs) printf "%s" -Ddocs=disabled ;;
    --enable-dsound) printf "%s" -Ddsound=enabled ;;
    --disable-dsound) printf "%s" -Ddsound=disabled ;;
    --enable-fdt) printf "%s" -Dfdt=enabled ;;
    --disable-fdt) printf "%s" -Dfdt=disabled ;;
    --enable-fdt=*) quote_sh "-Dfdt=$2" ;;
    --enable-fuse) printf "%s" -Dfuse=enabled ;;
    --disable-fuse) printf "%s" -Dfuse=disabled ;;
    --enable-fuse-lseek) printf "%s" -Dfuse_lseek=enabled ;;
    --disable-fuse-lseek) printf "%s" -Dfuse_lseek=disabled ;;
    --enable-fuzzing) printf "%s" -Dfuzzing=true ;;
    --disable-fuzzing) printf "%s" -Dfuzzing=false ;;
    --enable-gcrypt) printf "%s" -Dgcrypt=enabled ;;
    --disable-gcrypt) printf "%s" -Dgcrypt=disabled ;;
    --enable-gettext) printf "%s" -Dgettext=enabled ;;
    --disable-gettext) printf "%s" -Dgettext=disabled ;;
    --enable-gio) printf "%s" -Dgio=enabled ;;
    --disable-gio) printf "%s" -Dgio=disabled ;;
    --enable-glusterfs) printf "%s" -Dglusterfs=enabled ;;
    --disable-glusterfs) printf "%s" -Dglusterfs=disabled ;;
    --enable-gnutls) printf "%s" -Dgnutls=enabled ;;
    --disable-gnutls) printf "%s" -Dgnutls=disabled ;;
    --enable-gtk) printf "%s" -Dgtk=enabled ;;
    --disable-gtk) printf "%s" -Dgtk=disabled ;;
    --enable-gtk-clipboard) printf "%s" -Dgtk_clipboard=enabled ;;
    --disable-gtk-clipboard) printf "%s" -Dgtk_clipboard=disabled ;;
    --enable-guest-agent) printf "%s" -Dguest_agent=enabled ;;
    --disable-guest-agent) printf "%s" -Dguest_agent=disabled ;;
    --enable-guest-agent-msi) printf "%s" -Dguest_agent_msi=enabled ;;
    --disable-guest-agent-msi) printf "%s" -Dguest_agent_msi=disabled ;;
    --enable-hexagon-idef-parser) printf "%s" -Dhexagon_idef_parser=true ;;
    --disable-hexagon-idef-parser) printf "%s" -Dhexagon_idef_parser=false ;;
    --enable-hv-balloon) printf "%s" -Dhv_balloon=enabled ;;
    --disable-hv-balloon) printf "%s" -Dhv_balloon=disabled ;;
    --enable-hvf) printf "%s" -Dhvf=enabled ;;
    --disable-hvf) printf "%s" -Dhvf=disabled ;;
    --iasl=*) quote_sh "-Diasl=$2" ;;
    --enable-iconv) printf "%s" -Diconv=enabled ;;
    --disable-iconv) printf "%s" -Diconv=disabled ;;
    --includedir=*) quote_sh "-Dincludedir=$2" ;;
    --enable-install-blobs) printf "%s" -Dinstall_blobs=true ;;
    --disable-install-blobs) printf "%s" -Dinstall_blobs=false ;;
    --interp-prefix=*) quote_sh "-Dinterp_prefix=$2" ;;
    --enable-jack) printf "%s" -Djack=enabled ;;
    --disable-jack) printf "%s" -Djack=disabled ;;
    --enable-keyring) printf "%s" -Dkeyring=enabled ;;
    --disable-keyring) printf "%s" -Dkeyring=disabled ;;
    --enable-kvm) printf "%s" -Dkvm=enabled ;;
    --disable-kvm) printf "%s" -Dkvm=disabled ;;
    --enable-l1-dsp) printf "%s" -Dl1_dsp=enabled ;;
    --disable-l1-dsp) printf "%s" -Dl1_dsp=disabled ;;
    --enable-l1-grgsm) printf "%s" -Dl1_grgsm=enabled ;;
    --disable-l1-grgsm) printf "%s" -Dl1_grgsm=disabled ;;
    --enable-l2tpv3) printf "%s" -Dl2tpv3=enabled ;;
    --disable-l2tpv3) printf "%s" -Dl2tpv3=disabled ;;
    --enable-libcbor) printf "%s" -Dlibcbor=enabled ;;
    --disable-libcbor) printf "%s" -Dlibcbor=disabled ;;
    --enable-libdaxctl) printf "%s" -Dlibdaxctl=enabled ;;
    --disable-libdaxctl) printf "%s" -Dlibdaxctl=disabled ;;
    --libdir=*) quote_sh "-Dlibdir=$2" ;;
    --enable-libdw) printf "%s" -Dlibdw=enabled ;;
    --disable-libdw) printf "%s" -Dlibdw=disabled ;;
    --libexecdir=*) quote_sh "-Dlibexecdir=$2" ;;
    --enable-libiscsi) printf "%s" -Dlibiscsi=enabled ;;
    --disable-libiscsi) printf "%s" -Dlibiscsi=disabled ;;
    --enable-libkeyutils) printf "%s" -Dlibkeyutils=enabled ;;
    --disable-libkeyutils) printf "%s" -Dlibkeyutils=disabled ;;
    --enable-libnfs) printf "%s" -Dlibnfs=enabled ;;
    --disable-libnfs) printf "%s" -Dlibnfs=disabled ;;
    --enable-libpmem) printf "%s" -Dlibpmem=enabled ;;
    --disable-libpmem) printf "%s" -Dlibpmem=disabled ;;
    --enable-libssh) printf "%s" -Dlibssh=enabled ;;
    --disable-libssh) printf "%s" -Dlibssh=disabled ;;
    --enable-libudev) printf "%s" -Dlibudev=enabled ;;
    --disable-libudev) printf "%s" -Dlibudev=disabled ;;
    --enable-libusb) printf "%s" -Dlibusb=enabled ;;
    --disable-libusb) printf "%s" -Dlibusb=disabled ;;
    --enable-libvduse) printf "%s" -Dlibvduse=enabled ;;
    --disable-libvduse) printf "%s" -Dlibvduse=disabled ;;
    --enable-linux-aio) printf "%s" -Dlinux_aio=enabled ;;
    --disable-linux-aio) printf "%s" -Dlinux_aio=disabled ;;
    --enable-linux-io-uring) printf "%s" -Dlinux_io_uring=enabled ;;
    --disable-linux-io-uring) printf "%s" -Dlinux_io_uring=disabled ;;
    --localedir=*) quote_sh "-Dlocaledir=$2" ;;
    --localstatedir=*) quote_sh "-Dlocalstatedir=$2" ;;
    --enable-lzfse) printf "%s" -Dlzfse=enabled ;;
    --disable-lzfse) printf "%s" -Dlzfse=disabled ;;
    --enable-lzo) printf "%s" -Dlzo=enabled ;;
    --disable-lzo) printf "%s" -Dlzo=disabled ;;
    --enable-malloc=*) quote_sh "-Dmalloc=$2" ;;
    --enable-malloc-trim) printf "%s" -Dmalloc_trim=enabled ;;
    --disable-malloc-trim) printf "%s" -Dmalloc_trim=disabled ;;
    --mandir=*) quote_sh "-Dmandir=$2" ;;
    --enable-membarrier) printf "%s" -Dmembarrier=enabled ;;
    --disable-membarrier) printf "%s" -Dmembarrier=disabled ;;
    --enable-module-upgrades) printf "%s" -Dmodule_upgrades=true ;;
    --disable-module-upgrades) printf "%s" -Dmodule_upgrades=false ;;
    --enable-modules) printf "%s" -Dmodules=enabled ;;
    --disable-modules) printf "%s" -Dmodules=disabled ;;
    --enable-mpath) printf "%s" -Dmpath=enabled ;;
    --disable-mpath) printf "%s" -Dmpath=disabled ;;
    --enable-multiprocess) printf "%s" -Dmultiprocess=enabled ;;
    --disable-multiprocess) printf "%s" -Dmultiprocess=disabled ;;
    --enable-netmap) printf "%s" -Dnetmap=enabled ;;
    --disable-netmap) printf "%s" -Dnetmap=disabled ;;
    --enable-nettle) printf "%s" -Dnettle=enabled ;;
    --disable-nettle) printf "%s" -Dnettle=disabled ;;
    --enable-numa) printf "%s" -Dnuma=enabled ;;
    --disable-numa) printf "%s" -Dnuma=disabled ;;
    --enable-nvmm) printf "%s" -Dnvmm=enabled ;;
    --disable-nvmm) printf "%s" -Dnvmm=disabled ;;
    --enable-opengl) printf "%s" -Dopengl=enabled ;;
    --disable-opengl) printf "%s" -Dopengl=disabled ;;
    --enable-oss) printf "%s" -Doss=enabled ;;
    --disable-oss) printf "%s" -Doss=disabled ;;
    --enable-pa) printf "%s" -Dpa=enabled ;;
    --disable-pa) printf "%s" -Dpa=disabled ;;
    --enable-parallels) printf "%s" -Dparallels=enabled ;;
    --disable-parallels) printf "%s" -Dparallels=disabled ;;
    --enable-pipewire) printf "%s" -Dpipewire=enabled ;;
    --disable-pipewire) printf "%s" -Dpipewire=disabled ;;
    --enable-pixman) printf "%s" -Dpixman=enabled ;;
    --disable-pixman) printf "%s" -Dpixman=disabled ;;
    --with-pkgversion=*) quote_sh "-Dpkgversion=$2" ;;
    --enable-plugins) printf "%s" -Dplugins=true ;;
    --disable-plugins) printf "%s" -Dplugins=false ;;
    --enable-png) printf "%s" -Dpng=enabled ;;
    --disable-png) printf "%s" -Dpng=disabled ;;
    --prefix=*) quote_sh "-Dprefix=$2" ;;
    --enable-qatzip) printf "%s" -Dqatzip=enabled ;;
    --disable-qatzip) printf "%s" -Dqatzip=disabled ;;
    --enable-qcow1) printf "%s" -Dqcow1=enabled ;;
    --disable-qcow1) printf "%s" -Dqcow1=disabled ;;
    --enable-qed) printf "%s" -Dqed=enabled ;;
    --disable-qed) printf "%s" -Dqed=disabled ;;
    --firmwarepath=*) quote_sh "-Dqemu_firmwarepath=$(meson_option_build_array $2)" ;;
    --qemu-ga-distro=*) quote_sh "-Dqemu_ga_distro=$2" ;;
    --qemu-ga-manufacturer=*) quote_sh "-Dqemu_ga_manufacturer=$2" ;;
    --qemu-ga-version=*) quote_sh "-Dqemu_ga_version=$2" ;;
    --with-suffix=*) quote_sh "-Dqemu_suffix=$2" ;;
    --enable-qga-vss) printf "%s" -Dqga_vss=enabled ;;
    --disable-qga-vss) printf "%s" -Dqga_vss=disabled ;;
    --enable-qom-cast-debug) printf "%s" -Dqom_cast_debug=true ;;
    --disable-qom-cast-debug) printf "%s" -Dqom_cast_debug=false ;;
    --enable-qpl) printf "%s" -Dqpl=enabled ;;
    --disable-qpl) printf "%s" -Dqpl=disabled ;;
    --enable-rbd) printf "%s" -Drbd=enabled ;;
    --disable-rbd) printf "%s" -Drbd=disabled ;;
    --enable-rdma) printf "%s" -Drdma=enabled ;;
    --disable-rdma) printf "%s" -Drdma=disabled ;;
    --enable-relocatable) printf "%s" -Drelocatable=true ;;
    --disable-relocatable) printf "%s" -Drelocatable=false ;;
    --enable-replication) printf "%s" -Dreplication=enabled ;;
    --disable-replication) printf "%s" -Dreplication=disabled ;;
    --enable-rng-none) printf "%s" -Drng_none=true ;;
    --disable-rng-none) printf "%s" -Drng_none=false ;;
    --rtsig-map=*) quote_sh "-Drtsig_map=$2" ;;
    --enable-rust) printf "%s" -Drust=enabled ;;
    --disable-rust) printf "%s" -Drust=disabled ;;
    --enable-rutabaga-gfx) printf "%s" -Drutabaga_gfx=enabled ;;
    --disable-rutabaga-gfx) printf "%s" -Drutabaga_gfx=disabled ;;
    --enable-safe-stack) printf "%s" -Dsafe_stack=true ;;
    --disable-safe-stack) printf "%s" -Dsafe_stack=false ;;
    --enable-sdl) printf "%s" -Dsdl=enabled ;;
    --disable-sdl) printf "%s" -Dsdl=disabled ;;
    --enable-sdl-image) printf "%s" -Dsdl_image=enabled ;;
    --disable-sdl-image) printf "%s" -Dsdl_image=disabled ;;
    --enable-seccomp) printf "%s" -Dseccomp=enabled ;;
    --disable-seccomp) printf "%s" -Dseccomp=disabled ;;
    --enable-selinux) printf "%s" -Dselinux=enabled ;;
    --disable-selinux) printf "%s" -Dselinux=disabled ;;
    --enable-slirp) printf "%s" -Dslirp=enabled ;;
    --disable-slirp) printf "%s" -Dslirp=disabled ;;
    --enable-slirp-smbd) printf "%s" -Dslirp_smbd=enabled ;;
    --disable-slirp-smbd) printf "%s" -Dslirp_smbd=disabled ;;
    --enable-smartcard) printf "%s" -Dsmartcard=enabled ;;
    --disable-smartcard) printf "%s" -Dsmartcard=disabled ;;
    --smbd=*) quote_sh "-Dsmbd=$2" ;;
    --enable-snappy) printf "%s" -Dsnappy=enabled ;;
    --disable-snappy) printf "%s" -Dsnappy=disabled ;;
    --enable-sndio) printf "%s" -Dsndio=enabled ;;
    --disable-sndio) printf "%s" -Dsndio=disabled ;;
    --enable-sparse) printf "%s" -Dsparse=enabled ;;
    --disable-sparse) printf "%s" -Dsparse=disabled ;;
    --enable-spice) printf "%s" -Dspice=enabled ;;
    --disable-spice) printf "%s" -Dspice=disabled ;;
    --enable-spice-protocol) printf "%s" -Dspice_protocol=enabled ;;
    --disable-spice-protocol) printf "%s" -Dspice_protocol=disabled ;;
    --enable-stack-protector) printf "%s" -Dstack_protector=enabled ;;
    --disable-stack-protector) printf "%s" -Dstack_protector=disabled ;;
    --enable-strict-rust-lints) printf "%s" -Dstrict_rust_lints=true ;;
    --disable-strict-rust-lints) printf "%s" -Dstrict_rust_lints=false ;;
    --enable-strip) printf "%s" -Dstrip=true ;;
    --disable-strip) printf "%s" -Dstrip=false ;;
    --sysconfdir=*) quote_sh "-Dsysconfdir=$2" ;;
    --enable-tcg) printf "%s" -Dtcg=enabled ;;
    --disable-tcg) printf "%s" -Dtcg=disabled ;;
    --enable-tcg-interpreter) printf "%s" -Dtcg_interpreter=true ;;
    --disable-tcg-interpreter) printf "%s" -Dtcg_interpreter=false ;;
    --tls-priority=*) quote_sh "-Dtls_priority=$2" ;;
    --enable-tools) printf "%s" -Dtools=enabled ;;
    --disable-tools) printf "%s" -Dtools=disabled ;;
    --enable-tpm) printf "%s" -Dtpm=enabled ;;
    --disable-tpm) printf "%s" -Dtpm=disabled ;;
    --enable-trace-backends=*) quote_sh "-Dtrace_backends=$2" ;;
    --with-trace-file=*) quote_sh "-Dtrace_file=$2" ;;
    --enable-tsan) printf "%s" -Dtsan=true ;;
    --disable-tsan) printf "%s" -Dtsan=false ;;
    --enable-u2f) printf "%s" -Du2f=enabled ;;
    --disable-u2f) printf "%s" -Du2f=disabled ;;
    --enable-uadk) printf "%s" -Duadk=enabled ;;
    --disable-uadk) printf "%s" -Duadk=disabled ;;
    --enable-ubsan) printf "%s" -Dubsan=true ;;
    --disable-ubsan) printf "%s" -Dubsan=false ;;
    --enable-usb-redir) printf "%s" -Dusb_redir=enabled ;;
    --disable-usb-redir) printf "%s" -Dusb_redir=disabled ;;
    --enable-vde) printf "%s" -Dvde=enabled ;;
    --disable-vde) printf "%s" -Dvde=disabled ;;
    --enable-vdi) printf "%s" -Dvdi=enabled ;;
    --disable-vdi) printf "%s" -Dvdi=disabled ;;
    --enable-vduse-blk-export) printf "%s" -Dvduse_blk_export=enabled ;;
    --disable-vduse-blk-export) printf "%s" -Dvduse_blk_export=disabled ;;
    --enable-vfio-user-server) printf "%s" -Dvfio_user_server=enabled ;;
    --disable-vfio-user-server) printf "%s" -Dvfio_user_server=disabled ;;
    --enable-vhdx) printf "%s" -Dvhdx=enabled ;;
    --disable-vhdx) printf "%s" -Dvhdx=disabled ;;
    --enable-vhost-crypto) printf "%s" -Dvhost_crypto=enabled ;;
    --disable-vhost-crypto) printf "%s" -Dvhost_crypto=disabled ;;
    --enable-vhost-kernel) printf "%s" -Dvhost_kernel=enabled ;;
    --disable-vhost-kernel) printf "%s" -Dvhost_kernel=disabled ;;
    --enable-vhost-net) printf "%s" -Dvhost_net=enabled ;;
    --disable-vhost-net) printf "%s" -Dvhost_net=disabled ;;
    --enable-vhost-user) printf "%s" -Dvhost_user=enabled ;;
    --disable-vhost-user) printf "%s" -Dvhost_user=disabled ;;
    --enable-vhost-user-blk-server) printf "%s" -Dvhost_user_blk_server=enabled ;;
    --disable-vhost-user-blk-server) printf "%s" -Dvhost_user_blk_server=disabled ;;
    --enable-vhost-vdpa) printf "%s" -Dvhost_vdpa=enabled ;;
    --disable-vhost-vdpa) printf "%s" -Dvhost_vdpa=disabled ;;
    --enable-virglrenderer) printf "%s" -Dvirglrenderer=enabled ;;
    --disable-virglrenderer) printf "%s" -Dvirglrenderer=disabled ;;
    --enable-virtfs) printf "%s" -Dvirtfs=enabled ;;
    --disable-virtfs) printf "%s" -Dvirtfs=disabled ;;
    --enable-vmdk) printf "%s" -Dvmdk=enabled ;;
    --disable-vmdk) printf "%s" -Dvmdk=disabled ;;
    --enable-vmnet) printf "%s" -Dvmnet=enabled ;;
    --disable-vmnet) printf "%s" -Dvmnet=disabled ;;
    --enable-vnc) printf "%s" -Dvnc=enabled ;;
    --disable-vnc) printf "%s" -Dvnc=disabled ;;
    --enable-vnc-jpeg) printf "%s" -Dvnc_jpeg=enabled ;;
    --disable-vnc-jpeg) printf "%s" -Dvnc_jpeg=disabled ;;
    --enable-vnc-sasl) printf "%s" -Dvnc_sasl=enabled ;;
    --disable-vnc-sasl) printf "%s" -Dvnc_sasl=disabled ;;
    --enable-vpc) printf "%s" -Dvpc=enabled ;;
    --disable-vpc) printf "%s" -Dvpc=disabled ;;
    --enable-vte) printf "%s" -Dvte=enabled ;;
    --disable-vte) printf "%s" -Dvte=disabled ;;
    --enable-vvfat) printf "%s" -Dvvfat=enabled ;;
    --disable-vvfat) printf "%s" -Dvvfat=disabled ;;
    --enable-werror) printf "%s" -Dwerror=true ;;
    --disable-werror) printf "%s" -Dwerror=false ;;
    --enable-whpx) printf "%s" -Dwhpx=enabled ;;
    --disable-whpx) printf "%s" -Dwhpx=disabled ;;
    --x86-version=*) quote_sh "-Dx86_version=$2" ;;
    --enable-xen) printf "%s" -Dxen=enabled ;;
    --disable-xen) printf "%s" -Dxen=disabled ;;
    --enable-xen-pci-passthrough) printf "%s" -Dxen_pci_passthrough=enabled ;;
    --disable-xen-pci-passthrough) printf "%s" -Dxen_pci_passthrough=disabled ;;
    --enable-xkbcommon) printf "%s" -Dxkbcommon=enabled ;;
    --disable-xkbcommon) printf "%s" -Dxkbcommon=disabled ;;
    --enable-zstd) printf "%s" -Dzstd=enabled ;;
    --disable-zstd) printf "%s" -Dzstd=disabled ;;
    *) return 1 ;;
  esac
}

3.59 https://github.com/bbaranoff/qosmo/blob/main/scripts/populate-si.sh

3516 octets, 95 lignes → 93 lignes (1 groupes compactés)

#!/bin/bash
# populate-si.sh — static SI stub.
#
# Writes 5 fixed SI blobs (extracted from an osmo-bsc -> osmo-bts RSL trace)
# into /dev/shm/calypso_si.bin per doc/MMAP_SI_FORMAT.md v1.
#
# Lets the mmap interface be validated without depending on the RSL parser
# (scripts/rsl_si_tap.py).
#
# Usage:
#   ./populate-si.sh                              # default /dev/shm/calypso_si.bin
#   CALYPSO_SI_MMAP_PATH=/tmp/foo ./populate-si.sh
#
# Check:
#   xxd /dev/shm/calypso_si.bin | head           # check magic "CSI1" + slots

set -e

OUTPUT="${CALYPSO_SI_MMAP_PATH:-/dev/shm/calypso_si.bin}"

python3 - "$OUTPUT" << 'PYEOF'
import sys, struct, os

OUTPUT = sys.argv[1]

# 23-byte BCCH SI blobs, byte-exact from RSL re-attach trace
# (osmo-bsc emits BCCH INFORMATION when osmo-bts attaches via Abis OML).
# LAI 001/01/0001, CI=6001, BSIC=7, ARFCN=514.
SI1  = bytes([0x55, 0x06, 0x19, 0x8f, 0x01,
              0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
              0xe5, 0x04, 0x00, 0x2b])
SI2  = bytes([0x59, 0x06, 0x1a,
              0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
              0xff, 0xe5, 0x04, 0x00])
SI3  = bytes([0x49, 0x06, 0x1b, 0x17, 0x71, 0x00, 0xf1, 0x10, 0x00, 0x01, 0xc9, 0x03,
              0x05, 0x27, 0x47, 0x40, 0xe5, 0x04, 0x00, 0x2c, 0x0b, 0x2b, 0x2b])
SI4  = bytes([0x31, 0x06, 0x1c, 0x00, 0xf1, 0x10, 0x00, 0x01, 0x47, 0x40, 0xe5, 0x04,
              0x00, 0x01, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b])
SI13 = bytes([0x01, 0x06, 0x00, 0x90, 0x00, 0x18, 0x5a, 0x6f, 0xc9, 0xf2, 0xb5, 0x30,
              0x42, 0x08, 0xeb, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b])

assert len(SI⟨1⟩)  == 23, f"SI⟨2⟩  len={len(SI⟨3⟩)}"  ×4
    ⟨⟩ = (1,1,1) (2,2,2) (3,3,3) (4,4,4)
assert len(SI13) == 23, f"SI13 len={len(SI13)}"

# Layout per MMAP_SI_FORMAT.md v1 :
#   header  (16 bytes) : magic + version + slot_count + last_update_fn + reserved
#   slots   (5 × 32B)  : si_type + flags + blob_len + pad + 23B blob + 5B pad
SLOT_VALID = 0x01

def make_slot(si_type, blob):
    s = bytearray(32)
    s[0] = si_type
    s[1] = SLOT_VALID
    s[2] = len(blob)
    s[3] = 0  # padding
    s[4:4+23] = blob
    # 27..31 padding zero (already)
    return bytes(s)

buf = bytearray(176)
# Header
buf[0:4]  = b"CSI1"
buf[4]    = 0x01      # version
buf[5]    = 0x05      # slot_count
buf[6:8]  = struct.pack("<H", 0)  # last_update_fn = 0 (stub static, no live FN)
# bytes 8..15 reserved zero (already)

# Slots in fixed order : SI1, SI2, SI3, SI4, SI13
buf[16:48]    = make_slot(0x01, SI1)
buf[48:80]    = make_slot(0x02, SI2)
buf[80:112]   = make_slot(0x03, SI3)
buf[112:144]  = make_slot(0x04, SI4)
buf[144:176]  = make_slot(0x0d, SI13)

with open(OUTPUT, "wb") as f:
    f.write(buf)

# Verify
with open(OUTPUT, "rb") as f:
    chk = f.read()
assert chk == bytes(buf), "readback mismatch"
print(f"OK : wrote {len(buf)} bytes to {OUTPUT}")
print(f"     magic={chk[0:4]!r} version={chk[4]} slot_count={chk[5]}")
for slot_idx, name in enumerate(["SI1", "SI2", "SI3", "SI4", "SI13"]):
    off = 16 + slot_idx * 32
    si_type = chk[off]
    flags   = chk[off+1]
    blob_len= chk[off+2]
    blob    = chk[off+4:off+4+23]
    print(f"     slot[{slot_idx}] {name}: type=0x{si_type:02x} flags=0x{flags:02x} "
          f"len={blob_len} blob[0:4]={blob[:4].hex()} blob[-4:]={blob[-4:].hex()}")
PYEOF

3.60 https://github.com/bbaranoff/qosmo/blob/main/scripts/replay_pm_payload.py

6731 octets, 211 lignes → 211 lignes

#!/usr/bin/env python3
"""
replay_pm_payload.py - Reproduit le payload L1CTL PM scan observe dans
les logs (hdlc_send/recv sur DLCI 5).

Encode des L1CTL_PM_REQ + decode/print des L1CTL_PM_CONF.

Trois modes :
  1. --dump      : imprime les bytes attendus (verification format)
  2. --to-socket /tmp/osmocom_l2  : push vers un socket L1CTL existant
  3. --hdlc-pty  /dev/pts/X  : enveloppe en HDLC DLCI 5 et envoie sur PTY

Format L1CTL observe (32-bit big-endian) :
  hdlc payload = CMD(4B) ARG(4B) [PAYLOAD...]

Commandes vues dans le log :
  0x08 PM_REQ  : ARG=band, payload = 2x uint16 BE (arfcn_lo, arfcn_hi)
  0x09 PM_CONF : ARG=batch_id, payload = N x uint32 BE arfcn list
  0x0d         : sub-cmd ack/next (suit chaque PM_CONF)

Usage :
  ./replay_pm_payload.py --dump
  ./replay_pm_payload.py --to-socket /tmp/osmocom_l2
  ./replay_pm_payload.py --hdlc-pty /dev/pts/1
"""

import argparse
import socket
import struct
import sys


# L1CTL command IDs (deduits du log + osmocom-bb l1ctl_proto.h)
CMD_PM_REQ   = 0x08
CMD_PM_CONF  = 0x09
CMD_DONE     = 0x0d

# GSM band IDs
BAND_GSM900  = 0x00
BAND_DCS1800 = 0x01


def encode_pm_req(band: int, arfcn_lo: int, arfcn_hi: int) -> bytes:
    """
    Encode L1CTL_PM_REQ comme observe dans le log :
      08 00 00 00 BB 00 00 00 LL LL HH HH

    band = BB (0x00 GSM900, 0x01 DCS1800)
    arfcn_lo = LL LL (16-bit BE)
    arfcn_hi = HH HH (16-bit BE)
    """
    return struct.pack(
        ">II HH",
        CMD_PM_REQ,         # 32-bit BE cmd
        band,               # 32-bit BE band
        arfcn_lo,           # 16-bit BE
        arfcn_hi,           # 16-bit BE
    )


def encode_done(sub: int = 1) -> bytes:
    """Encode L1CTL_DONE (0x0d) : 0d 00 00 00 SS 00 00 00"""
    return struct.pack(">II", CMD_DONE, sub)


def decode_pm_conf(b: bytes) -> tuple[int, int, list[int]]:
    """
    Decode L1CTL_PM_CONF :
      09 00 00 00 SS 00 00 00 [arfcn 32-bit BE]*

    Returns (cmd, sub, arfcn_list)
    """
    if len(b) < 8:
        raise ValueError("PM_CONF too short")
    cmd, sub = struct.unpack(">II", b[:8])
    if cmd != CMD_PM_CONF:
        raise ValueError(f"not a PM_CONF (cmd=0x{cmd:02x})")
    n = (len(b) - 8) // 4
    arfcns = list(struct.unpack(f">{n}I", b[8 : 8 + 4 * n]))
    return cmd, sub, arfcns


# ---------- HDLC framing (DLCI 5) ----------
#
# osmocon hdlc protocol : frame_start(0x7E) [escaped payload] frame_end(0x7E)
# Escape : 0x7D 0x5D = literal 0x7D ; 0x7D 0x5E = literal 0x7E
#
# DLCI byte prepended to payload, then HDLC framing.
HDLC_FLAG    = 0x7E
HDLC_ESC     = 0x7D
HDLC_ESC_XOR = 0x20
DLCI_SERCOMM = 5


def hdlc_escape(b: bytes) -> bytes:
    out = bytearray()
    for x in b:
        if x in (HDLC_FLAG, HDLC_ESC):
            out.append(HDLC_ESC)
            out.append(x ^ HDLC_ESC_XOR)
        else:
            out.append(x)
    return bytes(out)


def hdlc_frame(dlci: int, payload: bytes) -> bytes:
    """Wrap payload in HDLC framing for given DLCI."""
    body = bytes([dlci]) + payload
    return bytes([HDLC_FLAG]) + hdlc_escape(body) + bytes([HDLC_FLAG])


# ---------- Scenarios ----------

def build_observed_sequence() -> list[tuple[str, bytes]]:
    """Reproduit la sequence exacte du log."""
    seq = []
    # DCS 1800 scan : ARFCN 940..954
    seq.append(("PM_REQ DCS 940-954", encode_pm_req(BAND_DCS1800, 940, 954)))
    # CONF expected : list of arfcn 940..954
    arfcn_list = list(range(940, 955))
    conf = struct.pack(">II", CMD_PM_CONF, 1) + b"".join(
        struct.pack(">I", a) for a in arfcn_list
    )
    seq.append(("PM_CONF DCS 940-954 (expected)", conf))
    seq.append(("DONE sub=1", encode_done(1)))

    # GSM 900 scan : ARFCN 1..124
    seq.append(("PM_REQ GSM900 1-124", encode_pm_req(BAND_GSM900, 1, 124)))
    arfcn_list2 = list(range(1, 125))
    conf2 = struct.pack(">II", CMD_PM_CONF, 0) + b"".join(
        struct.pack(">I", a) for a in arfcn_list2
    )
    seq.append(("PM_CONF GSM900 1-124 (expected)", conf2))
    seq.append(("DONE sub=1", encode_done(1)))
    return seq


# ---------- Main ----------

def hex_pretty(b: bytes, line_len: int = 32) -> str:
    out = []
    for i in range(0, len(b), line_len):
        chunk = b[i : i + line_len]
        hexpart = " ".join(f"{x:02x}" for x in chunk)
        asciipart = "".join(chr(x) if 32 <= x < 127 else "." for x in chunk)
        out.append(f"  {hexpart:<{line_len*3}}  {asciipart}")
    return "\n".join(out)


def main():
    ap = argparse.ArgumentParser(description="Replay L1CTL PM scan payload")
    ap.add_argument("--dump", action="store_true",
                    help="print expected bytes (no I/O)")
    ap.add_argument("--to-socket", metavar="PATH",
                    help="send PM_REQs as L1CTL frames to unix socket")
    ap.add_argument("--hdlc-pty", metavar="PATH",
                    help="wrap in HDLC DLCI 5 and write to PTY")
    ap.add_argument("--band", choices=["gsm900", "dcs1800"], default="gsm900")
    ap.add_argument("--arfcn-lo", type=int, default=1)
    ap.add_argument("--arfcn-hi", type=int, default=124)
    args = ap.parse_args()

    band = BAND_DCS1800 if args.band == "dcs1800" else BAND_GSM900
    custom_req = encode_pm_req(band, args.arfcn_lo, args.arfcn_hi)

    if args.dump:
        print("=== Observed sequence reproduction ===\n")
        for label, payload in build_observed_sequence():
            print(f"-- {label} ({len(payload)} bytes) --")
            print(hex_pretty(payload))
            print()
        print("=== Custom PM_REQ ===")
        print(f"  band={args.band} arfcn={args.arfcn_lo}-{args.arfcn_hi}")
        print(hex_pretty(custom_req))
        print()
        print("=== HDLC framed (DLCI 5) ===")
        framed = hdlc_frame(DLCI_SERCOMM, custom_req)
        print(hex_pretty(framed))
        return

    if args.to_socket:
        s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        s.connect(args.to_socket)
        # L1CTL frame = 16-bit BE length prefix + payload (per osmocom-bb l1ctl)
        framed = struct.pack(">H", len(custom_req)) + custom_req
        s.sendall(framed)
        print(f"sent {len(framed)} bytes to {args.to_socket}")
        # Optionally wait for CONF
        s.settimeout(2.0)
        try:
            r = s.recv(4096)
            print(f"recv {len(r)} bytes :")
            print(hex_pretty(r))
        except socket.timeout:
            print("(no response in 2s)")
        s.close()
        return

    if args.hdlc_pty:
        framed = hdlc_frame(DLCI_SERCOMM, custom_req)
        with open(args.hdlc_pty, "wb") as f:
            f.write(framed)
        print(f"sent HDLC frame ({len(framed)} bytes) to {args.hdlc_pty}")
        return

    ap.print_help()
    sys.exit(1)


if __name__ == "__main__":
    main()

3.61 https://github.com/bbaranoff/qosmo/blob/main/scripts/test_record_replay.sh

5606 octets, 131 lignes → 131 lignes

#!/bin/bash
# test_record_replay.sh — determinism diagnostic: live record + N replays
#
# Workflow:
#   1. RECORD: normal run with BSP_DUMP_RX_FILE=/tmp/bsp_rx.dump
#   2. REPLAY 1/2/3: runs with CALYPSO_BSP_REPLAY_FILE=/tmp/bsp_rx.dump
#   3. Prints a comparison table of the d_fb_det / SOFT-RESET-TRIG /
#      BURST-IN / etc. signatures to decide: determinism restored (= feed
#      race) or still nondeterministic (= other source).
#
# Usage: ./test_record_replay.sh [duration_sec]    (default 30)
#
# Assumptions:
#   - run.sh is in the PATH or /root/run.sh
#   - CALYPSO_NO_ATTACH=1 supported by run.sh to spawn in the background

set -u
DUR="${1:-30}"
DUMP=/tmp/bsp_rx.dump
LOGDIR=/tmp/record_replay_logs
mkdir -p "$LOGDIR"
RUN_SH="${RUN_SH:-${QEMU_TREE}/run.sh}"
QEMU_LOG="${QEMU_LOG:-/root/qemu.log}"

# ---- helper: kill the whole stack and clean up ----
killall_run() {
    tmux kill-session -t calypso 2>/dev/null || true
    killall -9 qemu-system-arm osmo-bts-trx mobile osmocon osmo-trx-ipc 2>/dev/null || true
    pkill -9 -f bridge.py 2>/dev/null || true
    pkill -9 -f calypso-ipc-device 2>/dev/null || true
    pkill -9 -f irda_capture.py 2>/dev/null || true
    sleep 1
}

# ---- helper: start a run in the background with env, wait DUR seconds ----
do_run() {
    local label="$1"; shift
    echo "===== $label : start (timeout=${DUR}s) ====="
    killall_run

    # CALYPSO_NO_ATTACH=1: run.sh exits once the tmux session is started in the background
    CALYPSO_NO_ATTACH=1 "$@" bash "$RUN_SH" > "$LOGDIR/$label.stdout" 2>&1 &
    local launcher_pid=$!

    # Wait DUR seconds, qemu.log fills up
    sleep "$DUR"

    # Snapshot qemu.log and kill the stack
    cp -f "$QEMU_LOG" "$LOGDIR/$label.qemu.log" 2>/dev/null || true
    killall_run
    wait "$launcher_pid" 2>/dev/null || true
    echo "===== $label : done, log=$LOGDIR/$label.qemu.log ($(wc -l < $LOGDIR/$label.qemu.log 2>/dev/null || echo 0) lines) ====="
    echo
}

# ---- helper: extract a compact signature from a qemu.log ----
sig() {
    local log="$1"
    if [ ! -s "$log" ]; then
        echo "  (vide ou inexistant)"
        return
    fi
    local boot_redirect soft_reset_trig fb_wr_total fb_wr_dom_pc fb_wr_dom_val
    local latch_consume_dom burst_mod burst_tonal burst_silent
    local task5 task6 sync_demod fbsb_state imr_zero last_insn

    boot_redirect=$(grep -c 'SILICON-BOOT-REDIRECT' "$log")
    soft_reset_trig=$(grep -c 'SOFT-RESET-TRIG' "$log")
    fb_wr_total=$(grep -c 'FB-DET-WR' "$log")
    fb_wr_dom_pc=$(grep 'FB-DET-WR' "$log" | grep -oE 'PC=0x[0-9a-f]+' | sort | uniq -c | sort -rn | head -1 | awk '{print $2"×"$1}')
    fb_wr_dom_val=$(grep 'FB-DET-WR' "$log" | grep -oE '<- 0x[0-9a-f]+' | sort | uniq -c | sort -rn | head -1 | awk '{print $3"×"$1}')
    latch_consume_dom=$(grep 'LATCH-CONSUME' "$log" | grep -oE '= 0x[0-9a-f]+' | sort | uniq -c | sort -rn | head -1 | awk '{print $3"×"$1}')
    burst_mod=$(grep 'BURST-IN' "$log" | grep -c MODULATED)
    burst_tonal=$(grep 'BURST-IN' "$log" | grep -c TONAL_FB)
    burst_silent=$(grep 'BURST-IN' "$log" | grep -c SILENT)
    task5=$(grep 'on_dsp_task_change' "$log" | grep -c 'task=5')
    task6=$(grep 'on_dsp_task_change' "$log" | grep -c 'task=6')
    sync_demod=$(grep '\[fbsb\]' "$log" | grep -oE 'last\(snr=[0-9]+ toa=[0-9]+ ang=[0-9]+ pm=[0-9]+\)' | sort | uniq | head -3 | tr '\n' ' | ')
    fbsb_state=$(grep '\[fbsb\]' "$log" | grep -oE 'state=[A-Z_0-9]+' | sort | uniq -c | tr '\n' ' ')
    imr_zero=$(grep -c 'IMR-W \*ZERO\*' "$log")
    last_insn=$(grep -oE 'insn=[0-9]+' "$log" | grep -oE '[0-9]+' | sort -n | tail -1)

    printf "  SILICON-BOOT-REDIRECT : %s\n" "$boot_redirect"
    printf "  SOFT-RESET-TRIG       : %s\n" "$soft_reset_trig"
    printf "  FB-DET-WR total       : %s\n" "$fb_wr_total"
    printf "  FB-DET-WR dom PC      : %s\n" "$fb_wr_dom_pc"
    printf "  FB-DET-WR dom val     : %s\n" "$fb_wr_dom_val"
    printf "  LATCH-CONSUME dom     : %s\n" "$latch_consume_dom"
    printf "  BURST-IN cat (M/T/S)  : %s / %s / %s\n" "$burst_mod" "$burst_tonal" "$burst_silent"
    printf "  ARM task=5 / task=6   : %s / %s\n" "$task5" "$task6"
    printf "  fbsb state distrib    : %s\n" "$fbsb_state"
    printf "  a_sync_demod uniq     : %s\n" "$sync_demod"
    printf "  IMR-W ZERO            : %s\n" "$imr_zero"
    printf "  Last insn_count       : %s\n" "$last_insn"
}

# ---- 1. RECORD ----
rm -f "$DUMP"
do_run RECORD env BSP_DUMP_RX_FILE="$DUMP"

if [ ! -s "$DUMP" ]; then
    echo "!!! RECORD failed: dump '$DUMP' vide ou absent (taille $(stat -c%s "$DUMP" 2>/dev/null || echo 0))"
    echo "Verifier qu'au moins 1 burst a ete livre au DSP (gate BDLENA)."
    exit 1
fi
echo ">>> RECORD dump size: $(stat -c%s "$DUMP") bytes"
echo

# ---- 2. REPLAY 1/2/3 ----
for i in 1 2 3; do
    do_run REPLAY$i env CALYPSO_BSP_REPLAY_FILE="$DUMP"
done

# ---- 3. Comparison ----
echo "##############################################"
echo "##       SIGNATURE COMPARISON TABLE         ##"
echo "##############################################"
for label in RECORD REPLAY1 REPLAY2 REPLAY3; do
    echo "----- $label -----"
    sig "$LOGDIR/$label.qemu.log"
    echo
done

echo "##############################################"
echo "## Interpretation :                          ##"
echo "## - REPLAY 1/2/3 identiques => determinisme ##"
echo "##   retabli avec feed fixe => course feed   ##"
echo "##   est la root cause                       ##"
echo "## - REPLAY 1/2/3 differents => nondeter-    ##"
echo "##   minisme ailleurs (DSP/host thread/etc)  ##"
echo "##############################################"

3.62 https://github.com/bbaranoff/qosmo/blob/main/scripts/qosmo/test-base.sh

1879 octets, 41 lignes → 41 lignes

#!/bin/bash
# The test of what qosmo IS: a platform that builds without a layer 1, and
# that says which one it carries.
set -uo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/../.."
RACINE="$PWD"
B="${QOSMO_BUILD:-$RACINE/build}"
QEMU="$B/qemu-system-arm"
rouge=$'\033[31m'; vert=$'\033[32m'; nc=$'\033[0m'
ok=0; ko=0

verdict() { # <expected> <obtained> <label>
    if [ "$2" = "$1" ]; then printf '%s  ✓%s %s\n' "$vert" "$nc" "$3"; ok=$((ok+1))
    else printf '%s  ✗%s %s\n     attendu : %s\n     obtenu  : %s\n' "$rouge" "$nc" "$3" "$1" "$2"; ko=$((ko+1)); fi
}

[ -x "$QEMU" ] || { echo "qemu-system-arm introuvable dans $B - construire d'abord"; exit 2; }

# 1. the machine exists
verdict "oui" "$("$QEMU" -M help 2>/dev/null | grep -q '^calypso ' && echo oui || echo non)" \
        "la machine 'calypso' est enregistree"

# 2. it names the layer 1 actually linked. This is the only short way to know
#    what was built; an L1 that sneaks into the build shows up here.
l1="$("$QEMU" -M help 2>/dev/null | sed -n 's/.*couche 1 : \([a-z0-9]*\)).*/\1/p')"
attendu="${QOSMO_L1_ATTENDUE:-aucune}"
verdict "$attendu" "${l1:-<absent>}" "la machine annonce sa couche 1 (QOSMO_L1_ATTENDUE=$attendu)"

# 3. without L1, no layer 1 symbol must have been linked
if [ "$attendu" = "aucune" ]; then
    fuites="$(nm -C "$QEMU" 2>/dev/null | grep -cE ' [Tt] (calypso_l1_grgsm|calypso_c54x|c54x_)' || true)"
    verdict "0" "${fuites:-0}" "aucun symbole de couche 1 lie dans la base nue"
fi

# 4. the binary starts and survives the second (no segfault at realize)
timeout 3 "$QEMU" -M calypso -nographic -serial null -monitor none >/dev/null 2>&1
verdict "124" "$?" "la machine demarre et tourne (124 = toujours vivante au timeout)"

echo
if [ "$ko" -gt 0 ]; then printf '%s%d echec(s), %d succes%s\n' "$rouge" "$ko" "$ok" "$nc"; exit 1; fi
printf '%s%d succes%s\n' "$vert" "$ok" "$nc"

3.63 https://github.com/bbaranoff/qosmo/blob/main/scripts/qosmo/verif-liens-doc.sh

2666 octets, 56 lignes → 56 lignes

#!/bin/bash
# Checks that every local path cited in a .md of the repository really exists.
# A doc that points into the void does not crash: it is just wrong.
set -uo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/../.."

rouge=$'\033[31m'; vert=$'\033[32m'; nc=$'\033[0m'
casses=0; verifies=0

while IFS= read -r md; do
    # 1. the targets of markdown links [text](path)
    # 2. the backtick-quoted paths that look like repository files
    {
        grep -oE '\]\([^)#][^)]*\)' "$md" | sed -e 's/^](//' -e 's/)$//'
        # Complete markdown links are removed first: a backtick-quoted name used
        # as a LABEL ([`x.c`](url)) is already covered by its target, handled
        # above. Otherwise a legitimate cross-repository reference is reported
        # as broken.
        sed -E 's/\[[^]]*\]\([^)]*\)//g' "$md" \
          | grep -oE '`[a-zA-Z0-9_./-]+\.(c|h|sh|py|md|txt|cfg|conf|build|yml)`' | tr -d '`'
    } 2>/dev/null | sort -u | while IFS= read -r cible; do
        case "$cible" in
            http://*|https://*|mailto:*|'') continue ;;
        esac
        cible="${cible%%#*}"
        [ -z "$cible" ] && continue
        # relative to the .md, then to the repository root
        base="$(dirname "$md")"
        # A bare name between backticks ("calypso_trx.c") is a MENTION, not a
        # path: a file of that name only has to exist somewhere in the
        # repository. An explicit path ("hw/arm/.../x.c", or the target of a
        # markdown link) must exist WHERE it is written.
        if [ -e "$base/$cible" ] || [ -e "$cible" ]; then
            echo "OK"
        elif [ "${cible%/*}" = "$cible" ] && \
             [ -n "$(find . -name "$cible" -not -path './build*' -print -quit 2>/dev/null)" ]; then
            echo "OK"
        else
            printf '%s  %s:%s cible introuvable : %s%s\n' \
                   "$rouge" "$md" "$(grep -n -- "$cible" "$md" | head -1 | cut -d: -f1)" "$cible" "$nc" >&2
            echo "KO"
        fi
    done
done < <(git ls-files '*.md' 2>/dev/null || find . -name '*.md' -not -path './build/*') > /tmp/.qosmo-liens.$$

# grep -c prints "0" when it finds nothing AND returns 1: a "|| echo 0" here
# would produce "0\n0", which [ -gt ] rejects. The code is ignored, the output kept.
verifies=$(grep -c . /tmp/.qosmo-liens.$$ 2>/dev/null); verifies=${verifies:-0}
casses=$(grep -c '^KO$' /tmp/.qosmo-liens.$$ 2>/dev/null); casses=${casses:-0}
rm -f /tmp/.qosmo-liens.$$

if [ "$casses" -gt 0 ]; then
    printf '%s%d lien(s) casse(s) sur %d verifie(s)%s\n' "$rouge" "$casses" "$verifies" "$nc" >&2
    exit 1
fi
printf '%s%d lien(s) de doc verifie(s), aucun casse%s\n' "$vert" "$verifies" "$nc"

3.64 https://github.com/bbaranoff/qosmo/blob/main/tests/run_tests.sh

1444 octets, 32 lignes → 32 lignes

#!/bin/bash
# Calypso test runner — LIVE stack (reads /root/qemu.log, /root/osmocon.log...).
# COMPLETE: all tests/*.py (calypso + generic top-level), only functional/ +
# guest-debug/ are IGNORED (QEMU integration suite: binaries/downloads absent).
# --continue-on-collection-errors = never blocks.
# LIVE output (pytest -v + tee, line-buffered) — each test shows up as it runs.
# Generates grafcet.html/md + rapport_final.pdf via the conftest::pytest_sessionfinish hook.
#
# Usage: run_tests.sh [loop]
set -u
PY="${CALYPSO_PY:-/root/.env/bin/python3}"
TESTS=${QEMU_TREE}/tests
OUT="${CALYPSO_TEST_OUT:-/root/test_reports}"
PERIOD="${CALYPSO_TEST_PERIOD:-90}"
mkdir -p "$OUT"

run_once() {
  echo "==================== pytest COMPLET (pile vivante) $(date +%H:%M:%S) ===================="
  ( cd "$TESTS" && CALYPSO_TEST_OUT="$OUT" \
      stdbuf -oL -eL "$PY" -m pytest -v -ra --color=yes \
        -p no:cacheprovider --continue-on-collection-errors \
        --ignore=functional --ignore=guest-debug --ignore=migration \
        --ignore=qemu-iotests 2>&1 | stdbuf -oL tee "$OUT/pytest_last.log" )
  echo "-> $OUT/{grafcet.html, grafcet.md, rapport_final.pdf}  (log: $OUT/pytest_last.log)"
}

if [ "${1:-}" = "loop" ]; then
  echo "[run_tests] LOOP mode, période=${PERIOD}s (Ctrl-C pour stop) — sortie en direct"
  while true; do run_once; echo "[run_tests] prochain run dans ${PERIOD}s…"; sleep "$PERIOD"; done
else
  run_once
fi

3.65 https://github.com/bbaranoff/qosmo/blob/main/tests/test_runtime_net_fs.py

3406 octets, 93 lignes → 93 lignes

"""
test_runtime_net_fs.py — surfaces réseau et FS du run (Phase 2 plan IrDA).

Markers : `runtime_net`, `runtime_fs`. Tests anti-régression sur des aspects
opérationnels du long-running (leak FDs, log size cap, ports inattendus,
disk container).
"""
from __future__ import annotations

import os
import subprocess

import pytest

CONTAINER = os.environ.get("CALYPSO_CONTAINER", "trying")
QEMU_LOG_CT = os.environ.get("CALYPSO_QEMU_LOG", "/root/qemu.log")
INSIDE = os.path.exists("/.dockerenv")


def _dexec_sh(cmd: str, timeout: float = 4.0) -> subprocess.CompletedProcess:
    """Exec un shell command à l'intérieur du container."""
    if INSIDE:
        return subprocess.run(["bash", "-c", cmd],
                              capture_output=True, text=True, timeout=timeout)
    return subprocess.run(["docker", "exec", CONTAINER, "bash", "-c", cmd],
                          capture_output=True, text=True, timeout=timeout)


# -----------------------------------------------------------------------------
@pytest.mark.runtime_net
def test_qemu_ports_listening():
    """Ports attendus listening : 1234 (gdb stub) + 4247 (mobile VTY)."""
    r = _dexec_sh("ss -tln 2>/dev/null || netstat -tln 2>/dev/null")
    out = r.stdout
    missing = []
    for port in ("1234", "4247"):
        if f":{port} " not in out and f":{port}\n" not in out:
            missing.append(port)
    assert not missing, f"ports manquants : {missing} — out : {out[:300]!r}"


@pytest.mark.runtime_net
def test_no_unexpected_high_ports():
    """Aucun listener sur port > 60000 (régression leak)."""
    r = _dexec_sh("ss -tln 2>/dev/null | awk 'NR>1 {print $4}' | grep -oE ':[0-9]+$' | sort -u")
    high = []
    for line in r.stdout.split():
        if line.startswith(":"):
            try:
                p = int(line[1:])
                if p > 60000:
                    high.append(p)
            except ValueError:
                continue
    assert not high, f"ports inattendus (>60000) : {high}"


@pytest.mark.runtime_fs
def test_qemu_fd_usage_below_limit():
    """qemu-system-arm doit avoir moins de 1024 fd (early warning leak)."""
    r = _dexec_sh("pid=$(pgrep qemu-system-arm | head -1); [ -n \"$pid\" ] && ls /proc/$pid/fd 2>/dev/null | wc -l")
    s = r.stdout.strip()
    if not s or s == "0":
        pytest.skip("qemu-system-arm pas trouvé")
    n_fds = int(s)
    assert n_fds < 1024, f"fd leak probable : {n_fds} fds (limite 1024)"
    print(f"\n[fs] qemu fd count = {n_fds}")


@pytest.mark.runtime_fs
def test_qemu_log_disk_size_under_2gb():
    """/root/qemu.log < 2 GB (anti-leak log)."""
    r = _dexec_sh(f"stat -c%s {QEMU_LOG_CT} 2>/dev/null")
    s = r.stdout.strip()
    if not s:
        pytest.skip(f"{QEMU_LOG_CT} absent")
    size_bytes = int(s)
    assert size_bytes < 2 * 1024**3, \
        f"qemu.log trop gros : {size_bytes / 1024**2:.0f} MB (limite 2 GB)"
    print(f"\n[fs] qemu.log = {size_bytes / 1024**2:.1f} MB")


@pytest.mark.runtime_fs
def test_container_disk_space_above_min():
    """Le filesystem `/` du container a au moins 500 MB libre."""
    r = _dexec_sh("df / | tail -1 | awk '{print $4}'")
    s = r.stdout.strip()
    if not s:
        pytest.skip("df pas dispo")
    free_kb = int(s)
    assert free_kb > 500_000, \
        f"disque container trop plein : {free_kb // 1024} MB libre (min 500 MB)"
    print(f"\n[fs] / free = {free_kb // 1024} MB")

3.66 https://github.com/bbaranoff/qosmo/blob/main/tests/test_timer_invariants.py

11268 octets, 300 lignes → 300 lignes

"""
test_timer_invariants.py — tests on the instrumented timer counters.

Targets the `[tdma]`, `[frame_irq]`, `[kick]` logs produced by
`calypso_trx.c` + the CSV `/tmp/log_timeline.csv` produced by `log_timeline.py`.

Invariants tested:
  - [tdma] log present (rebuild done, env CALYPSO_DSP_BUDGET honoured)
  - dsp_n_exec_* <= dsp_budget (sanity)
  - dsp saturation: if dsp_n_exec_5 == budget on N consecutive ticks → signal
  - virtual period between thinned [tdma] = 1000 × 4.615 ms ± 2%
  - frame_irq/tdma ratio ≈ 1
  - log_timeline.csv table produced, no completely empty bucket

Run:
    pytest -v -m timer_invariant
"""
from __future__ import annotations

import csv
import os
import re
import shutil
import subprocess
from pathlib import Path

import pytest

CONTAINER  = os.environ.get("CALYPSO_CONTAINER", "trying")
QEMU_LOG   = os.environ.get("CALYPSO_QEMU_LOG",  "/root/qemu.log")
CSV_PATH   = os.environ.get("CALYPSO_TIMELINE_CSV", "/tmp/log_timeline.csv")
# Standard Docker marker: present in the container, absent on the host.
INSIDE = os.path.exists("/.dockerenv")

TDMA_RE = re.compile(
    r'\[tdma\] tick #(\d+) fn=(\d+) t_virt=(\d+) '
    r'dsp_n_exec_2=(-?\d+) dsp_n_exec_5=(-?\d+) '
    r'dsp_insn_total=(\d+) budget=(\d+)'
)
FIRQ_RE = re.compile(r'\[frame_irq\] lower #(\d+) t_virt=(\d+)')
KICK_RE = re.compile(r'\[kick\] fire #(\d+) vt=(\d+) rt=(\d+)')

GSM_TDMA_NS = 4_615_000


def _read_lines(path: str, tail_n: int = 100000) -> list[str]:
    if INSIDE:
        try:
            with open(path, errors="replace") as f:
                return f.readlines()[-tail_n:]
        except FileNotFoundError:
            return []
    out = subprocess.run(
        ["docker", "exec", CONTAINER, "bash", "-c",
         f"tail -n {tail_n} {path} 2>/dev/null"],
        capture_output=True, text=True, timeout=8)
    return out.stdout.splitlines(keepends=True)


def _parse_tdma() -> list[dict]:
    out = []
    for line in _read_lines(QEMU_LOG):
        m = TDMA_RE.search(line)
        if not m: continue
        out.append({
            "tick":          int(m.group(1)),
            "fn":            int(m.group(2)),
            "t_virt_ns":     int(m.group(3)),
            "dsp_n_exec_2":  int(m.group(4)),
            "dsp_n_exec_5":  int(m.group(5)),
            "dsp_insn_total": int(m.group(6)),
            "budget":        int(m.group(7)),
        })
    return out


def _parse_firq() -> list[dict]:
    out = []
    for line in _read_lines(QEMU_LOG):
        m = FIRQ_RE.search(line)
        if not m: continue
        out.append({"n": int(m.group(1)), "t_virt_ns": int(m.group(2))})
    return out


def _parse_kick() -> list[dict]:
    out = []
    for line in _read_lines(QEMU_LOG):
        m = KICK_RE.search(line)
        if not m: continue
        out.append({"n": int(m.group(1)),
                    "vt": int(m.group(2)),
                    "rt": int(m.group(3))})
    return out


@pytest.fixture(scope="module")
def tdma_samples():
    s = _parse_tdma()
    if not s:
        pytest.skip(f"no [tdma] log lines in {QEMU_LOG} — rebuild QEMU pas appliqué ?")
    return s

@pytest.fixture(scope="module")
def firq_samples():
    s = _parse_firq()
    if not s:
        pytest.skip("no [frame_irq] log lines")
    return s

@pytest.fixture(scope="module")
def kick_samples():
    s = _parse_kick()
    if not s:
        pytest.skip("no [kick] log lines")
    return s


# -----------------------------------------------------------------------------
# Timer tests
# -----------------------------------------------------------------------------
@pytest.mark.timer_invariant
def test_tdma_log_present(tdma_samples):
    """At least 2 [tdma] lines (= 2000+ ticks observed)."""
    assert len(tdma_samples) >= 2, \
        f"only {len(tdma_samples)} [tdma] entries — wait for 2000+ ticks (~9s virtual)"


@pytest.mark.timer_invariant
def test_dsp_n_exec_within_budget(tdma_samples):
    """dsp_n_exec_2 and dsp_n_exec_5 must stay <= budget."""
    for s in tdma_samples:
        assert 0 <= s["dsp_n_exec_2"] <= s["budget"], \
            f"tick #{s['tick']}: dsp_n_exec_2={s['dsp_n_exec_2']} hors [0, {s['budget']}]"
        assert 0 <= s["dsp_n_exec_5"] <= s["budget"], \
            f"tick #{s['tick']}: dsp_n_exec_5={s['dsp_n_exec_5']} hors [0, {s['budget']}]"


@pytest.mark.timer_invariant
def test_dsp_budget_saturation_signal(tdma_samples):
    """If dsp_n_exec_5 == budget on the last 3 entries → DSP saturated,
    XFAIL with a message — an observable state, not a test bug."""
    if len(tdma_samples) < 3:
        pytest.skip("need >= 3 samples")
    last3 = tdma_samples[-3:]
    saturated = all(s["dsp_n_exec_5"] == s["budget"] for s in last3)
    if saturated:
        pytest.xfail(
            f"DSP saturé: dsp_n_exec_5 == budget ({last3[0]['budget']}) sur "
            f"les 3 derniers [tdma] ticks. Descendre CALYPSO_DSP_BUDGET cassera "
            f"probablement la fb-det convergence. Voir REPORT_CLAUDE_WEB_20260516_DSP_OVERRUN.md.")
    # Else: pass — DSP not saturated in steady state
    assert True


@pytest.mark.timer_invariant
def test_tdma_period_virtual_close_to_target(tdma_samples):
    """Virtual period between 2 thinned [tdma] (1000 ticks) ≈ 4.615 s ± 2%."""
    if len(tdma_samples) < 2:
        pytest.skip("need >= 2 samples")
    dt_virt_ns = tdma_samples[-1]["t_virt_ns"] - tdma_samples[-2]["t_virt_ns"]
    dtick = tdma_samples[-1]["tick"] - tdma_samples[-2]["tick"]
    if dtick != 1000:
        pytest.skip(f"non-1000-tick gap: {dtick}")
    expected_ns = 1000 * GSM_TDMA_NS
    rel_err = abs(dt_virt_ns - expected_ns) / expected_ns
    # Under icount the VIRTUAL time is not precise (deterministic but not
    # aligned with the wall clock): keep a wide sanity check, not a hard ±2%
    # assert. Fail only on an obviously wrong order of magnitude.
    if rel_err > 0.50:
        pytest.skip(
            f"period_virtual = {dt_virt_ns:,} ns vs target {expected_ns:,} "
            f"(err {rel_err*100:.1f}%) — VIRTUAL sous icount, hors fenêtre de sanité")
    assert rel_err <= 0.50, \
        f"period_virtual = {dt_virt_ns:,} ns vs target {expected_ns:,} (err {rel_err*100:.2f}%)"


@pytest.mark.timer_invariant
def test_frame_irq_per_tdma_ratio(tdma_samples, firq_samples):
    """frame_irq/tdma ratio ≈ 1 (the frame_irq lower fires on every tdma_tick)."""
    if not firq_samples:
        pytest.skip("no firq samples")
    n_tdma = tdma_samples[-1]["tick"]
    n_firq = firq_samples[-1]["n"]
    if n_tdma == 0:
        pytest.skip()
    ratio = n_firq / n_tdma
    # Wide tolerance: 0.5..1.5 (frame_irq may not fire at boot before the
    # hardware-frame-arm is set by the firmware)
    assert 0.5 <= ratio <= 1.5, \
        f"frame_irq/tdma = {ratio:.2f} (firq={n_firq} / tdma={n_tdma})"


@pytest.mark.timer_invariant
def test_kick_realtime_cadence(kick_samples):
    """Kick fires on REALTIME ~5 ms. Mean delta between 2 logs thinned 1/200
    should be ~1 s wall = ~1e9 ns realtime."""
    if len(kick_samples) < 3:
        pytest.skip("few kick samples")
    rts = [s["rt"] for s in kick_samples]
    deltas = [(rts[i+1] - rts[i]) for i in range(len(rts)-1)]
    mean = sum(deltas) / len(deltas)
    # 200 fires × 5 ms = 1 s = 1e9 ns. Tolerance ±50%.
    assert 0.5e9 <= mean <= 1.5e9, \
        f"kick realtime mean delta = {mean/1e9:.2f}s, expected ~1s (1e9 ns)"


# -----------------------------------------------------------------------------
# Tests on the log_timeline.csv table
# -----------------------------------------------------------------------------
def _has_docker(): return shutil.which("docker") is not None

def _csv_exists() -> tuple[bool, str]:
    """(exists, where) — check the CSV side: local or container."""
    if Path(CSV_PATH).exists():
        return (True, "local")
    if _has_docker():
        r = subprocess.run(
            ["docker", "exec", CONTAINER, "test", "-f", CSV_PATH],
            capture_output=True, timeout=4)
        if r.returncode == 0:
            return (True, "container")
    return (False, "none")

def _generate_csv() -> str:
    """Tries to produce the CSV. Priority: container if docker is available
    (canonical qemu.log on the container side), else local ONLY if in-container.
    Avoids the race where a stale host /root/qemu.log is read.
    """
    # 1) Container path (priority) — unless we ARE in the container
    if _has_docker() and not INSIDE:
        r = subprocess.run(
            ["docker", "exec", CONTAINER, "python3",
             "/opt/GSM/osmo-qemu-calypso/log_timeline.py",
             "--bucket-s", "10", "--csv", CSV_PATH],
            capture_output=True, text=True, timeout=20)
        return r.stderr or r.stdout[:200]
    # 2) Local (in-container: /root/qemu.log is canonical)
    script_local = Path(__file__).resolve().parent.parent / "log_timeline.py"
    if script_local.exists() and Path("/root/qemu.log").exists():
        r = subprocess.run(
            ["python3", str(script_local), "--bucket-s", "10", "--csv", CSV_PATH],
            capture_output=True, text=True, timeout=20)
        return r.stderr or r.stdout[:200]
    return "no path to run log_timeline.py"

@pytest.mark.timer_invariant
def test_log_timeline_csv_produced():
    """log_timeline.py must have produced the CSV (local or container).
    If absent, tries an on-the-fly generation then re-checks."""
    ok, where = _csv_exists()
    if ok:
        return
    err = _generate_csv()
    ok, where = _csv_exists()
    assert ok, f"{CSV_PATH} non produit (local={Path(CSV_PATH).exists()}, docker={_has_docker()}, err={err[:200]})"


def _read_csv_rows() -> list[dict]:
    if INSIDE:
        try:
            with open(CSV_PATH) as f:
                return list(csv.DictReader(f))
        except FileNotFoundError:
            return []
    out = subprocess.run(
        ["docker", "exec", CONTAINER, "cat", CSV_PATH],
        capture_output=True, text=True, timeout=5)
    return list(csv.DictReader(out.stdout.splitlines()))


@pytest.mark.timer_invariant
def test_log_timeline_csv_no_dead_bucket():
    """No bucket with 0 events across all sources (= run not stalled)."""
    rows = _read_csv_rows()
    if not rows:
        pytest.skip("CSV empty")
    dead = []
    sources = ["qemu", "bridge", "osmocon", "mobile"]
    for r in rows:
        total = sum(int(r.get(s, 0) or 0) for s in sources)
        if total == 0:
            dead.append(r["t_rel"])
    assert not dead, f"buckets totalement vides à t_rel = {dead[:5]} (run stallé ?)"


@pytest.mark.timer_invariant
def test_log_timeline_csv_steady_qemu_rate():
    """qemu rate per bucket must stay within a reasonable window
    (no 10× spike nor 10× drop on the majority of buckets)."""
    rows = _read_csv_rows()
    if len(rows) < 5:
        pytest.skip("too few buckets")
    qemu_rates = [int(r.get("qemu", 0) or 0) for r in rows[1:]]  # skip warmup
    if not qemu_rates: pytest.skip("no qemu rates")
    mean = sum(qemu_rates) / len(qemu_rates)
    outliers = [v for v in qemu_rates if v > 10*mean or v < mean/10]
    # Tolerates up to 20% outliers
    assert len(outliers) <= 0.2 * len(qemu_rates), \
        f"{len(outliers)}/{len(qemu_rates)} buckets outliers (>10× ou <1/10 mean={mean:.0f})"

3.67 https://github.com/bbaranoff/qosmo/blob/main/tests/test_timer_physical_audit.py

9891 octets, 261 lignes → 261 lignes

"""
test_timer_physical_audit.py — physical audit of the timers + temporal graphs.

Checks that each timer parameter in the code follows the physical equation
documented in TIMING_PHYSICS.md. For each timer, also measures the effective
rate on the run side (from qemu.log / bridge.log) and compares it to the
theoretical target.

Generates mermaid xychart graphs to visualise the periods.

Markers:
  - osmocom_clock
  - timer_audit
  - timer_graph
"""
from __future__ import annotations

import os
import re
import subprocess
from pathlib import Path
from collections import defaultdict
from statistics import mean, median, stdev

import pytest

CONTAINER = os.environ.get("CALYPSO_CONTAINER", "trying")
INSIDE = os.path.exists("/.dockerenv")
QEMU_SRC = "/opt/GSM/osmo-qemu-calypso"
QEMU_LOG = "/root/qemu.log"
(removed) = "/tmp/bridge.log"
BTS_LOG = "/tmp/bts.log"
GRAPHS_OUT = "/tmp/timer_graphs.mmd"


def _read(path: str) -> str:
    if INSIDE:
        try:
            return Path(path).read_text(errors="replace")
        except Exception:
            return ""
    try:
        r = subprocess.run(
            ["docker", "exec", CONTAINER, "cat", path],
            capture_output=True, text=True, timeout=10)
        return r.stdout
    except Exception:
        return ""


# === Physical audit: target vs actual =======================================
# Format: (param_name, target_value, target_units, source_file, search_regex,
#          parse_fn, tolerance_pct)
AUDIT_PARAMS = [
    # 1. TINT0_PERIOD_NS — target 60_000_000/13 = 4_615_384 ns
    ("TINT0_PERIOD_NS", 4_615_384, "ns",
     "hw/arm/calypso/calypso_tint0.h",
     r"#define\s+TINT0_PERIOD_NS\s+(\d+)",
     int, 0.1),
    # 2. GSM_TDMA_NS — target 4_615_384 ns
    ("GSM_TDMA_NS", 4_615_384, "ns",
     "include/hw/arm/calypso/calypso_trx.h",
     r"#define\s+GSM_TDMA_NS\s+(\d+)",
     int, 0.1),
    # 3. GSM_HYPERFRAME — exact 2_715_648
    ("GSM_HYPERFRAME", 2_715_648, "frames",
     "hw/arm/calypso/calypso_tint0.h",
     r"#define\s+GSM_HYPERFRAME\s+(\d+)",
     int, 0.0),
    # 4. BSP_DRAIN_PERIOD_MS — target ~5 (1 TDMA frame)
    ("BSP_DRAIN_PERIOD_MS", 5, "ms",
     "hw/arm/calypso/calypso_bsp.c",
     r"#define\s+BSP_DRAIN_PERIOD_MS\s+(\d+)",
     int, 20.0),  # ±20% acceptable
    # 5. WT_DELAY_NS — target 25_000_000 ns (ISO SIM spec)
    #    But 2_000_000 ns is accepted (fast emulation, documented)
    ("WT_DELAY_NS", 25_000_000, "ns",
     "hw/arm/calypso/calypso_sim.c",
     r"#define\s+WT_DELAY_NS\s+(\d+)",
     int, 95.0),  # known off by 12× for speed
    # 6. PCB_TICK_TDMA_US — target 4615 µs
    ("PCB_TICK_TDMA_US", 4615, "µs",
     "hw/arm/calypso/calypso_full_pcb.c",
     r"#define\s+PCB_TICK_TDMA_US\s+(\d+)",
     int, 0.1),
    # 7. PCB_TICK_TINT0_US — target 4615 µs
    ("PCB_TICK_TINT0_US", 4615, "µs",
     "hw/arm/calypso/calypso_full_pcb.c",
     r"#define\s+PCB_TICK_TINT0_US\s+(\d+)",
     int, 0.1),
    # 8. PCB_TICK_KICK_US — target 5000 µs (legacy 200 Hz)
    ("PCB_TICK_KICK_US", 5000, "µs",
     "hw/arm/calypso/calypso_full_pcb.c",
     r"#define\s+PCB_TICK_KICK_US\s+(\d+)",
     int, 0.0),
    # 9. PCB_TICK_FRAME_IRQ_US — target 1000 µs
    ("PCB_TICK_FRAME_IRQ_US", 1000, "µs",
     "hw/arm/calypso/calypso_full_pcb.c",
     r"#define\s+PCB_TICK_FRAME_IRQ_US\s+(\d+)",
     int, 0.0),
]


@pytest.mark.timer_audit
@pytest.mark.osmocom_clock
@pytest.mark.parametrize("param,voulu,units,src,regex,parse,tol",
                         AUDIT_PARAMS,
                         ids=[p[0] for p in AUDIT_PARAMS])
def test_timer_physical_param(param, voulu, units, src, regex, parse, tol):
    """Checks that each timer parameter in the code matches the value
    required by the HW physical equation (tolerance %)."""
    path = f"{QEMU_SRC}/{src}"
    content = _read(path)
    assert content, f"can't read {path}"
    m = re.search(regex, content)
    assert m, f"can't find {param} in {path} (regex={regex})"
    actual = parse(m.group(1))
    pct_diff = abs(actual - voulu) / voulu * 100.0 if voulu else 0.0
    msg = (f"{param}: voulu={voulu} {units}, actual={actual} {units}, "
           f"Δ={pct_diff:+.3f}% (tol={tol}%)")
    print(f"  AUDIT  {msg}")
    assert pct_diff <= tol, f"OFF SPEC : {msg}"


# === Runtime measurements from logs =========================================

def _parse_timestamps(log_content: str, pattern: str):
    """Returns list[(unix_ts, wall_offset)] for each line matching pattern."""
    out = []
    for line in log_content.splitlines():
        if not re.search(pattern, line):
            continue
        # Log format: `<unix_ts> +<wall>s ...`
        m = re.match(r"(\d+\.\d+)\s+\+(\d+\.\d+)s", line)
        if m:
            out.append((float(m.group(1)), float(m.group(2))))
    return out


def _periods_ms(timestamps):
    """Inter-event periods in ms."""
    return [(timestamps[i+1][0] - timestamps[i][0]) * 1000
            for i in range(len(timestamps) - 1)]


TIMER_RUNTIME_PROBES = [
    ("tdma_tick", QEMU_LOG, r"\[tdma\] tick #", 4.615, "TDMA frame (virtual)"),
    ("kick", QEMU_LOG, r"\[kick\] fire #", 5.0, "Kick (wall, was REALTIME)"),
    ("bridge_clk_ind", (removed), r"CLK IND", 235.4, "CLK IND (51 frames)"),
    ("bts_clk_ind", BTS_LOG, r"Clock indication: fn=", 235.4, "BTS CLK IND reçu"),
    ("bsp_burst", QEMU_LOG, r"\[BSP\] BURST fn=", 4.615, "BSP RX burst"),
]


@pytest.mark.timer_audit
@pytest.mark.parametrize("name,log_path,pattern,expected_ms,desc",
                         TIMER_RUNTIME_PROBES,
                         ids=[p[0] for p in TIMER_RUNTIME_PROBES])
def test_timer_runtime_period(name, log_path, pattern, expected_ms, desc):
    """Measures the real timer period from the logs vs the expected value.
    Skips if not enough samples (run too short)."""
    log = _read(log_path)
    if not log:
        pytest.skip(f"{log_path} vide")
    ts = _parse_timestamps(log, pattern)
    if len(ts) < 30:
        pytest.skip(f"{name}: only {len(ts)} samples in {log_path}")
    periods = _periods_ms(ts)
    # Filters extreme outliers (gap > 10× expected = pause / restart)
    periods = [p for p in periods if p < expected_ms * 10]
    if len(periods) < 20:
        pytest.skip(f"{name}: only {len(periods)} clean samples")
    p_med = median(periods)
    p_mean = mean(periods)
    p_std = stdev(periods) if len(periods) > 1 else 0.0
    pct = abs(p_med - expected_ms) / expected_ms * 100.0
    msg = (f"{name} ({desc}): expected={expected_ms} ms, "
           f"median={p_med:.2f} ms, mean={p_mean:.2f} ms, "
           f"σ={p_std:.2f} ms, Δ_med={pct:+.1f}% (N={len(periods)})")
    print(f"  RUNTIME {msg}")
    # Wide tolerance (50%): under icount=auto the rate drifts
    assert pct < 50.0, f"OFF EXPECTED : {msg}"


# === Mermaid graphs for the periods =========================================

@pytest.mark.timer_graph
def test_timer_temporal_graphs_emit_mermaid():
    """Generates a mermaid xychart file for each instrumented timer.
    Output: /tmp/timer_graphs.mmd holds the xycharts to paste into a
    markdown report."""
    out_lines = ["# Calypso Timer temporal graphs",
                 "",
                 "Auto-generated by `test_timer_temporal_graphs_emit_mermaid`.",
                 "Une xychart par timer : Y = période entre 2 events (ms), "
                 "X = numéro d'event.",
                 ""]
    found_any = False
    for name, log_path, pattern, expected_ms, desc in TIMER_RUNTIME_PROBES:
        log = _read(log_path)
        if not log:
            continue
        ts = _parse_timestamps(log, pattern)
        if len(ts) < 10:
            continue
        periods = _periods_ms(ts)
        # Sample down to ~50 points max for mermaid render
        N = min(50, len(periods))
        step = max(1, len(periods) // N)
        sampled = periods[::step][:N]
        if not sampled:
            continue
        found_any = True

        out_lines.extend([
            f"## {name} — {desc}",
            f"Expected: {expected_ms} ms ; samples: {len(periods)} (showing {len(sampled)})",
            "",
            "```mermaid",
            "xychart-beta",
            f'  title "{name} period (ms) vs event #"',
            f'  x-axis "event #" 1 --> {len(sampled)}',
            f'  y-axis "period (ms)" 0 --> {max(sampled) * 1.2:.1f}',
            f'  line [{", ".join(f"{p:.2f}" for p in sampled)}]',
            "```",
            "",
        ])
    if not found_any:
        pytest.skip("aucun timer log dispo pour générer graph")
    out = "\n".join(out_lines)
    Path(GRAPHS_OUT).write_text(out)
    print(f"\n  emitted {Path(GRAPHS_OUT)} ({len(out)} chars, "
          f"{out.count('xychart-beta')} graphs)")


@pytest.mark.timer_graph
def test_timer_drift_summary_table():
    """Generates a summary table of the drift across all timers."""
    rows = []
    for name, log_path, pattern, expected_ms, desc in TIMER_RUNTIME_PROBES:
        log = _read(log_path)
        if not log:
            rows.append((name, "—", expected_ms, "no log"))
            continue
        ts = _parse_timestamps(log, pattern)
        if len(ts) < 20:
            rows.append((name, "—", expected_ms, f"only {len(ts)} samples"))
            continue
        periods = [p for p in _periods_ms(ts) if p < expected_ms * 10]
        if not periods:
            rows.append((name, "—", expected_ms, "no clean samples"))
            continue
        med = median(periods)
        pct = (med - expected_ms) / expected_ms * 100.0
        status = "✓" if abs(pct) < 10 else "⚠" if abs(pct) < 30 else "✗"
        rows.append((name, f"{med:.2f} ms", f"{expected_ms} ms",
                     f"{status} Δ={pct:+.1f}%"))
    print("\n  Timer drift summary :")
    print(f"  {'name':<20} {'median':<12} {'expected':<12} {'status'}")
    for r in rows:
        print(f"  {r[0]:<20} {r[1]:<12} {r[2]:<12} {r[3]}")

3.68 https://github.com/bbaranoff/qosmo/blob/main/tmux_modules/_commun.sh

4978 octets, 113 lignes → 113 lignes

C_RADIO=colour81
C_COEUR=colour114
C_MS1=colour179
C_MS2=colour215
C_VOIX=colour176
SUIVRE="tail -n 100 -F"
# Display colours (tools/qosmo-couleurs.py): tags [trx] [sim] [c54x]..., fn=, hex,
# errors in red, asm trace (address, bytes, PC, symbol), mailbox (ARM>WR ...).
# Nothing is written to the files. QOSMO_COULEURS=0 turns it off.
_couleurs() {
    local p="${QOSMO_COULEURS_PY:-${QEMU_TREE:-.}/tools/qosmo-couleurs.py}"
    [ "${QOSMO_COULEURS:-1}" != 0 ] && [ -f "$p" ] && printf " | python3 -u '%s' --force" "$p"
}
: "${OSMO_LOG_DIR:=/var/log/osmocom}"

_paint() {
    [ -n "${2:-}" ] || return 0
    tmux select-pane -t "$TMUX_SESSION:$1" -P "fg=$2" 2>/dev/null || true
}

_w() {
    tmux new-window -t "$TMUX_SESSION" -n "$1" "$3" 2>/dev/null \
        && tmux select-pane -t "$TMUX_SESSION:$1" -T "$2" 2>/dev/null \
        && _paint "$1" "${4:-}"
}

_split() {
    tmux split-window -t "$TMUX_SESSION:$1" "$3" 2>/dev/null \
        && tmux select-pane -t "$TMUX_SESSION:$1" -T "$2" 2>/dev/null \
        && _paint "$1" "${4:-}" \
        && tmux select-layout -t "$TMUX_SESSION:$1" tiled 2>/dev/null
}

_tail() {
    printf "%s %s 2>/dev/null | stdbuf -oL tr -d '\\\\007'%s || sleep infinity" \
           "$SUIVRE" "$*" "$(_couleurs)"
}

# Pane for the asm trace (ASSEMBLY_LOGS=1 / QEMU_ASM): NOT a tail -F, the file
# grows up to 150 MB/s with exec,nochain and a tail -F lags by seconds (and
# bloats the scrollback). The last 60 lines are refreshed twice a second; with
# --dsp the file is the one of c54x_exe (/tmp/c54x-pont).
_asm_watch() {
    printf "watch -t -n 0.5 --color \"for f in /tmp/c54x-pont/qemu-asm.log '%s/qemu-asm.log'; do [ -f \\\$f ] && { tail -n 60 \\\$f%s; break; }; done\"" \
           "${LOG_DIR}" "$(_couleurs)"
}

_fenetre_coeur() {
    local O="${OSMO_LOG_DIR:-/var/log/osmocom}"
    _w     coeur "MSC | commutation circuit - appels, SMS, authentification" \
        "$(_tail "'$O/osmo-msc.log'")" "$C_COEUR"
    _split coeur "BSC | controleur de stations - allocation des canaux" \
        "$(_tail "'$O/osmo-bsc.log'")" "$C_COEUR"
    _split coeur "HLR | registre des abonnes - IMSI, cles, souscriptions" \
        "$(_tail "'$O/osmo-hlr.log'")" "$C_COEUR"
    _split coeur "MGW + STP | media MGCP/RTP et routage SS7" \
        "$(_tail "'$O/osmo-mgw.log' '$O/osmo-stp.log'")" "$C_COEUR"
}

_fenetre_voix() {
    local L="${LOG_DIR}" O="${OSMO_LOG_DIR:-/var/log/osmocom}"
    _w     voix "gapk | transcodage GSM-FR <-> PCM (actif pendant un appel)" \
        "$(_tail "'$L/gapk-auto.log'")" "$C_VOIX"
    _split voix "SIP | passerelle MNCC <-> SIP vers Asterisk" \
        "$(_tail "'$O/osmo-sip-connector.log'")" "$C_VOIX"
}

_fenetre_shell() {
    local aide=""
    local l; for l in "$@"; do aide="${aide}  ${l}\\n"; done
    _w shell "shell | depot qosmo" \
        "cd '${QEMU_TREE:-.}' && printf '\n  %s\n  %s\n${aide}\n' \
             'etat  : ./run.sh --status' 'arret : ./run.sh --stop' \
         && exec ${SHELL:-/bin/bash}"
}

_tmux_env_repropre() {
    command -v tmux >/dev/null 2>&1 || return 0
    tmux show-environment -g 2>/dev/null \
        | sed -n 's/^\(CALYPSO_[A-Za-z0-9_]*\)=.*/\1/p' \
        | while IFS= read -r _v; do
              [ -n "$_v" ] && tmux set-environment -g -u "$_v" 2>/dev/null
          done
    unset _v
}

_tmux_style() {
    local s="$TMUX_SESSION"
    tmux set-environment -t "$s" LANG   "${LANG:-C.UTF-8}"   2>/dev/null
    tmux set-environment -t "$s" LC_ALL "${LC_ALL:-C.UTF-8}" 2>/dev/null
    tmux set -t "$s" -g mouse on            2>/dev/null
    tmux set -t "$s" -g base-index 1        2>/dev/null
    tmux set -t "$s" -g pane-base-index 1   2>/dev/null
    tmux set -t "$s" -g history-limit 20000 2>/dev/null
    tmux set -t "$s" -g renumber-windows on 2>/dev/null
    tmux set -t "$s" -g bell-action none  2>/dev/null
    tmux set -t "$s" -g visual-bell off   2>/dev/null
    tmux set -t "$s" -g monitor-bell off  2>/dev/null
    tmux set -t "$s" -g status-style        "bg=colour234,fg=colour250" 2>/dev/null
    tmux set -t "$s" -g status-left-length  40 2>/dev/null
    tmux set -t "$s" -g status-left \
        "#[bg=colour31,fg=colour15,bold] calypso #[default] " 2>/dev/null
    tmux set -t "$s" -g status-right-length 76 2>/dev/null
    tmux set -t "$s" -g status-right \
        "#[fg=colour215]gr-gsm#[fg=colour244] · #[fg=colour114]pont ARFCN ${PONT_ARFCN:-?} BSIC ${PONT_BSIC:-?}#[fg=colour244] · #[fg=colour81]${ENCRYPTION:-?}#[default]  #[fg=colour250]%H:%M " 2>/dev/null
    tmux set -t "$s" -g window-status-format         " #I #W " 2>/dev/null
    tmux set -t "$s" -g window-status-current-format \
        "#[bg=colour31,fg=colour15,bold] #I #W #[default]" 2>/dev/null
    tmux set -t "$s" -g pane-border-style        "fg=colour238" 2>/dev/null
    tmux set -t "$s" -g pane-active-border-style "fg=colour31"  2>/dev/null
    tmux set -t "$s" -g pane-border-status top 2>/dev/null
    tmux set -t "$s" -g pane-border-format " #{pane_title} " 2>/dev/null
}

3.69 https://github.com/bbaranoff/qosmo/blob/main/tmux_modules/bench.sh

2339 octets, 43 lignes → 43 lignes

TMUX_FENETRE_PREMIERE=radio
TMUX_RESUME="6 fenêtres : radio · coeur · ms1 · ms2 (side-car) · voix · shell"

tmux_layout_premiere() {
    printf "tail -n 200 -F '%s/qemu.log' 2>/dev/null | stdbuf -oL tr -d '\\\\007'%s || sleep infinity" \
           "${LOG_DIR}" "$(_couleurs)"
}

tmux_layout() {
    local L="${LOG_DIR}"
    tmux select-pane -t "$TMUX_SESSION:radio" -T \
        "QEMU | baseband emule (ARM7, couche 1 gr-gsm) - traces du modele" 2>/dev/null
    _paint radio "$C_RADIO"
    _split radio "osmocon | lien serie firmware - romload puis transport L1CTL" \
        "$(_tail "'$L/osmocon.log'")" "$C_RADIO"
    _split radio "pont | transceiver du BTS#0 - bursts TRXD 5700-5702 <-> QEMU 4730/4731" \
        "$(_tail "'/dev/shm/pont.log'")" "$C_RADIO"
    # ARM asm trace when requested (--asm / --assembly-logs)
    if { [ -n "${QEMU_ASM:-}" ] && [ "${QEMU_ASM}" != 0 ]; } || [ "${ASSEMBLY_LOGS:-0}" = 1 ]; then
        _split radio "asm | blocs ARM (-d ${QEMU_ASM_FLAGS:-in_asm}), 60 dernieres lignes, 2x/s - anneau : tbring-dump" \
            "$(_asm_watch)" "$C_RADIO"
    fi
    _fenetre_coeur
    # MS1_LOG (osmo-operator, 09-mobile-cfg-tmux.sh): rvinterf.log with Magnetite
    _w     ms1 "MS#1 | ${MS1_LOG_NOM:-mobile} du telephone emule - camp, LU, appels" \
        "$(_tail "'${MS1_LOG:-$L/mobile.log}'")" "$C_MS1"
    _split ms1 "BTS#0 | sa station de base - ARFCN ${PONT_ARFCN:-514}, unit-id 6001" \
        "$(_tail "'$L/bts.log'")" "$C_MS1"
    _split ms1 "GSMTAP | capture pcap - a ouvrir dans Wireshark" \
        "$(_tail "'$L/gsmtap.log'")" "$C_MS1"
    _w     ms2 "MS#2 | second abonne - campe sur le BTS#1, appelle le MS#1" \
        "$(_tail "'$L/sidecar-mobile.log'")" "$C_MS2"
    _split ms2 "trxcon | sa couche 1 - TRXD <-> L1CTL (remplace le firmware)" \
        "$(_tail "'$L/sidecar-trxcon.log'")" "$C_MS2"
    _split ms2 "BTS#1 | seconde station - ARFCN ${SC_ARFCN:-516}, unit-id ${SC_UNIT_ID:-6002}" \
        "$(_tail "'$L/sidecar-bts.log'")" "$C_MS2"
    _split ms2 "fake_trx | commutateur de bursts UDP ${SC_TRX_PORT:-5720} <-> ${SC_BB_PORT:-6720}" \
        "$(_tail "'$L/sidecar-faketrx.log'")" "$C_MS2"
    _fenetre_voix
    _fenetre_shell \
        "MS#1  : telnet 127.0.0.1 4247   puis  show ms" \
        "MS#2  : telnet 127.0.0.1 ${SC_MOBILE_VTY_PORT:-4248}   puis  call 1 10001"
}

3.70 https://github.com/bbaranoff/qosmo/blob/main/tools/demo-layer1.py

6046 octets, 157 lignes → 157 lignes

# demo-layer1.py — "demo": show that we control the layer1 firmware (osmocom-bb)
# running in QEMU, from the telnet console (tools/gdb-telnet.py, port 44444).
#
#   demo            everything: stop the ARM, state, one TDMA frame at a breakpoint, a
#                   printf in the firmware (visible in osmocon.log), then the ARM resumes
#   demo etat       state only (pc, stack, fn, cell, DSP mailbox); stays stopped
#   demo trame      runs until the next TDMA frame (tdma_sched_execute); stays stopped
#   demo hello      call printf(...) in the firmware -> sercomm console (osmocon.log); stays stopped
#   demo go         the ARM resumes (continue &)
#
# Between two steps: 4 s to read (set $demo_pause = N to change, 0 = no pause).
# If the ARM is running, "demo" stops it first (interrupt) and goes on at the next
# stop event (gdb.events.stop): nothing is read until the target is stopped.
# Reads go through the cmd.gdb panel commands (fn, l1, tasks), loaded beforehand.
import gdb, time


def ex(c):
    try:
        return gdb.execute(c, to_string=True)
    except gdb.error as e:
        return "  (%s : %s)\n" % (c, e)


def out(t):
    gdb.write(t if t.endswith("\n") else t + "\n")
    gdb.flush()


def en_marche():
    try:
        return any(t.is_running() for t in gdb.selected_inferior().threads())
    except gdb.error:
        return False


def pause_etape():
    # between two steps, time to read: 4 s by default, "set $demo_pause = 2" to change
    p = gdb.convenience_variable("demo_pause")
    t = float(p) if p is not None else 4.0
    if t > 0:
        out("[demo]        ... (suite dans %.0f s)" % t)
        time.sleep(t)


def val(expr):
    try:
        return int(gdb.parse_and_eval(expr))
    except gdb.error:
        return None


def etat():
    fr = gdb.newest_frame()
    sal = fr.find_sal()
    ou = "%s:%d" % (sal.symtab.filename, sal.line) if sal.symtab else "?"
    out("[demo] ===== 1. L'ARM EST ARRETE : pc=0x%08x dans %s (%s)" % (fr.pc(), fr.name() or "?", ou))
    out(ex("bt 6"))
    out(ex("x/3i $pc"))
    pause_etape()
    out("[demo] ===== 2. CE QUE LA COUCHE 1 SAIT (symboles l1s du firmware) :")
    out(ex("fn"))
    out(ex("l1"))
    pause_etape()
    out("[demo] ===== 3. LA BOITE AUX LETTRES ARM<->DSP, lue par l'ARM a 0xffd00000")
    out("[demo]        (= « d@ 0800 8 » dans harvard.sh : meme memoire, /dev/shm/calypso_api_ram)")
    out(ex("x/8hx 0xffd00000"))
    out(ex("tasks"))
    pause_etape()


def trame():
    avant = val("l1s.current_time.fn")
    out("[demo] ===== 4. ON RELANCE JUSQU'A LA PROCHAINE TRAME TDMA (tbreak tdma_sched_execute), fn=%s" % avant)
    t0 = time.time()
    ex("tbreak tdma_sched_execute")
    gdb.execute("continue")
    apres = val("l1s.current_time.fn")
    d = (apres - avant) if (apres is not None and avant is not None) else "?"
    out("[demo]        arrete dans tdma_sched_execute : fn=%s (+%s) en %.0f ms  (une trame = 4,615 ms)"
        % (apres, d, 1000 * (time.time() - t0)))
    out(ex("bt 4"))
    pause_etape()


def hello():
    # No malloc in the firmware: gdb cannot place a string literal there. It is written
    # by hand just above the bss (_end + 0x100): free area (no heap, the stacks are at
    # the very top of RAM, TOP_OF_RAM 0x83fff0 in board/compal/macros.S), then puts() on it.
    fn = val("l1s.current_time.fn")
    out("[demo] ===== 5. ON FAIT PARLER LE FIRMWARE : la chaine ecrite en RAM libre, puis call puts() -> console sercomm (osmocon.log)")
    adr = val("(unsigned long)&_end")
    if adr is None:
        out("[demo]        (_end introuvable : ELF sans symboles ?)"); return
    adr = (adr + 0x100) & ~0xf
    # ANSI colours in the string itself: osmocon copies the sercomm console byte for byte
    # (hdlc_console_cb -> write(1, ...)), so "tail -f osmocon.log" in a terminal shows it in colour.
    # hello (green) a (white) 40c3 (purple): "hello a 40c3", written to RAM then output by puts().
    texte = ("\033[1;32mhello\033[0m \033[1;37ma\033[0m \033[1;35m40c3\033[0m "
             "\033[36m(0x%x, fn=%s)\033[0m\n" % (adr, fn))
    msg = texte.encode() + b"\0"
    gdb.selected_inferior().write_memory(adr, msg)
    out("[demo]        %d octets ecrits a 0x%08x : %s" % (len(msg), adr, ex("x/s 0x%x" % adr).strip()))
    out("[demo]        soit, rendu : " + texte.rstrip("\n"))
    out(ex("call (int)puts((const char *)0x%x)" % adr))
    out("[demo]        (regarder : tail -f /tmp/c54x-pont/osmocon.log)")
    pause_etape()


def fin():
    out(ex("continue &"))
    out("[demo] ===== FIN : l'ARM est RELANCE et tourne a nouveau, rien de casse (Ctrl-C ou stop pour l'arreter, help_osmo pour le reste).")
    try:
        time.sleep(1.0)
        out("[demo]        1 s plus tard, l'ARM a bien repris : %s" % ("il tourne (continue &)" if en_marche() else "il est ARRETE ?!"))
    except Exception:
        pass


def corps(arg):
    try:
        if arg in ("etat", "tout"):
            etat()
        if arg in ("trame", "tout"):
            trame()
        if arg in ("hello", "tout"):
            hello()
        if arg in ("go", "tout"):
            fin()
        if arg not in ("etat", "trame", "hello", "go", "tout"):
            out("usage : demo [tout|etat|trame|hello|go]")
    except Exception as e:
        out("[demo] erreur : %s" % e)


class Demo(gdb.Command):
    """demo [tout|etat|trame|hello|go] — control of the layer1 firmware: stop, state, one frame, printf, resume."""

    def __init__(self):
        super().__init__("demo", gdb.COMMAND_USER)

    def invoke(self, arg, from_tty):
        arg = arg.strip() or "tout"
        if en_marche():
            out("[demo] l'ARM tourne : interrupt ; la suite des que la cible est arretee...")

            def on_stop(ev):
                gdb.events.stop.disconnect(on_stop)
                gdb.post_event(lambda: corps(arg))
            gdb.events.stop.connect(on_stop)
            gdb.execute("interrupt")
        else:
            corps(arg)


Demo()
gdb.write("[demo-layer1] tape  demo  (ou demo etat | trame | hello | go)\n")

3.71 https://github.com/bbaranoff/qosmo/blob/main/tools/gdb-telnet.py

8327 octets, 204 lignes → 204 lignes

#!/usr/bin/env python3
"""
gdb-telnet.py — gdb console (ARM of the emulated Calypso) served over telnet, NON-BLOCKING.

    telnet localhost 44444

What you get: a gdb-multiarch attached to the QEMU gdbstub (-gdb tcp::1234),
the target RUNNING (`continue &`), and the (gdb) prompt available right away.
    Ctrl-C            stops the ARM (SIGINT -> gdb interrupts the target), inspect
    continue &        resumes, without blocking the prompt
    quit / Ctrl-]     the session closes, gdb is killed, QEMU resumes the ARM

Why a home-made server and not `socat ... EXEC:gdb,pty`: telnet stays in
"line by line" mode facing a silent server, and Ctrl-C then leaves as an
IAC IP sequence that gdb does not understand. Here ECHO+SGA (character mode)
is negotiated: the client sends a raw 0x03, the pty (isig) turns it into a
SIGINT for gdb. The remaining IAC sequences are filtered, IAC IP/BRK become a 0x03.

One session at a time: the QEMU gdbstub accepts only one client. As long as
nobody is connected, no gdb runs and the ARM is never touched.
"""
import argparse, os, pty, select, signal, socket, sys, time

IAC, DONT, DO, WONT, WILL, SB, SE = 255, 254, 253, 252, 251, 250, 240
IP, BRK = 244, 243
ECHO, SGA = 1, 3

# Ctrl-C: when the target runs IN THE BACKGROUND (continue &), a SIGINT at the
# gdb prompt does not stop it — readline just answers "Quit". What stops it is
# the `interrupt` command. The current line is erased (Ctrl-U) then it is
# sent; gdb prints "Program received signal SIGINT" and control comes back.
CTRL_C = b'\x15interrupt\n'


def log(msg):
    sys.stderr.write(time.strftime('%H:%M:%S ') + msg + '\n'); sys.stderr.flush()


def filter_telnet(data, state):
    """Strips the telnet commands, returns the bytes meant for gdb."""
    out = bytearray()
    buf = state['pending'] + data
    i = 0
    while i < len(buf):
        b = buf[i]
        if b == IAC:
            if i + 1 >= len(buf):
                break                       # truncated command: wait for the rest
            c = buf[i + 1]
            if c == IAC:
                out.append(IAC); i += 2; continue
            if c in (IP, BRK):
                out += CTRL_C; i += 2; continue           # telnet Ctrl-C -> interrupt
            if c in (DO, DONT, WILL, WONT):
                if i + 2 >= len(buf):
                    break
                i += 3; continue
            if c == SB:
                j = buf.find(bytes([IAC, SE]), i)
                if j < 0:
                    break
                i = j + 2; continue
            i += 2; continue
        if b == 13:                          # telnet Enter: CR LF or CR NUL -> CR
            out.append(13)
            i += 2 if i + 1 < len(buf) and buf[i + 1] in (0, 10) else 1
            continue
        if b == 0:
            i += 1; continue
        if b == 3:                           # raw Ctrl-C (character mode)
            out += CTRL_C; i += 1; continue
        out.append(b); i += 1
    state['pending'] = bytes(buf[i:])
    return bytes(out)


def session(conn, args):
    conn.sendall(bytes([IAC, WILL, ECHO, IAC, WILL, SGA, IAC, DO, SGA]))
    banner = (f"\r\n[gdb-telnet] gdb -> QEMU gdbstub :{args.stub}  elf={args.elf}\r\n"
              "[gdb-telnet] cible EN MARCHE (continue &). Ctrl-C = stop, go = repart, "
              "quit = ferme (QEMU reprend)\r\n"
              "[gdb-telnet] panneau osmocom charge : tape  help_osmo\r\n\r\n")
    conn.sendall(banner.encode())
    pid, fd = pty.fork()
    if pid == 0:
        os.environ['TERM'] = 'vt100'
        # readline without bracketed-paste: otherwise each prompt is wrapped in
        # ESC[?2004h/l sequences, unreadable in a bare telnet.
        rc = '/tmp/gdb-telnet.inputrc'
        try:
            with open(rc, 'w') as f:
                f.write('set enable-bracketed-paste off\nset bell-style none\n')
            os.environ['INPUTRC'] = rc
        except OSError:
            pass
        cmd = [args.gdb, '-q', '-nx',
               '-iex', 'set pagination off', '-iex', 'set confirm off',
               '-iex', 'set height 0', '-iex', 'set width 0', '-iex', 'set style enabled off',
               '-ex', 'set architecture armv5te',
               '-ex', f'target remote 127.0.0.1:{args.stub}',
               '-ex', 'continue &']
        # osmocom panel (tools/cmd.gdb, generated by gdb_cmd.sh): sourced automatically
        tools = os.path.dirname(os.path.abspath(__file__))
        panel = os.path.join(tools, 'cmd.gdb')
        if os.path.isfile(panel):
            # cmd.gdb looks for demo-layer1.py here first: the path frozen at
            # its generation may be that of a build rootfs.
            os.environ['OSMO_GDB_TOOLS'] = tools
            cmd += ['-x', panel]
        if args.elf:
            cmd.append(args.elf)
        try:
            os.execvp(cmd[0], cmd)
        except OSError as e:
            os.write(2, f"exec {cmd[0]} : {e}\n".encode()); os._exit(127)
    state = {'pending': b''}
    try:
        while True:
            r, _, _ = select.select([conn, fd], [], [], 1.0)
            if conn in r:
                try:
                    data = conn.recv(4096)
                except OSError:
                    break
                if not data:
                    break
                if b'\x03' in data or bytes([IAC, IP]) in data or bytes([IAC, BRK]) in data:
                    # real SIGINT to gdb: breaks a loop (trace_*, every) or a
                    # foreground execution; the following `interrupt` stops the
                    # target if it was running in the background (continue &).
                    try:
                        os.killpg(os.getpgid(pid), signal.SIGINT)
                    except OSError:
                        pass
                out = filter_telnet(data, state)
                if out:
                    os.write(fd, out)
            if fd in r:
                try:
                    data = os.read(fd, 4096)
                except OSError:
                    break
                if not data:
                    break
                conn.sendall(data)
            try:
                if os.waitpid(pid, os.WNOHANG)[0] == pid:
                    pid = 0; break
            except ChildProcessError:
                pid = 0; break
    finally:
        if pid:
            # detach first: the target resumes cleanly even if it was stopped
            try:
                os.write(fd, b'\x03')
                time.sleep(0.2)
                os.write(fd, b'detach\nquit\n')
                for _ in range(10):
                    if os.waitpid(pid, os.WNOHANG)[0] == pid:
                        pid = 0; break
                    time.sleep(0.1)
            except OSError:
                pass
            if pid:
                try:
                    os.kill(pid, signal.SIGKILL); os.waitpid(pid, 0)
                except OSError:
                    pass
        try:
            os.close(fd)
        except OSError:
            pass


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument('--port', type=int, default=44444, help='port telnet (defaut 44444)')
    ap.add_argument('--bind', default='127.0.0.1', help="adresse d'ecoute (defaut 127.0.0.1)")
    ap.add_argument('--stub', type=int, default=1234, help='port du gdbstub QEMU (defaut 1234)')
    ap.add_argument('--elf', default=os.environ.get('CALYPSO_GDB_ELF', ''), help='ELF du firmware (symboles)')
    ap.add_argument('--gdb', default='gdb-multiarch')
    args = ap.parse_args()
    signal.signal(signal.SIGCHLD, signal.SIG_DFL)
    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind((args.bind, args.port)); srv.listen(2)
    log(f"ecoute telnet {args.bind}:{args.port} -> gdbstub :{args.stub} ({args.gdb}, elf={args.elf or '-'})")
    while True:
        conn, peer = srv.accept()
        log(f"session {peer[0]}:{peer[1]}")
        try:
            session(conn, args)
        except Exception as e:
            log(f"session : {e}")
        finally:
            try:
                conn.close()
            except OSError:
                pass
            log("session fermee, gdb termine, QEMU reprend")


if __name__ == '__main__':
    main()

3.72 https://github.com/bbaranoff/qosmo/blob/main/tools/gdb_cmd.sh

25766 octets, 788 lignes → 778 lignes (2 groupes compactés)

#!/bin/bash
# =============================================================================
#  gdb_cmd.sh — generates cmd.gdb: the gdb command panel of the osmocom bench
# =============================================================================
#  ./gdb_cmd.sh              writes tools/cmd.gdb (next to this script)
#  ./gdb_cmd.sh --install    same + adds `source .../cmd.gdb` to ~/.gdbinit
#  ./gdb_cmd.sh --out FILE   writes elsewhere
#
#  cmd.gdb is sourced AUTOMATICALLY by the telnet console (tools/gdb-telnet.py,
#  `telnet localhost 44444`) and by gdb -x. Inside: `help_osmo` lists everything.
#  The commands read the osmocom-bb firmware THROUGH ITS SYMBOLS (l1s, dsp_api,
#  fbs, rxnb, afc_state...): the unstripped ELF (layer1.highram.elf) is needed,
#  the one -kernel loads and the launcher hands to gdb.
# -----------------------------------------------------------------------------
set -eu
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUT="$HERE/cmd.gdb"; INSTALL=0
while [ $# -gt 0 ]; do
    case "$1" in
        --install) INSTALL=1 ;;
        --out) OUT="$2"; shift ;;
        -h|--help) sed -n '2,15p' "$0"; exit 0 ;;
        *) echo "option inconnue : $1" >&2; exit 2 ;;
    esac; shift
done

cat > "$OUT" <<'GDB'
# cmd.gdb — osmocom panel for gdb (generated by tools/gdb_cmd.sh, do not edit)
# Type `help_osmo` for the list. The help texts are in French, short, and read
# the symbols of the osmocom-bb firmware (layer1.highram.elf).
set pagination off
set confirm off
set print pretty on
set height 0
set width 0
set $API = 0xffd00000

# ── dsp_api / l1_environment.h constants ────────────────────────────────────
set $B_BLUD = 15
set $B_SCH_CRC = 8
set $B_FIRE0 = 5
set $B_FIRE1 = 6
set $B_TASK_ABORT = 15

# =============================================================================
#  EXECUTION
# =============================================================================
define go
  continue &
end
document go
go — reprend la cible SANS bloquer le prompt (continue &). Ctrl-C = stop.
end

define stop
  interrupt
end
document stop
stop — arrete l'ARM (equivalent de Ctrl-C). Puis regs, bt10, fn, ndb...
end

define regs
  info registers
end
document regs
regs — tous les registres ARM (cible arretee).
end

define pcx
  x/8i $pc
end
document pcx
pcx — 8 instructions a partir de $pc.
end

define bt10
  bt 10
end
document bt10
bt10 — pile d'appels, 10 niveaux.
end

define si1
  stepi
  x/1i $pc
end
document si1
si1 — une instruction, affiche la suivante.
end

define ni1
  nexti
  x/1i $pc
end
document ni1
ni1 — une instruction sans entrer dans les appels.
end

define frame_step
  tbreak tdma_sched_execute
  continue
  fn
end
document frame_step
frame_step — laisse tourner jusqu'a la PROCHAINE trame TDMA (tdma_sched_execute) puis s'arrete et affiche fn.
end

# =============================================================================
#  TIME / SCHEDULER
# =============================================================================
define fn
  printf "fn=%u  (t1=%u t2=%u t3=%u)   next fn=%u   tpu_offset=%u corr=%d\n", l1s.current_time.fn, l1s.current_time.t1, l1s.current_time.t2, l1s.current_time.t3, l1s.next_time.fn, l1s.tpu_offset, l1s.tpu_offset_correction
end
document fn
fn — numero de trame courant de la L1 (l1s.current_time), suivant, et offset TPU.
end

define l1
  fn
  printf "cell   : arfcn=%u bsic=%u  fn_offset=%u  time_align=%u\n", l1s.serving_cell.arfcn, l1s.serving_cell.bsic, l1s.serving_cell.fn_offset, l1s.serving_cell.time_alignment
  printf "modes  : fb.mode=%u  pm.mode=%u  dedicated.type=%d tn=%u tsc=%u h=%u\n", l1s.fb.mode, l1s.pm.mode, l1s.dedicated.type, l1s.dedicated.tn, l1s.dedicated.tsc, l1s.dedicated.h
  printf "radio  : ta=%d tx_power=%u tch_mode=%u audio=%u  compl=0x%08x\n", l1s.ta, l1s.tx_power, l1s.tch_mode, l1s.audio_mode, l1s.scheduled_compl
  pages
end
document l1
l1 — resume de l'etat L1 : temps, cellule servante, modes FB/PM/dedie, TA, puissance, pages DSP.
end

define cell
  p l1s.serving_cell
end
document cell
cell — la cellule servante (arfcn, bsic, fn_offset, time_alignment, rxlev...).
end

define neigh
  p l1s.neigh_cell
  printf "neigh_pm : n=%u running=%u pos=%u\n", l1s.neigh_pm.n, l1s.neigh_pm.running, l1s.neigh_pm.pos
end
document neigh
neigh — les 6 voisines connues de la L1 et l'etat des mesures voisines.
end

define sched
  p l1s.tdma_sched
end
document sched
sched — l'ordonnanceur TDMA (les items programmes pour les prochaines trames).
end

define mframe
  p l1s.mframe_sched
end
document mframe
mframe — l'ordonnanceur multitrame (BCCH/CCCH/SDCCH/TCH actifs).
end

define compl
  printf "scheduled_compl = 0x%08x  (bit0 FB, 1 RACH, 2 TX_NB, 3 TCH, 4 TX_SB?, voir l1s.completion[])\n", l1s.scheduled_compl
end
document compl
compl — masque des completions L1 en attente (l1s.scheduled_compl).
end

define arfcn
  printf "rf_arfcn = %u\n", rf_arfcn
end
document arfcn
arfcn — ARFCN courant du frontal radio.
end

define afc
  p afc_state
end
document afc
afc — etat de l'AFC (boucle de correction de frequence).
end

# =============================================================================
#  FB / SB (sync) — what L1 has seen
# =============================================================================
define fbs
  p fbs
end
document fbs
fbs — etat complet de l'acquisition FB/SB (fbs.mon : bsic, time, toa, pm, angle, snr, attempt ; fbs.req).
end

define mon
  printf "mon : bsic=%u  toa=%d pm=%d angle=%d snr=%u  attempt=%d  fn_report=%u  time fn=%u\n", fbs.mon.bsic, (short)fbs.mon.toa, (short)fbs.mon.pm, (short)fbs.mon.angle, fbs.mon.snr, fbs.mon.attempt, fbs.mon.fnr_report, fbs.mon.time.fn
end
document mon
mon — le dernier resultat FB/SB tel que l'ARM l'a lu (fbs.mon).
end

# =============================================================================
#  MAILBOX ARM <-> DSP (dsp_api: ndb / db_r / db_w)
# =============================================================================
define pages
  printf "pages  : r_page=%d w_page=%d r_page_used=%d frame_ctr=%d   ndb.d_dsp_page=0x%04x\n", dsp_api.r_page, dsp_api.w_page, dsp_api.r_page_used, dsp_api.frame_ctr, dsp_api.ndb->d_dsp_page & 0xffff
  printf "         db_r=%p  db_w=%p  ndb=%p  param=%p\n", dsp_api.db_r, dsp_api.db_w, dsp_api.ndb, dsp_api.param
end
document pages
pages — pages de la double mise en tampon MCU<->DSP (r_page/w_page vus par l'ARM, d_dsp_page vu par le DSP).
end

define err
  set $e = dsp_api.ndb->d_error_status & 0xffff
  printf "d_error_status = 0x%04x", $e
  if $e & 0x0008
    printf "  DSP_ERR_DMA_PROG"
  end
  if $e & 0x0010
    printf "  DSP_ERR_DMA_TASK"
  end
  printf "\n"
end
document err
err — d_error_status du DSP, decode (8 = DMA_PROG, 0x10 = DMA_TASK ; temoin, pas un blocage).
end

define taskname
  if $arg0 == 0
    printf "-"
  end
  if $arg0 == 1
    printf "PM"
  end
  if $arg0 == 5
    printf "FB"
  end
  if $arg0 == 6
    printf "SB"
  end
  if $arg0 == 10
    printf "RACH"
  end
  if $arg0 == 19
    printf "NBS"
  end
  if $arg0 == 21
    printf "NP"
  end
  if $arg0 == 24
    printf "ALLC"
  end
  if $arg0 == 33
    printf "CHECKSUM"
  end
end

define tasks
  set $td = dsp_api.db_w->d_task_d & 0xffff
  set $tu = dsp_api.db_w->d_task_u & 0xffff
  set $tm = dsp_api.db_w->d_task_md & 0xffff
  set $tr = dsp_api.db_w->d_task_ra & 0xffff
  printf "db_w (ARM->DSP) : task_d=%u(", $td
  taskname $td
  printf ") burst_d=%u  task_u=%u(", dsp_api.db_w->d_burst_d & 0xffff, $tu
  taskname $tu
  printf ") burst_u=%u  task_md=%u(", dsp_api.db_w->d_burst_u & 0xffff, $tm
  taskname $tm
  printf ")  task_ra=%u\n", $tr
  printf "                 d_fn=0x%04x ctrl_abb=0x%04x afc=%d(0x%04x) ctrl_system=0x%04x power_ctl=0x%04x ctrl_tch=0x%04x\n", dsp_api.db_w->d_fn & 0xffff, dsp_api.db_w->d_ctrl_abb & 0xffff, (short)(dsp_api.db_w->d_afc & 0xffff), dsp_api.db_w->d_afc & 0xffff, dsp_api.db_w->d_ctrl_system & 0xffff, dsp_api.db_w->d_power_ctl & 0xffff, dsp_api.db_w->d_ctrl_tch & 0xffff
  set $rd = dsp_api.db_r->d_task_d & 0xffff
  printf "db_r (DSP->ARM) : task_d=%u(", $rd
  taskname $rd
  printf ") burst_d=%u task_u=%u burst_u=%u task_md=%u task_ra=%u\n", dsp_api.db_r->d_burst_d & 0xffff, dsp_api.db_r->d_task_u & 0xffff, dsp_api.db_r->d_burst_u & 0xffff, dsp_api.db_r->d_task_md & 0xffff, dsp_api.db_r->d_task_ra & 0xffff
end
document tasks
tasks — les taches programmees par l'ARM (page W) et celles que le DSP a rendues (page R), avec leur nom (FB, SB, PM, NBS, ALLC...).
end

define dbw
  p *dsp_api.db_w
end
document dbw
dbw — la page d'ecriture ARM->DSP complete (d_task_*, d_fn, d_ctrl_abb, a_a5fn, d_afc, d_ctrl_system...).
end

define dbr
  p *dsp_api.db_r
end
document dbr
dbr — la page de lecture DSP->ARM complete (taches rendues, a_serv_demod, a_pm, a_sch).
end

define sync
  printf "ndb  : d_fb_det=%u d_fb_mode=%u  a_sync_demod TOA=%d PM=%d ANGLE=%d SNR=%u\n", dsp_api.ndb->d_fb_det & 0xffff, dsp_api.ndb->d_fb_mode & 0xffff, (short)(dsp_api.ndb->a_sync_demod[0] & 0xffff), (short)(dsp_api.ndb->a_sync_demod[1] & 0xffff), (short)(dsp_api.ndb->a_sync_demod[2] & 0xffff), dsp_api.ndb->a_sync_demod[3] & 0xffff
  printf "db_r : a_serv_demod TOA=%d PM=%d(>>3=%d) ANGLE=%d SNR=%u\n", (short)(dsp_api.db_r->a_serv_demod[0] & 0xffff), (short)(dsp_api.db_r->a_serv_demod[1] & 0xffff), (short)(dsp_api.db_r->a_serv_demod[1] & 0xffff) >> 3, (short)(dsp_api.db_r->a_serv_demod[2] & 0xffff), dsp_api.db_r->a_serv_demod[3] & 0xffff
end
document sync
sync — resultats de synchro : d_fb_det + a_sync_demod (NDB, chemin FB) et a_serv_demod (page R, chemin SB/NB).
end

define sb
  set $s0 = dsp_api.db_r->a_sch[0] & 0xffff
  set $sbw = (dsp_api.db_r->a_sch[3] & 0xffff) | ((dsp_api.db_r->a_sch[4] & 0xffff) << 16)
  printf "a_sch = %04x %04x %04x %04x %04x   ", $s0, dsp_api.db_r->a_sch[1] & 0xffff, dsp_api.db_r->a_sch[2] & 0xffff, dsp_api.db_r->a_sch[3] & 0xffff, dsp_api.db_r->a_sch[4] & 0xffff
  if $s0 & (1 << $B_SCH_CRC)
    printf "CRC FAUX (B_SCH_CRC)"
  else
    printf "CRC ok"
  end
  if $s0 & (1 << $B_BLUD)
    printf "  BLUD"
  end
  set $t1 = (($sbw >> 23) & 1) | (($sbw >> 7) & 0x1fe) | (($sbw << 9) & 0x600)
  set $t2 = ($sbw >> 18) & 0x1f
  set $t3p = (($sbw >> 24) & 1) | (($sbw >> 15) & 6)
  set $t3 = $t3p * 10 + 1
  printf "\nSB 0x%08x : BSIC=%u  t1=%u t2=%u t3=%u  (fn = 51*26*t1 + 51*((t3-t2+26)%%26) + t3 = %u)\n", $sbw, ($sbw >> 2) & 0x3f, $t1, $t2, $t3, 51*26*$t1 + 51*(($t3 - $t2 + 26) % 26) + $t3
end
document sb
sb — decode a_sch comme prim_fbsb.c : CRC, mot SB, BSIC, t1/t2/t3 et fn.
end

define cd
  set $c0 = dsp_api.ndb->a_cd[0] & 0xffff
  printf "a_cd[0]=0x%04x", $c0
  if $c0 & (1 << $B_BLUD)
    printf " BLUD"
  end
  printf "  fire_crc=%u  biterr=%u\n", ($c0 & ((1 << $B_FIRE1) | (1 << $B_FIRE0))) >> $B_FIRE0, dsp_api.ndb->a_cd[2] & 0xffff
  printf "a_cd[1]=0x%04x  data[3..14] = ", dsp_api.ndb->a_cd[1] & 0xffff
  set $i = 3
  while $i < 15
    printf "%04x ", dsp_api.ndb->a_cd[$i] & 0xffff
    set $i = $i + 1
  end
  printf "\n"
end
document cd
cd — le bloc de donnees decode (a_cd : etat BLUD, CRC Fire, erreurs bits, 23 octets du bloc L2/SI).
end

define pm
  printf "a_pm = %d %d %d   (dBm8 : %d)\n", (short)(dsp_api.db_r->a_pm[0] & 0xffff), (short)(dsp_api.db_r->a_pm[1] & 0xffff), (short)(dsp_api.db_r->a_pm[2] & 0xffff), (short)(dsp_api.db_r->a_pm[0] & 0xffff) >> 3
end
document pm
pm — les 3 mesures de puissance rendues par le DSP (page R).
end

define ndb
  err
  sync
  printf "d_dsp_state=0x%04x  bg enable=0x%04x state=0x%04x abort=0x%04x  d_tch_mode=0x%04x  d_a5mode=0x%04x\n", dsp_api.ndb->d_dsp_state & 0xffff, dsp_api.ndb->d_background_enable & 0xffff, dsp_api.ndb->d_background_state & 0xffff, dsp_api.ndb->d_background_abort & 0xffff, dsp_api.ndb->d_tch_mode & 0xffff, dsp_api.ndb->d_a5mode & 0xffff
  printf "d_dsp_page=0x%04x  version=%04x/%04x  d_rach=0x%04x\n", dsp_api.ndb->d_dsp_page & 0xffff, dsp_api.ndb->d_version_number1 & 0xffff, dsp_api.ndb->d_version_number2 & 0xffff, dsp_api.ndb->d_rach & 0xffff
end
document ndb
ndb — les cellules NDB qui comptent : erreur, synchro, etat DSP, taches de fond, page, version, RACH.
end

define dsp
  pages
  tasks
  ndb
  sb
end
document dsp
dsp — vue d'ensemble de la mailbox : pages + taches + ndb + sb. La commande a taper d'abord.
end

define apiram
  x/$arg1hx ($API + 2*($arg0 - 0x800))
end
document apiram
apiram OFF N — N mots de la memoire API vus de l'ARM, a partir du mot DSP OFF (0xffd00000 = mot 0x0800 ; ex : apiram 0x800 32 = page 0 ; apiram 0x8f8 8 = d_fb_det...).
end

define apiw
  set *(unsigned short *)($API + 2*($arg0 - 0x800)) = $arg1
  printf "api[0x%04x] <- 0x%04x\n", $arg0, $arg1
end
document apiw
apiw OFF VAL — ecrit le mot DSP OFF (adresse cote DSP, ex 0x08f8) dans la memoire API partagee.
end

define rxnb
  p rxnb
end
document rxnb
rxnb — etat de la reception des bursts normaux (rxnb : meas[4], msg en cours).
end

# =============================================================================
#  FORCING / FAKE RESULTS  (write into the mailbox: use with the target STOPPED)
# =============================================================================
define fake_fb
  set dsp_api.ndb->d_fb_det = 1
  set dsp_api.ndb->a_sync_demod[⟨1⟩] = $arg⟨2⟩  ×4
    ⟨⟩ = (0,0) (1,1) (2,2) (3,3)
  printf "FB force : d_fb_det=1 toa=%d pm=%d angle=%d snr=%d\n", $arg0, $arg1, $arg2, $arg3
  sync
end
document fake_fb
fake_fb TOA PM ANGLE SNR — fait croire a l'ARM que le DSP a detecte un FB (ex : fake_fb 48 5511 -2 200).
end

define fake_sb
  set $t1 = $arg1
  set $t2 = $arg2
  set $t3p = ($arg3 - 1) / 10
  set $w = (($arg0 & 0x3f) << 2) | (($t1 & 1) << 23) | (($t1 & 0x1fe) << 7) | (($t1 & 0x600) >> 9) | (($t2 & 0x1f) << 18) | (($t3p & 1) << 24) | (($t3p & 6) << 15)
  set dsp_api.db_r->a_sch[0] = 0
  set dsp_api.db_r->a_sch[3] = $w & 0xffff
  set dsp_api.db_r->a_sch[4] = ($w >> 16) & 0xffff
  printf "SB force : 0x%08x  bsic=%u t1=%u t2=%u t3=%u\n", $w, $arg0, $t1, $t2, $arg3
  sb
end
document fake_sb
fake_sb BSIC T1 T2 T3 — compose un mot SB valide (CRC ok) dans a_sch pour que l'ARM decode ce BSIC et ce temps (ex : fake_sb 7 100 5 11).
end

define sb_force
  set dsp_api.db_r->a_sch[0] = dsp_api.db_r->a_sch[0] & ~(1 << $B_SCH_CRC)
  printf "B_SCH_CRC efface : l'ARM decodera a_sch[3..4] tel quel\n"
  sb
end
document sb_force
sb_force — efface le bit CRC de a_sch[0] : l'ARM accepte le mot SB present, meme si le DSP l'a marque faux.
end

define si_force
  set dsp_api.ndb->a_cd[0] = (1 << $B_BLUD)
  set dsp_api.ndb->a_cd[2] = 0
  printf "a_cd force : BLUD, fire_crc=0, biterr=0 -> l'ARM prend le bloc present comme bon\n"
  cd
end
document si_force
si_force — force l'acceptation du bloc a_cd present (BLUD + CRC Fire ok + 0 erreur) : l'ARM remonte ce SI/bloc a la L2.
end

define fake_cd
  set dsp_api.ndb->a_cd[⟨1⟩] = $arg⟨2⟩  ×10
    ⟨⟩ = (3,0) (4,1) (5,2) (6,3) (7,4) (8,5) (9,6) (10,7) (11,8) (12,9)
  si_force
end
document fake_cd
fake_cd W3 W4 ... W12 — pose 10 mots de donnees dans a_cd[3..12] puis si_force (les 2 derniers mots restent).
end

define fake_err
  set dsp_api.ndb->d_error_status = $arg0
  err
end
document fake_err
fake_err VAL — ecrit d_error_status (ex : fake_err 0 pour effacer DMA_PROG).
end

define fake_task
  set dsp_api.db_w->d_task_d = $arg0
  printf "db_w->d_task_d <- %u\n", $arg0
  tasks
end
document fake_task
fake_task N — programme la tache DL N dans la page W (5=FB 6=SB 1=PM 24=ALLC 19=NBS 21=NP 10=RACH).
end

define fake_page
  set dsp_api.ndb->d_dsp_page = $arg0
  pages
end
document fake_page
fake_page VAL — force d_dsp_page (bit0 = page W du DSP, bit1 = ...) pour tester la selection de page.
end

define abort_dsp
  set dsp_api.db_w->d_ctrl_system = dsp_api.db_w->d_ctrl_system | (1 << $B_TASK_ABORT)
  printf "B_TASK_ABORT pose dans d_ctrl_system (page W)\n"
end
document abort_dsp
abort_dsp — pose B_TASK_ABORT : demande au DSP d'abandonner sa tache en cours (ce que fait l1s_dsp_abort).
end

# =============================================================================
#  BREAKPOINTS / WATCH
# =============================================================================
define bfb
  break l1s_fbdet_resp
end
document bfb
bfb — s'arrete a chaque reponse de detection FB (l1s_fbdet_resp). Puis : mon, sync, fn.
end

define bsb
  break l1s_sbdet_resp
end
document bsb
bsb — s'arrete a chaque reponse SB (l1s_sbdet_resp). Puis : sb, mon.
end

define bfbsb
  break l1s_fbsb_req
end
document bfbsb
bfbsb — s'arrete quand la L1 recoit une demande FBSB du mobile (l1s_fbsb_req).
end

define bsync
  break synchronize_tdma
end
document bsync
bsync — s'arrete au recalage TDMA (synchronize_tdma), juste apres un FB/SB accepte.
end

define bnb
  break l1s_nb_resp
end
document bnb
bnb — s'arrete a chaque bloc normal recu (l1s_nb_resp). Puis : cd, rxnb.
end

define bpm
  break l1s_pm_resp
end
document bpm
bpm — s'arrete a chaque mesure de puissance rendue (l1s_pm_resp). Puis : pm.
end

define breset
  break l1s_reset
end
document breset
breset — s'arrete a chaque reset L1 (l1s_reset) : qui remet la L1 a zero et quand.
end

define bcompl
  break l1s_compl_sched
end
document bcompl
bcompl — s'arrete a chaque completion programmee (l1s_compl_sched) : voir compl.
end

define babort
  break l1s_dsp_abort
end
document babort
babort — s'arrete quand l'ARM abandonne la tache DSP (l1s_dsp_abort).
end

define bframe
  break tdma_sched_execute
end
document bframe
bframe — s'arrete a CHAQUE trame TDMA (tdma_sched_execute). Lourd : utiliser avec trace_frames.
end

define bl2
  break l1_queue_for_l2
end
document bl2
bl2 — s'arrete quand la L1 remonte un message a la L2 (SI, bloc, mesure).
end

define bafc
  break afc_input
end
document bafc
bafc — s'arrete a chaque entree AFC (afc_input freq_diff).
end

define wfb
  watch dsp_api.ndb->d_fb_det
end
document wfb
wfb — point d'arret sur ECRITURE de d_fb_det (vue ARM ; les ecritures du DSP ne passent pas par le bus ARM, seules celles de l'ARM declenchent).
end

define werr
  watch dsp_api.ndb->d_error_status
end
document werr
werr — point d'arret sur ecriture ARM de d_error_status.
end

define wtask
  watch dsp_api.db_w->d_task_d
end
document wtask
wtask — point d'arret sur programmation d'une tache DL par l'ARM.
end

define bl
  info breakpoints
end
document bl
bl — liste des points d'arret/surveillance.
end

define bclr
  delete
  printf "tous les points d'arret supprimes\n"
end
document bclr
bclr — supprime tous les points d'arret.
end

# =============================================================================
#  TRACE LOOPS  (target in the foreground: Ctrl-C interrupts the loop)
# =============================================================================
define trace_frames
  set $n = $arg0
  tbreak tdma_sched_execute
  continue
  while $n > 0
    fn
    tasks
    set $n = $n - 1
    if $n > 0
      tbreak tdma_sched_execute
      continue
    end
  end
  printf "-- %d trames tracees, cible ARRETEE (go pour reprendre)\n", $arg0
end
document trace_frames
trace_frames N — s'arrete N trames de suite (tdma_sched_execute) et affiche fn + taches a chaque fois. Ctrl-C pour couper. go apres.
end

define trace_fb
  set $n = $arg0
  while $n > 0
    tbreak l1s_fbdet_resp
    continue
    fn
    sync
    set $n = $n - 1
  end
  printf "-- %d reponses FB tracees, cible ARRETEE (go pour reprendre)\n", $arg0
end
document trace_fb
trace_fb N — attend N reponses FB (l1s_fbdet_resp), affiche fn + sync a chacune.
end

define trace_sb
  set $n = $arg0
  while $n > 0
    tbreak l1s_sbdet_resp
    continue
    fn
    sb
    set $n = $n - 1
  end
  printf "-- %d reponses SB tracees, cible ARRETEE (go pour reprendre)\n", $arg0
end
document trace_sb
trace_sb N — attend N reponses SB (l1s_sbdet_resp), affiche fn + decodage a_sch a chacune.
end

define trace_nb
  set $n = $arg0
  while $n > 0
    tbreak l1s_nb_resp
    continue
    fn
    cd
    set $n = $n - 1
  end
  printf "-- %d blocs traces, cible ARRETEE (go pour reprendre)\n", $arg0
end
document trace_nb
trace_nb N — attend N blocs normaux (l1s_nb_resp), affiche fn + a_cd a chacun.
end

define trace_l2
  set $n = $arg0
  while $n > 0
    tbreak l1_queue_for_l2
    continue
    fn
    printf "msg -> L2 : "
    p *(struct msgb *)$r0
    set $n = $n - 1
  end
  printf "-- %d messages L2 traces, cible ARRETEE (go pour reprendre)\n", $arg0
end
document trace_l2
trace_l2 N — attend N messages remontes a la L2 (l1_queue_for_l2) et les affiche.
end

define mon_dsp
  set $n = $arg0
  while $n > 0
    tbreak tdma_sched_execute
    continue
    printf "==== trame %u ====\n", l1s.current_time.fn
    dsp
    set $n = $n - 1
  end
  printf "-- %d echantillons, cible ARRETEE (go pour reprendre)\n", $arg0
end
document mon_dsp
mon_dsp N — N trames de suite, la vue `dsp` complete a chacune (pages, taches, ndb, sb).
end

define every
  set $n = $arg0
  while $n > 0
    tbreak tdma_sched_execute
    continue
    $arg1
    set $n = $n - 1
  end
  printf "-- fini, cible ARRETEE (go pour reprendre)\n"
end
document every
every N CMD — execute la commande CMD a chacune des N prochaines trames (ex : every 20 sync).
end

# =============================================================================
#  HELP
# =============================================================================
define help_osmo
  printf "\n============ PANNEAU OSMOCOM (cmd.gdb) ============\n"
  printf "EXECUTION       go            reprend sans bloquer (continue &)\n"
  printf "                stop          arrete l'ARM (= Ctrl-C)\n"
  printf "                regs / pcx / bt10 / si1 / ni1   registres, code a $pc, pile, pas a pas\n"
  printf "                frame_step    tourne jusqu'a la prochaine trame TDMA puis s'arrete\n"
  printf "TEMPS           fn            trame courante, suivante, tpu_offset\n"
  printf "                l1            resume L1 : temps, cellule, modes, TA, pages\n"
  printf "                cell / neigh / sched / mframe / compl / arfcn / afc\n"
  printf "SYNCHRO         fbs / mon     etat FB/SB vu par l'ARM (fbs, fbs.mon)\n"
  printf "                sync          d_fb_det + a_sync_demod + a_serv_demod\n"
  printf "                sb            a_sch decode : CRC, BSIC, t1/t2/t3, fn\n"
  printf "MAILBOX         dsp           TOUT : pages + tasks + ndb + sb\n"
  printf "                pages / tasks / dbw / dbr / ndb / err / cd / pm / rxnb\n"
  printf "                apiram OFF N  mots API bruts (adresse cote DSP)     apiw OFF VAL  ecrit un mot\n"
  printf "FORCAGES        fake_fb TOA PM ANGLE SNR   un FB detecte\n"
  printf "                fake_sb BSIC T1 T2 T3      un SB valide            sb_force   efface le CRC faux\n"
  printf "                si_force                   accepte le bloc a_cd    fake_cd W3..W12  pose 10 mots + si_force\n"
  printf "                fake_err V / fake_task N / fake_page V / abort_dsp\n"
  printf "POINTS D'ARRET  bfb bsb bfbsb bsync bnb bpm breset bcompl babort bframe bl2 bafc\n"
  printf "                wfb werr wtask (watch)      bl liste     bclr efface tout\n"
  printf "BOUCLES         trace_frames N / trace_fb N / trace_sb N / trace_nb N / trace_l2 N\n"
  printf "                mon_dsp N     la vue dsp a chaque trame, N fois\n"
  printf "                every N CMD   CMD a chaque trame, N fois (ex : every 20 sync)\n"
  printf "                Ctrl-C coupe une boucle ; la cible reste ARRETEE : go pour reprendre.\n"
  printf "DEMO            demo          la main sur le firmware : arret, etat, une trame, printf, reprise\n"
  printf "                demo etat | trame | hello | go      les etapes une a une\n"
  printf "AIDE            help_osmo     ceci      help CMD   le detail d'une commande\n"
  printf "Regle : les lectures (fn, dsp...) exigent la cible ARRETEE (stop) ; go ensuite.\n\n"
end
document help_osmo
help_osmo — liste toutes les commandes du panneau osmocom, avec leur role et leur usage.
end

printf "[cmd.gdb] panneau osmocom charge : help_osmo\n"
GDB
# The "hands on the firmware" demo (tools/demo-layer1.py, Python gdb): demo command.
# The demo path is NOT baked in: when build-iso (52-qemu.sh) regenerates cmd.gdb
# in the image rootfs, the generation directory is /var/tmp/iso-build-NNN/rootfs/
# opt/GSM/qosmo/tools, which does not exist on the installed machine. cmd.gdb
# therefore looks for the file: where the telnet console says the tools are
# (OSMO_GDB_TOOLS, set by gdb-telnet.py), then the generation path stripped of a
# possible rootfs prefix (GDB_CMD_HERE forces it), then the installed tree.
BAKED="${GDB_CMD_HERE:-$HERE}"
case "$BAKED" in */rootfs/opt/GSM/*) BAKED="/opt/GSM/${BAKED#*/rootfs/opt/GSM/}" ;; esac
cat >> "$OUT" <<GDB
python
import gdb, os
for _d in (os.environ.get("OSMO_GDB_TOOLS", ""), "$BAKED", "/opt/GSM/qosmo/tools"):
    _f = os.path.join(_d, "demo-layer1.py") if _d else ""
    if _f and os.path.isfile(_f):
        gdb.execute("source " + _f)
        break
else:
    gdb.write("[cmd.gdb] demo-layer1.py introuvable (OSMO_GDB_TOOLS=%r) : commande demo absente\\n"
              % os.environ.get("OSMO_GDB_TOOLS", ""))
end
GDB

echo "ecrit : $OUT ($(wc -l < "$OUT") lignes, $(grep -c '^define ' "$OUT") commandes)"
if [ "$INSTALL" = 1 ]; then
    touch ~/.gdbinit
    grep -qF "source $OUT" ~/.gdbinit || printf 'source %s\n' "$OUT" >> ~/.gdbinit
    echo "~/.gdbinit : source $OUT"
fi

3.73 https://github.com/bbaranoff/qosmo/blob/main/tools/injecteur_bruit.py

32362 octets, 710 lignes → 710 lignes

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# injecteur_bruit.py - UDP proxy that degrades the DOWNLINK direction between the
# bridge (osmo-operator/pont) and the layer 1 of the emulated phone.
#
# Single source file:
#   /opt/GSM/qosmo/tools/injecteur_bruit.py
# (c54x_exe/run.sh takes it from there, with a fallback on
# c54x_exe/tools/injecteur_bruit.py, a link to this file;
# qosmo/run_modules/38-bruit.sh starts it for the qosmo plan).
#
# WHERE IT SITS
#   dsp target  : pont_dsp.py --dsp-port 6702 -> [injector 127.0.0.1:6702]
#                 -> c54x_exe BSP moved to CALYPSO_BSP_PORT (16702 by default).
#                 Format: 8-byte header [tn, fn BE32, att, 0, 0] then 148
#                 bytes of HARD bits 0/1 (pont/trx.py Trx.run_data;
#                 calypso_bsp.c bsp_trxd_readable).
#                 The BSP learns the address of its uplink peer from the sender
#                 of the downlink: once the injector sits in between, what the
#                 BSP sends back (UL TRXDv0 bursts, if any) arrives here and
#                 leaves UNMODIFIED towards the source (the bridge,
#                 127.0.0.1:5702), from the listening port -- exactly the
#                 address the BSP had before.
#   grgsm target: pont.py -> GSMTAP [injector 127.0.0.1:14730] -> gr-gsm L1 of
#                 QEMU (127.0.0.1:4730, port fixed in calypso_l1_grgsm.c).
#                 The bridge sends there when PONT_GSMTAP_PORT=14730
#                 (pont/config.py; pont/pont.py sets it by itself when
#                 BRUIT_MODE is ber|relais).
#                 Format: 16-byte GSMTAP v2 header then the ALREADY DECODED L2
#                 block (23 bytes) -- NOT bursts, NO soft values.
#                 The SCH (4731, "SCH2") and TCH speech (/dev/shm/calypso_tch_dl)
#                 do not go through here and are not touched.
#
# MODES (downlink only; the uplink is always relayed as is)
#   relais  no alteration: checks that the insertion alone changes nothing.
#   ber     random bit inversion, mean rate --ber. --rafales L: clustered
#           errors (Gilbert-Elliott, bursts of L bits on average, rate
#           --ber-rafale inside a burst), same mean rate.
#           dsp: hard bits inverted (0<->1). grgsm: bits of the L2 bytes
#           inverted AFTER decoding -- not physical (neither the error
#           correcting code nor the CRC protect the block any more); --perte
#           is the realistic model for grgsm.
#   souple  AWGN at --snr-db on the soft values. Neither stream carries any
#           (dsp: hard bits 0/1; grgsm: L2 bytes). On hard bits: antipodal
#           +-1 modulation, AWGN, hard decision -- BER = Q(sqrt(2 snr)),
#           announced at start-up; the output stays 0/1, the only format the
#           BSP can read. If a source sends int8 soft values (TRXD convention:
#           +127 = bit 1), they are noised and kept soft. Refused for grgsm.
#   iq      EXPERIMENTAL. Bits -> GMSK BT=0.3 at 4 samples/symbol (same
#           modulator as calypso_gmsk.c, instant 0.5) + AWGN at --snr-db, sent
#           as interleaved int16 I/Q (BSP IQ_PASSTHROUGH, decimation by 4).
#           BYPASSES the BSP's tuned modulation (sb_moduler for the SCH:
#           instant 0.35, widening 1.0, own noise; gmsk_elargir 0.3 on the
#           NBs -- only the latter is imitated here, --iq-elargir).
#           INCOMPATIBLE with CALYPSO_BSP_STREAM=1 (the default of
#           start-native.sh --dsp): in STREAM the BSP stores the first 148
#           bytes of the packet as bits BEFORE the IQ_PASSTHROUGH branch
#           (calypso_bsp.c bsp_trxd_readable -> bsp_ts0_stocker). Refused in
#           that case, unless --forcer-iq. Requires numpy.
#   --perte P (all modes): one burst/block out of 1/P is erased. dsp: the
#           payload is replaced by random bits (fading: the BSP keeps its pace,
#           a dropped burst would make it wait CALYPSO_BSP_ATTENTE_MS).
#           grgsm: the block is not transmitted (= CRC failure on the L1 side).
#
# DETERMINISM: each packet draws its randomness from a seed derived from
# (--graine, fn, tn): same seed + same frames = same errors, whatever the
# arrival order (except --rafales, whose state follows the arrival order).
# Without --graine, a seed is drawn and PRINTED at start-up so a run can be
# replayed.
#
# Clean stop on SIGTERM/SIGINT (final statistics), SIGUSR1 = statistics right
# away. --verifier: checks the configuration and exits (0 = good).
import argparse
import logging
import math
import os
import random
import selectors
import signal
import socket
import struct
import sys
import time

VERSION = "2026-10-03"
log = logging.getLogger("bruit")

PROFILS = {
    # target: (listen = where the source sends, to = the layer 1)
    "dsp": ("127.0.0.1:6702", "127.0.0.1:16702"),
    "grgsm": ("127.0.0.1:14730", "127.0.0.1:4730"),
}
MODES = ("relais", "ber", "souple", "iq")
MODES_GRGSM = ("relais", "ber")

DSP_HDR = 8              # [tn, fn BE32, att, 0, 0]
NBITS = 148
IQ_MIN_OCTETS = 4 * 146  # calypso_bsp.c threshold to read the payload as I/Q
GSMTAP_TYPE_UM = 0x01
MASQUE64 = (1 << 64) - 1


def _adresse(txt, option):
    hote, sep, port = txt.rpartition(":")
    if not sep or not hote:
        raise argparse.ArgumentTypeError("%s : HOTE:PORT attendu, pas %r" % (option, txt))
    try:
        p = int(port)
    except ValueError:
        raise argparse.ArgumentTypeError("%s : port invalide %r" % (option, port))
    if not 0 < p < 65536:
        raise argparse.ArgumentTypeError("%s : port hors bornes %d" % (option, p))
    return (hote, p)


def _q_gauss(x):
    """Q(x) = P(N(0,1) > x)."""
    return 0.5 * math.erfc(x / math.sqrt(2.0))


def _geometrique(rng, p):
    """Number of trials up to the first success (>= 1), success probability p."""
    if p >= 1.0:
        return 1
    if p <= 0.0:
        return 1 << 62
    u = rng.random()
    return 1 + int(math.log1p(-u) / math.log1p(-p))


class Rafales:
    """Gilbert-Elliott: good state (no error) / bad state (rate q), geometric
    sojourns. Mean burst length L bits, mean rate p."""

    def __init__(self, p, longueur, q):
        self.q = q
        pi_m = p / q                          # fraction of the time in a burst
        self.p_mb = 1.0 / longueur            # bad -> good
        self.p_bm = self.p_mb * pi_m / (1.0 - pi_m) if pi_m < 1.0 else 1.0
        self.mauvais = False
        self.restant = None

    def positions(self, n, rng):
        if self.restant is None:
            self.restant = _geometrique(rng, self.p_bm)
        out = []
        pos = 0
        while pos < n:
            k = min(self.restant, n - pos)
            if self.mauvais:
                out.extend(i for i in range(pos, pos + k) if rng.random() < self.q)
            pos += k
            self.restant -= k
            if self.restant == 0:
                self.mauvais = not self.mauvais
                self.restant = _geometrique(rng, self.p_mb if self.mauvais else self.p_bm)
        return out


def positions_iid(n, p, rng):
    """Positions of the inverted bits, independent with probability p (geometric skip)."""
    if p <= 0.0:
        return []
    if p >= 1.0:
        return list(range(n))
    out = []
    lp = math.log1p(-p)
    i = -1
    while True:
        i += 1 + int(math.log1p(-rng.random()) / lp)
        if i >= n:
            return out
        out.append(i)


class ModulateurGmsk:
    """GMSK BT=0.3 (45.004) at `sps` samples per symbol, like calypso_gmsk.c
    gmsk_moduler(): differential coding d_i = b_i ^ b_{i-1} (b_{-1} = 1),
    alpha_i = 1 - 2 d_i, phase(t) = pi/2 * sum alpha_i q(t - i), sample j at
    instant t = j/sps + decalage. With sps=4 and decalage=0.5, the samples 4k
    (the ones the BSP keeps, decimation by 4) are those of
    gmsk_moduler(..., decalage=0.5)."""

    def __init__(self, np, sps=4, decalage=0.5):
        self.np = np
        self.sps = sps
        # The q(t) table is computed EXACTLY like calypso_gmsk.c q_init():
        # left Riemann sum, step T/16, normalised, then linear interpolation
        # (q_de). A finer integration would give another waveform (q(0) =
        # 0.500 instead of 0.477: up to 0.05 rad apart) and the iq mode would
        # no longer be "the same modulator" as the BSP.
        bt, os_, lg = 0.3, 16, 3
        k = 2.0 * math.pi * bt / math.sqrt(math.log(2.0))
        dt = 1.0 / os_
        tab, acc = [], 0.0
        for i in range(lg * os_ + 1):
            t = -1.5 + i * dt
            tab.append(acc)
            acc += (_q_gauss(k * (t - 0.5)) - _q_gauss(k * (t + 0.5))) * dt
        tab = [v / acc for v in tab]

        def q(t):
            if t <= -1.5:
                return 0.0
            if t >= 1.5:
                return 1.0
            x = (t + 1.5) * os_
            i = int(x)
            f = x - i
            if i >= lg * os_:
                return 1.0
            return tab[i] * (1.0 - f) + tab[i + 1] * f

        # K(n) = q(n/sps + decalage): 0 for t <= -1.5, 1 for t >= 1.5.
        n_lo = math.floor((-1.5 - decalage) * sps) + 1
        n_un = math.ceil((1.5 - decalage) * sps)
        self.n_lo = n_lo
        self.n_un = n_un
        self.k_trans = np.array([q(n / sps + decalage) for n in range(n_lo, n_un)])

    def phase(self, bits):
        np = self.np
        b = np.asarray(bits, dtype=np.int8) & 1
        prec = np.concatenate(([1], b[:-1]))
        alpha = 1.0 - 2.0 * (b ^ prec)
        n = len(b)
        long_ = n * self.sps
        d = np.zeros(long_)
        d[:: self.sps] = alpha
        conv = np.convolve(d, self.k_trans)                # conv[k] <-> j = k + n_lo
        j = np.arange(long_)
        part_trans = conv[j - self.n_lo]
        cumul = np.cumsum(d)
        idx = j - self.n_un
        part_marche = np.where(idx >= 0, cumul[np.clip(idx, 0, None)], 0.0)
        return (math.pi / 2.0) * (part_trans + part_marche)


class Stats:
    def __init__(self):
        self.t0 = time.monotonic()
        self.t_prec = self.t0
        self.dl_prec = 0
        self.dl = 0            # downlink packets received
        self.relayes = 0       # downlink packets sent to the L1
        self.alteres = 0       # packets whose payload was degraded
        self.effaces = 0       # --perte
        self.hors_tn = 0       # outside --tn, relayed as is
        self.autres = 0        # unrecognised format, relayed as is
        self.iq_deja = 0       # payload already in I/Q, relayed as is
        self.iq_produits = 0   # bursts converted to I/Q (iq mode)
        self.souples = 0       # packets with soft values seen
        self.bits = 0          # hard bits examined (ber, souple on hard bits)
        self.inverses = 0      # hard bits changed
        self.ul = 0            # uplink packets relayed to the source
        self.ul_sans_dest = 0  # uplink received before any downlink
        self.err_envoi = 0

    def ligne(self, final=False):
        now = time.monotonic()
        dt = max(now - self.t_prec, 1e-9)
        pps = (self.dl - self.dl_prec) / dt
        self.t_prec, self.dl_prec = now, self.dl
        ber = (self.inverses / self.bits) if self.bits else 0.0
        return ("%s t=%.0fs DL recus=%d relayes=%d alteres=%d effaces=%d hors_tn=%d autres=%d iq_deja=%d"
                " iq_produits=%d souples=%d | bits=%d inverses=%d BER=%.3e | UL relayes=%d sans_dest=%d"
                " | erreurs_envoi=%d | %.0f pq/s"
                % ("BILAN" if final else "STATS", now - self.t0, self.dl, self.relayes, self.alteres,
                   self.effaces, self.hors_tn, self.autres, self.iq_deja, self.iq_produits, self.souples,
                   self.bits, self.inverses, ber, self.ul, self.ul_sans_dest, self.err_envoi, pps))


class Degradeur:
    def __init__(self, a, np=None):
        self.a = a
        self.cible = a.cible
        self.mode = a.mode
        self.stats = Stats()
        self.graine = a.graine
        self.tn = a.tn_set
        self.rafales = Rafales(a.ber, a.rafales, a.ber_rafale) if (a.mode == "ber" and a.rafales) else None
        self.snr_lin = 10.0 ** (a.snr_db / 10.0) if a.snr_db is not None else None
        self.p_souple = _q_gauss(math.sqrt(2.0 * self.snr_lin)) if self.snr_lin is not None else 0.0
        self.np = np
        self.mod = ModulateurGmsk(np) if a.mode == "iq" else None

    def _rng(self, fn, tn, sel=0):
        x = (self.graine * 0x9E3779B97F4A7C15 + (fn + 1) * 0x100000001B3 + tn * 0x2545F491 + sel * 0x632BE5AB) & MASQUE64
        return random.Random(x)

    # -- dsp target ---------------------------------------------------------
    def _dsp(self, pkt):
        st = self.stats
        if len(pkt) < DSP_HDR + NBITS:
            st.autres += 1
            return pkt
        charge = len(pkt) - DSP_HDR
        if charge >= IQ_MIN_OCTETS:
            st.iq_deja += 1
            return pkt
        tn = pkt[0] & 0x07
        if self.tn is not None and tn not in self.tn:
            st.hors_tn += 1
            return pkt
        fn = struct.unpack_from(">I", pkt, 1)[0]
        rng = self._rng(fn, tn)
        ba = bytearray(pkt)
        dur = not bytes(ba[DSP_HDR:]).translate(None, b"\x00\x01")   # only 0/1: hard bits
        if not dur:
            st.souples += 1
        efface = self.a.perte > 0.0 and rng.random() < self.a.perte
        if efface:
            st.effaces += 1
            if dur:
                for i in range(DSP_HDR, len(ba)):
                    ba[i] = rng.getrandbits(1)
            else:
                for i in range(DSP_HDR, len(ba)):
                    ba[i] = 0                                  # soft: erasure = no information
        mode = self.mode
        if mode == "relais":
            if efface:
                st.alteres += 1
            return bytes(ba)
        if mode == "ber":
            n = len(ba) - DSP_HDR
            pos = self.rafales.positions(n, rng) if self.rafales else positions_iid(n, self.a.ber, rng)
            if dur:
                for i in pos:
                    ba[DSP_HDR + i] ^= 1
            else:
                for i in pos:                                  # soft: sign inverted
                    v = ba[DSP_HDR + i]
                    v = v - 256 if v > 127 else v
                    ba[DSP_HDR + i] = min(127, -v) & 0xFF
            st.bits += n
            st.inverses += len(pos)
            st.alteres += 1
            return bytes(ba)
        if mode == "souple":
            snr = self.snr_lin
            if dur:
                # +-1 + N(0, sigma^2), sigma^2 = 1/(2 snr), then hard decision:
                # each bit changes independently with probability
                # Q(1/sigma) = Q(sqrt(2 snr)). Those positions are therefore
                # drawn directly -- exactly the same law, without 148 gaussians.
                n = len(ba) - DSP_HDR
                pos = positions_iid(n, self.p_souple, rng)
                for i in pos:
                    ba[DSP_HDR + i] ^= 1
                st.bits += n
                st.inverses += len(pos)
            else:
                vals = [(v - 256 if v > 127 else v) for v in ba[DSP_HDR:]]
                amp = (sum(abs(v) for v in vals) / len(vals)) or 127.0
                sigma = amp / math.sqrt(2.0 * snr)
                for k, v in enumerate(vals):
                    w = int(round(v + rng.gauss(0.0, sigma)))
                    ba[DSP_HDR + k] = max(-127, min(127, w)) & 0xFF
            st.alteres += 1
            return bytes(ba)
        # iq mode
        if dur:
            bits = list(ba[DSP_HDR:DSP_HDR + NBITS])
        else:                                                  # soft int8: hard decision (> 0 = 1)
            bits = [1 if 0 < v <= 127 else 0 for v in ba[DSP_HDR:DSP_HDR + NBITS]]
        return bytes(ba[:DSP_HDR]) + self._iq(bits, fn, tn)

    def _iq(self, bits, fn, tn):
        np = self.np
        a = self.a
        ph = self.mod.phase(bits)
        i_ = a.amp * np.cos(ph)
        q_ = a.amp * np.sin(ph)
        if a.iq_elargir and any(bits):
            # Imitates the BSP's gmsk_elargir() on the samples it keeps
            # (neighbours at +-1 symbol = +-sps samples); FCCH (all zero)
            # left as is, like the BSP.
            s, e = self.mod.sps, a.iq_elargir
            for v in (i_, q_):
                w = v.copy()
                w[s:] += e * v[:-s]
                w[:-s] += e * v[s:]
                v[:] = w / (1.0 + 2.0 * e)
        if self.snr_lin is not None:
            sigma = math.sqrt(a.amp * a.amp / (2.0 * self.snr_lin))
            g = np.random.Generator(np.random.PCG64(self._rng(fn, tn, 1).getrandbits(64)))
            i_ = i_ + g.normal(0.0, sigma, i_.shape)
            q_ = q_ + g.normal(0.0, sigma, q_.shape)
        out = np.empty(2 * len(i_), dtype="<i2")
        out[0::2] = np.clip(np.rint(i_), -32768, 32767)
        out[1::2] = np.clip(np.rint(q_), -32768, 32767)
        self.stats.iq_produits += 1
        self.stats.alteres += 1
        return out.tobytes()

    # -- grgsm target -------------------------------------------------------
    def _grgsm(self, pkt):
        st = self.stats
        if len(pkt) < 16 or pkt[0] != 2 or pkt[2] != GSMTAP_TYPE_UM:
            st.autres += 1
            return pkt
        hdr = pkt[1] * 4
        if hdr < 16 or hdr >= len(pkt):
            st.autres += 1
            return pkt
        tn = pkt[3] & 0x07
        if self.tn is not None and tn not in self.tn:
            st.hors_tn += 1
            return pkt
        fn = struct.unpack_from(">I", pkt, 8)[0]
        rng = self._rng(fn, tn, pkt[12])
        if self.a.perte > 0.0 and rng.random() < self.a.perte:
            st.effaces += 1
            return None                                        # lost block = CRC failure on the L1 side
        if self.mode == "relais":
            return pkt
        ba = bytearray(pkt)
        n = (len(ba) - hdr) * 8
        pos = self.rafales.positions(n, rng) if self.rafales else positions_iid(n, self.a.ber, rng)
        for i in pos:
            ba[hdr + (i >> 3)] ^= 0x80 >> (i & 7)
        st.bits += n
        st.inverses += len(pos)
        st.alteres += 1
        return bytes(ba)

    def descendant(self, pkt):
        self.stats.dl += 1
        return self._dsp(pkt) if self.cible == "dsp" else self._grgsm(pkt)


def _parser():
    p = argparse.ArgumentParser(
        prog="injecteur_bruit.py",
        description="Proxy UDP transparent pont -> couche 1 qui degrade le sens descendant "
                    "(bits inverses, AWGN, I/Q bruite). Le montant est relaye sans modification.")
    p.add_argument("--cible", choices=sorted(PROFILS), default="dsp",
                   help="format du flux : dsp (TRXD 8 o + 148 bits, BSP de c54x_exe) ou grgsm "
                        "(GSMTAP vers la L1 gr-gsm de QEMU) ; defaut dsp")
    p.add_argument("--mode", choices=MODES, default="ber", help="defaut ber")
    p.add_argument("--ecoute", help="HOTE:PORT ou la source envoie (dsp 127.0.0.1:6702, grgsm 127.0.0.1:14730)")
    p.add_argument("--vers", help="HOTE:PORT de la couche 1 (dsp 127.0.0.1:16702, grgsm 127.0.0.1:4730)")
    p.add_argument("--source", help="HOTE:PORT local d'emission vers la L1 (defaut : port libre)")
    p.add_argument("--retour", help="HOTE:PORT ou renvoyer le montant (defaut : l'emetteur du dernier descendant)")
    p.add_argument("--ber", type=float, default=0.0, help="mode ber : taux moyen d'inversion (0..1)")
    p.add_argument("--rafales", type=float, default=0.0, metavar="L",
                   help="mode ber : erreurs en rafales de L bits en moyenne (Gilbert-Elliott)")
    p.add_argument("--ber-rafale", type=float, default=0.5, help="taux d'erreur dans une rafale (defaut 0,5)")
    p.add_argument("--snr-db", type=float, default=None,
                   help="modes souple et iq : SNR en dB, P_signal / (2 sigma^2) par echantillon complexe "
                        "(meme definition que CALYPSO_BSP_SNR_DB)")
    p.add_argument("--perte", type=float, default=0.0,
                   help="probabilite d'effacer un burst (dsp : bits aleatoires) ou un bloc (grgsm : non transmis)")
    p.add_argument("--tn", default="tous", help="intervalles a degrader, ex. 0 ou 1,2 (defaut tous)")
    p.add_argument("--graine", type=lambda s: int(s, 0), default=None, help="graine (deterministe)")
    p.add_argument("--amp", type=int, default=30000, help="mode iq : amplitude (defaut 30000, comme le BSP)")
    p.add_argument("--iq-elargir", type=float, default=0.3,
                   help="mode iq : elargissement des NB comme gmsk_elargir (defaut 0,3 ; 0 = aucun)")
    p.add_argument("--forcer-iq", action="store_true",
                   help="mode iq malgre CALYPSO_BSP_STREAM=1 ou CALYPSO_BSP_IQ_PASSTHROUGH=0")
    p.add_argument("--stats", type=float, default=10.0, help="periode des statistiques en s (0 = seulement le bilan)")
    p.add_argument("--journal", help="fichier journal (en plus de stderr)")
    p.add_argument("--pidfile", help="ecrit le pid, efface a l'arret")
    p.add_argument("--etiquette", default="", help="libre, pour reconnaitre le processus (pkill -f)")
    p.add_argument("--verifier", action="store_true", help="controle la configuration et sort")
    p.add_argument("--version", action="version", version="%(prog)s " + VERSION)
    return p


def _valider(a, p):
    """Completes and checks the arguments; returns numpy (iq mode) or None."""
    ecoute, vers = PROFILS[a.cible]
    a.ecoute = _adresse(a.ecoute or ecoute, "--ecoute")
    a.vers = _adresse(a.vers or vers, "--vers")
    a.source = _adresse(a.source, "--source") if a.source else None
    a.retour = _adresse(a.retour, "--retour") if a.retour else None
    if a.ecoute == a.vers:
        p.error("--ecoute et --vers designent la meme adresse %s:%d : l'injecteur s'enverrait ses propres paquets" % a.ecoute)
    if a.cible == "grgsm" and a.mode not in MODES_GRGSM:
        p.error("cible grgsm : mode %s impossible. Le flux GSMTAP vers QEMU porte des blocs L2 deja decodes "
                "(16 o d'en-tete + 23 o), ni valeurs souples ni bursts a moduler (calypso_l1_grgsm.c "
                "gsmtap_readable). Modes possibles : %s (et --perte)." % (a.mode, ", ".join(MODES_GRGSM)))
    for nom in ("ber", "perte", "ber_rafale"):
        v = getattr(a, nom)
        if not 0.0 <= v <= 1.0:
            p.error("--%s doit etre entre 0 et 1 (%g)" % (nom.replace("_", "-"), v))
    if a.mode == "ber" and a.rafales:
        if a.rafales < 1.0:
            p.error("--rafales : longueur moyenne en bits >= 1 (%g)" % a.rafales)
        if not 0.0 < a.ber_rafale <= 1.0 or a.ber >= a.ber_rafale:
            p.error("--rafales : il faut --ber (%g) < --ber-rafale (%g) <= 1" % (a.ber, a.ber_rafale))
    if a.mode == "souple" and a.snr_db is None:
        p.error("mode souple : --snr-db obligatoire")
    if a.snr_db is not None and not -50.0 <= a.snr_db <= 100.0:
        p.error("--snr-db hors bornes raisonnables (%g)" % a.snr_db)
    if a.tn in ("", "tous", "all"):
        a.tn_set = None
    else:
        try:
            a.tn_set = {int(x) for x in a.tn.split(",") if x.strip() != ""}
        except ValueError:
            p.error("--tn : liste d'intervalles 0..7 attendue (%r)" % a.tn)
        if not a.tn_set or any(not 0 <= x <= 7 for x in a.tn_set):
            p.error("--tn : intervalles 0..7 (%r)" % a.tn)
    if a.graine is None:
        a.graine = int.from_bytes(os.urandom(4), "little")
        a.graine_tiree = True
    else:
        a.graine_tiree = False
    a.graine &= MASQUE64
    np = None
    if a.mode == "iq":
        if a.cible != "dsp":
            p.error("mode iq : cible dsp seulement")
        try:
            import numpy as np  # noqa: F811 - only the iq mode needs it
        except ImportError:
            p.error("mode iq : numpy est necessaire et introuvable pour %s "
                    "(apt-get install python3-numpy, ou pip install numpy). "
                    "Les modes relais, ber et souple n'en ont pas besoin." % sys.executable)
        if not a.forcer_iq:
            if os.environ.get("CALYPSO_BSP_STREAM", "") == "1":
                p.error("mode iq refuse : CALYPSO_BSP_STREAM=1. En STREAM le BSP range les 148 premiers octets "
                        "de chaque paquet comme des BITS avant de regarder s'il s'agit d'I/Q "
                        "(calypso_bsp.c bsp_trxd_readable -> bsp_ts0_stocker) : l'I/Q y serait lu comme du bruit "
                        "de bits. Lancez c54x_exe sans STREAM, ou --forcer-iq (BRUIT_OPTS=--forcer-iq).")
            if os.environ.get("CALYPSO_BSP_IQ_PASSTHROUGH", "") == "0":
                p.error("mode iq refuse : CALYPSO_BSP_IQ_PASSTHROUGH=0, le BSP ne lit pas l'I/Q.")
        if not 1000 <= a.amp <= 32767:
            p.error("--amp : 1000..32767 (%d)" % a.amp)
        if not 0.0 <= a.iq_elargir <= 2.0:
            p.error("--iq-elargir : 0..2 (%g)" % a.iq_elargir)
    return np


def _ouvrir(a):
    amont = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        amont.bind(a.ecoute)
    except OSError as e:
        amont.close()
        raise SystemExit("injecteur_bruit : impossible d'ecouter sur %s:%d (%s) -- un autre processus "
                         "tient-il deja ce port ? ss -lunp | grep :%d" % (a.ecoute[0], a.ecoute[1], e.strerror, a.ecoute[1]))
    aval = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    if a.source:
        loc = a.source
    else:
        loc = ("127.0.0.1", 0) if a.vers[0].startswith("127.") or a.vers[0] == "localhost" else ("0.0.0.0", 0)
    try:
        aval.bind(loc)
    except OSError as e:
        amont.close()
        aval.close()
        raise SystemExit("injecteur_bruit : impossible de lier le port d'emission %s:%d (%s)" % (loc[0], loc[1], e.strerror))
    for s in (amont, aval):
        s.setblocking(False)
        try:
            s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1 << 20)
        except OSError:
            pass
    return amont, aval


def _journal(a):
    fmt = logging.Formatter("%(asctime)s [bruit] %(message)s", "%H:%M:%S")
    log.setLevel(logging.INFO)
    h = logging.StreamHandler(sys.stderr)
    h.setFormatter(fmt)
    log.addHandler(h)
    if a.journal:
        f = logging.FileHandler(a.journal)
        f.setFormatter(fmt)
        log.addHandler(f)


def _annoncer(a, aval, np=None):
    loc = aval.getsockname()
    log.info("injecteur de bruit %s : cible %s, mode %s%s", VERSION, a.cible, a.mode,
             " (EXPERIMENTAL)" if a.mode == "iq" else "")
    if a.mode == "ber":
        if a.rafales:
            log.info("BER moyen %.3e en rafales de %.1f bits (taux %.2f dans une rafale, Gilbert-Elliott)",
                     a.ber, a.rafales, a.ber_rafale)
        else:
            log.info("BER %.3e (bits independants)", a.ber)
        if a.cible == "grgsm" and a.ber > 0:
            log.info("ATTENTION grgsm : bits inverses dans le bloc L2 DEJA DECODE, rien ne les detecte "
                     "(ni FEC ni CRC) ; --perte modelise l'echec CRC")
    if a.mode == "souple":
        snr = 10.0 ** (a.snr_db / 10.0)
        log.info("SNR %.2f dB ; sur bits durs (le cas du pont) : +-1 + AWGN + decision dure, "
                 "BER attendu Q(sqrt(2 snr)) = %.3e", a.snr_db, _q_gauss(math.sqrt(2.0 * snr)))
    if a.mode == "iq":
        log.info("I/Q GMSK 4 ech/symbole, amplitude %d, elargissement NB %.2f, %s ; "
                 "contourne sb_moduler/gmsk_elargir du BSP", a.amp, a.iq_elargir,
                 ("AWGN %.2f dB" % a.snr_db) if a.snr_db is not None else "SANS AWGN")
        if a.snr_db is not None and np is not None:
            # int16 clipping: at 30000 only 2767 of headroom remain. Estimated
            # here to announce it (the C side, CALYPSO_BSP_SNR_DB, clips the same way).
            g = np.random.default_rng(0)
            m = 20000
            s = a.amp * np.exp(1j * g.uniform(0.0, 2.0 * math.pi, m))
            sig = math.sqrt(a.amp * a.amp / (2.0 * 10.0 ** (a.snr_db / 10.0)))
            y = s + sig * (g.standard_normal(m) + 1j * g.standard_normal(m))
            ecr = float(np.mean((np.abs(y.real) > 32767) | (np.abs(y.imag) > 32767)))
            if ecr > 0.01:
                yc = np.clip(y.real, -32768, 32767) + 1j * np.clip(y.imag, -32768, 32767)
                eff = 10.0 * math.log10(a.amp * a.amp / float(np.mean(np.abs(yc - s) ** 2)))
                log.info("ATTENTION ecretage int16 : ~%.0f %% des echantillons, SNR effectif ~%.1f dB "
                         "(bruit non gaussien) ; --amp plus bas pour l'eviter", 100.0 * ecr, eff)
    if a.perte:
        log.info("effacement : %.3e des %s", a.perte, "bursts (bits aleatoires)" if a.cible == "dsp" else "blocs (non transmis)")
    if (a.mode == "relais" or (a.mode == "ber" and a.ber == 0.0)) and not a.perte:
        log.info("aucune degradation configuree : relais transparent")
    log.info("intervalles : %s ; graine %d%s", "tous" if a.tn_set is None else ",".join(map(str, sorted(a.tn_set))),
             a.graine, " (tiree : --graine %d pour rejouer)" % a.graine if a.graine_tiree else "")
    log.info("PRET ecoute=%s:%d vers=%s:%d emission=%s:%d retour=%s", a.ecoute[0], a.ecoute[1], a.vers[0], a.vers[1],
             loc[0], loc[1], ("%s:%d" % a.retour) if a.retour else "emetteur du descendant")


def main(argv=None):
    p = _parser()
    a = p.parse_args(argv)
    np = _valider(a, p)
    if a.verifier:
        amont, aval = _ouvrir(a)
        amont.close()
        aval.close()
        print("injecteur_bruit : configuration valide (cible %s, mode %s, ecoute %s:%d -> %s:%d)"
              % (a.cible, a.mode, a.ecoute[0], a.ecoute[1], a.vers[0], a.vers[1]))
        return 0
    _journal(a)
    amont, aval = _ouvrir(a)
    deg = Degradeur(a, np)
    st = deg.stats
    if a.pidfile:
        with open(a.pidfile, "w") as f:
            f.write("%d\n" % os.getpid())

    arret = []
    flash = []
    signal.signal(signal.SIGTERM, lambda s, f: arret.append(s))
    signal.signal(signal.SIGINT, lambda s, f: arret.append(s))
    signal.signal(signal.SIGUSR1, lambda s, f: flash.append(s))

    _annoncer(a, aval, np)
    sel = selectors.DefaultSelector()
    sel.register(amont, selectors.EVENT_READ, "dl")
    sel.register(aval, selectors.EVENT_READ, "ul")
    source = None
    prochain = time.monotonic() + a.stats if a.stats > 0 else None
    try:
        while not arret:
            try:
                evts = sel.select(timeout=0.5)
            except InterruptedError:
                evts = []
            for cle, _ in evts:
                s = cle.fileobj
                for _ in range(512):
                    try:
                        pkt, adr = s.recvfrom(65536)
                    except (BlockingIOError, InterruptedError):
                        break
                    except OSError:
                        st.err_envoi += 1            # ICMP reported on the socket: carry on
                        break
                    if cle.data == "dl":
                        source = adr
                        sortie = deg.descendant(pkt)
                        if sortie is None:
                            continue
                        try:
                            aval.sendto(sortie, a.vers)
                            st.relayes += 1
                        except OSError:
                            st.err_envoi += 1
                    else:
                        dest = a.retour or source
                        if dest is None:
                            st.ul_sans_dest += 1
                            continue
                        try:
                            amont.sendto(pkt, dest)  # uplink: as is
                            st.ul += 1
                        except OSError:
                            st.err_envoi += 1
            now = time.monotonic()
            if flash or (prochain is not None and now >= prochain):
                del flash[:]
                log.info("%s", st.ligne())
                if prochain is not None:
                    prochain = now + a.stats
    finally:
        log.info("%s", st.ligne(final=True))
        log.info("arret (%s)", "signal %d" % arret[0] if arret else "fin")
        sel.close()
        amont.close()
        aval.close()
        if a.pidfile:
            try:
                os.unlink(a.pidfile)
            except OSError:
                pass
    return 0


if __name__ == "__main__":
    sys.exit(main())

3.74 https://github.com/bbaranoff/qosmo/blob/main/tools/qosmo-couleurs.py

4922 octets, 89 lignes → 89 lignes

#!/usr/bin/env python3
"""qosmo-couleurs : colore a la volee les journaux du banc (stdin -> stdout).

  tail -F qemu-asm.log | qosmo-couleurs            trace -d in_asm / exec de QEMU
  tail -F qemu.log     | qosmo-couleurs            [trx] [sim] [dcch] [c54x] ...
  tail -F mailbox.log  | qosmo-couleurs            ARM>WR ARM<RD DSP>WR DSP<RD
  watch -n 0.5 --color "tail -n 40 qemu-asm.log | qosmo-couleurs"

Les lignes deja colorees (mobile.log) passent telles quelles. QOSMO_COULEURS=0
ou une sortie qui n'est pas un terminal (et pas --force) : copie sans couleur.
Les panes tmux du banc (tmux_modules/_commun.sh) et ./run.sh --logs de c54x_exe
passent par ici. Rien n'est ecrit dans les fichiers : la couleur est a l'affichage."""
import os, re, sys, signal

def principal():
    force = '--force' in sys.argv[1:]
    actif = os.environ.get('QOSMO_COULEURS', '1') != '0' and (force or sys.stdout.isatty())
    out = sys.stdout
    if not actif:
        for l in sys.stdin:
            out.write(l); out.flush()
        return
    R = '\033[0m'
    def c(n, g=''):   return f'\033[{g}38;5;{n}m'
    DIM, GRAS = '\033[2m', '\033[1m'
    ADR, OCT, SYM, PC, HOTE = c(81, '1;'), c(242), c(114), c(221, '1;'), c(240)
    TAGS = {'trx': c(75), 'sim': c(176), 'dcch': c(80), 'c54x': c(179), 'mbx': c(114),
            'mailbox': c(114), 'sb': c(215), 'a_sch': c(215), 'sbwin': c(215), 'jalon': c(208, '1;'),
            'fb': c(215), 'pont': c(75), 'bsp': c(179), 'calypso-debug': c(242), 'run': c(250, '1;'),
            'gdb-telnet': c(250), 'api': c(114), 'a5': c(176), 'rhea': c(179)}
    SENS = {'ARM>WR': c(221, '1;'), 'ARM<RD': c(81), 'DSP>WR': c(114, '1;'), 'DSP<RD': c(176)}
    MAUVAIS = c(203, '1;'); BON = c(114, '1;'); ATTENTION = c(215)
    re_adr   = re.compile(r'^(0x[0-9a-f]{8}):(.*)$')
    re_objd  = re.compile(r'^(OBJD-T:)\s*([0-9a-f]+)\s*$')
    re_trace = re.compile(r'^(Trace \d+: )(0x[0-9a-f]+) \[([0-9a-f]+)/([0-9a-f]+)/([0-9a-f]+)/([0-9a-f]+)\]\s*(\S*)\s*$')
    re_stop  = re.compile(r'^(Stopped execution of TB chain before|Linking TBs|cpu_io_recompile)(.*)$')
    re_tag   = re.compile(r'^(\s*)\[([A-Za-z0-9_-]+)\]')
    re_mbx   = re.compile(r'^(\s*\d+\s+\d+\s+)(ARM>WR|ARM<RD|DSP>WR|DSP<RD)(\s+)(0x[0-9a-f]+|\S+)(\s+\S+\s+)(.*)$')
    re_fn    = re.compile(r'\b(fn=)(\d+)')
    re_hex   = re.compile(r'\b(0x[0-9A-Fa-f]{2,})\b')
    re_mal   = re.compile(r'(ERREUR|ECHEC|erreur|error|Error|ERROR|\bKO\b|perdu[es]?|PERDUE?|PAS (?:finie|recue)|crc_ko|Dropping|timeout|TIMEOUT|result=255|no cell|quand meme|INACTIF|introuvable|impossible|tick saute)')
    re_bon   = re.compile(r'(\bOK\b|CRC_OK|\bfinie\b|PLAUSIBLE|accept|ACTIF|tout tourne|Synchronize_TDMA|=> SB|camp)')
    re_att   = re.compile(r'(attente|attend|retard|en retard|toujours pas|LOST|saute)')
    re_asmins = re.compile(r'^(\s+[0-9a-f]+:\s+)([0-9a-f]{8}|[0-9a-f]{4} [0-9a-f]{4}|[0-9a-f]{4})(\s+)([a-z][a-z0-9.]*)(.*)$')
    def generique(s):
        s = re_mal.sub(MAUVAIS + r'\1' + R, s)
        s = re_bon.sub(BON + r'\1' + R, s)
        s = re_att.sub(ATTENTION + r'\1' + R, s)
        s = re_fn.sub(r'\1' + GRAS + r'\2' + R, s)
        s = re_hex.sub(c(81) + r'\1' + R, s)
        return s
    for l in sys.stdin:
        if '\033[' in l:                       # deja colore (mobile.log) : tel quel
            out.write(l); out.flush(); continue
        s = l.rstrip('\n'); t = s
        m = re_adr.match(s)
        if m:   t = f'{ADR}{m.group(1)}{R}:{m.group(2)}'
        elif (m := re_objd.match(s)):
            t = f'{DIM}{m.group(1)}{R} {OCT}{m.group(2)}{R}'
        elif (m := re_trace.match(s)):
            pc = m.group(4).lstrip('0') or '0'
            t = (f'{DIM}{m.group(1)}{R}{HOTE}{m.group(2)}{R} {DIM}[{m.group(3)}/{R}'
                 f'{PC}{"0x" + pc.rjust(8, "0")}{R}{DIM}/{m.group(5)}/{m.group(6)}]{R} {SYM}{m.group(7)}{R}')
        elif (m := re_stop.match(s)):
            t = f'{c(176)}{m.group(1)}{R}{DIM}{m.group(2)}{R}'
        elif s.startswith('IN:') or s.startswith('----'):
            t = f'{DIM}{s}{R}'
        elif (m := re_mbx.match(s)):
            t = (f'{DIM}{m.group(1)}{R}{SENS[m.group(2)]}{m.group(2)}{R}{m.group(3)}'
                 f'{ADR}{m.group(4)}{R}{SYM}{m.group(5)}{R}{generique(m.group(6))}')
        elif (m := re_asmins.match(s)):
            t = f'{DIM}{m.group(1)}{R}{OCT}{m.group(2)}{R}{m.group(3)}{PC}{m.group(4)}{R}{generique(m.group(5))}'
        elif s.startswith('#'):
            t = f'{DIM}{s}{R}'
        else:
            m = re_tag.match(s)
            if m:
                col = TAGS.get(m.group(2).lower(), c(250))
                t = f'{m.group(1)}{col}[{m.group(2)}]{R}' + generique(s[m.end():])
            else:
                t = generique(s)
        out.write(t + '\n'); out.flush()

if __name__ == '__main__':
    signal.signal(signal.SIGPIPE, signal.SIG_DFL)
    try:
        principal()
    except (KeyboardInterrupt, BrokenPipeError):
        pass

3.75 https://github.com/bbaranoff/qosmo/blob/main/tools/rapport-run.sh

6835 octets, 131 lignes → 131 lignes

#!/bin/bash
set -u

_pid_qemu="$(pgrep -x qemu-system-arm 2>/dev/null | head -1)"
_live=""
if [ -n "$_pid_qemu" ] && [ -r "/proc/$_pid_qemu/environ" ]; then
    _live="$(tr '\0' '\n' < "/proc/$_pid_qemu/environ" 2>/dev/null | sed -n 's/^LOG_DIR=//p' | head -1)"
fi
# start-native.sh publishes the path of the current run's mobile.log
# (/run/osmo-banc/mobile-log, OSMO_MOBILE_LOG): its directory takes precedence
# over the QEMU environment - with --dsp there is no qemu-system-arm from the
# fork, and the logs are under /tmp/c54x-pont.
_pub="${OSMO_MOBILE_LOG:-$(cat /run/osmo-banc/mobile-log 2>/dev/null)}"
[ -n "$_pub" ] && [ -f "$_pub" ] && _live="$(dirname "$_pub")"
LIVE_DIR="${LOG_DIR:-${_live:-/tmp/calypso/logs}}"
L="${1:-$LIVE_DIR}"
SORTIE="${2:-}"
[ -n "$SORTIE" ] && exec > >(tee "$SORTIE")

q="$L/qemu.log"; o="$L/osmocon.log"; m="$L/mobile.log"
b="$L/bts.log";  p="${PONT_LOG:-/dev/shm/pont.log}"

n() { [ -r "${2:-}" ] && grep -icE -- "$1" "$2" 2>/dev/null | head -1 || echo 0; }
temoin() {
    [ -r "${2:-}" ] || return
    grep -iE -- "$1" "$2" 2>/dev/null | tail -1 \
        | sed -E 's/\x1b\[[0-9;]*m//g; s/^[[:space:]]+//' | cut -c1-140
}
mes() {
    local lib="$1" mot="$2" fic="$3" v
    v="$(n "$mot" "$fic")"
    printf '  %-24s %7s' "$lib" "$v"
    if [ "$v" -gt 0 ] 2>/dev/null; then
        local t; t="$(temoin "$mot" "$fic")"; [ -n "$t" ] && printf '  | %s' "$t"
    else
        printf '  | motif jamais vu — absence NON prouvee'
    fi
    printf '\n'
}
titre() { printf '\n\033[1m== %s ==\033[0m\n' "$1"; }
info()  { printf '  %-24s %s\n' "$1" "$2"; }

printf '\033[1m########## RAPPORT DE RUN — %s ##########\033[0m\n' "$(date '+%Y-%m-%d %H:%M:%S')"
[ -r "$q" ] || { echo "ERREUR : $q illisible — mauvais repertoire ?"; exit 2; }
printf 'journaux : %s%s\n' "$L" "$([ "$L" = "$LIVE_DIR" ] && echo '  (run en cours)' || echo '  (photo archivee)')"
printf 'derniere ecriture : %s\n' "$(stat -c %y "$q" 2>/dev/null | cut -d. -f1)"
if [ "$L" = "$LIVE_DIR" ] && [ -n "${_pid_qemu:-}" ]; then
    _t0="$(stat -c %s "$q" 2>/dev/null)"; sleep 2; _t1="$(stat -c %s "$q" 2>/dev/null)"
    if [ "${_t0:-0}" = "${_t1:-0}" ]; then
        printf '\033[1m  ⚠️ qemu.log NE GROSSIT PAS alors que qemu tourne (pid %s).\n' "$_pid_qemu"
        printf '     Journaux probablement perimes — verifier LOG_DIR du processus.\033[0m\n'
    fi
fi

titre "0. IDENTITE DU RUN"
info "couche 1"     "$(temoin '\[l1\] backend' "$q")"
info "pont"         "$(temoin 'pont TRX démarré|pont TRX demarre' "$p")"
if [ "$L" = "$LIVE_DIR" ]; then
    P="$(pgrep -x qemu-system-arm 2>/dev/null | head -1)"
    [ -n "$P" ] && info "qemu vivant" "pid=$P age=$(ps -o etime= -p "$P" 2>/dev/null | tr -d ' ')" \
                || info "qemu vivant" "ARRETE"
    P="$(pgrep -f 'pont/pont(_dsp)?\.py' 2>/dev/null | head -1)"
    [ -n "$P" ] && info "pont vivant" "pid=$P age=$(ps -o etime= -p "$P" 2>/dev/null | tr -d ' ')" \
                || info "pont vivant" "ARRETE"
fi

titre "1. ACQUISITION L1 (osmocon.log)"
mes "L1CTL_FBSB_REQ"      'L1CTL_FBSB_REQ'                 "$o"
mes "FB detectee"         '^FB[01] \('                      "$o"
mes "Synchronize_TDMA"    'Synchronize_TDMA'                "$o"
mes "SB decodee"          '^SB[0-9] \(|=> SB 0x'            "$o"
mes "  BSIC non nul"      'BSIC=[1-9]'                      "$o"
mes "RESET FULL"          'L1CTL_RESET_REQ: FULL'           "$o"
[ -r "$o" ] && info "TOA les plus vus" "$(grep -oE 'TOA= *[0-9]+' "$o" | tr -s ' ' | sort | uniq -c | sort -rn | head -5 | tr '\n' ' ')"

titre "2. COUCHE 1 gr-gsm (qemu.log)"
mes "reset L1"            '\[l1\] reset L1'                 "$q"
mes "canal dedie"         '\[l1\] canal dedie'              "$q"
mes "chiffrement actif"   '\[l1\] chiffrement A5'           "$q"
mes "canal TCH"           '\[l1\] canal TCH'                "$q"
mes "couche 1 en clair"   '\[l1\] couche 1 en clair'        "$q"

titre "3. PONT TRX (pont.log)"
mes "bursts DL"           'STATS dl_bursts='                "$p"
mes "RACH"                'rach=[1-9]'                      "$p"
mes "erreurs"             'Traceback|error'                 "$p"
[ -r "$p" ] && info "derniere STATS" "$(grep 'STATS' "$p" 2>/dev/null | tail -1 | cut -c1-140)"

titre "4. COUCHES 2/3 (mobile.log) — jusqu ou va l abonne"
mes "sync ARFCN"          'Sync to ARFCN'                   "$m"
mes "SYSTEM INFORMATION"  'SYSTEM INFORMATION [0-9]'        "$m"
mes "  sans sysinfo"      'No sysinfo yet'                  "$m"
mes "camp"                'camping \(normal\)|camped normally' "$m"
mes "CHANNEL REQUEST"     'CHANNEL REQUEST'                 "$m"
mes "IMMEDIATE ASSIGN"    'IMMEDIATE ASSIGNMENT'            "$m"
mes "LAPDm SABM"          'LAPD_STATE_SABM_SENT'            "$m"
mes "cipher negocie"      'cipher [1-9]'                    "$m"
mes "LU REQUEST"          'LOCATION UPDATING REQUEST'       "$m"
mes "LU ACCEPT"           'LOCATION UPDATING ACCEPT'        "$m"
mes "LU REJECT"           'LOCATION UPDATING REJECT'        "$m"
mes "TMSI"                'TMSI'                            "$m"
mes "SMS"                 'SMS-DELIVER|new SMS|CP-DATA'     "$m"
mes "appel"               'CC_SETUP|MNCC_SETUP'             "$m"
[ -r "$m" ] && info "timers MM"  "$(grep -oE 'T3[0-9]{3}' "$m" | sort | uniq -c | sort -rn | head -4 | tr '\n' ' ')"
[ -r "$m" ] && info "ccch mode"  "$(grep -oE 'ccch mode [A-Z]+' "$m" | sort | uniq -c | tr '\n' ' ')"

titre "5. RESEAU (bts / side-car)"
mes "BTS erreurs"         'ERROR'                           "$b"
[ -r "$b" ] && info "erreur dominante" "$(grep -oE 'send\(\) failed on TRXD[^(]*|ERROR [a-z0-9.]+:' "$b" | sort | uniq -c | sort -rn | head -2 | tr '\n' ' ')"
mes "sidecar mobile err"  'error|fail'                      "$L/sidecar-mobile.log"

titre "6. SERVICES"
mes "SMSC"                'sms|deliver'                     "$L/smsc-op1.log"
mes "asterisk warn/err"   'error|warning'                   "$L/asterisk.log"

titre "7. VERDICT — jusqu ou va la chaine"
etape() {
    if [ "${2:-0}" -gt 0 ] 2>/dev/null; then printf '  [ OK ] %-22s (%s)\n' "$1" "$2"
    else printf '  [ .. ] %-22s (0)\n' "$1"; fi
}
etape "synchro FB"        "$(n '^FB[01] \(' "$o")"
etape "synchro SB"        "$(n '^SB[0-9] \(|=> SB 0x' "$o")"
etape "sysinfo"           "$(n 'SYSTEM INFORMATION [0-9]' "$m")"
etape "camp"              "$(n 'camping \(normal\)|camped normally' "$m")"
etape "acces RACH"        "$(n 'CHANNEL REQUEST' "$m")"
etape "canal dedie"       "$(n 'IMMEDIATE ASSIGNMENT' "$m")"
etape "LU demande"        "$(n 'LOCATION UPDATING REQUEST' "$m")"
etape "LU accepte"        "$(n 'LOCATION UPDATING ACCEPT' "$m")"
etape "SMS"               "$(n 'SMS-DELIVER|new SMS|CP-DATA' "$m")"
printf '\n  Methode : un 0 dit que le MOTIF n a pas ete vu. Verifier le motif\n'
printf '  avant de conclure a une absence.\n'

3.76 https://github.com/bbaranoff/qosmo/blob/main/tools/qosmo-tbring/tbring-dump.py

5007 octets, 110 lignes → 110 lignes

#!/usr/bin/env python3
"""tbring-dump : les N derniers blocs ARM executes, lus dans l'anneau du plugin tbring.

  tbring-dump [-n N] [-f /dev/shm/qemu-tbring] [-e layer1.highram.elf] [-a qemu-asm.log] [-r]

  -n N    nombre de blocs (defaut 200), les plus recents en dernier
  -e ELF  symboles (nm) : fonction+decalage a cote de chaque PC
  -a LOG  trace -d in_asm du meme run : les octets « OBJD-T » du bloc, desassembles
          avec arm-none-eabi-objdump si present (ce build de QEMU n'a pas capstone)
  -r      compte les blocs par PC sur toute la fenetre (histogramme) au lieu de la liste
  -c/-C   couleurs forcees / coupees (defaut : si la sortie est un terminal)

Lecture a chaud, sans arreter QEMU : l'anneau est de la memoire partagee."""
import argparse, struct, subprocess, shutil, collections, os, sys, tempfile

def lire(path, n):
    with open(path, 'rb') as f:
        hdr = f.read(64)
        magic, entries, count, shift = struct.unpack('<8sQQQ', hdr[:32])
        if magic != b'TBRING01':
            sys.exit(f"{path}: pas un anneau tbring")
        n = min(n, entries, count)
        out = []
        for i in range(count - n, count):
            f.seek(64 + (i & (entries - 1)) * 8)
            pc, tour = struct.unpack('<II', f.read(8))
            if tour != ((i >> shift) & 0xffffffff):
                out.append((None, i))      # ecrase entre-temps (QEMU a fait un tour)
            else:
                out.append((pc, i))
        return entries, count, out

def symboles(elf):
    syms = []
    try:
        txt = subprocess.run(['nm', '-n', '-S', elf], capture_output=True, text=True, check=True).stdout
    except Exception as e:
        print(f"nm {elf}: {e}", file=sys.stderr); return syms
    for l in txt.splitlines():
        p = l.split()
        if len(p) == 4 and p[2] in 'tTwW' and not p[3].startswith('$'):
            syms.append((int(p[0], 16), int(p[1], 16), p[3]))
    syms.sort()
    return syms

def resoudre(syms, pc):
    import bisect
    i = bisect.bisect_right([s[0] for s in syms], pc) - 1
    if i >= 0:
        a, sz, nom = syms[i]
        if pc < a + max(sz, 4):
            return f"{nom}+0x{pc - a:x}"
    return "?"

def in_asm(log):
    """pc -> (octets hex) pour chaque bloc traduit (premiere occurrence)."""
    table = {}
    pc = None
    with open(log, errors='replace') as f:
        for l in f:
            if l.startswith('0x') and l[10:11] == ':':
                try: pc = int(l[:10], 16)
                except ValueError: pc = None
            elif l.startswith('OBJD-T:') and pc is not None and pc not in table:
                table[pc] = l.split(':', 1)[1].strip()
                pc = None
    return table

def desasm(pc, hexs):
    od = shutil.which('arm-none-eabi-objdump')
    if not od:
        return [f"    .word {hexs}"]
    with tempfile.NamedTemporaryFile(suffix='.bin', delete=False) as t:
        t.write(bytes.fromhex(hexs)); tmp = t.name
    try:
        r = subprocess.run([od, '-D', '-b', 'binary', '-m', 'arm', '-M', 'force-thumb' if pc & 1 else 'no-force-thumb',
                            f'--adjust-vma=0x{pc & ~1:x}', tmp], capture_output=True, text=True)
        return ['    ' + l.strip() for l in r.stdout.splitlines() if l[:1] == ' ' and ':' in l]
    finally:
        os.unlink(tmp)

ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('-n', type=int, default=200); ap.add_argument('-f', default='/dev/shm/qemu-tbring')
ap.add_argument('-e'); ap.add_argument('-a'); ap.add_argument('-r', action='store_true')
ap.add_argument('-c', action='store_true'); ap.add_argument('-C', action='store_true')
a = ap.parse_args()
COUL = (a.c or (sys.stdout.isatty() and os.environ.get('QOSMO_COULEURS', '1') != '0')) and not a.C
def K(n, s, g=''):
    return f"\033[{g}38;5;{n}m{s}\033[0m" if COUL else str(s)
entries, count, blocs = lire(a.f, (1 << 62) if a.r and a.n == 200 else a.n)   # -r sans -n : toute la fenetre
syms = symboles(a.e) if a.e else []
table = in_asm(a.a) if a.a else {}
print(K(242, f"# {a.f} : {count} blocs executes, anneau de {entries}, {len(blocs)} affiches"))
if a.r:
    h = collections.Counter(pc for pc, _ in blocs if pc is not None)
    for pc, k in h.most_common(60):
        print(f"{K(221, f'{k:10d}', '1;')}  {K(81, f'0x{pc:08x}', '1;')}  {K(114, resoudre(syms, pc) if syms else '')}")
else:
    for pc, i in blocs:
        if pc is None:
            print(K(203, f"{i:12d}  (ecrase)")); continue
        print(f"{K(242, f'{i:12d}')}  {K(81, f'0x{pc:08x}', '1;')}  {K(114, resoudre(syms, pc) if syms else '')}")
        if pc in table:
            for l in desasm(pc, table[pc]):
                p = l.split(None, 2)   # adresse:  octets  mnemonique operandes
                if len(p) == 3 and COUL:
                    m = p[2].split(None, 1)
                    print(f"    {K(242, p[0])} {K(242, p[1])}\t{K(221, m[0], '1;')} {m[1] if len(m) > 1 else ''}")
                else:
                    print(l)

4 Autres

4.1 https://github.com/bbaranoff/qosmo/blob/main/accel/tcg/cpu-exec.c

34364 octets, 1105 lignes → 1105 lignes

/*
 *  emulator main execution loop
 *
 *  Copyright (c) 2003-2005 Fabrice Bellard
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see <http://www.gnu.org/licenses/>.
 */

#include "qemu/osdep.h"
#include "qemu/qemu-print.h"
#include "qapi/error.h"
#include "qapi/type-helpers.h"
#include "hw/core/tcg-cpu-ops.h"
#include "trace.h"
#include "disas/disas.h"
#include "exec/exec-all.h"
#include "tcg/tcg.h"
#include "qemu/atomic.h"
#include "qemu/rcu.h"
#include "exec/log.h"
#include "qemu/main-loop.h"
#include "sysemu/cpus.h"
#include "exec/cpu-all.h"
#include "sysemu/cpu-timers.h"
#include "exec/replay-core.h"
#include "sysemu/tcg.h"
#include "exec/helper-proto-common.h"
#include "tb-jmp-cache.h"
#include "tb-hash.h"
#include "tb-context.h"
#include "internal-common.h"
#include "internal-target.h"

/* -icount align implementation. */

typedef struct SyncClocks {
    int64_t diff_clk;
    int64_t last_cpu_icount;
    int64_t realtime_clock;
} SyncClocks;

#if !defined(CONFIG_USER_ONLY)
/* Allow the guest to have a max 3ms advance.
 * The difference between the 2 clocks could therefore
 * oscillate around 0.
 */
#define VM_CLOCK_ADVANCE 3000000
#define THRESHOLD_REDUCE 1.5
#define MAX_DELAY_PRINT_RATE 2000000000LL
#define MAX_NB_PRINTS 100

int64_t max_delay;
int64_t max_advance;

static void align_clocks(SyncClocks *sc, CPUState *cpu)
{
    int64_t cpu_icount;

    if (!icount_align_option) {
        return;
    }

    cpu_icount = cpu->icount_extra + cpu->neg.icount_decr.u16.low;
    sc->diff_clk += icount_to_ns(sc->last_cpu_icount - cpu_icount);
    sc->last_cpu_icount = cpu_icount;

    if (sc->diff_clk > VM_CLOCK_ADVANCE) {
#ifndef _WIN32
        struct timespec sleep_delay, rem_delay;
        sleep_delay.tv_sec = sc->diff_clk / 1000000000LL;
        sleep_delay.tv_nsec = sc->diff_clk % 1000000000LL;
        if (nanosleep(&sleep_delay, &rem_delay) < 0) {
            sc->diff_clk = rem_delay.tv_sec * 1000000000LL + rem_delay.tv_nsec;
        } else {
            sc->diff_clk = 0;
        }
#else
        Sleep(sc->diff_clk / SCALE_MS);
        sc->diff_clk = 0;
#endif
    }
}

static void print_delay(const SyncClocks *sc)
{
    static float threshold_delay;
    static int64_t last_realtime_clock;
    static int nb_prints;

    if (icount_align_option &&
        sc->realtime_clock - last_realtime_clock >= MAX_DELAY_PRINT_RATE &&
        nb_prints < MAX_NB_PRINTS) {
        if ((-sc->diff_clk / (float)1000000000LL > threshold_delay) ||
            (-sc->diff_clk / (float)1000000000LL <
             (threshold_delay - THRESHOLD_REDUCE))) {
            threshold_delay = (-sc->diff_clk / 1000000000LL) + 1;
            qemu_printf("Warning: The guest is now late by %.1f to %.1f seconds\n",
                        threshold_delay - 1,
                        threshold_delay);
            nb_prints++;
            last_realtime_clock = sc->realtime_clock;
        }
    }
}

static void init_delay_params(SyncClocks *sc, CPUState *cpu)
{
    if (!icount_align_option) {
        return;
    }
    sc->realtime_clock = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL_RT);
    sc->diff_clk = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) - sc->realtime_clock;
    sc->last_cpu_icount
        = cpu->icount_extra + cpu->neg.icount_decr.u16.low;
    if (sc->diff_clk < max_delay) {
        max_delay = sc->diff_clk;
    }
    if (sc->diff_clk > max_advance) {
        max_advance = sc->diff_clk;
    }

    /* Print every 2s max if the guest is late. We limit the number
       of printed messages to NB_PRINT_MAX(currently 100) */
    print_delay(sc);
}
#else
static void align_clocks(SyncClocks *sc, const CPUState *cpu)
{
}

static void init_delay_params(SyncClocks *sc, const CPUState *cpu)
{
}
#endif /* CONFIG USER ONLY */

bool tcg_cflags_has(CPUState *cpu, uint32_t flags)
{
    return cpu->tcg_cflags & flags;
}

void tcg_cflags_set(CPUState *cpu, uint32_t flags)
{
    cpu->tcg_cflags |= flags;
}

uint32_t curr_cflags(CPUState *cpu)
{
    uint32_t cflags = cpu->tcg_cflags;

    /*
     * Record gdb single-step.  We should be exiting the TB by raising
     * EXCP_DEBUG, but to simplify other tests, disable chaining too.
     *
     * For singlestep and -d nochain, suppress goto_tb so that
     * we can log -d cpu,exec after every TB.
     */
    if (unlikely(cpu->singlestep_enabled)) {
        cflags |= CF_NO_GOTO_TB | CF_NO_GOTO_PTR | CF_SINGLE_STEP | 1;
    } else if (qatomic_read(&one_insn_per_tb)) {
        cflags |= CF_NO_GOTO_TB | 1;
    } else if (qemu_loglevel_mask(CPU_LOG_TB_NOCHAIN)) {
        cflags |= CF_NO_GOTO_TB;
    }

    return cflags;
}

struct tb_desc {
    vaddr pc;
    uint64_t cs_base;
    CPUArchState *env;
    tb_page_addr_t page_addr0;
    uint32_t flags;
    uint32_t cflags;
};

static bool tb_lookup_cmp(const void *p, const void *d)
{
    const TranslationBlock *tb = p;
    const struct tb_desc *desc = d;

    if ((tb_cflags(tb) & CF_PCREL || tb->pc == desc->pc) &&
        tb_page_addr0(tb) == desc->page_addr0 &&
        tb->cs_base == desc->cs_base &&
        tb->flags == desc->flags &&
        tb_cflags(tb) == desc->cflags) {
        /* check next page if needed */
        tb_page_addr_t tb_phys_page1 = tb_page_addr1(tb);
        if (tb_phys_page1 == -1) {
            return true;
        } else {
            tb_page_addr_t phys_page1;
            vaddr virt_page1;

            /*
             * We know that the first page matched, and an otherwise valid TB
             * encountered an incomplete instruction at the end of that page,
             * therefore we know that generating a new TB from the current PC
             * must also require reading from the next page -- even if the
             * second pages do not match, and therefore the resulting insn
             * is different for the new TB.  Therefore any exception raised
             * here by the faulting lookup is not premature.
             */
            virt_page1 = TARGET_PAGE_ALIGN(desc->pc);
            phys_page1 = get_page_addr_code(desc->env, virt_page1);
            if (tb_phys_page1 == phys_page1) {
                return true;
            }
        }
    }
    return false;
}

static TranslationBlock *tb_htable_lookup(CPUState *cpu, vaddr pc,
                                          uint64_t cs_base, uint32_t flags,
                                          uint32_t cflags)
{
    tb_page_addr_t phys_pc;
    struct tb_desc desc;
    uint32_t h;

    desc.env = cpu_env(cpu);
    desc.cs_base = cs_base;
    desc.flags = flags;
    desc.cflags = cflags;
    desc.pc = pc;
    phys_pc = get_page_addr_code(desc.env, pc);
    if (phys_pc == -1) {
        return NULL;
    }
    desc.page_addr0 = phys_pc;
    h = tb_hash_func(phys_pc, (cflags & CF_PCREL ? 0 : pc),
                     flags, cs_base, cflags);
    return qht_lookup_custom(&tb_ctx.htable, &desc, h, tb_lookup_cmp);
}

/* Might cause an exception, so have a longjmp destination ready */
static inline TranslationBlock *tb_lookup(CPUState *cpu, vaddr pc,
                                          uint64_t cs_base, uint32_t flags,
                                          uint32_t cflags)
{
    TranslationBlock *tb;
    CPUJumpCache *jc;
    uint32_t hash;

    /* we should never be trying to look up an INVALID tb */
    tcg_debug_assert(!(cflags & CF_INVALID));

    hash = tb_jmp_cache_hash_func(pc);
    jc = cpu->tb_jmp_cache;

    tb = qatomic_read(&jc->array[hash].tb);
    if (likely(tb &&
               jc->array[hash].pc == pc &&
               tb->cs_base == cs_base &&
               tb->flags == flags &&
               tb_cflags(tb) == cflags)) {
        goto hit;
    }

    tb = tb_htable_lookup(cpu, pc, cs_base, flags, cflags);
    if (tb == NULL) {
        return NULL;
    }

    jc->array[hash].pc = pc;
    qatomic_set(&jc->array[hash].tb, tb);

hit:
    /*
     * As long as tb is not NULL, the contents are consistent.  Therefore,
     * the virtual PC has to match for non-CF_PCREL translations.
     */
    assert((tb_cflags(tb) & CF_PCREL) || tb->pc == pc);
    return tb;
}

static void log_cpu_exec(vaddr pc, CPUState *cpu,
                         const TranslationBlock *tb)
{
    if (qemu_log_in_addr_range(pc)) {
        qemu_log_mask(CPU_LOG_EXEC,
                      "Trace %d: %p [%08" PRIx64
                      "/%016" VADDR_PRIx "/%08x/%08x] %s\n",
                      cpu->cpu_index, tb->tc.ptr, tb->cs_base, pc,
                      tb->flags, tb->cflags,
                      tb->log_sym ? tb->log_sym : lookup_symbol(pc));

        if (qemu_loglevel_mask(CPU_LOG_TB_CPU)) {
            FILE *logfile = qemu_log_trylock();
            if (logfile) {
                int flags = 0;

                if (qemu_loglevel_mask(CPU_LOG_TB_FPU)) {
                    flags |= CPU_DUMP_FPU;
                }
#if defined(TARGET_I386)
                flags |= CPU_DUMP_CCOP;
#endif
                if (qemu_loglevel_mask(CPU_LOG_TB_VPU)) {
                    flags |= CPU_DUMP_VPU;
                }
                cpu_dump_state(cpu, logfile, flags);
                qemu_log_unlock(logfile);
            }
        }
    }
}

static bool check_for_breakpoints_slow(CPUState *cpu, vaddr pc,
                                       uint32_t *cflags)
{
    CPUBreakpoint *bp;
    bool match_page = false;

    /*
     * Singlestep overrides breakpoints.
     * This requirement is visible in the record-replay tests, where
     * we would fail to make forward progress in reverse-continue.
     *
     * TODO: gdb singlestep should only override gdb breakpoints,
     * so that one could (gdb) singlestep into the guest kernel's
     * architectural breakpoint handler.
     */
    if (cpu->singlestep_enabled) {
        return false;
    }

    QTAILQ_FOREACH(bp, &cpu->breakpoints, entry) {
        /*
         * If we have an exact pc match, trigger the breakpoint.
         * Otherwise, note matches within the page.
         */
        if (pc == bp->pc) {
            bool match_bp = false;

            if (bp->flags & BP_GDB) {
                match_bp = true;
            } else if (bp->flags & BP_CPU) {
#ifdef CONFIG_USER_ONLY
                g_assert_not_reached();
#else
                const TCGCPUOps *tcg_ops = cpu->cc->tcg_ops;
                assert(tcg_ops->debug_check_breakpoint);
                match_bp = tcg_ops->debug_check_breakpoint(cpu);
#endif
            }

            if (match_bp) {
                cpu->exception_index = EXCP_DEBUG;
                return true;
            }
        } else if (((pc ^ bp->pc) & TARGET_PAGE_MASK) == 0) {
            match_page = true;
        }
    }

    /*
     * Within the same page as a breakpoint, single-step,
     * returning to helper_lookup_tb_ptr after each insn looking
     * for the actual breakpoint.
     *
     * TODO: Perhaps better to record all of the TBs associated
     * with a given virtual page that contains a breakpoint, and
     * then invalidate them when a new overlapping breakpoint is
     * set on the page.  Non-overlapping TBs would not be
     * invalidated, nor would any TB need to be invalidated as
     * breakpoints are removed.
     */
    if (match_page) {
        *cflags = (*cflags & ~CF_COUNT_MASK) | CF_NO_GOTO_TB | CF_BP_PAGE | 1;
    }
    return false;
}

static inline bool check_for_breakpoints(CPUState *cpu, vaddr pc,
                                         uint32_t *cflags)
{
    return unlikely(!QTAILQ_EMPTY(&cpu->breakpoints)) &&
        check_for_breakpoints_slow(cpu, pc, cflags);
}

/**
 * helper_lookup_tb_ptr: quick check for next tb
 * @env: current cpu state
 *
 * Look for an existing TB matching the current cpu state.
 * If found, return the code pointer.  If not found, return
 * the tcg epilogue so that we return into cpu_tb_exec.
 */
const void *HELPER(lookup_tb_ptr)(CPUArchState *env)
{
    CPUState *cpu = env_cpu(env);
    TranslationBlock *tb;
    vaddr pc;
    uint64_t cs_base;
    uint32_t flags, cflags;

    /*
     * By definition we've just finished a TB, so I/O is OK.
     * Avoid the possibility of calling cpu_io_recompile() if
     * a page table walk triggered by tb_lookup() calling
     * probe_access_internal() happens to touch an MMIO device.
     * The next TB, if we chain to it, will clear the flag again.
     */
    cpu->neg.can_do_io = true;
    cpu_get_tb_cpu_state(env, &pc, &cs_base, &flags);

    cflags = curr_cflags(cpu);
    if (check_for_breakpoints(cpu, pc, &cflags)) {
        cpu_loop_exit(cpu);
    }

    tb = tb_lookup(cpu, pc, cs_base, flags, cflags);
    if (tb == NULL) {
        return tcg_code_gen_epilogue;
    }

    if (qemu_loglevel_mask(CPU_LOG_TB_CPU | CPU_LOG_EXEC)) {
        log_cpu_exec(pc, cpu, tb);
    }

    return tb->tc.ptr;
}

/* Execute a TB, and fix up the CPU state afterwards if necessary */
/*
 * Disable CFI checks.
 * TCG creates binary blobs at runtime, with the transformed code.
 * A TB is a blob of binary code, created at runtime and called with an
 * indirect function call. Since such function did not exist at compile time,
 * the CFI runtime has no way to verify its signature and would fail.
 * TCG is not considered a security-sensitive part of QEMU so this does not
 * affect the impact of CFI in environment with high security requirements
 */
static inline TranslationBlock * QEMU_DISABLE_CFI
cpu_tb_exec(CPUState *cpu, TranslationBlock *itb, int *tb_exit)
{
    uintptr_t ret;
    TranslationBlock *last_tb;
    const void *tb_ptr = itb->tc.ptr;

    if (qemu_loglevel_mask(CPU_LOG_TB_CPU | CPU_LOG_EXEC)) {
        log_cpu_exec(log_pc(cpu, itb), cpu, itb);
    }

    qemu_thread_jit_execute();
    ret = tcg_qemu_tb_exec(cpu_env(cpu), tb_ptr);
    cpu->neg.can_do_io = true;
    qemu_plugin_disable_mem_helpers(cpu);
    /*
     * TODO: Delay swapping back to the read-write region of the TB
     * until we actually need to modify the TB.  The read-only copy,
     * coming from the rx region, shares the same host TLB entry as
     * the code that executed the exit_tb opcode that arrived here.
     * If we insist on touching both the RX and the RW pages, we
     * double the host TLB pressure.
     */
    last_tb = tcg_splitwx_to_rw((void *)(ret & ~TB_EXIT_MASK));
    *tb_exit = ret & TB_EXIT_MASK;

    trace_exec_tb_exit(last_tb, *tb_exit);

    if (*tb_exit > TB_EXIT_IDX1) {
        /* We didn't start executing this TB (eg because the instruction
         * counter hit zero); we must restore the guest PC to the address
         * of the start of the TB.
         */
        CPUClass *cc = cpu->cc;
        const TCGCPUOps *tcg_ops = cc->tcg_ops;

        if (tcg_ops->synchronize_from_tb) {
            tcg_ops->synchronize_from_tb(cpu, last_tb);
        } else {
            tcg_debug_assert(!(tb_cflags(last_tb) & CF_PCREL));
            assert(cc->set_pc);
            cc->set_pc(cpu, last_tb->pc);
        }
        if (qemu_loglevel_mask(CPU_LOG_EXEC)) {
            vaddr pc = log_pc(cpu, last_tb);
            if (qemu_log_in_addr_range(pc)) {
                qemu_log("Stopped execution of TB chain before %p [%016"
                         VADDR_PRIx "] %s\n",
                         last_tb->tc.ptr, pc, lookup_symbol(pc));
            }
        }
    }

    /*
     * If gdb single-step, and we haven't raised another exception,
     * raise a debug exception.  Single-step with another exception
     * is handled in cpu_handle_exception.
     */
    if (unlikely(cpu->singlestep_enabled) && cpu->exception_index == -1) {
        cpu->exception_index = EXCP_DEBUG;
        cpu_loop_exit(cpu);
    }

    return last_tb;
}


static void cpu_exec_enter(CPUState *cpu)
{
    const TCGCPUOps *tcg_ops = cpu->cc->tcg_ops;

    if (tcg_ops->cpu_exec_enter) {
        tcg_ops->cpu_exec_enter(cpu);
    }
}

static void cpu_exec_exit(CPUState *cpu)
{
    const TCGCPUOps *tcg_ops = cpu->cc->tcg_ops;

    if (tcg_ops->cpu_exec_exit) {
        tcg_ops->cpu_exec_exit(cpu);
    }
}

static void cpu_exec_longjmp_cleanup(CPUState *cpu)
{
    /* Non-buggy compilers preserve this; assert the correct value. */
    g_assert(cpu == current_cpu);

#ifdef CONFIG_USER_ONLY
    clear_helper_retaddr();
    if (have_mmap_lock()) {
        mmap_unlock();
    }
#else
    /*
     * For softmmu, a tlb_fill fault during translation will land here,
     * and we need to release any page locks held.  In system mode we
     * have one tcg_ctx per thread, so we know it was this cpu doing
     * the translation.
     *
     * Alternative 1: Install a cleanup to be called via an exception
     * handling safe longjmp.  It seems plausible that all our hosts
     * support such a thing.  We'd have to properly register unwind info
     * for the JIT for EH, rather that just for GDB.
     *
     * Alternative 2: Set and restore cpu->jmp_env in tb_gen_code to
     * capture the cpu_loop_exit longjmp, perform the cleanup, and
     * jump again to arrive here.
     */
    if (tcg_ctx->gen_tb) {
        tb_unlock_pages(tcg_ctx->gen_tb);
        tcg_ctx->gen_tb = NULL;
    }
#endif
    if (bql_locked()) {
        bql_unlock();
    }
    assert_no_pages_locked();
}

void cpu_exec_step_atomic(CPUState *cpu)
{
    CPUArchState *env = cpu_env(cpu);
    TranslationBlock *tb;
    vaddr pc;
    uint64_t cs_base;
    uint32_t flags, cflags;
    int tb_exit;

    if (sigsetjmp(cpu->jmp_env, 0) == 0) {
        start_exclusive();
        g_assert(cpu == current_cpu);
        g_assert(!cpu->running);
        cpu->running = true;

        cpu_get_tb_cpu_state(env, &pc, &cs_base, &flags);

        cflags = curr_cflags(cpu);
        /* Execute in a serial context. */
        cflags &= ~CF_PARALLEL;
        /* After 1 insn, return and release the exclusive lock. */
        cflags |= CF_NO_GOTO_TB | CF_NO_GOTO_PTR | 1;
        /*
         * No need to check_for_breakpoints here.
         * We only arrive in cpu_exec_step_atomic after beginning execution
         * of an insn that includes an atomic operation we can't handle.
         * Any breakpoint for this insn will have been recognized earlier.
         */

        tb = tb_lookup(cpu, pc, cs_base, flags, cflags);
        if (tb == NULL) {
            mmap_lock();
            tb = tb_gen_code(cpu, pc, cs_base, flags, cflags);
            mmap_unlock();
        }

        cpu_exec_enter(cpu);
        /* execute the generated code */
        trace_exec_tb(tb, pc);
        cpu_tb_exec(cpu, tb, &tb_exit);
        cpu_exec_exit(cpu);
    } else {
        cpu_exec_longjmp_cleanup(cpu);
    }

    /*
     * As we start the exclusive region before codegen we must still
     * be in the region if we longjump out of either the codegen or
     * the execution.
     */
    g_assert(cpu_in_exclusive_context(cpu));
    cpu->running = false;
    end_exclusive();
}

void tb_set_jmp_target(TranslationBlock *tb, int n, uintptr_t addr)
{
    /*
     * Get the rx view of the structure, from which we find the
     * executable code address, and tb_target_set_jmp_target can
     * produce a pc-relative displacement to jmp_target_addr[n].
     */
    const TranslationBlock *c_tb = tcg_splitwx_to_rx(tb);
    uintptr_t offset = tb->jmp_insn_offset[n];
    uintptr_t jmp_rx = (uintptr_t)tb->tc.ptr + offset;
    uintptr_t jmp_rw = jmp_rx - tcg_splitwx_diff;

    tb->jmp_target_addr[n] = addr;
    tb_target_set_jmp_target(c_tb, n, jmp_rx, jmp_rw);
}

static inline void tb_add_jump(TranslationBlock *tb, int n,
                               TranslationBlock *tb_next)
{
    uintptr_t old;

    qemu_thread_jit_write();
    assert(n < ARRAY_SIZE(tb->jmp_list_next));
    qemu_spin_lock(&tb_next->jmp_lock);

    /* make sure the destination TB is valid */
    if (tb_next->cflags & CF_INVALID) {
        goto out_unlock_next;
    }
    /* Atomically claim the jump destination slot only if it was NULL */
    old = qatomic_cmpxchg(&tb->jmp_dest[n], (uintptr_t)NULL,
                          (uintptr_t)tb_next);
    if (old) {
        goto out_unlock_next;
    }

    /* patch the native jump address */
    tb_set_jmp_target(tb, n, (uintptr_t)tb_next->tc.ptr);

    /* add in TB jmp list */
    tb->jmp_list_next[n] = tb_next->jmp_list_head;
    tb_next->jmp_list_head = (uintptr_t)tb | n;

    qemu_spin_unlock(&tb_next->jmp_lock);

    qemu_log_mask(CPU_LOG_EXEC, "Linking TBs %p index %d -> %p\n",
                  tb->tc.ptr, n, tb_next->tc.ptr);
    return;

 out_unlock_next:
    qemu_spin_unlock(&tb_next->jmp_lock);
    return;
}

static inline bool cpu_handle_halt(CPUState *cpu)
{
#ifndef CONFIG_USER_ONLY
    if (cpu->halted) {
        const TCGCPUOps *tcg_ops = cpu->cc->tcg_ops;
        bool leave_halt = tcg_ops->cpu_exec_halt(cpu);

        if (!leave_halt) {
            return true;
        }

        cpu->halted = 0;
    }
#endif /* !CONFIG_USER_ONLY */

    return false;
}

static inline void cpu_handle_debug_exception(CPUState *cpu)
{
    const TCGCPUOps *tcg_ops = cpu->cc->tcg_ops;
    CPUWatchpoint *wp;

    if (!cpu->watchpoint_hit) {
        QTAILQ_FOREACH(wp, &cpu->watchpoints, entry) {
            wp->flags &= ~BP_WATCHPOINT_HIT;
        }
    }

    if (tcg_ops->debug_excp_handler) {
        tcg_ops->debug_excp_handler(cpu);
    }
}

static inline bool cpu_handle_exception(CPUState *cpu, int *ret)
{
    if (cpu->exception_index < 0) {
#ifndef CONFIG_USER_ONLY
        if (replay_has_exception()
            && cpu->neg.icount_decr.u16.low + cpu->icount_extra == 0) {
            /* Execute just one insn to trigger exception pending in the log */
            cpu->cflags_next_tb = (curr_cflags(cpu) & ~CF_USE_ICOUNT)
                | CF_NOIRQ | 1;
        }
#endif
        return false;
    }

    if (cpu->exception_index >= EXCP_INTERRUPT) {
        /* exit request from the cpu execution loop */
        *ret = cpu->exception_index;
        if (*ret == EXCP_DEBUG) {
            cpu_handle_debug_exception(cpu);
        }
        cpu->exception_index = -1;
        return true;
    }

#if defined(CONFIG_USER_ONLY)
    /*
     * If user mode only, we simulate a fake exception which will be
     * handled outside the cpu execution loop.
     */
#if defined(TARGET_I386)
    const TCGCPUOps *tcg_ops = cpu->cc->tcg_ops;
    tcg_ops->fake_user_interrupt(cpu);
#endif /* TARGET_I386 */
    *ret = cpu->exception_index;
    cpu->exception_index = -1;
    return true;
#else
    if (replay_exception()) {
        const TCGCPUOps *tcg_ops = cpu->cc->tcg_ops;

        bql_lock();
        tcg_ops->do_interrupt(cpu);
        bql_unlock();
        cpu->exception_index = -1;

        if (unlikely(cpu->singlestep_enabled)) {
            /*
             * After processing the exception, ensure an EXCP_DEBUG is
             * raised when single-stepping so that GDB doesn't miss the
             * next instruction.
             */
            *ret = EXCP_DEBUG;
            cpu_handle_debug_exception(cpu);
            return true;
        }
    } else if (!replay_has_interrupt()) {
        /* give a chance to iothread in replay mode */
        *ret = EXCP_INTERRUPT;
        return true;
    }
#endif

    return false;
}

static inline bool icount_exit_request(CPUState *cpu)
{
    if (!icount_enabled()) {
        return false;
    }
    if (cpu->cflags_next_tb != -1 && !(cpu->cflags_next_tb & CF_USE_ICOUNT)) {
        return false;
    }
    return cpu->neg.icount_decr.u16.low + cpu->icount_extra == 0;
}

static inline bool cpu_handle_interrupt(CPUState *cpu,
                                        TranslationBlock **last_tb)
{
    /*
     * If we have requested custom cflags with CF_NOIRQ we should
     * skip checking here. Any pending interrupts will get picked up
     * by the next TB we execute under normal cflags.
     */
    if (cpu->cflags_next_tb != -1 && cpu->cflags_next_tb & CF_NOIRQ) {
        return false;
    }

    /* Clear the interrupt flag now since we're processing
     * cpu->interrupt_request and cpu->exit_request.
     * Ensure zeroing happens before reading cpu->exit_request or
     * cpu->interrupt_request (see also smp_wmb in cpu_exit())
     */
    qatomic_set_mb(&cpu->neg.icount_decr.u16.high, 0);

    if (unlikely(qatomic_read(&cpu->interrupt_request))) {
        int interrupt_request;
        bql_lock();
        interrupt_request = cpu->interrupt_request;
        if (unlikely(cpu->singlestep_enabled & SSTEP_NOIRQ)) {
            /* Mask out external interrupts for this step. */
            interrupt_request &= ~CPU_INTERRUPT_SSTEP_MASK;
        }
        if (interrupt_request & CPU_INTERRUPT_DEBUG) {
            cpu->interrupt_request &= ~CPU_INTERRUPT_DEBUG;
            cpu->exception_index = EXCP_DEBUG;
            bql_unlock();
            return true;
        }
#if !defined(CONFIG_USER_ONLY)
        if (replay_mode == REPLAY_MODE_PLAY && !replay_has_interrupt()) {
            /* Do nothing */
        } else if (interrupt_request & CPU_INTERRUPT_HALT) {
            replay_interrupt();
            cpu->interrupt_request &= ~CPU_INTERRUPT_HALT;
            cpu->halted = 1;
            cpu->exception_index = EXCP_HLT;
            bql_unlock();
            return true;
        }
#if defined(TARGET_I386)
        else if (interrupt_request & CPU_INTERRUPT_INIT) {
            X86CPU *x86_cpu = X86_CPU(cpu);
            CPUArchState *env = &x86_cpu->env;
            replay_interrupt();
            cpu_svm_check_intercept_param(env, SVM_EXIT_INIT, 0, 0);
            do_cpu_init(x86_cpu);
            cpu->exception_index = EXCP_HALTED;
            bql_unlock();
            return true;
        }
#else
        else if (interrupt_request & CPU_INTERRUPT_RESET) {
            replay_interrupt();
            cpu_reset(cpu);
            bql_unlock();
            return true;
        }
#endif /* !TARGET_I386 */
        /* The target hook has 3 exit conditions:
           False when the interrupt isn't processed,
           True when it is, and we should restart on a new TB,
           and via longjmp via cpu_loop_exit.  */
        else {
            const TCGCPUOps *tcg_ops = cpu->cc->tcg_ops;

            if (tcg_ops->cpu_exec_interrupt(cpu, interrupt_request)) {
                if (!tcg_ops->need_replay_interrupt ||
                    tcg_ops->need_replay_interrupt(interrupt_request)) {
                    replay_interrupt();
                }
                /*
                 * After processing the interrupt, ensure an EXCP_DEBUG is
                 * raised when single-stepping so that GDB doesn't miss the
                 * next instruction.
                 */
                if (unlikely(cpu->singlestep_enabled)) {
                    cpu->exception_index = EXCP_DEBUG;
                    bql_unlock();
                    return true;
                }
                cpu->exception_index = -1;
                *last_tb = NULL;
            }
            /* The target hook may have updated the 'cpu->interrupt_request';
             * reload the 'interrupt_request' value */
            interrupt_request = cpu->interrupt_request;
        }
#endif /* !CONFIG_USER_ONLY */
        if (interrupt_request & CPU_INTERRUPT_EXITTB) {
            cpu->interrupt_request &= ~CPU_INTERRUPT_EXITTB;
            /* ensure that no TB jump will be modified as
               the program flow was changed */
            *last_tb = NULL;
        }

        /* If we exit via cpu_loop_exit/longjmp it is reset in cpu_exec */
        bql_unlock();
    }

    /* Finally, check if we need to exit to the main loop.  */
    if (unlikely(qatomic_read(&cpu->exit_request)) || icount_exit_request(cpu)) {
        qatomic_set(&cpu->exit_request, 0);
        if (cpu->exception_index == -1) {
            cpu->exception_index = EXCP_INTERRUPT;
        }
        return true;
    }

    return false;
}

static inline void cpu_loop_exec_tb(CPUState *cpu, TranslationBlock *tb,
                                    vaddr pc, TranslationBlock **last_tb,
                                    int *tb_exit)
{
    trace_exec_tb(tb, pc);
    tb = cpu_tb_exec(cpu, tb, tb_exit);
    if (*tb_exit != TB_EXIT_REQUESTED) {
        *last_tb = tb;
        return;
    }

    *last_tb = NULL;
    if (cpu_loop_exit_requested(cpu)) {
        /* Something asked us to stop executing chained TBs; just
         * continue round the main loop. Whatever requested the exit
         * will also have set something else (eg exit_request or
         * interrupt_request) which will be handled by
         * cpu_handle_interrupt.  cpu_handle_interrupt will also
         * clear cpu->icount_decr.u16.high.
         */
        return;
    }

    /* Instruction counter expired.  */
    assert(icount_enabled());
#ifndef CONFIG_USER_ONLY
    /* Ensure global icount has gone forward */
    icount_update(cpu);
    /* Refill decrementer and continue execution.  */
    int32_t insns_left = MIN(0xffff, cpu->icount_budget);
    cpu->neg.icount_decr.u16.low = insns_left;
    cpu->icount_extra = cpu->icount_budget - insns_left;

    /*
     * If the next tb has more instructions than we have left to
     * execute we need to ensure we find/generate a TB with exactly
     * insns_left instructions in it.
     */
    if (insns_left > 0 && insns_left < tb->icount)  {
        assert(insns_left <= CF_COUNT_MASK);
        assert(cpu->icount_extra == 0);
        cpu->cflags_next_tb = (tb->cflags & ~CF_COUNT_MASK) | insns_left;
    }
#endif
}

/* main execution loop */

static int __attribute__((noinline))
cpu_exec_loop(CPUState *cpu, SyncClocks *sc)
{
    int ret;

    /* if an exception is pending, we execute it here */
    while (!cpu_handle_exception(cpu, &ret)) {
        TranslationBlock *last_tb = NULL;
        int tb_exit = 0;

        while (!cpu_handle_interrupt(cpu, &last_tb)) {
            TranslationBlock *tb;
            vaddr pc;
            uint64_t cs_base;
            uint32_t flags, cflags;

            cpu_get_tb_cpu_state(cpu_env(cpu), &pc, &cs_base, &flags);

            /*
             * When requested, use an exact setting for cflags for the next
             * execution.  This is used for icount, precise smc, and stop-
             * after-access watchpoints.  Since this request should never
             * have CF_INVALID set, -1 is a convenient invalid value that
             * does not require tcg headers for cpu_common_reset.
             */
            cflags = cpu->cflags_next_tb;
            if (cflags == -1) {
                cflags = curr_cflags(cpu);
            } else {
                cpu->cflags_next_tb = -1;
            }

            if (check_for_breakpoints(cpu, pc, &cflags)) {
                break;
            }

            tb = tb_lookup(cpu, pc, cs_base, flags, cflags);
            if (tb == NULL) {
                CPUJumpCache *jc;
                uint32_t h;

                mmap_lock();
                tb = tb_gen_code(cpu, pc, cs_base, flags, cflags);
                mmap_unlock();

                /*
                 * We add the TB in the virtual pc hash table
                 * for the fast lookup
                 */
                h = tb_jmp_cache_hash_func(pc);
                jc = cpu->tb_jmp_cache;
                jc->array[h].pc = pc;
                qatomic_set(&jc->array[h].tb, tb);
            }

#ifndef CONFIG_USER_ONLY
            /*
             * We don't take care of direct jumps when address mapping
             * changes in system emulation.  So it's not safe to make a
             * direct jump to a TB spanning two pages because the mapping
             * for the second page can change.
             */
            if (tb_page_addr1(tb) != -1) {
                last_tb = NULL;
            }
#endif
            /* See if we can patch the calling TB. */
            if (last_tb) {
                tb_add_jump(last_tb, tb_exit, tb);
            }

            cpu_loop_exec_tb(cpu, tb, pc, &last_tb, &tb_exit);

            /* Try to align the host and virtual clocks
               if the guest is in advance */
            align_clocks(sc, cpu);
        }
    }
    return ret;
}

static int cpu_exec_setjmp(CPUState *cpu, SyncClocks *sc)
{
    /* Prepare setjmp context for exception handling. */
    if (unlikely(sigsetjmp(cpu->jmp_env, 0) != 0)) {
        cpu_exec_longjmp_cleanup(cpu);
    }

    return cpu_exec_loop(cpu, sc);
}

int cpu_exec(CPUState *cpu)
{
    int ret;
    SyncClocks sc = { 0 };

    /* replay_interrupt may need current_cpu */
    current_cpu = cpu;

    if (cpu_handle_halt(cpu)) {
        return EXCP_HALTED;
    }

    RCU_READ_LOCK_GUARD();
    cpu_exec_enter(cpu);

    /*
     * Calculate difference between guest clock and host clock.
     * This delay includes the delay of the last cycle, so
     * what we have to do is sleep until it is 0. As for the
     * advance/delay we gain here, we try to fix it next time.
     */
    init_delay_params(&sc, cpu);

    ret = cpu_exec_setjmp(cpu, &sc);

    cpu_exec_exit(cpu);
    return ret;
}

bool tcg_exec_realizefn(CPUState *cpu, Error **errp)
{
    static bool tcg_target_initialized;

    if (!tcg_target_initialized) {
        /* Check mandatory TCGCPUOps handlers */
#ifndef CONFIG_USER_ONLY
        assert(cpu->cc->tcg_ops->cpu_exec_halt);
        assert(cpu->cc->tcg_ops->cpu_exec_interrupt);
#endif /* !CONFIG_USER_ONLY */
        cpu->cc->tcg_ops->initialize();
        tcg_target_initialized = true;
    }

    cpu->tb_jmp_cache = g_new0(CPUJumpCache, 1);
    tlb_init(cpu);
#ifndef CONFIG_USER_ONLY
    tcg_iommu_init_notifier_list(cpu);
#endif /* !CONFIG_USER_ONLY */
    /* qemu_plugin_vcpu_init_hook delayed until cpu_index assigned. */

    return true;
}

/* undo the initializations in reverse order */
void tcg_exec_unrealizefn(CPUState *cpu)
{
#ifndef CONFIG_USER_ONLY
    tcg_iommu_free_notifier_list(cpu);
#endif /* !CONFIG_USER_ONLY */

    tlb_destroy(cpu);
    g_free_rcu(cpu->tb_jmp_cache, rcu);
}

4.2 https://github.com/bbaranoff/qosmo/blob/main/accel/tcg/translate-all.c

21297 octets, 666 lignes → 666 lignes

/*
 *  Host code generation
 *
 *  Copyright (c) 2003 Fabrice Bellard
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see <http://www.gnu.org/licenses/>.
 */

#include "qemu/osdep.h"

#include "trace.h"
#include "disas/disas.h"
#include "exec/exec-all.h"
#include "tcg/tcg.h"
#if defined(CONFIG_USER_ONLY)
#include "qemu.h"
#if defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
#include <sys/param.h>
#if __FreeBSD_version >= 700104
#define HAVE_KINFO_GETVMMAP
#define sigqueue sigqueue_freebsd  /* avoid redefinition */
#include <sys/proc.h>
#include <machine/profile.h>
#define _KERNEL
#include <sys/user.h>
#undef _KERNEL
#undef sigqueue
#include <libutil.h>
#endif
#endif
#else
#include "exec/ram_addr.h"
#endif

#include "exec/cputlb.h"
#include "exec/translate-all.h"
#include "exec/translator.h"
#include "exec/tb-flush.h"
#include "qemu/bitmap.h"
#include "qemu/qemu-print.h"
#include "qemu/main-loop.h"
#include "qemu/cacheinfo.h"
#include "qemu/timer.h"
#include "exec/log.h"
#include "sysemu/cpus.h"
#include "sysemu/cpu-timers.h"
#include "sysemu/tcg.h"
#include "qapi/error.h"
#include "hw/core/tcg-cpu-ops.h"
#include "tb-jmp-cache.h"
#include "tb-hash.h"
#include "tb-context.h"
#include "internal-common.h"
#include "internal-target.h"
#include "tcg/perf.h"
#include "tcg/insn-start-words.h"

TBContext tb_ctx;

/*
 * Encode VAL as a signed leb128 sequence at P.
 * Return P incremented past the encoded value.
 */
static uint8_t *encode_sleb128(uint8_t *p, int64_t val)
{
    int more, byte;

    do {
        byte = val & 0x7f;
        val >>= 7;
        more = !((val == 0 && (byte & 0x40) == 0)
                 || (val == -1 && (byte & 0x40) != 0));
        if (more) {
            byte |= 0x80;
        }
        *p++ = byte;
    } while (more);

    return p;
}

/*
 * Decode a signed leb128 sequence at *PP; increment *PP past the
 * decoded value.  Return the decoded value.
 */
static int64_t decode_sleb128(const uint8_t **pp)
{
    const uint8_t *p = *pp;
    int64_t val = 0;
    int byte, shift = 0;

    do {
        byte = *p++;
        val |= (int64_t)(byte & 0x7f) << shift;
        shift += 7;
    } while (byte & 0x80);
    if (shift < 64 && (byte & 0x40)) {
        val |= -(int64_t)1 << shift;
    }

    *pp = p;
    return val;
}

/* Encode the data collected about the instructions while compiling TB.
   Place the data at BLOCK, and return the number of bytes consumed.

   The logical table consists of TARGET_INSN_START_WORDS target_ulong's,
   which come from the target's insn_start data, followed by a uintptr_t
   which comes from the host pc of the end of the code implementing the insn.

   Each line of the table is encoded as sleb128 deltas from the previous
   line.  The seed for the first line is { tb->pc, 0..., tb->tc.ptr }.
   That is, the first column is seeded with the guest pc, the last column
   with the host pc, and the middle columns with zeros.  */

static int encode_search(TranslationBlock *tb, uint8_t *block)
{
    uint8_t *highwater = tcg_ctx->code_gen_highwater;
    uint64_t *insn_data = tcg_ctx->gen_insn_data;
    uint16_t *insn_end_off = tcg_ctx->gen_insn_end_off;
    uint8_t *p = block;
    int i, j, n;

    for (i = 0, n = tb->icount; i < n; ++i) {
        uint64_t prev, curr;

        for (j = 0; j < TARGET_INSN_START_WORDS; ++j) {
            if (i == 0) {
                prev = (!(tb_cflags(tb) & CF_PCREL) && j == 0 ? tb->pc : 0);
            } else {
                prev = insn_data[(i - 1) * TARGET_INSN_START_WORDS + j];
            }
            curr = insn_data[i * TARGET_INSN_START_WORDS + j];
            p = encode_sleb128(p, curr - prev);
        }
        prev = (i == 0 ? 0 : insn_end_off[i - 1]);
        curr = insn_end_off[i];
        p = encode_sleb128(p, curr - prev);

        /* Test for (pending) buffer overflow.  The assumption is that any
           one row beginning below the high water mark cannot overrun
           the buffer completely.  Thus we can test for overflow after
           encoding a row without having to check during encoding.  */
        if (unlikely(p > highwater)) {
            return -1;
        }
    }

    return p - block;
}

static int cpu_unwind_data_from_tb(TranslationBlock *tb, uintptr_t host_pc,
                                   uint64_t *data)
{
    uintptr_t iter_pc = (uintptr_t)tb->tc.ptr;
    const uint8_t *p = tb->tc.ptr + tb->tc.size;
    int i, j, num_insns = tb->icount;

    host_pc -= GETPC_ADJ;

    if (host_pc < iter_pc) {
        return -1;
    }

    memset(data, 0, sizeof(uint64_t) * TARGET_INSN_START_WORDS);
    if (!(tb_cflags(tb) & CF_PCREL)) {
        data[0] = tb->pc;
    }

    /*
     * Reconstruct the stored insn data while looking for the point
     * at which the end of the insn exceeds host_pc.
     */
    for (i = 0; i < num_insns; ++i) {
        for (j = 0; j < TARGET_INSN_START_WORDS; ++j) {
            data[j] += decode_sleb128(&p);
        }
        iter_pc += decode_sleb128(&p);
        if (iter_pc > host_pc) {
            return num_insns - i;
        }
    }
    return -1;
}

/*
 * The cpu state corresponding to 'host_pc' is restored in
 * preparation for exiting the TB.
 */
void cpu_restore_state_from_tb(CPUState *cpu, TranslationBlock *tb,
                               uintptr_t host_pc)
{
    uint64_t data[TARGET_INSN_START_WORDS];
    int insns_left = cpu_unwind_data_from_tb(tb, host_pc, data);

    if (insns_left < 0) {
        return;
    }

    if (tb_cflags(tb) & CF_USE_ICOUNT) {
        assert(icount_enabled());
        /*
         * Reset the cycle counter to the start of the block and
         * shift if to the number of actually executed instructions.
         */
        cpu->neg.icount_decr.u16.low += insns_left;
    }

    cpu->cc->tcg_ops->restore_state_to_opc(cpu, tb, data);
}

bool cpu_restore_state(CPUState *cpu, uintptr_t host_pc)
{
    /*
     * The host_pc has to be in the rx region of the code buffer.
     * If it is not we will not be able to resolve it here.
     * The two cases where host_pc will not be correct are:
     *
     *  - fault during translation (instruction fetch)
     *  - fault from helper (not using GETPC() macro)
     *
     * Either way we need return early as we can't resolve it here.
     */
    if (in_code_gen_buffer((const void *)(host_pc - tcg_splitwx_diff))) {
        TranslationBlock *tb = tcg_tb_lookup(host_pc);
        if (tb) {
            cpu_restore_state_from_tb(cpu, tb, host_pc);
            return true;
        }
    }
    return false;
}

bool cpu_unwind_state_data(CPUState *cpu, uintptr_t host_pc, uint64_t *data)
{
    if (in_code_gen_buffer((const void *)(host_pc - tcg_splitwx_diff))) {
        TranslationBlock *tb = tcg_tb_lookup(host_pc);
        if (tb) {
            return cpu_unwind_data_from_tb(tb, host_pc, data) >= 0;
        }
    }
    return false;
}

void page_init(void)
{
    page_table_config_init();
}

/*
 * Isolate the portion of code gen which can setjmp/longjmp.
 * Return the size of the generated code, or negative on error.
 */
static int setjmp_gen_code(CPUArchState *env, TranslationBlock *tb,
                           vaddr pc, void *host_pc,
                           int *max_insns, int64_t *ti)
{
    int ret = sigsetjmp(tcg_ctx->jmp_trans, 0);
    if (unlikely(ret != 0)) {
        return ret;
    }

    tcg_func_start(tcg_ctx);

    tcg_ctx->cpu = env_cpu(env);
    gen_intermediate_code(env_cpu(env), tb, max_insns, pc, host_pc);
    assert(tb->size != 0);
    tcg_ctx->cpu = NULL;
    *max_insns = tb->icount;

    return tcg_gen_code(tcg_ctx, tb, pc);
}

/* Called with mmap_lock held for user mode emulation.  */
TranslationBlock *tb_gen_code(CPUState *cpu,
                              vaddr pc, uint64_t cs_base,
                              uint32_t flags, int cflags)
{
    CPUArchState *env = cpu_env(cpu);
    TranslationBlock *tb, *existing_tb;
    tb_page_addr_t phys_pc, phys_p2;
    tcg_insn_unit *gen_code_buf;
    int gen_code_size, search_size, max_insns;
    int64_t ti;
    void *host_pc;

    assert_memory_lock();
    qemu_thread_jit_write();

    phys_pc = get_page_addr_code_hostp(env, pc, &host_pc);

    if (phys_pc == -1) {
        /* Generate a one-shot TB with 1 insn in it */
        cflags = (cflags & ~CF_COUNT_MASK) | 1;
    }

    max_insns = cflags & CF_COUNT_MASK;
    if (max_insns == 0) {
        max_insns = TCG_MAX_INSNS;
    }
    QEMU_BUILD_BUG_ON(CF_COUNT_MASK + 1 != TCG_MAX_INSNS);

 buffer_overflow:
    assert_no_pages_locked();
    tb = tcg_tb_alloc(tcg_ctx);
    if (unlikely(!tb)) {
        /* flush must be done */
        tb_flush(cpu);
        mmap_unlock();
        /* Make the execution loop process the flush as soon as possible.  */
        cpu->exception_index = EXCP_INTERRUPT;
        cpu_loop_exit(cpu);
    }

    gen_code_buf = tcg_ctx->code_gen_ptr;
    tb->tc.ptr = tcg_splitwx_to_rx(gen_code_buf);
    if (!(cflags & CF_PCREL)) {
        tb->pc = pc;
    }
    tb->cs_base = cs_base;
    tb->flags = flags;
    tb->cflags = cflags;
    tb->log_sym = qemu_loglevel_mask(CPU_LOG_EXEC) ? lookup_symbol(pc) : "";
    tb_set_page_addr0(tb, phys_pc);
    tb_set_page_addr1(tb, -1);
    if (phys_pc != -1) {
        tb_lock_page0(phys_pc);
    }

    tcg_ctx->gen_tb = tb;
    tcg_ctx->addr_type = TARGET_LONG_BITS == 32 ? TCG_TYPE_I32 : TCG_TYPE_I64;
#ifdef CONFIG_SOFTMMU
    tcg_ctx->page_bits = TARGET_PAGE_BITS;
    tcg_ctx->page_mask = TARGET_PAGE_MASK;
    tcg_ctx->tlb_dyn_max_bits = CPU_TLB_DYN_MAX_BITS;
#endif
    tcg_ctx->insn_start_words = TARGET_INSN_START_WORDS;
#ifdef TCG_GUEST_DEFAULT_MO
    tcg_ctx->guest_mo = TCG_GUEST_DEFAULT_MO;
#else
    tcg_ctx->guest_mo = TCG_MO_ALL;
#endif

 restart_translate:
    trace_translate_block(tb, pc, tb->tc.ptr);

    gen_code_size = setjmp_gen_code(env, tb, pc, host_pc, &max_insns, &ti);
    if (unlikely(gen_code_size < 0)) {
        switch (gen_code_size) {
        case -1:
            /*
             * Overflow of code_gen_buffer, or the current slice of it.
             *
             * TODO: We don't need to re-do gen_intermediate_code, nor
             * should we re-do the tcg optimization currently hidden
             * inside tcg_gen_code.  All that should be required is to
             * flush the TBs, allocate a new TB, re-initialize it per
             * above, and re-do the actual code generation.
             */
            qemu_log_mask(CPU_LOG_TB_OP | CPU_LOG_TB_OP_OPT,
                          "Restarting code generation for "
                          "code_gen_buffer overflow\n");
            tb_unlock_pages(tb);
            tcg_ctx->gen_tb = NULL;
            goto buffer_overflow;

        case -2:
            /*
             * The code generated for the TranslationBlock is too large.
             * The maximum size allowed by the unwind info is 64k.
             * There may be stricter constraints from relocations
             * in the tcg backend.
             *
             * Try again with half as many insns as we attempted this time.
             * If a single insn overflows, there's a bug somewhere...
             */
            assert(max_insns > 1);
            max_insns /= 2;
            qemu_log_mask(CPU_LOG_TB_OP | CPU_LOG_TB_OP_OPT,
                          "Restarting code generation with "
                          "smaller translation block (max %d insns)\n",
                          max_insns);

            /*
             * The half-sized TB may not cross pages.
             * TODO: Fix all targets that cross pages except with
             * the first insn, at which point this can't be reached.
             */
            phys_p2 = tb_page_addr1(tb);
            if (unlikely(phys_p2 != -1)) {
                tb_unlock_page1(phys_pc, phys_p2);
                tb_set_page_addr1(tb, -1);
            }
            goto restart_translate;

        case -3:
            /*
             * We had a page lock ordering problem.  In order to avoid
             * deadlock we had to drop the lock on page0, which means
             * that everything we translated so far is compromised.
             * Restart with locks held on both pages.
             */
            qemu_log_mask(CPU_LOG_TB_OP | CPU_LOG_TB_OP_OPT,
                          "Restarting code generation with re-locked pages");
            goto restart_translate;

        default:
            g_assert_not_reached();
        }
    }
    tcg_ctx->gen_tb = NULL;

    search_size = encode_search(tb, (void *)gen_code_buf + gen_code_size);
    if (unlikely(search_size < 0)) {
        tb_unlock_pages(tb);
        goto buffer_overflow;
    }
    tb->tc.size = gen_code_size;

    /*
     * For CF_PCREL, attribute all executions of the generated code
     * to its first mapping.
     */
    perf_report_code(pc, tb, tcg_splitwx_to_rx(gen_code_buf));

    if (qemu_loglevel_mask(CPU_LOG_TB_OUT_ASM) &&
        qemu_log_in_addr_range(pc)) {
        FILE *logfile = qemu_log_trylock();
        if (logfile) {
            int code_size, data_size;
            const tcg_target_ulong *rx_data_gen_ptr;
            size_t chunk_start;
            int insn = 0;

            if (tcg_ctx->data_gen_ptr) {
                rx_data_gen_ptr = tcg_splitwx_to_rx(tcg_ctx->data_gen_ptr);
                code_size = (const void *)rx_data_gen_ptr - tb->tc.ptr;
                data_size = gen_code_size - code_size;
            } else {
                rx_data_gen_ptr = 0;
                code_size = gen_code_size;
                data_size = 0;
            }

            /* Dump header and the first instruction */
            fprintf(logfile, "OUT: [size=%d]\n", gen_code_size);
            fprintf(logfile,
                    "  -- guest addr 0x%016" PRIx64 " + tb prologue\n",
                    tcg_ctx->gen_insn_data[insn * TARGET_INSN_START_WORDS]);
            chunk_start = tcg_ctx->gen_insn_end_off[insn];
            disas(logfile, tb->tc.ptr, chunk_start);

            /*
             * Dump each instruction chunk, wrapping up empty chunks into
             * the next instruction. The whole array is offset so the
             * first entry is the beginning of the 2nd instruction.
             */
            while (insn < tb->icount) {
                size_t chunk_end = tcg_ctx->gen_insn_end_off[insn];
                if (chunk_end > chunk_start) {
                    fprintf(logfile, "  -- guest addr 0x%016" PRIx64 "\n",
                            tcg_ctx->gen_insn_data[insn * TARGET_INSN_START_WORDS]);
                    disas(logfile, tb->tc.ptr + chunk_start,
                          chunk_end - chunk_start);
                    chunk_start = chunk_end;
                }
                insn++;
            }

            if (chunk_start < code_size) {
                fprintf(logfile, "  -- tb slow paths + alignment\n");
                disas(logfile, tb->tc.ptr + chunk_start,
                      code_size - chunk_start);
            }

            /* Finally dump any data we may have after the block */
            if (data_size) {
                int i;
                fprintf(logfile, "  data: [size=%d]\n", data_size);
                for (i = 0; i < data_size / sizeof(tcg_target_ulong); i++) {
                    if (sizeof(tcg_target_ulong) == 8) {
                        fprintf(logfile,
                                "0x%08" PRIxPTR ":  .quad  0x%016" TCG_PRIlx "\n",
                                (uintptr_t)&rx_data_gen_ptr[i], rx_data_gen_ptr[i]);
                    } else if (sizeof(tcg_target_ulong) == 4) {
                        fprintf(logfile,
                                "0x%08" PRIxPTR ":  .long  0x%08" TCG_PRIlx "\n",
                                (uintptr_t)&rx_data_gen_ptr[i], rx_data_gen_ptr[i]);
                    } else {
                        qemu_build_not_reached();
                    }
                }
            }
            fprintf(logfile, "\n");
            qemu_log_unlock(logfile);
        }
    }

    qatomic_set(&tcg_ctx->code_gen_ptr, (void *)
        ROUND_UP((uintptr_t)gen_code_buf + gen_code_size + search_size,
                 CODE_GEN_ALIGN));

    /* init jump list */
    qemu_spin_init(&tb->jmp_lock);
    tb->jmp_list_head = (uintptr_t)NULL;
    tb->jmp_list_next[0] = (uintptr_t)NULL;
    tb->jmp_list_next[1] = (uintptr_t)NULL;
    tb->jmp_dest[0] = (uintptr_t)NULL;
    tb->jmp_dest[1] = (uintptr_t)NULL;

    /* init original jump addresses which have been set during tcg_gen_code() */
    if (tb->jmp_reset_offset[0] != TB_JMP_OFFSET_INVALID) {
        tb_reset_jump(tb, 0);
    }
    if (tb->jmp_reset_offset[1] != TB_JMP_OFFSET_INVALID) {
        tb_reset_jump(tb, 1);
    }

    /*
     * If the TB is not associated with a physical RAM page then it must be
     * a temporary one-insn TB, and we have nothing left to do. Return early
     * before attempting to link to other TBs or add to the lookup table.
     */
    if (tb_page_addr0(tb) == -1) {
        assert_no_pages_locked();
        return tb;
    }

    /*
     * Insert TB into the corresponding region tree before publishing it
     * through QHT. Otherwise rewinding happened in the TB might fail to
     * lookup itself using host PC.
     */
    tcg_tb_insert(tb);

    /*
     * No explicit memory barrier is required -- tb_link_page() makes the
     * TB visible in a consistent state.
     */
    existing_tb = tb_link_page(tb);
    assert_no_pages_locked();

    /* if the TB already exists, discard what we just translated */
    if (unlikely(existing_tb != tb)) {
        uintptr_t orig_aligned = (uintptr_t)gen_code_buf;

        orig_aligned -= ROUND_UP(sizeof(*tb), qemu_icache_linesize);
        qatomic_set(&tcg_ctx->code_gen_ptr, (void *)orig_aligned);
        tcg_tb_remove(tb);
        return existing_tb;
    }
    return tb;
}

/* user-mode: call with mmap_lock held */
void tb_check_watchpoint(CPUState *cpu, uintptr_t retaddr)
{
    TranslationBlock *tb;

    assert_memory_lock();

    tb = tcg_tb_lookup(retaddr);
    if (tb) {
        /* We can use retranslation to find the PC.  */
        cpu_restore_state_from_tb(cpu, tb, retaddr);
        tb_phys_invalidate(tb, -1);
    } else {
        /* The exception probably happened in a helper.  The CPU state should
           have been saved before calling it. Fetch the PC from there.  */
        CPUArchState *env = cpu_env(cpu);
        vaddr pc;
        uint64_t cs_base;
        tb_page_addr_t addr;
        uint32_t flags;

        cpu_get_tb_cpu_state(env, &pc, &cs_base, &flags);
        addr = get_page_addr_code(env, pc);
        if (addr != -1) {
            tb_invalidate_phys_range(addr, addr);
        }
    }
}

#ifndef CONFIG_USER_ONLY
/*
 * In deterministic execution mode, instructions doing device I/Os
 * must be at the end of the TB.
 *
 * Called by softmmu_template.h, with iothread mutex not held.
 */
void cpu_io_recompile(CPUState *cpu, uintptr_t retaddr)
{
    TranslationBlock *tb;
    CPUClass *cc;
    uint32_t n;

    tb = tcg_tb_lookup(retaddr);
    if (!tb) {
        cpu_abort(cpu, "cpu_io_recompile: could not find TB for pc=%p",
                  (void *)retaddr);
    }
    cpu_restore_state_from_tb(cpu, tb, retaddr);

    /*
     * Some guests must re-execute the branch when re-executing a delay
     * slot instruction.  When this is the case, adjust icount and N
     * to account for the re-execution of the branch.
     */
    n = 1;
    cc = CPU_GET_CLASS(cpu);
    if (cc->tcg_ops->io_recompile_replay_branch &&
        cc->tcg_ops->io_recompile_replay_branch(cpu, tb)) {
        cpu->neg.icount_decr.u16.low++;
        n = 2;
    }

    /*
     * Exit the loop and potentially generate a new TB executing the
     * just the I/O insns. We also limit instrumentation to memory
     * operations only (which execute after completion) so we don't
     * double instrument the instruction.
     */
    cpu->cflags_next_tb = curr_cflags(cpu) | CF_MEMI_ONLY | n;

    if (qemu_loglevel_mask(CPU_LOG_EXEC)) {
        vaddr pc = cpu->cc->get_pc(cpu);
        if (qemu_log_in_addr_range(pc)) {
            qemu_log("cpu_io_recompile: rewound execution of TB to %016"
                     VADDR_PRIx "\n", pc);
        }
    }

    cpu_loop_exit_noexc(cpu);
}

#endif /* CONFIG_USER_ONLY */

/*
 * Called by generic code at e.g. cpu reset after cpu creation,
 * therefore we must be prepared to allocate the jump cache.
 */
void tcg_flush_jmp_cache(CPUState *cpu)
{
    CPUJumpCache *jc = cpu->tb_jmp_cache;

    /* During early initialization, the cache may not yet be allocated. */
    if (unlikely(jc == NULL)) {
        return;
    }

    for (int i = 0; i < TB_JMP_CACHE_SIZE; i++) {
        qatomic_set(&jc->array[i].tb, NULL);
    }
}

4.3 https://github.com/bbaranoff/qosmo/blob/main/contrib/hors-qemu/cales-qemu.c

3136 octets, 72 lignes → 72 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * QEMU shims that let the DSP run outside QEMU.
 *
 * [2026-09-16] Measured before writing a line: across the 26631 lines of the
 * C54x layer 1, the hook into QEMU comes to FOURTEEN symbols, eight of them in
 * calypso_full_pcb.c and calypso_tint0.c, i.e. the wiring rather than the DSP.
 * The calypso_c54x.c core itself needs TWO (the mutexes), and
 * calypso_{arm2dsp,dma,fbsb,mailbox,rhea_dma,rif,twl3025}.c need NONE. That is
 * what makes this binary possible.
 *
 * Nothing here models QEMU: these are POSIX equivalents, chosen so the DSP code
 * stays untouched. The day a shim has to become something other than a pthread,
 * the DSP has started depending on QEMU and that must be noticed.
 */
#include <pthread.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <stdio.h>

/* -- clock ---------------------------------------------------------------- */
int64_t qemu_clock_get_ns(int type)
{
    struct timespec ts;
    (void)type;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec;
}

/* -- mutexes / condvars / threads: plain pthread --------------------------- */
void qemu_mutex_init(pthread_mutex_t *m) { pthread_mutex_init(m, NULL); }
void qemu_mutex_lock_func(pthread_mutex_t *m, const char *f, int l)
{ (void)f; (void)l; pthread_mutex_lock(m); }
void qemu_mutex_unlock_impl(pthread_mutex_t *m, const char *f, int l)
{ (void)f; (void)l; pthread_mutex_unlock(m); }
void qemu_cond_init(pthread_cond_t *c) { pthread_cond_init(c, NULL); }
void qemu_cond_signal(pthread_cond_t *c) { pthread_cond_signal(c); }
void qemu_cond_wait_func(pthread_cond_t *c, pthread_mutex_t *m,
                         const char *f, int l)
{ (void)f; (void)l; pthread_cond_wait(c, m); }

void qemu_thread_create(void *thread, const char *name,
                        void *(*start)(void *), void *arg, int mode)
{
    pthread_t t;
    (void)name; (void)mode;
    pthread_create(&t, NULL, start, arg);
    if (thread) {
        memcpy(thread, &t, sizeof(t));
    }
}

/* -- timers: the harness keeps time itself, so these shims are inert ------- */
void timer_init_full(void *ts, void *lg, int type, int scale,
                     int attributes, void (*cb)(void *), void *opaque)
{ (void)ts; (void)lg; (void)type; (void)scale; (void)attributes;
  (void)cb; (void)opaque; }
void timer_mod(void *ts, int64_t expire) { (void)ts; (void)expire; }
void timer_mod_ns(void *ts, int64_t expire) { (void)ts; (void)expire; }

/* -- I/O plumbing: without QEMU there is no IRQ line and no main loop ------ */
void qemu_set_irq(void *irq, int level) { (void)irq; (void)level; }
void qemu_set_fd_handler(int fd, void *rd, void *wr, void *opaque)
{ (void)fd; (void)rd; (void)wr; (void)opaque; }
void qemu_notify_event(void) { }

/* -- glib, when it is not linked in ---------------------------------------- */
void *g_malloc(size_t n) { void *p = malloc(n ? n : 1); if (!p) abort(); return p; }
void *g_malloc0(size_t n) { void *p = calloc(1, n ? n : 1); if (!p) abort(); return p; }
void g_free(void *p) { free(p); }

4.4 https://github.com/bbaranoff/qosmo/blob/main/contrib/hors-qemu/doublures/exec/cpu-common.h

439 octets, 9 lignes → 9 lignes

#ifndef QOSMO_DOUBLURE_CPU_COMMON_H
#define QOSMO_DOUBLURE_CPU_COMMON_H
#include <stdbool.h>
#include <stdint.h>
/* The "guest" memory is provided by the harness (see cales-qemu.c). */
void cpu_physical_memory_rw(uint64_t addr, void *buf, uint64_t len, bool is_write);
#define cpu_physical_memory_read(a, b, l)  cpu_physical_memory_rw(a, b, l, false)
#define cpu_physical_memory_write(a, b, l) cpu_physical_memory_rw(a, b, l, true)
#endif

4.5 https://github.com/bbaranoff/qosmo/blob/main/contrib/hors-qemu/doublures/exec/memory.h

559 octets, 12 lignes → 12 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
/* Stand-in for exec/memory.h.
 * [2026-09-16] calypso_trx.h includes it for ONE type, used only in the
 * calypso_trx_init() prototype. The real header drags in the whole QEMU memory
 * subsystem (memop -> host-utils -> bswap -> rcu -> ramlist): 112 errors on
 * calypso_bsp.c. An opaque type is enough. */
#ifndef QOSMO_DOUBLURE_MEMORY_H
#define QOSMO_DOUBLURE_MEMORY_H
typedef struct MemoryRegion MemoryRegion;
typedef struct MemoryRegionOps MemoryRegionOps;
typedef struct AddressSpace AddressSpace;
#endif

4.6 https://github.com/bbaranoff/qosmo/blob/main/contrib/hors-qemu/doublures/hw/irq.h

348 octets, 9 lignes → 9 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef QOSMO_DOUBLURE_IRQ_H
#define QOSMO_DOUBLURE_IRQ_H
/* Without QEMU there is no interrupt line: levels go nowhere. */
typedef struct IRQState *qemu_irq;
void qemu_set_irq(qemu_irq irq, int level);
#define qemu_irq_raise(i) qemu_set_irq(i, 1)
#define qemu_irq_lower(i) qemu_set_irq(i, 0)
#endif

4.7 https://github.com/bbaranoff/qosmo/blob/main/contrib/hors-qemu/doublures/qemu/atomic.h

348 octets, 7 lignes → 7 lignes

#ifndef QOSMO_DOUBLURE_ATOMIC_H
#define QOSMO_DOUBLURE_ATOMIC_H
#define qatomic_read(p)      __atomic_load_n(p, __ATOMIC_RELAXED)
#define qatomic_set(p, v)    __atomic_store_n(p, v, __ATOMIC_RELAXED)
#define qatomic_inc(p)       __atomic_fetch_add(p, 1, __ATOMIC_RELAXED)
#define smp_mb()             __atomic_thread_fence(__ATOMIC_SEQ_CST)
#endif

4.8 https://github.com/bbaranoff/qosmo/blob/main/contrib/hors-qemu/doublures/qemu/bswap.h

1515 octets, 33 lignes → 31 lignes (2 groupes compactés)

/* SPDX-License-Identifier: GPL-2.0-or-later */
/* Stub for qemu/bswap.h: only the accessors the gr-gsm L1 actually uses.
 * The real header drags in host-utils, then rcu and ramlist. */
#ifndef QOSMO_DOUBLURE_BSWAP_H
#define QOSMO_DOUBLURE_BSWAP_H
#include <stdint.h>
#include <string.h>
static inline uint16_t lduw_be_p(const void *p)
{ uint16_t v; memcpy(&v, p, 2); return __builtin_bswap16(v); }
static inline uint32_t ldl_be_p(const void *p)
{ uint32_t v; memcpy(&v, p, 4); return __builtin_bswap32(v); }
static inline void stw_be_p(void *p, uint16_t v)
{ v = __builtin_bswap16(v); memcpy(p, &v, 2); }
static inline void stl_be_p(void *p, uint32_t v)
{ v = __builtin_bswap32(v); memcpy(p, &v, 4); }
static inline uint16_t lduw_le_p(const void *p)
{ uint16_t v; memcpy(&v, p, 2); return v; }
static inline uint32_t ldl_le_p(const void *p)
{ uint32_t v; memcpy(&v, p, 4); return v; }
static inline void stw_le_p(void *p, uint16_t v) { memcpy(p, &v, 2); }
static inline void stl_le_p(void *p, uint32_t v) { memcpy(p, &v, 4); }
#define be16_to_cpu(x) __builtin_bswap16(x)
#define be32_to_cpu(x) __builtin_bswap32(x)
#define cpu_to_be16(x) __builtin_bswap16(x)
#define cpu_to_be32(x) __builtin_bswap32(x)
/* Native little-endian on x86/ARM: these conversions are no-ops. */
#define le⟨1⟩_to_cpu(x) ((uint⟨2⟩_t)(x))  ×3
    ⟨⟩ = (16,16) (32,32) (64,64)
#define cpu_to_le⟨1⟩(x) ((uint⟨2⟩_t)(x))  ×3
    ⟨⟩ = (16,16) (32,32) (64,64)
#endif

4.9 https://github.com/bbaranoff/qosmo/blob/main/contrib/hors-qemu/doublures/qemu/log.h

276 octets, 8 lignes → 8 lignes

#ifndef QOSMO_DOUBLURE_LOG_H
#define QOSMO_DOUBLURE_LOG_H
#include <stdio.h>
#define qemu_log(...)      fprintf(stderr, __VA_ARGS__)
#define qemu_log_mask(m, ...) do { (void)(m); fprintf(stderr, __VA_ARGS__); } while (0)
#define LOG_UNIMP   0
#define LOG_GUEST_ERROR 0
#endif

4.10 https://github.com/bbaranoff/qosmo/blob/main/contrib/hors-qemu/doublures/qemu/main-loop.h

421 octets, 11 lignes → 11 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef QOSMO_DOUBLURE_MAINLOOP_H
#define QOSMO_DOUBLURE_MAINLOOP_H
typedef void IOHandler(void *opaque);
void qemu_set_fd_handler(int fd, IOHandler *rd, IOHandler *wr, void *opaque);
void qemu_notify_event(void);
#define qemu_mutex_lock_iothread() ((void)0)
#define qemu_mutex_unlock_iothread() ((void)0)
#define bql_lock() ((void)0)
#define bql_unlock() ((void)0)
#endif

4.11 https://github.com/bbaranoff/qosmo/blob/main/contrib/hors-qemu/doublures/qemu/module.h

251 octets, 6 lignes → 6 lignes

#ifndef QOSMO_DOUBLURE_MODULE_H
#define QOSMO_DOUBLURE_MODULE_H
/* Turn type_init() into a plain ELF constructor: same effect, without QOM. */
#define type_init(fn) \
    static void __attribute__((constructor)) qosmo_ctor_##fn(void) { fn(); }
#endif

4.12 https://github.com/bbaranoff/qosmo/blob/main/contrib/hors-qemu/doublures/qemu/osdep.h

1014 octets, 35 lignes → 35 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
/* Stand-in for qemu/osdep.h, to build the Calypso sources OUTSIDE QEMU.
 * [2026-09-16] Measured before writing this file: the 12 sources that include
 * it use nothing from osdep.h but the standard C headers. */
#ifndef QOSMO_DOUBLURE_OSDEP_H
#define QOSMO_DOUBLURE_OSDEP_H
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <stddef.h>
#include <inttypes.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <math.h>
#include <assert.h>
/* The real osdep.h exposes this through _GNU_SOURCE; calypso_invariants.c uses it. */
extern char **environ;

typedef uint64_t hwaddr;
#ifndef MIN
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#endif
#ifndef MAX
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#endif
#define g_assert_not_reached() abort()
void *g_malloc(size_t n);
void *g_malloc0(size_t n);
void g_free(void *p);
#define g_new0(t, n) ((t *)g_malloc0(sizeof(t) * (n)))
#endif

4.13 https://github.com/bbaranoff/qosmo/blob/main/contrib/hors-qemu/doublures/qemu/thread.h

975 octets, 21 lignes → 21 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef QOSMO_DOUBLURE_THREAD_H
#define QOSMO_DOUBLURE_THREAD_H
#include <pthread.h>
typedef pthread_mutex_t QemuMutex;
typedef pthread_cond_t  QemuCond;
typedef struct { pthread_t t; } QemuThread;
void qemu_mutex_init(QemuMutex *m);
void qemu_mutex_lock_func(QemuMutex *m, const char *f, int l);
void qemu_mutex_unlock_impl(QemuMutex *m, const char *f, int l);
void qemu_cond_init(QemuCond *c);
void qemu_cond_signal(QemuCond *c);
void qemu_cond_wait_func(QemuCond *c, QemuMutex *m, const char *f, int l);
void qemu_thread_create(QemuThread *th, const char *name,
                        void *(*start)(void *), void *arg, int mode);
#define qemu_mutex_lock(m)   qemu_mutex_lock_func(m, __FILE__, __LINE__)
#define qemu_mutex_unlock(m) qemu_mutex_unlock_impl(m, __FILE__, __LINE__)
#define qemu_cond_wait(c, m) qemu_cond_wait_func(c, m, __FILE__, __LINE__)
#define QEMU_THREAD_JOINABLE 0
#define QEMU_THREAD_DETACHED 1
#endif

4.14 https://github.com/bbaranoff/qosmo/blob/main/contrib/hors-qemu/doublures/qemu/timer.h

764 octets, 17 lignes → 17 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef QOSMO_DOUBLURE_TIMER_H
#define QOSMO_DOUBLURE_TIMER_H
#include <stdint.h>
/* Timers are inert outside QEMU: the harness drives the beat. */
typedef struct QEMUTimer { int vide; } QEMUTimer;
typedef enum { QEMU_CLOCK_REALTIME, QEMU_CLOCK_VIRTUAL, QEMU_CLOCK_HOST } QEMUClockType;
#define SCALE_NS 1
#define SCALE_US 1000
#define SCALE_MS 1000000
int64_t qemu_clock_get_ns(int type);
void timer_init_full(QEMUTimer *ts, void *lg, int type, int scale,
                     int attributes, void (*cb)(void *), void *opaque);
void timer_mod(QEMUTimer *ts, int64_t expire);
void timer_mod_ns(QEMUTimer *ts, int64_t expire);
#define timer_new_ns(type, cb, opaque) ((QEMUTimer *)g_malloc0(sizeof(QEMUTimer)))
#endif

4.15 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_armio.c

13009 octets, 409 lignes → 401 lignes (1 groupes compactés)

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * calypso_armio.c - Calypso ARMIO (0xFFFE4800): GPIO + matrix keypad.
 *
 * The Compal E88 / Motorola C123 keypad is modelled the way the firmware
 * sees it (calypso/keypad.c):
 *   - KBC (0x0c)        : driven columns, active low (bits 0-4); 0 = all
 *                          (idle state, where the IRQ is useful), 0xff = none;
 *   - KBR_LATCH (0x0a)  : rows read back, active low (bits 0-4): a key
 *                          (column c, row r) pulls row r to 0 while column c
 *                          is driven;
 *   - IRQ_KEYPAD_GPIO (8) : high level as long as a key is pressed AND all
 *                          columns are driven (KBC = 0). The firmware masks
 *                          the IRQ, scans column by column, then re-enables
 *                          it once everything is released.
 * Key number = column * 5 + row (board/compal/keymap.h: KEY_OK = 0,
 * KEY_UP = 1, ..., KEY_1 = 15, KEY_0 = 13, KEY_POWER = 24).
 *
 * Two input sources:
 *   - a Unix datagram socket (CALYPSO_KEYS_SOCK, default /tmp/calypso_keys.sock,
 *     "none" to not open it), one command per datagram:
 *         press <key> | release <key> | tap <key> [ms]
 *     <key>: 0-9, star, hash, menu, lsb, rsb, up, down, left, right, ok,
 *     power, or k<N> (raw number). This is what osmo-operator/tools/
 *     osmo-c123-gtk.py speaks (phone picture with clickable keys);
 *   - the keyboard of the QEMU graphical console (-display sdl): digits,
 *     arrows, Enter = menu, keypad Enter = ok, Esc = power, F1/F2 = soft
 *     keys, s = *, h = #.
 */

#include "qemu/osdep.h"
#include "qemu/log.h"
#include "qemu/timer.h"
#include "qemu/main-loop.h"
#include "hw/sysbus.h"
#include "hw/irq.h"
#include "ui/input.h"
#include "qom/object.h"
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <fcntl.h>

#define TYPE_CALYPSO_ARMIO "calypso-armio"
OBJECT_DECLARE_SIMPLE_TYPE(CalypsoArmioState, CALYPSO_ARMIO)

#define ARMIO_LATCH_IN      0x00
#define ARMIO_LATCH_OUT     0x02
#define ARMIO_IO_CNTL       0x04
#define ARMIO_KBR_LATCH     0x0a
#define ARMIO_KBC           0x0c
#define ARMIO_GPIO_INT      0x16
#define ARMIO_GPIO_MASKIT   0x18

#define KEYS_SOCK_DEFAULT   "/tmp/calypso_keys.sock"
#define TAP_MS_DEFAULT      150
#define KEY_BITS            25          /* 5 columns x 5 rows */

struct CalypsoArmioState {
    SysBusDevice parent_obj;
    MemoryRegion iomem;
    qemu_irq irq;
    uint16_t latch_out, io_cntl, kbc, gpio_int, gpio_maskit;
    uint32_t keys;          /* bit c*5+r : touche enfoncee */
    uint32_t tap_mask;      /* touches a relacher a l'echeance de tap_timer */
    QEMUTimer *tap_timer;
    int sock;
    char *sock_path;
    bool debug;
};

static const struct { const char *name; int bit; } key_names[] = {
    { "ok", 0 }, { "up", 1 }, { "down", 2 }, { "left", 3 }, { "right", 4 },
    { "3", 5 }, { "6", 6 }, { "9", 7 }, { "hash", 8 }, { "#", 8 }, { "rsb", 9 },
    { "2", 10 }, { "5", 11 }, { "8", 12 }, { "0", 13 }, { "menu", 14 },
    { "1", 15 }, { "4", 16 }, { "7", 17 }, { "star", 18 }, { "*", 18 },
    { "lsb", 19 }, { "power", 24 },
};

static int armio_key_bit(const char *name)
{
    unsigned i;
    for (i = 0; i < ARRAY_SIZE(key_names); i++) {
        if (!strcmp(key_names[i].name, name)) {
            return key_names[i].bit;
        }
    }
    if (name[0] == 'k' && name[1] >= '0' && name[1] <= '9') {
        int b = atoi(name + 1);
        return (b >= 0 && b < KEY_BITS) ? b : -1;
    }
    return -1;
}

static const char *armio_key_name(int bit)
{
    unsigned i;
    for (i = 0; i < ARRAY_SIZE(key_names); i++) {
        if (key_names[i].bit == bit) {
            return key_names[i].name;
        }
    }
    return "?";
}

/* The Magnetite firmware (kpd driver, BOARD 41) masks the source through AI_MaskIT (bit
 * ARMIO_MASKIT_KBD = 1 in ARMIO_KBD_GPIO_MASKIT, 0x18), then activates a HISR that reads the
 * matrix. The mask must therefore gate the IRQ line, otherwise the CPU re-enters the IRQ in a
 * loop and the HISR never runs. The source is also mirrored in ARMIO_GPIO_INT (0x16). */
static void armio_update_irq(CalypsoArmioState *s)
{
    int active = (s->keys != 0) && ((s->kbc & 0x1f) == 0);
    int level = active && !(s->gpio_maskit & 0x1);
    s->gpio_int = active ? 0x1 : 0;
    qemu_set_irq(s->irq, level);
}

static void armio_set_key(CalypsoArmioState *s, int bit, bool down)
{
    uint32_t m = 1u << bit;
    if (down) {
        s->keys |= m;
    } else {
        s->keys &= ~m;
        s->tap_mask &= ~m;
    }
    if (s->debug) {
        fprintf(stderr, "[armio] touche %s (k%d) %s -> keys=0x%07x kbc=0x%02x\n",
                armio_key_name(bit), bit, down ? "enfoncee" : "relachee",
                s->keys, s->kbc);
    }
    armio_update_irq(s);
}

static void armio_tap_expire(void *opaque)
{
    CalypsoArmioState *s = opaque;
    uint32_t m = s->tap_mask;
    int b;
    s->tap_mask = 0;
    for (b = 0; b < KEY_BITS; b++) {
        if (m & (1u << b)) {
            armio_set_key(s, b, false);
        }
    }
}

static void armio_command(CalypsoArmioState *s, char *line)
{
    char *cmd, *name, *arg, *save = NULL;
    int bit;

    cmd = strtok_r(line, " \t\r\n", &save);
    if (!cmd) {
        return;
    }
    name = strtok_r(NULL, " \t\r\n", &save);
    arg = strtok_r(NULL, " \t\r\n", &save);
    if (!name) {
        fprintf(stderr, "[armio] commande sans touche : « %s »\n", cmd);
        return;
    }
    bit = armio_key_bit(name);
    if (bit < 0) {
        fprintf(stderr, "[armio] touche inconnue : « %s »\n", name);
        return;
    }
    if (!strcmp(cmd, "press") || !strcmp(cmd, "down")) {
        armio_set_key(s, bit, true);
    } else if (!strcmp(cmd, "release") || !strcmp(cmd, "up")) {
        armio_set_key(s, bit, false);
    } else if (!strcmp(cmd, "tap")) {
        int ms = arg ? atoi(arg) : TAP_MS_DEFAULT;
        if (ms <= 0) {
            ms = TAP_MS_DEFAULT;
        }
        armio_set_key(s, bit, true);
        s->tap_mask |= 1u << bit;
        timer_mod(s->tap_timer, qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) + ms);
    } else {
        fprintf(stderr, "[armio] commande inconnue : « %s » (press|release|tap)\n", cmd);
    }
}

static void armio_sock_read(void *opaque)
{
    CalypsoArmioState *s = opaque;
    char buf[256];
    ssize_t n;

    while ((n = recv(s->sock, buf, sizeof(buf) - 1, 0)) > 0) {
        char *p, *nl;
        buf[n] = '\0';
        for (p = buf; p && *p; p = nl) {
            nl = strchr(p, '\n');
            if (nl) {
                *nl++ = '\0';
            }
            if (*p) {
                armio_command(s, p);
            }
        }
    }
}

static void armio_sock_open(CalypsoArmioState *s)
{
    const char *env = getenv("CALYPSO_KEYS_SOCK");
    struct sockaddr_un addr;
    int fd;

    s->sock = -1;
    s->sock_path = g_strdup(env && *env ? env : KEYS_SOCK_DEFAULT);
    if (!strcmp(s->sock_path, "none")) {
        return;
    }
    if (strlen(s->sock_path) >= sizeof(addr.sun_path)) {
        fprintf(stderr, "[armio] chemin de socket trop long : %s\n", s->sock_path);
        return;
    }
    fd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (fd < 0) {
        fprintf(stderr, "[armio] socket : %s\n", strerror(errno));
        return;
    }
    unlink(s->sock_path);
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strcpy(addr.sun_path, s->sock_path);
    if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        fprintf(stderr, "[armio] bind %s : %s\n", s->sock_path, strerror(errno));
        close(fd);
        return;
    }
    chmod(s->sock_path, 0666);
    fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
    s->sock = fd;
    qemu_set_fd_handler(fd, armio_sock_read, NULL, s);
    fprintf(stderr, "[armio] clavier C123 : socket %s (press|release|tap <touche> [ms])\n",
            s->sock_path);
}

/* ---- keyboard of the QEMU graphical console (-display sdl) -------------- */

static int armio_qcode_bit(int qcode)
{
    switch (qcode) {
    case Q_KEY_CODE_⟨1⟩: case Q_KEY_CODE_KP_⟨2⟩: return ⟨3⟩;  ×10
    ⟨⟩ = (0,0,13) (1,1,15) (2,2,10) (3,3,5) (4,4,16) (5,5,11) (6,6,6) (7,7,17) (8,8,12) (9,9,7)
    case Q_KEY_CODE_KP_MULTIPLY: case Q_KEY_CODE_S: return 18;
    case Q_KEY_CODE_H: return 8;
    case Q_KEY_CODE_UP: return 1;
    case Q_KEY_CODE_DOWN: return 2;
    case Q_KEY_CODE_LEFT: return 3;
    case Q_KEY_CODE_RIGHT: return 4;
    case Q_KEY_CODE_RET: return 14;
    case Q_KEY_CODE_KP_ENTER: case Q_KEY_CODE_O: return 0;
    case Q_KEY_CODE_ESC: case Q_KEY_CODE_P: return 24;
    case Q_KEY_CODE_F1: return 19;
    case Q_KEY_CODE_F2: return 9;
    default: return -1;
    }
}

static void armio_input_event(DeviceState *dev, QemuConsole *src, InputEvent *evt)
{
    CalypsoArmioState *s = CALYPSO_ARMIO(dev);
    InputKeyEvent *key = evt->u.key.data;
    int bit = armio_qcode_bit(qemu_input_key_value_to_qcode(key->key));
    if (bit >= 0) {
        armio_set_key(s, bit, key->down);
    }
}

static const QemuInputHandler armio_input_handler = {
    .name  = "Calypso C123 keypad",
    .mask  = INPUT_EVENT_MASK_KEY,
    .event = armio_input_event,
};

/* ---- registres ------------------------------------------------------------ */

static uint32_t armio_guest_pc(void)
{
    return (first_cpu && first_cpu->cc && first_cpu->cc->get_pc) ? (uint32_t)first_cpu->cc->get_pc(first_cpu) : 0;
}

static uint64_t armio_read(void *opaque, hwaddr offset, unsigned size)
{
    CalypsoArmioState *s = opaque;
    uint32_t rows = 0;
    int col;

    switch (offset) {
    case ARMIO_LATCH_IN:
        return 0;
    case ARMIO_LATCH_OUT:
        return s->latch_out;
    case ARMIO_IO_CNTL:
        return s->io_cntl;
    case ARMIO_KBR_LATCH:
        for (col = 0; col < 5; col++) {
            if (!(s->kbc & (1u << col))) {
                rows |= (s->keys >> (col * 5)) & 0x1f;
            }
        }
        if (s->debug) {
            static int nlec;
            if (nlec++ < 40 || nlec % 2000 == 0) {
                fprintf(stderr, "[armio] lecture KBR=0x%02x keys=0x%07x kbc=0x%02x (x%d) pc=0x%08x\n",
                        (~rows) & 0x1f, s->keys, s->kbc, nlec, armio_guest_pc());
            }
        }
        return (~rows) & 0x1f;
    case ARMIO_KBC:
        return s->kbc;
    case ARMIO_GPIO_INT:
        return s->gpio_int;
    case ARMIO_GPIO_MASKIT:
        return s->gpio_maskit;
    default:
        return 0;
    }
}

static void armio_write(void *opaque, hwaddr offset, uint64_t value, unsigned size)
{
    CalypsoArmioState *s = opaque;

    switch (offset) {
    case ARMIO_LATCH_OUT:
        s->latch_out = value;
        break;
    case ARMIO_IO_CNTL:
        s->io_cntl = value;
        break;
    case ARMIO_KBC:
        s->kbc = value & 0xff;
        if (s->debug) {
            static int necr;
            if (necr++ < 40 || necr % 2000 == 0) {
                fprintf(stderr, "[armio] ecriture KBC=0x%02x (x%d) pc=0x%08x\n", s->kbc, necr, armio_guest_pc());
            }
        }
        armio_update_irq(s);
        break;
    case ARMIO_GPIO_INT:
        s->gpio_int = value;
        break;
    case ARMIO_GPIO_MASKIT:
        s->gpio_maskit = value;
        armio_update_irq(s);
        break;
    default:
        break;
    }
}

static const MemoryRegionOps armio_ops = {
    .read = armio_read,
    .write = armio_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
    .impl = { .min_access_size = 2, .max_access_size = 2 },
};

static void armio_realize(DeviceState *dev, Error **errp)
{
    CalypsoArmioState *s = CALYPSO_ARMIO(dev);
    const char *dbg = getenv("CALYPSO_KEYS_DEBUG");

    s->debug = dbg && *dbg && strcmp(dbg, "0");
    s->kbc = 0;
    s->keys = 0;
    memory_region_init_io(&s->iomem, OBJECT(dev), &armio_ops, s, "calypso.armio", 0x100);
    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->iomem);
    sysbus_init_irq(SYS_BUS_DEVICE(dev), &s->irq);
    s->tap_timer = timer_new_ms(QEMU_CLOCK_VIRTUAL, armio_tap_expire, s);
    armio_sock_open(s);
    qemu_input_handler_register(dev, &armio_input_handler);
}

static void armio_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    dc->realize = armio_realize;
    dc->desc = "Calypso ARMIO (GPIO + clavier matriciel C123)";
}

static const TypeInfo armio_info = {
    .name          = TYPE_CALYPSO_ARMIO,
    .parent        = TYPE_SYS_BUS_DEVICE,
    .instance_size = sizeof(CalypsoArmioState),
    .class_init    = armio_class_init,
};

static void armio_register_types(void)
{
    type_register_static(&armio_info);
}

type_init(armio_register_types)

4.16 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_asm4532.c

3028 octets, 76 lignes → 76 lignes

/*
 * calypso_asm4532.c - ASM4532 antenna switch.
 *
 * Driven by the TSPACT lines, not by the serial TSP. Line mapping taken
 * as-is from osmocom-bb board/compal/rffe_dualband.c (C123 board):
 *   TRENA    = TSPACT(6)  Transmit Enable (antenna switch)  -- ACTIVE LOW
 *   GSM_TXEN = TSPACT(8)  GSM as opposed to DCS             -- ACTIVE LOW
 *
 * This model is PASSIVE: it latches the state, counts TX windows and reports
 * inconsistencies. It gates nothing, because the downlink is synthetic and
 * gating Rx on the switch position would only break working benches. It
 * becomes useful once the uplink is really transmitted: it is what will tell
 * whether the antenna was switched over during the burst.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#include "qemu/osdep.h"
#include <stdio.h>
#include "hw/arm/calypso/calypso_asm4532.h"
#include "hw/arm/calypso/calypso_debug.h"

/* TSPACT lines, ported from the firmware (TSPACT(n) = 1 << n). */
#define ASM_TRENA       (1u << 6)   /* active LOW */
#define ASM_GSM_TXEN    (1u << 8)   /* active LOW */

#define ASM_LOG(fmt, ...) \
    do { if (calypso_debug_enabled("TPU")) \
        fprintf(stderr, "[asm4532] " fmt "\n", ##__VA_ARGS__); } while (0)

static struct {
    bool     init;
    uint16_t tspact;        /* last state seen */
    bool     tx;            /* TRENA asserted    -> antenna on the PA */
    bool     gsm;           /* GSM_TXEN asserted -> GSM900 band */
    uint32_t tx_windows;    /* number of TX windows opened */
} asm4532;

void calypso_asm4532_tspact_update(uint16_t tspact, uint32_t fn)
{
    bool tx_new, gsm_new;

    /* ACTIVE LOW: the line is asserted when the bit reads 0. */
    tx_new  = !(tspact & ASM_TRENA);
    gsm_new = !(tspact & ASM_GSM_TXEN);

    if (!asm4532.init) {
        asm4532.init = true;
        ASM_LOG("etat initial : tspact=0x%04x TX=%d bande=%s fn=%u",
                tspact, tx_new, gsm_new ? "GSM900" : "DCS1800", fn);
    }

    if (tx_new != asm4532.tx) {
        if (tx_new) {
            asm4532.tx_windows++;
        }
        ASM_LOG("TRENA %s -> antenne sur %s (bande %s) fn=%u  [fenetre #%u]",
                tx_new ? "ASSERTEE" : "relachee",
                tx_new ? "le PA (TX)" : "le chemin Rx",
                gsm_new ? "GSM900" : "DCS1800", fn, asm4532.tx_windows);
    } else if (gsm_new != asm4532.gsm && tx_new) {
        /* Band change DURING a TX window: the firmware does not do this,
         * rffe_mode() sets both lines together. Report it rather than
         * absorbing it silently. */
        ASM_LOG("⚠ bande changee PENDANT la fenetre TX : %s -> %s fn=%u",
                asm4532.gsm ? "GSM900" : "DCS1800",
                gsm_new ? "GSM900" : "DCS1800", fn);
    }

    asm4532.tspact = tspact;
    asm4532.tx     = tx_new;
    asm4532.gsm    = gsm_new;
}

bool     calypso_asm4532_tx_connected(void) { return asm4532.tx; }
bool     calypso_asm4532_band_gsm(void)     { return asm4532.gsm; }
uint32_t calypso_asm4532_tx_windows(void)   { return asm4532.tx_windows; }

4.17 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_comp128.c

10678 octets, 252 lignes → 252 lignes

/* calypso_comp128.c - COMP128v1 for the emulated SIM card (calypso_sim.c, RUN GSM ALGORITHM).
 * Copy of libosmocore src/gsm/comp128.c (QEMU does not link libosmogsm), so that the Ki loaded
 * from mobile.cfg yields the SRES/Kc the MSC expects (COMP128v1). */
/*! \file comp128.c
 *  COMP128 v1; common/old GSM Authentication Algorithm (A3/A8).
 *
 * This code is inspired by original code from :
 *  Marc Briceno <marc@scard.org>, Ian Goldberg <iang@cs.berkeley.edu>,
 *  and David Wagner <daw@cs.berkeley.edu>
 *
 * But it has been fully rewritten from various PDFs found online describing
 * the algorithm because the licence of the code referenced above was unclear.
 * A comment snippet from the original code is included below, it describes
 * where the doc came from and how the algorithm was reverse engineered.
 *
 * This code derived from a leaked document from the GSM standards.
 * Some missing pieces were filled in by reverse-engineering a working SIM.
 * We have verified that this is the correct COMP128 algorithm.
 *
 * The first page of the document identifies it as
 *
 *  _Technical Information: GSM System Security Study_.
 *  10-1617-01, 10th June 1988.
 *
 * The bottom of the title page is marked
 *
 *  Racal Research Ltd.
 *  Worton Drive, Worton Grange Industrial Estate,
 *  Reading, Berks. RG2 0SB, England.
 *  Telephone: Reading (0734) 868601   Telex: 847152
 *
 * The relevant bits are in Part I, Section 20 (pages 66--67).  Enjoy!
 *
 * Note: There are three typos in the spec (discovered by
 * reverse-engineering).
 * - First, "z = (2 * x[n] + x[n]) mod 2^(9-j)" should clearly read
 *   "z = (2 * x[m] + x[n]) mod 2^(9-j)".
 * - Second, the "k" loop in the "Form bits from bytes" section is severely
 *   botched: the k index should run only from 0 to 3, and clearly the range
 *   on "the (8-k)th bit of byte j" is also off (should be 0..7, not 1..8,
 *   to be consistent with the subsequent section).
 * - Third, SRES is taken from the first 8 nibbles of x[], not the last 8 as
 *   claimed in the document.  (And the document doesn't specify how Kc is
 *   derived, but that was also easily discovered with reverse engineering.)
 * All of these typos have been corrected in the following code.
 */
/*
 * (C) 2009 by Sylvain Munaut <tnt@246tNt.com>
 *
 * All Rights Reserved
 *
 * SPDX-License-Identifier: GPL-2.0+
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 */

#include <string.h>
#include <stdint.h>
void comp128v1(const uint8_t *ki, const uint8_t *rand, uint8_t *sres, uint8_t *kc);
void comp128(const uint8_t *ki, const uint8_t *rand, uint8_t *sres, uint8_t *kc);

/*! \addtogroup auth
 *  @{
 * \file comp128.c */

/* The compression tables (just copied ...) */
static const uint8_t table_0[512] = {
 102, 177, 186, 162,   2, 156, 112,  75,  55,  25,   8,  12, 251, 193, 246, 188,
 109, 213, 151,  53,  42,  79, 191, 115, 233, 242, 164, 223, 209, 148, 108, 161,
 252,  37, 244,  47,  64, 211,   6, 237, 185, 160, 139, 113,  76, 138,  59,  70,
  67,  26,  13, 157,  63, 179, 221,  30, 214,  36, 166,  69, 152, 124, 207, 116,
 247, 194,  41,  84,  71,   1,  49,  14,  95,  35, 169,  21,  96,  78, 215, 225,
 182, 243,  28,  92, 201, 118,   4,  74, 248, 128,  17,  11, 146, 132, 245,  48,
 149,  90, 120,  39,  87, 230, 106, 232, 175,  19, 126, 190, 202, 141, 137, 176,
 250,  27, 101,  40, 219, 227,  58,  20,  51, 178,  98, 216, 140,  22,  32, 121,
  61, 103, 203,  72,  29, 110,  85, 212, 180, 204, 150, 183,  15,  66, 172, 196,
  56, 197, 158,   0, 100,  45, 153,   7, 144, 222, 163, 167,  60, 135, 210, 231,
 174, 165,  38, 249, 224,  34, 220, 229, 217, 208, 241,  68, 206, 189, 125, 255,
 239,  54, 168,  89, 123, 122,  73, 145, 117, 234, 143,  99, 129, 200, 192,  82,
 104, 170, 136, 235,  93,  81, 205, 173, 236,  94, 105,  52,  46, 228, 198,   5,
  57, 254,  97, 155, 142, 133, 199, 171, 187,  50,  65, 181, 127, 107, 147, 226,
 184, 218, 131,  33,  77,  86,  31,  44,  88,  62, 238,  18,  24,  43, 154,  23,
  80, 159, 134, 111,   9, 114,   3,  91,  16, 130,  83,  10, 195, 240, 253, 119,
 177, 102, 162, 186, 156,   2,  75, 112,  25,  55,  12,   8, 193, 251, 188, 246,
 213, 109,  53, 151,  79,  42, 115, 191, 242, 233, 223, 164, 148, 209, 161, 108,
  37, 252,  47, 244, 211,  64, 237,   6, 160, 185, 113, 139, 138,  76,  70,  59,
  26,  67, 157,  13, 179,  63,  30, 221,  36, 214,  69, 166, 124, 152, 116, 207,
 194, 247,  84,  41,   1,  71,  14,  49,  35,  95,  21, 169,  78,  96, 225, 215,
 243, 182,  92,  28, 118, 201,  74,   4, 128, 248,  11,  17, 132, 146,  48, 245,
  90, 149,  39, 120, 230,  87, 232, 106,  19, 175, 190, 126, 141, 202, 176, 137,
  27, 250,  40, 101, 227, 219,  20,  58, 178,  51, 216,  98,  22, 140, 121,  32,
 103,  61,  72, 203, 110,  29, 212,  85, 204, 180, 183, 150,  66,  15, 196, 172,
 197,  56,   0, 158,  45, 100,   7, 153, 222, 144, 167, 163, 135,  60, 231, 210,
 165, 174, 249,  38,  34, 224, 229, 220, 208, 217,  68, 241, 189, 206, 255, 125,
  54, 239,  89, 168, 122, 123, 145,  73, 234, 117,  99, 143, 200, 129,  82, 192,
 170, 104, 235, 136,  81,  93, 173, 205,  94, 236,  52, 105, 228,  46,   5, 198,
 254,  57, 155,  97, 133, 142, 171, 199,  50, 187, 181,  65, 107, 127, 226, 147,
 218, 184,  33, 131,  86,  77,  44,  31,  62,  88,  18, 238,  43,  24,  23, 154,
 159,  80, 111, 134, 114,   9,  91,   3, 130,  16,  10,  83, 240, 195, 119, 253,
}, table_1[256] = {
  19,  11,  80, 114,  43,   1,  69,  94,  39,  18, 127, 117,  97,   3,  85,  43,
  27, 124,  70,  83,  47,  71,  63,  10,  47,  89,  79,   4,  14,  59,  11,   5,
  35, 107, 103,  68,  21,  86,  36,  91,  85, 126,  32,  50, 109,  94, 120,   6,
  53,  79,  28,  45,  99,  95,  41,  34,  88,  68,  93,  55, 110, 125, 105,  20,
  90,  80,  76,  96,  23,  60,  89,  64, 121,  56,  14,  74, 101,   8,  19,  78,
  76,  66, 104,  46, 111,  50,  32,   3,  39,   0,  58,  25,  92,  22,  18,  51,
  57,  65, 119, 116,  22, 109,   7,  86,  59,  93,  62, 110,  78,  99,  77,  67,
  12, 113,  87,  98, 102,   5,  88,  33,  38,  56,  23,   8,  75,  45,  13,  75,
  95,  63,  28,  49, 123, 120,  20, 112,  44,  30,  15,  98, 106,   2, 103,  29,
  82, 107,  42, 124,  24,  30,  41,  16, 108, 100, 117,  40,  73,  40,   7, 114,
  82, 115,  36, 112,  12, 102, 100,  84,  92,  48,  72,  97,   9,  54,  55,  74,
 113, 123,  17,  26,  53,  58,   4,   9,  69, 122,  21, 118,  42,  60,  27,  73,
 118, 125,  34,  15,  65, 115,  84,  64,  62,  81,  70,   1,  24, 111, 121,  83,
 104,  81,  49, 127,  48, 105,  31,  10,   6,  91,  87,  37,  16,  54, 116, 126,
  31,  38,  13,   0,  72, 106,  77,  61,  26,  67,  46,  29,  96,  37,  61,  52,
 101,  17,  44, 108,  71,  52,  66,  57,  33,  51,  25,  90,   2, 119, 122,  35,
}, table_2[128] = {
 52,  50,  44,   6,  21,  49,  41,  59,  39,  51,  25,  32,  51,  47,  52,  43,
 37,   4,  40,  34,  61,  12,  28,   4,  58,  23,   8,  15,  12,  22,   9,  18,
 55,  10,  33,  35,  50,   1,  43,   3,  57,  13,  62,  14,   7,  42,  44,  59,
 62,  57,  27,   6,   8,  31,  26,  54,  41,  22,  45,  20,  39,   3,  16,  56,
 48,   2,  21,  28,  36,  42,  60,  33,  34,  18,   0,  11,  24,  10,  17,  61,
 29,  14,  45,  26,  55,  46,  11,  17,  54,  46,   9,  24,  30,  60,  32,   0,
 20,  38,   2,  30,  58,  35,   1,  16,  56,  40,  23,  48,  13,  19,  19,  27,
 31,  53,  47,  38,  63,  15,  49,   5,  37,  53,  25,  36,  63,  29,   5,   7,
}, table_3[64] = {
  1,   5,  29,   6,  25,   1,  18,  23,  17,  19,   0,   9,  24,  25,   6,  31,
 28,  20,  24,  30,   4,  27,   3,  13,  15,  16,  14,  18,   4,   3,   8,   9,
 20,   0,  12,  26,  21,   8,  28,   2,  29,   2,  15,   7,  11,  22,  14,  10,
 17,  21,  12,  30,  26,  27,  16,  31,  11,   7,  13,  23,  10,   5,  22,  19,
}, table_4[32] = {
 15,  12,  10,   4,   1,  14,  11,   7,   5,   0,  14,   7,   1,   2,  13,   8,
 10,   3,   4,   9,   6,   0,   3,   2,   5,   6,   8,   9,  11,  13,  15,  12,
};

static const uint8_t *_comp128_table[5] = { table_0, table_1, table_2, table_3, table_4 };


static inline void
_comp128_compression_round(uint8_t *x, int n, const uint8_t *tbl)
{
    int i, j, m, a, b, y, z;
    m = 4 - n;
    for (i=0; i<(1<<n); i++)
        for (j=0; j<(1<<m); j++) {
            a = j + i * (2<<m);
            b = a + (1<<m);
            y = (x[a] + (x[b]<<1)) & ((32<<m)-1);
            z = ((x[a]<<1) + x[b]) & ((32<<m)-1);
            x[a] = tbl[y];
            x[b] = tbl[z];
        }
}

static inline void
_comp128_compression(uint8_t *x)
{
    int n;
    for (n=0; n<5; n++)
        _comp128_compression_round(x, n, _comp128_table[n]);
}

static inline void
_comp128_bitsfrombytes(uint8_t *x, uint8_t *bits)
{
    int i;
    memset(bits, 0x00, 128);
    for (i=0; i<128; i++)
        if (x[i>>2] & (1<<(3-(i&3))))
            bits[i] = 1;
}

static inline void
_comp128_permutation(uint8_t *x, uint8_t *bits)
{
    int i;
    memset(&x[16], 0x00, 16);
    for (i=0; i<128; i++)
        x[(i>>3)+16] |= bits[(i*17) & 127] << (7-(i&7));
}

/*! Perform COMP128v1 algorithm
 *  \param[in] ki Secret Key K(i) of subscriber
 *  \param[in] rand Random Challenge
 *  \param[out] sres user-supplied buffer for storing computed SRES value
 *  \param[out] kc user-supplied buffer for storing computed Kc value */
void
comp128v1(const uint8_t *ki, const uint8_t *rand, uint8_t *sres, uint8_t *kc)
{
    int i;
    uint8_t x[32], bits[128];

    /* x[16-31] = RAND */
    memcpy(&x[16], rand, 16);

    /* Round 1-7 */
    for (i=0; i<7; i++) {
        /* x[0-15] = Ki */
        memcpy(x, ki, 16);

        /* Compression */
        _comp128_compression(x);

        /* FormBitFromBytes */
        _comp128_bitsfrombytes(x, bits);

        /* Permutation */
        _comp128_permutation(x, bits);
    }

    /* Round 8 (final) */
        /* x[0-15] = Ki */
    memcpy(x, ki, 16);

        /* Compression */
    _comp128_compression(x);

    /* Output stage */
    for (i=0; i<8; i+=2)
        sres[i>>1] = x[i]<<4 | x[i+1];

    for (i=0; i<12; i+=2)
        kc[i>>1] = (x[i + 18] << 6) |
                   (x[i + 19] << 2) |
                   (x[i + 20] >> 2);

    kc[6] = (x[30]<<6) | (x[31]<<2);
    kc[7] = 0;
}


/*! Perform COMP128v1 algorithm
 *  \param[in] ki Secret Key K(i) of subscriber
 *  \param[in] rand Random Challenge
 *  \param[out] sres user-supplied buffer for storing computed SRES value
 *  \param[out] kc user-supplied buffer for storing computed Kc value */
void
comp128(const uint8_t *ki, const uint8_t *rand, uint8_t *sres, uint8_t *kc)
{
    comp128v1(ki, rand, sres, kc);
}

/*! @} */

4.18 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_dcch_tap.c

12265 octets, 295 lignes → 295 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * calypso_dcch_tap.c - which dedicated channel the mobile uses, for the bridge.
 *
 * pont.py does not read the IMMEDIATE ASSIGNMENT itself: its Dedicated class
 * (pont/state.py) waits to be told the channel through
 * /dev/shm/calypso_dcch_cfg, and as long as that file does not exist,
 * uplink.py:_poll_sdcch() drops every uplink block, including the SABM that
 * carries the LOCATION UPDATING REQUEST.
 *
 * In the gr-gsm setup, l1-grgsm/calypso_l1ctl_tap.c writes it, hooked through
 * the layer 1 vtable (uart_tx_byte). Under CALYPSO_DSP_EXTERN=1 that layer 1
 * is disabled (calypso_l1_disable), so the file must be written from here:
 * the SABM block is captured by c54x_exe (src/montant.c) and published in
 * /dev/shm/calypso_sdcch_ul, and the bridge only emits it once it knows the
 * channel.
 *
 * This tap depends on no layer 1: it sniffs the sercomm stream the firmware
 * sends to osmocon and only keeps the channel number of the
 * L1CTL_DATA_CONF / DATA_IND. It publishes only the side-band; the reactions
 * specific to the gr-gsm layer 1 (a_cd window, DCCH guard) stay there.
 */
#include "qemu/osdep.h"
#include "qemu/bswap.h"
#include "hw/arm/calypso/calypso_dcch_tap.h"
#include "hw/arm/calypso/calypso_trx.h"
#include <fcntl.h>
#include <unistd.h>

#define SERCOMM_FLAG        0x7E
#define SERCOMM_ESCAPE      0x7D
#define SERCOMM_ESCAPE_XOR  0x20
#define SERCOMM_DLCI_L1CTL  5

#define L1CTL_DATA_IND      0x03
#define L1CTL_RACH_CONF     0x0c
#define L1CTL_DATA_CONF     0x0f
/* PDCH: the GPRS firmware (layer1 l1gprs_fw.c) does not report DATA_IND on a
 * PDTCH but L1CTL_GPRS_DL_BLOCK_IND (hdr fn BE32 + tn). */
#define L1CTL_GPRS_DL_BLOCK_IND 0x23
#define L1CTL_GPRS_UL_BLOCK_CNF 0x26

/* Downlink PDTCH counter, to separate the ROM losses from the other causes of
 * T3190/T3168 (banc-max "data"). The firmware (l1gprs_fw.c l1gprs_fw_dl_block)
 * reports ALL the PDCH blocks: payload = l1ctl_hdr(4) fn(4) tn(1) pad(3)
 * ber10k(2) ci(2) rx_lev(1) usf(1) [+ 23 bytes if CRC good AND our TFI or a
 * control block]. usf != 0xff only if the CRC is good. Hence:
 *   donnees   = 41 bytes (PTCCH excluded)   autre_tfi = 18 bytes, usf != 0xff
 *   crc_ko    = 18 bytes, usf == 0xff, fn%104 != 12 (block lost by the ROM)
 *   ptcch     = fn%104 == 12                ul_cnf    = L1CTL_GPRS_UL_BLOCK_CNF
 * A "[pdtch]" line on stderr (qemu.log) every 100 blocks and whenever a PDCH is
 * armed. */
static struct {
    unsigned donnees, autre_tfi, crc_ko, ptcch, ul_cnf;
    unsigned long ber_ko;
} pdtch;

static void pdtch_journal(const char *pourquoi)
{
    fprintf(stderr, "[pdtch] %s : dl donnees=%u autre_tfi=%u crc_ko=%u ptcch=%u | ul_cnf=%u | ber moyen crc_ko=%.1f%%\n",
            pourquoi, pdtch.donnees, pdtch.autre_tfi, pdtch.crc_ko, pdtch.ptcch, pdtch.ul_cnf,
            pdtch.crc_ko ? (double)pdtch.ber_ko / pdtch.crc_ko / 100.0 : 0.0);
}

#define SHM_DCCH_CFG        "/dev/shm/calypso_dcch_cfg"
#define DCCH_RELEASED       0xFF

/* How many frames must separate the last block of the dedicated channel from a
 * block on a common channel before concluding that the channel is released. A
 * late CCCH block, delivered just after the switch, carries a frame number
 * close to those of the dedicated channel: it must not release anything. */
#define DCCH_LIBERE_APRES_TRAMES  100

static struct {
    enum { SC_IDLE, SC_IN_FRAME, SC_ESCAPE } state;
    uint8_t buf[512];
    int len;
    uint8_t dernier_chan_nr;
    uint32_t seq;
    uint32_t fn_dedie;       /* last frame seen on the dedicated channel */
    bool     fn_dedie_vue;
    uint8_t  trx_chan_nr;    /* last SDCCH (or 0xFF) told to calypso_trx */
} tap = { .dernier_chan_nr = 0xFF, .trx_chan_nr = 0xFF };

/* TCH kinds. The readers (montant.c scruter_dcch, pont/dsp/dedicated.py)
 * understand them in addition to 0/1/0xFF; the grgsm pont.py never reads this
 * tap (its layer 1 writes the file itself). */
#define DCCH_TCH_F          2
#define DCCH_TCH_H          3
/* kind 4 = PDCH (chan_nr 0b11000TTT, Osmocom extension): the BSP plays ALL the
 * frames of the timeslot, like a TCH (calypso_bsp.c). */
#define DCCH_PDCH           4

/* 16 bytes, as expected by pont/state.py:Dedicated.read():
 * seq(4) kind(1) ss(1) tn(1) chan_nr(1). kind 0 = SDCCH/4, 1 = SDCCH/8,
 * 2 = TCH/F, 3 = TCH/H (ss = half-rate subslot), 0xFF = channel released. */
static void dcch_cfg_publier(int kind, int ss, uint8_t chan_nr)
{
    uint8_t b[16] = {0};
    tap.seq++;
    memcpy(b, &tap.seq, 4);
    b[4] = (uint8_t)kind;
    b[5] = (uint8_t)ss;
    b[6] = chan_nr & 0x07;
    b[7] = chan_nr;
    int fd = open(SHM_DCCH_CFG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd < 0) {
        return;
    }
    if (write(fd, b, sizeof(b)) < 0) {
        close(fd);
        return;
    }
    close(fd);
    /* calypso_trx (PONT_DCCH -> pont.c -> calypso_bsp_set_dedie) is only told
     * about a new SDCCH or the release. The TCH is not passed on: switching
     * the BSP to the TCH is the job of montant.c (suivre_tache_tch), which a
     * PONT_DCCH of kind 2 would bypass. The return to the SAME SDCCH after
     * the TCH (ASSIGNMENT FAILURE) is not passed on either. */
    if (kind != DCCH_TCH_F && kind != DCCH_TCH_H && chan_nr != tap.trx_chan_nr) {
        tap.trx_chan_nr = kind == DCCH_RELEASED ? 0xFF : chan_nr;
        calypso_trx_dcch(kind, ss, chan_nr & 0x07);
    }
    if (kind == DCCH_PDCH) {
        fprintf(stderr, "[dcch] canal dedie arme : chan_nr=0x%02x PDCH TN=%d (GPRS)\n", chan_nr, chan_nr & 7);
        return;
    }
    if (kind == DCCH_TCH_F || kind == DCCH_TCH_H) {
        fprintf(stderr, "[dcch] canal dedie arme : chan_nr=0x%02x TCH/%c SS=%d TN=%d\n",
                chan_nr, kind == DCCH_TCH_F ? 'F' : 'H', ss, chan_nr & 7);
        return;
    }
    fprintf(stderr, "[dcch] canal dedie %s : chan_nr=0x%02x SDCCH/%d SS=%d TN=%d\n",
            kind == DCCH_RELEASED ? "libere" : "arme", chan_nr,
            kind == 1 ? 8 : 4, ss, chan_nr & 7);
}

static void trame_complete(void)
{
    if (tap.len < 3 || tap.buf[0] != SERCOMM_DLCI_L1CTL) {
        return;
    }
    const uint8_t *charge = &tap.buf[2];
    int plen = tap.len - 2;
    uint8_t type = charge[0];
    if (type == L1CTL_RACH_CONF && plen >= 12) {
        /* The RACH transmit frame, as reported by the firmware. */
        calypso_trx_rach_conf(ldl_be_p(charge + 8));
        return;
    }
    if (type == L1CTL_GPRS_DL_BLOCK_IND && plen >= 9) {
        /* Downlink PDTCH block: l1ctl_hdr(4) fn(4, BE) tn(1). The firmware is on
         * the PDCH of this TN: the BSP must play all its frames. Same
         * "publish on change" logic as the dedicated channels. */
        uint8_t tn = charge[8] & 0x07;
        uint8_t chan_nr = 0xC0 | tn;
        {
            uint32_t bfn = ldl_be_p(charge + 4);
            if (bfn % 104 == 12) {
                pdtch.ptcch++;
            } else if (plen >= 41) {
                pdtch.donnees++;
            } else if (plen >= 18 && charge[17] != 0xff) {
                pdtch.autre_tfi++;
            } else if (plen >= 18) {
                pdtch.crc_ko++;
                pdtch.ber_ko += ((unsigned)charge[12] << 8) | charge[13];
                if (pdtch.crc_ko <= 5)
                    fprintf(stderr, "[pdtch] bloc DL fn=%u TN=%d perdu (CRC faux, ber %u.%02u%%)\n", bfn, tn,
                            (((unsigned)charge[12] << 8) | charge[13]) / 100,
                            (((unsigned)charge[12] << 8) | charge[13]) % 100);
            }
            unsigned total = pdtch.donnees + pdtch.autre_tfi + pdtch.crc_ko + pdtch.ptcch;
            if (total % 100 == 0)
                pdtch_journal("releve");
        }
        tap.fn_dedie = ldl_be_p(charge + 4);
        tap.fn_dedie_vue = true;
        if (chan_nr != tap.dernier_chan_nr) {
            tap.dernier_chan_nr = chan_nr;
            dcch_cfg_publier(DCCH_PDCH, 0, chan_nr);
            pdtch_journal("PDCH arme");
        }
        return;
    }
    if (type == L1CTL_GPRS_UL_BLOCK_CNF) {
        pdtch.ul_cnf++;     /* does not touch dernier_chan_nr */
        return;
    }
    if ((type != L1CTL_DATA_CONF && type != L1CTL_DATA_IND) || plen < 5) {
        return;
    }
    /* chan_nr, 44.004 8.3: 0b001SSTTT = SDCCH/4, 0b01SSSTTT = SDCCH/8,
     * 0b00001TTT = TCH/F, 0b0001STTT = TCH/H.
     *
     * The TCH is a dedicated channel like any other: after the assignment
     * (DM_EST_REQ chan_nr=0x0a, TCH/F TS2), the DATA_IND/CONF of the FACCH
     * and of the SACCH/T carry 0x0a, and the tap announces the TCH (kind
     * 2/3). The SDCCH that was left is not "released" (0xFF remains the end
     * of the connection: pont/dsp/uplink.py _poll_release, montant.c) and
     * the readers keep it in memory for the ASSIGNMENT FAILURE case, where
     * the firmware returns to it and the tap republishes it (chan_nr
     * changes). */
    uint8_t chan_nr = charge[4];
    int kind = -1, ss = 0;
    if ((chan_nr & 0xE0) == 0x20) {
        kind = 0;
        ss = (chan_nr >> 3) & 0x03;
    } else if ((chan_nr & 0xC0) == 0x40) {
        kind = 1;
        ss = (chan_nr >> 3) & 0x07;
    } else if ((chan_nr & 0xF8) == 0x08) {
        kind = DCCH_TCH_F;
    } else if ((chan_nr & 0xF0) == 0x10) {
        kind = DCCH_TCH_H;
        ss = (chan_nr >> 3) & 0x01;
    }
    uint32_t fn = (plen >= 12) ? ldl_be_p(charge + 8) : 0;
    if (kind >= 0) {
        tap.fn_dedie = fn;
        tap.fn_dedie_vue = true;
        if (chan_nr != tap.dernier_chan_nr) {
            tap.dernier_chan_nr = chan_nr;
            dcch_cfg_publier(kind, ss, chan_nr);
        }
        return;
    }
    /* The release is detected here, not on d_dsp_page: the firmware does an
     * l1s_dsp_abort() (sync.c:308) precisely when it switches to the
     * dedicated channel ("resetting scheduler" in the mobile log, right
     * after the IMMEDIATE ASSIGNMENT), so d_dsp_page returning to 0 does not
     * mean the channel is released.
     *
     * The reliable signal is a block received or sent on a NON-dedicated
     * channel (BCCH, CCCH): the mobile is back on the common channels. In
     * dedicated mode it reads none, so this cannot happen in the middle of a
     * connection. */
    /* And only for a REAL common channel: 44.004 8.3 gives 0x80 BCCH,
     * 0x88 RACH, 0x90 PCH/AGCH. A chan_nr of 0x00 is not a channel and must
     * not release anything, otherwise the BSP would switch between TS0 and
     * TS1 at every block during the connection and no dedicated downlink
     * would be decodable. */
    if (tap.dernier_chan_nr != 0xFF && (chan_nr & 0xE0) == 0x80) {
        /* And not at the first CCCH block that comes: layer 1 still delivers
         * one or two of them just after the switch to the dedicated channel.
         * Releasing there would cut the dedicated downlink in the middle of
         * the establishment, so the common block is required to be clearly
         * LATER than the last block of the dedicated channel. */
        uint32_t ecart = tap.fn_dedie_vue ? ((fn - tap.fn_dedie) % 2715648u) : 0xFFFFFFFFu;
        if (!tap.fn_dedie_vue || (ecart >= DCCH_LIBERE_APRES_TRAMES && ecart < 2715648u / 2)) {
            tap.dernier_chan_nr = 0xFF;
            tap.fn_dedie_vue = false;
            dcch_cfg_publier(DCCH_RELEASED, 0, 0);
        }
    }
}

void calypso_dcch_tap_tx_byte(uint8_t byte)
{
    switch (tap.state) {
    case SC_IDLE:
        if (byte == SERCOMM_FLAG) {
            tap.state = SC_IN_FRAME;
            tap.len = 0;
        }
        break;
    case SC_IN_FRAME:
        if (byte == SERCOMM_FLAG) {
            if (tap.len > 0) {
                trame_complete();
            }
            tap.len = 0;
        } else if (byte == SERCOMM_ESCAPE) {
            tap.state = SC_ESCAPE;
        } else if (tap.len < (int)sizeof(tap.buf)) {
            tap.buf[tap.len++] = byte;
        }
        break;
    case SC_ESCAPE:
        if (tap.len < (int)sizeof(tap.buf)) {
            tap.buf[tap.len++] = byte ^ SERCOMM_ESCAPE_XOR;
        }
        tap.state = SC_IN_FRAME;
        break;
    }
}

void calypso_dcch_tap_reset(void)
{
    if (tap.dernier_chan_nr == 0xFF) {
        return;
    }
    tap.dernier_chan_nr = 0xFF;
    dcch_cfg_publier(DCCH_RELEASED, 0, 0);
}

4.19 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_dcch_tap.h

361 octets, 12 lignes → 12 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef HW_ARM_CALYPSO_DCCH_TAP_H
#define HW_ARM_CALYPSO_DCCH_TAP_H

#include <stdint.h>

/* Byte emitted by the firmware on the modem UART (sercomm). */
void calypso_dcch_tap_tx_byte(uint8_t ch);
/* The firmware reset its layer 1: the dedicated channel is released. */
void calypso_dcch_tap_reset(void);

#endif

4.20 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_debug.c

5811 octets, 176 lignes → 176 lignes

/*
 * calypso_debug.c - env-gated probe lookup implementation
 *
 * One env var: CALYPSO_DEBUG="probe1,probe2,probe3,...".
 * A list containing "ALL" enables every probe.
 *
 * Lookup is O(N) over the list, parsed once on first call; negligible for the
 * 10-20 probes typically active.
 *
 * Normalisation: probe name upper-cased, '-' ' ' '.' '/' mapped to '_', so
 * "IMR-W" matches the entry "IMR_W" as well as "imr-w".
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#include "qemu/osdep.h"
#include "hw/arm/calypso/calypso_debug.h"

#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <pthread.h>

#define MAX_ENTRIES   128
#define NAME_MAX_     64

static char     s_entries[MAX_ENTRIES][NAME_MAX_];
static int      s_entries_n = 0;
static bool     s_all = false;
static bool     s_inited = false;
static pthread_mutex_t s_mu = PTHREAD_MUTEX_INITIALIZER;

/* Master gate: -1 = not initialised yet, 0 = CALYPSO_DEBUG empty (every probe
 * OFF, fast path inlined in the header), 1 = at least one probe active. */
int calypso_debug_master = -1;

/* Normalize a probe name in place: upper-case, separators to '_'. */
static void normalize(char *s)
{
    for (; *s; s++) {
        char c = *s;
        if (c == '-' || c == ' ' || c == '/' || c == '.') c = '_';
        *s = toupper((unsigned char)c);
    }
}

static void parse_env_locked(void)
{
    if (s_inited) return;
    s_inited = true;

    const char *e = getenv("CALYPSO_DEBUG");
    if (!e || !*e) return;

    /* Walk comma-separated tokens. */
    const char *p = e;
    while (*p && s_entries_n < MAX_ENTRIES) {
        /* Skip leading separators (= comma, space, tab). */
        while (*p == ',' || *p == ' ' || *p == '\t') p++;
        if (!*p) break;
        const char *start = p;
        while (*p && *p != ',') p++;
        size_t len = p - start;
        if (len == 0) continue;
        /* Trim trailing whitespace. */
        while (len > 0 && (start[len-1] == ' ' || start[len-1] == '\t')) len--;
        if (len >= NAME_MAX_) len = NAME_MAX_ - 1;
        memcpy(s_entries[s_entries_n], start, len);
        s_entries[s_entries_n][len] = '\0';
        normalize(s_entries[s_entries_n]);
        if (strcmp(s_entries[s_entries_n], "ALL") == 0) {
            s_all = true;
        }
        s_entries_n++;
    }

    fprintf(stderr,
        "[calypso-debug] CALYPSO_DEBUG parsed: %d probe(s), ALL=%d\n",
        s_entries_n, s_all);
    for (int i = 0; i < s_entries_n; i++) {
        fprintf(stderr, "[calypso-debug]   - %s\n", s_entries[i]);
    }
}

/* One-shot init of the master gate: parses the env and sets
 * calypso_debug_master. Called from the inline calypso_debug_enabled() in the
 * header on first use. */
void calypso_debug_master_init(void)
{
    pthread_mutex_lock(&s_mu);
    parse_env_locked();
    calypso_debug_master = (s_all || s_entries_n > 0) ? 1 : 0;
    pthread_mutex_unlock(&s_mu);
}

/* Out-of-line implementation. Only reached with master == 1, so
 * parse_env_locked() has already run; the !s_inited block is belt and braces. */
bool calypso_debug_enabled_(const char *probe_name)
{
    if (!probe_name) return false;

    if (!s_inited) {
        pthread_mutex_lock(&s_mu);
        parse_env_locked();
        pthread_mutex_unlock(&s_mu);
    }

    if (s_all) return true;
    if (s_entries_n == 0) return false;

    /* Normalize probe_name into local buffer for compare. */
    char norm[NAME_MAX_];
    size_t n = strlen(probe_name);
    if (n >= NAME_MAX_) n = NAME_MAX_ - 1;
    memcpy(norm, probe_name, n);
    norm[n] = '\0';
    normalize(norm);

    for (int i = 0; i < s_entries_n; i++) {
        if (strcmp(s_entries[i], norm) == 0) return true;
    }
    return false;
}

/* calypso_gate - see calypso_debug.h for the semantics and the rationale.
 *
 * No cache: a caller that wants to memoise already does so in its own `static
 * int`. Caching HERE would prevent changing a gate at runtime and would hide
 * the cases where two modules read the same variable at different times. */
int calypso_gate(const char *nom, int defaut)
{
    const char *e = nom ? calypso_getenv(nom) : NULL;
    if (!e) {
        return defaut;          /* unset: the caller decides */
    }
    if (!*e) {
        return 0;               /* set but EMPTY = explicitly off */
    }
    /* Case-insensitive compare over the spellings people actually write in a
     * .env. Anything that is not a negation counts as yes: better to enable on
     * "yes" than to silently ignore a value the operator believed was
     * understood. */
    if (!strcasecmp(e, "0")     || !strcasecmp(e, "no") ||
        !strcasecmp(e, "off")   || !strcasecmp(e, "false") ||
        !strcasecmp(e, "n")) {
        return 0;
    }
    return 1;
}


/* ── getenv() memoised, see calypso_debug.h ────────────────────────────── */
#define GETENV_MEMO 512
static struct { const char *name; const char *val; } s_env[GETENV_MEMO];
static int s_env_n;
static pthread_mutex_t s_env_mu = PTHREAD_MUTEX_INITIALIZER;

const char *calypso_getenv(const char *name)
{
    if (!name) return NULL;
    /* fast path: string literals have one address per call site, and the
     * same variable is usually asked from the same site */
    int n = __atomic_load_n(&s_env_n, __ATOMIC_ACQUIRE);
    for (int i = 0; i < n; i++)
        if (s_env[i].name == name) return s_env[i].val;
    for (int i = 0; i < n; i++)
        if (!strcmp(s_env[i].name, name)) return s_env[i].val;
    const char *v = getenv(name);
    pthread_mutex_lock(&s_env_mu);
    if (s_env_n < GETENV_MEMO) {
        s_env[s_env_n].name = name;
        s_env[s_env_n].val  = v;
        __atomic_store_n(&s_env_n, s_env_n + 1, __ATOMIC_RELEASE);
    }
    pthread_mutex_unlock(&s_env_mu);
    return v;
}

4.21 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_i2c_ctrl.c

5399 octets, 180 lignes → 180 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * calypso_i2c_ctrl.c - the Calypso I2C controller at its real address
 * (0xFFFE2800, firmware calypso/i2c.c), backed by a real QEMU I2C bus.
 *
 * The "calypso-i2c" object of hw/ssi/calypso_i2c.c is mapped at 0xFFFE1800
 * (the RTC address, not the I2C one) and does nothing; it is kept as is (it
 * answers 4 at offset 4). This file implements the ten byte registers of
 * the real controller and the START command, which replays the transaction
 * on the bus (slave address, internal address byte, then the 16-byte FIFO).
 * The only slave on the E88 board is the ST7558 LCD (0x3c), created in
 * calypso_soc.c.
 *
 * Registers (calypso/i2c.c): 0 DEVICE, 1 ADDRESS, 2 DATA_WR, 3 DATA_RD,
 * 4 CMD (bit0 soft reset, bit1 en_clk, bit2 START, bit3 rw_read), 5 CONF_FIFO
 * (len-1), 6 CONF_CLK, 7 CONF_CLK_FUNC_REF, 8 STATUS_FIFO, 9 STATUS_ACTIVITY
 * (bit2 = 1 while the transfer is not finished: here, always finished).
 */

#include "qemu/osdep.h"
#include "qemu/log.h"
#include "hw/sysbus.h"
#include "hw/i2c/i2c.h"
#include "qom/object.h"

#define TYPE_CALYPSO_I2C_CTRL "calypso-i2c-ctrl"
OBJECT_DECLARE_SIMPLE_TYPE(CalypsoI2CCtrlState, CALYPSO_I2C_CTRL)

enum {
    REG_DEVICE = 0, REG_ADDRESS, REG_DATA_WR, REG_DATA_RD, REG_CMD,
    REG_CONF_FIFO, REG_CONF_CLK, REG_CONF_CLK_REF, REG_STATUS_FIFO,
    REG_STATUS_ACTIVITY,
};

#define CMD_SOFT_RESET  (1 << 0)
#define CMD_EN_CLK      (1 << 1)
#define CMD_START       (1 << 2)
#define CMD_RW_READ     (1 << 3)

#define STATUS_ERROR_DATA (1 << 0)
#define STATUS_ERROR_DEV  (1 << 1)
#define STATUS_BUSY       (1 << 2)

#define FIFO_LEN 16

struct CalypsoI2CCtrlState {
    SysBusDevice parent_obj;
    MemoryRegion iomem;
    I2CBus *bus;
    uint8_t device, address, cmd, conf_fifo, conf_clk, conf_clk_ref, status;
    uint8_t fifo[FIFO_LEN];
    int fifo_len;
    uint8_t rd;
};

static void i2c_ctrl_transfer(CalypsoI2CCtrlState *s)
{
    int i;

    s->status &= ~(STATUS_ERROR_DATA | STATUS_ERROR_DEV);
    if (s->cmd & CMD_RW_READ) {
        if (i2c_start_recv(s->bus, s->device) < 0) {
            s->status |= STATUS_ERROR_DEV;
        } else {
            s->rd = i2c_recv(s->bus);
            i2c_end_transfer(s->bus);
        }
    } else if (i2c_start_send(s->bus, s->device) < 0) {
        s->status |= STATUS_ERROR_DEV;
        qemu_log_mask(LOG_GUEST_ERROR, "calypso-i2c: aucun esclave en 0x%02x\n",
                      s->device);
    } else {
        i2c_send(s->bus, s->address);
        for (i = 0; i < s->fifo_len; i++) {
            i2c_send(s->bus, s->fifo[i]);
        }
        i2c_end_transfer(s->bus);
    }
    s->fifo_len = 0;
}

static uint64_t i2c_ctrl_read(void *opaque, hwaddr offset, unsigned size)
{
    CalypsoI2CCtrlState *s = opaque;

    switch (offset) {
    case REG_DEVICE:          return s->device;
    case REG_ADDRESS:         return s->address;
    case REG_DATA_RD:         return s->rd;
    case REG_CMD:             return s->cmd;
    case REG_CONF_FIFO:       return s->conf_fifo;
    case REG_CONF_CLK:        return s->conf_clk;
    case REG_CONF_CLK_REF:    return s->conf_clk_ref;
    case REG_STATUS_FIFO:     return s->fifo_len;
    case REG_STATUS_ACTIVITY: return s->status & ~STATUS_BUSY;  /* always idle */
    default:                  return 0;
    }
}

static void i2c_ctrl_write(void *opaque, hwaddr offset, uint64_t value, unsigned size)
{
    CalypsoI2CCtrlState *s = opaque;
    uint8_t v = value;

    switch (offset) {
    case REG_DEVICE:
        s->device = v & 0x7f;
        break;
    case REG_ADDRESS:
        s->address = v;
        break;
    case REG_DATA_WR:
        if (s->fifo_len < FIFO_LEN) {
            s->fifo[s->fifo_len++] = v;
        } else {
            s->status |= STATUS_ERROR_DATA;
        }
        break;
    case REG_CMD:
        if (v & CMD_SOFT_RESET) {
            s->fifo_len = 0;
            s->status = 0;
            v &= ~CMD_SOFT_RESET;
        }
        s->cmd = v & ~CMD_START;
        if (v & CMD_START) {
            i2c_ctrl_transfer(s);
        }
        break;
    case REG_CONF_FIFO:
        s->conf_fifo = v;
        break;
    case REG_CONF_CLK:
        s->conf_clk = v;
        break;
    case REG_CONF_CLK_REF:
        s->conf_clk_ref = v;
        break;
    default:
        break;
    }
}

static const MemoryRegionOps i2c_ctrl_ops = {
    .read = i2c_ctrl_read,
    .write = i2c_ctrl_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
    .impl = { .min_access_size = 1, .max_access_size = 1 },
};

static void i2c_ctrl_realize(DeviceState *dev, Error **errp)
{
    CalypsoI2CCtrlState *s = CALYPSO_I2C_CTRL(dev);

    s->bus = i2c_init_bus(dev, "i2c");
    memory_region_init_io(&s->iomem, OBJECT(dev), &i2c_ctrl_ops, s,
                          "calypso.i2c", 0x100);
    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->iomem);
}

static void i2c_ctrl_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    dc->realize = i2c_ctrl_realize;
    dc->desc = "Calypso I2C controller (0xFFFE2800)";
}

static const TypeInfo i2c_ctrl_info = {
    .name          = TYPE_CALYPSO_I2C_CTRL,
    .parent        = TYPE_SYS_BUS_DEVICE,
    .instance_size = sizeof(CalypsoI2CCtrlState),
    .class_init    = i2c_ctrl_class_init,
};

static void i2c_ctrl_register_types(void)
{
    type_register_static(&i2c_ctrl_info);
}

type_init(i2c_ctrl_register_types)

4.22 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_invariants.c

2801 octets, 94 lignes → 94 lignes

/*
 * calypso_invariants.c — screaming invariants + run manifest.
 *
 * Internal probes raise RESOLUTION, not INDEPENDENCE: a hole in the BSP model
 * is a hole in the BSP probe. Only two things earn their keep on those terms,
 * and they are what this file holds:
 *   1. a run manifest (every active CALYPSO_* forcing, logged once);
 *   2. invariants that scream on their own and act as a non-regression suite.
 * External TRUTH (is the DSP output valid GSM?) cannot come from this code —
 * that is GSMTAP into a decoder we did not write.
 */
#include "qemu/osdep.h"
#include "hw/arm/calypso/calypso_invariants.h"
#include <stdarg.h>
#include "hw/arm/calypso/calypso_debug.h"


#define INV_MAX 64
static struct { const char *tag; unsigned fails; } inv_tab[INV_MAX];
static int inv_n;
static int inv_enabled = -1;

static int inv_on(void)
{
    if (inv_enabled < 0) {
        const char *e = calypso_getenv("CALYPSO_INVARIANTS");
        inv_enabled = (e && e[0] == '1') ? 1 : 0;   /* default OFF (boot safety) */
    }
    return inv_enabled;
}

static void inv_summary(void)
{
    if (!inv_n) {
        fprintf(stderr, "[INVARIANT-SUMMARY] all invariants held this run\n");
        return;
    }
    for (int i = 0; i < inv_n; i++) {
        fprintf(stderr, "[INVARIANT-SUMMARY] %s : %u violation(s)\n",
                inv_tab[i].tag, inv_tab[i].fails);
    }
    fflush(stderr);
}

bool calypso_invariant(const char *tag, bool ok, const char *fmt, ...)
{
    if (ok || !inv_on()) {
        return ok;
    }
    int idx = -1;
    for (int i = 0; i < inv_n; i++) {
        if (!strcmp(inv_tab[i].tag, tag)) { idx = i; break; }
    }
    if (idx < 0 && inv_n < INV_MAX) {
        idx = inv_n;
        inv_tab[inv_n].tag = tag;
        inv_tab[inv_n].fails = 0;
        if (inv_n == 0) {
            atexit(inv_summary);
        }
        inv_n++;
    }
    unsigned n = (idx >= 0) ? (++inv_tab[idx].fails) : 1;
    if (n == 1) {                       /* scream ONCE per tag */
        char buf[256];
        va_list ap;
        va_start(ap, fmt);
        vsnprintf(buf, sizeof(buf), fmt, ap);
        va_end(ap);
        fprintf(stderr, "[INVARIANT-FAIL] %s : %s\n", tag, buf);
        fflush(stderr);
    }
    return ok;
}

void calypso_manifest_once(void)
{
    if (!inv_on()) { return; }   /* gate defaults to OFF */
    static int done;
    if (done) {
        return;
    }
    done = 1;
    fprintf(stderr, "[MANIFEST] ===== run manifest : active CALYPSO_* forcings =====\n");
    int n = 0;
    for (char **e = environ; e && *e; e++) {
        if (!strncmp(*e, "CALYPSO_", 8)) {
            fprintf(stderr, "[MANIFEST] %s\n", *e);
            n++;
        }
    }
    fprintf(stderr, "[MANIFEST] ===== %d CALYPSO_* variables (see artifact appendix) =====\n", n);
    fflush(stderr);
}

4.23 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_iota.c

3402 octets, 101 lignes → 101 lignes

/*
 * TWL3025 / IOTA model — implementation.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#include "qemu/osdep.h"
#include <stdio.h>
#include <string.h>
#include "hw/arm/calypso/calypso_iota.h"

#include "hw/arm/calypso/calypso_debug.h"
#define IOTA_LOG(fmt, ...) \
    do { if (calypso_debug_enabled("IOTA")) \
        fprintf(stderr, "[iota] " fmt "\n", ##__VA_ARGS__); } while (0)

/* Pending BDLENA windows queued by the TPU sequencer, waiting for a
 * matching downlink burst to arrive on the BSP. Sized for one full TDMA
 * frame's worth of slots so successive armings on different TNs queue up
 * cleanly. */
#define IOTA_PENDING_MAX 32

static struct {
    uint8_t  last_byte;     /* most recent TSP byte */
    bool     bdl_ena;       /* current state of BDLENA pin */
    bool     bul_ena;       /* current state of BULENA pin */
    uint32_t bdl_pulses;    /* total BDLENA rising edges */
    uint32_t writes_seen;
    /* Pending pulses: each holds the TN the L1 armed for. */
    uint8_t  pending_tn[IOTA_PENDING_MAX];
    int      pending_head, pending_tail;
} iota;

static int iota_pending_count(void)
{
    int n = iota.pending_tail - iota.pending_head;
    if (n < 0) n += IOTA_PENDING_MAX;
    return n;
}

static void iota_pending_push(uint8_t tn)
{
    if (iota_pending_count() >= IOTA_PENDING_MAX - 1) {
        IOTA_LOG("WARN pending queue full, dropping oldest");
        iota.pending_head = (iota.pending_head + 1) % IOTA_PENDING_MAX;
    }
    iota.pending_tn[iota.pending_tail] = tn;
    iota.pending_tail = (iota.pending_tail + 1) % IOTA_PENDING_MAX;
}

void calypso_iota_init(void)
{
    memset(&iota, 0, sizeof(iota));
    IOTA_LOG("init");
}

void calypso_iota_tsp_write(uint8_t data, uint8_t expected_tn)
{
    bool prev_bdl = iota.bdl_ena;
    bool prev_bul = iota.bul_ena;

    iota.last_byte = data;
    iota.bdl_ena   = !!(data & IOTA_TSP_BDLENA);
    iota.bul_ena   = !!(data & IOTA_TSP_BULENA);
    iota.writes_seen++;

    if (!prev_bdl && iota.bdl_ena) {
        iota.bdl_pulses++;
        iota_pending_push(expected_tn);
        if (iota.bdl_pulses <= 10 || (iota.bdl_pulses % 100) == 0) {
            IOTA_LOG("BDLENA rising edge #%u tn=%u pending=%d",
                     iota.bdl_pulses, expected_tn, iota_pending_count());
        }
    }
    if (prev_bul != iota.bul_ena && iota.writes_seen <= 10) {
        IOTA_LOG("BULENA -> %d (data=0x%02x)", iota.bul_ena, data);
    }
}

bool calypso_iota_bdl_ena(void)            { return iota.bdl_ena; }
uint32_t calypso_iota_bdl_ena_pulses(void) { return iota.bdl_pulses; }

bool calypso_iota_take_bdl_pulse(uint8_t tn)
{
    /* Walk the pending queue from oldest to newest looking for a TN
     * match. Newer pending pulses past the matched one stay queued. */
    int n = iota_pending_count();
    for (int i = 0; i < n; i++) {
        int idx = (iota.pending_head + i) % IOTA_PENDING_MAX;
        if (iota.pending_tn[idx] == tn) {
            /* Consume: shift everything from head..idx forward by 1 */
            for (int j = i; j > 0; j--) {
                int dst = (iota.pending_head + j) % IOTA_PENDING_MAX;
                int src = (iota.pending_head + j - 1) % IOTA_PENDING_MAX;
                iota.pending_tn[dst] = iota.pending_tn[src];
            }
            iota.pending_head = (iota.pending_head + 1) % IOTA_PENDING_MAX;
            return true;
        }
    }
    return false;
}

4.24 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_l1_dispatch.c

6955 octets, 210 lignes → 210 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * Platform -> layer 1 dispatch.
 *
 * Exists so that calypso_trx.c, calypso_mb.c and calypso_uart.c never name
 * the layer 1 implementation they are linked against. See calypso_l1_ops.h
 * for the reasoning behind the split.
 *
 * THE DEFAULTS ARE NOT ERRORS. With no layer 1 registered, qosmo alone is a
 * Calypso whose modem receives nothing: the osmocom-bb firmware boots, scans,
 * finds no cell, and that is the expected behaviour. It is what lets the
 * platform - boot, IRQ, TDMA, UART, SIM - be tested without dragging in
 * gr-gsm or the C54x.
 */
#include "qemu/osdep.h"
#include <stdlib.h>
#include <string.h>
#include "hw/arm/calypso/calypso_api.h"
#include "hw/arm/calypso/calypso_l1_ops.h"
#include "hw/arm/calypso/calypso_dcch_tap.h"

static const CalypsoL1Ops *l1;

/* ── The loaded firmware ─────────────────────────────────────────────────
 *
 * calypso_mb.c passes -kernel here; it is kept for the users that need a
 * firmware SYMBOL rather than a hard-coded address. The gr-gsm layer 1 has
 * its own lookup (elf_symbol/arm_read32) but is disabled with an external
 * DSP, and calypso_trx.c needs it for "last_rach". */
static const char *g_firmware_elf;

const char *calypso_firmware_elf(void)
{
    return g_firmware_elf;
}

/* Address of a symbol of the loaded ELF, 0 if not found. ELF32 little-endian
 * symbol table, as in l1-grgsm/calypso_l1_grgsm.c:elf_symbol(). */
uint32_t calypso_firmware_symbol(const char *want)
{
    if (!g_firmware_elf || !want) {
        return 0;
    }
    FILE *f = fopen(g_firmware_elf, "rb");
    if (!f) {
        return 0;
    }
    fseek(f, 0, SEEK_END);
    long sz = ftell(f);
    fseek(f, 0, SEEK_SET);
    if (sz < 52 || sz > (64L << 20)) {
        fclose(f);
        return 0;
    }
    uint8_t *b = g_malloc((size_t)sz);
    size_t got = fread(b, 1, (size_t)sz, f);
    fclose(f);
    uint32_t ret = 0;
    if (got == (size_t)sz && b[0] == 0x7f && b[1] == 'E' && b[2] == 'L' &&
        b[3] == 'F' && b[4] == 1) {
#define R16(o) ((uint32_t)b[o] | ((uint32_t)b[(o) + 1] << 8))
#define R32(o) (R16(o) | (R16((o) + 2) << 16))
        uint32_t shoff = R32(0x20), shent = R16(0x2e), shnum = R16(0x30);
        for (uint32_t si = 0; si < shnum; si++) {
            uint32_t sh = shoff + si * shent;
            if ((long)(sh + 40) > sz) {
                break;
            }
            if (R32(sh + 4) != 2) {          /* SHT_SYMTAB */
                continue;
            }
            uint32_t symoff = R32(sh + 0x10), symsz = R32(sh + 0x14);
            uint32_t link = R32(sh + 0x18), entsz = R32(sh + 0x24);
            uint32_t strsh = shoff + link * shent;
            if ((long)(strsh + 40) > sz || entsz < 16) {
                break;
            }
            uint32_t stroff = R32(strsh + 0x10), strsz = R32(strsh + 0x14);
            for (uint32_t o = 0; o + 16 <= symsz && (long)(symoff + o + 16) <= sz;
                 o += entsz) {
                uint32_t ni = R32(symoff + o);
                if (ni < strsz && !strcmp((const char *)(b + stroff + ni), want)) {
                    ret = R32(symoff + o + 4);
                    break;
                }
            }
            break;
        }
#undef R16
#undef R32
    }
    g_free(b);
    return ret;
}

void calypso_l1_register(const CalypsoL1Ops *ops)
{
    if (l1 && ops) {
        /* Two layer 1 implementations linked into the same binary: the
         * fork's meson.build picked up the other one's sources. Keep the last
         * registered so the result stays deterministic, but say so loudly. */
        fprintf(stderr, "calypso: DEUX couches 1 enregistrees (%s puis %s) - "
                        "verifier hw/arm/calypso/meson.build\n",
                l1->name ? l1->name : "?", ops->name ? ops->name : "?");
    }
    l1 = ops;
}

/* External DSP (CALYPSO_DSP_EXTERN): the C54x in c54x_exe holds layer 1, so
 * the gr-gsm shunt must neither initialise itself (its UDP listeners would
 * steal the bench's stream) nor substitute anything in the API RAM. Every
 * calypso_l1_do_* then becomes a no-op. */
void calypso_l1_disable(const char *why)
{
    if (l1) {
        fprintf(stderr, "calypso: couche 1 « %s » desactivee (%s)\n",
                l1->name ? l1->name : "?", why ? why : "");
    }
    l1 = NULL;
}

const char *calypso_l1_name(void)
{
    return (l1 && l1->name) ? l1->name : "aucune";
}

void calypso_l1_do_init(const char *firmware_elf)
{
    g_firmware_elf = firmware_elf;   /* before any return: see calypso_firmware_symbol() */
    /* First of all: with an external DSP the shunt must not even open its
     * ports, hence the check here - calypso_mb.c calls this before
     * calypso_trx_init(). */
    const char *ext = getenv("CALYPSO_DSP_EXTERN");
    if (ext && *ext) {
        calypso_l1_disable("DSP externe, CALYPSO_DSP_EXTERN");
        return;
    }
    if (l1 && l1->init) {
        l1->init(firmware_elf);
    }
}

void calypso_l1_do_frame_tick(void)
{
    if (l1 && l1->frame_tick) {
        l1->frame_tick();
    }
}

bool calypso_l1_do_api_read_override(uint32_t off, uint16_t *out)
{
    /* false = the base serves the real API RAM contents. */
    return (l1 && l1->api_read_override) ? l1->api_read_override(off, out)
                                         : false;
}

bool calypso_l1_do_si_valid(void)
{
    return (l1 && l1->si_valid) ? l1->si_valid() : false;
}

uint32_t calypso_l1_do_l1s_fn(void)
{
    /* With no layer 1, the only frame number that exists is the platform's
     * TDMA clock. Returning 0 here breaks the burst ring continuity check in
     * api_write(). */
    return (l1 && l1->l1s_fn) ? l1->l1s_fn() : calypso_trx_get_fn();
}

void calypso_l1_do_burst_written(uint16_t d_burst_d)
{
    if (l1 && l1->burst_written) {
        l1->burst_written(d_burst_d);
    }
}

void calypso_l1_do_rach_written(uint16_t d_rach, uint32_t fn)
{
    if (l1 && l1->rach_written) {
        l1->rach_written(d_rach, fn);
    }
}

void calypso_l1_do_page_written(uint16_t d_dsp_page)
{
    if (l1 && l1->page_written) {
        l1->page_written(d_dsp_page);
    }
    /* Do NOT release the dedicated channel on d_dsp_page == 0: the firmware
     * does an l1s_dsp_abort() when switching TO the dedicated channel. The
     * release is inferred from the return to a common channel, in the tap. */
}

void calypso_l1_do_uart_tx_byte(uint8_t ch)
{
    if (l1 && l1->uart_tx_byte) {
        l1->uart_tx_byte(ch);
    } else {
        /* No layer 1 registered (external DSP): the bridge tap still publishes
         * the dedicated channel, otherwise pont.py drops the uplink. */
        calypso_dcch_tap_tx_byte(ch);
    }
}

void calypso_l1_do_api_write_observed(uint32_t off, uint16_t val, unsigned size)
{
    if (l1 && l1->api_write_observed) {
        l1->api_write_observed(off, val, size);
    }
}

4.25 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_mb.c

6362 octets, 159 lignes → 159 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "qemu/osdep.h"
#include "qapi/error.h"
#include "hw/boards.h"
#include "hw/sysbus.h"
#include "hw/irq.h"
#include "hw/loader.h"
#include "hw/block/flash.h"
#include "sysemu/sysemu.h"
#include "sysemu/blockdev.h"
#include "sysemu/block-backend.h"
#include "qemu/error-report.h"
#include "exec/address-spaces.h"
#include "elf.h"
#include "target/arm/cpu.h"
#include "hw/arm/calypso/calypso_soc.h"
#include "hw/arm/calypso/calypso_uart.h"
#include "hw/arm/calypso/calypso_l1_ops.h"

#define CALYPSO_XRAM_BASE     0x01000000
#define CALYPSO_XRAM_SIZE     (8 * 1024 * 1024)
#define CALYPSO_FLASH_BASE    0x00000000
#define CALYPSO_FLASH_SIZE    (4 * 1024 * 1024)
#define CALYPSO_FLASH_SECTOR  (64 * 1024)
#define FLASH_MFR_INTEL       0x0089
#define FLASH_DEV_28F320J3    0x0018
#define FLASH_DEV_28F320C3B   0x88C4      /* Mot C139 flash (Magnetite ffs/board/dev.c) */

typedef struct CalypsoMachineState {
    MachineState parent;
    ARMCPU *cpu;
    CalypsoSoCState soc;
    MemoryRegion xram;
    MemoryRegion bootrom;
} CalypsoMachineState;

#define TYPE_CALYPSO_MACHINE MACHINE_TYPE_NAME("calypso")
OBJECT_DECLARE_SIMPLE_TYPE(CalypsoMachineState, CALYPSO_MACHINE)

static void calypso_machine_init(MachineState *machine)
{
    CalypsoMachineState *s = CALYPSO_MACHINE(machine);
    MemoryRegion *sysmem = get_system_memory();
    Error *err = NULL;

    Object *cpuobj = object_new(machine->cpu_type);
    s->cpu = ARM_CPU(cpuobj);
    if (!qdev_realize(DEVICE(cpuobj), NULL, &err)) {
        error_report_err(err);
        exit(1);
    }

    object_initialize_child(OBJECT(machine), "soc", &s->soc, TYPE_CALYPSO_SOC);
    if (!sysbus_realize(SYS_BUS_DEVICE(&s->soc), &err)) {
        error_report_err(err);
        exit(1);
    }
    sysbus_connect_irq(SYS_BUS_DEVICE(&s->soc), 0,
                       qdev_get_gpio_in(DEVICE(&s->cpu->parent_obj), ARM_CPU_IRQ));
    sysbus_connect_irq(SYS_BUS_DEVICE(&s->soc), 1,
                       qdev_get_gpio_in(DEVICE(&s->cpu->parent_obj), ARM_CPU_FIQ));

    memory_region_init_ram(&s->xram, OBJECT(&s->soc.parent_obj), "calypso.xram",
                           CALYPSO_XRAM_SIZE, &error_fatal);
    memory_region_add_subregion(sysmem, CALYPSO_XRAM_BASE, &s->xram);

    DriveInfo *dinfo = drive_get(IF_PFLASH, 0, 0);
    /* Real firmware in flash (no -kernel): the Magnetite FFS
     * (ffs/board/dev.c, CONFIG_TARGET_COMPAL table) recognises the Mot C139
     * flash by its Intel identifier 0x88C4 (28F320C3B) and places its FFS at
     * 0x3C0000, 3 x 64 KiB. */
    bool flash_boot = !machine->kernel_filename && dinfo;
    /* Width 2: the C1xx flash is a 16-bit part. With width 1 (osmocom-bb does
     * not care) the identifier read returns 0x89c4 at offset 0 and 0 at
     * offset 2, which the Magnetite FFS (ffs/board/drv.c
     * ffsdrv_device_id_read) does not recognise. */
    pflash_cfi01_register(CALYPSO_FLASH_BASE, "calypso.flash", CALYPSO_FLASH_SIZE,
                          dinfo ? blk_by_legacy_dinfo(dinfo) : NULL,
                          CALYPSO_FLASH_SECTOR, flash_boot ? 2 : 1,
                          /* without "device-width", pflash_cfi01 returns ident0<<8|ident1 at
                           * offset 0 and ident2<<8|ident3 at offset 2: place 0x0089 (Intel)
                           * and 0x88C4 (28F320C3B) there, as drv.c reads them */
                          flash_boot ? 0x00 : FLASH_MFR_INTEL,
                          flash_boot ? FLASH_MFR_INTEL : FLASH_DEV_28F320J3,
                          flash_boot ? (FLASH_DEV_28F320C3B >> 8) : 0,
                          flash_boot ? (FLASH_DEV_28F320C3B & 0xff) : 0, 0);

    uint32_t vectors[16];
    for (int i = 0; i < 8; i++) {
        vectors[i] = 0xe59ff018;
    }
    vectors[8] = 0x00820000;
    for (int i = 9; i < 16; i++) {
        vectors[i] = 0x0080001C + 4 * (i - 9);
    }
    /* Without -kernel but with a flash (-drive if=pflash,...), this is a real
     * firmware (FreeCalypso Magnetite: Compal bootloader in sector 0, image
     * at 0x10000): no fake vectors to the IRAM, the CPU starts at 0 in the
     * flash, as on a C1xx (nIBOOT, boot ROM disabled). */
    if (!flash_boot) {
        memory_region_init_ram(&s->bootrom, NULL, "calypso.bootrom", sizeof(vectors),
                               &error_fatal);
        memory_region_add_subregion_overlap(sysmem, 0x00000000, &s->bootrom, 1);
        memcpy(memory_region_get_ram_ptr(&s->bootrom), vectors, sizeof(vectors));
    } else {
        s->soc.flash_boot = true;
        calypso_uart_romload_passthrough();
        fprintf(stderr, "[calypso] demarrage dans la flash a 0x00000000 (pas de -kernel)\n");
    }

    if (machine->kernel_filename) {
        uint64_t entry;
        int ret = load_elf(machine->kernel_filename, NULL, NULL, NULL, &entry, NULL,
                           NULL, NULL, 0, EM_ARM, 1, 0);
        if (ret < 0) {
            ret = load_image_targphys(machine->kernel_filename, CALYPSO_XRAM_BASE,
                                      CALYPSO_XRAM_SIZE);
            if (ret < 0) {
                error_report("firmware illisible : '%s'", machine->kernel_filename);
                exit(1);
            }
            entry = CALYPSO_XRAM_BASE;
        }
        cpu_set_pc(CPU(s->cpu), entry);
    }

    calypso_l1_do_init(machine->kernel_filename);
}

static void calypso_machine_class_init(ObjectClass *oc, void *data)
{
    MachineClass *mc = MACHINE_CLASS(oc);
    /* The description names the L1 actually registered, or "none" when qosmo
     * runs bare (see calypso_l1_ops.h): hard-coding one L1 here is wrong as
     * soon as the machine runs under another. */
    static char desc[96];
    snprintf(desc, sizeof(desc),
             "TI Calypso baseband (firmware osmocom-bb, couche 1 : %s)",
             calypso_l1_name());
    mc->desc = desc;
    mc->init = calypso_machine_init;
    mc->max_cpus = 1;
    mc->default_cpu_type = ARM_CPU_TYPE_NAME("arm946");
    mc->default_ram_size = 0;
}

static const TypeInfo calypso_machine_info = {
    .name = TYPE_CALYPSO_MACHINE,
    .parent = TYPE_MACHINE,
    .instance_size = sizeof(CalypsoMachineState),
    .class_init = calypso_machine_class_init,
};

static void calypso_machine_register_types(void)
{
    type_register_static(&calypso_machine_info);
}

type_init(calypso_machine_register_types)

4.26 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_rf3166.c

2478 octets, 81 lignes → 81 lignes

/*
 * calypso_rf3166.c - RF3166 power amplifier.
 *
 * Enable = PA_ENABLE = TSPACT(1), ACTIVE HIGH (osmocom-bb rffe_dualband.c).
 * Order imposed by rffe_mode(): at rest `tspact &= ~PA_ENABLE`; to transmit
 * `tspact &= ~TRENA` (antenna switch) then `tspact |= PA_ENABLE`, so the PA
 * only comes up AFTER the antenna has been switched over. That ordering is
 * what this model checks.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#include "qemu/osdep.h"
#include <stdio.h>
#include "hw/arm/calypso/calypso_rf3166.h"
#include "hw/arm/calypso/calypso_asm4532.h"
#include "hw/arm/calypso/calypso_debug.h"

#define PA_ENABLE   (1u << 1)   /* TSPACT(1), active HIGH */

#define PA_LOG(fmt, ...) \
    do { if (calypso_debug_enabled("TPU")) \
        fprintf(stderr, "[rf3166] " fmt "\n", ##__VA_ARGS__); } while (0)

/* Ramp model: 0..255 -> 0..33 dBm. ⚠️ MADE UP, not calibrated. */
#define RF3166_MAX_DBM  33

static struct {
    bool     on;
    bool     apc_known;
    uint8_t  apc;
    uint32_t bursts;        /* number of PA activations */
    uint32_t faults;        /* activations with the switch not in TX position */
} pa;

void calypso_rf3166_tspact_update(uint16_t tspact, uint32_t fn)
{
    bool on_new = !!(tspact & PA_ENABLE);

    if (on_new == pa.on) {
        return;
    }

    if (on_new) {
        pa.bursts++;
        /* Cross-check against the switch: the firmware switches BEFORE
         * powering up. If the antenna is not on the PA, we transmit into
         * nothing. */
        if (!calypso_asm4532_tx_connected()) {
            pa.faults++;
            fprintf(stderr, "[rf3166] ⚠ PA_ENABLE assertee alors que l'antenne "
                    "n'est PAS commutee sur le PA (TRENA relachee) — fn=%u, "
                    "faute #%u\n", fn, pa.faults);
        } else {
            PA_LOG("PA ON  (bande %s) fn=%u  [burst #%u]",
                   calypso_asm4532_band_gsm() ? "GSM900" : "DCS1800",
                   fn, pa.bursts);
        }
    } else {
        PA_LOG("PA OFF fn=%u", fn);
    }

    pa.on = on_new;
}

void calypso_rf3166_set_apc(uint8_t apc_level)
{
    pa.apc = apc_level;
    pa.apc_known = true;
}

bool calypso_rf3166_enabled(void) { return pa.on; }

int32_t calypso_rf3166_out_dbm(void)
{
    if (!pa.on || !pa.apc_known) {
        return INT32_MIN;   /* off, or APC never set: unknown */
    }
    return ((int32_t)pa.apc * RF3166_MAX_DBM) / 255;
}

uint32_t calypso_rf3166_faults(void) { return pa.faults; }

4.27 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_sim.c

38222 octets, 1002 lignes → 1001 lignes (1 groupes compactés)

/* SPDX-License-Identifier: GPL-2.0-or-later */

#include "qemu/osdep.h"
#include "qemu/timer.h"
#include "qemu/main-loop.h"
#include "exec/cpu-common.h"
#include "hw/core/cpu.h"
#include "target/arm/cpu.h"
#include "hw/arm/calypso/calypso_sim.h"

#define SIM_LOG(...) do { fprintf(stderr, "[sim] " __VA_ARGS__); fputc('\n', stderr); } while(0)
static uint32_t sim_guest_pc(void)
{
    return (first_cpu && first_cpu->cc && first_cpu->cc->get_pc) ? (uint32_t)first_cpu->cc->get_pc(first_cpu) : 0;
}
static uint32_t sim_guest_lr(void)
{
    ARMCPU *cpu = ARM_CPU(first_cpu);
    return (uint32_t)cpu->env.regs[14];
}

#define APDU_MAX_LEN     261
#define RX_FIFO_SIZE     512
#define ATR_DELAY_NS     1000000

static const uint8_t SIM_ATR[] = { 0x3B, 0x02, 0x14, 0x50 };

typedef enum {
    EF_TRANSPARENT = 0,
    EF_LINEAR_FIXED = 1,
    EF_CYCLIC = 2,
    EF_DF = 0xF0,
    EF_MF = 0xF1,
} EfStructure;

typedef struct SimFile {
    uint16_t fid;
    uint16_t parent;
    uint8_t  structure;
    uint16_t size;
    uint8_t  rec_len;
    uint8_t  data[64];
} SimFile;

static SimFile sim_files[] = {
    { 0x3F00, 0x0000, EF_MF, 0, 0, {0} },
    { 0x7F20, 0x3F00, EF_DF, 0, 0, {0} },
    { 0x7F10, 0x3F00, EF_DF, 0, 0, {0} },
    { 0x2FE2, 0x3F00, EF_TRANSPARENT, 10, 0,
      { 0x98, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF1 } },
    { 0x6F07, 0x7F20, EF_TRANSPARENT, 9, 0,
      { 0x08, 0x09, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF1 } },
    { 0x6F30, 0x7F20, EF_TRANSPARENT, 24, 0,
      { 0x00, 0xF1, 0x10,
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF } },
    { 0x6F38, 0x7F20, EF_TRANSPARENT, 14, 0,
      { 0xFF, 0x33, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } },
    { 0x6F78, 0x7F20, EF_TRANSPARENT, 2, 0,
      { 0x00, 0x01 } },
    { 0x6FAD, 0x7F20, EF_TRANSPARENT, 4, 0,
      { 0x00, 0x00, 0x00, 0x02 } },
    { 0x6F7E, 0x7F20, EF_TRANSPARENT, 11, 0,
      { 0xFF, 0xFF, 0xFF, 0xFF,
        0xFF, 0xFF, 0xFF,
        0x00, 0x00,
        0xFF, 0x00 } },
    /* EF_BCCH (BA list): only ARFCN 1 (bit 6 of byte 0, MSB first). With 0xFF the list
     * would cover almost every ARFCN and the search would scan 1..409 at random. */
    { 0x6F74, 0x7F20, EF_TRANSPARENT, 16, 0,
      { 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } },
    { 0x6F46, 0x7F20, EF_TRANSPARENT, 17, 0,
      { 0x01, 'Q','E','M','U','-','S','I','M', ' ',' ',' ',' ',' ',' ',' ',' ' } },
    /* Required by Magnetite: the TI stack reads Kc and PHASE at activation and a
     * "file not found" there ends in "SIM Invalid". GSM 11.11:
     * Kc = 8 bytes + CKSN (0x07 = no key); PHASE 0x03 = phase 2+;
     * HPLMN 0xFF = search disabled; FPLMN empty (0xFF). */
    { 0x6F20, 0x7F20, EF_TRANSPARENT, 9, 0,
      { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07 } },
    /* ECC (112) and LP (English): read right after activation; a "file not found"
     * there makes the MMI display "SIM Invalid" (activation SIM_NO_ERROR, cause 0x80FF). */
    { 0x6FB7, 0x7F20, EF_TRANSPARENT, 3, 0,
      { 0x11, 0xF2, 0xFF } },
    { 0x6F05, 0x7F20, EF_TRANSPARENT, 1, 0,
      { 0x01 } },
    /* EF_ADN (phonebook): the SST announces ADN (service 1). Without this file the SIM
     * driver sets crdFun = SIM_NO_OPERATION -> SS_INV -> CME_ERR_SimWrong -> "SIM Invalid". */
    { 0x6FAE, 0x7F20, EF_TRANSPARENT, 1, 0,
      { 0x03 } },
    { 0x6F31, 0x7F20, EF_TRANSPARENT, 1, 0,
      { 0xFF } },
    { 0x6F7B, 0x7F20, EF_TRANSPARENT, 12, 0,
      { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF } },
    /* EF_SMS: SMS storage capacity (memory.total = number of records). Without this file
     * g_max_messages = 0 -> ALLOC_MEMORY(0) in smsidle_sms_cb -> "mfwAlloc invalid" and
     * "Messages: full". 4 free records of 176 bytes. */
    { 0x6F3C, 0x7F20, EF_LINEAR_FIXED, 704, 176,
      { 0 } },
    /* SMS init: EF SMSP (1 free record), SMSS and CBMI empty */
    { 0x6F42, 0x7F20, EF_LINEAR_FIXED, 28, 28,
      { 0xFF } },
    { 0x6F43, 0x7F20, EF_TRANSPARENT, 2, 0,
      { 0xFF, 0xFF } },
    { 0x6F45, 0x7F20, EF_TRANSPARENT, 2, 0,
      { 0xFF, 0xFF } },
    { 0x6F48, 0x7F20, EF_TRANSPARENT, 2, 0,
      { 0xFF, 0xFF } },
    /* EFs Magnetite requests: after every received SMS the phone reads EF_ACM (6F39), and
     * the whole SMS menu (read, memory status) stays on "Please wait" if any of them is
     * missing. Neutral contents (GSM 11.11): counters at zero, no tariff, empty
     * phonebooks (free records = FF). */
    { 0x6F39, 0x7F20, EF_CYCLIC, 9, 3,          /* EF_ACM: 3 records of 3 bytes, 0 */
      { 0 } },
    { 0x6F37, 0x7F20, EF_TRANSPARENT, 3, 0,     /* EF_ACMmax: 0 = no limit */
      { 0x00, 0x00, 0x00 } },
    { 0x6F41, 0x7F20, EF_TRANSPARENT, 5, 0,     /* EF_PUCT: currency and price absent */
      { 0xFF, 0xFF, 0xFF, 0x00, 0x00 } },
    { 0x6F3E, 0x7F20, EF_TRANSPARENT, 1, 0,     /* EF_GID1 */
      { 0xFF } },
    { 0x6F3F, 0x7F20, EF_TRANSPARENT, 1, 0,     /* EF_GID2 */
      { 0xFF } },
    { 0x6F3A, 0x7F10, EF_LINEAR_FIXED, 280, 28, /* EF_ADN: 10 free entries */
      { 0xFF } },
    { 0x6F3B, 0x7F10, EF_LINEAR_FIXED, 28, 28,  /* EF_FDN: 1 free entry */
      { 0xFF } },
    { 0x6F40, 0x7F10, EF_LINEAR_FIXED, 28, 28,  /* EF_MSISDN */
      { 0xFF } },
    { 0x6F44, 0x7F10, EF_CYCLIC, 140, 28,       /* EF_LND: 5 last numbers */
      { 0xFF } },
    { 0x6F4A, 0x7F10, EF_LINEAR_FIXED, 13, 13,  /* EF_EXT1 */
      { 0xFF } },
    { 0x6F4B, 0x7F10, EF_LINEAR_FIXED, 13, 13,  /* EF_EXT2 */
      { 0xFF } },
    /* CPHS (firmware built with FF_CPHS): 6F11/6F13/6F14/6F16 must exist, otherwise the
     * Messages menu (which carries the CPHS voice mail) stays on "Please wait". CPHS info
     * (6F16): phase 2, no service allocated; empty indicators (0x55 = no message / no
     * forwarding), operator name absent. */
    { 0x6F16, 0x7F20, EF_TRANSPARENT, 3, 0,     /* CPHS info: phase 2, services 00 00 */
      { 0x02, 0x00, 0x00 } },
    { 0x6F11, 0x7F20, EF_TRANSPARENT, 2, 0,     /* CPHS voice message waiting */
      { 0x55, 0x55 } },
    { 0x6F13, 0x7F20, EF_TRANSPARENT, 2, 0,     /* CPHS call forward flags */
      { 0x55, 0x55 } },
    { 0x6F14, 0x7F20, EF_TRANSPARENT, 10, 0,    /* CPHS operator name string */
      { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF } },
    { 0x6F15, 0x7F20, EF_TRANSPARENT, 2, 0,     /* CPHS customer service profile */
      { 0xFF, 0xFF } },
    { 0x6F17, 0x7F20, EF_LINEAR_FIXED, 28, 28,  /* CPHS mailbox numbers: 1 free entry */
      { 0xFF } },
};
#define SIM_FILE_COUNT (sizeof(sim_files) / sizeof(sim_files[0]))

static SimFile *find_file(uint16_t fid, uint16_t parent_pref)
{

    if (parent_pref) {
        for (size_t i = 0; i < SIM_FILE_COUNT; i++)
            if (sim_files[i].fid == fid && sim_files[i].parent == parent_pref)
                return &sim_files[i];
    }
    for (size_t i = 0; i < SIM_FILE_COUNT; i++)
        if (sim_files[i].fid == fid)
            return &sim_files[i];
    return NULL;
}

static int encode_imsi_bcd(const char *imsi_str, uint8_t out[9])
{
    int n = 0;
    while (imsi_str[n] >= '0' && imsi_str[n] <= '9') n++;
    if (n < 1 || n > 15) return -1;
    bool odd = (n & 1) != 0;
    int data_len = (n / 2) + 1;
    out[0] = (uint8_t)data_len;
    out[1] = (uint8_t)(((imsi_str[0] - '0') << 4) | (odd ? 0x9 : 0x1));
    int wpos = 2, ipos = 1;
    while (ipos < n) {
        uint8_t lo = imsi_str[ipos] - '0';
        uint8_t hi = (ipos + 1 < n) ? (imsi_str[ipos + 1] - '0') : 0xF;
        out[wpos++] = (uint8_t)((hi << 4) | lo);
        ipos += 2;
    }
    while (wpos < 9) out[wpos++] = 0xFF;
    return data_len + 1;
}

struct CalypsoSim {
    qemu_irq    irq;
    QEMUTimer  *atr_timer;
    QEMUTimer  *wt_timer;

    uint8_t     ki[16];
    bool        ki_valid;

    uint16_t    cmd, stat, conf1, conf2, maskit, it_cd;

    uint16_t    it;

    uint8_t     apdu[APDU_MAX_LEN];
    int         apdu_pos;
    int         apdu_expected;

    uint8_t     rx[RX_FIFO_SIZE];
    int         rx_head, rx_tail;

    uint16_t    selected_df;
    uint16_t    selected_ef;

    uint8_t     resp_buf[64];
    int         resp_len;

    bool        powered;
    bool        atr_manual_done;   /* ATR already delivered for this reset (TI manual mode) */
    /* card responses deferred by a transmission time (the TI driver waits for
     * the procedure byte AFTER it has finished transmitting and switched SIO
     * to reception; a synchronous response inside the DTX write arrives too
     * early and makes it restart the command) */
    bool        defer;
    uint8_t     pend[APDU_MAX_LEN + 16];
    int         pend_len;
    QEMUTimer  *resp_timer;
    /* IT TX after one character time (see calypso_sim_reg_write, DTX) */
    QEMUTimer  *tx_timer;
    int64_t     last_tx_ns;
};

G_GNUC_UNUSED
static int rx_count(CalypsoSim *s)
{
    int c = s->rx_head - s->rx_tail;
    if (c < 0) c += RX_FIFO_SIZE;
    return c;
}

static void rx_push(CalypsoSim *s, uint8_t b)
{
    if (s->defer) {
        if (s->pend_len < (int)sizeof(s->pend)) {
            s->pend[s->pend_len++] = b;
        }
        return;
    }
    int next = (s->rx_head + 1) % RX_FIFO_SIZE;
    if (next == s->rx_tail) {
        SIM_LOG("RX FIFO overflow");
        return;
    }
    s->rx[s->rx_head] = b;
    s->rx_head = next;
}

G_GNUC_UNUSED
static int rx_pop(CalypsoSim *s, uint8_t *out)
{
    if (s->rx_head == s->rx_tail) return 0;
    *out = s->rx[s->rx_tail];
    s->rx_tail = (s->rx_tail + 1) % RX_FIFO_SIZE;
    return 1;
}

static void rx_push_n(CalypsoSim *s, const uint8_t *buf, int n)
{
    for (int i = 0; i < n; i++) rx_push(s, buf[i]);
}

static void update_irq(CalypsoSim *s);

#define WT_DELAY_NS  2000000

static void fire_wt(void *opaque)
{
    CalypsoSim *s = opaque;
    if (rx_count(s) > 0) return;
    /* response being transmitted (proportional delay, resp_delai_ns): the card is
     * "talking"; on a real card the waiting time restarts at every byte -> no WT. */
    if (s->pend_len > 0) { timer_mod_ns(s->wt_timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + WT_DELAY_NS); return; }
    s->it |= CALYPSO_SIM_IT_WT;
    SIM_LOG("WT timeout fired (RX FIFO empty)");
    update_irq(s);

}

static void schedule_wt(CalypsoSim *s)
{
    if (s->wt_timer)
        timer_mod_ns(s->wt_timer,
                     qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + WT_DELAY_NS);
}

static void refresh_it_rx(CalypsoSim *s)
{
    if (rx_count(s) > 0) s->it |= CALYPSO_SIM_IT_RX;
    else                 s->it &= ~CALYPSO_SIM_IT_RX;
}

static void update_irq(CalypsoSim *s)
{
    if (!s->irq) return;
    refresh_it_rx(s);
    bool active = (s->it & ~(uint16_t)s->maskit) != 0;
    static unsigned log_count;
    static bool last_active;
    if ((active != last_active && log_count < 30) || (log_count < 10)) {
        SIM_LOG("IRQ %s  IT=0x%04x MASKIT=0x%04x rx_count=%d",
                active ? "RAISE" : "lower",
                s->it, s->maskit, rx_count(s));
        log_count++;
        last_active = active;
    }
    if (active) qemu_irq_raise(s->irq);
    else        qemu_irq_lower(s->irq);
}

#define RESP_DELAY_NS 1500000   /* ~1.5 ms: one byte at 9600 baud + margin */
/* Delay proportional to the response size. With a fixed 1.5 ms per response, the whole
 * Magnetite SMS initialisation (4 x 176 bytes of EF_SMS, SMSP, CBMI) finishes before the
 * idle screen subscribes to E_SMS_READY (smsidle_start, end of power-on): the event is
 * lost, sms_initialised stays FALSE and the Messages menu stays on "Please wait"
 * (SmsRead_R_exec_cb). A real card at 9600 baud delivers ~1.04 ms per byte.
 * CALYPSO_SIM_US_OCTET (default 1040, 0 = fixed delay RESP_DELAY_NS). */
static long us_octet(void)
{
    static long us = -1;
    if (us < 0) { const char *e = getenv("CALYPSO_SIM_US_OCTET"); us = (e && *e) ? atol(e) : 1040; }
    return us;
}

static int64_t resp_delai_ns(int n)
{
    int64_t d = (int64_t)n * us_octet() * 1000;
    return d > RESP_DELAY_NS ? d : RESP_DELAY_NS;
}

static void tx_fire(void *opaque)
{
    CalypsoSim *s = opaque;
    s->it |= CALYPSO_SIM_IT_TX;
    update_irq(s);
}

static void resp_fire(void *opaque)
{
    CalypsoSim *s = opaque;
    int i;
    bool was = s->defer;
    s->defer = false;
    for (i = 0; i < s->pend_len; i++) {
        rx_push(s, s->pend[i]);
    }
    if (s->pend_len) {
        SIM_LOG("reponse carte : %d octet(s) livres", s->pend_len);
    }
    s->pend_len = 0;
    s->defer = was && false;
    update_irq(s);
}

static void raise_rx_irq(CalypsoSim *s)
{
    if (s->defer) {
        timer_mod_ns(s->resp_timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + resp_delai_ns(s->pend_len));
        return;
    }
    update_irq(s);
}

static void deliver_atr(void *opaque)
{
    CalypsoSim *s = opaque;
    if (!s->powered) return;
    rx_push_n(s, SIM_ATR, sizeof(SIM_ATR));
    SIM_LOG("ATR queued (%zu bytes)", sizeof(SIM_ATR));
    raise_rx_irq(s);
}

G_GNUC_UNUSED
static void schedule_atr(CalypsoSim *s)
{
    timer_mod_ns(s->atr_timer,
                 qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + ATR_DELAY_NS);
}

static int build_select_response(CalypsoSim *s, SimFile *f, uint8_t *out)
{

    int p = 0;
    out[p++] = 0x00; out[p++] = 0x00;
    out[p++] = (f->size >> 8) & 0xFF;
    out[p++] =  f->size       & 0xFF;
    out[p++] = (f->fid >> 8) & 0xFF;
    out[p++] =  f->fid       & 0xFF;
    if (f->structure == EF_DF || f->structure == EF_MF) {
        /* GSM 11.11 9.2.1, response to SELECT of an MF/DF:
         *  6 type (01 MF, 02 DF); 7-12 RFU; 13 file characteristics;
         *  14 number of DFs; 15 number of EFs; 16 number of CHVs; 17 RFU;
         *  18 CHV1 status; 19 UNBLOCK CHV1; 20 CHV2; 21 UNBLOCK CHV2.
         * Byte 13 must not announce "5 V only" (b5 = 0): the TI SIM driver
         * (sim.c, test 27.17.1.5.2) would power the card off and reject it.
         * 0x11 = clock stop allowed, 3 V technology. CHV1 initialised
         * (0x83), 3 attempts, same for CHV2 and the unblock codes. */
        out[p++] = (f->structure == EF_MF) ? 0x01 : 0x02;   /* 6 */
        out[p++] = 0x00; out[p++] = 0x00; out[p++] = 0x00;  /* 7-9 */
        out[p++] = 0x00; out[p++] = 0x00; out[p++] = 0x00;  /* 10-12 */
        out[p++] = 0x91;                                    /* 13: + b8 CHV1 disabled (no "Enter PIN1") */
        out[p++] = 0x02;                                    /* 14 DFs */
        out[p++] = 0x09;                                    /* 15 EFs */
        out[p++] = 0x04;                                    /* 16 CHVs */
        out[p++] = 0x00;                                    /* 17 */
        out[p++] = 0x83; out[p++] = 0x8A;                   /* 18-19 */
        out[p++] = 0x83; out[p++] = 0x8A;                   /* 20-21 */
        out[p++] = 0x00; out[p++] = 0x00; out[p++] = 0x00;  /* 22-24 */
    } else {
        out[p++] = 0x04;
        out[p++] = 0x00;
        out[p++] = 0x00; out[p++] = 0x00;
        out[p++] = 0x00;
        /* file status (byte 11): b1 = 1 -> file not invalidated. With b1 = 0,
         * app_check_imsi_loci_validation() considers IMSI/LOCI invalid ->
         * rehabilitation -> SIM_NO_OPERATION ("SIM Invalid") */
        out[p++] = 0xAB;
        /* bytes 13-15 (GSM 11.11 9.2.1): length of the following data (2), structure,
         * record length. The SIM entity derives the record count of EF SMS from them:
         * a record length of 0 means an SMS capacity of 0 -> mfwAlloc(0) in a loop and
         * "Messages: full" in the MMI. */
        out[p++] = 0x02;
        out[p++] = (f->structure == EF_TRANSPARENT) ? 0x00
                : (f->structure == EF_LINEAR_FIXED) ? 0x01 : 0x03;
        out[p++] = f->rec_len;
    }
    return p;
}

static void respond_sw(CalypsoSim *s, uint8_t sw1, uint8_t sw2)
{
    rx_push(s, sw1);
    rx_push(s, sw2);
}

static void respond_with_data_pending(CalypsoSim *s,
                                       const uint8_t *data, int len)
{
    if (len > (int)sizeof(s->resp_buf)) len = sizeof(s->resp_buf);
    memcpy(s->resp_buf, data, len);
    s->resp_len = len;

    respond_sw(s, 0x9F, (uint8_t)len);
}

static void cmd_select(CalypsoSim *s, uint8_t p1, uint8_t p2,
                       uint8_t lc, const uint8_t *data)
{
    if (lc != 2) { respond_sw(s, 0x67, 0x00); return; }
    uint16_t fid = (data[0] << 8) | data[1];

    SimFile *f = NULL;
    if (fid == 0x3F00) {
        f = find_file(0x3F00, 0);
        s->selected_df = 0x3F00;
    } else if ((fid & 0xFF00) == 0x7F00) {
        f = find_file(fid, 0);
        if (f) s->selected_df = fid;
    } else if ((fid & 0xFF00) == 0x2F00) {
        f = find_file(fid, 0x3F00);
    } else {
        f = find_file(fid, s->selected_df);
        if (!f) f = find_file(fid, 0);
    }

    if (!f) {
        SIM_LOG("SELECT 0x%04x → file not found", fid);
        respond_sw(s, 0x6A, 0x82);
        return;
    }
    s->selected_ef = f->fid;

    uint8_t resp[32];
    int n = build_select_response(s, f, resp);
    SIM_LOG("pc=0x%08x SELECT 0x%04x (%s) → %d bytes pending", sim_guest_pc(),
            fid,
            f->structure == EF_MF ? "MF" :
            f->structure == EF_DF ? "DF" : "EF",
            n);
    respond_with_data_pending(s, resp, n);
}

static void cmd_get_response(CalypsoSim *s, uint8_t le)
{
    if (s->resp_len == 0) {
        respond_sw(s, 0x6F, 0x00);
        return;
    }
    int n = (le == 0) ? s->resp_len : (le > s->resp_len ? s->resp_len : le);
    rx_push_n(s, s->resp_buf, n);
    respond_sw(s, 0x90, 0x00);
    s->resp_len = 0;
}

/* Stored records. A free SMS record starts with the status byte 0x00 (GSM 11.11 10.5.3);
 * 0xFF is not a free record, and the Magnetite SMS stack would find no room
 * ("[cmhSMS_checkAccess]: Wrong SIM status", "Messages: full") and answer RP-ERROR to
 * every incoming SMS. One buffer per record file, initialised free (EF_SMS: 00 then FF;
 * the others: FF), read and written per record (UPDATE RECORD is kept). */
#define SIM_REC_MAX 1024
/* SMS centre. With EF_SMSP left free (FF), the C139 asks for "Cent." by hand on an MO SMS
 * and the firmware ends in dar_reset (watchdog reset loop). The bench centre is placed in
 * record 1 (GSM 11.11 10.5.6): indicators E1 = SC address, PID, DCS and VP present, TP-DA
 * absent. The number comes from "sms-service-center" in CALYPSO_SIM_CFG (mobile.cfg),
 * otherwise the bench default. */
static char g_sim_smsc[24] = "+3366612340001";
static void smsp_remplir(uint8_t *r)          /* r: 28-byte record, already FF */
{
    const char *d = g_sim_smsc;
    uint8_t ton = 0x81;
    if (*d == '+') { ton = 0x91; d++; }
    int n = 0;
    uint8_t bcd[10];
    memset(bcd, 0xFF, sizeof bcd);
    for (; *d && n < 20; d++) {
        if (*d < '0' || *d > '9') continue;
        uint8_t v = (uint8_t)(*d - '0');
        bcd[n / 2] = (n & 1) ? (uint8_t)((bcd[n / 2] & 0x0F) | (v << 4)) : (uint8_t)(0xF0 | v);
        n++;
    }
    if (!n) return;
    r[0] = 0xE1;
    r[13] = (uint8_t)(1 + (n + 1) / 2);       /* length: TON/NPI + digits */
    r[14] = ton;
    memcpy(&r[15], bcd, (n + 1) / 2);
    r[25] = 0x00;                             /* TP-PID */
    r[26] = 0x00;                             /* TP-DCS */
    r[27] = 0xA7;                             /* TP-VP: 24 h */
}
static uint8_t *rec_store(SimFile *f)
{
    static uint8_t *store[SIM_FILE_COUNT];
    size_t i = (size_t)(f - sim_files);
    if (i >= SIM_FILE_COUNT || f->rec_len == 0 || f->size > SIM_REC_MAX) return NULL;
    if (!store[i]) {
        store[i] = g_malloc(f->size);
        memset(store[i], 0xFF, f->size);
        if (f->fid == 0x6F3C)
            for (int r = 0; r < f->size / f->rec_len; r++) store[i][r * f->rec_len] = 0x00;
        if (f->fid == 0x6F39) memset(store[i], 0x00, f->size);   /* EF_ACM: counter at zero */
        if (f->fid == 0x6F42 && f->rec_len == 28) smsp_remplir(store[i]);   /* EF_SMSP: SMS centre */
    }
    return store[i];
}

static void cmd_read_record(CalypsoSim *s, uint8_t rec, uint8_t mode, uint8_t le)
{
    SimFile *f = find_file(s->selected_ef, 0);
    if (!f || (f->structure != EF_LINEAR_FIXED && f->structure != EF_CYCLIC)) {
        respond_sw(s, 0x94, 0x00);
        return;
    }
    int nrec = f->size / f->rec_len;
    /* modes 02 (next) / 03 (previous) in addition to 04 (absolute), GSM 11.11 9.2.5 */
    static uint16_t rec_ef; static int rec_cur;
    if (rec_ef != s->selected_ef) { rec_ef = s->selected_ef; rec_cur = 0; }
    if (mode == 0x02) rec = (uint8_t)(rec_cur + 1);
    else if (mode == 0x03) rec = (uint8_t)(rec_cur > 0 ? rec_cur - 1 : nrec);
    else if (mode != 0x04) { respond_sw(s, 0x6B, 0x00); return; }
    if (rec < 1 || rec > nrec) {
        respond_sw(s, 0x94, 0x02);
        return;
    }
    rec_cur = rec;
    /* free records (0xFF): no content stored for this file */
    uint8_t buf[256];
    int n = (le == 0) ? f->rec_len : le;
    if (n > f->rec_len) n = f->rec_len;
    memset(buf, 0xFF, n);
    { uint8_t *st = rec_store(f); if (st) memcpy(buf, st + (rec - 1) * f->rec_len, n); }
    rx_push_n(s, buf, n);
    SIM_LOG("READ_RECORD EF=0x%04x rec=%d len=%d : %02x %02x %02x %02x %02x %02x %02x %02x ...", s->selected_ef, rec, n,
            buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7]);
    respond_sw(s, 0x90, 0x00);
}

static void cmd_read_binary(CalypsoSim *s, uint8_t p1, uint8_t p2, uint8_t le)
{
    SimFile *f = find_file(s->selected_ef, 0);
    if (!f || f->structure != EF_TRANSPARENT) {
        respond_sw(s, 0x94, 0x00);
        return;
    }
    int offset = (p1 << 8) | p2;
    int n = (le == 0) ? 256 : le;
    if (offset + n > f->size) {
        respond_sw(s, 0x67, 0x00);
        return;
    }
    rx_push_n(s, f->data + offset, n);
    SIM_LOG("READ_BINARY EF=0x%04x off=%d len=%d", s->selected_ef, offset, n);
    respond_sw(s, 0x90, 0x00);
}

/* UPDATE BINARY / UPDATE RECORD: the phone writes LOCI, Kc, FPLMN, SMS... A transparent
 * file is kept in data[] as long as it fits; records go to the per-file record store
 * (rec_store), or are accepted without being kept when the file has none. */
static void cmd_update_binary(CalypsoSim *s, uint8_t p1, uint8_t p2, uint8_t lc, const uint8_t *data)
{
    SimFile *f = find_file(s->selected_ef, 0);
    if (!f || f->structure != EF_TRANSPARENT) { respond_sw(s, 0x94, 0x08); return; }
    int offset = (p1 << 8) | p2;
    if (offset + lc > f->size) { respond_sw(s, 0x67, 0x00); return; }
    if (offset + lc <= (int)sizeof f->data) memcpy(f->data + offset, data, lc);
    SIM_LOG("UPDATE_BINARY EF=0x%04x off=%d len=%d", s->selected_ef, offset, lc);
    respond_sw(s, 0x90, 0x00);
}

static void cmd_update_record(CalypsoSim *s, uint8_t rec, uint8_t mode, uint8_t lc, const uint8_t *data)
{
    SimFile *f = find_file(s->selected_ef, 0);
    if (!f || (f->structure != EF_LINEAR_FIXED && f->structure != EF_CYCLIC)) { respond_sw(s, 0x94, 0x08); return; }
    if (lc != f->rec_len) { respond_sw(s, 0x67, 0x00); return; }
    int nrec = f->size / f->rec_len;
    if (mode == 0x04 && (rec < 1 || rec > nrec)) { respond_sw(s, 0x94, 0x02); return; }
    if (mode == 0x04) { uint8_t *st = rec_store(f); if (st && data) memcpy(st + (rec - 1) * f->rec_len, data, lc); }
    SIM_LOG("UPDATE_RECORD EF=0x%04x rec=%d mode=%d len=%d : %02x %02x %02x %02x %02x %02x %02x %02x ...", s->selected_ef, rec, mode, lc,
            data ? data[0] : 0, data ? data[1] : 0, data ? data[2] : 0, data ? data[3] : 0,
            data ? data[4] : 0, data ? data[5] : 0, data ? data[6] : 0, data ? data[7] : 0);
    respond_sw(s, 0x90, 0x00);
}

static void cmd_status(CalypsoSim *s, uint8_t le)
{
    SimFile *df = find_file(s->selected_df, 0);
    if (!df) { respond_sw(s, 0x6F, 0x00); return; }
    uint8_t resp[32];
    int n = build_select_response(s, df, resp);
    int outn = (le == 0 || le > n) ? n : le;
    rx_push_n(s, resp, outn);
    respond_sw(s, 0x90, 0x00);
}

static void cmd_verify_chv(CalypsoSim *s, uint8_t p2, uint8_t lc,
                           const uint8_t *data)
{

    (void)p2; (void)lc; (void)data;
    SIM_LOG("VERIFY_CHV → always OK");
    respond_sw(s, 0x90, 0x00);
}

void comp128v1(const uint8_t *ki, const uint8_t *rand, uint8_t *sres, uint8_t *kc);   /* calypso_comp128.c */
static void cmd_run_gsm_algo(CalypsoSim *s, uint8_t lc, const uint8_t *data)
{

    if (lc != 16) { respond_sw(s, 0x67, 0x00); return; }

    uint8_t resp[12];
    /* COMP128v1 with the Ki from mobile.cfg (calypso_comp128.c). The osmocom mobile does not
     * depend on it (sim test: comp128 done in layer23), but the TI firmware (Magnetite) asks
     * the card for the computation, and the MSC checks the SRES. Without a Ki, a dummy
     * SRES (RAND ^ 0xAA, Kc 0) is returned. */
    if (s->ki_valid) {
        comp128v1(s->ki, data, resp, resp + 4);
        SIM_LOG("RUN_GSM_ALGORITHM COMP128v1 (RAND[0]=0x%02x) → SRES=%02x%02x%02x%02x Kc[0]=0x%02x",
                data[0], resp[0], resp[1], resp[2], resp[3], resp[4]);
    } else {
        for (int i = 0; i < 4; i++) resp[i]    = data[i] ^ 0xAA;
        for (int i = 0; i < 8; i++) resp[4+i] = 0x00;
        SIM_LOG("RUN_GSM_ALGORITHM SANS Ki (CALYPSO_SIM_CFG ?) → SRES bidon RAND^0xAA");
    }
    respond_with_data_pending(s, resp, 12);
}

static void dispatch_apdu(CalypsoSim *s)
{
    uint8_t cla = s->apdu[0];
    uint8_t ins = s->apdu[1];
    uint8_t p⟨1⟩  = s->apdu[⟨2⟩];  ×3
    ⟨⟩ = (1,2) (2,3) (3,4)
    uint8_t *data = (s->apdu_pos > 5) ? s->apdu + 5 : NULL;
    int     dlen = s->apdu_pos - 5;

    SIM_LOG("APDU CLA=%02x INS=%02x P1=%02x P2=%02x P3=%02x dlen=%d",
            cla, ins, p1, p2, p3, dlen > 0 ? dlen : 0);

    /* T=0: a command that RETURNS data (GET RESPONSE, READ BINARY/RECORD,
     * STATUS, FETCH) starts with the procedure byte (ACK = INS) before the
     * data, then SW1 SW2. The TI driver (sim32.c, moderx 1) requires it;
     * without it the first data byte is taken for an invalid ACK and
     * SIM_GetResponse fails. Outgoing commands already received their ACK
     * after the header (apdu_tx_byte); commands without data (moderx 5)
     * wait directly for SW1 SW2. */
    if (ins == 0xC0 || ins == 0xB0 || ins == 0xB2 || ins == 0xF2 || ins == 0x12) {
        rx_push(s, ins);
    }

    switch (ins) {
    case 0xA4: cmd_select(s, p1, p2, p3, data ? data : (const uint8_t *)""); break;
    case 0xB0: cmd_read_binary(s, p1, p2, p3); break;
    case 0xB2: cmd_read_record(s, p1, p2, p3); break;
    case 0xD6: cmd_update_binary(s, p1, p2, p3, data ? data : (const uint8_t *)""); break;
    case 0xDC: cmd_update_record(s, p1, p2, p3, data); break;
    case 0xC0: cmd_get_response(s, p3); break;
    case 0xF2: cmd_status(s, p3); break;
    case 0x20: cmd_verify_chv(s, p2, p3, data ? data : (const uint8_t *)""); break;
    case 0x88: cmd_run_gsm_algo(s, p3, data ? data : (const uint8_t *)""); break;
    default:
        SIM_LOG("INS=0x%02x not supported → SW=6D00", ins);
        respond_sw(s, 0x6D, 0x00);
        break;
    }

    raise_rx_irq(s);
}

G_GNUC_UNUSED
static void apdu_tx_byte_inner(CalypsoSim *s, uint8_t b);
static void apdu_tx_byte(CalypsoSim *s, uint8_t b)
{
    s->defer = true;
    apdu_tx_byte_inner(s, b);
    s->defer = false;
    if (s->pend_len) {
        timer_mod_ns(s->resp_timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + resp_delai_ns(s->pend_len));
    }
}

/* Resynchronisation. If the TI driver has not seen the ACK (SIM_ERR_WAIT after 159 to 1815
 * ticks, at least 0.7 s), the layer above sends a fresh header while the rest of the
 * command is still expected; taking "a0 a4" for the FID (SELECT 0xa0a4) would leave every
 * following command shifted by 2 bytes (CLA=00 INS=00 -> 6D00 forever -> "SIM Invalid").
 * Within a command the bytes follow each other at ~1 ms: a silence of more than 500 ms
 * means the terminal abandoned the command, and the received byte opens a new APDU. */
#define APDU_ABANDON_NS 500000000LL
static void apdu_tx_byte_inner(CalypsoSim *s, uint8_t b)
{
    int64_t now = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
    if (s->apdu_pos > 0 && now - s->last_tx_ns > APDU_ABANDON_NS) {
        SIM_LOG("APDU abandonne par le terminal apres %d octet(s) (silence %lld ms) : resynchronisation",
                s->apdu_pos, (long long)((now - s->last_tx_ns) / 1000000));
        s->apdu_pos = 0;
        s->apdu_expected = 0;
    }
    s->last_tx_ns = now;
    if (s->apdu_pos < APDU_MAX_LEN) {
        s->apdu[s->apdu_pos++] = b;
    }
    if (s->apdu_pos == 5) {

        uint8_t ins = s->apdu[1];
        bool is_outgoing_data =
            (ins == 0xA4) || (ins == 0xD6) || (ins == 0xDC) ||
            (ins == 0x20) || (ins == 0x24) || (ins == 0x88);
        if (is_outgoing_data && s->apdu[4] != 0) {
            s->apdu_expected = 5 + s->apdu[4];
        } else {
            s->apdu_expected = 5;
        }

        if (is_outgoing_data && s->apdu[4] != 0) {
            rx_push(s, ins);
            raise_rx_irq(s);
        }
    }
    if (s->apdu_pos == s->apdu_expected) {
        dispatch_apdu(s);
        s->apdu_pos = 0;
        s->apdu_expected = 0;
    }
}

uint16_t calypso_sim_reg_read(CalypsoSim *s, hwaddr off)
{
    switch (off) {
    case CALYPSO_SIM_REG_CMD:
        return s->cmd;
    case CALYPSO_SIM_REG_STAT: {
        uint16_t v = 0;
        if (s->powered) v |= CALYPSO_SIM_STAT_NOCARD;
        v |= CALYPSO_SIM_STAT_TXPAR;
        if (rx_count(s) == 0) v |= CALYPSO_SIM_STAT_FIFOEMPTY;
        if (rx_count(s) >= RX_FIFO_SIZE - 1) v |= CALYPSO_SIM_STAT_FIFOFULL;
        return v;
    }
    case CALYPSO_SIM_REG_CONF1:  return s->conf1;
    case CALYPSO_SIM_REG_CONF2:  return s->conf2;
    case CALYPSO_SIM_REG_IT: {
        refresh_it_rx(s);
        uint16_t v = s->it;

        uint16_t edge_seen = v & (CALYPSO_SIM_IT_NATR |
                                  CALYPSO_SIM_IT_WT   |
                                  CALYPSO_SIM_IT_OV   |
                                  CALYPSO_SIM_IT_TX);

        s->it &= ~edge_seen;
        update_irq(s);

        static unsigned itrd;
        if (itrd++ < 40) {
            uintptr_t io_pc = current_cpu ? current_cpu->mem_io_pc : 0;
            fprintf(stderr,
                    "[sim] SIM_IT read=0x%04x rx_count=%d edge_cleared=0x%04x "
                    "post_it=0x%04x mem_io_pc=0x%lx\n",
                    v, rx_count(s), edge_seen, s->it,
                    (unsigned long)io_pc);
        }
        return v;
    }
    case CALYPSO_SIM_REG_DRX: {
        uint8_t b = 0;
        rx_pop(s, &b);
        update_irq(s);

        return b | (1 << 8);
    }
    case CALYPSO_SIM_REG_DTX:    return 0;
    case CALYPSO_SIM_REG_MASKIT: return s->maskit;
    case CALYPSO_SIM_REG_IT_CD:  return s->it_cd;
    default: return 0;
    }
}

void calypso_sim_reg_write(CalypsoSim *s, hwaddr off, uint16_t val)
{
    switch (off) {
    case CALYPSO_SIM_REG_CMD:
        s->cmd = val;
        if (val & CALYPSO_SIM_CMD_START) {
            s->powered = true;
            SIM_LOG("CMDSTART → ATR delivered (synchronous)");

            deliver_atr(s);
        }
        if (val & CALYPSO_SIM_CMD_STOP) {
            s->powered = false;
            SIM_LOG("CMDSTOP");
        }
        if (val & (CALYPSO_SIM_CMD_CARDRST | CALYPSO_SIM_CMD_IFRST)) {
            /* The TI SIM driver (Magnetite, sim.c SIM_ManualStart) never writes
             * START: CLKEN, STOP, then SWRST in automatic mode, and it waits for
             * the ATR. The ATR is not delivered here (SWRST + CLKEN): the card
             * would answer before SIM_ManualStart has set VCC, SIO and the
             * clock, and the sequence would never finish. The manual-mode ATR
             * is delivered on CONF1 (SCLKEN / SRSTLEV, below). osmocom writes
             * START first, so the card is powered and the ATR is delivered. */
            SIM_LOG("RESET → ATR delivered (synchronous) pc=0x%08x lr=0x%08x", sim_guest_pc(), sim_guest_lr());
            s->apdu_pos = 0;
            s->apdu_expected = 0;
            s->resp_len = 0;
            s->rx_head = s->rx_tail = 0;
            s->selected_df = 0x3F00;
            s->selected_ef = 0x3F00;

            if (s->powered) deliver_atr(s);
        }
        break;
    case CALYPSO_SIM_REG_STAT:    s->stat   = val; break;
    case CALYPSO_SIM_REG_CONF1: {
        /* Manual mode (BYPASS) of the TI SIM driver: it powers the card
         * (SVCCLEV), releases SIO, starts the clock (SCLKEN) and waits for the
         * ATR, then raises SRSTLEV if it does not come. On silicon the card
         * answers when leaving reset: the ATR is delivered on the rising edge
         * of SCLKEN or of SRSTLEV, once per reset. */
        uint16_t old = s->conf1;
        s->conf1 = val;
        if (val != old) {
            SIM_LOG("CONF1 <- 0x%04x (powered=%d rx=%d)", val, s->powered, rx_count(s));
        }
        if (val & CALYPSO_SIM_CONF1_SVCCLEV) {
            s->powered = true;
        }
        /* warm reset (SIM_WARMReset: SRSTLEV low for 400 ETU then high): new ATR */
        if (!(val & CALYPSO_SIM_CONF1_SRSTLEV) && (old & CALYPSO_SIM_CONF1_SRSTLEV)) {
            s->atr_manual_done = false;
            s->rx_head = s->rx_tail = 0;
            s->apdu_pos = 0;
            s->apdu_expected = 0;
        }
        if ((val & CALYPSO_SIM_CONF1_BYPASS) && s->powered && !s->atr_manual_done &&
            (((val & CALYPSO_SIM_CONF1_SCLKEN) && !(old & CALYPSO_SIM_CONF1_SCLKEN)) ||
             ((val & CALYPSO_SIM_CONF1_SRSTLEV) && !(old & CALYPSO_SIM_CONF1_SRSTLEV)))) {
            s->atr_manual_done = true;
            SIM_LOG("mode manuel (TI) : ATR livre sur CONF1=0x%04x", val);
            schedule_atr(s);
        }
        if (!(val & CALYPSO_SIM_CONF1_SVCCLEV) && (old & CALYPSO_SIM_CONF1_SVCCLEV)) {
            s->atr_manual_done = false;
        }
        break;
    }
    case CALYPSO_SIM_REG_CONF2:   s->conf2  = val; break;
    case CALYPSO_SIM_REG_IT:           break;
    case CALYPSO_SIM_REG_DRX:            break;
    case CALYPSO_SIM_REG_DTX:
        SIM_LOG("DTX <- 0x%02x", (unsigned)(val & 0xff));
        apdu_tx_byte(s, (uint8_t)(val & 0xFF));
        /* Byte sent: IT TX (the TI driver, sim32.c, only sends the next byte of
         * a command on this IT, STAT TXPAR at 1). Acknowledged when SIM_IT is
         * read, like NATR/WT/OV. */
        /* ... but after one character time, not immediately. Raised at once, the IT
         * would send the 5 header bytes within a few us and the ACK would follow 1.5 ms
         * after the FIRST byte. SIM_Command_Base writes the first byte (SIM_WriteBuffer)
         * and only then sets moderx to 6 (waiting for the ACK): a SIM task preempted
         * 1.5 ms between the two would receive the ACK in moderx 5 (stored as SW1), wait
         * for an ACK that no longer comes, SIM_ERR_WAIT, header resent -> loss of
         * synchronisation (see APDU_ABANDON_NS). At 9600 baud a byte lasts ~1 ms
         * (CALYPSO_SIM_US_OCTET), the header ~5 ms as on a real card. */
        timer_mod_ns(s->tx_timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + (int64_t)us_octet() * 1000);
        break;
    case CALYPSO_SIM_REG_MASKIT: {

        s->maskit = val;
        update_irq(s);
        bool wt_unmasked = (val & CALYPSO_SIM_IT_WT) == 0;
        bool wt_not_pending = (s->it & CALYPSO_SIM_IT_WT) == 0;
        if (wt_unmasked && wt_not_pending && rx_count(s) == 0) {
            schedule_wt(s);
        }
        break;
    }
    case CALYPSO_SIM_REG_IT_CD:   s->it_cd  = val; break;
    default: break;
    }
}

static void load_config_from_file(CalypsoSim *s, const char *path)
{
    FILE *fp = fopen(path, "r");
    if (!fp) {
        SIM_LOG("config %s not found — keeping default file system", path);
        return;
    }
    char line[256];
    bool in_test_sim = false;
    while (fgets(line, sizeof(line), fp)) {
        char *p = line;
        while (*p == ' ' || *p == '\t') p++;
        if (strncmp(p, "test-sim", 8) == 0) {
            in_test_sim = true;
            continue;
        }
        if (strncmp(p, "sms-service-center ", 19) == 0) {   /* -> EF_SMSP, see rec_store */
            sscanf(p + 19, "%23s", g_sim_smsc);
            SIM_LOG("centre SMS (EF_SMSP) depuis %s : %s", path, g_sim_smsc);
            continue;
        }
        if (!in_test_sim) continue;

        if (line[0] != ' ' && line[0] != '\t' && line[0] != '\n') {
            in_test_sim = false;
            continue;
        }
        if (strncmp(p, "imsi ", 5) == 0) {
            const char *imsi = p + 5;
            uint8_t bcd[9];
            if (encode_imsi_bcd(imsi, bcd) > 0) {
                SimFile *ef = find_file(0x6F07, 0x7F20);
                if (ef) {
                    memcpy(ef->data, bcd, 9);
                    ef->size = 9;
                    SIM_LOG("EF_IMSI loaded from %s: %.15s", path, imsi);
                }
            }
        } else if (strncmp(p, "ki comp128 ", 11) == 0) {
            const char *hex = p + 11;
            int n = 0;
            while (n < 16 && *hex) {
                while (*hex == ' ') hex++;
                if (!*hex) break;
                unsigned v;
                if (sscanf(hex, "%2x", &v) != 1) break;
                s->ki[n++] = (uint8_t)v;
                hex += 2;
            }
            if (n == 16) {
                s->ki_valid = true;
                SIM_LOG("Ki loaded from %s (16 bytes)", path);
            }
        }
    }
    fclose(fp);
}

CalypsoSim *calypso_sim_new(qemu_irq sim_irq)
{
    CalypsoSim *s = g_new0(CalypsoSim, 1);
    s->irq = sim_irq;
    s->atr_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, deliver_atr, s);
    s->resp_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, resp_fire, s);
    s->wt_timer  = timer_new_ns(QEMU_CLOCK_VIRTUAL, fire_wt,     s);
    s->tx_timer  = timer_new_ns(QEMU_CLOCK_VIRTUAL, tx_fire,     s);

    s->selected_df = 0x3F00;
    s->selected_ef = 0x3F00;

    const char *cfg_path = getenv("CALYPSO_SIM_CFG");
    if (cfg_path && *cfg_path) {
        load_config_from_file(s, cfg_path);
    } else {
        SIM_LOG("CALYPSO_SIM_CFG unset — keeping default IMSI/Ki");
    }
    return s;
}

4.28 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_soc.c

12363 octets, 317 lignes → 317 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "qemu/osdep.h"
#include "hw/i2c/i2c.h"
#include "qapi/error.h"
#include "hw/sysbus.h"
#include "hw/irq.h"
#include "hw/qdev-properties.h"
#include "hw/qdev-properties-system.h"
#include "hw/char/serial.h"
#include "chardev/char.h"
#include "exec/address-spaces.h"
#include "sysemu/sysemu.h"
#include "sysemu/runstate.h"
#include "hw/core/cpu.h"
#include "hw/arm/calypso/calypso_soc.h"
#include "hw/arm/calypso/calypso_trx.h"
#include "hw/arm/calypso/calypso_uart.h"

CalypsoUARTState *g_uart_modem;
CalypsoUARTState *g_uart_irda;

#define CALYPSO_IRAM_BASE     0x00800000
#define CALYPSO_IRAM_SIZE     (256 * 1024)
#define CALYPSO_INTH_BASE     0xFFFFFA00
#define CALYPSO_TIMER1_BASE   0xFFFE3800
/* TIMER2 is at TIMER1 + 0x3000 (firmware calypso/timer.c TIMER2_OFFSET): 0xFFFE6800, not
 * 0xFFFE3C00. timer_init() writes there; jiffies, timer_irq(), keypad_poll() and the firmware
 * osmo_timers all depend on this mapping. */
#define CALYPSO_TIMER2_BASE   0xFFFE6800
#define CALYPSO_SPI_BASE      0xFFFE3000
#define CALYPSO_I2C_BASE      0xFFFE1800
#define CALYPSO_KEYPAD_BASE   0xFFFE4800    /* ARMIO: GPIO + keypad (calypso_armio.c) */
#define CALYPSO_UWIRE_BASE    0xFFFE4000    /* uWire, C139 LCD on CS0 (calypso_uwire.c) */
#define CALYPSO_I2C_CTRL_BASE 0xFFFE2800    /* real I2C (calypso_i2c_ctrl.c); 0xFFFE1800 is RTC */
#define CALYPSO_IRQ_KEYPAD_GPIO 8
#define CALYPSO_UART_IRDA     0xFFFF5000
#define CALYPSO_UART_MODEM    0xFFFF5800
#define CALYPSO_CNTL_BASE     0xFFFFFD00
#define CALYPSO_RHEA_DMA_BASE 0xFFFFFC00

static uint64_t stub_read(void *o, hwaddr a, unsigned s)
{
    return 0;
}

static void stub_write(void *o, hwaddr a, uint64_t v, unsigned s)
{
}

static const MemoryRegionOps stub8_ops = {
    .read = stub_read,
    .write = stub_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
    .impl = { .min_access_size = 1, .max_access_size = 1 },
};

static const MemoryRegionOps stub16_ops = {
    .read = stub_read,
    .write = stub_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
    .impl = { .min_access_size = 2, .max_access_size = 2 },
};

static uint64_t cntl_read(void *opaque, hwaddr offset, unsigned size)
{
    CalypsoSoCState *s = CALYPSO_SOC(opaque);
    if (offset == 4) {
        return s->cntl_rst;
    }
    return offset == 0 ? s->extra_conf : 0;
}

static void cntl_write(void *opaque, hwaddr offset, uint64_t value, unsigned size)
{
    CalypsoSoCState *s = CALYPSO_SOC(opaque);
    if (offset == 4) {
        /* CNTL_RST: dsp_pre_boot() asserts then releases RESET_DSP (bit 1).
         * On silicon that restarts the DSP boot ROM, which then writes IDLE
         * into BL_CMD_STATUS. */
        s->cntl_rst = (uint16_t)value;
        calypso_trx_dsp_reset_line((value & 0x2) != 0);
        return;
    }
    if (offset != 0) {
        return;
    }
    s->extra_conf = (uint16_t)value;
    bool bootrom_enabled = (value >> 8) & 3;
    MemoryRegion *sysmem = get_system_memory();
    /* On a C1xx the boot ROM is wired out (nIBOOT): the flash stays at 0
     * whatever the firmware writes here. Magnetite writes 0x40 into
     * CNTL_ARM_CLK at startup; without this guard the IRAM (zeros = ARM NOP)
     * would overlay the flash. */
    if (s->flash_boot) {
        return;
    }
    if (!bootrom_enabled && !s->iram_at_zero) {
        memory_region_init_alias(&s->iram_alias, OBJECT(s), "calypso.iram_at_zero",
                                 &s->iram, 0, CALYPSO_IRAM_SIZE);
        memory_region_add_subregion_overlap(sysmem, 0x00000000, &s->iram_alias, 1);
        s->iram_at_zero = true;
    } else if (bootrom_enabled && s->iram_at_zero) {
        memory_region_del_subregion(sysmem, &s->iram_alias);
        object_unparent(OBJECT(&s->iram_alias));
        s->iram_at_zero = false;
    }
}

static const MemoryRegionOps cntl_ops = {
    .read = cntl_read,
    .write = cntl_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
    .impl = { .min_access_size = 2, .max_access_size = 2 },
};

/* Watchdog: dar_reset() (the only caller of WATCHDOG_TIM_MODE = 0xFFFFF804) writes 0x80F5
 * then waits for the hardware reset of the ARM; without it the firmware loops in dar_reset.
 * The reset is emulated by restarting the machine (flash preserved). */
static uint64_t wdt_read(void *opaque, hwaddr off, unsigned size)
{
    return 0;
}

static void wdt_write(void *opaque, hwaddr off, uint64_t val, unsigned size)
{
    if (off == 0x04 && (val & 0xffff) == 0x80F5) {
        /* cpu_reset alone can loop (the ARM restarts on peripherals left in their previous
         * state and falls back into dar_reset). Beyond 3 resets in 2 s, full machine reset
         * (qemu_system_reset_request). */
        static int64_t t0; static int n;
        int64_t t = qemu_clock_get_ms(QEMU_CLOCK_REALTIME);
        if (t - t0 > 2000) { t0 = t; n = 0; }
        if (++n > 3) {
            fprintf(stderr, "[wdt] chien de garde : resets en boucle -> reset complet de la machine\n");
            n = 0; t0 = t;
            qemu_system_reset_request(SHUTDOWN_CAUSE_GUEST_RESET);
            return;
        }
        fprintf(stderr, "[wdt] chien de garde : 0x80F5 -> reset de l'ARM\n");
        cpu_reset(first_cpu);   /* ARM CPU reset: back to the reset vector, boot restarts */
    }
}

static const MemoryRegionOps wdt_ops = {
    .read = wdt_read,
    .write = wdt_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
    .valid = { .min_access_size = 1, .max_access_size = 4 },
};

static void add_stub(MemoryRegion *sys, const char *name, hwaddr base,
                     const MemoryRegionOps *ops, uint64_t size)
{
    MemoryRegion *mr = g_new(MemoryRegion, 1);
    memory_region_init_io(mr, NULL, ops, NULL, name, size);
    memory_region_add_subregion(sys, base, mr);
}

static void init_uart(CalypsoSoCState *s, CalypsoUARTState *uart, const char *label,
                      int serial_index, hwaddr base, qemu_irq irq, Error **errp)
{
    Chardev *chr = qemu_chr_find(label);
    if (!chr) {
        chr = serial_hd(serial_index);
    }
    object_initialize_child(OBJECT(s), label, uart, TYPE_CALYPSO_UART);
    qdev_prop_set_string(DEVICE(uart), "label", label);
    if (chr) {
        qdev_prop_set_chr(DEVICE(uart), "chardev", chr);
    }
    if (!sysbus_realize(SYS_BUS_DEVICE(uart), errp)) {
        return;
    }
    sysbus_mmio_map(SYS_BUS_DEVICE(uart), 0, base);
    sysbus_connect_irq(SYS_BUS_DEVICE(uart), 0, irq);
}

static void calypso_soc_realize(DeviceState *dev, Error **errp)
{
    CalypsoSoCState *s = CALYPSO_SOC(dev);
    SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
    MemoryRegion *sysmem = get_system_memory();
    Error *err = NULL;

    memory_region_init_ram(&s->iram, OBJECT(dev), "calypso.iram", CALYPSO_IRAM_SIZE,
                           &error_fatal);
    memory_region_add_subregion(sysmem, CALYPSO_IRAM_BASE, &s->iram);

    object_initialize_child(OBJECT(dev), "inth", &s->inth, TYPE_CALYPSO_INTH);
    if (!sysbus_realize(SYS_BUS_DEVICE(&s->inth), &err)) {
        error_propagate(errp, err);
        return;
    }
    sysbus_mmio_map(SYS_BUS_DEVICE(&s->inth), 0, CALYPSO_INTH_BASE);
    sysbus_pass_irq(sbd, SYS_BUS_DEVICE(&s->inth));
#define INTH_IRQ(n) qdev_get_gpio_in(DEVICE(&s->inth), (n))

    object_initialize_child(OBJECT(dev), "timer1", &s->timer1, TYPE_CALYPSO_TIMER);
    if (!sysbus_realize(SYS_BUS_DEVICE(&s->timer1), &err)) {
        error_propagate(errp, err);
        return;
    }
    sysbus_mmio_map(SYS_BUS_DEVICE(&s->timer1), 0, CALYPSO_TIMER1_BASE);
    sysbus_connect_irq(SYS_BUS_DEVICE(&s->timer1), 0, INTH_IRQ(CALYPSO_IRQ_TIMER1));
    calypso_timer_register_lost(DEVICE(&s->timer1));

    object_initialize_child(OBJECT(dev), "timer2", &s->timer2, TYPE_CALYPSO_TIMER);
    if (!sysbus_realize(SYS_BUS_DEVICE(&s->timer2), &err)) {
        error_propagate(errp, err);
        return;
    }
    sysbus_mmio_map(SYS_BUS_DEVICE(&s->timer2), 0, CALYPSO_TIMER2_BASE);
    sysbus_connect_irq(SYS_BUS_DEVICE(&s->timer2), 0, INTH_IRQ(CALYPSO_IRQ_TIMER2));

    DeviceState *i2c = qdev_new("calypso-i2c");
    sysbus_realize_and_unref(SYS_BUS_DEVICE(i2c), &error_fatal);
    sysbus_mmio_map(SYS_BUS_DEVICE(i2c), 0, CALYPSO_I2C_BASE);

    object_initialize_child(OBJECT(dev), "spi", &s->spi, TYPE_CALYPSO_SPI);
    if (!sysbus_realize(SYS_BUS_DEVICE(&s->spi), &err)) {
        error_propagate(errp, err);
        return;
    }
    sysbus_mmio_map(SYS_BUS_DEVICE(&s->spi), 0, CALYPSO_SPI_BASE);
    sysbus_connect_irq(SYS_BUS_DEVICE(&s->spi), 0, INTH_IRQ(CALYPSO_IRQ_SPI));

    init_uart(s, &s->uart_modem, "modem", 0, CALYPSO_UART_MODEM,
              INTH_IRQ(CALYPSO_IRQ_UART_MODEM), &err);
    if (err) {
        error_propagate(errp, err);
        return;
    }
    g_uart_modem = &s->uart_modem;

    init_uart(s, &s->uart_irda, "irda", 1, CALYPSO_UART_IRDA,
              INTH_IRQ(CALYPSO_IRQ_UART_IRDA), &err);
    if (err) {
        error_propagate(errp, err);
        return;
    }
    g_uart_irda = &s->uart_irda;

    qemu_irq *irqs = g_new0(qemu_irq, CALYPSO_NUM_IRQS);
    for (int i = 0; i < CALYPSO_NUM_IRQS; i++) {
        irqs[i] = INTH_IRQ(i);
    }
    calypso_trx_init(sysmem, irqs);
#undef INTH_IRQ

    /* ARMIO + C123 matrix keypad (calypso_armio.c), IRQ 8, and the real I2C
     * controller (calypso_i2c_ctrl.c) with the ST7558 LCD (0x3c,
     * calypso_st7558.c) behind it: rssi.highram.elf, menu, trx display and
     * read the keys. */
    {
        DeviceState *armio = qdev_new("calypso-armio");
        sysbus_realize_and_unref(SYS_BUS_DEVICE(armio), &error_fatal);
        sysbus_mmio_map(SYS_BUS_DEVICE(armio), 0, CALYPSO_KEYPAD_BASE);
        sysbus_connect_irq(SYS_BUS_DEVICE(armio), 0,
                           qdev_get_gpio_in(DEVICE(&s->inth), CALYPSO_IRQ_KEYPAD_GPIO));

        DeviceState *i2cc = qdev_new("calypso-i2c-ctrl");
        sysbus_realize_and_unref(SYS_BUS_DEVICE(i2cc), &error_fatal);
        sysbus_mmio_map(SYS_BUS_DEVICE(i2cc), 0, CALYPSO_I2C_CTRL_BASE);
        /* CALYPSO_LCD=c139: the 96x64 colour LCD of the C139 on uWire CS0
         * (FreeCalypso Magnetite, c139 target) instead of the C123 ST7558 on
         * I2C. The uWire controller is present in both cases. */
        const char *lcd = getenv("CALYPSO_LCD");
        bool c139 = lcd && !strcmp(lcd, "c139");
        if (!c139) {
            i2c_slave_create_simple(I2C_BUS(qdev_get_child_bus(i2cc, "i2c")), "st7558", 0x3c);
        }
        DeviceState *uwire = qdev_new("calypso-uwire");
        qdev_prop_set_bit(uwire, "c139-lcd", c139);
        sysbus_realize_and_unref(SYS_BUS_DEVICE(uwire), &error_fatal);
        sysbus_mmio_map(SYS_BUS_DEVICE(uwire), 0, CALYPSO_UWIRE_BASE);
    }
    /* no stub at 0xFFFE6800: that is TIMER2 (CALYPSO_TIMER2_BASE), mapped above */
    add_stub(sysmem, "calypso.mmio_80xx", 0xFFFE8000, &stub8_ops, 0x100);
    add_stub(sysmem, "calypso.conf", 0xFFFEF000, &stub16_ops, 0x100);
    add_stub(sysmem, "calypso.dpll", 0xFFFF9800, &stub16_ops, 0x100);
    add_stub(sysmem, "calypso.mmio_f0xx", 0xFFFFF000, &stub16_ops, 0x100);
    add_stub(sysmem, "calypso.wdt", 0xFFFFF800, &wdt_ops, 0x100);
    add_stub(sysmem, "calypso.rhea", 0xFFFFF900, &stub16_ops, 0x100);
    add_stub(sysmem, "calypso.clkm", 0xFFFFFB00, &stub16_ops, 0x100);
    add_stub(sysmem, "calypso.rhea_dma", CALYPSO_RHEA_DMA_BASE, &stub16_ops, 0x100);
    add_stub(sysmem, "calypso.dio", 0xFFFFFF00, &stub8_ops, 0x100);

    memory_region_init_io(&s->cntl_iomem, OBJECT(dev), &cntl_ops, s, "calypso.cntl", 0x100);
    memory_region_add_subregion(sysmem, CALYPSO_CNTL_BASE, &s->cntl_iomem);
    s->extra_conf = 0x0300;
    s->iram_at_zero = false;

    MemoryRegion *catchall = g_new(MemoryRegion, 1);
    memory_region_init_io(catchall, NULL, &stub8_ops, NULL, "calypso.catchall", 0x100000);
    memory_region_add_subregion_overlap(sysmem, 0xFFF00000, catchall, -1);
}

static void calypso_soc_class_init(ObjectClass *oc, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(oc);
    dc->realize = calypso_soc_realize;
    dc->user_creatable = false;
}

static const TypeInfo calypso_soc_type_info = {
    .name = TYPE_CALYPSO_SOC,
    .parent = TYPE_SYS_BUS_DEVICE,
    .instance_size = sizeof(CalypsoSoCState),
    .class_init = calypso_soc_class_init,
};

static void calypso_soc_register_types(void)
{
    type_register_static(&calypso_soc_type_info);
}

type_init(calypso_soc_register_types)

4.29 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_st7558.c

7582 octets, 246 lignes → 246 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * calypso_st7558.c - the Motorola C123 display: Sitronix ST7558 LCD, 96x65
 * monochrome pixels, I2C slave 0x3c (firmware: fb/fb_st7558.c).
 *
 * Protocol, as the firmware speaks it:
 *   - first byte after the address = control byte: 0x00 -> commands,
 *     0x40 -> data (RAM);
 *   - commands: 0x40|page (Y address, 9 pages of 8 rows), 0x80|x (X address,
 *     0..101), 0x20/0x21 (normal/extended instruction set), 0x0c normal
 *     video, 0x0d inverse video, 0x08 display off; the rest (bias, V_OP,
 *     booster, mirrors) is accepted without effect;
 *   - data: one byte = 8 vertical pixels (bit 0 on top) of column x of the
 *     current page, x advances after each byte.
 * fb_st7558_flush() sends in packets of 16 bytes (the I2C FIFO): the display
 * is republished at the end of every transaction that touched the RAM.
 *
 * Two outputs:
 *   - the QEMU graphical console (-display sdl: 96x65 window scaled x3);
 *   - an image file under /dev/shm (CALYPSO_LCD_SHM, default
 *     /dev/shm/calypso_lcd, "none" to write nothing): 32-byte header
 *     (magic "C123LCD", width, height, pages, flags bit0 inverse bit1 off,
 *     version counter) then 96 x 9 bytes of pages, which
 *     osmo-operator/tools/osmo-c123-gtk.py reads to overlay the display on
 *     the phone picture.
 */

#include "qemu/osdep.h"
#include "hw/i2c/i2c.h"
#include "ui/console.h"
#include "qom/object.h"
#include <sys/stat.h>
#include <fcntl.h>

#define TYPE_ST7558 "st7558"
OBJECT_DECLARE_SIMPLE_TYPE(St7558State, ST7558)

#define ST7558_COLS   102
#define ST7558_PAGES  9
#define LCD_W         96
#define LCD_H         65
#define MAGNIFY       3
#define SHM_DEFAULT   "/dev/shm/calypso_lcd"
#define SHM_HDR       32

enum { MODE_IDLE, MODE_CMD, MODE_DATA };

struct St7558State {
    I2CSlave parent_obj;
    QemuConsole *con;
    uint8_t ram[ST7558_PAGES * ST7558_COLS];
    int page, x, mode;
    bool ext, inverse, off;
    bool redraw, dirty;
    int shm_fd;
    uint32_t seq;
    char *shm_path;
};

static void st7558_export(St7558State *s)
{
    uint8_t buf[SHM_HDR + LCD_W * ST7558_PAGES];
    int p;

    if (s->shm_fd < 0) {
        return;
    }
    s->seq++;
    memset(buf, 0, SHM_HDR);
    memcpy(buf, "C123LCD", 8);
    buf[8] = LCD_W; buf[9] = 0;
    buf[10] = LCD_H; buf[11] = 0;
    buf[12] = ST7558_PAGES; buf[13] = 0;
    buf[14] = (s->inverse ? 1 : 0) | (s->off ? 2 : 0);
    buf[16] = s->seq & 0xff; buf[17] = (s->seq >> 8) & 0xff;
    buf[18] = (s->seq >> 16) & 0xff; buf[19] = (s->seq >> 24) & 0xff;
    for (p = 0; p < ST7558_PAGES; p++) {
        memcpy(buf + SHM_HDR + p * LCD_W, s->ram + p * ST7558_COLS, LCD_W);
    }
    if (pwrite(s->shm_fd, buf, sizeof(buf), 0) != (ssize_t)sizeof(buf)) {
        fprintf(stderr, "[st7558] ecriture de %s : %s\n", s->shm_path, strerror(errno));
    }
}

static uint8_t st7558_recv(I2CSlave *i2c)
{
    return 0xff;
}

static int st7558_send(I2CSlave *i2c, uint8_t data)
{
    St7558State *s = ST7558(i2c);

    switch (s->mode) {
    case MODE_IDLE:
        s->mode = (data & 0x40) ? MODE_DATA : MODE_CMD;
        break;
    case MODE_DATA:
        if (s->page < ST7558_PAGES && s->x < ST7558_COLS) {
            s->ram[s->page * ST7558_COLS + s->x] = data;
            s->dirty = s->redraw = true;
        }
        s->x++;
        break;
    case MODE_CMD:
        if (data & 0x80) {
            s->x = data & 0x7f;
        } else if ((data & 0xf0) == 0x40) {
            s->page = data & 0x0f;
        } else if ((data & 0xfe) == 0x20) {
            s->ext = data & 1;
        } else if (!s->ext) {
            switch (data) {
            case 0x0c: s->inverse = false; s->off = false; s->dirty = s->redraw = true; break;
            case 0x0d: s->inverse = true;  s->off = false; s->dirty = s->redraw = true; break;
            case 0x08: s->off = true; s->dirty = s->redraw = true; break;
            default: break;
            }
        }
        break;
    }
    return 0;
}

static int st7558_event(I2CSlave *i2c, enum i2c_event event)
{
    St7558State *s = ST7558(i2c);

    switch (event) {
    case I2C_START_SEND:
    case I2C_START_RECV:
        s->mode = MODE_IDLE;
        break;
    case I2C_FINISH:
        s->mode = MODE_IDLE;
        if (s->dirty) {
            s->dirty = false;
            st7558_export(s);
        }
        break;
    case I2C_NACK:
        break;
    default:
        return -1;
    }
    return 0;
}

static void st7558_update_display(void *opaque)
{
    St7558State *s = opaque;
    DisplaySurface *surface = qemu_console_surface(s->con);
    uint32_t on, offc;
    int x, y, k, stride;
    uint8_t *base;

    if (!s->redraw || surface_bits_per_pixel(surface) != 32) {
        return;
    }
    /* grey-green of the C123 LCD: dark pixels on a light background */
    on = s->inverse ? 0xffc8d8b0u : 0xff1a2a1au;
    offc = s->inverse ? 0xff1a2a1au : 0xffc8d8b0u;
    if (s->off) {
        on = offc = 0xff9aa892u;
    }
    base = surface_data(surface);
    stride = surface_stride(surface);
    for (y = 0; y < LCD_H; y++) {
        const uint8_t *src = s->ram + ST7558_COLS * (y >> 3);
        uint8_t mask = 1 << (y & 7);
        uint32_t *row = (uint32_t *)(base + (size_t)y * MAGNIFY * stride);
        for (x = 0; x < LCD_W; x++) {
            uint32_t c = (src[x] & mask) ? on : offc;
            for (k = 0; k < MAGNIFY; k++) {
                row[x * MAGNIFY + k] = c;
            }
        }
        for (k = 1; k < MAGNIFY; k++) {
            memcpy(base + ((size_t)y * MAGNIFY + k) * stride, row, LCD_W * MAGNIFY * 4);
        }
    }
    s->redraw = false;
    dpy_gfx_update(s->con, 0, 0, LCD_W * MAGNIFY, LCD_H * MAGNIFY);
}

static void st7558_invalidate_display(void *opaque)
{
    St7558State *s = opaque;
    s->redraw = true;
}

static const GraphicHwOps st7558_ops = {
    .invalidate = st7558_invalidate_display,
    .gfx_update = st7558_update_display,
};

static void st7558_realize(DeviceState *dev, Error **errp)
{
    St7558State *s = ST7558(dev);
    const char *env = getenv("CALYPSO_LCD_SHM");

    s->shm_fd = -1;
    s->shm_path = g_strdup(env && *env ? env : SHM_DEFAULT);
    if (strcmp(s->shm_path, "none")) {
        s->shm_fd = open(s->shm_path, O_RDWR | O_CREAT | O_TRUNC, 0666);
        if (s->shm_fd < 0) {
            fprintf(stderr, "[st7558] %s : %s (pas d'image partagee)\n",
                    s->shm_path, strerror(errno));
        } else {
            fchmod(s->shm_fd, 0666);
            if (ftruncate(s->shm_fd, SHM_HDR + LCD_W * ST7558_PAGES) < 0) {
                fprintf(stderr, "[st7558] ftruncate %s : %s\n", s->shm_path, strerror(errno));
            }
            st7558_export(s);
            fprintf(stderr, "[st7558] ecran C123 96x65 : image partagee %s\n", s->shm_path);
        }
    }
    s->con = graphic_console_init(dev, 0, &st7558_ops, s);
    qemu_console_resize(s->con, LCD_W * MAGNIFY, LCD_H * MAGNIFY);
}

static void st7558_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    I2CSlaveClass *k = I2C_SLAVE_CLASS(klass);

    dc->realize = st7558_realize;
    dc->desc = "Sitronix ST7558 LCD 96x65 (Motorola C123)";
    k->event = st7558_event;
    k->recv = st7558_recv;
    k->send = st7558_send;
}

static const TypeInfo st7558_info = {
    .name          = TYPE_ST7558,
    .parent        = TYPE_I2C_SLAVE,
    .instance_size = sizeof(St7558State),
    .class_init    = st7558_class_init,
};

static void st7558_register_types(void)
{
    type_register_static(&st7558_info);
}

type_init(st7558_register_types)

4.30 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_tpu.c

2967 octets, 112 lignes → 112 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "qemu/osdep.h"
#include "hw/arm/calypso/calypso_trx.h"
#include "hw/arm/calypso/calypso_tsp.h"

#define TPU_OP_SLEEP     0
#define TPU_OP_AT        1
#define TPU_OP_OFFSET    2
#define TPU_OP_SYNCHRO   3
#define TPU_OP_MOVE      4
#define TPU_OP_WAIT      5

#define QBITS_PER_TDMA   5000

static struct {
    uint16_t insns[CALYPSO_TPU_RAM_SIZE / 2];
    int len;
    int cursor;
    int qbit;
    int wait_frames;
    bool active;
    uint16_t *tpu_regs;
} seq;

static void seq_run(uint32_t fn)
{
    while (seq.active && seq.cursor < seq.len) {
        uint16_t insn = seq.insns[seq.cursor];
        if (insn == 0x0000) {
            int next = seq.cursor + 1;
            bool had_any = seq.cursor > 0;
            if (had_any && (next >= seq.len || seq.insns[next] == 0x0000)) {
                seq.active = false;
                return;
            }
            seq.cursor++;
            continue;
        }
        uint8_t opcode = (insn >> 13) & 0x7;
        uint16_t payload = insn & 0x1FFF;

        if (opcode == TPU_OP_AT) {
            seq.cursor++;
            if (payload > seq.qbit) {
                seq.qbit = payload;
                continue;
            }
            seq.qbit = payload;
            seq.wait_frames = 1;
            return;
        }
        if (opcode == TPU_OP_WAIT) {
            seq.cursor++;
            int target = seq.qbit + (int)payload;
            if (target < QBITS_PER_TDMA) {
                seq.qbit = target;
                continue;
            }
            seq.wait_frames = target / QBITS_PER_TDMA;
            seq.qbit = target % QBITS_PER_TDMA;
            return;
        }
        if (opcode == TPU_OP_SYNCHRO || opcode == TPU_OP_OFFSET) {
            if (seq.tpu_regs) {
                seq.tpu_regs[(opcode == TPU_OP_SYNCHRO ? TPU_SYNCHRO : TPU_OFFSET) / 2] = payload;
            }
            seq.cursor++;
            continue;
        }
        if (opcode == TPU_OP_MOVE) {
            uint8_t addr = insn & 0x1F;
            uint8_t data = (insn >> 5) & 0xFF;
            if (calypso_tsp_owns_addr(addr)) {
                calypso_tsp_move(addr, data, fn);
            }
            seq.cursor++;
            continue;
        }
        seq.cursor++;
    }
    seq.active = false;
}

/* current position of the sequencer in the frame (calypso_tsp.c: fake air) */
int calypso_tpu_qbit(void)
{
    return seq.qbit;
}

void calypso_tpu_run_scenario(uint16_t *tpu_ram, uint32_t fn, uint16_t *tpu_regs)
{
    memcpy(seq.insns, tpu_ram, sizeof(seq.insns));
    seq.len = CALYPSO_TPU_RAM_SIZE / 2;
    seq.cursor = 0;
    seq.qbit = 0;
    seq.wait_frames = 0;
    seq.tpu_regs = tpu_regs;
    seq.active = true;
    seq_run(fn);
}

void calypso_tpu_sequencer_tick(uint32_t fn)
{
    if (!seq.active || seq.wait_frames <= 0) {
        return;
    }
    seq.wait_frames--;
    if (seq.wait_frames > 0) {
        return;
    }
    seq_run(fn);
}

4.31 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_trf6151.c

4635 octets, 154 lignes → 154 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "qemu/osdep.h"
#include "hw/arm/calypso/calypso_trf6151.h"

#define SYSTEM_INHERENT_GAIN    71
#define TRF6151_FE_GAIN_LOW      7
#define TRF6151_FE_GAIN_HIGH    27
#define TRF6151_VGA_GAIN_MIN    14
#define RX_VGA_GAIN_SHIFT       11

#define TRF6151_REG_RX           0
#define TRF6151_REG_PLL          1
#define TRF6151_REG_PWR          2
#define TRF6151_REG_ADDR_MASK    0x7
#define TRF6151_BAND_1800        2

#define TRF6151_REG_RX_RESET    0x9E00
#define TRF6151_TSP_DEV          1

static uint16_t g_reg_rx = TRF6151_REG_RX_RESET;
static uint16_t g_reg_pll;
static uint16_t g_reg_pwr;

static int trf6151_gain_from_reg(uint16_t reg_rx)
{
    int gain = 0;
    unsigned vga;

    switch ((reg_rx >> 9) & 3) {
    case 0: gain += TRF6151_FE_GAIN_LOW;  break;
    case 3: gain += TRF6151_FE_GAIN_HIGH; break;
    default:   break;
    }

    vga = (reg_rx >> RX_VGA_GAIN_SHIFT) & 0x1f;
    if (vga < 6) {
        vga = 6;
    }
    gain += TRF6151_VGA_GAIN_MIN + (int)(vga - 6) * 2;

    return gain;
}

void calypso_trf6151_tsp_write(uint8_t dev_idx, uint32_t word)
{
    /* TSPEN1 (osmocom, Pirelli) or TSPEN2: the TI firmware of the Motorola C1xx addresses the
     * TRF6151 on TSP device 2 (tpudrv12.h, TC1_DEVICE_RF = TC1_DEVICE2); both must be accepted
     * for the ARFCN to be known and the fake air to filter. */
    if (dev_idx != TRF6151_TSP_DEV && dev_idx != 2) {
        return;
    }
    switch (word & TRF6151_REG_ADDR_MASK) {
    case TRF6151_REG_RX:
        g_reg_rx = (uint16_t)word;
        break;
    case TRF6151_REG_PLL:
        g_reg_pll = (uint16_t)word;
        break;
    case TRF6151_REG_PWR:
        g_reg_pwr = (uint16_t)word;
        break;
    default:
        break;
    }
}

int calypso_trf6151_arfcn(void)
{
    unsigned a = (g_reg_pll >> 3) & 0x3f;
    unsigned b = ((g_reg_pll >> 9) & 0x7f) + 64;
    unsigned n = b * 64 + a;
    bool high_band = ((g_reg_pwr >> 6) & 3) == TRF6151_BAND_1800;
    unsigned freq10 = high_band ? n * 2 : n;
    if (!g_reg_pll) {
        return -1;
    }
    if (freq10 >= 9350 && freq10 <= 9598) {
        return (int)((freq10 - 9350) / 2);
    }
    if (freq10 >= 9252 && freq10 < 9350) {
        return 1024 - (int)(9350 - freq10) / 2;
    }
    if (freq10 >= 18052 && freq10 <= 18798) {
        return (int)(512 + (freq10 - 18052) / 2);
    }
    if (freq10 >= 19302 && freq10 <= 19898) {
        return (int)(512 + (freq10 - 19302) / 2);
    }
    return -1;
}

/* Realistic air (c54x_exe, CALYPSO_AIR_NIVEAUX): synthesizer state for the `reserve`
 * field of calypso_rf_arfcn.h. bit 15: RX mode (PWR bit 11); bit 14: TX mode (PWR
 * bit 13); bit 13: frequency in an UPLINK band (GSM900/E-GSM 880-915 MHz, DCS
 * 1710-1785 MHz), matching ARFCN in bits 0..10. calypso_trf6151_arfcn() only decodes
 * the downlink bands and returns -1 for these frequencies, so this is how the uplink
 * view of the rssi app (RX tuned to the UL) is told apart from "unknown". */
int calypso_trf6151_rf_flags(void)
{
    unsigned a = (g_reg_pll >> 3) & 0x3f;
    unsigned b = ((g_reg_pll >> 9) & 0x7f) + 64;
    unsigned n = b * 64 + a;
    bool high_band = ((g_reg_pwr >> 6) & 3) == TRF6151_BAND_1800;
    unsigned freq10 = high_band ? n * 2 : n;
    int f = 0, ul = -1;
    if (g_reg_pwr & (1u << 11)) {
        f |= 1 << 15;
    }
    if (g_reg_pwr & (1u << 13)) {
        f |= 1 << 14;
    }
    if (!g_reg_pll) {
        return f;
    }
    if (freq10 >= 8902 && freq10 <= 9148) {
        ul = (int)((freq10 - 8900) / 2);
    } else if (freq10 >= 8802 && freq10 < 8900) {
        ul = 1024 - (int)(8900 - freq10) / 2;
    } else if (freq10 >= 17102 && freq10 <= 17848) {
        ul = (int)(512 + (freq10 - 17102) / 2);
    }
    if (ul >= 0) {
        f |= (1 << 13) | (ul & 0x7ff);
    }
    return f;
}

int calypso_trf6151_total_gain_db(void)
{
    return SYSTEM_INHERENT_GAIN + trf6151_gain_from_reg(g_reg_rx);
}

uint16_t calypso_trf6151_apm_for_rf(int target_rf_dbm)
{
    int total_gain = calypso_trf6151_total_gain_db();
    int bb_dbm = target_rf_dbm + total_gain;
    int apm;

    if (bb_dbm < 0) {
        bb_dbm = 0;
    }
    apm = bb_dbm * 64;
    /* Never 0: a real DSP always measures some noise. a_pm = 0 counts as a failure for
     * the TI L1 (l1_check_pm_error: 100 null PMs -> l1_initialize_for_recovery, DSP
     * restarted every ~173 frames while scanning empty frequencies). The L1 reads
     * pm = a_pm >> 5 (l1ddsp_meas_read): floor 64 (1 dB above 0), pm = 2. */
    if (apm < 64) {
        apm = 64;
    }
    if (apm > 0xFFFF) {
        apm = 0xFFFF;
    }
    return (uint16_t)apm;
}

4.32 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_trf6151.h

422 octets, 15 lignes → 15 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef HW_ARM_CALYPSO_TRF6151_H
#define HW_ARM_CALYPSO_TRF6151_H

#include <stdint.h>

void calypso_trf6151_tsp_write(uint8_t dev_idx, uint32_t word);

int calypso_trf6151_total_gain_db(void);

uint16_t calypso_trf6151_apm_for_rf(int target_rf_dbm);
int calypso_trf6151_arfcn(void);
int calypso_trf6151_rf_flags(void);   /* RX/TX mode + uplink ARFCN, see the .c */

#endif

4.33 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_trx.c

58750 octets, 1438 lignes → 1438 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "qemu/osdep.h"
#include "qemu/timer.h"
#include "qemu/main-loop.h"
#include "qemu/log.h"
#include "sysemu/runstate.h"
#include "exec/address-spaces.h"
#include "exec/cpu-common.h"
#include "hw/core/cpu.h"
#include "target/arm/cpu.h"
#include "hw/irq.h"
#include "hw/arm/calypso/calypso_api.h"
#include "hw/arm/calypso/calypso_l1_ops.h"
#include "hw/arm/calypso/calypso_trx.h"
#include "hw/arm/calypso/calypso_inth.h"
/* l1-dsp/ is only linked under --enable-l1-dsp: WEAK symbol, tested before the
 * call, so this file stays common to both L1s (see the l1-dsp island). */
extern void calypso_twl3025_set_afc_dac(int16_t dac_value) __attribute__((weak));
#include "hw/arm/calypso/calypso_uart.h"
#include "hw/arm/calypso/calypso_timer.h"
#include "hw/arm/calypso/calypso_sim.h"
#include "hw/arm/calypso/calypso_dsp_pont.h"
#include "hw/arm/calypso/l1-dsp/calypso_mailbox.h"
#include <pthread.h>
#include <time.h>
#include <poll.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>

extern CalypsoUARTState *g_uart_modem;
extern CalypsoUARTState *g_uart_irda;

#define FRAME_IRQ_PULSE_NS     1000000

/* CPU KICK PERIOD.
 *
 * cpu_kick() does `cpu_exit(first_cpu)` + `qemu_notify_event()`: that is what
 * hands control back to the main loop, hence what lets the TDMA timer be
 * SERVED. As long as the CPU does not exit, an expired timer is seen by nobody.
 * The period has no measurable effect on the frame rate (flat from 5 ms down
 * to 0.29 ms): the per-frame cost is the work itself (lock-step serialises
 * the ARM and the DSP, and the C54x interpreter executes the frame).
 * CALYPSO_CPU_KICK_NS remains tunable for measurements. */
#define CPU_KICK_NS_DEFAUT     5000000    /* no measurable effect; default kept */
static int64_t cpu_kick_ns(void)
{
    static int64_t ns;
    if (!ns) {
        const char *e = getenv("CALYPSO_CPU_KICK_NS");
        ns = e ? (int64_t)strtoll(e, NULL, 0) : 0;
        if (ns < 10000 || ns > 100000000) {
            ns = CPU_KICK_NS_DEFAUT;
        }
    }
    return ns;
}
#define CPU_KICK_NS            cpu_kick_ns()

/* LOCK-STEP RETRY QUANTUM.
 *
 * When the ARM's l1_sync or the DSP has not finished, we do not block: the
 * TDMA tick is rescheduled a little later and we come back. That "little
 * later" is GSM_TDMA_NS/16 = 288 us. The quantum has no measurable effect on
 * the frame rate (/16 and /64 give the same rate).
 * What matters: a late frame must not be dropped. osmo-bts-trx cannot follow
 * a slow clock (its drift filter is an empty TODO, scheduler_trx.c:571); it
 * resynchronises and produces nothing meanwhile, i.e. gaps of ~200 frames.
 *
 * The TOTAL wait budget is preserved: the retry ceilings are scaled by the
 * same factor (see PONT_ATTENTES_MAX).
 * CALYPSO_PONT_RETRY_DIV allows measuring other values without rebuilding. */
#define PONT_RETRY_DIV_DEFAUT  16        /* no measurable effect; default kept */
static int pont_retry_div(void)
{
    static int div;
    if (!div) {
        const char *e = getenv("CALYPSO_PONT_RETRY_DIV");
        div = e ? atoi(e) : 0;
        if (div < 1 || div > 8192) {
            div = PONT_RETRY_DIV_DEFAUT;
        }
    }
    return div;
}
#define PONT_RETRY_NS      ((int64_t)GSM_TDMA_NS / pont_retry_div())
/* same wall-clock budget as 256 retries of GSM_TDMA_NS/16 */
#define PONT_ATTENTES_MAX  (256 * pont_retry_div() / 16)
#define IDLE_PC_LO             0x00823000u
#define IDLE_PC_HI             0x00826000u
#define INTH_MASK_ADDR         0xFFFFFA08u
#define BOOT_POLLS_BEFORE_BOOT 3
#define FN_SYNC_TOLERANCE      16
#define FN_SYNC_WINDOW         5

typedef struct CalypsoTRX {
    qemu_irq *irqs;
    MemoryRegion api_iomem;
    uint16_t *api_ram;          /* g_new0, or mmap of the bridge shared segment */
    /* EXTERNAL DSP (CALYPSO_DSP_EXTERN=<socket>|1): layer 1 is a real C54x
     * running inside c54x_exe instead of a gr-gsm shunt. API RAM is shared
     * through /dev/shm and the frame is locked by TICK/DONE (see
     * include/hw/arm/calypso/calypso_dsp_pont.h). */
    bool pont;
    bool pont_pending;          /* a TICK was sent, DONE not collected yet */
    int pont_fd;
    QEMUTimer *pont_boot_timer; /* clocks the DSP alone, before the firmware starts the TDMA */
    unsigned pont_timeouts;
    uint64_t pont_frames;
    uint64_t pont_api_irqs;
    uint8_t dsp_page;
    bool bl_booted;
    unsigned bl_polls;
    uint32_t bl_lu_fn;          /* frame of the ARM's last DOWNLOAD_STATUS read (ARM lock) */
    MemoryRegion tpu_iomem;
    MemoryRegion tpu_ram_iomem;
    uint16_t tpu_regs[CALYPSO_TPU_SIZE / 2];
    uint16_t tpu_ram[CALYPSO_TPU_RAM_SIZE / 2];
    MemoryRegion tsp_iomem;
    uint16_t tsp_regs[CALYPSO_TSP_SIZE / 2];
    MemoryRegion ulpd_iomem;
    uint16_t ulpd_regs[CALYPSO_ULPD_SIZE / 2];
    uint32_t ulpd_counter;
    MemoryRegion sim_iomem;
    CalypsoSim *sim;
    QEMUTimer *tdma_timer;
    QEMUTimer *frame_irq_timer;
    QEMUTimer *kick_timer;
    uint32_t fn;
    int64_t fn_offset;
    int64_t fn_offset_hist[FN_SYNC_WINDOW];
    unsigned fn_offset_n;
    bool fn_synced;
    bool tdma_running;
    /* [2026-09-21] ARM FIRST (lock-step): the TICK of frame N is handed to the
     * DSP only once the firmware's l1_sync(N) is over (INTH end of service of
     * the frame IRQ). The TICK parameters are latched when the frame IRQ is
     * raised: DSP_EN and d_dsp_page then still describe the scenario the ARM
     * ended in frame N-1, i.e. the one the DSP must run in frame N.
     * [2026-09-21] Two phases: TICK before the ARM IRQ (the ROM's ISR reads
     * the page of N-1 and writes the R page), PONT_GO after l1_sync(N). */
    int      tick_phase;        /* 0: frame start, 1: waiting for phase A + the ARM's l1_sync */
    bool     deux_phases;       /* set while pont_echange() must flag the TICK */
    bool     phase_a_recue;     /* DONE|PHASE_A of this frame collected */
    bool     go_inutile;        /* single-phase DSP answered a final DONE already */
    uint64_t eoi_cible;         /* frame_eoi to reach before the TICK goes out */
    unsigned eoi_attentes, eoi_timeouts;
    unsigned eoi_longues;       /* l1_sync waits that went beyond 256 retries */
    uint8_t burst_ring[8];
    unsigned burst_w, burst_r;
    uint16_t burst_cur;
    uint32_t burst_last_fn;
    /* Request reference of the IMMEDIATE ASSIGNMENT, see pont_reqref_corrigee():
     * the RA of the last d_rach write, and the frame the firmware memorised for
     * each one (its last_rach, sampled once per frame). */
    uint8_t  rach_ra;
    bool     rach_ra_vue;
    uint32_t rach_fn[256];
    uint8_t  rach_file[8];       /* written RAs, waiting for their RACH_CONF */
    unsigned rach_file_w, rach_file_r;
    /* Dedicated channel to announce to the DSP at the next TICK (PONT_DCCH). */
    bool     dcch_a_dire;
    uint8_t  dcch_tn, dcch_ss;
    int      dcch_genre;
} CalypsoTRX;

static CalypsoTRX *g_trx;
static volatile uint32_t g_wall_fn;
static volatile bool g_wall_running;
static pthread_t g_wall_thread;

uint16_t *calypso_api_ram(void)
{
    return g_trx->api_ram;
}

uint32_t calypso_trx_get_fn(void)
{
    if (!g_trx) {
        return 0;
    }
    int64_t fn = ((int64_t)g_trx->fn + g_trx->fn_offset) % (int64_t)GSM_HYPERFRAME;
    if (fn < 0) {
        fn += GSM_HYPERFRAME;
    }
    return (uint32_t)fn;
}

void calypso_trx_autosync_fn(uint32_t sch_fn)
{
    if (!g_trx) {
        return;
    }
    int64_t raw = (int64_t)sch_fn - (int64_t)g_trx->fn;

    /* gr-gsm/UDP/QEMU-loop latency can only delay the local fn read, hence
     * inflate fn and shrink the measured offset. The maximum over the last few
     * SCHs is the value closest to the truth. */
    g_trx->fn_offset_hist[g_trx->fn_offset_n % FN_SYNC_WINDOW] = raw;
    g_trx->fn_offset_n++;
    unsigned n = g_trx->fn_offset_n < FN_SYNC_WINDOW ? g_trx->fn_offset_n : FN_SYNC_WINDOW;
    int64_t offset = g_trx->fn_offset_hist[0];
    for (unsigned i = 1; i < n; i++) {
        if (g_trx->fn_offset_hist[i] > offset) {
            offset = g_trx->fn_offset_hist[i];
        }
    }

    int64_t drift = offset - g_trx->fn_offset;
    if (drift < 0) {
        drift = -drift;
    }
    if (g_trx->fn_synced && drift <= FN_SYNC_TOLERANCE) {
        return;
    }
    g_trx->fn_offset = offset;
    g_trx->fn_synced = true;
    fprintf(stderr, "[trx] horloge FN calee sur le SCH gr-gsm : fn=%u offset=%lld (brut=%lld)\n",
            sch_fn, (long long)offset, (long long)raw);
}

/* ── external DSP bridge ──────────────────────────────────────────────── */
static bool pont_send(CalypsoTRX *s, uint32_t type, uint32_t a, uint32_t b, uint32_t c)
{
    CalypsoPontMsg m = { type, a, b, c };
    if (s->pont_fd < 0) {
        return false;
    }
    return send(s->pont_fd, &m, sizeof(m), MSG_NOSIGNAL) == (ssize_t)sizeof(m);
}

static bool pont_recv(CalypsoTRX *s, CalypsoPontMsg *m, int timeout_ms)
{
    struct pollfd pfd = { .fd = s->pont_fd, .events = POLLIN };
    int rc = poll(&pfd, 1, timeout_ms);
    if (rc <= 0) {
        return false;
    }
    ssize_t n = recv(s->pont_fd, m, sizeof(*m), 0);
    if (n != (ssize_t)sizeof(*m)) {
        fprintf(stderr, "[trx] pont DSP : connexion perdue (recv=%zd)\n", n);
        close(s->pont_fd);
        s->pont_fd = -1;
        return false;
    }
    return true;
}

static void pont_connect(CalypsoTRX *s, const char *spec)
{
    const char *sock = (strcmp(spec, "1") == 0) ? CALYPSO_PONT_SOCK : spec;
    int shm = shm_open(CALYPSO_PONT_SHM, O_RDWR, 0);
    if (shm < 0) {
        fprintf(stderr, "[trx] pont DSP : shm_open(%s) : %s - lancez d'abord "
                "c54x_exe --arm\n", CALYPSO_PONT_SHM, strerror(errno));
        exit(1);
    }
    struct stat st;
    if (fstat(shm, &st) < 0 || st.st_size < CALYPSO_PONT_SHM_BYTES) {
        fprintf(stderr, "[trx] pont DSP : segment %s trop petit (%lld < %d)\n",
                CALYPSO_PONT_SHM, (long long)st.st_size, CALYPSO_PONT_SHM_BYTES);
        exit(1);
    }
    /* 64 KB page-aligned ARM window; the first 16 KB are the shared segment
     * (the DSP API window), the rest is private and empty. */
    void *base = NULL;
    if (posix_memalign(&base, 4096, CALYPSO_API_SIZE) != 0) {
        fprintf(stderr, "[trx] pont DSP : posix_memalign\n");
        exit(1);
    }
    memset(base, 0, CALYPSO_API_SIZE);
    void *map = mmap(base, CALYPSO_PONT_SHM_BYTES, PROT_READ | PROT_WRITE,
                     MAP_SHARED | MAP_FIXED, shm, 0);
    close(shm);
    if (map == MAP_FAILED || map != base) {
        fprintf(stderr, "[trx] pont DSP : mmap : %s\n", strerror(errno));
        exit(1);
    }
    s->api_ram = base;

    s->pont_fd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
    struct sockaddr_un sa = { .sun_family = AF_UNIX };
    snprintf(sa.sun_path, sizeof(sa.sun_path), "%s", sock);
    if (s->pont_fd < 0 || connect(s->pont_fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
        fprintf(stderr, "[trx] pont DSP : connect(%s) : %s - lancez d'abord "
                "c54x_exe --arm\n", sock, strerror(errno));
        exit(1);
    }
    CalypsoPontMsg m;
    if (!pont_send(s, PONT_HELLO, CALYPSO_API_WORDS, CALYPSO_PONT_MAGIC, 0) ||
        !pont_recv(s, &m, 2000) || m.type != PONT_HELLO_OK ||
        m.a != CALYPSO_API_WORDS || m.b != CALYPSO_PONT_MAGIC) {
        fprintf(stderr, "[trx] pont DSP : poignee de main refusee par %s\n", sock);
        exit(1);
    }
    s->pont = true;
    fprintf(stderr, "[trx] pont DSP : API RAM partagee (%s) + socket %s - le C54x "
            "est dans c54x_exe, la couche 1 gr-gsm est desactivee\n",
            CALYPSO_PONT_SHM, sock);
    calypso_l1_disable("DSP externe");
}

/* ── Request reference of the IMMEDIATE ASSIGNMENT (DSP setup) ───────────
 *
 * The mobile accepts an IMM ASS only if its request reference matches
 * EXACTLY what ITS layer 1 confirmed: gsm48_match_ra() (osmocom-bb
 * gsm48_rr.c:3359) compares the RA *and* T1'/T2/T3, and otherwise logs
 * "request %02x matches but not frame number".
 *
 * The bench does not transmit the access burst in the frame the firmware
 * believes it did: pont.py schedules it on ITS own clock (pont/uplink.py,
 * _poll_rach -> _next_fn(4, ...)), several frames later. The BTS therefore
 * stamps the reference with another frame and the mobile discards the
 * assignment (it stays in "connection pending", never sets up an uplink
 * SDCCH task; on the BSC side, SDCCH lchans end in "lchan allocation
 * failed ... Timeout").
 *
 * The gr-gsm layer 1 fixes this by rewriting bytes 8-9 of the block
 * (l1-grgsm/calypso_l1_grgsm.c:836-845, feed_agch). With an external DSP it
 * is disabled, so the same thing is done here, on the ARM read of the
 * relevant a_cd word, from the firmware's "last_rach" — the frame the
 * firmware itself memorised and reported in L1CTL_RACH_CONF.
 *
 * The fix is only applied if the assignment's RA is the one of the last
 * d_rach write: otherwise the assignment answers an attempt older than
 * last_rach, and forging a match would be worse than the failure.
 * MONTANT_REQREF=0 disables the correction.
 */
static uint32_t pont_last_rach_fn(void)
{
    static uint32_t adr;
    static int cherche;
    if (!cherche) {
        cherche = 1;
        adr = calypso_firmware_symbol("last_rach");
        fprintf(stderr, "[trx] reference de requete : last_rach %s (%s)\n",
                adr ? "trouve" : "INTROUVABLE",
                calypso_firmware_elf() ? calypso_firmware_elf() : "pas d'ELF");
    }
    if (!adr) {
        return 0;
    }
    uint32_t fn = 0;
    cpu_physical_memory_read(adr, &fn, sizeof(fn));
    return le32_to_cpu(fn);
}

/* RA -> transmit frame pairing.
 *
 * The RAs are queued when d_rach is written, and every L1CTL_RACH_CONF (seen
 * by calypso_dcch_tap.c in the sercomm stream) dequeues one. In a burst of
 * attempts the firmware writes the d_rach of the next attempt BEFORE
 * last_rach has moved for the previous one, so sampling last_rach.fn once per
 * frame would attribute the frame to the wrong RA. The queue is the same
 * pairing the mobile does, which stores cr_ra and the RACH_CONF fn together in
 * cr_hist -- exactly the value gsm48_match_ra() will compare. */
/* calypso_dcch_tap.c has just learnt the mobile's dedicated channel. It is not
 * sent right away: the bridge protocol is synchronous, and a message slipped
 * in while waiting for a PONT_GO would be ignored by the DSP. It goes out
 * just before the next TICK. */
void calypso_trx_dcch(int genre, int ss, int tn)
{
    CalypsoTRX *s = g_trx;
    if (!s || !s->pont) {
        return;
    }
    s->dcch_genre = genre;
    s->dcch_ss = (uint8_t)ss;
    s->dcch_tn = (uint8_t)tn;
    s->dcch_a_dire = true;
}

void calypso_trx_rach_conf(uint32_t fn)
{
    CalypsoTRX *s = g_trx;
    if (!s || !fn) {
        return;
    }
    if (s->rach_file_r == s->rach_file_w) {
        static unsigned n;
        if (n++ < 5) {
            fprintf(stderr, "[trx] RACH_CONF fn=%u sans RA en attente\n", fn);
        }
        return;
    }
    uint8_t ra = s->rach_file[s->rach_file_r++ % ARRAY_SIZE(s->rach_file)];
    s->rach_fn[ra] = fn;
    s->rach_ra = ra;
    s->rach_ra_vue = true;
}

static bool pont_reqref_corrigee(CalypsoTRX *s, hwaddr woff, uint16_t *out)
{
    static int actif = -1;
    if (actif < 0) {
        const char *e = getenv("MONTANT_REQREF");
        actif = (e && *e == '0') ? 0 : 1;
    }
    /* MONTANT_REQREF=0 (max ROM profile): always compare and log, but never
     * rewrite -- the BTS reference reaches the firmware as is, like on a real
     * phone (if it does not recognise it, it sends another RACH). */
    if (woff != API_NDB + NDB_A_CD + 14) {
        return false;
    }

    /* a_cd: word 0 = status, the 23 L2 bytes start at word 3 (byte +6).
     * L3 = [pseudo-length, PD, type, ...] -> 06 3f = IMMEDIATE ASSIGNMENT,
     * the RA is byte 7 and the request reference bytes 8-9. */
    const uint8_t *d = (const uint8_t *)s->api_ram + API_NDB + NDB_A_CD + 6;
    if (d[1] != 0x06 || d[2] != 0x3f) {
        return false;
    }
    uint32_t memo = s->rach_fn[d[7]];
    if (!memo && s->rach_ra_vue && d[7] == s->rach_ra) {
        memo = pont_last_rach_fn();
    }
    if (!memo) {
        static unsigned n;
        if (n++ < 5) {
            fprintf(stderr, "[trx] IMM ASS ra=0x%02x : aucune trame memorisee "
                    "pour cette RA - reference laissee telle quelle\n", d[7]);
        }
        return false;
    }

    uint16_t t1p = (uint16_t)((memo / 1326u) % 32u);
    uint8_t t2 = (uint8_t)(memo % 26u);
    uint8_t t3 = (uint8_t)(memo % 51u);
    uint8_t o8 = (uint8_t)((t1p << 3) | ((t3 >> 3) & 7));
    uint8_t o9 = (uint8_t)(((t3 & 7) << 5) | (t2 & 0x1f));
    *out = (uint16_t)(o8 | (o9 << 8));

    if (*out != (uint16_t)(d[8] | (d[9] << 8)) && !actif) {
        static unsigned n0;
        if (n0++ < 20)
            fprintf(stderr, "[trx] IMM ASS ra=0x%02x : reference de la BTS != last_rach (fn=%u), "
                    "laissee telle quelle (MONTANT_REQREF=0)\n", d[7], memo);
        return false;
    }
    if (*out != (uint16_t)(d[8] | (d[9] << 8))) {
        static unsigned n;
        if (n++ < 20) {
            uint8_t at1 = (d[8] >> 3) & 0x1f;
            uint8_t at3 = (uint8_t)(((d[8] & 7) << 3) | ((d[9] >> 5) & 7));
            uint8_t at2 = d[9] & 0x1f;
            fprintf(stderr, "[trx] IMM ASS ra=0x%02x : reference %u/%u/%u de la BTS "
                    "-> %u/%u/%u (last_rach fn=%u)\n",
                    d[7], at1, at2, at3, t1p, t2, t3, memo);
        }
    } else {
        /* The ROM's RACH goes out at its own fn (c54x_exe tsp_tx, PONT_UL_RAW): the
         * BTS reference is already the one the firmware expects, nothing to rewrite.
         * Logged for the coverage table (99-couverture.sh: ROM path). */
        static unsigned n;
        if (n++ < 20)
            fprintf(stderr, "[trx] IMM ASS ra=0x%02x : reference %u/%u/%u identique a last_rach (fn=%u), rien a reecrire\n",
                    d[7], t1p, t2, t3, memo);
    }
    return true;
}

static void pont_echange(CalypsoTRX *s);
static void pont_boot_pas(CalypsoTRX *s);

/* CALYPSO_PONT_BOOT_POLL (default 1): see api_read, LEAD_READY wait */
static bool pont_boot_poll(void)
{
    static int v = -1;
    if (v < 0) {
        const char *e = getenv("CALYPSO_PONT_BOOT_POLL");
        v = (e && *e == '0') ? 0 : 1;
    }
    return v;
}

static uint64_t api_read(void *opaque, hwaddr off, unsigned size)
{
    CalypsoTRX *s = opaque;
    if (off >= CALYPSO_API_SIZE) {
        return 0;
    }
    const uint16_t *src = &s->api_ram[off / 2];
    uint64_t val = (size == 2) ? src[0] :
                   (size == 4) ? ((uint32_t)src[0] | ((uint32_t)src[1] << 16)) :
                   ((const uint8_t *)src)[off & 1];
    if (s->pont && (size == 2 || size == 4)) {
        uint16_t fix;
        if (size == 2) {
            if (pont_reqref_corrigee(s, off, &fix)) {
                val = fix;
            }
        } else {
            if (pont_reqref_corrigee(s, off, &fix)) {
                val = (val & 0xFFFF0000u) | fix;
            }
            if (pont_reqref_corrigee(s, off + 2, &fix)) {
                val = (val & 0x0000FFFFu) | ((uint32_t)fix << 16);
            }
        }
    }
    /* Mailbox monitor, ARM side (CALYPSO_MAILBOX=1, file mailbox-arm.log; the
     * DSP side is in c54x_exe). DSP word = 0x0800 + off/2, ctx = ARM PC,
     * fn = current bridge frame. */
    if (calypso_mbx_actif && (size == 2 || size == 4)) {
        calypso_mbx_suffixe = "arm";
        uint32_t pc = (first_cpu && first_cpu->cc && first_cpu->cc->get_pc) ? (uint32_t)first_cpu->cc->get_pc(first_cpu) : 0;
        calypso_mbx(MBX_ARM_RD, (uint16_t)(0x0800 + off / 2), (uint16_t)val, 0, pc, s->fn, 0);
        if (size == 4) {
            calypso_mbx(MBX_ARM_RD, (uint16_t)(0x0801 + off / 2), (uint16_t)(val >> 16), 0, pc, s->fn, 0);
        }
    }
    if (size != 2) {
        return val;
    }

    if (s->pont) {
        /* The external C54x writes d_task_d / d_burst_d into the read page
         * itself, and its ROM bootloader (parked at 0xb41c) answers the
         * BL_CMD_STATUS commands itself. The emulation below, written for the
         * DSP-less shunt, would force IDLE after three reads, BEFORE the DSP
         * has seen the command, making it miss the startup COPY_BLOCK. */
        /* DSP DOWNLOAD TIMED IN LOOP ITERATIONS. The TI firmware (leadapi.c,
         * LA_LoadPage) waits for LEAD_READY in DOWNLOAD_STATUS (= 0x0FFE) for at most
         * LA_TIMEOUT = 10000 reads: a few ms on the real Calypso, a few us here, while
         * the DSP only advances one frame per bridge exchange. Without help the
         * download may end in LA_ERR_TIMEOUT (DSP left in its bootloader, version read
         * 4e2a, neither L1 nor audio). Before the TDMA runs, a read of 0x0FFE without
         * LEAD_READY therefore advances the DSP by one frame every 32 reads.
         * CALYPSO_PONT_BOOT_POLL=0 disables this. */
        if (off == API_BL_STATUS) {
            s->bl_lu_fn = s->fn + 1;    /* +1: 0 = never read */
        }
        if (off == API_BL_STATUS && val != 1 && !s->tdma_running && pont_boot_poll()) {
            if ((++s->bl_polls & 31) == 0) {
                pont_boot_pas(s);
                val = s->api_ram[API_BL_STATUS / 2];
            }
        }
        return val;
    }
    uint16_t rv;
    if (calypso_l1_do_api_read_override((uint32_t)off, &rv)) {
        val = rv;
    }
    if (off == API_R_PAGE(0) + RP_D_TASK_D || off == API_R_PAGE(1) + RP_D_TASK_D) {
        s->burst_cur = s->burst_ring[s->burst_r++ & 7u];
        if (val == 0 && calypso_l1_do_si_valid()) {
            val = ALLC_DSP_TASK;
        }
    }
    if (off == API_R_PAGE(0) + RP_D_BURST_D || off == API_R_PAGE(1) + RP_D_BURST_D) {
        val = (uint16_t)((s->burst_cur + 3) & 3);
    }
    if (off == API_BL_STATUS && !s->bl_booted && ++s->bl_polls > BOOT_POLLS_BEFORE_BOOT) {
        s->api_ram[API_BL_STATUS / 2] = BL_STATUS_BOOT;
        s->api_ram[API_VERSION / 2] = API_VERSION_VALUE;
        s->api_ram[API_VERSION2 / 2] = 0;
        s->bl_booted = true;
        val = BL_STATUS_BOOT;
    }
    return val;
}

static void api_write(void *opaque, hwaddr off, uint64_t value, unsigned size)
{
    CalypsoTRX *s = opaque;
    if (off >= CALYPSO_API_SIZE) {
        return;
    }
    if (calypso_mbx_actif && (size == 2 || size == 4)) {        /* see api_read */
        calypso_mbx_suffixe = "arm";
        uint32_t pc = (first_cpu && first_cpu->cc && first_cpu->cc->get_pc) ? (uint32_t)first_cpu->cc->get_pc(first_cpu) : 0;
        calypso_mbx(MBX_ARM_WR, (uint16_t)(0x0800 + off / 2), (uint16_t)value, s->api_ram[off / 2], pc, s->fn, 0);
        if (size == 4) {
            calypso_mbx(MBX_ARM_WR, (uint16_t)(0x0801 + off / 2), (uint16_t)(value >> 16), s->api_ram[off / 2 + 1], pc, s->fn, 0);
        }
    }
    if (size == 2) {
        s->api_ram[off / 2] = (uint16_t)value;
    } else if (size == 4) {
        s->api_ram[off / 2] = (uint16_t)value;
        s->api_ram[off / 2 + 1] = (uint16_t)(value >> 16);
    } else {
        ((uint8_t *)s->api_ram)[off] = (uint8_t)value;
    }
    /* [2026-09-17] AFC RELAY. The firmware's afc_load_dsp() writes
     * dsp_api.db_w->d_afc (word 15 of the W page: byte 0x001E on page 0,
     * 0x0046 on page 1). On silicon the DSP serialises it to the TWL3025 over
     * the TSP; here this hook is the only relay. Without it the sample
     * rotation never moves, the frequency error seen by the FB detector never
     * converges, the firmware never reaches the SB threshold (800 Hz) and
     * replays FB forever. Measured on a deterministic replay: df ~ thousands
     * of Hz without the relay, below 100 Hz with it, and the 224 SB attempts
     * do take place. */
    if ((off == 0x001E || off == 0x0046) && size == 2 && calypso_twl3025_set_afc_dac) {
        calypso_twl3025_set_afc_dac((int16_t)(uint16_t)value);
    }

    /* The whole window, for the L1s that decode it (the C54x). Order matters:
     * after the store above and before the named hooks below, so an L1 that
     * reads API RAM back from this callback sees the value just written. */
    if (size == 2) {
        calypso_l1_do_api_write_observed((uint32_t)off, (uint16_t)value, size);
    }

    if (size == 2 && (off == API_W_PAGE(0) + WP_D_BURST_D ||
                      off == API_W_PAGE(1) + WP_D_BURST_D)) {
        uint32_t wfn = calypso_l1_do_l1s_fn();
        if (wfn != s->burst_last_fn + 1) {
            s->burst_w = s->burst_r = 0;
        }
        s->burst_last_fn = wfn;
        s->burst_ring[s->burst_w++ & 7u] = (uint8_t)(value & 3);
        calypso_l1_do_burst_written((uint16_t)value);
    }
    if (off == API_NDB + NDB_D_RACH && value != 0 && (size == 2 || size == 4)) {
        /* prim_rach.c:72 writes (uic|bsic)<<2 | ra<<8 just before setting
         * d_task_ra. The RA is kept: pont_reqref_corrigee() uses it to correct
         * only the assignment that answers THIS attempt. */
        uint8_t ra = (uint8_t)((value >> 8) & 0xff);
        if (s->rach_file_w - s->rach_file_r >= ARRAY_SIZE(s->rach_file)) {
            s->rach_file_r++;   /* queue full: drop the oldest */
        }
        s->rach_file[s->rach_file_w++ % ARRAY_SIZE(s->rach_file)] = ra;
        calypso_l1_do_rach_written((uint16_t)value, s->fn);
    }
    if (off == API_NDB + NDB_D_DSP_PAGE && size == 2) {
        s->dsp_page = value & 1;
        calypso_l1_do_page_written((uint16_t)value);
    }
    if (off == API_BL_STATUS) {
        if (value == 0 && !s->pont) {
            s->bl_booted = false;
            s->bl_polls = 0;
        } else if (value == BL_STATUS_READY) {
            /* With an external DSP this value is in fact BL_CMD_COPY_BLOCK
             * (2): the real bootloader consumes it, so leave API RAM alone. */
            if (!s->pont) {
                s->api_ram[API_VERSION / 2] = API_VERSION_VALUE;
                s->api_ram[API_VERSION2 / 2] = 0;
            }
            uint16_t mask;
            cpu_physical_memory_read(INTH_MASK_ADDR, &mask, 2);
            mask &= ~(1 << CALYPSO_IRQ_API);
            cpu_physical_memory_write(INTH_MASK_ADDR, &mask, 2);
            if (!s->pont) {
                s->api_ram[API_NDB / 2] = 0;
            }
        }
    }
}

void calypso_trx_dsp_reset_line(bool active)
{
    static bool precedent;
    CalypsoTRX *s = g_trx;
    if (s && s->pont && precedent && !active) {
        /* Reset release: the boot ROM restarts at 0xff80, posts IDLE and
         * parks at 0xb41c, ready for the ARM COPY_BLOCKs. This is the ONLY
         * point where the C54x is reset - never on a bootloader command, which
         * would destroy the pending command. */
        /* The reset goes to c54x_exe asynchronously. The LEAD_READY (1) left in 0x0FFE
         * by the bootloader from BEFORE the reset would otherwise be read right away by
         * the ARM (Magnetite, l1_func.c dsp_power_on), which would write its start command
         * there (2, address 0x7000); the bootloader restarted by the reset would then
         * rewrite 1 (ROM b419) and the start would be lost (DSP stuck in its 0xb41c loop).
         * On the real Calypso the bootloader restarts within a few us, before the ARM
         * read. The word is therefore cleared: the ARM waits for the LEAD_READY of the
         * NEW bootloader (api_read advances the DSP during that wait). */
        s->api_ram[API_BL_STATUS / 2] = 0;
        pont_send(s, PONT_RESET, 0, 0, 0);
        fprintf(stderr, "[trx] pont DSP : RESET_DSP relache par le firmware -> "
                "PONT_RESET (fn=%u)\n", s->fn);
    }
    precedent = active;
}

static const MemoryRegionOps api_ops = {
    .read = api_read,
    .write = api_write,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .valid = { .min_access_size = 1, .max_access_size = 4 },
    .impl = { .min_access_size = 1, .max_access_size = 4 },
};

static void tpu_done(CalypsoTRX *s)
{
    s->tpu_regs[TPU_CTRL / 2] &= ~TPU_CTRL_EN;
    calypso_tpu_run_scenario(s->tpu_ram, s->fn, s->tpu_regs);
    qemu_irq_raise(s->irqs[CALYPSO_IRQ_API]);
}

static void tdma_tick(void *opaque);

/* Externally imposed beat, at a given frame number: the clock master of the
 * C54x L1, where the DSP TINT0 timer drives the TDMA rather than the platform
 * wall clock. L1 -> platform direction, hence a direct symbol and not a vtable
 * entry (see calypso_l1_ops.h): only one implementation is possible. */
void calypso_trx_force_tick(uint32_t fn)
{
    if (!g_trx) {
        return;
    }
    g_trx->fn = fn;
    tdma_tick(g_trx);
}

static void tdma_start(CalypsoTRX *s);

static uint64_t tpu_read(void *o, hwaddr off, unsigned sz)
{
    CalypsoTRX *s = o;
    if (off == TPU_IT_DSP_PG) {
        return s->dsp_page;
    }
    return (off / 2 < CALYPSO_TPU_SIZE / 2) ? s->tpu_regs[off / 2] : 0;
}

static void tpu_write(void *o, hwaddr off, uint64_t val, unsigned sz)
{
    CalypsoTRX *s = o;
    if (off / 2 < CALYPSO_TPU_SIZE / 2) {
        s->tpu_regs[off / 2] = val;
    }
    if (off == TPU_CTRL && (val & TPU_CTRL_EN)) {
        s->tpu_regs[TPU_CTRL / 2] &= ~(TPU_CTRL_EN | TPU_CTRL_IDLE);
        tpu_done(s);
        /* The TI layer 1 (FreeCalypso Magnetite, tpudrv.c) never touches
         * TPU_INT_CTRL: the MCU frame IRQ is enabled at reset and the TDMA runs
         * as soon as the TPU is enabled (TP_Enable). Nucleus needs this start for
         * its tick (IQ_FrameHandler -> TMT_Timer_Interrupt), and the UARTs are
         * pumped by tdma_tick. osmocom-bb writes TPU_INT_CTRL beforehand, so
         * nothing changes for it. */
        if (!s->tdma_running && !(s->tpu_regs[TPU_INT_CTRL / 2] & ICTRL_MCU_FRAME)) {
            fprintf(stderr, "[trx] TPU valide (TP_Enable) sans ecriture de TPU_INT_CTRL : depart du TDMA\n");
            tdma_start(s);
        }
    }
    if (off == TPU_INT_CTRL && !(val & ICTRL_MCU_FRAME) && !s->tdma_running) {
        tdma_start(s);
    }
    if (off == TPU_IT_DSP_PG) {
        s->dsp_page = val & 1;
    }
}

static const MemoryRegionOps tpu_ops = {
    .read = tpu_read,
    .write = tpu_write,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .valid = { .min_access_size = 1, .max_access_size = 4 },
    .impl = { .min_access_size = 1, .max_access_size = 4 },
};

static uint64_t tpu_ram_read(void *o, hwaddr off, unsigned sz)
{
    CalypsoTRX *s = o;
    return (off / 2 < CALYPSO_TPU_RAM_SIZE / 2) ? s->tpu_ram[off / 2] : 0;
}

static void tpu_ram_write(void *o, hwaddr off, uint64_t v, unsigned sz)
{
    CalypsoTRX *s = o;
    if (off / 2 < CALYPSO_TPU_RAM_SIZE / 2) {
        s->tpu_ram[off / 2] = v;
    }
}

static const MemoryRegionOps tpu_ram_ops = {
    .read = tpu_ram_read,
    .write = tpu_ram_write,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .valid = { .min_access_size = 1, .max_access_size = 4 },
    .impl = { .min_access_size = 1, .max_access_size = 4 },
};

static uint64_t tsp_read(void *o, hwaddr off, unsigned sz)
{
    CalypsoTRX *s = o;
    if (off == TSP_RX_REG) {
        return 0xFFFF;
    }
    return (off / 2 < CALYPSO_TSP_SIZE / 2) ? s->tsp_regs[off / 2] : 0;
}

static void tsp_write(void *o, hwaddr off, uint64_t v, unsigned sz)
{
    CalypsoTRX *s = o;
    if (off / 2 < CALYPSO_TSP_SIZE / 2) {
        s->tsp_regs[off / 2] = v;
    }
}

static const MemoryRegionOps tsp_ops = {
    .read = tsp_read,
    .write = tsp_write,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .valid = { .min_access_size = 1, .max_access_size = 4 },
    .impl = { .min_access_size = 1, .max_access_size = 4 },
};

static uint64_t ulpd_read(void *o, hwaddr off, unsigned sz)
{
    CalypsoTRX *s = o;
    if (off >= 0x20 && off <= 0x40) {
        return 0;
    }
    switch (off) {
    case ULPD_SETUP_CLK13:
        return 0x2003;
    case ULPD_COUNTER_HI:
        s->ulpd_counter += 100;
        return (s->ulpd_counter >> 16) & 0xFFFF;
    case ULPD_COUNTER_LO:
        return s->ulpd_counter & 0xFFFF;
    case ULPD_GAUGING_CTRL:
        return 1;
    case ULPD_GSM_TIMER:
        return s->fn & 0xFFFF;
    default:
        return (off / 2 < CALYPSO_ULPD_SIZE / 2) ? s->ulpd_regs[off / 2] : 0;
    }
}

static void ulpd_write(void *o, hwaddr off, uint64_t v, unsigned sz)
{
    CalypsoTRX *s = o;
    if (off >= 0x20 && off <= 0x40) {
        return;
    }
    if (off / 2 < CALYPSO_ULPD_SIZE / 2) {
        s->ulpd_regs[off / 2] = v;
    }
}

static const MemoryRegionOps ulpd_ops = {
    .read = ulpd_read,
    .write = ulpd_write,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .valid = { .min_access_size = 1, .max_access_size = 2 },
    .impl = { .min_access_size = 1, .max_access_size = 2 },
};

static uint64_t sim_read(void *o, hwaddr off, unsigned sz)
{
    CalypsoTRX *s = o;
    return calypso_sim_reg_read(s->sim, off);
}

static void sim_write(void *o, hwaddr off, uint64_t v, unsigned sz)
{
    CalypsoTRX *s = o;
    calypso_sim_reg_write(s->sim, off, (uint16_t)v);
}

static const MemoryRegionOps sim_ops = {
    .read = sim_read,
    .write = sim_write,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .valid = { .min_access_size = 1, .max_access_size = 4 },
    .impl = { .min_access_size = 1, .max_access_size = 4 },
};

static void cpu_idle_park(void)
{
    CPUState *cs = first_cpu;
    if (!cs) {
        return;
    }
    uint64_t pc = (cs->cc && cs->cc->get_pc) ? cs->cc->get_pc(cs) : 0;
    if (pc < IDLE_PC_LO || pc >= IDLE_PC_HI) {
        return;
    }
    /* NO PARKING IN INTERRUPT CONTEXT. The IDLE_PC range (0x823000-0x826000)
     * contains l1a_l23_handler, mframe_schedule and the puts() of l1_sync:
     * code that also runs inside the frame IRQ and UART handlers (IRQ mode,
     * I bit set). With -d exec,nochain the ARM is 5 times slower (2 ms of CPU
     * per frame instead of 0.4) and is often there, in the middle of an ISR,
     * when the 1 ms pulse falls; halting it with IRQs masked would leave it
     * asleep until the next IRQ, the frame EOI would never come and the bridge
     * would wait up to 2.4 s. The real idle (main's while(1)) runs in SYS/SVC
     * mode with IRQs enabled: only that one is parked. */
    {
        CPUARMState *env = &ARM_CPU(cs)->env;
        uint32_t cpsr = cpsr_read(env);
        uint32_t mode = cpsr & CPSR_M;
        if ((cpsr & CPSR_I) || mode == ARM_CPU_MODE_IRQ || mode == ARM_CPU_MODE_FIQ ||
            mode == ARM_CPU_MODE_ABT || mode == ARM_CPU_MODE_UND) {
            return;
        }
    }
    cs->halted = 1;
    cpu_exit(cs);
}

static void calypso_trx_vm_state(void *opaque, bool running, RunState state)
{
    if (!running) {
        qemu_log_flush();
    }
}

static void frame_irq_lower(void *o)
{
    CalypsoTRX *s = o;
    qemu_irq_lower(s->irqs[CALYPSO_IRQ_TPU_FRAME]);
    calypso_l1_do_frame_tick();
    cpu_idle_park();
}

static void *wall_clock_loop(void *arg)
{
    struct timespec next;
    clock_gettime(CLOCK_MONOTONIC, &next);
    while (g_wall_running) {
        next.tv_nsec += GSM_TDMA_NS;
        while (next.tv_nsec >= 1000000000L) {
            next.tv_nsec -= 1000000000L;
            next.tv_sec += 1;
        }
        int rc = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL);
        if (rc != 0 && rc != EINTR) {
            break;
        }
        __atomic_add_fetch(&g_wall_fn, 1, __ATOMIC_RELEASE);
    }
    return NULL;
}

/* CALYPSO_PONT_ARM_LOCK=1: see tdma_tick (wait for the ARM without forced GO). */
static bool pont_arm_lock(void)
{
    static int v = -1;
    if (v < 0) {
        const char *e = getenv("CALYPSO_PONT_ARM_LOCK");
        v = (e && *e == '1') ? 1 : 0;
    }
    return v == 1;
}

/* TICK.b: d_dsp_page plus the one-shot DSP frame interrupt bit, consumed here. */
static uint32_t pont_tick_b(CalypsoTRX *s)
{
    uint32_t b = s->dsp_page;
    bool arme = (s->tpu_regs[TPU_CTRL / 2] & TPU_CTRL_DSP_EN) &&
                !(s->tpu_regs[TPU_INT_CTRL / 2] & ICTRL_DSP_FRAME);
    if (arme) {
        b |= CALYPSO_PONT_TICK_IRQ_TRAME;
        s->tpu_regs[TPU_CTRL / 2] &= (uint16_t)~TPU_CTRL_DSP_EN;
    }
    return b;
}

/* One TICK/DONE exchange with the external DSP: collect the previous frame's
 * DONE without blocking, then send this frame's TICK if the DSP is idle. */
static void pont_echange(CalypsoTRX *s)
{
    if (s->pont_fd < 0) {
        return;
    }
    bool libre = !s->pont_pending;
    if (s->pont_pending) {
        CalypsoPontMsg m;
        if (pont_recv(s, &m, 0) && m.type == PONT_DONE) {
            s->pont_pending = false;
            libre = true;
            s->pont_frames++;
            if (m.a & PONT_DONE_API_IRQ) {
                s->pont_api_irqs++;
                qemu_irq_raise(s->irqs[CALYPSO_IRQ_API]);
            }
        } else if (s->pont_fd >= 0) {
            if (++s->pont_timeouts == 1 || (s->pont_timeouts % 2170) == 0) {
                fprintf(stderr, "[trx] pont DSP : DSP en retard, tick saute "
                        "(fn=%u, %u sauts, %" PRIu64 " trames jouees)\n",
                        s->fn, s->pont_timeouts, s->pont_frames);
            }
        }
    }
    /* [2026-09-20] DSP FRAME INTERRUPT = TPU_CTRL_DSP_EN, ONE-SHOT. The
     * firmware raises it in dsp_end_scenario() (tpu_dsp_frameirq_enable(),
     * set again on EVERY scenario) and never lowers it; the ROM never clears
     * d_task_md nor d_dsp_page, and l1_sync() only flips the write page on
     * frames carrying a DSP item. So a frame interrupt on every tick made the
     * ROM re-read the SAME page for 10+ frames: the FB task restarted at each
     * FCCH and the SB job never got the channel (0xaba4 dispatched, 0xb219
     * never reached, no 764-byte window). Bit 16 of TICK.b says whether the
     * ARM armed the interrupt since the last tick; DSP_EN is consumed here.
     * ICTRL_DSP_FRAME is active-low (tpu_frame_irq_en). */
    uint32_t b = s->dsp_page;
    if (libre) {
        b = pont_tick_b(s);
        if (s->deux_phases) {
            b |= CALYPSO_PONT_TICK_DEUX_PHASES;
        }
    }
    if (libre && s->dcch_a_dire) {
        s->dcch_a_dire = false;
        pont_send(s, PONT_DCCH, s->dcch_tn, (uint32_t)s->dcch_genre, s->dcch_ss);
    }
    if (libre && pont_send(s, PONT_TICK, s->fn, b, s->tpu_regs[TPU_OFFSET / 2])) {
        s->pont_pending = true;
    }
}

/* One DSP frame while the ARM waits for LEAD_READY (api_read): like pont_echange,
 * but WAITING for the DONE (20 ms at most), otherwise the ARM loop comes back
 * before the DSP has finished its frame and nothing advances. */
static void pont_boot_pas(CalypsoTRX *s)
{
    if (s->pont_fd < 0) {
        return;
    }
    if (!s->pont_pending) {
        pont_echange(s);                 /* sends the TICK */
    }
    if (s->pont_pending) {
        CalypsoPontMsg m;
        if (pont_recv(s, &m, 20) && m.type == PONT_DONE) {
            s->pont_pending = false;
            s->pont_frames++;
            if (m.a & PONT_DONE_API_IRQ) {
                s->pont_api_irqs++;
                qemu_irq_raise(s->irqs[CALYPSO_IRQ_API]);
            }
        }
    }
}

static bool arm_first(void)
{
    static int v = -1;
    if (v < 0) {
        const char *ls = getenv("CALYPSO_PONT_LOCKSTEP");
        const char *e = getenv("CALYPSO_PONT_ARM_FIRST");
        v = (ls && *ls == '1' && !(e && *e == '0')) ? 1 : 0;
    }
    return v;
}

/* Before the firmware enables the TPU (which starts tdma_tick) the external
 * DSP must already run: dsp_power_on() waits for its bootloader. This timer
 * does ONLY the DSP exchange - no TPU-frame IRQ, no sequencer, since the ARM
 * has not installed its vectors yet. It stops by itself once the real tick
 * takes over. */
static void pont_boot_tick(void *opaque)
{
    CalypsoTRX *s = opaque;
    if (s->tdma_running) {
        fprintf(stderr, "[trx] pont DSP : le TDMA du firmware prend le relais du "
                "timer de boot (%" PRIu64 " trames de boot)\n", s->pont_frames);
        return;
    }
    if (runstate_is_running()) {
        pont_echange(s);
    }
    timer_mod_ns(s->pont_boot_timer, qemu_clock_get_ns(QEMU_CLOCK_REALTIME) + GSM_TDMA_NS);
}

static void tdma_pacer(CalypsoTRX *s);

static void tdma_tick(void *opaque)
{
    CalypsoTRX *s = opaque;
    if (!runstate_is_running()) {
        timer_mod_ns(s->tdma_timer, qemu_clock_get_ns(QEMU_CLOCK_REALTIME) + GSM_TDMA_NS);
        return;
    }
    /* [2026-09-21] ARM FIRST, phase 1: the frame IRQ of frame fn went to the
     * ARM; hand the frame to the DSP once l1_sync(fn) is over. Measured before
     * this: the DSP wrote the R page of burst N while the ARM was still
     * reading burst N-2 from it (prim_rx_nb.c "BURST ID 2!=0", "EMPTY"),
     * with a lag that drifted with the ARM's load. On silicon the ARM reads at
     * the frame start and the DSP writes after the burst, later in the frame. */
    if (s->tick_phase == 1) {
        if (!s->phase_a_recue) {
            CalypsoPontMsg m;
            if (pont_recv(s, &m, 0) && m.type == PONT_DONE) {
                if (m.a & PONT_DONE_PHASE_A) {
                    s->phase_a_recue = true;
                } else {
                    /* single-phase DSP: this is the final DONE */
                    s->pont_pending = false; s->pont_frames++;
                    s->phase_a_recue = true; s->go_inutile = true;
                    if (m.a & PONT_DONE_API_IRQ) { s->pont_api_irqs++; qemu_irq_raise(s->irqs[CALYPSO_IRQ_API]); }
                }
            }
            if (s->phase_a_recue) {
                /* the ROM's ISR is over (page read, R page header written):
                 * now the ARM's frame, l1_sync(fn) */
                if (s->eoi_cible < calypso_inth_frame_eoi()) s->eoi_cible = calypso_inth_frame_eoi();
                s->eoi_cible += 1;
                calypso_timer_lost_frame_tick(s->fn);
                qemu_irq_raise(s->irqs[CALYPSO_IRQ_TPU_FRAME]);
                timer_mod_ns(s->frame_irq_timer,
                             qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + FRAME_IRQ_PULSE_NS);
            }
        }
        bool fini = s->phase_a_recue &&
                    (calypso_inth_frame_eoi() >= s->eoi_cible ||
                     calypso_inth_irq_masked(CALYPSO_IRQ_TPU_FRAME));
        /* DO NOT FORCE THE GO WHILE THE ARM HAS NOT SERVED THE FRAME IRQ.
         * Forcing the GO after 256 waits (74 ms) and realigning the EOI target
         * would make the ROM read a stale W page (a_a5fn of the previous frame)
         * and, with the frame IRQ lost (1 ms pulse, ARM busy with the UART), the
         * firmware's frame counter would slip by one for the rest of the channel.
         * The IRQ is latched in the INTH (calypso_inth.c); it is awaited up to
         * PONT_ATTENTES_LONG (~2.4 s) and long waits are traced. The lost wall
         * time costs BTS frames arriving too late at the BSP (radio losses),
         * not a TDMA slip. */
        if (!fini && s->eoi_attentes + 1 >= (unsigned)PONT_ATTENTES_MAX &&
            s->eoi_attentes + 1 < (unsigned)PONT_ATTENTES_MAX * 32 && s->phase_a_recue) {
            if (s->eoi_attentes + 1 == (unsigned)PONT_ATTENTES_MAX && ++s->eoi_longues <= 20) {
                fprintf(stderr, "[trx] pont DSP : l1_sync de l'ARM toujours pas finie apres 256 attentes "
                        "(fn=%u, IT trame %s, EOI %" PRIu64 "/%" PRIu64 ") : on attend au lieu de forcer le GO\n",
                        s->fn, calypso_inth_irq_pending(CALYPSO_IRQ_TPU_FRAME) ? "EN ATTENTE" : "servie",
                        calypso_inth_frame_eoi(), s->eoi_cible);
            }
            s->eoi_attentes++;
            if (g_uart_modem) {          /* the ARM may be waiting on the serial line: serve it */
                calypso_uart_poll_backend(g_uart_modem);
                calypso_uart_kick_rx(g_uart_modem);
            }
            if (g_uart_irda) {
                calypso_uart_poll_backend(g_uart_irda);
                calypso_uart_kick_rx(g_uart_irda);
            }
            timer_mod_ns(s->tdma_timer, qemu_clock_get_ns(QEMU_CLOCK_REALTIME) + PONT_RETRY_NS);
            return;
        }
        if (fini && s->eoi_attentes >= (unsigned)PONT_ATTENTES_MAX && s->eoi_longues <= 20) {
            fprintf(stderr, "[trx] pont DSP : l1_sync finie apres %u attentes (fn=%u)\n", s->eoi_attentes, s->fn);
        }
        if (!fini && ++s->eoi_attentes < (unsigned)PONT_ATTENTES_MAX) {
            if (g_uart_modem) {
                calypso_uart_poll_backend(g_uart_modem);
                calypso_uart_kick_rx(g_uart_modem);
            }
            if (g_uart_irda) {
                calypso_uart_poll_backend(g_uart_irda);
                calypso_uart_kick_rx(g_uart_irda);
            }
            timer_mod_ns(s->tdma_timer, qemu_clock_get_ns(QEMU_CLOCK_REALTIME) + PONT_RETRY_NS);
            return;
        }
        /* CALYPSO_PONT_ARM_LOCK=1 (Magnetite): do not force the GO while the ARM has not
         * finished l1_sync. Forcing makes frames arrive faster than L1 processes them: the
         * L1 queue fills up (9/9), "IRQ write attempt to L1 queue failed" -> os_SystemError. */
        /* Bound: at startup (fn=3) the ARM takes the frame IRQ while l1_sync is not running
         * yet (EOI 2/3); without a bound the bridge would wait for the ARM and the ARM for
         * the DSP, forever. Beyond 64 x PONT_ATTENTES_MAX (~5 s) the GO goes out anyway. */
        /* No wait either while the ARM polls DOWNLOAD_STATUS (DSP restart by the firmware,
         * dsp_power_on): l1_sync is not running, waiting would only freeze the instance. */
        bool arm_relance_dsp = s->bl_lu_fn && (uint32_t)(s->fn + 1 - s->bl_lu_fn) <= 4;
        if (!fini && pont_arm_lock() && !arm_relance_dsp && s->eoi_attentes < (unsigned)PONT_ATTENTES_MAX * 64) {
            if (++s->eoi_attentes == (unsigned)PONT_ATTENTES_MAX * 64) {
                fprintf(stderr, "[trx] pont DSP : CALYPSO_PONT_ARM_LOCK, l1_sync toujours pas finie "
                        "(fn=%u, EOI %" PRIu64 "/%" PRIu64 ") : GO force\n",
                        s->fn, calypso_inth_frame_eoi(), s->eoi_cible);
            }
            timer_mod_ns(s->tdma_timer, qemu_clock_get_ns(QEMU_CLOCK_REALTIME) + PONT_RETRY_NS);
            return;
        }
        if (!fini) {
            if (!s->phase_a_recue) {
                /* no ISR answer: give the ARM its frame anyway, do not stall */
                calypso_timer_lost_frame_tick(s->fn);
                qemu_irq_raise(s->irqs[CALYPSO_IRQ_TPU_FRAME]);
                timer_mod_ns(s->frame_irq_timer,
                             qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + FRAME_IRQ_PULSE_NS);
            }
            s->eoi_cible = calypso_inth_frame_eoi();   /* resync after a lost frame IRQ */
            if (++s->eoi_timeouts <= 20 || (s->eoi_timeouts % 2170) == 0) {
                fprintf(stderr, "[trx] pont DSP : phase A %s, l1_sync de l'ARM %s apres 8192 attentes "
                        "(fn=%u, %u fois), GO envoye quand meme\n",
                        s->phase_a_recue ? "recue" : "PAS recue",
                        calypso_inth_frame_eoi() >= s->eoi_cible ? "finie" : "PAS finie",
                        s->fn, s->eoi_timeouts);
            }
        }
        s->tick_phase = 0;
        if (!s->go_inutile) {
            pont_send(s, PONT_GO, s->fn, 0, 0);
        }
        tdma_pacer(s);
        return;
    }
    /* [2026-09-17] DSP LOCK-STEP (CALYPSO_PONT_LOCKSTEP=1): advance the frame
     * (fn + TPU-frame IT to the firmware) ONLY once the external DSP has
     * finished the previous one. Otherwise QEMU produces frames faster than
     * the DSP consumes them, ticks are skipped and the firmware frame counter
     * runs away (fn_offset ~600k): the three clocks (BTS/QEMU/firmware)
     * diverge and acquisition never locks. In lock-step firmware and DSP stay
     * on the SAME frame: coherent clocks, stable TOA, native lock possible. */
    if (s->pont && s->pont_fd >= 0 && s->pont_pending) {
        static int ls = -1;
        if (ls < 0) { const char *e = getenv("CALYPSO_PONT_LOCKSTEP"); ls = (e && *e=='1') ? 1 : 0; }
        if (ls) {
            CalypsoPontMsg m;
            if (pont_recv(s, &m, 0) && m.type == PONT_DONE) {
                s->pont_pending = false; s->pont_frames++;
                if (m.a & PONT_DONE_API_IRQ) { s->pont_api_irqs++; qemu_irq_raise(s->irqs[CALYPSO_IRQ_API]); }
            } else {
                /* DSP not ready yet: do not advance, retry soon.
                 * [2026-09-20] The UARTs are pumped by this tick (below) and
                 * do not depend on the DSP: keep serving them here, or the
                 * serial link only moves at the DSP's pace and osmocon's
                 * romload, which times out per block, stalls at 38-55 %. */
                if (g_uart_modem) {
                    calypso_uart_poll_backend(g_uart_modem);
                    calypso_uart_kick_rx(g_uart_modem);
                }
                if (g_uart_irda) {
                    calypso_uart_poll_backend(g_uart_irda);
                    calypso_uart_kick_rx(g_uart_irda);
                }
                timer_mod_ns(s->tdma_timer, qemu_clock_get_ns(QEMU_CLOCK_REALTIME) + PONT_RETRY_NS);
                return;
            }
        }
    }
    {
        static int ls2 = -1;
        if (ls2 < 0) { const char *e = getenv("CALYPSO_PONT_LOCKSTEP"); ls2 = (e && *e=='1') ? 1 : 0; }
        if (s->pont && s->pont_fd >= 0 && ls2) {
            s->fn = (s->fn + 1) % GSM_HYPERFRAME;   /* DSP pace, not the wall clock */
        } else {
            uint32_t wfn = __atomic_load_n(&g_wall_fn, __ATOMIC_ACQUIRE);
            s->fn = (wfn ? wfn : s->fn + 1) % GSM_HYPERFRAME;
        }
    }

    /* [2026-09-19] WHO WRITES a_sch? Every writer on the DSP side has been
     * eliminated by measurement: the C54x core (instruction probe: 0 stores),
     * its internal DMA (probe: 0 transfers), the RX_FBFLAGS/POKE crutches
     * (witnesses silent), the gr-gsm L1 (disabled under CALYPSO_DSP_EXTERN),
     * PONT_CAN_SB (off). Only the ARM firmware is left, writing through the
     * shared mapping, which no DSP-side probe can see. a_sch[0] carries plain
     * numbers (0x1111, 0x1388) where only B_BLUD and B_SCH_CRC mean anything,
     * so name the ARM PC that puts them there. a_sch = R_PAGE + 0x1E (bytes),
     * i.e. api_ram words 0x37 (page 0) and 0x4b (page 1). */
    {
        static uint16_t prev[2]; static int first = 1; static unsigned n;
        if (s->api_ram) {
            uint16_t v0 = s->api_ram[0x37], v1 = s->api_ram[0x4b];
            if (!first && n < 40 && (v0 != prev[0] || v1 != prev[1])) {
                uint32_t pc = 0;
                if (first_cpu) {
                    ARMCPU *ac = ARM_CPU(first_cpu);
                    pc = ac->env.regs[15];
                }
                fprintf(stderr, "[trx] A_SCH fn=%u page0 %04x->%04x page1 %04x->%04x "
                        "PC_ARM=0x%08x\n", s->fn, prev[0], v0, prev[1], v1, pc);
                n++;
            }
            prev[0] = v0; prev[1] = v1; first = 0;
        }
    }

    calypso_tpu_sequencer_tick(s->fn);
    if (g_uart_modem) {
        calypso_uart_poll_backend(g_uart_modem);
        calypso_uart_kick_rx(g_uart_modem);
    }
    if (g_uart_irda) {
        calypso_uart_poll_backend(g_uart_irda);
        calypso_uart_kick_rx(g_uart_irda);
    }

    if (s->pont) {
        /* The C54x consumes d_task_ra / d_task_u itself (the firmware's
         * l1s_compl() re-runs dsp_api_memset() on the write page): do NOT
         * clear them here. */
        /* [2026-09-17] PIPELINE, NO WAIT. Waiting for DONE here held the
         * global lock for a whole DSP frame, ~26 ms for 64000 insn, and the
         * ARM vCPU, which needs that lock for every MMIO access, stopped
         * advancing (stuck in hwtimer_config). The DONE of frame N is now
         * collected at tick N+1 without blocking; if it is missing the DSP is
         * late and this tick is skipped for it. Cost: the API IRQ arrives one
         * frame later than with the internal DSP. */
        /* ARM FIRST: the TICK goes out now (phase A, the ROM's frame ISR),
         * the burst only after l1_sync(fn), with PONT_GO (phase B). */
        if (arm_first()) {
            s->deux_phases = true;
        }
        pont_echange(s);
        s->deux_phases = false;
    } else {
        *api_wp(s->dsp_page, WP_D_TASK_RA) = 0;
        *api_wp(s->dsp_page, WP_D_TASK_U) = 0;
    }

    if (s->pont && s->pont_fd >= 0 && arm_first() && s->pont_pending) {
        /* Two phases: the ARM frame IRQ waits for DONE|PHASE_A (the ROM's ISR
         * has read its page), then l1_sync(fn), then PONT_GO. The EOI target
         * is cumulative: one end of service per frame IRQ raised, so a lagging
         * l1_sync(N-1) cannot pass for l1_sync(N). Resynchronised on timeout. */
        s->eoi_attentes = 0;
        s->phase_a_recue = false;
        s->go_inutile = false;
        s->tick_phase = 1;
        timer_mod_ns(s->tdma_timer, qemu_clock_get_ns(QEMU_CLOCK_REALTIME) + PONT_RETRY_NS);
        return;
    }
    /* Without an external DSP (Magnetite: no bridge), nobody clears
     * TPU_CTRL_DSP_EN: the l1_sync HISR would see "DSP IRQ pending" every frame
     * and log "MCU CPU overload" in a loop. Here the DSP is considered done
     * within the frame: the bit is acknowledged before the next frame. */
    if (s->pont_fd < 0) {
        s->tpu_regs[TPU_CTRL / 2] &= (uint16_t)~TPU_CTRL_DSP_EN;
    }
    calypso_timer_lost_frame_tick(s->fn);
    qemu_irq_raise(s->irqs[CALYPSO_IRQ_TPU_FRAME]);
    timer_mod_ns(s->frame_irq_timer,
                 qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + FRAME_IRQ_PULSE_NS);
    tdma_pacer(s);
}

/* Next frame at the TDMA pace of the wall clock (or as soon as we are late). */
static void tdma_pacer(CalypsoTRX *s)
{
    static int64_t target;
    int64_t now = qemu_clock_get_ns(QEMU_CLOCK_REALTIME);
    if (target == 0) {
        target = now;
    }
    target += GSM_TDMA_NS;
    /* CATCH UP INSTEAD OF SKIPPING A SLOT. Aligning the next TICK on the
     * wall-clock grid (`while (target <= now) target += GSM_TDMA_NS`) would
     * lose a whole frame for every 0.1 ms of lateness; in TCH (DSP ~4.8 ms on
     * some frames) one frame in four would slip (171 frames/s instead of
     * 216.7, speech at 40 frames/s for an ALSA at 50). Instead the next frame
     * starts right away and the lateness is caught up, within a limit of
     * CALYPSO_PACER_RATTRAPAGE frames of cumulated lateness (20 by default);
     * beyond that the target is realigned on now. 0 = strict grid. */
    static int rattrapage = -1;
    if (rattrapage < 0) {
        const char *e = getenv("CALYPSO_PACER_RATTRAPAGE");
        rattrapage = (e && *e) ? atoi(e) : 20;
    }
    if (rattrapage == 0) {
        while (target <= now) {
            target += GSM_TDMA_NS;
        }
    } else if (target < now - (int64_t)rattrapage * GSM_TDMA_NS) {
        target = now;
    }
    timer_mod_ns(s->tdma_timer, target);
}

static void tdma_start(CalypsoTRX *s)
{
    s->tdma_running = true;
    s->fn = 0;
    timer_mod_ns(s->tdma_timer, qemu_clock_get_ns(QEMU_CLOCK_REALTIME) + GSM_TDMA_NS);
}

static void cpu_kick(void *o)
{
    CalypsoTRX *s = o;
    if (first_cpu) {
        cpu_exit(first_cpu);
    }
    qemu_notify_event();
    timer_mod_ns(s->kick_timer, qemu_clock_get_ns(QEMU_CLOCK_REALTIME) + CPU_KICK_NS);
}

static void map_io(MemoryRegion *sysmem, MemoryRegion *mr, const MemoryRegionOps *ops,
                   void *opaque, const char *name, hwaddr base, uint64_t size)
{
    memory_region_init_io(mr, NULL, ops, opaque, name, size);
    memory_region_add_subregion(sysmem, base, mr);
}

void calypso_trx_init(MemoryRegion *sysmem, qemu_irq *irqs)
{
    CalypsoTRX *s = g_new0(CalypsoTRX, 1);
    g_trx = s;
    s->irqs = irqs;
    s->pont_fd = -1;

    const char *pont_spec = getenv("CALYPSO_DSP_EXTERN");
    if (pont_spec && *pont_spec) {
        pont_connect(s, pont_spec);
    } else {
        s->api_ram = g_new0(uint16_t, CALYPSO_API_WORDS);
    }

    map_io(sysmem, &s->api_iomem, &api_ops, s, "calypso.dsp_api", CALYPSO_API_BASE,
           CALYPSO_API_SIZE);
    if (!s->pont) {
        /* Initial state of the EMULATED bootloader. With an external DSP the
         * real ROM bootloader (parked at 0xb41c by c54x_reset) has posted IDLE
         * in that cell and the real L1 writes its version: touch nothing. */
        s->api_ram[API_BL_STATUS / 2] = BL_STATUS_READY;
        s->api_ram[API_VERSION / 2] = API_VERSION_VALUE;
    }
    s->bl_booted = true;

    map_io(sysmem, &s->tpu_iomem, &tpu_ops, s, "calypso.tpu", CALYPSO_TPU_BASE,
           CALYPSO_TPU_SIZE);
    map_io(sysmem, &s->tpu_ram_iomem, &tpu_ram_ops, s, "calypso.tpu_ram",
           CALYPSO_TPU_RAM_BASE, CALYPSO_TPU_RAM_SIZE);
    map_io(sysmem, &s->tsp_iomem, &tsp_ops, s, "calypso.tsp", CALYPSO_TSP_BASE,
           CALYPSO_TSP_SIZE);
    map_io(sysmem, &s->ulpd_iomem, &ulpd_ops, s, "calypso.ulpd", CALYPSO_ULPD_BASE,
           CALYPSO_ULPD_SIZE);
    s->sim = calypso_sim_new(s->irqs[CALYPSO_IRQ_SIM]);
    map_io(sysmem, &s->sim_iomem, &sim_ops, s, "calypso.sim", CALYPSO_SIM_BASE,
           CALYPSO_SIM_SIZE);

    s->tdma_timer = timer_new_ns(QEMU_CLOCK_REALTIME, tdma_tick, s);
    /* -D is buffered (QEMU_LOG_BUFFERED): on every VM stop (Ctrl-C from the
     * gdb console, monitor "stop") the buffer is flushed so that the trace read
     * at that moment goes up to the last executed block. */
    qemu_add_vm_change_state_handler(calypso_trx_vm_state, s);
    s->frame_irq_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, frame_irq_lower, s);
    s->kick_timer = timer_new_ns(QEMU_CLOCK_REALTIME, cpu_kick, s);
    timer_mod_ns(s->kick_timer, qemu_clock_get_ns(QEMU_CLOCK_REALTIME) + CPU_KICK_NS);

    g_wall_running = true;
    pthread_create(&g_wall_thread, NULL, wall_clock_loop, NULL);
    pthread_setname_np(g_wall_thread, "cal-tdma-clock");

    if (s->pont) {
        /* The external DSP only runs on TICKs, but the firmware's
         * dsp_power_on() waits for the bootloader (IDLE) BEFORE enabling the
         * TPU, which is what normally starts the tick. Clocking it from init
         * breaks that deadlock, where the ARM waits for the DSP waiting for
         * the ARM. The later tdma_start() from the TPU only resets fn to 0. */
        s->pont_boot_timer = timer_new_ns(QEMU_CLOCK_REALTIME, pont_boot_tick, s);
        timer_mod_ns(s->pont_boot_timer, qemu_clock_get_ns(QEMU_CLOCK_REALTIME) + GSM_TDMA_NS);
        fprintf(stderr, "[trx] pont DSP : timer de boot lance (echange DSP seul, "
                "sans IRQ TPU, jusqu'a ce que le firmware active le TDMA)\n");
    }
}

4.34 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_tsp.c

4005 octets, 107 lignes → 105 lignes (1 groupes compactés)

/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "qemu/osdep.h"
#include "hw/arm/calypso/calypso_trf6151.h"
#include "hw/arm/calypso/calypso_tsp.h"
#include "hw/arm/calypso/calypso_rf_arfcn.h"
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>


/* Fake air: every synthesizer programming is sent to c54x_exe
 * (calypso_rf_arfcn.h). Opened on the first write; without /dev/shm, nothing. */
static void rf_arfcn_publier(uint32_t fn)
{
    static CalypsoRfArfcnShm *m;
    static int essaye;
    if (!m && !essaye) {
        essaye = 1;
        int fd = open(CALYPSO_RF_ARFCN_SHM, O_RDWR | O_CREAT, 0644);
        if (fd >= 0) {
            if (ftruncate(fd, sizeof *m) == 0) {
                void *p = mmap(NULL, sizeof *m, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
                if (p != MAP_FAILED) { m = p; m->magic = CALYPSO_RF_ARFCN_MAGIC; m->widx = 0; }
            }
            close(fd);
        }
    }
    if (!m) {
        return;
    }
    CalypsoRfProg *e = &m->e[m->widx % CALYPSO_RF_ARFCN_N];
    e->fn = fn;
    e->qbit = (int16_t)calypso_tpu_qbit();
    e->arfcn = (int16_t)calypso_trf6151_arfcn();
    e->gain_db = (int16_t)calypso_trf6151_total_gain_db();
    e->reserve = (int16_t)calypso_trf6151_rf_flags();   /* see calypso_rf_arfcn.h */
    __atomic_store_n(&m->widx, m->widx + 1, __ATOMIC_RELEASE);
}

static struct {
    uint8_t tx[4];
    uint8_t ctrl1;
} tsp;

bool calypso_tsp_owns_addr(uint8_t addr)
{
    switch (addr) {
    case TPUI_TSP_CTRL1: case TPUI_TSP_CTRL2:
    case TPUI_TX_1: case TPUI_TX_2: case TPUI_TX_3: case TPUI_TX_4:
    case TPUI_TSP_ACT_L: case TPUI_TSP_ACT_U:
    case TPUI_TSP_SET1: case TPUI_TSP_SET2: case TPUI_TSP_SET3:
        return true;
    default:
        return false;
    }
}

void calypso_tsp_move(uint8_t addr, uint8_t data, uint32_t fn)
{
    switch (addr) {
    case TPUI_TX_⟨1⟩: tsp.tx[⟨2⟩] = data; break;  ×4
    ⟨⟩ = (1,0) (2,1) (3,2) (4,3)
    case TPUI_TSP_CTRL1:
        tsp.ctrl1 = data;
        break;
    case TPUI_TSP_CTRL2:
        if (data & TPUI_CTRL2_WR) {
            uint8_t dev_idx = (tsp.ctrl1 >> 5) & 0x07;
            uint8_t bitlen = (tsp.ctrl1 & 0x1F) + 1;
            uint32_t dout;
            if (bitlen <= 8) {
                dout = tsp.tx[0];
            } else if (bitlen <= 16) {
                dout = ((uint32_t)tsp.tx[0] << 8) | tsp.tx[1];
            } else if (bitlen <= 24) {
                dout = ((uint32_t)tsp.tx[0] << 16) | ((uint32_t)tsp.tx[1] << 8) | tsp.tx[2];
            } else {
                dout = ((uint32_t)tsp.tx[0] << 24) | ((uint32_t)tsp.tx[1] << 16) |
                       ((uint32_t)tsp.tx[2] << 8) | tsp.tx[3];
            }
            calypso_trf6151_tsp_write(dev_idx, dout);
            /* Live trace (`touch /dev/shm/tsp_trace`, 6000 lines): every TSP write. In
             * Magnetite idle mode, no programming towards the BA-list neighbour (277 on
             * the TI L1 side) is seen by the TRF6151 decoding: its measurement windows
             * are classified "cell" by the fake air. */
            {
                static unsigned n_appels, n_lignes; static int on;
                if (n_appels++ % 256 == 0) on = (access("/dev/shm/tsp_trace", F_OK) == 0);
                if (on && n_lignes < 6000) {
                    n_lignes++;
                    fprintf(stderr, "[tsp] fn=%u qbit=%d dev=%u bits=%u dout=0x%08x reg=%u -> arfcn=%d gain=%d\n",
                            fn, calypso_tpu_qbit(), dev_idx, bitlen, dout, dout & 7,
                            calypso_trf6151_arfcn(), calypso_trf6151_total_gain_db());
                }
            }
            /* TRF6151 RX (gain), PLL or PWR (band), TSPEN1 or 2 */
            if ((dev_idx == 1 || dev_idx == 2) && ((dout & 7) == 0 || (dout & 7) == 1 || (dout & 7) == 2)) {
                rf_arfcn_publier(fn);
            }
        }
        break;
    default:
        break;
    }
}

4.35 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_uwire.c

10212 octets, 328 lignes → 325 lignes (3 groupes compactés)

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * calypso_uwire.c - the Calypso uWire controller (0xFFFE4000) and, behind
 * its chip select 0, the 96x64 colour LCD of the Motorola C139/C140.
 *
 * Written for FreeCalypso Magnetite (c139 target, hybrid-ui-vo config):
 * its r2d driver (src/cs/drivers/drv_app/r2d/lcds/c139/) talks to the LCD
 * in 9-bit words on uWire CS0, bit 8 = data (1) or command (0):
 *   - command 0x10/0x11 = window start (x, y), 0x12/0x13 = window end,
 *     0x14/0x15 = cursor; each command is followed by ONE parameter word
 *     (bit 8 set). 0x3F, 0x20, 0x31 (init) are accepted without effect;
 *   - data outside a command: two bytes per pixel (high then low), RGB565,
 *     the cursor advances in x, then returns to xstart and moves to the next
 *     row at the end of the window.
 * The firmware only displays a centred 84x48 UI (TI "C-Sample"): border
 * 0x5555, white 0xFFFF, black 0x0000.
 *
 * uWire registers (16 bits): 0x00 TDR/RDR (left-aligned data), 0x02 CSR
 * (bit 13 START, bit 12 CS_CMD, bits 10-11 CS index, bits 5-9 number of
 * bits written, bit 14 CSRB busy: always 0 here), 0x04/0x06/0x08 SR1-3.
 *
 * Outputs, as in calypso_st7558.c: the QEMU console (-display sdl, x3) and
 * a shared image CALYPSO_LCD_SHM (default /dev/shm/calypso_lcd, "none"):
 * 32-byte header (magic "C139LCD", width, height, bytes per pixel = 2,
 * flags, counter) then 96 x 64 x 2 bytes RGB565 little-endian.
 * tools/osmo-c123-gtk.py recognises both magics.
 *
 * Enabling: property "c139-lcd" (calypso_soc.c sets it when
 * CALYPSO_LCD=c139); without it the controller silently swallows the words.
 */

#include "qemu/osdep.h"
#include "qemu/timer.h"
#include "hw/sysbus.h"
#include "hw/qdev-properties.h"
#include "ui/console.h"
#include "qom/object.h"
#include <sys/stat.h>
#include <fcntl.h>

#define TYPE_CALYPSO_UWIRE "calypso-uwire"
OBJECT_DECLARE_SIMPLE_TYPE(CalypsoUwireState, CALYPSO_UWIRE)

#define UW_TDR   0x00
#define UW_CSR   0x02
#define UW_SR⟨1⟩   0x⟨2⟩  ×3
    ⟨⟩ = (1,04) (2,06) (3,08)

#define CSR_START   (1 << 13)
#define CSR_CS_CMD  (1 << 12)
#define CSR_CSRB    (1 << 14)

#define LCD_W     96
#define LCD_H     64
#define MAGNIFY   3
#define SHM_DEFAULT "/dev/shm/calypso_lcd"
#define SHM_HDR   32
#define FLUSH_MS  40

struct CalypsoUwireState {
    SysBusDevice parent_obj;
    MemoryRegion iomem;
    uint16_t tdr, csr, sr1, sr2, sr3;
    bool lcd_enabled;
    bool debug;

    /* C139 LCD */
    uint16_t ram[LCD_W * LCD_H];
    int xs, xe, ys, ye, cx, cy;
    int cmd;            /* command waiting for its parameter, -1 otherwise */
    int pix_hi;         /* high byte of the current pixel, -1 otherwise */
    bool dirty, redraw;
    QEMUTimer *flush;
    QemuConsole *con;
    int shm_fd;
    uint32_t seq;
    char *shm_path;
};

static void c139_export(CalypsoUwireState *s)
{
    uint8_t buf[SHM_HDR + LCD_W * LCD_H * 2];
    int i;

    if (s->shm_fd < 0) {
        return;
    }
    s->seq++;
    memset(buf, 0, SHM_HDR);
    memcpy(buf, "C139LCD", 8);
    buf[8] = LCD_W; buf[9] = 0;
    buf[10] = LCD_H; buf[11] = 0;
    buf[12] = 2; buf[13] = 0;               /* bytes per pixel */
    buf[14] = 0;
    buf[16] = s->seq & 0xff; buf[17] = (s->seq >> 8) & 0xff;
    buf[18] = (s->seq >> 16) & 0xff; buf[19] = (s->seq >> 24) & 0xff;
    for (i = 0; i < LCD_W * LCD_H; i++) {
        buf[SHM_HDR + 2 * i] = s->ram[i] & 0xff;
        buf[SHM_HDR + 2 * i + 1] = s->ram[i] >> 8;
    }
    if (pwrite(s->shm_fd, buf, sizeof(buf), 0) != (ssize_t)sizeof(buf)) {
        fprintf(stderr, "[c139lcd] ecriture de %s : %s\n", s->shm_path, strerror(errno));
    }
}

static void c139_flush_cb(void *opaque)
{
    CalypsoUwireState *s = opaque;

    if (s->dirty) {
        s->dirty = false;
        c139_export(s);
    }
    timer_mod(s->flush, qemu_clock_get_ms(QEMU_CLOCK_REALTIME) + FLUSH_MS);
}

static void c139_word(CalypsoUwireState *s, uint16_t w)
{
    uint8_t d = w & 0xff;

    if (!(w & 0x100)) {                     /* command */
        s->cmd = d;
        s->pix_hi = -1;
        return;
    }
    if (s->cmd >= 0) {                      /* command parameter */
        switch (s->cmd) {
        case 0x10: s->xs = d; break;
        case 0x11: s->ys = d; break;
        case 0x12: s->xe = d; break;
        case 0x13: s->ye = d; break;
        case 0x14: s->cx = d; break;
        case 0x15: s->cy = d; break;
        default: break;
        }
        if (s->debug) {
            fprintf(stderr, "[c139lcd] cmd 0x%02x <- 0x%02x\n", s->cmd, d);
        }
        s->cmd = -1;
        return;
    }
    if (s->pix_hi < 0) {                    /* high byte of the pixel */
        s->pix_hi = d;
        return;
    }
    if (s->cx < LCD_W && s->cy < LCD_H) {
        s->ram[s->cy * LCD_W + s->cx] = (s->pix_hi << 8) | d;
        s->dirty = s->redraw = true;
    }
    s->pix_hi = -1;
    s->cx++;
    if (s->cx > s->xe) {
        s->cx = s->xs;
        s->cy++;
        if (s->cy > s->ye) {
            s->cy = s->ys;
        }
    }
}

static uint64_t uwire_read(void *opaque, hwaddr offset, unsigned size)
{
    CalypsoUwireState *s = opaque;

    switch (offset) {
    case UW_TDR: return 0;                  /* RDR: nothing to read */
    case UW_CSR: return s->csr & ~(CSR_START | CSR_CSRB);
    case UW_SR⟨1⟩: return s->sr⟨2⟩;  ×3
    ⟨⟩ = (1,1) (2,2) (3,3)
    default: return 0;
    }
}

static void uwire_write(void *opaque, hwaddr offset, uint64_t value, unsigned size)
{
    CalypsoUwireState *s = opaque;
    uint16_t v = value;

    switch (offset) {
    case UW_TDR:
        s->tdr = v;
        break;
    case UW_CSR:
        s->csr = v;
        if (v & CSR_START) {
            int nbits = (v >> 5) & 0x1f;
            int cs = (v >> 10) & 3;
            uint16_t word;

            if (nbits == 0) {
                nbits = 16;
            }
            word = (s->tdr >> (16 - nbits)) & ((1u << nbits) - 1);
            if (cs == 0 && s->lcd_enabled) {
                c139_word(s, word);
            } else if (s->debug) {
                fprintf(stderr, "[uwire] CS%d <- 0x%03x (%d bits)\n", cs, word, nbits);
            }
            s->csr &= ~CSR_START;
        }
        break;
    case UW_SR⟨1⟩: s->sr⟨2⟩ = v; break;  ×3
    ⟨⟩ = (1,1) (2,2) (3,3)
    default: break;
    }
}

static const MemoryRegionOps uwire_ops = {
    .read = uwire_read,
    .write = uwire_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
    .impl = { .min_access_size = 2, .max_access_size = 2 },
};

static inline uint32_t rgb565_to_xrgb(uint16_t p)
{
    uint32_t r = (p >> 11) & 0x1f, g = (p >> 5) & 0x3f, b = p & 0x1f;
    return 0xff000000u | ((r << 3 | r >> 2) << 16) | ((g << 2 | g >> 4) << 8) | (b << 3 | b >> 2);
}

static void c139_update_display(void *opaque)
{
    CalypsoUwireState *s = opaque;
    DisplaySurface *surface = qemu_console_surface(s->con);
    int x, y, k, stride;
    uint8_t *base;

    if (!s->redraw || surface_bits_per_pixel(surface) != 32) {
        return;
    }
    base = surface_data(surface);
    stride = surface_stride(surface);
    for (y = 0; y < LCD_H; y++) {
        uint32_t *row = (uint32_t *)(base + (size_t)y * MAGNIFY * stride);
        for (x = 0; x < LCD_W; x++) {
            uint32_t c = rgb565_to_xrgb(s->ram[y * LCD_W + x]);
            for (k = 0; k < MAGNIFY; k++) {
                row[x * MAGNIFY + k] = c;
            }
        }
        for (k = 1; k < MAGNIFY; k++) {
            memcpy(base + ((size_t)y * MAGNIFY + k) * stride, row, LCD_W * MAGNIFY * 4);
        }
    }
    s->redraw = false;
    dpy_gfx_update(s->con, 0, 0, LCD_W * MAGNIFY, LCD_H * MAGNIFY);
}

static void c139_invalidate_display(void *opaque)
{
    CalypsoUwireState *s = opaque;
    s->redraw = true;
}

static const GraphicHwOps c139_gfx_ops = {
    .invalidate = c139_invalidate_display,
    .gfx_update = c139_update_display,
};

static void uwire_realize(DeviceState *dev, Error **errp)
{
    CalypsoUwireState *s = CALYPSO_UWIRE(dev);
    const char *env;

    s->cmd = -1;
    s->pix_hi = -1;
    s->xe = LCD_W - 1;
    s->ye = LCD_H - 1;
    s->shm_fd = -1;
    s->debug = (env = getenv("CALYPSO_LCD_DEBUG")) && *env && strcmp(env, "0");
    memory_region_init_io(&s->iomem, OBJECT(dev), &uwire_ops, s, "calypso.uwire", 0x10);
    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->iomem);
    if (!s->lcd_enabled) {
        return;
    }
    env = getenv("CALYPSO_LCD_SHM");
    s->shm_path = g_strdup(env && *env ? env : SHM_DEFAULT);
    if (strcmp(s->shm_path, "none")) {
        s->shm_fd = open(s->shm_path, O_RDWR | O_CREAT | O_TRUNC, 0666);
        if (s->shm_fd < 0) {
            fprintf(stderr, "[c139lcd] %s : %s (pas d'image partagee)\n",
                    s->shm_path, strerror(errno));
        } else {
            fchmod(s->shm_fd, 0666);
            if (ftruncate(s->shm_fd, SHM_HDR + LCD_W * LCD_H * 2) < 0) {
                fprintf(stderr, "[c139lcd] ftruncate %s : %s\n", s->shm_path, strerror(errno));
            }
            c139_export(s);
            fprintf(stderr, "[c139lcd] ecran C139 96x64 couleur (uWire CS0) : image partagee %s\n",
                    s->shm_path);
        }
    }
    s->flush = timer_new_ms(QEMU_CLOCK_REALTIME, c139_flush_cb, s);
    timer_mod(s->flush, qemu_clock_get_ms(QEMU_CLOCK_REALTIME) + FLUSH_MS);
    s->con = graphic_console_init(dev, 0, &c139_gfx_ops, s);
    qemu_console_resize(s->con, LCD_W * MAGNIFY, LCD_H * MAGNIFY);
}

static Property uwire_properties[] = {
    DEFINE_PROP_BOOL("c139-lcd", CalypsoUwireState, lcd_enabled, false),
    DEFINE_PROP_END_OF_LIST(),
};

static void uwire_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);

    dc->realize = uwire_realize;
    dc->desc = "Calypso uWire + LCD couleur 96x64 du Motorola C139";
    device_class_set_props(dc, uwire_properties);
}

static const TypeInfo uwire_info = {
    .name          = TYPE_CALYPSO_UWIRE,
    .parent        = TYPE_SYS_BUS_DEVICE,
    .instance_size = sizeof(CalypsoUwireState),
    .class_init    = uwire_class_init,
};

static void uwire_register_types(void)
{
    type_register_static(&uwire_info);
}

type_init(uwire_register_types)

4.36 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_xio.c

8135 octets, 198 lignes → 198 lignes

/*
 * calypso_xio.c - two DSP XIO windows: API Control (F900) and the DSP INTH
 * (FA00). Both used to fall through into the PORTR/PORTW no-op, so writes such
 * as the 0x0c00 the firmware stores into the DSP INTH were dropped.
 *
 * The DMA routine at PROM0 0xa636-0xa660 brackets its DMA2_CTRL accesses with
 * F900 bit 2 (BRIDGE_CLK_EN):
 *
 *   0xa637  orm   *(0x3fdc), #0x0004      ; set bit 2
 *   0xa63a  portw *(0x3fdc), 0xf900       ; -> XIO:F900  = API Control
 *   0xa640  portr *(0x4356), 0xfc28       ; read DMA2_CTRL
 *   0xa643  andm  *(0x4356), #0xfffe      ; clear ENABLE (disarm, no polling)
 *   0xa646  portw *(0x4356), 0xfc28       ; write DMA2_CTRL back
 *   0xa652  portr *(0x0011), 0xfc28       ; re-read DMA2_CTRL
 *   0xa655  st    *(0x4356), #0x0c00
 *   0xa658  portw *(0x4356), 0xfa01       ; -> XIO:FA01  = DSP INTH
 *   0xa65b  andm  *(0x3fdc), #0xfffb      ; clear bit 2
 *   0xa65e  portw *(0x3fdc), 0xf900       ; -> XIO:F900
 *
 * That is exactly the sequence prescribed by CAL207 11.3.5: "DMA1_CTRL is only
 * writable and readable when the DMA controller clock runs [...] it must set
 * the BRIDGE_CLK_EN bit of DSP API configuration register to '1'".
 *
 * This module INVENTS NO SEMANTICS: it stores, returns and logs. The DSP INTH
 * bit fields are documented only by their window (CAL207 7.2.2, "INTH
 * FA00-FAFF"), so raw values and their bit decomposition are printed without
 * naming fields we cannot name.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#include "qemu/osdep.h"
#include "hw/arm/calypso/calypso_debug.h"
#include "hw/arm/calypso/calypso_xio.h"

#include <stdio.h>
#include <stdlib.h>

#define XIO_APIC_BASE  0xF900   /* API Control  (CAL207 7.2.2)         */
#define XIO_APIC_END   0xF9FF
#define XIO_INTH_BASE  0xFA00   /* DSP INTH     (CAL207 7.2.2, 3.7.6)  */
#define XIO_INTH_END   0xFAFF

/* ---- API_CONF @ XIO:F900 - CAL207 9.1, Table 14 (3 R/W bits) ----------------
 *   bit 0                 reserved, must be 0
 *   bit 1  API_HOM        0 = API in SAM mode, 1 = API in HOM mode
 *   bit 2  BRIDGE_CLK_EN  1 = force the ARM-RHEA bridge and DMA clock
 * CAL207 gives reset as "???? ???? ???? ?010", i.e. API_HOM=1 (HOM) out of reset.
 *
 * WARNING: the two TI documents contradict each other. CAL000 7.2.1 states "SAM
 * mode is the default configuration when the DSP exits from a reset phase".
 * Unresolved. The firmware writes API_CONF=0x0000 (SAM) early in boot anyway
 * (0xb416, 0xb36f), as if it trusted neither.
 *
 * CAL207 9.1 on BRIDGE_CLK_EN: "each time the DSP software want to access to
 * these registers [DMA1_CTRL/DMA2_CTRL], it must set the BRIDGE_CLK_EN bit to
 * '1', then access to these registers and then set back BRIDGE_CLK_EN bit to '0'
 * in order to conserve power." The firmware does exactly that (0xb3b1 -> 0xb3c6).
 *
 * CAL207 9.2.3: the DSP cannot reach the APIC directly; SMODE and HINT are read
 * and written through bits 2 and 3 of BSCR (MMR data 0x0029). Hence the
 * `orm *(0x0029), #0x0004` at 0xa68d, right before the mode switch. */
#define APIC_HOM           0x0002
#define APIC_BRIDGE_CLK_EN 0x0004

/* ---- DSP INTH @ XIO:FA00/FA01 - CAL207 15.2 --------------------------------
 * FA00 CNTRL_REG (13 bits): edge/level assignment of the 12 channels.
 *      bits 11:0  CHx  1 = channel x is EDGE-triggered, 0 = LEVEL
 *      bit 12     INT4 switch: 0 = channel 4 -> INT4N (0x58), 1 -> nNMI (0x04)
 * FA01 CLEAR_REG (12 bits, W): writing 1 in a bit clears that channel (required
 *      for channels assigned as LEVEL and shared).
 * Channel N maps to INTNn, confirmed by measurement: the firmware writes 0x0380
 * (channels 7, 8, 9 as edge) and CAL207 15.1 marks exactly INT7n, INT8n and
 * INT9n as "edge". */
#define INTH_CNTRL_REG  0x00
#define INTH_CLEAR_REG  0x01
#define INTH_INT4_SWITCH 0x1000

static uint16_t apic[0x100];
static uint16_t inth[0x100];
static bool     g_init;

static bool xio_on(void)
{
    static int on = -1;
    if (on < 0) {
        on = calypso_gate("CALYPSO_XIO_MISC", 1);
        if (!on)
            fprintf(stderr, "[xio] CALYPSO_XIO_MISC=0 : F900/FA00 redeviennent "
                    "des no-op (comportement d'avant le 03/08)\n");
    }
    return on != 0;
}

static void xio_init(void)
{
    if (g_init)
        return;
    g_init = true;
    fprintf(stderr, "[xio] fenetres XIO ouvertes : API Control @0xF900 (bit2 = "
            "BRIDGE_CLK_EN, note du §11.3.5) et INTH du DSP @0xFA00 (§3.7.6). "
            "ENREGISTREMENT SEUL : aucune semantique n'est inventee.\n");
}

static void bits16(uint16_t v, char out[24])
{
    int k = 0;
    for (int b = 15; b >= 0; b--) {
        out[k++] = (v & (1u << b)) ? '1' : '0';
        if (b == 12 || b == 8 || b == 4)
            out[k++] = ' ';
    }
    out[k] = 0;
}

/* Current API RAM mode as the DSP programmed it (CAL207 9.1 bit 1). */
bool calypso_xio_api_hom(void)
{
    return (apic[0x00] & APIC_HOM) != 0;
}

bool calypso_xio_misc(bool write, uint16_t pa, uint16_t *val, uint16_t pc)
{
    bool is_apic = (pa >= XIO_APIC_BASE && pa <= XIO_APIC_END);
    bool is_inth = (pa >= XIO_INTH_BASE && pa <= XIO_INTH_END);
    if (!is_apic && !is_inth)
        return false;
    if (!xio_on())
        return false;
    xio_init();

    uint16_t *bank = is_apic ? apic : inth;
    unsigned  off  = pa & 0xFF;
    const char *nom = is_apic ? "API-CTRL" : "INTH-DSP";

    if (!write) {
        *val = bank[off];
        static unsigned nr = 0;
        if (nr++ < 30)
            fprintf(stderr, "[xio] %s PORTR PA=0x%04x -> 0x%04x PC=0x%04x\n",
                    nom, pa, *val, pc);
        return true;
    }

    bank[off] = *val;

    char b[24];
    bits16(*val, b);
    /* Dedupe on the (register, value, PC) triple: each combination is logged
     * once, then summarised periodically. Deduping on "value differs from the
     * previous one" does not work here - the firmware toggles API_CONF between
     * 0x0002 (HOM) and 0x0000 (SAM) every frame, so that test is always true:
     * 2238 lines out of 20000, 11% of the QEMU log, for two alternating
     * values. */
    static struct { uint16_t pa, val, pc; unsigned long long n; } seen[32];
    static int nseen = 0;
    int i, k = -1;
    for (i = 0; i < nseen; i++)
        if (seen[i].pa == pa && seen[i].val == *val && seen[i].pc == pc) { k = i; break; }
    if (k >= 0) {
        if (++seen[k].n % 5000 == 0)
            fprintf(stderr, "[xio] %s PA=0x%04x <- 0x%04x PC=0x%04x × %llu\n",
                    nom, pa, *val, pc, seen[k].n);
        return true;
    }
    if (nseen < 32) { seen[nseen].pa = pa; seen[nseen].val = *val;
                      seen[nseen].pc = pc; seen[nseen].n = 1; nseen++; }
    {
        if (is_apic) {
            fprintf(stderr, "[xio] API_CONF PORTW PA=0x%04x <- 0x%04x [%s] "
                    "API_HOM=%d(%s) BRIDGE_CLK_EN=%d PC=0x%04x\n",
                    pa, *val, b, !!(*val & APIC_HOM),
                    (*val & APIC_HOM) ? "HOM: API reservee ARM/DMA"
                                      : "SAM: acces partage",
                    !!(*val & APIC_BRIDGE_CLK_EN), pc);
        } else {
            if (off == INTH_CNTRL_REG) {
                fprintf(stderr, "[xio] *** INTH CNTRL_REG <- 0x%04x [%s] : canaux en "
                        "FRONT =", *val, b);
                for (int ch = 0; ch <= 11; ch++)
                    if (*val & (1u << ch))
                        fprintf(stderr, " INT%dn", ch);
                fprintf(stderr, " ; INT4_switch=%d (%s) PC=0x%04x\n",
                        !!(*val & INTH_INT4_SWITCH),
                        (*val & INTH_INT4_SWITCH) ? "canal 4 -> nNMI"
                                                  : "canal 4 -> INT4N",
                        pc);
            } else {
                fprintf(stderr, "[xio] *** INTH CLEAR_REG <- 0x%04x [%s] : efface les "
                        "canaux", *val, b);
                for (int ch = 0; ch <= 11; ch++)
                    if (*val & (1u << ch))
                        fprintf(stderr, " INT%dn", ch);
                fprintf(stderr, " PC=0x%04x\n", pc);
            }
        }
    }
    return true;
}

4.37 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/calypso_xio.h

652 octets, 20 lignes → 20 lignes

/*
 * calypso_xio.h - DSP XIO windows not modelled elsewhere (API Control F900,
 * DSP INTH FA00). Source: CAL207 7.2.2 and 11.3.5 (note), CAL000 3.7.6.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#ifndef CALYPSO_XIO_H
#define CALYPSO_XIO_H

#include <stdint.h>
#include <stdbool.h>

/* Returns true when PA falls in one of the windows handled here. */
bool calypso_xio_misc(bool write, uint16_t pa, uint16_t *val, uint16_t pc);

/* True when the DSP has put the API RAM in HOM (Host Only Mode, 9.1 bit 1):
 * the API window is then reserved for the ARM and the DMA, CAL000 7.2.1. */
bool calypso_xio_api_hom(void);

#endif /* CALYPSO_XIO_H */

4.38 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/fw_console.c

2738 octets, 81 lignes → 81 lignes

/*
 * fw_console.c — diagnostic poller for the layer1 firmware printf_buffer
 *
 * The osmocom-bb compal_e88 firmware (layer1.highram.elf) builds printf
 * output in `printf_buffer` (symbol at 0x00831018) via cons_puts. On real
 * hardware cons_puts pushes the assembled string to the LCD framebuffer
 * (fb_bw8_putstr at 0x82a1b4); QEMU does not emulate the LCD, so the
 * strings just sit in RAM and get overwritten on the next printf.
 *
 * This poller wakes every FW_POLL_MS simulated milliseconds, snapshots the
 * buffer via cpu_physical_memory_read, and emits the string to
 * /tmp/qemu-fw-console.log + stderr whenever its content changes. Polling
 * misses printfs that get overwritten between two ticks; for noisy paths
 * lower FW_POLL_MS or mirror the buffer to stderr at additional event
 * sites (DSP IDLE, IRQ entry, etc.).
 */

#include "qemu/osdep.h"
#include "qemu/timer.h"
#include "exec/cpu-common.h"
#include "hw/arm/calypso/fw_console.h"

#define FW_PRINTF_ADDR  0x00831018u
#define FW_PRINTF_LEN   512u
#define FW_POLL_MS      10u

static QEMUTimer *fw_poll_timer;
static FILE *fw_log_fp;
static uint8_t fw_last[FW_PRINTF_LEN];

static void fw_console_emit(const uint8_t *buf, size_t len)
{
    while (len > 0 && (buf[len-1] == '\n' || buf[len-1] == '\r'))
        len--;
    if (len == 0)
        return;
    if (fw_log_fp) {
        fprintf(fw_log_fp, "[fw] %.*s\n", (int)len, buf);
        fflush(fw_log_fp);
    }
    fprintf(stderr, "[fw-console] %.*s\n", (int)len, buf);
}

static void fw_console_poll(void *opaque)
{
    uint8_t cur[FW_PRINTF_LEN];
    cpu_physical_memory_read(FW_PRINTF_ADDR, cur, FW_PRINTF_LEN);

    if (memcmp(cur, fw_last, FW_PRINTF_LEN) != 0) {
        size_t len = 0;
        while (len < FW_PRINTF_LEN && cur[len] != 0)
            len++;
        if (len > 0)
            fw_console_emit(cur, len);
        memcpy(fw_last, cur, FW_PRINTF_LEN);
    }

    timer_mod_ns(fw_poll_timer,
                 qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) +
                 (uint64_t)FW_POLL_MS * 1000000ULL);
}

void fw_console_init(void)
{
    fw_log_fp = fopen("/tmp/qemu-fw-console.log", "w");
    if (fw_log_fp) {
        setvbuf(fw_log_fp, NULL, _IOLBF, 0);
        fprintf(fw_log_fp,
                "# QEMU firmware console - poll printf_buffer @ 0x%08x every %u ms\n",
                FW_PRINTF_ADDR, FW_POLL_MS);
    }

    fw_poll_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, fw_console_poll, NULL);
    timer_mod_ns(fw_poll_timer,
                 qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) +
                 (uint64_t)FW_POLL_MS * 1000000ULL);

    fprintf(stderr,
            "[fw-console] polling 0x%08x every %u ms -> /tmp/qemu-fw-console.log\n",
            FW_PRINTF_ADDR, FW_POLL_MS);
}

4.39 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1ctl_sock.c

16969 octets, 481 lignes → 481 lignes

/*
 * l1ctl_sock.c - L1CTL unix socket server (legacy QEMU-internal path)
 *
 * Serves a unix socket (default /tmp/osmocom_l2) that speaks L1CTL
 * (length-prefixed messages) to the OsmocomBB mobile, translating between:
 *   - sercomm framing (FLAG/ESCAPE/DLCI) on the firmware UART side
 *   - L1CTL length prefixes on the mobile socket side
 *
 * NOT WIRED IN THIS TREE. The file builds (meson.build, under the l1-dsp
 * layer 1) but nothing calls l1ctl_sock_init(): the only call site lives in
 * the qosmo-dsp fork. In a qosmo launcher run osmocon owns /tmp/osmocom_l2
 * (osmocon -m romload -s /tmp/osmocom_l2) and relays to the firmware over the
 * modem pty, so the mobile never connects here. When it was wired, the
 * mobile -> firmware direction stayed empty: "RX<-mobile" logged 0 frames over
 * a whole run while osmocon saw the traffic. The blocks below that depend on
 * that direction are therefore dead, and marked as such.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "qemu/osdep.h"
#include "qemu/main-loop.h"
#include "hw/arm/calypso/calypso_uart.h"

#include <sys/socket.h>
#include <sys/un.h>
#include <fcntl.h>
#include <errno.h>

/* Sercomm constants */
#define SERCOMM_FLAG       0x7E
#define SERCOMM_ESCAPE     0x7D
#define SERCOMM_ESCAPE_XOR 0x20
#define SERCOMM_DLCI_L1CTL 5

/* L1CTL socket path */
#define L1CTL_SOCK_PATH    "/tmp/osmocom_l2"

#define L1CTL_LOG(fmt, ...) \
    fprintf(stderr, "[l1ctl-sock] " fmt "\n", ##__VA_ARGS__)

/* Readable L1CTL message names (l1ctl_proto.h), diagnostics only. This log
 * only ever sees the firmware -> mobile direction via sercomm; the real
 * mobile <-> firmware exchange is in osmocon.log (hdlc). */
static inline const char *l1ctl_tname(uint8_t t)
{
    switch (t) {
    case 0x01: return "FBSB_REQ";       case 0x02: return "FBSB_CONF";
    case 0x03: return "DATA_IND";       case 0x04: return "RACH_REQ";
    case 0x05: return "DM_EST_REQ";     case 0x06: return "DATA_REQ";
    case 0x07: return "RESET_IND";      case 0x08: return "PM_REQ";
    case 0x09: return "PM_CONF";        case 0x0c: return "RACH_CONF";
    case 0x0d: return "RESET_REQ";      case 0x0e: return "RESET_CONF";
    case 0x0f: return "DATA_CONF";      case 0x10: return "CCCH_MODE_REQ";
    case 0x11: return "CCCH_MODE_CONF"; case 0x12: return "DM_REL_REQ";
    case 0x13: return "PARAM_REQ";      default:   return "?";
    }
}

/* ---- Sercomm TX parser (firmware → mobile) ---- */

typedef enum {
    SC_IDLE,      /* waiting for FLAG */
    SC_IN_FRAME,  /* collecting frame bytes */
    SC_ESCAPE,    /* next byte is escaped */
} SercommState;

typedef struct L1CTLSock {
    /* Server socket */
    int srv_fd;

    /* Client connection */
    int cli_fd;

    /* Sercomm TX parser (firmware UART output → mobile) */
    SercommState sc_state;
    uint8_t  sc_buf[512];
    int      sc_len;

    /* L1CTL RX parser (mobile → firmware UART input) */
    uint8_t  lp_buf[4096];  /* length-prefix accumulator */
    int      lp_len;

    /* Reference to UART modem for RX injection */
    CalypsoUARTState *uart;
} L1CTLSock;

static L1CTLSock g_l1ctl;


/* ---- Sercomm helpers ---- */

static int sercomm_wrap(uint8_t dlci, const uint8_t *payload, int plen,
                        uint8_t *out, int out_size)
{
    int pos = 0;
    if (pos >= out_size) return -1;
    out[pos++] = SERCOMM_FLAG;

    /* DLCI + CTRL */
    uint8_t hdr[2] = { dlci, 0x03 };
    for (int i = 0; i < 2; i++) {
        if (hdr[i] == SERCOMM_FLAG || hdr[i] == SERCOMM_ESCAPE) {
            if (pos + 2 > out_size) return -1;
            out[pos++] = SERCOMM_ESCAPE;
            out[pos++] = hdr[i] ^ SERCOMM_ESCAPE_XOR;
        } else {
            if (pos + 1 > out_size) return -1;
            out[pos++] = hdr[i];
        }
    }

    /* Payload */
    for (int i = 0; i < plen; i++) {
        if (payload[i] == SERCOMM_FLAG || payload[i] == SERCOMM_ESCAPE) {
            if (pos + 2 > out_size) return -1;
            out[pos++] = SERCOMM_ESCAPE;
            out[pos++] = payload[i] ^ SERCOMM_ESCAPE_XOR;
        } else {
            if (pos + 1 > out_size) return -1;
            out[pos++] = payload[i];
        }
    }

    if (pos >= out_size) return -1;
    out[pos++] = SERCOMM_FLAG;
    return pos;
}

/* ---- Send L1CTL message to mobile (length-prefix) ---- */

static void l1ctl_send_to_mobile(L1CTLSock *s, const uint8_t *payload, int len)
{
    if (s->cli_fd < 0 || len <= 0 || len > UINT16_MAX) return;

    uint8_t hdr[2] = { (uint8_t)(len >> 8), (uint8_t)(len & 0xFF) };
    struct iovec iov[2] = {
        { .iov_base = hdr,                  .iov_len = sizeof(hdr) },
        { .iov_base = (void *)payload,      .iov_len = (size_t)len },
    };
    struct msghdr msg = { .msg_iov = iov, .msg_iovlen = 2 };

    int total = (int)sizeof(hdr) + len;
    ssize_t sent = sendmsg(s->cli_fd, &msg, MSG_NOSIGNAL);
    if (sent != total) {
        L1CTL_LOG("client send error (%zd/%d), closing", sent, total);
        close(s->cli_fd);
        s->cli_fd = -1;
    }
}

/* ---- Process a complete sercomm frame from firmware TX ---- */

static void sercomm_frame_complete(L1CTLSock *s)
{
    if (s->sc_len < 2) return;  /* need at least DLCI + CTRL */

    uint8_t dlci = s->sc_buf[0];
    /* uint8_t ctrl = s->sc_buf[1]; */
    uint8_t *payload = &s->sc_buf[2];
    int plen = s->sc_len - 2;

    if (dlci == SERCOMM_DLCI_L1CTL && plen > 0) {
        /* Current dedicated channel -> /dev/shm/calypso_dcch_cfg, for
         * external tools (l1-grgsm/calypso_l1ctl_tap.c reads it).
         *
         * Read it here, in the firmware -> mobile direction, because that is
         * the only L1CTL flow this file parses. DATA_CONF (0x0f) and DATA_IND
         * (0x03) carry l1ctl_info_dl.chan_nr in payload[4], filled by the
         * firmware from its own mframe scheduler, so it is OUR mobile's
         * channel and not a neighbour's. Not taken from the CCCH IMM ASSIGN,
         * which carries every subscriber's (68 for RA=0x07 and 12 for RA=0x0a
         * against a single RACH of ours at RA=0x08): the active subchannel
         * jumped 60 times per run.
         *
         * chan_nr (GSM 08.58 9.3.1): 001SSTTT = SDCCH/4, 01SSSTTT = SDCCH/8.
         * BCCH (0x80), CCCH (0x90) and TCH (00001TTT) are ignored here. */
        if ((payload[0] == 0x0f || payload[0] == 0x03) && plen >= 5) {
            uint8_t chan_nr = payload[4];
            int kind = -1, ss = 0;
            if ((chan_nr & 0xE0) == 0x20)      { kind = 0; ss = (chan_nr >> 3) & 0x03; }
            else if ((chan_nr & 0xC0) == 0x40) { kind = 1; ss = (chan_nr >> 3) & 0x07; }
            static uint8_t last_chan_nr = 0xFF;
            /* Publish on change only: while on a dedicated channel the
             * mobile also reads neighbour BCCHs for its measurements, so
             * chan_nr toggles constantly (121 transitions measured for 2
             * dedicated channels). */
            if (kind >= 0 && chan_nr != last_chan_nr) {
                static uint32_t dcch_seq;
                last_chan_nr = chan_nr;
                dcch_seq++;
                uint8_t b[16];
                memset(b, 0, sizeof(b));
                memcpy(b, &dcch_seq, 4);
                b[4] = (uint8_t)kind; b[5] = (uint8_t)ss;
                b[6] = chan_nr & 0x07; b[7] = chan_nr;
                int dfd = open("/dev/shm/calypso_dcch_cfg",
                               O_WRONLY | O_CREAT | O_TRUNC, 0666);
                if (dfd >= 0) {
                    if (write(dfd, b, sizeof(b)) < 0) { /* ignore */ }
                    close(dfd);
                }
                L1CTL_LOG("DCCH #%u : chan_nr=0x%02x -> SDCCH/%d SS=%d TN=%u "
                          "(vu sur %s)", dcch_seq, chan_nr, kind ? 8 : 4, ss,
                          chan_nr & 0x07, l1ctl_tname(payload[0]));
            }
        }
        L1CTL_LOG("TX→mobile: dlci=%d len=%d type=0x%02x %s", dlci, plen, payload[0],
                  l1ctl_tname(payload[0]));
        l1ctl_send_to_mobile(s, payload, plen);
    }
    /* Ignore other DLCIs (debug console, loader, etc.) */
}

/* ---- Feed firmware UART TX bytes into sercomm parser ---- */

void l1ctl_sock_uart_tx_byte(uint8_t byte)
{
    L1CTLSock *s = &g_l1ctl;

    switch (s->sc_state) {
    case SC_IDLE:
        if (byte == SERCOMM_FLAG) {
            s->sc_state = SC_IN_FRAME;
            s->sc_len = 0;
        }
        break;

    case SC_IN_FRAME:
        if (byte == SERCOMM_FLAG) {
            if (s->sc_len > 0) {
                sercomm_frame_complete(s);
            }
            /* Stay in IN_FRAME for next frame */
            s->sc_len = 0;
        } else if (byte == SERCOMM_ESCAPE) {
            s->sc_state = SC_ESCAPE;
        } else {
            if (s->sc_len < (int)sizeof(s->sc_buf)) {
                s->sc_buf[s->sc_len++] = byte;
            }
        }
        break;

    case SC_ESCAPE:
        if (s->sc_len < (int)sizeof(s->sc_buf)) {
            s->sc_buf[s->sc_len++] = byte ^ SERCOMM_ESCAPE_XOR;
        }
        s->sc_state = SC_IN_FRAME;
        break;
    }
}

/* ---- Receive L1CTL from mobile, inject into firmware UART RX ---- */

static void l1ctl_client_readable(void *opaque)
{
    L1CTLSock *s = (L1CTLSock *)opaque;

    uint8_t tmp[4096];
    ssize_t n = recv(s->cli_fd, tmp, sizeof(tmp), MSG_DONTWAIT);
    if (n < 0) {
        if (errno == EAGAIN || errno == EWOULDBLOCK)
            return;  /* no data available yet */
        L1CTL_LOG("client recv error: %s", strerror(errno));
        qemu_set_fd_handler(s->cli_fd, NULL, NULL, NULL);
        close(s->cli_fd);
        s->cli_fd = -1;
        s->lp_len = 0;
        return;
    }
    if (n == 0) {
        L1CTL_LOG("client disconnected");
        qemu_set_fd_handler(s->cli_fd, NULL, NULL, NULL);
        close(s->cli_fd);
        s->cli_fd = -1;
        s->lp_len = 0;
        return;
    }

    /* Accumulate in length-prefix buffer */
    if (s->lp_len + (int)n > (int)sizeof(s->lp_buf)) {
        s->lp_len = 0;  /* overflow, reset */
    }
    memcpy(&s->lp_buf[s->lp_len], tmp, n);
    s->lp_len += (int)n;

    /* Parse complete L1CTL messages */
    while (s->lp_len >= 2) {
        int msglen = (s->lp_buf[0] << 8) | s->lp_buf[1];
        if (s->lp_len < 2 + msglen) break;  /* incomplete */

        uint8_t *payload = &s->lp_buf[2];

        /* Kc capture (A5 ciphering): L1CTL_CRYPTO_REQ (0x15), mobile -> fw.
         * payload: [0]=0x15 [1]flags [2..3]pad [4]chan_nr [5]link_id [6..7]pad
         * [8]algo [9]key_len [10..]Kc. Written to /dev/shm/calypso_kc as
         * (seq, algo, key_len, Kc); si_bridge.py reads it and restarts grgsm
         * with -k to decrypt the downlink. The captured Kc is the one the
         * mobile derived (A8), i.e. exactly the network's. */
        if (payload[0] == 0x15 && msglen >= 10) {
            uint8_t algo = payload[8];
            uint8_t klen = payload[9];
            if (klen > 16) klen = 16;
            /* Guard on algo, matching the live writer in osmocon. Without
             * it an algo=0/klen=0 request still bumps the sequence number, and
             * a reader would take the file as holding a valid Kc and cipher
             * with an all-zero key. */
            if (algo >= 1 && algo <= 3 && 10 + (int)klen <= msglen) {
                static uint32_t kc_seq = 0;
                uint8_t kbuf[32];
                memset(kbuf, 0, sizeof(kbuf));
                kc_seq++;
                memcpy(kbuf, &kc_seq, 4);              /* [0..3] seq (LE) */
                kbuf[4] = algo; kbuf[5] = klen;        /* [4]algo [5]key_len */
                memcpy(kbuf + 6, &payload[10], klen);  /* [6..] Kc */
                int kfd = open("/dev/shm/calypso_kc",
                               O_WRONLY | O_CREAT | O_TRUNC, 0666);
                if (kfd >= 0) {
                    if (write(kfd, kbuf, sizeof(kbuf)) < 0) { /* ignore */ }
                    close(kfd);
                }
                L1CTL_LOG("CRYPTO_REQ: algo=%u klen=%u "
                          "Kc=%02x%02x%02x%02x%02x%02x%02x%02x -> "
                          "/dev/shm/calypso_kc#%u", algo, klen,
                          payload[10], payload[11], payload[12], payload[13],
                          payload[14], payload[15], payload[16], payload[17],
                          kc_seq);
            }
        }
        /* Clear the cipher on dedicated channel setup and release: every new
         * channel starts in the clear until its own CIPHER MODE COMMAND,
         * otherwise a stale Kc would cipher the next channel's SABM. */
        if (payload[0] == 0x05 || payload[0] == 0x12) {   /* DM_EST_REQ / DM_REL_REQ */
            /* ⚠️ DEAD IN THE CURRENT SETUP: this mobile -> firmware direction
             * measured 0 frames, so the Kc reset below never runs. Check this
             * before enabling A5/1. */
            int kfd = open("/dev/shm/calypso_kc",
                           O_WRONLY | O_CREAT | O_TRUNC, 0666);
            if (kfd >= 0) {
                uint8_t z[32]; memset(z, 0, sizeof(z));
                if (write(kfd, z, sizeof(z)) < 0) { /* ignore */ }
                close(kfd);
            }

        }

        /* Wrap in sercomm and inject into UART RX */
        uint8_t frame[1024];
        int flen = sercomm_wrap(SERCOMM_DLCI_L1CTL, payload, msglen,
                                frame, sizeof(frame));
        if (flen > 0 && s->uart) {
            L1CTL_LOG("RX←mobile: len=%d type=0x%02x %s → sercomm %d bytes",
                      msglen, payload[0], l1ctl_tname(payload[0]), flen);
            /* Hex dump of sercomm frame being injected */
            {
                fprintf(stderr, "[l1ctl-sock] INJECT %d bytes:", flen);
                for (int j = 0; j < flen && j < 32; j++)
                    fprintf(stderr, " %02x", frame[j]);
                if (flen > 32) fprintf(stderr, " ...");
                fprintf(stderr, "\n");
            }
            calypso_uart_receive(s->uart, frame, flen);
        }

        /* Consume from buffer */
        int consumed = 2 + msglen;
        memmove(s->lp_buf, &s->lp_buf[consumed], s->lp_len - consumed);
        s->lp_len -= consumed;
    }
}

/* ---- Accept new client connection ---- */

static void l1ctl_accept_cb(void *opaque)
{
    L1CTLSock *s = (L1CTLSock *)opaque;

    int fd = accept(s->srv_fd, NULL, NULL);
    if (fd < 0) return;

    /* Only one client at a time */
    if (s->cli_fd >= 0) {
        L1CTL_LOG("replacing existing client");
        qemu_set_fd_handler(s->cli_fd, NULL, NULL, NULL);
        close(s->cli_fd);
    }

    /* Set non-blocking */
    int flags = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);

    s->cli_fd = fd;
    s->lp_len = 0;
    s->sc_state = SC_IDLE;
    s->sc_len = 0;

    qemu_set_fd_handler(fd, l1ctl_client_readable, NULL, s);
    L1CTL_LOG("client connected (fd=%d)", fd);
}

/* ---- Init ---- */

void l1ctl_sock_init(CalypsoUARTState *uart, const char *path)
{
    L1CTLSock *s = &g_l1ctl;
    memset(s, 0, sizeof(*s));
    s->srv_fd = -1;
    s->cli_fd = -1;
    s->uart = uart;

    if (!path) path = L1CTL_SOCK_PATH;

    /* Remove stale socket */
    unlink(path);

    /* Create unix socket server */
    s->srv_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (s->srv_fd < 0) {
        L1CTL_LOG("ERROR: socket(): %s", strerror(errno));
        return;
    }

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);

    if (bind(s->srv_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        L1CTL_LOG("ERROR: bind(%s): %s", path, strerror(errno));
        close(s->srv_fd);
        s->srv_fd = -1;
        return;
    }

    if (listen(s->srv_fd, 1) < 0) {
        L1CTL_LOG("ERROR: listen(): %s", strerror(errno));
        close(s->srv_fd);
        s->srv_fd = -1;
        return;
    }

    /* Set non-blocking */
    int flags = fcntl(s->srv_fd, F_GETFL);
    fcntl(s->srv_fd, F_SETFL, flags | O_NONBLOCK);

    qemu_set_fd_handler(s->srv_fd, l1ctl_accept_cb, NULL, s);
    L1CTL_LOG("listening on %s", path);
}

/* ---- Manual poll (called from TDMA tick) ---- */

void l1ctl_sock_poll(void)
{
    L1CTLSock *s = &g_l1ctl;

    /* Try to accept a pending client */
    if (s->srv_fd >= 0 && s->cli_fd < 0) {
        int fd = accept(s->srv_fd, NULL, NULL);
        if (fd >= 0) {
            int flags = fcntl(fd, F_GETFL);
            fcntl(fd, F_SETFL, flags | O_NONBLOCK);
            s->cli_fd = fd;
            s->lp_len = 0;
            s->sc_state = SC_IDLE;
            s->sc_len = 0;
            qemu_set_fd_handler(fd, l1ctl_client_readable, NULL, s);
            L1CTL_LOG("client connected via poll (fd=%d)", fd);
        }
    }

    /* Try to read from connected client */
    if (s->cli_fd >= 0) {
        l1ctl_client_readable(s);
    }
}

bool l1ctl_client_active(void)
{
    return g_l1ctl.cli_fd >= 0;
}

4.40 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/sercomm_gate.c

9101 octets, 251 lignes → 251 lignes

/*
 * sercomm_gate.c — Sercomm DLCI router on the modem UART (PTY).
 *
 * The sercomm HDLC stream coming from the host (mobile/ccch_scan) is decoded
 * here, re-wrapped and pushed into the UART RX FIFO, so the firmware's own
 * sercomm driver parses it through the real code path. L1CTL = DLCI 5;
 * TRXC = DLCI 4, intercepted here with stub responses wrapped back out. No
 * DLCI on the PTY ever carries radio bursts.
 *
 * Nothing else lands here: TRXC over UDP is stubbed by calypso-ipc-device on
 * port 5701 and never reaches QEMU, TRXD (bursts) belongs to calypso_bsp.c via
 * calypso_orch, and QEMU, being the clock master, sends CLK ticks to the
 * bridge from calypso_trx.c instead of listening for them.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "qemu/osdep.h"
#include "qemu/main-loop.h"
#include "qemu/error-report.h"
#include "chardev/char-fe.h"
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <errno.h>

#include "hw/arm/calypso/calypso_uart.h"
#include "hw/arm/calypso/sercomm_gate.h"

#define GATE_LOG(fmt, ...) \
    fprintf(stderr, "[gate] " fmt "\n", ##__VA_ARGS__)

/* UART captured on the first sercomm_gate_feed() call. */
static CalypsoUARTState *g_uart;

/* ============================================================
 * 1. PTY side — sercomm HDLC parser (L1CTL only)
 * ============================================================ */

#define SERCOMM_FLAG   0x7E
#define SERCOMM_ESCAPE 0x7D
#define SERCOMM_XOR    0x20

#define GATE_BUF_SIZE  512

enum gate_rx_state {
    GATE_WAIT_FLAG,
    GATE_IN_FRAME,
    GATE_ESCAPE,
};

static uint8_t  sc_buf[GATE_BUF_SIZE];
static int      sc_len;
static enum gate_rx_state sc_state;

/*
 * Re-wrap a decoded sercomm frame (DLCI + CTRL + payload) and push it
 * back into the UART RX FIFO. The firmware's sercomm driver will parse
 * it again — keeping the firmware code path 100% identical to hardware.
 */
static void gate_push_to_fifo(CalypsoUARTState *s,
                              const uint8_t *frame, int len)
{
    fprintf(stderr,
            "[gate→fw-fifo] DLCI=%u CTRL=%02x payload=%d bytes (rx_count_before=%u)\n",
            len > 0 ? frame[0] : 0xff,
            len > 1 ? frame[1] : 0xff,
            len > 2 ? len - 2 : 0,
            (unsigned)s->rx_count);
    { uint8_t _b = SERCOMM_FLAG; calypso_uart_inject_raw(s, &_b, 1); }
    for (int i = 0; i < len; i++) {
        uint8_t c = frame[i];
        /* Standard sercomm HDLC byte stuffing : escape FLAG (0x7e) and
         * ESCAPE (0x7d) so they survive in payload bytes. Mandatory for
         * the firmware sercomm parser ; removing this corrupts every
         * frame whose payload contains 0x7e or 0x7d. */
        if (c == SERCOMM_FLAG || c == SERCOMM_ESCAPE) {
            { uint8_t _e = SERCOMM_ESCAPE; calypso_uart_inject_raw(s, &_e, 1); }
            { uint8_t _x = c ^ SERCOMM_XOR; calypso_uart_inject_raw(s, &_x, 1); }
        } else {
            calypso_uart_inject_raw(s, &c, 1);
        }
    }
    { uint8_t _b = SERCOMM_FLAG; calypso_uart_inject_raw(s, &_b, 1); }
}

#define SERCOMM_DLCI_TRXC 4

/* Wrap a payload in sercomm DLCI 4 (TRXC) and send it back via the
 * chardev TX (→ PTY → calypso-ipc-device → osmo-bts-trx 5701). */
static void gate_send_trxc_rsp(CalypsoUARTState *s,
                                const uint8_t *payload, int plen)
{
    if (!s) return;
    uint8_t frame[1024];
    int pos = 0;
    frame[pos++] = SERCOMM_FLAG;
    uint8_t hdr[2] = { SERCOMM_DLCI_TRXC, 0x03 };
    for (int i = 0; i < 2; i++) {
        if (hdr[i] == SERCOMM_FLAG || hdr[i] == SERCOMM_ESCAPE) {
            frame[pos++] = SERCOMM_ESCAPE;
            frame[pos++] = hdr[i] ^ SERCOMM_XOR;
        } else {
            frame[pos++] = hdr[i];
        }
    }
    for (int i = 0; i < plen && pos + 2 < (int)sizeof(frame); i++) {
        uint8_t c = payload[i];
        if (c == SERCOMM_FLAG || c == SERCOMM_ESCAPE) {
            frame[pos++] = SERCOMM_ESCAPE;
            frame[pos++] = c ^ SERCOMM_XOR;
        } else {
            frame[pos++] = c;
        }
    }
    frame[pos++] = SERCOMM_FLAG;
    qemu_chr_fe_write_all(&s->chr, frame, pos);
    fprintf(stderr, "[gate-trxc] TX→bridge %d bytes (sercomm framed=%d)\n",
            plen, pos);
}

/* Parse a TRXC CMD string and produce a RSP string.
 * Mirrors calypso-ipc-device::trxc_response. Returns response length, or 0 if
 * the command is not a CMD (silently ignored). */
static int gate_trxc_handle(const uint8_t *cmd_buf, int cmd_len,
                             char *rsp, int rsp_size)
{
    /* Strip trailing \0 and find verb/args */
    int n = cmd_len;
    while (n > 0 && (cmd_buf[n-1] == 0 || cmd_buf[n-1] == "\n"[0])) n--;
    if (n < 4) return 0;
    if (memcmp(cmd_buf, "CMD ", 4) != 0) return 0;

    char tmp[512];
    int  tn = n - 4 < (int)sizeof(tmp) - 1 ? n - 4 : (int)sizeof(tmp) - 1;
    memcpy(tmp, cmd_buf + 4, tn);
    tmp[tn] = 0;

    /* Split verb and args */
    char *verb = tmp;
    char *args = strchr(tmp, " "[0]);
    if (args) { *args = 0; args++; }
    else args = (char *)"";

    fprintf(stderr, "[gate-trxc] RX←bridge CMD %s args=%s\n", verb, args);

    int rl;
    if (strcmp(verb, "POWERON") == 0)
        rl = snprintf(rsp, rsp_size, "RSP POWERON 0");
    else if (strcmp(verb, "POWEROFF") == 0)
        rl = snprintf(rsp, rsp_size, "RSP POWEROFF 0");
    else if (strcmp(verb, "SETFORMAT") == 0)
        rl = snprintf(rsp, rsp_size, "RSP SETFORMAT 0 %s", args[0] ? args : "0");
    else if (strcmp(verb, "NOMTXPOWER") == 0)
        rl = snprintf(rsp, rsp_size, "RSP NOMTXPOWER 0 50");
    else if (strcmp(verb, "MEASURE") == 0)
        rl = snprintf(rsp, rsp_size, "RSP MEASURE 0 %s -60", args[0] ? args : "0");
    else if (args[0])
        rl = snprintf(rsp, rsp_size, "RSP %s 0 %s", verb, args);
    else
        rl = snprintf(rsp, rsp_size, "RSP %s 0", verb);

    if (rl > 0 && rl < rsp_size) rsp[rl++] = 0;  /* trailing NUL like bridge */
    return rl;
}

void sercomm_gate_feed(CalypsoUARTState *s, const uint8_t *buf, int size)
{
    if (!g_uart) g_uart = s;
    for (int i = 0; i < size; i++) {
        uint8_t b = buf[i];

        switch (sc_state) {
        case GATE_WAIT_FLAG:
            if (b == SERCOMM_FLAG) {
                sc_state = GATE_IN_FRAME;
                sc_len = 0;
            } else {
                /* Pre-sercomm raw bytes pass through (loader/console). */
                calypso_uart_inject_raw(s, &b, 1);
            }
            break;

        case GATE_ESCAPE:
            if (sc_len < GATE_BUF_SIZE)
                sc_buf[sc_len++] = b ^ SERCOMM_XOR;
            sc_state = GATE_IN_FRAME;
            break;

        case GATE_IN_FRAME:
            if (b == SERCOMM_FLAG) {
                if (sc_len >= 2) {
                    /* DLCI 5 = L1CTL from the mobile (via the bridge):
                     * trace it, then push it to the firmware FIFO so the
                     * real sercomm parser dispatches it. Every other DLCI
                     * (console, debug, ...) goes straight to the FIFO. */
                    if (sc_buf[0] == SERCOMM_DLCI_TRXC && sc_len >= 2) {
                        char rsp[512];
                        int rl = gate_trxc_handle(sc_buf + 2, sc_len - 2,
                                                   rsp, sizeof(rsp));
                        if (rl > 0)
                            gate_send_trxc_rsp(s, (uint8_t *)rsp, rl);
                        sc_len = 0;
                        break;
                    }
                    if (sc_buf[0] == 5) {
                        int plen = sc_len - 2;
                        uint8_t mt = plen > 0 ? sc_buf[2] : 0;
                        fprintf(stderr,
                                "[PTY-L1CTL] <<<RX %d bytes (mobile→fw) mt=0x%02x:",
                                plen, mt);
                        for (int j = 0; j < plen && j < 32; j++)
                            fprintf(stderr, " %02x", sc_buf[2 + j]);
                        if (plen > 32) fprintf(stderr, " ...");
                        fprintf(stderr, "\n");

                    }
                    gate_push_to_fifo(s, sc_buf, sc_len);
                }
                sc_len = 0;
            } else if (b == SERCOMM_ESCAPE) {
                sc_state = GATE_ESCAPE;
            } else {
                if (sc_len < GATE_BUF_SIZE)
                    sc_buf[sc_len++] = b;
            }
            break;
        }
    }
}

/* ============================================================
 * 2. Clock — no listener
 * ============================================================ */

static int g_clk_fd = -1;

/* ---------- init ---------- */

void sercomm_gate_init(int base_port)
{
    if (base_port <= 0) base_port = 6700;
    int clk_port = base_port + 0;

    /* No CLK listener: QEMU is the clock master and sends the ticks straight
     * to the bridge from calypso_trx.c (port 6700), so the gate never has to
     * receive a CLK IND. */
    (void)clk_port;
    g_clk_fd = -1;
    GATE_LOG("TRXD: owned by calypso_bsp.c via calypso_orch");
}

4.41 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/c54x_decode.c

22368 octets, 436 lignes → 436 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * c54x_decode.c - operand decode: resolve_smem/lmem/xmem, condition codes.
 *
 * Split out of calypso_c54x.c on 2026-09-18. File map in c54x_internal.h.
 */
#include "c54x_internal.h"
#include "hw/arm/calypso/calypso_debug.h"

/* MMR operand of LDM / STLM / STM (SPRU131G 5.6 "Memory-mapped
 * register addressing", SPRU172C STM/STLM examples). Direct form: the 7 LSBs
 * of the opcode are the MMR address, DP/CPL ignored. Indirect form (bit 7 = 1,
 * only *ARx, *ARx-, *ARx+ allowed): the 7 LSBs of ARx give the MMR address,
 * and after the access ARx is post-modified with its 9 MSBs forced to 0
 * (STM #8765h,*AR7+ : AR7 8010h -> 0011h, writes AR0 ; STLM B,*AR1- : AR1
 * 3F17h -> 0016h). The old code took the opcode's low 7 bits in both cases,
 * i.e. wrote MMR 0x17/0x0F... for an indirect operand and never touched ARx.
 * The ROM has no indirect-MMR site (PROM0.dis), so this is ISA-only. */
uint16_t resolve_mmr(C54xState *s, uint16_t opcode)
{
    if (!(opcode & 0x80)) return opcode & 0x7F;
    int nar = opcode & 0x07, mod = (opcode >> 3) & 0x0F;
    if ((s->st1 & ST1_CMPT) && nar == 0) nar = arp(s);
    else if (s->st1 & ST1_CMPT)
        s->st0 = (s->st0 & ~ST0_ARP_MASK) | (nar << ST0_ARP_SHIFT);
    uint16_t addr = s->ar[nar] & 0x7F;
    int d = mod == 0x1 ? -1 : mod == 0x2 ? +1 : 0;
    s->ar[nar] = (uint16_t)(addr + d);
    return addr;
}

uint16_t resolve_smem(C54xState *s, uint16_t opcode, bool *indirect)
{
    if (opcode & 0x80) {
        /* Indirect addressing. Per SPRU131G 5.4.1 Table 5-5, bits 2:0 (ARF)
         * select the AR used by THIS instruction; ARP in ST0 is only then
         * updated to ARF, for the NEXT direct-Smem reference. Driving the
         * access from ARP instead of ARF is an off-by-one that makes every
         * indirect instruction modify the PREVIOUS one's AR. */
        *indirect = true;
        int mod = (opcode >> 3) & 0x0F;
        int nar = opcode & 0x07;
        int cur_arp = nar;
        /* SPRU172C MAR / SPRU131G 5.5: in compatibility mode (CMPT = 1), an
         * ARx field = AR0 designates AR(ARP) and does not modify the ARP
         * (e.g. MAR *AR0- with ARP = 4: AR4 is decremented, ARP stays 4).
         * No effect in native mode: the ROM never sets CMPT to 1 (ST1 =
         * 0x2900/0x6900). */
        bool cmpt_ar0 = (s->st1 & ST1_CMPT) && nar == 0;
        if (cmpt_ar0) cur_arp = arp(s);
        uint16_t addr = s->ar[cur_arp];
        uint16_t ar_before = s->ar[cur_arp];  /* MOD-MISMATCH probe: base before post-modify */

        /* Probe: first use of each AR as an address base. A value still equal
         * to its reset content means read-before-write, i.e. the reset value
         * is load-bearing. Diagnostic only, to be removed. */
        {
            static uint8_t ar_used = 0;
            if (!(ar_used & (1 << cur_arp))) {
                ar_used |= (1 << cur_arp);
                fprintf(stderr, "[c54x] AR-FIRSTUSE AR%d=0x%04x PC=0x%04x insn=%u\n",
                        cur_arp, addr, s->pc, s->insn_count);
            }
        }

        if (cur_arp == 2 && addr < 0x0820) {
            /* @BEQUILLE - AR2_FLOOR_DROP  (CALYPSO_AR2_FLOOR_DROP, EQ1, default OFF)
             *   masks  : the AR2 address computation in the correlator, which underflows
             *            the 0x0800 DARAM buffer down into MMR space (0x00=IMR, 0x1E=XPC)
             *            and clobbers it. The drop redirects the access to 0xFFFF instead
             *            of fixing the pointer. Note the LOG is gated by the AR2-FLOOR
             *            debug token, the DROP is NOT.
             *   remove : once AR2 stays inside [0x0800,0x2b28) over the whole FB kernel
             *            (AR2-FLOOR token counter at 0 on a full run).
             */
            static int ar2_drop = -1;
            if (ar2_drop < 0) {
                const char *e = calypso_getenv("CALYPSO_AR2_FLOOR_DROP");
                ar2_drop = (e && *e == '1') ? 1 : 0;
            }
            if (calypso_debug_enabled("AR2-FLOOR"))
                C54_DBG("AR2-FLOOR",
                    "AR2=0x%04x < floor PC=0x%04x op=0x%04x BK=0x%04x A=%010llx insn=%u",
                    addr, s->pc, prog_fetch(s, s->pc), s->bk,
                    (unsigned long long)(s->a & 0xFFFFFFFFFFULL), s->insn_count);
            if (ar2_drop && addr < 0x0800)
                addr = 0xFFFF;  /* scratch address: keeps the write out of MMR space */
        }

        /* Post-modify */
        switch (mod) {
        case 0x0: /* *ARn */
            break;
        case 0x1: /* *ARn- */
            s->ar[cur_arp]--;
            break;
        case 0x2: /* *ARn+ */
            s->ar[cur_arp]++;
            break;
        case 0x3: /* *+ARn */
            addr = ++s->ar[cur_arp];
            break;
        /* MOD 4-11, canonical C54x order per binutils tic54x-dis.c:506-518:
         * 4=-0B 5=-0 6=+0 7=+0B 8=-% 9=-0% 10=+% 11=+0%.
         * Modes 4/7 are reverse-carry (bit-reversed) adds, used by the FB/SB
         * correlator; CALYPSO_ISA_BITREV=0 reverts them to a plain +/-AR0.
         * Circular modes 8-11 go through c54x_circ_ref, where BK=0 stays
         * LINEAR: the firmware issues STM #0,BK deliberately. */
        case 0x4: /* *ARn-0B - reverse carry */
            s->ar[cur_arp] = c54x_revcarry(s, s->ar[cur_arp], s->ar[0], 1);
            break;
        case 0x5: /* *ARn-0 */
            s->ar[cur_arp] -= s->ar[0];
            break;
        case 0x6: /* *ARn+0 */
            s->ar[cur_arp] += s->ar[0];
            break;
        case 0x7: /* *ARn+0B - reverse carry */
            s->ar[cur_arp] = c54x_revcarry(s, s->ar[cur_arp], s->ar[0], 0);
            break;
        case 0x8: /* *ARn-% (circular -1) */
            s->ar[cur_arp] = c54x_circ_ref(s->ar[cur_arp], -1, s->bk);
            break;
        case 0x9: /* *ARn-0% (circular -AR0) */
            s->ar[cur_arp] = c54x_circ_ref(s->ar[cur_arp], -(int16_t)s->ar[0], s->bk);
            break;
        case 0xA: /* *ARn+% (circular +1) */
            s->ar[cur_arp] = c54x_circ_ref(s->ar[cur_arp], +1, s->bk);
            break;
        case 0xB: /* *ARn+0% (circular +AR0) */
            s->ar[cur_arp] = c54x_circ_ref(s->ar[cur_arp], +(int16_t)s->ar[0], s->bk);
            break;
        /* Indirect modes 12..15 use a long-immediate operand from the next
         * program word. Encoding per tic54x-dis.c (MOD field = bits 6:3 of
         * the smem byte) and SPRU131G Table 5-9:
         *   12 : *AR(x)(lk)        - addr = AR(x) + lk, NO modify
         *   13 : *+AR(x)(lk)       - premod: AR(x) += lk; addr = AR(x)
         *   14 : *+AR(x)(lk)%      - premod circular: AR(x) = circ(AR(x)+lk)
         *   15 : *(lk)             - ABSOLUTE long address (lk itself)
         *
         * MOD=15 is an ABSOLUTE address, not AR(x)+lk: the PROM0 bootloader
         * at 0xb429 reads BL_ADDR_LO with `LDU *(0x0ffe), A`, and decoding it
         * as AR-relative yields AR(x)+0x0ffe and loads garbage. */
        case 0xC: /* *AR(x)(lk) */
            addr = s->ar[cur_arp] + prog_fetch(s, s->pc + 1);
            s->lk_used = true;
            break;
        case 0xD: /* *+AR(x)(lk) */
            s->ar[cur_arp] += prog_fetch(s, s->pc + 1);
            addr = s->ar[cur_arp];
            s->lk_used = true;
            break;
        case 0xE: { /* *+AR(x)(lk)% - circular */
            /* Same rule as modes 8-11: c54x_circ_ref (base on a 2^N boundary,
             * index = AR & (2^N-1)). A "base = AR - AR % BK" grid would be wrong
             * for the xCCH deinterleaver (PROM0 0x9a15/0x9a34/0x9a59
             * `mar *+AR4(57)%`, BK=456, buffer 0x2a00): AR4=0x2aab+57 must give
             * 0x2ae4, not 0x291c, or the odd-nibble half of every 456-bit
             * block landed below the buffer and the Viterbi saw stale words. */
            uint16_t lk = prog_fetch(s, s->pc + 1);
            s->ar[cur_arp] = c54x_circ_ref(s->ar[cur_arp], (int16_t)lk, s->bk);
            addr = s->ar[cur_arp];
            s->lk_used = true;
            break;
        }
        case 0xF: /* *(lk) - absolute address */
            addr = prog_fetch(s, s->pc + 1);
            s->lk_used = true;
            break;
        }

        /* MOD-MISMATCH probe: recompute the silicon-correct AR in parallel and
         * compare. Pure observation, it must NOT alter execution. Reference:
         * tic54x-dis.c:506-518 for the canonical MOD order. Diagnostic only,
         * to be removed. */
        {
            int16_t  a0  = (int16_t)s->ar[0];
            uint16_t bk  = s->bk;
            uint16_t sil;               /* AR value expected on silicon */
            switch (mod) {
            case 0x0: sil = ar_before;                       break; /* *ar      */
            case 0x1: sil = (uint16_t)(ar_before - 1);       break; /* *ar-     */
            case 0x2: sil = (uint16_t)(ar_before + 1);       break; /* *ar+     */
            case 0x3: sil = (uint16_t)(ar_before + 1);       break; /* *+ar     */
            case 0x4: sil = (uint16_t)(ar_before - a0);      break; /* *ar-0B   */
            case 0x5: sil = (uint16_t)(ar_before - a0);      break; /* *ar-0    */
            case 0x6: sil = (uint16_t)(ar_before + a0);      break; /* *ar+0    */
            case 0x7: sil = (uint16_t)(ar_before + a0);      break; /* *ar+0B   */
            case 0x8: sil = c54x_circ_ref(ar_before, -1,  bk); break; /* *ar-%  */
            case 0x9: sil = c54x_circ_ref(ar_before, -a0, bk); break; /* *ar-0% */
            case 0xA: sil = c54x_circ_ref(ar_before, +1,  bk); break; /* *ar+%  */
            case 0xB: sil = c54x_circ_ref(ar_before, +a0, bk); break; /* *ar+0% */
            default:  sil = s->ar[cur_arp];                  break; /* 12-15 lk : skip */
            }
            /* which MOD encodings the firmware actually uses (first hit each) */
            static uint16_t mod_seen = 0;
            if (!(mod_seen & (1u << mod))) {
                mod_seen |= (1u << mod);
                fprintf(stderr, "[c54x] MOD-FIRSTHIT mod=%2d AR%d PC=0x%04x op=0x%04x insn=%u\n",
                        mod, cur_arp, s->pc, opcode, s->insn_count);
            }
            /* silicon vs model divergence (modes 0..11 only) */
            if (mod <= 0xB && sil != s->ar[cur_arp]) {
                static uint32_t mm_n[16] = {0};
                if (mm_n[mod] < 8)
                    fprintf(stderr, "[c54x] MOD-MISMATCH mod=%2d AR%d ar0=0x%04x bk=0x%04x "
                            "base=0x%04x qemu=0x%04x silicon=0x%04x PC=0x%04x op=0x%04x insn=%u\n",
                            mod, cur_arp, (uint16_t)a0, bk, ar_before,
                            s->ar[cur_arp], sil, s->pc, opcode, s->insn_count);
                mm_n[mod]++;
            }
        }

        /* Update ARP - only in compatibility mode (ST1.CMPT = 1, SPRU131G 5.4.1);
         * with CMPT = 0 the ARP is left alone (manual example MAR *AR3+, ARP stays 0). */
        if ((s->st1 & ST1_CMPT) && !cmpt_ar0)
            s->st0 = (s->st0 & ~ST0_ARP_MASK) | (nar << ST0_ARP_SHIFT);

        return addr;
    } else {
        /* Direct addressing. SPRU131G 5.3: CPL=0 -> dma = DP:offset (DP<<7 | 7 bits);
         * CPL=1 -> dma = SP + offset. CPL matters: the ROM runs its C tasks with
         * CPL=1 (0x71d5 `stm #0x6900, ST1` before `cala A`) and clears it for the
         * hand-written DSP kernels (0x7c20 `rsbx CPL`), so the SP-relative locals
         * of the C parts depend on it. */
        *indirect = false;
        uint16_t offset = opcode & 0x7F;
        if (s->st1 & ST1_CPL) return (uint16_t)(s->sp + offset);
        return (dp(s) << 7) | offset;
    }
}

/* Resolve an Lmem (long-word, 32-bit) operand for the dual long-word family
 * (DADD/DSUB/DLD/DRSUB/DADST/DSUBT/DSADT, 0x50-0x5F). Returns the even-aligned
 * base address (data[addr]=high, data[addr+1]=low) and applies the LONG-operand
 * post-modify: the implicit unit step is 2 words, NOT 1 (SPRU172C, e.g. the
 * DADST example: "long-operand instruction, AR incremented/decremented by 2").
 * AR0-indexed and long-offset (lk) steps use their value as-is. Mirrors
 * resolve_smem's MOD field decode (bits 6:3). */
uint16_t resolve_lmem(C54xState *s, uint16_t opcode)
{
    /* The address is returned AS IS: the C54x reads the high word at the
     * given address and the low word at address ^ 1 (SPRU172C DADD example,
     * AR3 = 0101h: hi = data[0101h], lo = data[0100h]). Forcing the address
     * even would swap the halves of every odd-addressed long operand. */
    if (!(opcode & 0x80)) {
        /* Direct - dmad pair, no post-mod. CPL selects SP- or DP-relative. */
        uint16_t off = opcode & 0x7F;
        if (s->st1 & ST1_CPL) return (uint16_t)(s->sp + off);
        return (uint16_t)(((s->st0 & ST0_DP_MASK) << 7) | off);
    }
    int mod = (opcode >> 3) & 0x0F;
    int nar = opcode & 0x07;
    uint16_t addr = s->ar[nar];
    switch (mod) {
    case 0x0: break;                                              /* *ARn      */
    case 0x1: s->ar[nar] -= 2; break;                             /* *ARn-     */
    case 0x2: s->ar[nar] += 2; break;                             /* *ARn+     */
    case 0x3: s->ar[nar] += 2; addr = s->ar[nar]; break;          /* *+ARn     */
    case 0x4: s->ar[nar] = c54x_revcarry(s, s->ar[nar], s->ar[0], 1); break; /* *ARn-0B */
    case 0x5: s->ar[nar] -= s->ar[0]; break;                                 /* *ARn-0  */
    case 0x6: s->ar[nar] += s->ar[0]; break;                                 /* *ARn+0  */
    case 0x7: s->ar[nar] = c54x_revcarry(s, s->ar[nar], s->ar[0], 0); break; /* *ARn+0B */
    case 0x8: s->ar[nar] = c54x_circ_ref(s->ar[nar], -2, s->bk); break;                 /* *ARn-%  */
    case 0x9: s->ar[nar] = c54x_circ_ref(s->ar[nar], -(int16_t)s->ar[0], s->bk); break; /* *ARn-0% */
    case 0xA: s->ar[nar] = c54x_circ_ref(s->ar[nar], +2, s->bk); break;                 /* *ARn+%  */
    case 0xB: s->ar[nar] = c54x_circ_ref(s->ar[nar], +(int16_t)s->ar[0], s->bk); break; /* *ARn+0% */
    case 0xC: addr = (uint16_t)(s->ar[nar] + prog_fetch(s, s->pc + 1)); s->lk_used = true; break;
    case 0xD: s->ar[nar] += prog_fetch(s, s->pc + 1); addr = s->ar[nar]; s->lk_used = true; break;
    case 0xE: { uint16_t lk = prog_fetch(s, s->pc + 1);
                s->ar[nar] = c54x_circ_ref(s->ar[nar], (int16_t)lk, s->bk);
                addr = s->ar[nar]; s->lk_used = true; break; }
    case 0xF: addr = prog_fetch(s, s->pc + 1); s->lk_used = true; break;
    }
    return addr;
}

/* SP ledger for the IRQ push/pop asymmetry diagnostic. c54x_run counts
 * pushes and pops from the sign of the SP delta; c54x_interrupt_ex counts
 * IRQ entries with their word count. A net_words that drifts instead of
 * staying near 0 means an asymmetry, e.g. IRQ entry pushing one word while
 * FRET pops two, which walks SP until it wraps. */
struct c54x_g_sp_ledger_s g_sp_ledger;

/* Xmem operand decode, per the XMEM/XMOD/XARX macros of binutils tic54x.h:
 *   XMEM(OP) = opcode bits [7:4], the 4-bit Xmem nibble
 *   XMOD     = nibble bits [3:2] : 0=*AR, 1=*AR-, 2=*AR+, 3=*AR+0%
 *   XARX     = nibble bits [1:0] + 2, so AR2..AR5 only
 *
 * Xmem is INDIRECT-ONLY: unlike Smem it has no DP-relative direct mode.
 * Decoding an Xmem operand with resolve_smem reads the low byte as Smem
 * direct addressing whenever bit 7 is clear and lands the write in MMR space
 * (0x00-0x1F); one such write (PC=0x8a46, op=0x9918, STL B,*AR2) took
 * SP from 0x4800 to 0x0000 and then IMR to 0, idling the DSP for good.
 *
 * xmod=3 (*AR+0%) is CIRCULAR modulo BK via c54x_circ_ref, with BK=0 staying
 * linear; a plain `addr + AR0` here drifts over 16 bits and runs AR2 away.
 * The same rule applies to the other dual-operand handlers (MVDD, MAC D0-D9,
 * MASA DB, SQDST DC, ST||LD C8-CB).
 *
 * Beware: the 1/2 sign convention is NOT uniform across those handlers
 * (MVDD has 1=- 2=+, MAC has 1=+ 2=-). That inconsistency is unresolved. */
uint16_t resolve_xmem(C54xState *s, uint16_t op)
{
    uint8_t xmem  = (op >> 4) & 0xF;
    int     xar   = (xmem & 0x3) + 2;
    int     xmod  = (xmem & 0xC) >> 2;
    uint16_t addr = s->ar[xar];
    switch (xmod) {
    case 0: break;
    case 1: s->ar[xar] = addr - 1; break;
    case 2: s->ar[xar] = addr + 1; break;
    case 3: s->ar[xar] = c54x_circ_ref(addr, +(int16_t)s->ar[0], s->bk); break; /* *AR+0%, circular modulo BK (BK=0 -> linear) */
    }
    return addr;
}

/* PC ring buffer for the pre-IDLE trace. */
uint16_t pc_ring[256];
int pc_ring_idx = 0;

/* Evaluate a C54x condition from the LOW byte of the opcode, per the
 * condition_codes[] table of binutils opcodes/tic54x-opc.c:
 *   CC1=0x40 selects an accumulator test, CCB=0x08 selects B instead of A,
 *   test bits[2:0] = EQ 5, NEQ 4, LT 3, LEQ 7, GT 6, GEQ 2;
 *   AOV=0x70 ANOV=0x60; TC=0x30 NTC=0x20; C=0x0C NC=0x08; UNC=0x00.
 * The condition lives in the low byte, not in (op>>4)&0xF: that field only
 * agrees for UNC and AEQ, and getting it wrong flips conditional calls, so
 * pushes go missing and SP is over-popped into a runaway return address. */
bool c54x_cond_true(C54xState *s, uint8_t cc)
{
    if (cc == 0x00) return true;                       /* UNC */
    if (cc & 0x40) {                                   /* CC1 : test accu */
        int64_t acc = (cc & 0x08) ? sext40(s->b) : sext40(s->a);
        bool ov = (cc & 0x08) ? !!(s->st0 & (1 << 9))  /* OVB */
                              : !!(s->st0 & (1 << 8));  /* OVA */
        /* OVA/OVB are cleared once a conditional tests them */
        if ((cc & 0x70) == 0x70) { s->st0 &= (cc & 0x08) ? ~(1 << 9) : ~(1 << 8); return ov; }    /* AOV/BOV  */
        if ((cc & 0x70) == 0x60) { s->st0 &= (cc & 0x08) ? ~(1 << 9) : ~(1 << 8); return !ov; }   /* ANOV/BNOV */
        switch (cc & 0x07) {
        case 0x05: return acc == 0;                    /* EQ  */
        case 0x04: return acc != 0;                    /* NEQ */
        case 0x03: return acc <  0;                    /* LT  */
        case 0x07: return acc <= 0;                    /* LEQ */
        case 0x06: return acc >  0;                    /* GT  */
        case 0x02: return acc >= 0;                    /* GEQ */
        default:   return true;
        }
    }
    /* Group 2 conditions combine: binutils lets TC/NTC (0x30/0x20),
     * C/NC (0x0C/0x08) and BIO/NBIO (0x03/0x02) appear together (`bc 1000h,tc,nc,bio`,
     * SPRU172C BC example, isa_test 28) and the instruction runs only when ALL hold.
     * BIO is not wired: the pin reads high (inactive), so BIO is false and NBIO true. */
    bool r = true;
    if ((cc & 0x30) == 0x30) r = r &&  (s->st0 & ST0_TC);
    if ((cc & 0x30) == 0x20) r = r && !(s->st0 & ST0_TC);
    if ((cc & 0x0C) == 0x0C) r = r &&  (s->st0 & ST0_C);
    if ((cc & 0x0C) == 0x08) r = r && !(s->st0 & ST0_C);
    if ((cc & 0x03) == 0x03) r = false;
    return r;
}

/* Per-instruction interrupt LEVEL check, gated by CALYPSO_C54X_IRQ_LEVEL.
 * The base model only services interrupts at the c54x_interrupt_ex call edge,
 * so an IFR bit latched while INTM=1 is never taken afterwards. Real silicon
 * re-checks pending unmasked interrupts at every instruction boundary.
 *
 * This matters for the frame IT (bit12 / vector 28), which c54x_interrupt_ex
 * raises as an EDGE once per frame. The firmware keeps INTM=1 nearly all the
 * time (wait loop at 0xdde6 plus the critical sections around 0xb52x), and its
 * 5-instruction INTM=0 window rarely coincides with bit12 being pending:
 * vector 28 was dispatched 80 times over ~36000 frames, starving the FB
 * kernel. Two gated workarounds follow from that measurement:
 *  - LEVEL: hold bit12 asserted in the IFR, re-asserting it every instruction,
 *    until vector 28 actually vectors, so the next INTM 1->0 transition cannot
 *    miss it.
 *  - PRIO: when bit12 and a lower-priority bit (e.g. bit5/BRINT0) are pending
 *    in the same window, take bit12 first instead of the raw ctz, otherwise
 *    BRINT0 steals the rare window and re-masks with INTM=1. */
bool g_frame_it_level = false;

/* IFR-CLEAR-WHO: name every site that clears an IFR bit.
 *
 * All IFR clear sites funnel through here and pass their own name, because a
 * pending interrupt can vanish before the INTM=0 window opens: the window is
 * open 99% of the time, yet `intm0+pend` measures 0.
 *
 * Only UNMASKED bits (IMR set) are logged; clearing a masked bit is harmless.
 * A clear from a "vector-*" site is legitimate, the interrupt was serviced.
 * A clear from "mmio-write" on an unmasked, unserviced bit is a lost
 * interrupt. Gated by CALYPSO_IFR_CLEAR_WHO (default on), log rate capped.
 */
void c54x_ifr_clear(C54xState *s, uint16_t mask, const char *site)
{
    uint16_t before = s->ifr;
    s->ifr &= (uint16_t)~mask;
    uint16_t perdus = (uint16_t)(before & ~s->ifr & s->imr);   /* unmasked bits only */
    if (!perdus) {
        return;
    }
    static int en = -1;
    if (en < 0) {
        en = calypso_gate("CALYPSO_IFR_CLEAR_WHO", 1);
        if (en)
            fprintf(stderr, "[c54x] IFR-CLEAR-WHO arme : tout effacement d'un bit "
                    "DEMASQUE de l'IFR est nomme par son site\n");
    }
    if (!en) {
        return;
    }
    static unsigned long long par_bit[16];
    static unsigned nlog = 0;
    for (int b = 0; b < 16; b++) {
        if (!(perdus & (1u << b))) {
            continue;
        }
        par_bit[b]++;
        /* bit 5 = BRINT0 (I/Q delivery by the BSP) and bit 12 = frame are the
         * two bits of interest: log the first 20 of each, then periodically. */
        bool cible = (b == 5 || b == 12);
        if ((cible && par_bit[b] <= 20) || (par_bit[b] % 5000) == 0) {
            nlog++;
            fprintf(stderr, "[c54x] IFR-CLEAR-WHO bit=%d%s #%llu site=%s "
                    "IFR 0x%04x->0x%04x IMR=0x%04x INTM=%d PC=0x%04x insn=%u\n",
                    b, (b == 5) ? "(BRINT0)" : (b == 12) ? "(frame)" : "",
                    (unsigned long long)par_bit[b], site, before, s->ifr, s->imr,
                    (s->st1 & ST1_INTM) ? 1 : 0, s->pc, s->insn_count);
        }
    }
    if ((nlog % 200) == 199)
        fprintf(stderr, "[c54x] IFR-CLEAR-WHO resume : bit5=%llu bit12=%llu insn=%u\n",
                (unsigned long long)par_bit[5], (unsigned long long)par_bit[12],
                s->insn_count);
}

4.42 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/c54x_exec.c

423877 octets, 7542 lignes → 7525 lignes (7 groupes compactés)

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * c54x_exec.c - execution core: c54x_exec_one and its instruction families.
 *
 * Split out of calypso_c54x.c on 2026-09-18 (one file per role).
 * File map in c54x_internal.h.
 */
#include "c54x_internal.h"
#include "calypso_a5.h"
#include "hw/arm/calypso/calypso_debug.h"

/* 0xF4xx source/destination selectors: bit 9 = src, bit 8 = dst (TI SPRU172C). */
/* NORM AND TC. SPRU172C: NORM shifts the accumulator by T and does not touch
 * TC. The ROM does `bitf *ar2,#8000h ; ld #1,a ; norm a ; bc ...,ntc` (PROM0
 * 0x909c, uplink speech frame marker): a NORM clearing TC would mean the
 * marker is never set and the 0xb711 gate builds no speech burst.
 * CALYPSO_NORM_TC=0: NORM does not touch TC (silicon). Default: NORM sets TC
 * on bit39 != bit38 (legacy model). */
/* BC heuristic probe (CALYPSO_BCTC_SONDE=1): see the BC F82x/F83x. */
static struct { uint16_t pc, prev; unsigned long n, divergent; } bctc_sites[256];
static int bctc_nsites;
static void bctc_sonde_note(uint16_t pc, uint16_t prev, bool diverge)
{
    int i;
    for (i = 0; i < bctc_nsites && bctc_sites[i].pc != pc; i++) { }
    if (i == bctc_nsites) { if (bctc_nsites == 256) return; bctc_sites[bctc_nsites].pc = pc; bctc_sites[bctc_nsites].prev = prev; bctc_nsites++; }
    bctc_sites[i].n++;
    if (diverge) bctc_sites[i].divergent++;
}
/* RELIABLE TC ACROSS A MEMORY WRITE. CMPM/BITF (0x60/0x61) set TC; ANDM/ORM/XORM/ADDM #lk
 * (0x68..0x6B) and ST/STM #k (0x76/0x77) do not touch it (SPRU172C). A bc/bcd tc|ntc right after
 * one of those writes, itself right after the CMPM/BITF, therefore tests the real TC. PROM0 0xb81e
 * `bitf *ar2(9),#8000h` (d_ctrl_tch B_SUBCHANNEL); 0xb821 `andm #-17,*(0x3fbf)`; 0xb824
 * `bc 0xb82f,ntc`: with the ACC heuristic (A=0 -> no jump) `orm #256,*(0x3fab)` would run on every
 * TCH/F frame -> TCH/H sub-channel state table (0xde57), table overrun at the 1st multiframe (list
 * 0xbc5a, call 0x3f8c), DSP reset, Assignment Failure. CALYPSO_C54X_BCTC_RMW=0 falls back to the
 * heuristic. */
/* A single interposed write is not enough: PROM0 0xb925 `bitf *(0x3fab),#256`; `st #de57,*(0x4366)`;
 * `st #decd,*(0x4367)`; 0xb92e `bc 0xb936,tc` -- the heuristic (tc: jump if A == 0, A = fn%26) would
 * keep base 0xde57 at every fn%26 = 0: same table overrun. g_tc_fiable follows the chain
 * (calypso_c54x.c). */
int g_tc_fiable;
static int c54x_tc_par_rmw(void)
{
    static int on = -1;
    if (on < 0) on = calypso_gate("CALYPSO_C54X_BCTC_RMW", 1);
    return on && g_tc_fiable;
}
static void bctc_sonde_resume(void)
{
    for (int i = 0; i < bctc_nsites; i++)
        if (bctc_sites[i].divergent)
            printf("BCTC-SONDE pc=%04x prev_op=%04x executions=%lu divergences=%lu\n", bctc_sites[i].pc,
                   bctc_sites[i].prev, bctc_sites[i].n, bctc_sites[i].divergent);
    fflush(stdout);
}

/* BANZ[D] test value (CALYPSO_BANZ_LKADDR, default 1): for MOD 12..15 the
 * address generator output (ARx+lk, pre-modified ARx, or lk) ; otherwise the
 * pre-modify ARx as before (identical to the generated address for MOD 0..11
 * except *+ARx). =0 restores the old "always ARx" test. */
static uint16_t c54x_banz_value(uint16_t op, uint16_t pre, uint16_t ea)
{
    static int g = -1;
    if (g < 0) g = calypso_gate("CALYPSO_BANZ_LKADDR", 1);
    if (g && (op & 0x80) && ((op >> 3) & 0x0F) >= 0xC) return ea;
    return pre;
}

/* [2026-10-15] FAR call frame order (CALYPSO_FAR_STACK_SPRU, default 1).
 * SPRU172C FCALL/FCALLD/FCALA/FCALAD: (SP)-1->SP, ret PC->TOS, (SP)-1->SP,
 * XPC->TOS ; FRET/FRETD/FRETE/FRETED: TOS->XPC, SP+1, TOS->PC, SP+1. XPC is on
 * TOP, as in the interrupt entry frame (calypso_c54x.c pushes PC then XPC) and
 * as FRETD/FRETED (F6E4/F6E5) already popped. The legacy order (PC on top,
 * popped first by FRET) was symmetric for FCALL/FRET pairs only; =0 restores it. */
static int c54x_far_spru(void)
{
    static int g = -1;
    if (g < 0) g = calypso_gate("CALYPSO_FAR_STACK_SPRU", 1);
    return g;
}
static void c54x_far_push(C54xState *s, uint16_t ret_pc)
{
    if (c54x_far_spru()) {
        s->sp = (s->sp - 1) & 0xFFFF; data_write(s, s->sp, ret_pc);
        s->sp = (s->sp - 1) & 0xFFFF; data_write(s, s->sp, s->xpc);
    } else {
        s->sp = (s->sp - 1) & 0xFFFF; data_write(s, s->sp, s->xpc);
        s->sp = (s->sp - 1) & 0xFFFF; data_write(s, s->sp, ret_pc);
    }
}

static void norm_tc(C54xState *s, int bit39, int bit38)
{
    static int garder = -1;
    if (garder < 0) {
        const char *e = calypso_getenv("CALYPSO_NORM_TC");
        garder = !(e && *e == '0');
        if (!garder) fprintf(stderr, "[c54x] NORM ne touche plus TC (CALYPSO_NORM_TC=0)\n");
    }
    if (!garder) return;
    if (bit39 != bit38) s->st0 |= ST0_TC;
    else                s->st0 &= ~ST0_TC;
}

static inline void c54x_f4_srcdst(uint16_t op, int *src, int *dst)
{
    *src = (op >> 9) & 1;
    *dst = (op >> 8) & 1;
}

/* [2026-10-15] NEG/ABS overflow, SPRU172C (NEG, ABS): "Affected by OVM,
 * affects OVdst". The generic post-instruction OVM pass (calypso_c54x.c,
 * CALYPSO_C54X_OVM) only looks at accumulators whose value CHANGED, so it
 * misses the two fixed points of these instructions: NEG 80 0000 0000h
 * (-(-2^39) wraps to itself: OVdst=1, and 00 7FFF FFFFh with OVM=1) and ABS
 * of a positive value above 7FFF FFFFh (|src| = src: OVdst=1, saturated with
 * OVM=1). isa_test 3, 149, 150. Same switch as the generic pass. */
static inline void c54x_neg_abs_ovf(C54xState *s, int dst, int64_t r)
{
    static int on = -1;
    if (on < 0) { const char *e = getenv("CALYPSO_C54X_OVM"); on = (e && *e == '0') ? 0 : 1; }
    int64_t *acc = dst ? &s->b : &s->a;
    *acc = sext40(r);
    if (!on || (s->st1 & ST1_C16)) return;
    if (r > 0x7FFFFFFFLL || r < -0x80000000LL) {
        s->st0 |= dst ? ST0_OVB : ST0_OVA;
        if (s->st1 & ST1_OVM) *acc = (r < 0) ? -0x80000000LL : 0x7FFFFFFFLL;
    }
}

/* ═══════════════════════════════════════════════════════════════════════════
 * FIX_DECODE_BRANCH — MAC/bit handlers made reachable.
 *
 * These handlers were written under `case 0xF:` of switch(hi4) while their
 * opcodes have hi4 = 2 or 3, so they were dead by construction and the opcodes
 * fell through to the blind MAC of `case 0x3:` (`A = A + T*Smem`) or to
 * `case 0x2:`.
 *
 * [2026-08-04] sweep_reach.py: 161 handlers scanned, 11 unreachable, all of
 * them moved here: 0x2800 MAC, 0x2A00/0x2E00 MACR/MASR, 0x3000 LD Smem,T,
 * 0x3100 MPYA, 0x3200 LD Smem,ASM, 0x3300 MASA, 0x3400 BITT, 0x3500 MACA,
 * 0x3700 MACAR.
 *
 * ⚠️ The bodies are verbatim: only reachability is fixed, not the logic, so
 * every mask subtlety that existed before still exists. In particular
 * `0x2800/FC00` spans 0x2800..0x2BFF and therefore swallows the `0x2A00`
 * (MACR) test that follows it; that overlap predates this function and still
 * has to be checked against SPRU172C.
 *
 * ⚠️ Called BEFORE the `resolve_smem` of each case: every handler does its
 * own. Calling it afterwards would double post-increment the ARs.
 *
 * Returns -1 when the opcode is not handled, else the words consumed.
 * ═══════════════════════════════════════════════════════════════════════════ */
static int c54x_mac_ancien(void)
{   /* MACR/MASR/MACA[R]/MASA/MPYA per SPRU172C (rounding of the RESULT, A(32-16) on 17 bits,
     * 40-bit product, MAC mask 0xFE00 which gives MACR its own handler): isa_test 147 -> 152.
     * OPT-IN (CALYPSO_MAC=1): on the bench the strict variant raises the voice noise floor
     * (+9.4 dB with, +59.6 dB without, same binary) while the downlink speech decoded by the
     * ROM is bit-identical to the replay: a downstream defect, outside the replayed channel.
     * CALYPSO_MAC_ANCIEN=1 forces the legacy variant (default). */
    static int v = -1;
    if (v < 0) {
        const char *e = getenv("CALYPSO_MAC_ANCIEN"), *n = getenv("CALYPSO_MAC");
        v = (e && *e == '1') ? 1 : (n && *n == '1') ? 0 : 1;
    }
    return v;
}
/* SPRU172C MACA/MASA/MPYA : « A(32-16) is used as a 17-bit operand for the multiplier ». */
static inline int64_t c54x_ahi17(const C54xState *s)
{
    int64_t h = (s->a >> 16) & 0x1FFFF;
    return (h & 0x10000) ? h - 0x20000 : h;
}
static int c54x_mac_bit_family(C54xState *s, uint16_t op, int consumed)
{
            /* MAC/MAS family Smem,SRC (0x28xx..0x2Fxx, mask FE00, 1 word).
             * Per tic54x-opc.c:
             *   0x2800 mac Smem,SRC      SRC = SRC + T * data[Smem]
             *   0x2A00 macr Smem,SRC     SRC = SRC + T * data[Smem] + 0x8000
             *   0x2C00 mas Smem,SRC      SRC = SRC - T * data[Smem]
             *   0x2E00 masr Smem,SRC     SRC = SRC - T * data[Smem] + 0x8000
             * bit 8 = SRC selector (0=A, 1=B). FRCT (ST1) shifts the product
             * left by one (Q15*Q15 -> Q31).
             * Smem forms only; the dual-MAC Xmem/Ymem variants
             * (0xA000..0xBFFF) are not covered. */
            /* [2026-10-03] binutils : mac 0x2800/0xFE00, macr 0x2A00/0xFE00 (mas 0x2C00, masr 0x2E00).
             * The old mask 0xFC00 took 0x2A (MACR) here with bit 9 read as "subtract": MACR ran as
             * an unrounded MAS (isa_test 104). Only MAC itself belongs here now ; CALYPSO_MAC_ANCIEN=1
             * restores the old mask. */
            if ((op & (c54x_mac_ancien() ? 0xFC00 : 0xFE00)) == 0x2800) {
                int mac_sub = (op >> 9) & 1;       /* 0=add, 1=subtract */
                int mac_rnd = (op >> 8) & 0; /* not used here, separate below */
                (void)mac_rnd;
                bool mac_ind;
                uint16_t mac_addr = resolve_smem(s, op, &mac_ind);
                int16_t mac_mem = (int16_t)data_read(s, mac_addr);
                int32_t mac_prod = (int32_t)(int16_t)s->t * (int32_t)mac_mem;
                if (s->st1 & ST1_FRCT) mac_prod <<= 1;
                /* 0x2800/FE00 encoding per tic54x-opc.c: bits 15..9 op family
                 * (mac/mas/macr/masr), bit 8 SRC (0=A, 1=B), bits 7..0 Smem. */
                int mac_dst = (op >> 8) & 1;
                int64_t *mac_acc = mac_dst ? &s->b : &s->a;
                int64_t mac_term = (int64_t)(int32_t)mac_prod;
                if (mac_sub) mac_term = -mac_term;
                int64_t mac_new = sext40((*mac_acc + mac_term) & 0xFFFFFFFFFFULL);
                *mac_acc = mac_new;
                return (int)(consumed + s->lk_used);
            }
            /* MACR/MASR (mask FE00, base 0x2A00/0x2E00): MAC/MAS plus the
             * +0x8000 round, low half cleared. */
            if ((op & 0xFE00) == 0x2A00 || (op & 0xFE00) == 0x2E00) {
                int macr_sub = ((op & 0xFE00) == 0x2E00) ? 1 : 0;
                bool macr_ind;
                uint16_t macr_addr = resolve_smem(s, op, &macr_ind);
                int16_t macr_mem = (int16_t)data_read(s, macr_addr);
                int64_t macr_prod = (int64_t)(int16_t)s->t * (int64_t)macr_mem;
                if (s->st1 & ST1_FRCT) macr_prod <<= 1;
                int macr_dst = (op >> 8) & 1;
                int64_t *macr_acc = macr_dst ? &s->b : &s->a;
                if (c54x_mac_ancien()) {
                    macr_prod = (int64_t)(int32_t)((macr_prod + 0x8000) & ~0xFFFFLL);
                    *macr_acc = sext40((*macr_acc + (macr_sub ? -macr_prod : macr_prod)) & 0xFFFFFFFFFFULL);
                } else {   /* SPRU172C MACR/MASR : dst = rnd(src +/- T*Smem) -- the RESULT is rounded */
                    int64_t r = *macr_acc + (macr_sub ? -macr_prod : macr_prod);
                    *macr_acc = sext40((r + 0x8000) & ~0xFFFFLL);
                }
                return (int)(consumed + s->lk_used);
            }

            /* 0x3500 MACA Smem[,B] (mask FF00, 1 word): B = B + A.hi * data[Smem].
             * The multiplier is A.hi (A[31:16]), not T. */
            if ((op & 0xFF00) == 0x3500) {
                bool maca_ind;
                uint16_t maca_addr = resolve_smem(s, op, &maca_ind);
                int16_t maca_mem = (int16_t)data_read(s, maca_addr);
                int64_t maca_ahi = c54x_mac_ancien() ? (int16_t)((s->a >> 16) & 0xFFFF) : c54x_ahi17(s);
                int64_t maca_prod = maca_ahi * (int64_t)maca_mem;
                if (s->st1 & ST1_FRCT) maca_prod <<= 1;
                if (c54x_mac_ancien()) maca_prod = (int32_t)maca_prod;
                s->b = sext40((s->b + maca_prod) & 0xFFFFFFFFFFULL);
                s->t = (uint16_t)maca_mem;      /* SPRU172C: MACA Smem also loads T */
                return (int)(consumed + s->lk_used);
            }

            /* 0x3300 MASA Smem[,B] (mask FF00, 1 word): B = B - A.hi * data[Smem]. */
            if ((op & 0xFF00) == 0x3300) {
                bool masa_ind;
                uint16_t masa_addr = resolve_smem(s, op, &masa_ind);
                int16_t masa_mem = (int16_t)data_read(s, masa_addr);
                int64_t masa_ahi = c54x_mac_ancien() ? (int16_t)((s->a >> 16) & 0xFFFF) : c54x_ahi17(s);
                int64_t masa_prod = masa_ahi * (int64_t)masa_mem;
                if (s->st1 & ST1_FRCT) masa_prod <<= 1;
                if (c54x_mac_ancien()) masa_prod = (int32_t)masa_prod;
                s->b = sext40((s->b - masa_prod) & 0xFFFFFFFFFFULL);
                s->t = (uint16_t)masa_mem;      /* SPRU172C: MASA Smem also loads T */
                return (int)(consumed + s->lk_used);
            }

            /* 0x3700 MACAR Smem[,B] (mask FF00, 1 word): MACA plus round. */
            if ((op & 0xFF00) == 0x3700) {
                bool macar_ind;
                uint16_t macar_addr = resolve_smem(s, op, &macar_ind);
                int16_t macar_mem = (int16_t)data_read(s, macar_addr);
                if (c54x_mac_ancien()) {
                    int16_t macar_ahi = (int16_t)((s->a >> 16) & 0xFFFF);
                    int32_t macar_prod = (int32_t)macar_ahi * (int32_t)macar_mem;
                    if (s->st1 & ST1_FRCT) macar_prod <<= 1;
                    macar_prod += 0x8000;
                    macar_prod &= ~0xFFFF;
                    s->b = sext40((s->b + (int64_t)(int32_t)macar_prod) & 0xFFFFFFFFFFULL);
                } else {   /* SPRU172C MACAR : B = rnd(B + Smem * A(32-16)) -- the RESULT is rounded */
                    int64_t macar_prod = c54x_ahi17(s) * (int64_t)macar_mem;
                    if (s->st1 & ST1_FRCT) macar_prod <<= 1;
                    s->b = sext40((s->b + macar_prod + 0x8000) & ~0xFFFFLL);
                }
                s->t = (uint16_t)macar_mem;     /* SPRU172C: MACAR Smem also loads T */
                return (int)(consumed + s->lk_used);
            }

            /* 0x3100 MPYA Smem (mask FF00, 1 word): B = A.hi * data[Smem]. */
            if ((op & 0xFF00) == 0x3100) {
                bool mpya_ind;
                uint16_t mpya_addr = resolve_smem(s, op, &mpya_ind);
                int16_t mpya_mem = (int16_t)data_read(s, mpya_addr);
                int64_t mpya_ahi = c54x_mac_ancien() ? (int16_t)((s->a >> 16) & 0xFFFF) : c54x_ahi17(s);
                int64_t mpya_prod = mpya_ahi * (int64_t)mpya_mem;
                if (s->st1 & ST1_FRCT) mpya_prod <<= 1;
                if (c54x_mac_ancien()) mpya_prod = (int32_t)mpya_prod;
                s->b = sext40(mpya_prod);
                s->t = (uint16_t)mpya_mem;      /* SPRU172C: MPYA Smem also loads T */
                return (int)(consumed + s->lk_used);
            }

            /* [2026-08-23] MPY Smem,dst (0x2000/0xFE00) and LTD Smem
             * (0x4C00/0xFF00) were decoded nowhere, while the neighbouring
             * LD Smem,T (0x3000) and ST T,Smem (0x8C00) were.
             * Critical path: the loop feeding blocks 3 and 4 of the SCH bank is
             * 0x81e3 LD #1,ASM; 0x81e4 MPY Smem,dst; 0x81e5/0x81e6 ST A,*ARx+
             * (parallel stores). Without MPY, A stayed zero and the stores
             * overwrote the correlator's sane copies with zeros: 112 non-zero
             * writes from the MVDD at 0x7ce0/0x7ce4, then 210 zeros.
             * Semantics: MPY Smem,dst -> dst = T * Smem (bit 8 = accumulator)
             *            LTD Smem     -> T = Smem; data[Smem+1] = Smem
             * ⚠️ Global effect: alters the DSP instruction set as a whole. */
            if ((op & 0xFE00) == 0x2000) {
                bool mp_ind;
                uint16_t mp_addr = resolve_smem(s, op, &mp_ind);
                int16_t  mp_v = (int16_t)data_read(s, mp_addr);
                int64_t  mp_p = (int64_t)(int16_t)s->t * (int64_t)mp_v;
                if (s->st1 & ST1_FRCT) mp_p <<= 1;
                if ((op >> 8) & 1) s->b = sext40(mp_p);
                else               s->a = sext40(mp_p);
                return (int)(consumed + s->lk_used);
            }
            if ((op & 0xFF00) == 0x4C00) {
                bool lt_ind;
                uint16_t lt_addr = resolve_smem(s, op, &lt_ind);
                uint16_t lt_v = data_read(s, lt_addr);
                s->t = lt_v;
                data_write(s, (uint16_t)(lt_addr + 1), lt_v);  /* delay insertion */
                return (int)(consumed + s->lk_used);
            }

            /* 0x3000 LD Smem,T (mask FF00, 1 word): T = data[Smem]. */
            if ((op & 0xFF00) == 0x3000) {
                bool ldt_ind;
                uint16_t ldt_addr = resolve_smem(s, op, &ldt_ind);
                s->t = data_read(s, ldt_addr);
                return (int)(consumed + s->lk_used);
            }

            /* 0x3200 LD Smem,ASM (mask FF00, 1 word): ASM = data[Smem] & 0x1F. */
            if ((op & 0xFF00) == 0x3200) {
                bool ldasm_ind;
                uint16_t ldasm_addr = resolve_smem(s, op, &ldasm_ind);
                uint16_t ldasm_v = data_read(s, ldasm_addr) & ST1_ASM_MASK;
                s->st1 = (s->st1 & ~ST1_ASM_MASK) | ldasm_v;
                return (int)(consumed + s->lk_used);
            }

    return -1;
}


/* ═══════════════════════════════════════════════════════════════════════════
 * FIX_SFTA_CARRY — SFTA sets the carry.
 *
 * TI SPRU172C, SFTA page:
 *     If SHIFT < 0 : (src((-SHIFT)-1)) -> C ; src << SHIFT -> dst
 *                    high fill = src(39) when SXM=1, else 0
 *     Else         : (src(39 - SHIFT)) -> C ; src << SHIFT -> dst
 *     Status Bits  : Affected by SXM and OVM / Affects C and OVdst
 *
 * [2026-08-04] Without the carry, the DSP firmware bit-transfer loop at
 * 0x9ac7..0x9ace (`sfta A` -> carry, `rol B` -> B = B<<1 | C,
 * `stl *AR2+, B`) left B at 0, so `stl` wrote 0x0000 to 0x2c3c and the `mvdd`
 * at 0x9723 published that hole into a_cd[3..14], the 12 payload words the ARM
 * lifts up to L2. The real opcode is `f47f`: shift = 0x1F - 32 = -1, i.e. move
 * bit 0 of A into C.
 *
 * ⚠️ The SXM-dependent fill belongs to the same SPRU172C rule and is applied
 * here too; a right shift is arithmetic only when SXM=1.
 * ═══════════════════════════════════════════════════════════════════════════ */
/* [2026-09-21] Carry of the 40-bit adder, SPRU172C: ADD sets C on a carry out
 * of the accumulator, SUB (A + ~B + 1) resets C on a borrow. */
static inline void c54x_carry_add40(C54xState *s, int64_t a, int64_t b)
{
    uint64_t ua = (uint64_t)a & 0xFFFFFFFFFFULL, ub = (uint64_t)b & 0xFFFFFFFFFFULL;
    if (((ua + ub) >> 40) & 1) s->st0 |= ST0_C; else s->st0 &= ~ST0_C;
}
static inline void c54x_carry_sub40(C54xState *s, int64_t a, int64_t b)
{
    uint64_t ua = (uint64_t)a & 0xFFFFFFFFFFULL, ub = (uint64_t)b & 0xFFFFFFFFFFULL;
    if (ua >= ub) s->st0 |= ST0_C; else s->st0 &= ~ST0_C;
}

static void c54x_sfta_exec(C54xState *s, uint16_t op)
{
    int src, dst;
    c54x_f4_srcdst(op, &src, &dst);
    int shift = op & 0x1F;
    if (shift > 15) shift -= 32;
    int64_t sv = sext40(src ? s->b : s->a);

    /* [2026-09-21] left shift: C = the last bit shifted out of the 40-bit
     * accumulator, src(40 - SHIFT) - the SFTL rule (src(31-(SHIFT-1)), i.e.
     * 32 - SHIFT) transposed to 40 bits. Measured on the ROM's Fire-code LFSR
     * (PROM0 0xa168-0xa175: `sfta A,1` then `xc 1,C -> xor #0x0482,16,A /
     * xor @60(=9),A`): the 40-bit syndrome register only reaches zero on a
     * correct 224-bit block when C is the old bit 39; with bit 31 (or the
     * manual's literal src(39-SHIFT) = bit 38) every block reads FIRE1 and
     * the ARM drops it. The SPRU172C example (80AA001234 << 5 -> C = 1) fits
     * bit 39 or 27 but not 35, so isa_test 188 now fails: manual example vs
     * silicon, the ROM wins. */
    int cbit = (shift < 0) ? (int)((sv >> ((-shift) - 1)) & 1)
             : (shift > 0) ? (int)((sv >> (40 - shift)) & 1) : ((s->st0 & ST0_C) ? 1 : 0);
    if (cbit) s->st0 |= ST0_C;
    else      s->st0 &= ~ST0_C;

    if (shift >= 0) {
        sv <<= shift;
    } else if (!(s->st1 & ST1_SXM)) {
        sv = (int64_t)(((uint64_t)sv & 0xFFFFFFFFFFULL) >> (-shift));
    } else {
        sv >>= (-shift);
    }
    if (dst) s->b = sext40(sv);
    else     s->a = sext40(sv);
}


/* ═══════════════════════════════════════════════════════════════════════════
 * FIX_ROL_CARRY_BIT — ROR/ROL read the carry from the right bit.
 *
 * `ST0_C` is `(1 << 11)` (calypso_c54x.h), but the ROR/ROL handlers read
 * `(s->st0 >> 8) & 1`, i.e. bit 8, which belongs to `ST0_DP_MASK` (bits 8-0,
 * data page pointer), while writing the carry correctly through
 * `s->st0 |= ST0_C`. Read and write named different bits, so ROL rotated a
 * page-pointer bit instead of the carry.
 *
 * [2026-08-04] Measured: even with FIX_SFTA_CARRY posting the carry at bit 11,
 * the firmware loop at 0x9ac7..0x9ace (`sfta A` then `rol B`) kept B at zero,
 * `stl *AR2+, B` wrote 0x0000 to 0x2c3c and the `mvdd` at 0x9723 published that
 * hole into a_cd[3..14].
 * ═══════════════════════════════════════════════════════════════════════════ */
static inline uint16_t c54x_carry_in(C54xState *s)
{
    return (s->st0 & ST0_C) ? 1 : 0;
}

/* SFTL src, SHIFT [, dst] — SPRU172C 4-158. A 32-bit LOGICAL shift of src(31-0):
 *   SHIFT < 0 : src((-SHIFT)-1) -> C ; src(31-0) >> -SHIFT -> dst(31-0), zero fill
 *   SHIFT = 0 : 0 -> C ; dst(31-0) = src(31-0)
 *   SHIFT > 0 : src(32-SHIFT) -> C ; (src(31-0) << SHIFT) & 0xFFFFFFFF -> dst(31-0)
 *   always     0 -> dst(39-32)
 * [2026-09-20] The three former copies shifted the 40-bit accumulator and never
 * set C. Manual example SFTL A, -5, B: A = 00 8765 0055 -> B = 00 043B 2802, C = 1.
 * One body, called from every dispatch copy. */
static void c54x_sftl_exec(C54xState *s, uint16_t op)
{
    int src, dst;
    c54x_f4_srcdst(op, &src, &dst);
    int shift = op & 0x1F;
    if (shift > 15) shift -= 32;
    uint32_t v = (uint32_t)((src ? s->b : s->a) & 0xFFFFFFFFULL);
    uint32_t r; int c;
    if (shift < 0)      { c = (v >> (-shift - 1)) & 1; r = v >> (-shift); }
    else if (shift == 0){ c = 0;                        r = v; }
    else                { c = (v >> (32 - shift)) & 1;  r = v << shift; }
    if (c) s->st0 |= ST0_C; else s->st0 &= ~ST0_C;
    if (dst) s->b = (int64_t)r; else s->a = (int64_t)r;
}

/* AND/OR/XOR src,SHIFT,dst (F0xx..F3xx, mask FCE0, 1 word): the shift is LOGICAL over the whole 40-bit
 * accumulator (guard bits included, ZERO fill on a right shift). [2026-10-03] The three copies shifted the
 * sign-extended int64 with C's arithmetic >>, so a right shift of an accumulator whose bit 39 is set
 * (the uplink convolutional encoder keeps its data in the guard byte : PROM0 0x8ece `sfta a,8`, then
 * 0x8ec7..0x8eca `xor b,-6,a` `xor b,-8,a` `xor a,-1,a` `xor b,-3,a`) filled the vacated bits with ones.
 * Measured with injected one-bit L2 blocks: 77 of the 413 varying bits of the 0x4280 output buffer were
 * not the GSM 05.03 coded bits (a code tap missing at every word-boundary lane) ; with the zero fill the
 * second coded word of a block with data bit 28 set is 0xC000, as the standard encoder gives, instead
 * of 0x4000. A first attempt that also dropped the guard bits (32-bit shift) broke FB detection : the
 * guard byte takes part. CALYPSO_LOGIC40=1 restores the old arithmetic behaviour. */
static inline int c54x_logic40(void)
{
    static int v = -1;
    if (v < 0) { const char *e = getenv("CALYPSO_LOGIC40"); v = (e && *e == '1') ? 1 : 0; }
    return v;
}
static inline int64_t c54x_logic_shifted(int64_t acc, int shift)
{
    if (c54x_logic40()) return (shift >= 0) ? (acc << shift) : (acc >> (-shift));
    const uint64_t M = 0xFFFFFFFFFFULL;
    uint64_t v = (uint64_t)acc & M;
    uint64_t r = (shift >= 0) ? ((shift >= 40) ? 0 : (v << shift) & M) : ((-shift >= 40) ? 0 : v >> (-shift));
    return (int64_t)r;
}
static inline int64_t c54x_logic_result(int64_t r) { return r; }

/* LD/SUB Smem,16,dst (0x40-0x45): SPRU172C — the 16-bit operand is sign-extended into the guard bits only when
 * SXM=1 ; with SXM=0 it is zero-extended. [2026-10-03] Both handlers always sign-extended. Measured on the
 * uplink convolutional encoder (PROM0 0x8ec2 `ld *ar4+,16,a`, SXM=0, word 0x8000) : A came out 0xFF80000000
 * instead of 0x0080000000, and the guard ones fed by the XOR-with-shift ladder that follows put wrong bits in
 * the first coded word of every 16-bit chunk (one input bit of the L2 block -> 4 of the expected 7 coded bits).
 * CALYPSO_LD16_SEXT=1 restores the old behaviour. */
static inline int64_t c54x_smem16_shifted(C54xState *s, uint16_t w)
{
    static int old = -1;
    if (old < 0) { const char *e = getenv("CALYPSO_LD16_SEXT"); old = (e && *e == '1') ? 1 : 0; }
    int64_t v = (old || (s->st1 & ST1_SXM)) ? (int64_t)(int16_t)w : (int64_t)(uint16_t)w;
    return v << 16;
}

bool calypso_fix_enabled(const char *name)
{
    static char buf[1024];
    static int  init = 0;
    if (!init) {
        const char *e = calypso_getenv("CALYPSO_FIXES");
        snprintf(buf, sizeof(buf), "%s", e ? e : "");
        init = 1;
        if (buf[0])
            fprintf(stderr, "[c54x] CALYPSO_FIXES=%s (sas temporaire — effacer le gate "
                            "de chaque correctif confirme)\n", buf);
    }
    if (!buf[0]) return false;
    if (strcmp(buf, "all") == 0) return true;
    size_t n = strlen(name);
    const char *p = buf;
    while ((p = strstr(p, name)) != NULL) {
        char before = (p == buf) ? ',' : p[-1];
        char after  = p[n];
        if ((before == ',' || before == ' ') &&
            (after == '\0' || after == ',' || after == ' '))
            return true;
        p += n;
    }
    return false;
}

/* CAL000 §5.1: the DSP timer is TINT = IMR bit 3 = vector 19, not vec20/bit4
 * (RINT, SPI receive). The generic C54x table in SPRU131 lists FOUR external
 * lines before TINT where the Calypso has THREE, hence the off-by-one.
 *
 * ⚠️ Unmeasured behaviour change: the observed IMR (0x52ed) has bit 3 unmasked
 * and bit 4 masked, so the timer interrupt used to be dropped silently by
 * c54x_interrupt_ex (which honours the IMR) and is now really dispatched on
 * vec19 at every underflow. The vec19 ROM handler is a RETE stub, so the effect
 * should be benign, but RETE stack symmetry is a known weak spot: if the stack
 * drifts under load, look here first. */
void c54x_fire_tint(C54xState *s)
{
    c54x_interrupt_ex(s, C54X_IT_TINT_VEC, C54X_IT_TINT_BIT);
}

int c54x_exec_one(C54xState *s)
{
    if (c54x_irq_level_check(s)) {
        return 1;   /* per-instruction IRQ vectoring consumed this step */
    }
    uint16_t op = prog_fetch(s, s->pc);
    /* [fft-dp] the codec FFT (PDROM 0xf0d0..0xf19c, called by 0xc13c `ld #112,dp` -> fcall 0xf0c1 ->
     * 0xf439 -> call 0xf0d0) keeps its variables at DP+0x0b/0x16/0x17/0x18 and expects DP=0x70
     * (with DP=0 at 0xf15c the stack ends at SP=0x001f and the DSP is lost). At the entry and at
     * 0xf150, if DP is not 0x70: DP, SP, the stack and the path (once each). */
    if (s->xpc == 0 && (s->pc == 0xf0d0 || s->pc == 0xf150) && (s->st0 & 0x1ff) != 0x70) {
        static unsigned n_e, n_m;
        unsigned *n = (s->pc == 0xf0d0) ? &n_e : &n_m;
        if ((*n)++ < 3) {
            extern void c54x_anneau_vider_stdout(const char *etiquette, unsigned n_max);
            printf("  [fft-dp] pc=0x%04x DP=0x%03x (attendu 0x070) ST1=%04x SP=%04x pile=%04x %04x %04x %04x %04x %04x "
                   "IMR=%04x IFR=%04x insn=%u\n", s->pc, s->st0 & 0x1ff, s->st1, s->sp,
                   s->data[s->sp], s->data[(uint16_t)(s->sp + 1)], s->data[(uint16_t)(s->sp + 2)],
                   s->data[(uint16_t)(s->sp + 3)], s->data[(uint16_t)(s->sp + 4)], s->data[(uint16_t)(s->sp + 5)],
                   s->imr, s->ifr, s->insn_count);
            if (s->pc == 0xf0d0 || *n == 1) c54x_anneau_vider_stdout("fft-dp", 512);
        }
    }
    /* B1 probe (CALYPSO_B1): at the MAC kernel 0xa076, dump the correlator
     * reference table data[0x2c00..0x2c0f] plus a checksum, which tells whether
     * the boot copy 0x76f8 -> 0x2c00 ran or the table is still zero.
     * [2026-09-23] Sonde pure, derriere l'interrupteur C54X_SONDES. */
    if (C54X_SONDES) {
        static int _b1 = -1; static unsigned _b1n = 0;
        if (_b1 < 0) _b1 = calypso_gate("CALYPSO_B1", 0);
        if (_b1 && s->xpc == 0 && s->pc == 0xa076 && _b1n < 20) {
            _b1n++;
            uint32_t _ck = 0;
            for (int _i = 0; _i < 0x100; _i++) _ck += s->data[0x2c00 + _i];
            fprintf(stderr, "[c54x] B1 @0xa076 refTable[0x2c00..0f]=");
            for (int _i = 0; _i < 16; _i++) fprintf(stderr, "%04x ", s->data[0x2c00 + _i]);
            fprintf(stderr, "| cksum(2c00..2cff)=0x%08x insn=%u\n", _ck, s->insn_count);
        }
    }
    {
        /* @BEQUILLE — CORR_BANK  (CALYPSO_CORR_BANK, VALUE, default -1/OFF)
         *   masks   : the overlay/bank selection of the FB handler. s->xpc is
         *             OVERWRITTEN at every instruction in [0x8d00..0xa200]
         *             instead of the native dispatcher setting the right bank.
         *   remove  : once the CALA dispatcher at 0xb01e resolves the bank on its
         *             own (observed XPC == expected bank without forcing).
         *   TRAP    : the value "0" does NOT turn this off, it forces XPC=0.
         *             Only leaving the variable unset disables it.
         */
        static int cbk = -2;
        if (cbk == -2) { const char *e = calypso_getenv("CALYPSO_CORR_BANK");
                         cbk = (e && *e) ? atoi(e) : -1; }   /* -1 = off; 0..3 = forced XPC */
        if (cbk >= 0 && cbk <= 3 && s->pc >= 0x8d00 && s->pc <= 0xa200 && s->xpc != (uint16_t)cbk) {
            s->xpc = (uint16_t)cbk;
        }
    }
    {
        /* @BEQUILLE — FORCE_3FAE  (CALYPSO_FORCE_3FAE, EXISTS, default OFF)
         *   masks   : the FB handshake flags that nothing implements yet --
         *             data[0x3faa] bit2/bit8, [0x3fab] bit8, [0x3fae] bit8. Set at
         *             EVERY instruction of the handler (xpc=0, pc 0x8d00..0xa200).
         *   remove  : once the RX/BRINT0 chain writes those flags itself.
         */
        static int f3ae = -1;
        if (f3ae < 0) f3ae = calypso_gate("CALYPSO_FORCE_3FAE", 0);
        if (f3ae && s->xpc == 0 && s->pc >= 0x8d00 && s->pc <= 0xa200) {
            /* The whole FB-detect handshake the handler polls (0x8866 and the
             * BITF sites 0x90c8/0x90ed/0x9128): 0x3faa bit2/bit8, 0x3fab bit8,
             * 0x3fae bit8. Without them the handler spins and the MAC kernel at
             * 0xa076 is never reached. */
            s->data[0x3faa] |= 0x0104;
            s->data[0x3fab] |= 0x0100;
            s->data[0x3fae] |= 0x0100;
        }
    }
    /* CORR-FLOW probe (CALYPSO_CORR_FLOW): traces the FB handler flow in bank 0
     * (0x8600..0xa200, XPC=0) -- raw PC/opcode, ST0 TC/C, AR1..AR5 -- to check
     * against SPRU172C where and why the flow leaves the MAC kernel at 0xa076.
     * Marks 0xa076 and 0x9a80. [2026-09-23] sonde pure. */
    if (C54X_SONDES) {
        static int cf = -1; static unsigned cfn = 0;
        if (cf < 0) cf = calypso_gate("CALYPSO_CORR_FLOW", 0);
        /* The range starts at 0x8600 to cover the handshake subroutine at
         * 0x8866. AR3 is the CMPS/coefficient pointer; AR1/AR2 show the pointer
         * setup. The copy loop 0x8866-0x886c (opcode 8091, ~134 iterations per
         * call) is skipped and consecutive repeats of the same PC are dropped:
         * otherwise they consume the whole log budget. */
        static uint16_t cf_lastpc = 0;
        if (cf && s->xpc == 0 && s->pc >= 0x8600 && s->pc <= 0xa200 && cfn < 20000
            /* Trace only while a real FB/SB task is active (task_md = 5 or 6),
             * otherwise the budget is spent on idle spinning. */
            && (s->data[0x0804] == 5 || s->data[0x0804] == 6
                || s->data[0x0818] == 5 || s->data[0x0818] == 6
                || s->pc >= 0xa000)   /* also trace the 0xa0xx flow, where task_md is 0 */
            && !(s->pc >= 0x8866 && s->pc <= 0x886c)
            && s->pc != cf_lastpc) {
            cf_lastpc = s->pc;
            cfn++;
            const char *mk = (s->pc==0xa076) ? " <<<KERNEL-a076"
                           : (s->pc==0x9a80) ? " <<<KERNEL-9a80"
                           : (s->pc==0x8d00) ? " [handler-entry]"
                           : (s->pc==0x8866) ? " [subr-8866]"
                           : (s->ar[5]==0x2a00 || s->ar[3]==0x2a00) ? " <<<PTR=0x2a00!" : "";
            fprintf(stderr, "[c54x] CORR-FLOW PC=0x%04x op=%04x TC=%d C=%d "
                    "AR1=%04x AR2=%04x AR3=%04x AR4=%04x AR5=%04x%s insn=%u\n",
                    s->pc, op, !!(s->st0 & ST0_TC), !!(s->st0 & ST0_C),
                    s->ar[1], s->ar[2], s->ar[3], s->ar[4], s->ar[5], mk, s->insn_count);
        }
    }
    uint16_t op2;
    bool ind;
    uint16_t addr;
    int consumed = 1;
    s->lk_used = false;  /* reset before each instruction */
    s->writer_kind = WK_UNKNOWN;  /* attribution tag for DATA-W-MMR */

    /* DERAIL-EE00 probe: catches jumps into the empty PROM window 0xee00
     * (op=0x0000). Logs the source PC, its opcode and XPC, which separates a
     * runaway firmware branch from an XPC paging bug (a legitimate banked
     * address fetched from page 0). Capped at 12 hits.
     * [2026-09-23] Sondes pures DERAIL-EE00 .. AR0_DEBUG, derriere
     * l'interrupteur. */
    if (C54X_SONDES) {
    if (s->pc >= 0xee00 && s->pc < 0xef00 &&
        !(s->last_exec_pc >= 0xee00 && s->last_exec_pc < 0xef00)) {
        static unsigned dr = 0;
        if (dr < 12) {
            fprintf(stderr, "[c54x] DERAIL-EE00 #%u entré PC=0x%04x DEPUIS last_pc=0x%04x op_src=0x%04x XPC=%u op@pc=0x%04x SP=0x%04x insn=%u\n",
                    dr, s->pc, s->last_exec_pc, prog_fetch(s, s->last_exec_pc),
                    s->xpc & 0xFF, op, s->sp, s->insn_count);
            dr++;
        }
    }

    static int ct_lo = -1, ct_hi = -1;
    if (ct_lo < 0) {
        const char *l = calypso_getenv("CALYPSO_CORR_LO"); const char *h = calypso_getenv("CALYPSO_CORR_HI");
        ct_lo = l ? (int)strtol(l, NULL, 0) : 0x8560;
        ct_hi = h ? (int)strtol(h, NULL, 0) : 0x8590;
    }
    if (s->insn_count > 60000000u && s->pc >= (uint16_t)ct_lo && s->pc <= (uint16_t)ct_hi
        && calypso_debug_enabled("CORR-TRACE")) {
        static unsigned ct = 0;
        if (ct < 60) {
            int64_t aa = (s->a & 0x8000000000LL) ? (int64_t)(s->a | ~0xFFFFFFFFFFLL) : (int64_t)s->a;
            fprintf(stderr, "[c54x] CORR-TRACE #%u PC=0x%04x op=%04x op2=%04x AR3=%04x data[AR3]=%04x A=%lld T=%04x BRC=%u insn=%u\n",
                    ct, s->pc, op, prog_fetch(s, s->pc + 1), s->ar[3], s->data[s->ar[3]],
                    (long long)aa, s->t, s->brc, s->insn_count);
            ct++;
        }
    }

    /* AR-CLOBBER probe (CALYPSO_DEBUG=AR_CLOBBER): tracks AR1/AR2/AR6/AR7
     * going to 0. Once an AR pointer is 0, any later indirect store *ARx hits
     * data[0x00], which is the IMR MMR. Logs the instruction that made the
     * transition (last_exec_pc and its opcode) to name the culprit. */
    /* [2026-09-19] A_SCH-1111: which instruction puts 0x1111 into a_sch[0]?
     * a_sch[0] = data[0x0837] (R page 0) and data[0x084b] (R page 1). It is the
     * MOST FREQUENT value in that cell (27 of 60 changes measured) and it is a
     * plain number where only B_BLUD (bit15) and B_SCH_CRC (bit8) have meaning;
     * no firmware and no emulation source contains it as a literal, so it is
     * computed. Runs per instruction like its AR-CLOBBER neighbour, and names
     * the instruction that JUST executed (last_exec_pc), not the next one --
     * the distinction that made the gdb watchpoint report the wrong site. */
    {
        static uint16_t a_prev0, a_prev1; static bool a_ini; static unsigned a_n;
        uint16_t a_now0 = s->data[0x0837], a_now1 = s->data[0x084b];
        if (a_ini && a_n < 30) {
            for (int pg = 0; pg < 2; pg++) {
                uint16_t was = pg ? a_prev1 : a_prev0, now = pg ? a_now1 : a_now0;
                if (now != was && (now & 0x8000)) {   /* BLUD=1: a RESULT */
                    fprintf(stderr, "[c54x] A_SCH-RESULTAT page=%d %04x -> %04x "
                            "par PC=0x%04x op=0x%04x A=%lld B=%lld T=%04x "
                            "AR1=%04x AR2=%04x AR3=%04x AR4=%04x insn=%u\n",
                            pg, was, now, s->last_exec_pc,
                            prog_fetch(s, s->last_exec_pc),
                            (long long)s->a, (long long)s->b, s->t,
                            s->ar[1], s->ar[2], s->ar[3], s->ar[4], s->insn_count);
                    a_n++;
                }
            }
        }
        a_prev0 = a_now0; a_prev1 = a_now1; a_ini = true;
    }
    {
        static uint16_t prev_ar1, prev_ar2, prev_ar6, prev_ar7;
        static bool init_done = false;
        static unsigned clob_log = 0;
        if (!init_done) {
            prev_ar1 = s->ar[1]; prev_ar2 = s->ar[2];
            prev_ar6 = s->ar[6]; prev_ar7 = s->ar[7];
            init_done = true;
        }
        for (int i = 0; i < 4; i++) {
            int idx = (int[]){1, 2, 6, 7}[i];
            uint16_t *prev = (uint16_t*[]){&prev_ar1, &prev_ar2,
                                            &prev_ar6, &prev_ar7}[i];
            if (*prev != 0 && s->ar[idx] == 0) {
                if (calypso_debug_enabled("AR_CLOBBER") && clob_log < 30) {
                    uint16_t culprit_op = prog_fetch(s, s->last_exec_pc);
                    fprintf(stderr,
                            "[c54x] AR-CLOBBER #%u AR%d %04x->0 by "
                            "PC=0x%04x op=0x%04x cur_PC=0x%04x cur_op=0x%04x "
                            "SP=0x%04x insn=%u\n",
                            clob_log, idx, *prev,
                            s->last_exec_pc, culprit_op,
                            s->pc, op, s->sp, s->insn_count);
                    fflush(stderr);
                    clob_log++;
                }
            }
            *prev = s->ar[idx];
        }
    }

    if (s->pc == 0x013b && calypso_getenv("CALYPSO_AR0_DEBUG")) {
        static int d13 = 0;
        if (!d13) { d13 = 1;
            fprintf(stderr, "[c54x] SUB-013B A=0x%06llx DP=0x%03x d_page(08D4)=0x%04x insn=%u\n",
                    (unsigned long long)(s->a & 0xFFFFFF), s->st0 & 0x1FF,
                    /* 0x08D4 lives in the API window: read api_ram when it is
                     * mapped, data[] only as a fallback. */
                    s->api_ram ? s->api_ram[0x08D4 - C54X_API_BASE] : s->data[0x08D4],
                    s->insn_count);
            for (uint16_t a = 0x0138; a <= 0x014c; a += 4)
                fprintf(stderr, "[c54x] PROG[0x%04x..]= %04x %04x %04x %04x\n",
                        a, s->prog[a], s->prog[(uint16_t)(a+1)],
                        s->prog[(uint16_t)(a+2)], s->prog[(uint16_t)(a+3)]);
        }
    }
    if (s->pc == 0x8869 && calypso_getenv("CALYPSO_AR0_DEBUG")) {
        static int d88 = 0;
        if (!d88) { d88 = 1;
            fprintf(stderr, "[c54x] TASK-8869 A=0x%06llx DP=0x%03x AR2=%04x AR3=%04x "
                    "AR5=%04x task_md@058a=0x%04x insn=%u\n",
                    (unsigned long long)(s->a & 0xFFFFFF), s->st0 & 0x1FF,
                    s->ar[2], s->ar[3], s->ar[5], s->data[0x058a], s->insn_count);
            for (uint16_t a = 0x8860; a <= 0x8884; a += 4)
                fprintf(stderr, "[c54x] PROG[0x%04x..]= %04x %04x %04x %04x\n",
                        a, s->prog[a], s->prog[(uint16_t)(a+1)],
                        s->prog[(uint16_t)(a+2)], s->prog[(uint16_t)(a+3)]);
        }
    }
    if (s->pc == 0x7234 && calypso_getenv("CALYPSO_AR0_DEBUG")) {
        static int d72 = 0;
        if (!d72) { d72 = 1;
            int ovly = !!(s->pmst & PMST_OVLY);
            fprintf(stderr, "[c54x] DERAIL-013B XPC=0x%02x PMST=0x%04x OVLY=%d fetch(0x013b)=0x%04x insn=%u\n",
                    s->xpc & 0xFF, s->pmst, ovly, prog_fetch(s, 0x013b), s->insn_count);
            fprintf(stderr, "[c54x] OVERLAY data[0x0138..]= %04x %04x %04x %04x %04x %04x %04x %04x\n",
                    s->data[0x0138], s->data[0x0139], s->data[0x013a], s->data[0x013b],
                    s->data[0x013c], s->data[0x013d], s->data[0x013e], s->data[0x013f]);
            /* The go-live loop 0xa4de..0xa4e8 and the soft vector data[0x3f6d]
             * that drives the trampoline. */
            fprintf(stderr, "[c54x] GOLIVE-CODE fetch: 0xa4de=%04x 0xa4df=%04x 0xa4e0=%04x 0xa4e1=%04x "
                    "0xa4e2=%04x 0xa4e3=%04x 0xa4e4=%04x 0xa4e5=%04x  data[0x3f6d]=0x%04x\n",
                    prog_fetch(s,0xa4de), prog_fetch(s,0xa4df), prog_fetch(s,0xa4e0), prog_fetch(s,0xa4e1),
                    prog_fetch(s,0xa4e2), prog_fetch(s,0xa4e3), prog_fetch(s,0xa4e4), prog_fetch(s,0xa4e5),
                    s->data[0x3f6d]);
            fprintf(stderr, "[c54x] SOFTVEC data[0x3f6a]=0x%04x (CALA cible) 0x3f6b=0x%04x 0x3f6c=0x%04x "
                    "0x3f6d=0x%04x  (0xa671=OK, 0x71f4=RECURSE)\n",
                    s->data[0x3f6a], s->data[0x3f6b], s->data[0x3f6c], s->data[0x3f6d]);
            fprintf(stderr, "[c54x] PROM0-src[0x7138..]= %04x %04x %04x %04x %04x %04x %04x %04x\n",
                    s->prog[0x7138], s->prog[0x7139], s->prog[0x713a], s->prog[0x713b],
                    s->prog[0x713c], s->prog[0x713d], s->prog[0x713e], s->prog[0x713f]);
            /* Scheduler code at 0x7234, to reconfirm the CALL 0x013b. */
            fprintf(stderr, "[c54x] PROG[0x7234..]= %04x %04x %04x %04x\n",
                    s->prog[0x7234], s->prog[0x7235], s->prog[0x7236], s->prog[0x7237]);
        }
    }
    }   /* C54X_SONDES : DERAIL-EE00 .. AR0_DEBUG */
    uint8_t hi4 = (op >> 12) & 0xF;
    uint8_t hi8 = (op >> 8) & 0xFF;

    /* Instruction-length fixes, gated by CALYPSO_FIXES (see calypso_fix_enabled).
     * Each entry quotes binutils tic54x-opc.c, whose second field IS the word
     * count. The decoder consumed 2 words for these one-word instructions, which
     * desynchronises every instruction decoded after them. */
    {   /* The fixes below without a calypso_fix_enabled() call are validated and
         * unconditional. Those still behind calypso_fix_enabled("FIX_...") are
         * formally correct but contradicted by measurement; see each comment. */
        /* LD Xmem,SHFT,dst — binutils { "ld", 1,3,3, 0x9400, 0xFE00,
         * {OP_Xmem,OP_SHFT,OP_DST} }; was decoded as a 2-word MVDK/MVKD. */
        if ((op & 0xFE00) == 0x9400) {
            uint16_t a = resolve_xmem(s, op);
            uint16_t v = data_read(s, a);
            int shft = op & 0xF, d = (op >> 8) & 1;
            int64_t x = (s->st1 & ST1_SXM) ? (int64_t)(int16_t)v : (int64_t)(uint16_t)v;
            x <<= shft;
            if (d) s->b = sext40(x); else s->a = sext40(x);
            return 1;
        }
        /* BIT Xmem,BITC: TC = Xmem(15-BITC) — binutils
         * { "bit", 1,2,2, 0x9600, 0xFF00 }; was decoded as a 2-word MVDP. */
        if ((op & 0xFF00) == 0x9600) {
            uint16_t a = resolve_xmem(s, op);
            uint16_t v = data_read(s, a);
            int bitc = op & 0xF;
            if ((v >> (15 - bitc)) & 1) s->st0 |= ST0_TC; else s->st0 &= ~ST0_TC;
            return 1;
        }
        /* SUB Xmem,Ymem,dst: dst = (Xmem - Ymem) << 16 — binutils
         * { "sub", 1,..., 0xA200, 0xFE00 }; was decoded as a 2-word ADD/SUB #lk. */
        if ((op & 0xFE00) == 0xA200) {
            uint16_t xa = resolve_xmem(s, op);
            uint8_t ym = op & 0xF; int yar = (ym & 3) + 2, ymod = (ym & 0xC) >> 2;
            uint16_t ya = s->ar[yar];
            switch (ymod) {
            case 1: s->ar[yar] = ya - 1; break;
            case 2: s->ar[yar] = ya + 1; break;
            case 3: s->ar[yar] = c54x_circ_ref(ya, +(int16_t)s->ar[0], s->bk); break;
            default: break;
            }
            int64_t xv = (int16_t)data_read(s, xa), yv = (int16_t)data_read(s, ya);
            int64_t r = (xv - yv) << 16;
            if ((op >> 8) & 1) s->b = sext40(r); else s->a = sext40(r);
            return 1;
        }
        /* LD Xmem,dst || MAC/MAS/MASR Ymem — binutils
         * { "ld", 1,..., 0xA800/0xAC00/0xAE00, 0xFE00 }; were decoded as 2-word
         * AND #lk / MACP / MACD. Only the LD half runs, the parallel half is
         * dropped: the RESULT is approximate but the LENGTH is right again, so
         * the instruction stream stops drifting. */
        if (((op & 0xFE00) == 0xA800 || (op & 0xFE00) == 0xAC00 || (op & 0xFE00) == 0xAE00)
            && calypso_fix_enabled("FIX_LD_PARALLEL")) {
            uint16_t a = resolve_xmem(s, op);
            uint16_t v = data_read(s, a);
            int64_t x = (s->st1 & ST1_SXM) ? (int64_t)(int16_t)v : (int64_t)(uint16_t)v;
            if ((op >> 8) & 1) s->b = sext40(x << 16); else s->a = sext40(x << 16);
            return 1;
        }

        /* LDM MMR,dst — binutils { "ldm", 1,2,2, 0x4800, 0xFE00, {OP_MMR,OP_DST} }.
         * An MMR is an UNSIGNED 16-bit value (pointer, counter, status register):
         * sign-extending turns AR=0x8000 into a negative 40-bit value. SPRU172C
         * gives "LDM MMR, dst : dst = MMR" with no sign extension (LDU is the
         * explicitly unsigned form; LDM has no signed variant). */
        /* [2026-09-20] Gate removed: SPRU172C example LDM AR4, A with AR4=FFFF
         * gives A = 00 0000 FFFF. Unconditional. */
        if ((op & 0xFE00) == 0x4800) {
            int mmr = resolve_mmr(s, op);   /* direct or *ARx / *ARx- / *ARx+ */
            uint16_t v = data_read(s, mmr);
            if ((op >> 8) & 1) s->b = (int64_t)(uint16_t)v; else s->a = (int64_t)(uint16_t)v;
            return 1 + s->lk_used;
        }
        /* DST src,Lmem — binutils { "dst", 1,2,2, 0x4E00, 0xFE00, {OP_SRC1,OP_Lmem} }.
         * Lmem is a LONG (2-word) operand, so the pointer advances by 2, not 1.
         * A post-modification of 1 shifts every later element of a long-word
         * array; the error is silent and cumulative. */
        if ((op & 0xFE00) == 0x4E00) {
            int src = (op >> 8) & 1;
            int64_t v = src ? s->b : s->a;
            /* [2026-09-20] All Lmem addressing modes through resolve_lmem (the old
             * code knew *ARx+ and *ARx- only, and ignored CPL for the direct form),
             * high word AT the address, low word at address ^ 1 (SPRU172C DST
             * example, AR3 = 0101h: data[0101h] = hi, data[0100h] = lo). */
            uint16_t a = resolve_lmem(s, op);
            data_write(s, a,                 (uint16_t)((v >> 16) & 0xFFFF));
            data_write(s, (uint16_t)(a ^ 1), (uint16_t)(v & 0xFFFF));
            return 1 + s->lk_used;
        }
        /* STL/STH src,SHFT,Xmem — binutils { "stl"/"sth", 1,.., 0x9800/0x9A00,
         * 0xFE00, {OP_SRC1,OP_SHFT,OP_Xmem} }. The SHFT field (bits 3-0) was
         * ignored, so the stored value had the wrong scale.
         * Per SPRU172C 4-169/4-172, "syntax 3: when SHFT = 0 the opcode assembles
         * as syntax 1", so every 0x98/0x9A encountered has SHFT != 0. The SB
         * demodulator has 4 such sites (0x7694/0x7697 `9a91/9a11`, 0x8217). */
        if ((op & 0xFE00) == 0x9800 || (op & 0xFE00) == 0x9A00) {
            uint16_t a = resolve_xmem(s, op);
            int shft = op & 0xF;
            int src = (op >> 8) & 1;
            int64_t v = src ? s->b : s->a;
            v <<= shft;
            uint16_t w = ((op & 0xFE00) == 0x9A00) ? (uint16_t)((v >> 16) & 0xFFFF)
                                                   : (uint16_t)(v & 0xFFFF);
            data_write(s, a, w);
            return 1;
        }
        /* SUB Smem,16,src[,dst] — binutils { "sub", 1,.., 0x4000, 0xFC00,
         * {OP_Smem,OP_16,OP_SRC,OPT|OP_DST} }. Two distinct fields: bit 9 = SRC
         * (source accumulator), bit 8 = DST. Bit 9 was ignored, so the
         * subtraction always started from the same accumulator. */
        if ((op & 0xFC00) == 0x4000) {
            bool ind2; uint16_t a = resolve_smem(s, op, &ind2);
            uint16_t v = data_read(s, a);
            int srcb = (op >> 9) & 1, dstb = (op >> 8) & 1;
            int64_t sv = srcb ? s->b : s->a;
            int64_t r  = sv - c54x_smem16_shifted(s, v);
            if (dstb) s->b = sext40(r); else s->a = sext40(r);
            return 1 + s->lk_used;
        }
        /* STL B,ASM,Smem — binutils { "stl", 1,..., 0x8400, 0xFE00 } also covers
         * 0x85 (src = B); was decoded as a 2-word MVPD. Exact mirror of the
         * already validated 0x84 handler. */
        if ((op & 0xFF00) == 0x8500) {
            bool ind2; uint16_t a = resolve_smem(s, op, &ind2);
            int shift = asm_shift(s);
            int64_t v = s->b;
            if (shift >= 0) v <<= shift; else v >>= (-shift);
            data_write(s, a, (uint16_t)(v & 0xFFFF));
            return 1 + s->lk_used;
        }
        /* ST TRN,Smem — binutils { "st", 1,..., 0x8D00, 0xFF00 }; was decoded as
         * a 2-word MVDD. */
        if ((op & 0xFF00) == 0x8D00) {
            bool ind2; uint16_t a = resolve_smem(s, op, &ind2);
            data_write(s, a, s->trn);
            return 1 + s->lk_used;
        }
    }

    /* DISP-ENTRY probe (CALYPSO_DEBUG=DISP-ENTRY): tells an interrupt preemption
     * apart from a clobber. Logs the dispatcher entry at 0x8341 only, with
     * DP/ST0/SP/AR2, the interrupt state (INTM/IFR/INT3-pending), the context of
     * the last interrupt served (vector, insn delta, preempted foreground PC and
     * DP) and the LUT slot that 0x834d will read, data[(DP<<7)|0x07]. A bad entry
     * has DP != 0x124; if it always coincides with a recent interrupt, the cause
     * is preemption rather than a stale DP. */
    if (s->pc == 0x7234) {
        /* One-shot dump of the 0x7234 scheduler (CALYPSO_AR0_DEBUG): what it does
         * and which indirect pointer sends it to 0x013b.
         * Pure probe; FORCE_DISPATCH below stays active. */
        if (C54X_SONDES && calypso_getenv("CALYPSO_AR0_DEBUG")) {
            static int d7 = 0;
            if (!d7) { d7 = 1;
                fprintf(stderr, "[c54x] SCHED-7234 A=0x%06llx ST0=0x%04x DP=0x%03x "
                        "AR1=%04x AR2=%04x AR5=%04x d_page(08D4)=0x%04x d584=0x%04x insn=%u\n",
                        (unsigned long long)(s->a & 0xFFFFFF), s->st0, s->st0 & 0x1FF,
                        s->ar[1], s->ar[2], s->ar[5],
                        /* d_dsp_page is 0x08D4 in api_ram, not 0x08E2 in data[]. */
                        s->api_ram ? s->api_ram[0x08D4 - C54X_API_BASE] : s->data[0x08D4],
                        s->data[0x0584], s->insn_count);
                for (uint16_t a = 0x7230; a <= 0x7240; a += 4)
                    fprintf(stderr, "[c54x] PROG[0x%04x..]= %04x %04x %04x %04x\n",
                            a, s->prog[a], s->prog[(uint16_t)(a+1)],
                            s->prog[(uint16_t)(a+2)], s->prog[(uint16_t)(a+3)]);
            }
        }
        /* @BEQUILLE — FORCE_DISPATCH  (CALYPSO_FORCE_DISPATCH, atoi>0, default
         *              OFF; calypso_wire.env sets it to 1)
         *   masks   : the frame scheduler at 0x7234 is reached with a garbage DP
         *             and d_dsp_page at 0, so the LUT at 0x8341 does not resolve
         *             and the GSM/FB task is never dispatched. DP is forced to
         *             0x124 and d_dsp_page / data[0x0584] to 0x0002.
         *   remove  : once the 0x013b prologue restores a valid DP and the
         *             producer of d_dsp_page writes B_GSM_TASK (bit 1) through
         *             the ARM path.
         */
        static int fd = -1;
        if (fd < 0) { const char *e = calypso_getenv("CALYPSO_FORCE_DISPATCH"); fd = (e && atoi(e) > 0) ? 1 : 0; }
        if (fd) {
            /* d_dsp_page is 0x08D4 (0x08E2 is d_dsp_state) and must be written
             * in api_ram: that is the array the ROM reads for the 0x0800+ range.
             * Writing data[] instead leaves the cell inert. */
            uint16_t old = (uint16_t)(s->st0 & 0x1FF);
            uint16_t oldpg = s->api_ram ? s->api_ram[0x08D4 - C54X_API_BASE]
                                        : s->data[0x08D4];
            s->st0 = (uint16_t)((s->st0 & ~0x1FF) | (0x124 & 0x1FF));
            if (s->api_ram)
                s->api_ram[0x08D4 - C54X_API_BASE] = 0x0002;  /* B_GSM_TASK | w_page=0 */
            else
                s->data[0x08D4] = 0x0002;
            s->data[0x0584] = 0x0002;
            static unsigned fdl = 0;
            if (fdl++ < 16)
                fprintf(stderr, "[c54x] FORCE-DISPATCH @0x7234 DP 0x%03x->0x124 "
                        "d_page 0x%04x->0x0002 insn=%u\n",
                        old, oldpg, s->insn_count);
        }
    }
    /* SBFN-PROBE (read-only, gate CALYPSO_SBFN) ===============================
     * Answers: which frame does the DARAM buffer hold when the SB task (0x9841)
     * reads it? calypso_bsp.c documents that by default EVERY frame writes into
     * 0x2a00, so about 9 non-FCCH bursts land between two FCCH. If SB correlates
     * an arbitrary burst, the SCH word comes out invalid and the DSP raises
     * B_SCH_CRC rightly: a TIMING failure, not a processing one.
     * Prints the fn of the last deposit, fn%51 (SCH frames are {1,11,21,31,41},
     * GSM 45.002), how many bursts were deposited since the previous SB, and the
     * buffer amplitude. Writes nothing, so it cannot change behaviour.
     * [2026-09-23] Sondes pures SBFN, SUBC, MVDD derriere l'interrupteur. */
    if (C54X_SONDES) {
    if (s->pc == 0x9841) {
        static int on = -1;
        if (on < 0) { const char *e = calypso_getenv("CALYPSO_SBFN"); on = (e && *e && atoi(e)) ? 1 : 0; }
        if (on) {
            static unsigned n = 0, prevwr = 0;
            if (n++ < 400) {
                int mn = 32767, mx = -32768; long en = 0;
                for (int k = 0; k < 296; k++) {
                    int v = (int16_t)s->data[(uint16_t)(0x2a00 + k)];
                    if (v < mn) { mn = v; }
                    if (v > mx) { mx = v; }
                    en += (long)v * v;
                }
                unsigned fn = calypso_daram_last_fn;
                fprintf(stderr, "[c54x] SBFN-PROBE #%u fn=%u p51=%u %s depots_depuis_SB=%u "
                        "iq=[%d..%d] energie=%ld insn=%u\n",
                        n, fn, fn % 51,
                        ((fn % 51) % 10 == 1 && (fn % 51) <= 41) ? "SCH" :
                        ((fn % 51) % 10 == 0 && (fn % 51) <= 40) ? "FCCH" : "AUTRE",
                        calypso_daram_wr_count - prevwr, mn, mx, en, s->insn_count);
                prevwr = calypso_daram_wr_count;
            }
        }
    }
    /* SUBC-PROBE (read-only, gate CALYPSO_SUBC) ===============================
     * The whole cascade hangs on one value, the quotient of the 16-step division
     * in the caller:
     *     0x7d1c  RPT #15
     *     0x7d1d  SUBC *(0x0b), A      ; division
     *     0x7d1e  STL  A, *(0x0a)      ; THE QUOTIENT
     *     0x7d21  LD   *(0x0a), T      ; T is loaded here
     *     0x7d24  CALLD 0x81df         ; subroutine of the MPY at 0x81e4
     * zero quotient -> zero T -> zero product -> blocks 3/4 overwritten -> empty
     * coefficient source -> FIRS multiplies by nothing -> B_SCH_CRC raised.
     *
     * Liveness control, same gate: passes through 0x989f (the bad-CRC branch),
     * which is known to execute, are counted too. A quotient count stuck at zero
     * while the control count rises is a measured fact; both silent means the
     * probe is dead, not the code. */
    if (s->pc == 0x7d19 || s->pc == 0x7d1b || s->pc == 0x7d1c ||
        s->pc == 0x7d1e || s->pc == 0x81e4 || s->pc == 0x989f) {
        static int on = -1;
        if (on < 0) { const char *e = calypso_getenv("CALYPSO_SUBC"); on = (e && *e && atoi(e)) ? 1 : 0; }
        if (on) {
            static unsigned n_q = 0, n_mpy = 0, n_ctl = 0;
            if (s->pc == 0x989f) {
                n_ctl++;
                if (n_ctl <= 3 || (n_ctl % 25) == 0)
                    fprintf(stderr, "[c54x] SUBC-PROBE CONTROLE 0x989f #%u "
                            "(quotient=%u mpy=%u) insn=%u\n",
                            n_ctl, n_q, n_mpy, s->insn_count);
            } else if (s->pc == 0x7d19 || s->pc == 0x7d1b || s->pc == 0x7d1c) {
                /* The dividend is the constant 1 shifted (ld #1,A; sfta A,<n>).
                 * If the shift leaves A below the divisor the quotient is zero by
                 * construction, which is not a SUBC bug, so A is sampled at three
                 * points: before the LD, after the LD and just before the
                 * division. */
                static unsigned n_s = 0;
                if (n_s++ < 60)
                    fprintf(stderr, "[c54x] SUBC-PROBE DIVIDENDE@0x%04x A=0x%010llx "
                            "op=0x%04x ST1=0x%04x diviseur=0x%04x insn=%u\n",
                            s->pc, (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                            prog_fetch(s, s->pc), s->st1,
                            s->data[(uint16_t)(((s->st0 & 0x1FF) << 7) | 0x0B)],
                            s->insn_count);
            } else if (s->pc == 0x7d1e) {
                if (n_q++ < 40) {
                    unsigned dp = s->st0 & 0x1FF;
                    uint16_t divis = s->data[(uint16_t)((dp << 7) | 0x0B)];
                    uint16_t prev  = s->data[(uint16_t)((dp << 7) | 0x0A)];
                    fprintf(stderr, "[c54x] SUBC-PROBE QUOTIENT #%u A=0x%010llx "
                            "bas16=0x%04x diviseur[DP:0x0b]=0x%04x ancien[0x0a]=0x%04x "
                            "T=0x%04x DP=0x%03x insn=%u\n",
                            n_q, (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                            (unsigned)(s->a & 0xFFFF), divis, prev, s->t, dp,
                            s->insn_count);
                }
            } else {
                if (n_mpy++ < 40)
                    fprintf(stderr, "[c54x] SUBC-PROBE MPY@0x81e4 #%u T=0x%04x "
                            "A=0x%010llx insn=%u\n",
                            n_mpy, s->t, (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                            s->insn_count);
            }
        }
    }
    /* MVDD-PROBE (read-only, gate CALYPSO_SUBC) ===============================
     * Measured: the dividend of the 0x7d1d division is not a physical quantity
     * but the INDEX of the first non-zero word of blocks 3/4 (backward scan
     * 0x7cf5..0x7d06, falling through to `xor A` when nothing is found). Empty
     * blocks -> A=0 -> quotient 0 -> T=0 -> zero coefficients; the blocks are the
     * cause, not a consequence of T.
     *
     * Blocks 3/4 are meant to be filled by two 7-word copies:
     *     0x7cda  stm #0x2cce, AR2        ; destination = block 3
     *     0x7cdc  stm #0x2c56, AR3        ; source = CORR A
     *     0x7cde  mar *AR3+0              ; AR3 += AR0   <-- offset
     *     0x7cdf  rpt #6 / 0x7ce0 mvdd
     *     0x7ce1  mar *+AR3(0x2b) / 0x7ce3 rpt #6 / 0x7ce4 mvdd  ; block 4
     * and that copy is SKIPPED by `bcd 0x7ced, ANEQ` at 0x7ccd when the blocks
     * are already non-zero. The probe prints whether 0x7ccd is reached and taken,
     * whether the copy runs, with which AR0/AR3, and what the source holds. */
    if (s->pc == 0x7ccd || s->pc == 0x7ce0 || s->pc == 0x7ce4) {
        static int on = -1;
        if (on < 0) { const char *e = calypso_getenv("CALYPSO_SUBC"); on = (e && *e && atoi(e)) ? 1 : 0; }
        if (on) {
            static unsigned n_g = 0, n_c3 = 0, n_c4 = 0;
            if (s->pc == 0x7ccd) {
                if (n_g++ < 20) {
                    long som = 0;
                    for (int k = 0; k < 14; k++) som += (int16_t)s->data[(uint16_t)(0x2cce + k)];
                    fprintf(stderr, "[c54x] MVDD-PROBE GARDE@0x7ccd A=0x%010llx "
                            "(saute-la-copie si A!=0) somme_blocs=%ld AR0=0x%04x insn=%u\n",
                            (unsigned long long)(s->a & 0xFFFFFFFFFFULL), som,
                            s->ar[0], s->insn_count);
                }
            } else if (s->pc == 0x7ce0) {
                if (n_c3++ < 20)
                    fprintf(stderr, "[c54x] MVDD-PROBE COPIE bloc3 AR3(src)=0x%04x "
                            "AR2(dst)=0x%04x AR0=0x%04x src[0..3]=%04x %04x %04x %04x insn=%u\n",
                            s->ar[3], s->ar[2], s->ar[0],
                            s->data[s->ar[3]], s->data[(uint16_t)(s->ar[3]+1)],
                            s->data[(uint16_t)(s->ar[3]+2)], s->data[(uint16_t)(s->ar[3]+3)],
                            s->insn_count);
            } else {
                if (n_c4++ < 20)
                    fprintf(stderr, "[c54x] MVDD-PROBE COPIE bloc4 AR3(src)=0x%04x "
                            "AR2(dst)=0x%04x src[0..3]=%04x %04x %04x %04x insn=%u\n",
                            s->ar[3], s->ar[2],
                            s->data[s->ar[3]], s->data[(uint16_t)(s->ar[3]+1)],
                            s->data[(uint16_t)(s->ar[3]+2)], s->data[(uint16_t)(s->ar[3]+3)],
                            s->insn_count);
            }
        }
    }
    }   /* C54X_SONDES : SBFN, SUBC, MVDD */
    if (s->pc == 0x8341) {
        /* @BEQUILLE — FORCE_DP (+ FORCE_DP_FROM as a scope)  (CALYPSO_FORCE_DP,
         *              VALUE, default OFF)
         *   masks   : the DP field of ST0 at the dispatcher entry is a stack
         *             residue (over-pop, ST0 not restored) instead of the
         *             expected data page.
         *   remove  : once the DISP-ENTRY probe shows the dispatcher resolving
         *             without forcing, i.e. once ST0 push/pop is balanced.
         */
        static int inited = 0, force_dp = -1, force_from = -1;
        if (!inited) {
            inited = 1;
            const char *e = calypso_getenv("CALYPSO_FORCE_DP");
            force_dp = (e && *e) ? (int)strtol(e, NULL, 0) : -1;
            const char *ef = calypso_getenv("CALYPSO_FORCE_DP_FROM"); /* scoped: force only when DP==FROM */
            force_from = (ef && *ef) ? (int)strtol(ef, NULL, 0) : -1; /* -1 = unscoped */
        }
        if (force_dp >= 0) {
            int cur = s->st0 & 0x1FF;
            if (force_from < 0 || cur == force_from)
                s->st0 = (uint16_t)((s->st0 & ~0x1FF) | (force_dp & 0x1FF));
        }
    }
    if (C54X_SONDES && s->pc == 0x8341 && calypso_debug_enabled("DISP-ENTRY")) {   /* [2026-09-23] sonde */
        static unsigned de_n = 0;
        if (de_n++ < 20000) {
            uint16_t lut_ea = (uint16_t)(((s->st0 & 0x1FF) << 7) | 0x07);
            uint16_t lut    = s->data[lut_ea];
            uint64_t d_intr = s->insn_count - g_last_intr_insn;
            fprintf(stderr,
                "[c54x] DISP-ENTRY DP=0x%03x ST0=0x%04x SP=0x%04x AR2=0x%04x "
                "INTM=%d IFR=0x%04x INT3pend=%d  lut[0x%04x]=0x%04x %s  "
                "prevPC=0x%04x  lastLDP{pc=0x%04x val=0x%03x kind=%d}  "
                "lastST0w{pc=0x%04x op=0x%04x xpc=%u val=0x%04x prev=0x%04x}  "
                "lastIT{vec=%d dInsn=%llu fgPC=0x%04x fgDP=0x%03x} insn=%u\n",
                (unsigned)(s->st0 & 0x1FF), s->st0, s->sp, s->ar[2],
                !!(s->st1 & ST1_INTM), s->ifr, !!(s->ifr & (1 << 3)),
                lut_ea, lut, (lut == 0xff72 ? "OK" : "BAD"),
                g_prev_pc, g_last_ldp_pc, g_last_ldp_val, g_last_ldp_kind,
                g_last_st0w_pc, g_last_st0w_op, g_last_st0w_xpc,
                g_last_st0w_val, g_last_st0w_prev,
                g_last_intr_vec, (unsigned long long)d_intr,
                g_last_intr_fg_pc, g_last_intr_fg_dp, s->insn_count);
            if (lut != 0xff72) {   /* bad dispatcher: dump the ST0 push/pop ring */
                fprintf(stderr, "[c54x] ST0-RING@dispBAD DP=0x%03x SP=0x%04x (anciens→récents) :",
                        (unsigned)(s->st0 & 0x1FF), s->sp);
                unsigned rn = g_st0_ring_idx < ST0_RING_N ? g_st0_ring_idx : ST0_RING_N;
                for (unsigned i = 0; i < rn; i++) {
                    St0Ev *e = &g_st0_ring[(g_st0_ring_idx - rn + i) % ST0_RING_N];
                    fprintf(stderr, " %c@%04x:%04x v=%04x(DP=%03x)SP=%04x",
                            e->kind, e->pc, e->op, e->val,
                            (unsigned)(e->val & 0x1FF), e->sp);
                }
                fprintf(stderr, "\n");
            }
        }
    }

    /* DISP-TRACE (CALYPSO_DEBUG=DISP-TRACE): traces the task dispatcher
     * 0x8341..0x8353, which computes the CALAD target (0x8353 = CALAD A). When it
     * fails, A_L ends up at 0x70c3 instead of an entry of the branch table at
     * 0x8359 (B 0x8365/0x8394/...). A at the ENTRY (0x8341) is the index
     * preloaded by the caller (task selector / d_task_md): if A is already
     * garbage there, the fault is upstream and the dispatcher is innocent. */
    if (C54X_SONDES && s->pc >= 0x8341 && s->pc <= 0x8354 && calypso_debug_enabled("DISP-TRACE")) {   /* [2026-09-23] sonde */
        static unsigned disp_n = 0;
        if (disp_n++ < 300) {
            /* At 0x834d (op 0x6f07 = LD Smem<<1,A): compute the exact direct EA
             * (DP<<7)|dma and log the value read, which becomes A. Legitimate is
             * 0xff86 (-> A_L=0x8261); corrupt is 0xf6b7 (-> 0x70c3). */
            uint16_t ea = (uint16_t)(((s->st0 & 0x1FF) << 7) | (op & 0x7F));
            fprintf(stderr,
                "[c54x] DISP-TRACE PC=0x%04x op=0x%04x A=0x%010llx DP=0x%03x EA=0x%04x "
                "data[EA]=0x%04x d[9187]=0x%04x d[9207]=0x%04x AR1=%04x AR5=%04x insn=%u\n",
                s->pc, op, (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                (unsigned)(s->st0 & 0x1FF), ea, s->data[ea],
                s->data[0x9187], s->data[0x9207],
                s->ar[1], s->ar[5], s->insn_count);
        }
    }

    /* Coarse default: any MMR write happening inside this opcode handler
     * gets attributed to the opcode family so we can read the trace. */
    if (hi8 == 0xF3)                    s->writer_kind = WK_OPCODE_F3;
    else if (hi8 >= 0x80 && hi8 <= 0x8F) s->writer_kind = WK_OPCODE_8x;
    else if (hi8 == 0x77)                s->writer_kind = WK_OPCODE_77;
    else if (hi8 == 0x76)                s->writer_kind = WK_OPCODE_76;
    else                                 s->writer_kind = WK_OPCODE_OTHER;

    /* INTM-TRANS probe: logs every INTM 0->1 transition, with the PC that set it
     * and the stack return address, to name the caller of an orphan SSBX INTM.
     * Capped at 200 transitions, otherwise boot floods the log.
     * [2026-09-23] Sondes pures INTM-TRANS .. NOP-SLIDE (dont COEFFS-DUMP,
     * D_FB_DET-WR-SITE, READ-AMONT, CORR-PUBLISH-A, ENTER-7740/7700, ...),
     * derriere l'interrupteur. */
    if (C54X_SONDES) {
    {
        static int prev_intm = -1;
        static unsigned itrans_total;
        int cur_intm = !!(s->st1 & ST1_INTM);
        if (prev_intm == 0 && cur_intm == 1) {
            itrans_total++;
            if (itrans_total <= 200) {
                uint16_t ret = s->data[s->sp];
                uint16_t ret_p1 = s->data[(uint16_t)(s->sp + 1)];
                if (calypso_debug_enabled("INTM-TRANS")) fprintf(stderr,
                        "[c54x] INTM-TRANS #%u 0->1 PC=0x%04x insn=%u SP=0x%04x "
                        "RET=%04x RET+1=%04x op=0x%04x IMR=0x%04x IFR=0x%04x\n",
                        itrans_total, s->pc, s->insn_count, s->sp,
                        ret, ret_p1, op, s->imr, s->ifr);
            }
        }
        prev_intm = cur_intm;
    }

    /* Detect when DSP enters DARAM code zone (0x0080-0x27FF) from ROM */
    {
        static uint16_t prev_pc = 0;
        static int daram_log = 0;
        if (s->pc >= 0x0080 && s->pc < 0x2800 && prev_pc >= 0x7000 && daram_log < 3) {
            C54_LOG("ROM->DARAM jump: 0x%04x->0x%04x op=0x%04x insn=%u SP=0x%04x XPC=%d",
                    prev_pc, s->pc, op, s->insn_count, s->sp, s->xpc);
            C54_LOG("  trail: %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x",
                    pc_ring[(pc_ring_idx-⟨1⟩)&255], pc_ring[(pc_ring_idx-⟨2⟩)&255],  ×4
    ⟨⟩ = (10,9) (8,7) (6,5) (4,3)
                    pc_ring[(pc_ring_idx-2)&255], pc_ring[(pc_ring_idx-1)&255]);
            daram_log++;
        }
        /* 0x7700 entry tracer: log when PC enters 0x7700 from elsewhere
         * (i.e. prev_pc != 0x76FF, the natural sequential predecessor).
         * Reveals which CALL/B/RET sources land here. PC HIST shows
         * 7700/7701 as the hottest non-loop addresses — find the callers. */
        if (s->pc == 0x7700 && prev_pc != 0x76FF) {
            static uint64_t e7700;
            e7700++;
            if (e7700 <= 30 || (e7700 % 5000) == 0) {
                C54_LOG("ENTER-7700 #%llu from PC=0x%04x A=%010llx B=%010llx SP=0x%04x trail: %04x %04x %04x %04x %04x",
                        (unsigned long long)e7700, prev_pc,
                        (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                        (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                        s->sp,
                        pc_ring[(pc_ring_idx-5)&255], pc_ring[(pc_ring_idx-4)&255],
                        pc_ring[(pc_ring_idx-3)&255], pc_ring[(pc_ring_idx-2)&255],
                        pc_ring[(pc_ring_idx-1)&255]);
            }
        }
        /* === ENTER-770c — dispatcher target, post-flag entry ===
         * The PROM0 idle dispatcher at 0xCC62..0xCC6F polls data[0x62];
         * when set, it CALAs to api[0x1f0c]=0x770c. So 0x770c is the
         * runtime task handler entry. If DARAM[0x60..0x70] never gets
         * set, this PC is never reached. Its appearance in the log is
         * therefore the binary signal that the dispatcher gate has
         * unlocked. Log every entry with full AR/SP/INTM context.
         * Cap to avoid log explosion if it ever runs hot. */
        if (s->pc == 0x770c) {
            static uint64_t e770c;
            e770c++;
            if (e770c <= 30 || (e770c % 1000) == 0) {
                C54_LOG("ENTER-770c #%llu from PC=0x%04x SP=0x%04x INTM=%d "
                        "ARs: %04x %04x %04x %04x %04x %04x %04x %04x insn=%u",
                        (unsigned long long)e770c, prev_pc, s->sp,
                        !!(s->st1 & ST1_INTM),
                        s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                        s->ar[4], s->ar[5], s->ar[6], s->ar[7],
                        s->insn_count);
            }
        }
        /* MVDD-CASCADE probe (env-gated CALYPSO_PROBE_BOOTSTUB=1).
         * PC=0x8e8c op=0xe5ba is an MVDD-family write that lands garbage in NDB
         * cells (random values at d_fb_det where 0x001e is expected). Logs the
         * ARs, B and the source address read, to tell a genuine firmware
         * computation from corrupted indirect addressing. */
        if (s->pc == 0x8e8c) {
            static int probe_mvdd = -1;
            if (probe_mvdd < 0) {
                const char *e = cdbg_env("BOOTSTUB");
                probe_mvdd = (e && e[0] == '1') ? 1 : 0;
            }
            if (probe_mvdd) {
                static uint32_t e_mvdd;
                e_mvdd++;
                if (e_mvdd <= 50 || (e_mvdd % 1000) == 0) {
                    fprintf(stderr,
                            "[c54x] MVDD-CASCADE #%u PC=0x8e8c op=0x%04x SP=0x%04x "
                            "A=0x%010llx B=0x%010llx T=0x%04x "
                            "AR= %04x %04x %04x %04x %04x %04x %04x %04x "
                            "data[AR4]=0x%04x data[AR5]=0x%04x "
                            "trail: %04x %04x %04x %04x %04x %04x\n",
                            e_mvdd, op, s->sp,
                            (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                            (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                            s->t,
                            s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                            s->ar[4], s->ar[5], s->ar[6], s->ar[7],
                            s->data[s->ar[4]], s->data[s->ar[5]],
                            pc_ring[(pc_ring_idx-6)&255], pc_ring[(pc_ring_idx-5)&255],
                            pc_ring[(pc_ring_idx-4)&255], pc_ring[(pc_ring_idx-3)&255],
                            pc_ring[(pc_ring_idx-2)&255], pc_ring[(pc_ring_idx-1)&255]);
                }
            }
        }

        /* === DF92-LOOP probe (env-gated CALYPSO_PROBE_BOOTSTUB=1) ===
         * Compute loop at PC=0xdf92-0xdfa3 = correlator accumulator with
         * 15x unrolled ADD *AR7+. Called via CALL 0xdfb1 from 0xdf90.
         * Probe at first PC=0xdf92 (loop entry) — log AR7, BRC, accumulator,
         * caller (from stack[SP]). If AR7 is corrupted or BRC mis-set, the
         * loop runs forever and blocks task=24 scheduling downstream.
         * Fire only on entries from non-loop-internal predecessors. */
        if (s->pc == 0xdf92 && (prev_pc < 0xdf90 || prev_pc > 0xdfa3)) {
            static int probe_df = -1;
            if (probe_df < 0) {
                const char *e = cdbg_env("BOOTSTUB");
                probe_df = (e && e[0] == '1') ? 1 : 0;
            }
            if (probe_df) {
                static uint32_t e_df;
                e_df++;
                if (e_df <= 30) {
                    fprintf(stderr,
                            "[c54x] DF92-LOOP #%u entry from PC=0x%04x prev_op=0x%04x "
                            "SP=0x%04x ret_addr=stk[SP]=0x%04x "
                            "A=0x%010llx B=0x%010llx "
                            "AR7=0x%04x BK=0x%04x BRC=0x%04x  "
                            "trail: %04x %04x %04x %04x %04x %04x\n",
                            e_df, prev_pc, s->prog[prev_pc],
                            s->sp, s->data[s->sp],
                            (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                            (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                            s->ar[7], s->bk, s->brc,
                            pc_ring[(pc_ring_idx-6)&255], pc_ring[(pc_ring_idx-5)&255],
                            pc_ring[(pc_ring_idx-4)&255], pc_ring[(pc_ring_idx-3)&255],
                            pc_ring[(pc_ring_idx-2)&255], pc_ring[(pc_ring_idx-1)&255]);
                }
            }
        }

        /* === BL-REENTRY probe (env-gated CALYPSO_PROBE_BOOTSTUB=1) ===
         * The DSP bootloader at PC=0xb41c polls data[0x0fff] for cmd code
         * 4 or 2. Legitimate loop entry comes from 0xb427 (BC NTC 0xb41c)
         * or 0xb433 (CALL 0xb41c). Any OTHER entry path indicates the DSP
         * has been routed back into the bootloader by mistake after boot
         * has completed — that's the post-cascade blocker. Logs prev_pc,
         * SP, stack contents, ARs, and a trail to identify the bad caller.
         * Caps: 50 events to avoid log flood. */
        if (s->pc == 0xb41c && prev_pc != 0xb427 && prev_pc != 0xb433) {
            static int probe_bl = -1;
            if (probe_bl < 0) {
                const char *e = cdbg_env("BOOTSTUB");
                probe_bl = (e && e[0] == '1') ? 1 : 0;
            }
            if (probe_bl) {
                static uint32_t e_bl;
                e_bl++;
                if (e_bl <= 50) {
                    fprintf(stderr,
                            "[c54x] BL-REENTRY #%u from PC=0x%04x prev_op=0x%04x "
                            "SP=0x%04x stk[SP..+3]= %04x %04x %04x %04x "
                            "data[0x0fff]=0x%04x data[0x0ffe]=0x%04x "
                            "AR= %04x %04x %04x %04x %04x %04x %04x %04x "
                            "trail: %04x %04x %04x %04x %04x %04x %04x %04x\n",
                            e_bl, prev_pc, s->prog[prev_pc],
                            s->sp,
                            s->data[(uint16_t)(s->sp+0)], s->data[(uint16_t)(s->sp+1)],
                            s->data[(uint16_t)(s->sp+2)], s->data[(uint16_t)(s->sp+3)],
                            s->data[0x0fff], s->data[0x0ffe],
                            s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                            s->ar[4], s->ar[5], s->ar[6], s->ar[7],
                            pc_ring[(pc_ring_idx-⟨1⟩)&255], pc_ring[(pc_ring_idx-⟨2⟩)&255],  ×3
    ⟨⟩ = (8,7) (6,5) (4,3)
                            pc_ring[(pc_ring_idx-2)&255], pc_ring[(pc_ring_idx-1)&255]);
                }
            }
        }

        /* SEED-SOURCE probe (env-gated CALYPSO_PROBE_BOOTSTUB=1).
         * PC=0xf8de (CALA B -> 0x7700) is the single event that spawns the whole
         * boot-stub RET-loop cascade: [2026-05-24] one ENTER-7700 produced 435
         * entries to PC=0x0000. Captures the state BEFORE the CALA fires -- SP
         * and stack contents (what the later POPs will pull), A and B (B is the
         * jump target), AR0..AR7, ST0, ST1, and a 10-PC trail -- to tell whether
         * the function holding 0xf8de was itself called with a proper push, and
         * what the stack should have held when POPM ST0 and RCD UNC run at the
         * 0x7706/0x7707 dispatcher. */
        if (s->pc == 0xf8de) {
            static int probe_seed = -1;
            if (probe_seed < 0) {
                const char *e = cdbg_env("BOOTSTUB");
                probe_seed = (e && e[0] == '1') ? 1 : 0;
            }
            if (probe_seed) {
                static uint32_t e_seed;
                e_seed++;
                if (e_seed <= 50) {
                    fprintf(stderr,
                            "[c54x] SEED-SOURCE #%u PC=0xf8de op=0x%04x "
                            "SP=0x%04x  stk[SP..+7]= %04x %04x %04x %04x %04x %04x %04x %04x  "
                            "A=0x%010llx B=0x%010llx "
                            "AR= %04x %04x %04x %04x %04x %04x %04x %04x  "
                            "ST0=0x%04x ST1=0x%04x  "
                            "trail: %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x\n",
                            e_seed, op, s->sp,
                            s->data[(uint16_t)(s->sp+⟨1⟩)], s->data[(uint16_t)(s->sp+⟨2⟩)],  ×4
    ⟨⟩ = (0,1) (2,3) (4,5) (6,7)
                            (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                            (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                            s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                            s->ar[4], s->ar[5], s->ar[6], s->ar[7],
                            s->st0, s->st1,
                            pc_ring[(pc_ring_idx-10)&255], pc_ring[(pc_ring_idx-9)&255],
                            pc_ring[(pc_ring_idx-⟨1⟩)&255],  pc_ring[(pc_ring_idx-⟨2⟩)&255],  ×3
    ⟨⟩ = (8,7) (6,5) (4,3)
                            pc_ring[(pc_ring_idx-2)&255],  pc_ring[(pc_ring_idx-1)&255]);
                }
            }
        }

        /* BOOTSTUB-ENTRY probe (env-gated CALYPSO_PROBE_BOOTSTUB=1).
         * Traces every entry to PC=0x0000 (boot stub LDMM SP,B + RET).
         * Re-entering the boot stub at runtime seeds the SP-wrap -> AR6=0 ->
         * IMR=0 cascade. Captures:
         *   - prev_pc + op@prev_pc  → who jumped to 0x0000
         *   - entry mechanism (RET-family / branch / other)
         *   - B accumulator (becomes SP via LDMM SP,B at 0x0000)
         *   - SP + stk[SP-1] (just-popped value if RET)
         *   - 6-entry PC trail (caller context). */
        if (s->pc == 0x0000) {
            static int probe_bootstub = -1;
            if (probe_bootstub < 0) {
                const char *e = cdbg_env("BOOTSTUB");
                probe_bootstub = (e && e[0] == '1') ? 1 : 0;
                if (probe_bootstub)
                    fprintf(stderr, "[c54x] PROBE-BOOTSTUB enabled\n");
            }
            if (probe_bootstub) {
                static uint32_t e0;
                e0++;
                if (e0 <= 200 || (e0 % 500) == 0) {
                    uint16_t prev_op = s->prog[prev_pc];
                    const char *mech;
                    if (prev_op == 0xFC00)                          mech = "RET";
                    else if (prev_op == 0xF273)                     mech = "RETD";
                    else if (prev_op == 0xF4EB || prev_op == 0xF4E3) mech = "RETE";
                    else if (prev_op == 0xF4E4 || prev_op == 0xF4E5) mech = "FRET";
                    else if ((prev_op & 0xFF00) == 0xF800)          mech = "B/CC";
                    else if ((prev_op & 0xFF00) == 0xF000)          mech = "F0xx";
                    else if (prev_op == 0xF074)                     mech = "CALL";
                    else                                            mech = "OTHER";
                    /* Just-popped slot is at SP-1 after RET (SP was incremented). */
                    uint16_t stk_just_popped = s->data[(uint16_t)(s->sp - 1)];
                    fprintf(stderr,
                            "[c54x] BOOTSTUB-ENTRY #%u prev_PC=0x%04x prev_op=0x%04x "
                            "mech=%s B=0x%010llx B[31:16]=0x%04x SP=0x%04x "
                            "stk[SP-1]=0x%04x trail: %04x %04x %04x %04x %04x %04x\n",
                            e0, prev_pc, prev_op, mech,
                            (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                            (unsigned)((s->b >> 16) & 0xFFFF),
                            s->sp, stk_just_popped,
                            pc_ring[(pc_ring_idx-6)&255], pc_ring[(pc_ring_idx-5)&255],
                            pc_ring[(pc_ring_idx-4)&255], pc_ring[(pc_ring_idx-3)&255],
                            pc_ring[(pc_ring_idx-2)&255], pc_ring[(pc_ring_idx-1)&255]);
                }
            }
        }
        /* INT3-VEC-TRACE probe (CALYPSO_DEBUG=INT3_VEC or ALL).
         * Triggers at PC=0xFFCC, the INT3 vector entry (IPTR=0x1FF + vec 19*4),
         * and captures the next 32 PCs to follow the ISR path: normal return
         * through RETE, RSBX INTM in the 0xA4D0+ region, or a drift down to the
         * 0x0000 boot stub, in which case the trace names the opcode that
         * derailed. */
        {
            static int trace_n = -1;        /* -1 = not active, >= 0 = countdown */
            static uint16_t trace_pcs[64];
            static uint16_t trace_ops[64];
            static int trace_idx = 0;
            static unsigned trace_dumps = 0;
            const unsigned DUMP_LIMIT = 8;   /* max 8 full traces logged */

            if (s->pc == 0xFFCC && trace_n < 0 && trace_dumps < DUMP_LIMIT) {
                trace_n = 32;                /* capture next 32 insns */
                trace_idx = 0;
                if (calypso_debug_enabled("INT3_VEC")) {
                    fprintf(stderr,
                            "[c54x] INT3-VEC-TRACE BEGIN #%u pc=0xFFCC "
                            "ST1=0x%04x INTM=%d IFR=0x%04x IMR=0x%04x SP=0x%04x\n",
                            trace_dumps + 1, s->st1, !!(s->st1 & ST1_INTM),
                            s->ifr, s->imr, s->sp);
                }
            }
            if (trace_n >= 0 && trace_idx < 64) {
                trace_pcs[trace_idx] = s->pc;
                trace_ops[trace_idx] = prog_fetch(s, s->pc);
                trace_idx++;
                trace_n--;
                if (trace_n <= 0) {
                    if (calypso_debug_enabled("INT3_VEC")) {
                        fprintf(stderr,
                                "[c54x] INT3-VEC-TRACE END #%u captured=%d "
                                "final_ST1=0x%04x INTM=%d\n",
                                trace_dumps + 1, trace_idx,
                                s->st1, !!(s->st1 & ST1_INTM));
                        for (int i = 0; i < trace_idx; i++) {
                            fprintf(stderr,
                                    "[c54x] INT3-VEC-TRACE #%u step %02d: "
                                    "PC=0x%04x op=0x%04x\n",
                                    trace_dumps + 1, i,
                                    trace_pcs[i], trace_ops[i]);
                        }
                        fflush(stderr);
                    }
                    trace_dumps++;
                    trace_n = -1;            /* re-arm */
                }
            }
        }

        /* D_FB_DET-WR-SITE probe at PC=0x8f51, the PC that writes d_fb_det.
         * Snapshots AR0..AR7, data[AR0..AR7], BK and A to identify the DARAM
         * region the FB-det correlator reads when producing its output, and
         * compares it with the BSP DMA target (default 0x3fb0..0x3fbf):
         * same region means the correlator reads the samples, a different one
         * means source and sink disagree and the DSP expects the samples
         * somewhere other than where the BSP writes them.
         *
         * COEFFS-TABLE-DUMP: once at startup and at every FB-det sweep, dumps
         * data[0x2bc0..0x2bcf], the region AR4 points at for the correlator
         * coefficients. [2026-05-14] data[AR4] was 0x0000 on all 50 captured
         * hits, i.e. the coefficient table is empty in memory; the dump tells
         * whether it is already empty at boot and whether anything ever fills
         * it. */
        {
            static int coeffs_log_n;
            static uint64_t coeffs_last_insn;
            bool first_call = (coeffs_log_n == 0);
            bool periodic = (s->insn_count - coeffs_last_insn > 1000000);
            bool at_8f51 = (s->pc == 0x8f51);
            if ((first_call || periodic || at_8f51) && coeffs_log_n < 30) {
                coeffs_log_n++;
                coeffs_last_insn = s->insn_count;
                C54_LOG("COEFFS-DUMP #%d insn=%u PC=0x%04x "
                        "data[0x2bc0..0x2bcF]= %04x %04x %04x %04x %04x %04x %04x %04x "
                        "%04x %04x %04x %04x %04x %04x %04x %04x",
                        coeffs_log_n, s->insn_count, s->pc,
                        s->data[0x2bc0], s->data[0x2bc1], s->data[0x2bc2], s->data[0x2bc3],
                        s->data[0x2bc4], s->data[0x2bc5], s->data[0x2bc6], s->data[0x2bc7],
                        s->data[0x2bc8], s->data[0x2bc9], s->data[0x2bca], s->data[0x2bcb],
                        s->data[0x2bcc], s->data[0x2bcd], s->data[0x2bce], s->data[0x2bcf]);
            }
        }
        if (s->pc == 0x8f51) {
            /* Per-fire log capped at 500 to cover several FB-det sweeps rather
             * than the first one only; the aggregate stats below count every
             * fire, the cap applies to the per-fire log alone. */
            static int dfbwr_n;
            g_fb_det_timing.fb_det_total++;
            uint16_t ar4 = s->ar[4];
            uint16_t dAR4 = s->data[ar4];
            uint16_t ar3 = s->ar[3];
            bool ar4_in_zone = (ar4 >= 0x2bc0 && ar4 <= 0x2bff);
            if (ar4_in_zone) g_fb_det_timing.fb_det_ar4_in_zone++;
            else             g_fb_det_timing.fb_det_ar4_outside++;
            if (dAR4 == 0x0000)      g_fb_det_timing.fb_det_dar4_zero++;
            else if (dAR4 == 0xfffe) g_fb_det_timing.fb_det_dar4_sentinel++;
            else                     g_fb_det_timing.fb_det_dar4_other++;
            /* Sweep boundary: AR3 dropping below the last observed value marks
             * a new sweep, so log the previous one (non-zero count, final A). */
            uint64_t A_lo = (uint64_t)(s->a & 0xFFFFFFFFFFULL);
            if (ar3 < g_fb_det_timing.last_ar3_at_fire
                && g_fb_det_timing.last_ar3_at_fire > 0) {
                C54_LOG("D_FB_DET-SWEEP id=%llu nonzero=%llu/50 "
                        "A_final=0x%010llx insn=%u",
                        (unsigned long long)g_fb_det_timing.sweep_id,
                        (unsigned long long)g_fb_det_timing.sweep_nonzero_count,
                        (unsigned long long)A_lo, s->insn_count);
                g_fb_det_timing.sweep_id++;
                g_fb_det_timing.sweep_nonzero_count = 0;
            }
            g_fb_det_timing.last_ar3_at_fire = ar3;
            if (dAR4 != 0) g_fb_det_timing.sweep_nonzero_count++;
            int64_t delta_compute = (int64_t)s->insn_count -
                                    (int64_t)g_fb_det_timing.last_compute_insn;
            int64_t delta_clear   = (int64_t)s->insn_count -
                                    (int64_t)g_fb_det_timing.last_clear_insn;
            int64_t delta_pattern = (int64_t)s->insn_count -
                                    (int64_t)g_fb_det_timing.last_pattern_insn;
            if (dfbwr_n++ < 500) {
                C54_LOG("D_FB_DET-WR-SITE #%d AR0..AR7=%04x %04x %04x %04x %04x %04x %04x %04x "
                        "data[AR0]=%04x data[AR1]=%04x data[AR2]=%04x "
                        "data[AR3]=%04x data[AR4]=%04x data[AR5]=%04x "
                        "data[AR6]=%04x data[AR7]=%04x "
                        "BK=%04x A=0x%010llx "
                        "ar4_in_zone=%d dcompute=%lld dclear=%lld dpattern=%lld "
                        "last_compute_addr=0x%04x last_clear_addr=0x%04x "
                        "last_pattern_addr=0x%04x "
                        "insn=%u",
                        dfbwr_n, s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                        s->ar[4], s->ar[5], s->ar[6], s->ar[7],
                        s->data[s->ar[0]], s->data[s->ar[1]], s->data[s->ar[2]],
                        s->data[s->ar[3]], s->data[s->ar[4]], s->data[s->ar[5]],
                        s->data[s->ar[6]], s->data[s->ar[7]],
                        s->bk, (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                        ar4_in_zone ? 1 : 0,
                        (long long)delta_compute, (long long)delta_clear,
                        (long long)delta_pattern,
                        g_fb_det_timing.last_compute_addr,
                        g_fb_det_timing.last_clear_addr,
                        g_fb_det_timing.last_pattern_addr,
                        s->insn_count);
            }
            /* Summary every 100 fires of 0x8f51: AR4-in-zone distribution and
             * data[AR4] histogram over the whole history. */
            if ((g_fb_det_timing.fb_det_total % 100) == 0) {
                C54_LOG("D_FB_DET-STATS total=%llu "
                        "ar4_in_zone=%llu outside=%llu "
                        "dar4_zero=%llu sentinel=%llu other=%llu",
                        (unsigned long long)g_fb_det_timing.fb_det_total,
                        (unsigned long long)g_fb_det_timing.fb_det_ar4_in_zone,
                        (unsigned long long)g_fb_det_timing.fb_det_ar4_outside,
                        (unsigned long long)g_fb_det_timing.fb_det_dar4_zero,
                        (unsigned long long)g_fb_det_timing.fb_det_dar4_sentinel,
                        (unsigned long long)g_fb_det_timing.fb_det_dar4_other);
            }
        }
        /* READ-AMONT probe: at each trigger PC (the d_fb_det sites), emits the
         * per-range read delta since the previous trigger. A dominant LOW means
         * the correlator reads [0..0x3A3], APIRAM means the samples arrive
         * through the ARM-driven API RAM, WRAP means it runs on the PROM1 mirror
         * wrap, OTHER means an uncatalogued region. */
        if (!c54x_rapide) {
            read_stats_trigger_check(s);
            throughput_tick(s->insn_count);
        }
        /* WAIT-A21A probe: at PC=0xa21a, snapshots INTM, IMR and IFR.
         *   INTM=1, IFR=0,  IMR set -> the hardware is simply silent
         *   INTM=1, IFR!=0, IMR set -> a pending IRQ is being blocked (bug)
         *   INTM=0                  -> IRQs are serviceable, so the path to
         *                              0x7740 is broken upstream */
        /* CORR-PUBLISH-A probe: at PC=0x9ac0, just before STL A -> *AR2-,
         * snapshots the full A and AR2, the publication address. Capped at 200. */
        if (s->pc == 0x9ac0) {
            static unsigned cpa_log;
            const unsigned LIMIT = 200;
            if (cpa_log < LIMIT) {
                fprintf(stderr,
                        "[c54x] CORR-PUBLISH-A #%u PC=0x9ac0 "
                        "A=0x%010llx (A_lo=0x%04x A_hi=0x%04x) "
                        "AR2=0x%04x (= *AR2- target) insn=%u\n",
                        cpa_log,
                        (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                        (uint16_t)(s->a & 0xFFFF),
                        (uint16_t)((s->a >> 16) & 0xFFFF),
                        s->ar[2], s->insn_count);
                cpa_log++;
                if (cpa_log == LIMIT) {
                    fprintf(stderr,
                            "[c54x] CORR-PUBLISH-A log capped at %u\n",
                            LIMIT);
                }
            }
        }
        if (s->pc == 0xa21a) {
            static uint64_t a21a_total;
            a21a_total++;
            if (a21a_total <= 5 || (a21a_total % 100000) == 0) {
                C54_LOG("WAIT-A21A #%llu insn=%u INTM=%d IMR=0x%04x IFR=0x%04x "
                        "ST0=0x%04x ST1=0x%04x SP=0x%04x",
                        (unsigned long long)a21a_total, s->insn_count,
                        !!(s->st1 & ST1_INTM), s->imr, s->ifr,
                        s->st0, s->st1, s->sp);
            }
        }
        /* CALLER-7740 tracer: at the 0x7740 entry, logs the caller context.
         * data[SP] is the return address pushed by the preceding CALL/CALLD, and
         * INTM=1 means an IRQ context, which separates "called from an ISR" from
         * "called from regular flow" and lets the caller chain be walked back to
         * the IRQ vector. */
        if (s->pc == 0x7740) {
            static uint64_t enter7740;
            enter7740++;
            uint16_t ret_addr = s->data[s->sp];
            uint16_t ret_addr_p1 = s->data[(uint16_t)(s->sp + 1)];
            C54_LOG("ENTER-7740 #%llu insn=%u SP=%04x RET=%04x RET+1=%04x "
                    "INTM=%d XPC=%02x AR2=%04x AR3=%04x BK=%04x",
                    (unsigned long long)enter7740, s->insn_count,
                    s->sp, ret_addr, ret_addr_p1,
                    !!(s->st1 & ST1_INTM), s->xpc,
                    s->ar[2], s->ar[3], s->bk);
        }
        /* MAC-7700 tracer: at PC=0x7700 (MAC *AR2-, A) we want to know
         * what AR2 points at, what data[AR2] holds, T, and A before/after.
         * Helps determine if AR2 references the BSP RX zone (correlator
         * FB-det) or somewhere else. Also dumps full AR0-AR7 + ST0/ST1. */
        if (s->pc == 0x7700) {
            static uint64_t mac7700_total;
            mac7700_total++;
            if (mac7700_total <= 50 || (mac7700_total % 5000) == 0) {
                uint16_t ar2 = s->ar[2];
                uint16_t v_at_ar2 = s->data[ar2];
                C54_LOG("MAC-7700 #%llu AR2=0x%04x data[AR2]=0x%04x T=0x%04x "
                        "A_pre=%010llx ST0=0x%04x ST1=0x%04x",
                        (unsigned long long)mac7700_total, ar2, v_at_ar2,
                        s->t,
                        (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                        s->st0, s->st1);
                C54_LOG("MAC-7700 #%llu ARs: AR0=%04x AR1=%04x AR2=%04x AR3=%04x "
                        "AR4=%04x AR5=%04x AR6=%04x AR7=%04x SP=%04x",
                        (unsigned long long)mac7700_total,
                        s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                        s->ar[4], s->ar[5], s->ar[6], s->ar[7], s->sp);
            }
        }
        /* RCD-75e8 tracer: when DSP arrives at PC=0x75e8 (cond=0x47 = LEQ),
         * log A. The RCD takes if A <= 0; report whether the loop will
         * exit this iteration. */
        if (s->pc == 0x75e8) {
            static uint64_t rcd75e8_total;
            rcd75e8_total++;
            if (rcd75e8_total <= 50 || (rcd75e8_total % 5000) == 0) {
                int64_t acc = sext40(s->a);
                C54_LOG("RCD-75e8 #%llu A=%010llx (signed=%lld) RCD-taken=%d AR2=%04x",
                        (unsigned long long)rcd75e8_total,
                        (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                        (long long)acc, (acc <= 0), s->ar[2]);
            }
        }
        prev_pc = s->pc;
        /* DARAM 0x1100-0x1130 tracer: dump first 64 visits */
        static int daram1110_log = 0;
        if (s->pc >= 0x1100 && s->pc <= 0x1130 && daram1110_log < 64) {
            C54_LOG("DARAM110x PC=0x%04x op=0x%04x A=%08x B=%08x AR2=%04x AR3=%04x AR4=%04x AR5=%04x BRC=%d",
                    s->pc, op, (uint32_t)s->a, (uint32_t)s->b,
                    s->ar[2], s->ar[3], s->ar[4], s->ar[5], s->brc);
            daram1110_log++;
        }
    }
    if (s->pc >= 0xFE00 && s->pc <= 0xFFFF && op == 0x0000) {
        static int nop_slide = 0;
        if (nop_slide == 0) {
            C54_LOG("NOP-SLIDE PC=0x%04x insn=%u SP=0x%04x PMST=0x%04x XPC=%d OVLY=%d",
                    s->pc, s->insn_count, s->sp, s->pmst, s->xpc, !!(s->pmst & PMST_OVLY));
            C54_LOG("  trail: %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x",
                    pc_ring[(pc_ring_idx-⟨1⟩)&255], pc_ring[(pc_ring_idx-⟨2⟩)&255],  ×4
    ⟨⟩ = (10,9) (8,7) (6,5) (4,3)
                    pc_ring[(pc_ring_idx-2)&255], pc_ring[(pc_ring_idx-1)&255]);
        }
        nop_slide++;
    }
    }   /* C54X_SONDES : INTM-TRANS .. NOP-SLIDE */

    switch (hi4) {
    case 0xF:
        /* 0xF --- large group: branches, misc, short immediates */
        if (op == 0xF495) return consumed + s->lk_used;  /* NOP */

        /* XC n, cond — Execute Conditionally (SPRU172C p.4-198)
         * Opcode: 1111 11N1 CCCCCCCC
         * 0xFDxx = XC 1, cond (N=0, execute next 1 instruction)
         * 0xFFxx = XC 2, cond (N=1, execute next 2 instructions)
         * If condition true: execute normally. If false: skip n instructions. */
        if (hi8 == 0xFD || hi8 == 0xFF) {
            int n_insns = (hi8 == 0xFF) ? 2 : 1;
            uint8_t cc = op & 0xFF;
            bool cond = false;
            /* Evaluate condition code per SPRU172C condition table */
            /* Conditions can be combined (OR'd bits), but common single conditions: */
            if (cc == 0x00)      cond = true;                          /* UNC */
            else if (cc == 0x0C) cond = (s->st0 & ST0_C) != 0;       /* C */
            else if (cc == 0x08) cond = !(s->st0 & ST0_C);            /* NC */
            else if (cc == 0x30) cond = (s->st0 & ST0_TC) != 0;       /* TC */
            else if (cc == 0x20) cond = !(s->st0 & ST0_TC);           /* NTC */
            else if (cc == 0x45) cond = (sext40(s->a) == 0);          /* AEQ */
            else if (cc == 0x44) cond = (sext40(s->a) != 0);          /* ANEQ */
            else if (cc == 0x46) cond = (sext40(s->a) > 0);           /* AGT */
            else if (cc == 0x42) cond = (sext40(s->a) >= 0);          /* AGEQ */
            else if (cc == 0x43) cond = (sext40(s->a) < 0);           /* ALT */
            else if (cc == 0x47) cond = (sext40(s->a) <= 0);          /* ALEQ */
            else if (cc == 0x4D) cond = (sext40(s->b) == 0);          /* BEQ */
            else if (cc == 0x4C) cond = (sext40(s->b) != 0);          /* BNEQ */
            else if (cc == 0x4E) cond = (sext40(s->b) > 0);           /* BGT */
            else if (cc == 0x4A) cond = (sext40(s->b) >= 0);          /* BGEQ */
            else if (cc == 0x4B) cond = (sext40(s->b) < 0);           /* BLT */
            else if (cc == 0x4F) cond = (sext40(s->b) <= 0);          /* BLEQ */
            else if (cc == 0x70) { cond = (s->st0 & ST0_OVA) != 0; s->st0 &= ~ST0_OVA; }    /* AOV, cleared once tested */
            else if (cc == 0x60) { cond = !(s->st0 & ST0_OVA);       s->st0 &= ~ST0_OVA; }    /* ANOV */
            else if (cc == 0x78) { cond = (s->st0 & ST0_OVB) != 0; s->st0 &= ~ST0_OVB; }    /* BOV */
            else if (cc == 0x68) { cond = !(s->st0 & ST0_OVB);       s->st0 &= ~ST0_OVB; }    /* BNOV */
            else {
                /* Combined conditions: OR the individual condition bits */
                cond = false;
                if (cc & 0x0C) cond |= ((cc & 0x04) ? (s->st0 & ST0_C) != 0 : !(s->st0 & ST0_C));
                if (cc & 0x30) cond |= ((cc & 0x10) ? (s->st0 & ST0_TC) != 0 : !(s->st0 & ST0_TC));
                if (cc & 0x40) {
                    int64_t acc = (cc & 0x08) ? s->b : s->a;
                    int c3 = cc & 0x07;
                    switch (c3) {
                    case 0x5: cond |= (sext40(acc) == 0); break;
                    case 0x4: cond |= (sext40(acc) != 0); break;
                    case 0x6: cond |= (sext40(acc) > 0); break;
                    case 0x2: cond |= (sext40(acc) >= 0); break;
                    case 0x3: cond |= (sext40(acc) < 0); break;
                    case 0x7: cond |= (sext40(acc) <= 0); break;
                    default: cond = true; break;
                    }
                }
                if (cc & 0x70 && !(cc & 0x40)) {
                    if (cc & 0x08) cond |= (s->st0 & ST0_OVB) != 0;
                    else           cond |= (s->st0 & ST0_OVA) != 0;
                }
            }
            if (!cond) {
                /* Skip n instructions — count consumed words for skipped insns */
                /* Each skipped insn is 1 word (simplified — multi-word insns rare after XC) */
                return 1 + n_insns;
            }
            return consumed + s->lk_used;  /* condition true: just advance past XC, execute next normally */
        }

        /* F4E2 = BACC A, F5E2 = BACC B (per tic54x-opc.c, mask 0xFEFF) */
        /* F4E3 = CALA A, F5E3 = CALA B — push next-PC, jump to acc low 16 bits */
        /* DYN-CALL tracer: targets are computed at runtime, invisible to static
         * disasm. Log every BACC/CALA, plus an extra hot tag when the target
         * lands in any FB-det zone (PROM0 0x77xx-0x79xx, 0x88xx, 0xa0xx-0xa1xx). */
        if (op == 0xF4E2 || op == 0xF5E2 || op == 0xF4E3 || op == 0xF5E3) {
            int is_b = (op & 0x0100) != 0;
            int is_call = (op & 1) != 0;
            uint16_t tgt = (uint16_t)((is_b ? s->b : s->a) & 0xFFFF);
            uint16_t src_pc = s->pc;
            /* [bacc-soi] DSP stuck at 0x01f6 (DARAM copy of PROM0 0x71f6, `bacc a` with A=0x01f6),
             * busy 120000 insn/frame: the path leading there, once. */
            /* [liste-tache] the PROM0 dispatcher 0xbc2e..0xbc36 calls a 0-terminated list of routines;
             * the list comes from be3e: base (0xde27/0xde57, b928) + data[0x3fba]*12 + data[0x4365]/2.
             * A list read outside the table yields entries such as 0x01f6 (DROM coefficients). Logged:
             * every call outside the ROM, and the first 40 after entering dedicated mode. */
            if (is_call && src_pc == 0xbc36) {
                static unsigned n_lt;
                uint16_t ent = (uint16_t)(s->ar[1] - 1);
                bool hors_rom = tgt < 0x7000;
                if (hors_rom || (s->insn_count > 3000000 && n_lt < 40)) {
                    n_lt++;
                    printf("  [liste-tache] entree data[0x%04x]=0x%04x%s liste=0x%04x base=? 3fba=%u 4364=%u 4365=%u 3fab=%04x insn=%u\n",
                           ent, tgt, hors_rom ? " HORS ROM" : "", s->data[0x4356], s->data[0x3fba],
                           s->data[0x4364], s->data[0x4365], s->data[0x3fab], s->insn_count);
                    /* first call outside the ROM: base 0xde57 + state 4 -> list = 0xbc5a; where the base comes from */
                    static unsigned n_hr;
                    if (hors_rom && n_hr++ == 0) {
                        extern void c54x_anneau_vider_stdout(const char *etiquette, unsigned n_max);
                        c54x_anneau_vider_stdout("liste-hors", 512);
                    }
                }
                /* GUARD: END OF LIST ON AN ENTRY OUTSIDE THE ROM. At every end of TCH, 0xb456 resets the
                 * TCH state data[0x3fba] to -1 and the dispatcher runs once more on base + (-1)*12:
                 * entries 0x1b04, 0x5213 then 0x01f6 (`bacc a` onto itself, [bacc-soi]) -> DSP busy
                 * forever, 25 ms per frame, 1000+ frames late. A task routine is always in the ROM: an
                 * entry outside the ROM is treated as the 0 that terminates the list (exit through the
                 * `bc pmad,aeq` of 0xbc32, pmad at 0xbc33). CALYPSO_C54X_GARDE_LISTE=0 disables. */
                static int garde_liste = -1;
                if (garde_liste < 0) garde_liste = calypso_gate("CALYPSO_C54X_GARDE_LISTE", 1);
                if (garde_liste && hors_rom) {
                    static unsigned n_gl;
                    uint16_t sortie = s->prog[0xbc33];
                    if (n_gl++ < 40 || n_gl % 1000 == 0)
                        printf("  [garde-liste] entree 0x%04x hors ROM (3fba=%u) : fin de liste -> 0x%04x insn=%u [%u]\n",
                               tgt, s->data[0x3fba], sortie, s->insn_count, n_gl);
                    s->pc = sortie;
                    return 0;
                }
            }
            if (!is_call && tgt == src_pc) {
                static unsigned n_soi;
                if (n_soi++ == 0) {
                    extern void c54x_anneau_vider_stdout(const char *etiquette, unsigned n_max);
                    printf("  [bacc-soi] BACC sur soi-meme pc=0x%04x xpc=%u A=%010llx B=%010llx SP=0x%04x pile=%04x %04x %04x %04x insn=%u\n",
                           src_pc, s->xpc, (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                           (unsigned long long)(s->b & 0xFFFFFFFFFFULL), s->sp, s->data[s->sp],
                           s->data[(uint16_t)(s->sp + 1)], s->data[(uint16_t)(s->sp + 2)],
                           s->data[(uint16_t)(s->sp + 3)], s->insn_count);
                    c54x_anneau_vider_stdout("bacc-soi", 512);
                }
            }
            /* FB-ENERGY reroute: the live FB dispatch (CALA at 0xb01e) resolves to
             * the SYMBOL correlator 0x8d00 / stub 0xab38, which never touches the
             * IQ buffer 0x2a00 nor the 0xa076 kernel. The CALA is redirected to
             * the FB ENERGY correlator entry 0x94f5 (0x9500 sets AR4=0x2a00 ->
             * f274 a033 -> a040 -> f273 a076). On that path AR5=0x2c00 is the
             * reference and the IQ 0x2a00 sits in AR4/AR1, NOT in AR5.
             * Gate CALYPSO_FB_ENERGY; entry override CALYPSO_FB_CORR_ENTRY. */
            if (is_call && src_pc == 0xb01e) {
                /* CALA-FB: the dispatcher's NATIVE target plus d_task_md, logged
                 * BEFORE any reroute. */
                { static int _cf = -1; static unsigned _cfn = 0;
                  if (_cf < 0) _cf = calypso_gate("CALYPSO_CALA_FB", 0);
                  if (_cf && _cfn < 40) { _cfn++;
                      fprintf(stderr, "[c54x] CALA-FB tgt=0x%04x md0804=%u md0818=%u md058a=%u "
                              "(0x7700=routine resultat FB, 0xab38=?, 0x8d00=corr symbole) "
                              "A=0x%06llx insn=%u\n", tgt, (unsigned)s->data[0x0804],
                              (unsigned)s->data[0x0818], (unsigned)s->data[0x058a],
                              (unsigned long long)(s->a & 0xFFFFFFULL), s->insn_count); } }
                static int _fbe = -1; static uint16_t _fbentry = 0x94f5;
                if (_fbe < 0) {
                    /* @BEQUILLE — FB_ENERGY + FB_CORR_ENTRY  (CALYPSO_FB_ENERGY,
                     *                CALYPSO_FB_CORR_ENTRY, default OFF)
                     *   masks   : the missing NATIVE dispatch to the correlator.
                     *             Execution is REROUTED to 0x9500 / 0x94f5 instead
                     *             of letting the firmware get there on its own.
                     *   remove  : once BRINT0 (vec 21) is served and the native
                     *             path reaches the correlator by itself.
                     *   ⚠️ Any measurement taken under this reroute is a measurement
                     *   UNDER CRUTCH: on the pure native path this stage is NEVER
                     *   executed ([2026-07-28] CALYPSO_WATCH_9F00_RD = 0). */
                    const char *_e = calypso_getenv("CALYPSO_FB_ENERGY"); _fbe = (_e && atoi(_e) > 0) ? 1 : 0;
                    const char *_p = calypso_getenv("CALYPSO_FB_CORR_ENTRY");
                    if (_p && *_p) _fbentry = (uint16_t)strtol(_p, NULL, 0);
                }
                if (_fbe && s->data[0x058a] == 5) {   /* d_task_md == 5 (FB command) */
                    static unsigned _fbn = 0;
                    if (_fbn++ < 32)
                        fprintf(stderr, "[c54x] FB-ENERGY-REROUTE CALA@0xb01e tgt 0x%04x -> 0x%04x insn=%u\n",
                                tgt, _fbentry, s->insn_count);
                    tgt = _fbentry;
                }
            }
            /* Self-CALA black hole: fires ONCE when a CALA targets its own PC in
             * 0x7000-0x70FF (the 0x70c3 black hole). Reports the inherited DP, the
             * LUT slot read by the dispatcher at 0x834d (ea and value), and the
             * POPM ST0 and LDP that set that DP: the whole culprit chain in one
             * line, unlike DISP-TRACE. */
            if (is_call && tgt == src_pc && tgt >= 0x7000 && tgt <= 0x70FF) {
                static int bh_logged = 0;
                if (!bh_logged++) {
                    fprintf(stderr,
                        "[c54x] *** BLACKHOLE-CALA *** tgt=0x%04x DP=0x%03x "
                        "SP=0x%04x prevPC=0x%04x dispLUT{ea=0x%04x val=0x%04x} "
                        "lastST0w{pc=0x%04x val=0x%04x prev=0x%04x} "
                        "lastLDP{pc=0x%04x val=0x%03x} insn=%u\n",
                        tgt, (unsigned)(s->st0 & 0x1FF), s->sp, g_prev_pc,
                        g_disp_lut_ea, g_disp_lut_val,
                        g_last_st0w_pc, g_last_st0w_val, g_last_st0w_prev,
                        g_last_ldp_pc, g_last_ldp_val, s->insn_count);
                    /* Stack around SP: shows the orphan word (0xf487) and its
                     * neighbours. A plausible ST0 (0x0xxx/0x4xxx, sane DP) sitting
                     * at SP+-1 means a one-word imbalance; stacked PC-shaped words
                     * (0xf4xx/0x7xxx) mean a cumulative drain. */
                    fprintf(stderr, "[c54x]     STACK around SP=0x%04x :", s->sp);
                    for (int k = -2; k <= 9; k++) {
                        uint16_t a = (uint16_t)(s->sp + k);
                        fprintf(stderr, " %s[%04x]=%04x",
                                k == 0 ? ">" : "", a, s->data[a]);
                    }
                    fprintf(stderr, "\n");
                    /* SP-event ring: the last 28 push/pop events (pc:op delta).
                     * A push (delta<0) with no matching pop is the leak.
                     * The ring is only fed under C54X_SONDES. */
                    if (C54X_SONDES) {
                    fprintf(stderr, "[c54x]     SP-EVENTS net_words=%lld pushes=%llu pops=%llu (récents, anciens→récents):\n[c54x]    ",
                            (long long)g_sp_ledger.net_words,
                            (unsigned long long)g_sp_ledger.sp_pushes,
                            (unsigned long long)g_sp_ledger.sp_pops);
                    for (int k = 28; k >= 1; k--) {
                        struct sp_evt *e = &g_spring[(g_spring_idx - k) & 63];
                        fprintf(stderr, " %04x:%04x%+d", e->pc, e->op, e->delta);
                    }
                    fprintf(stderr, "\n");
                    }   /* C54X_SONDES : SP-EVENTS */
                    fflush(stderr);
                }
            }
            int fb_zone = (tgt >= 0x7730 && tgt <= 0x7990) ||
                          (tgt >= 0x8800 && tgt <= 0x88FF) ||
                          (tgt >= 0xA000 && tgt <= 0xA1FF);
            static uint64_t dyn_total = 0;
            static uint64_t dyn_fb = 0;
            dyn_total++;
            if (fb_zone) dyn_fb++;
            /* When OVLY=1 and src_pc in [0x80, 0x2800], the executed opcode
             * comes from data[] (DARAM), not prog[]. Reflect this in the
             * dump so we see the *actual* bytes that drove the CALA. */
            int ovly_active = (s->pmst & PMST_OVLY) && src_pc >= 0x80 && src_pc < 0x2800;
            uint16_t m0 = ovly_active ? s->data[(uint16_t)(src_pc - 2)] : s->prog[(uint16_t)(src_pc - 2)];
            uint16_t m1 = ovly_active ? s->data[(uint16_t)(src_pc - 1)] : s->prog[(uint16_t)(src_pc - 1)];
            uint16_t m2 = ovly_active ? s->data[src_pc] : s->prog[src_pc];
            uint16_t m3 = ovly_active ? s->data[(uint16_t)(src_pc + 1)] : s->prog[(uint16_t)(src_pc + 1)];
            if (dyn_total <= 200 || fb_zone || (dyn_total % 5000) == 0) {
                C54_LOG("DYN-CALL #%llu %s%c src=0x%04x tgt=0x%04x A=%010llx B=%010llx SP=0x%04x mem[%c]=%04x %04x %04x %04x%s",
                        (unsigned long long)dyn_total,
                        is_call ? "CALA" : "BACC",
                        is_b ? 'B' : 'A',
                        src_pc, tgt,
                        (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                        (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                        s->sp,
                        ovly_active ? 'D' : 'P',
                        m0, m1, m2, m3,
                        fb_zone ? " *FB-ZONE*" : "");
            }
            if (is_call) {
                uint16_t ret_pc = src_pc + 1;
                s->sp = (s->sp - 1) & 0xFFFF;
                data_write(s, s->sp, ret_pc);
            }
            s->pc = tgt;
            return 0;
        }
        /* F4E6 = FBACC A   FL_FAR  (far branch on acc, no push, no delay)
         * F4E7 = FCALA A   FL_FAR  (far call on acc, pushes 2 words, no delay)
         * F5E6 = FBACC B / F5E7 = FCALA B  (accumulator B variants)
         *
         * Per binutils tic54x-opc.c (FL_FAR flag) and SPRU172C:
         *   XPC = A(22:16), PC = A(15:0). FCALA pushes XPC then ret_pc (PC+1),
         *   the order FRET (F4E4) expects when popping PC first, then XPC.
         * Same semantics as the existing FCALAD/FBACCD (F6E6/F6E7), without the
         * delay slots.
         *
         * ⚠️ This test must stay AHEAD of the F4E0-F4FF block below, which would
         * otherwise swallow these four opcodes as NOPs and derail control flow
         * silently. */
        if (op == 0xF4E6 || op == 0xF4E7 || op == 0xF5E6 || op == 0xF5E7) {
            int is_b    = (op & 0x0100) != 0;
            int is_call = (op & 1) != 0;
            int64_t acc = is_b ? s->b : s->a;
            uint16_t tgt     = (uint16_t)(acc & 0xFFFF);
            uint8_t  new_xpc = (uint8_t)((acc >> 16) & 0xFF);
            if (new_xpc > 3) new_xpc &= 3;
            static uint64_t facc_total;
            facc_total++;
            if (facc_total <= 30 || (facc_total % 5000) == 0) {
                C54_LOG("%s%c FAR #%llu PC=0x%04x → XPC=%u PC=0x%04x "
                        "(A=%010llx SP=0x%04x was XPC=%u)",
                        is_call ? "FCALA" : "FBACC",
                        is_b ? 'B' : 'A',
                        (unsigned long long)facc_total,
                        s->pc, new_xpc, tgt,
                        (unsigned long long)(acc & 0xFFFFFFFFFFULL),
                        s->sp, s->xpc & 0x3);
            }
            if (is_call) {
                /* FCALA: return PC+1 and XPC, frame order per c54x_far_push. */
                c54x_far_push(s, (uint16_t)(s->pc + 1));
            }
            s->xpc = new_xpc;
            s->pc  = tgt;
            return 0;
        }
        /* F4E0-F4FF catch-all: NOP by default, except for the opcodes in that
         * range that have their own handler -- F4E1 IDLE 1, F4E2 BACC A,
         * F4E3 CALA A, F4E4 FRET, F4E6 FBACC, F4E7 FCALA, F4EB RETE.
         * ⚠️ Without the F4E1 exception, IDLE 1 is silently swallowed as a NOP,
         * the DSP never raises s->idle, the IRQ handler never dispatches and
         * INTM stays stuck at 1. */
        /* FIX_RETF — F49B RETF (binutils 0xF49B exact; SPRU172C p.4-147):
         * (RTN = TOS) -> PC, SP+1 -> SP, 0 -> INTM. It was not decoded at all
         * and fell through as a 1-word NOP. No F49B word in PROM0..3/PDROM. */
        if (op == 0xF49B) {
            s->pc = data_read(s, s->sp);
            s->sp = (s->sp + 1) & 0xFFFF;
            s->st1 &= ~ST1_INTM;
            return 0;
        }
        if (op >= 0xF4E0 && op <= 0xF4FF &&
            op != 0xF4E1 && op != 0xF4E4 && op != 0xF4E5 && op != 0xF4EB) {
            return consumed + s->lk_used;
        }
        /* F4EB = RETE (return from interrupt). Pop PC, pop XPC iff APTS=1.
         * Symmetric with c54x_interrupt_ex push order. */
        if (op == 0xF4EB) {
            uint16_t prev_xpc = s->xpc;
            /* @BEQUILLE — CALYPSO_RETE_POP2 (default 0).
             *
             * Both interrupt entry paths in calypso_c54x.c push UNCONDITIONALLY:
             *     s->sp--; data_write(s, s->sp, s->pc + 1);
             *     s->sp--; data_write(s, s->sp, s->xpc);
             *     g_sp_ledger.net_words += 2;
             * so entry is always 2 words, while the pop below stayed conditional on
             * a heuristic (does the top of stack look like an XPC?). An
             * unconditional push against a guessed pop cannot be symmetric in every
             * case.
             *
             * [2026-07-30] Reference bench, no crutch enabled:
             *   ORPHAN-RETURN pc=0x0107 op=0xf4eb SP=0x5aa8 -> ret_tgt=0xddfb
             *   net_words=-18880, i.e. -4 words PER FRAME (events spaced 65536
             *   instructions apart), reported as over-pop above SP_base. 0x0107 is
             *   the return of the frame interrupt itself, so the context is damaged
             *   once per frame and no task survives from one frame to the next.
             *
             * masks   : the asymmetry itself. =1 pops 2 words unconditionally, an
             *           exact mirror of the entry.
             * remove  : once the default is flipped. It stays 0 for now because it
             *           changes the interrupt return of every profile; validate
             *           with CALYPSO_RETE_POP2=1 and CALYPSO_ORPHAN=1, where the
             *           ORPHAN probe must go silent and net_words stay bounded.
             */
            static int _rp2 = -1;
            if (_rp2 < 0) {
                _rp2 = calypso_gate("CALYPSO_RETE_POP2", 0);
                if (_rp2)
                    fprintf(stderr, "[c54x] RETE_POP2=1 : depilement de 2 mots "
                            "inconditionnel, symetrique du push d'entree\n");
            }
            uint16_t top = data_read(s, s->sp);
            if (_rp2) {
                s->xpc = top & 3; s->sp++;  /* pop XPC, always, mirroring the push */
            } else if (top <= 3) {          /* historical heuristic */
                s->xpc = top & 3; s->sp++;
            }
            uint16_t ra = data_read(s, s->sp); s->sp++;   /* pop PC */
            s->st1 &= ~ST1_INTM;
            { extern int g_piege_pile_on; g_piege_pile_on = 0; }   /* [2026-10-22 22:30] piege pile (c54x_mem.c) */

            /* RETE-AUDIT (CALYPSO_RETE_AUDIT=1, default 0).
             *
             * Are there MORE RETURNS THAN ENTRIES? Interrupt entry pushes 2 words
             * and RETE pops 2, so equal counts balance exactly. ORPHAN however
             * measures a constant over-pop of -4 words per frame: a RETE executed
             * without a preceding interrupt pops 2 words nobody pushed, and two
             * such per frame give exactly -4.
             *
             * Both sides are counted here and the gap printed; `irq_entries` is
             * already kept by the ledger at each real dispatch. Capped at one line
             * every 500 RETE, 40 lines total.
             */
            {
                static int _ra_g = -1; static unsigned long long _ra_n = 0;
                static unsigned _ra_log = 0;
                if (_ra_g < 0) _ra_g = calypso_gate("CALYPSO_RETE_AUDIT", 0);
                /* net_words comes from the SP ring, fed only under C54X_SONDES:
                 * the probe follows the same switch. */
                if (_ra_g && C54X_SONDES) {
                    static unsigned long long _ra_vec = 0;   /* RETE inside the vector zone */
                    _ra_n++;
                    /* RETE has two distinct uses and only one may be compared with
                     * irq_entries. Measured: 500 RETE for 167 irq_entries, because
                     * CALA (0xF4E3) pushes only ONE word (the return address) and the
                     * ROM uses it heavily, so most RETE return from a CALA-called
                     * handler rather than from an interrupt. That is legitimate and
                     * the pop heuristic handles it: a pushed XPC is 0..3 while a
                     * return address is >= 0x0080, so `top <= 3` pops 2 or 1
                     * correctly.
                     * Only the case ORPHAN points at is suspect: a RETE whose PC is
                     * in the VECTOR zone, hence an interrupt return, with a virgin
                     * stack above SP_base, hence no matching entry. That one alone is
                     * counted against irq_entries. Zone: pc < 0x0200 (post-boot table
                     * at 0x0080, 4-word slots, plus the OVLY trampolines). */
                    if (s->pc < 0x0200) {
                        _ra_vec++;
                    }
                    if ((_ra_n % 500) == 0 && _ra_log < 40) {
                        _ra_log++;
                        long long ecart = (long long)_ra_vec -
                                          (long long)g_sp_ledger.irq_entries;
                        fprintf(stderr, "[c54x] RETE-AUDIT total=%llu dont_vecteurs=%llu "
                                "irq_entries=%llu ecart_vect=%+lld net_words=%lld "
                                "insn=%u%s\n",
                                (unsigned long long)_ra_n,
                                (unsigned long long)_ra_vec,
                                (unsigned long long)g_sp_ledger.irq_entries,
                                ecart, (long long)g_sp_ledger.net_words,
                                s->insn_count,
                                ecart > 0 ? "  <== RETOURS D'IT SANS ENTREE" : "");
                    }
                }
            }
                if (g_vec28_tracing) {
                    g_vec28_trace_pops++;
                    fprintf(stderr, "[c54x] VEC28-STACK-TRACE #%u RETE(2w,symmetric) PC=0x%04x "
                            "popped=0x%04x words=2 SP_before=0x%04x SP_after=0x%04x "
                            "insn=%u\n", g_vec28_trace_pops, s->pc, ra,
                            (uint16_t)(s->sp - 2), s->sp, s->insn_count);
                    if (s->sp >= (uint16_t)(g_vec28_sp_entry + 2)) {
                        fprintf(stderr, "[c54x] VEC28-STACK-TRACE DISARMED SP=0x%04x "
                                "back at or above pre-interrupt level 0x%04x\n",
                                s->sp, g_vec28_sp_entry);
                        g_vec28_tracing = false;
                    }
                }
            /* INT3-CYCLE-TRACE end-good hook NOT here : firmware exits ISR via
             * POPM ST1 + RCD (not RETE 0xF4EB), so this path is dead. Hook
             * moved to generic INTM 1→0 detector below — catches all idioms. */
            {
                static uint64_t rete_count;
                rete_count++;
                if (rete_count <= 20 || (rete_count % 100) == 0)
                    C54_LOG("RETE #%llu PC=0x%04x -> ra=0x%04x XPC=%u→%u SP=0x%04x",
                            (unsigned long long)rete_count,
                            s->pc, ra, prev_xpc, s->xpc, s->sp);
            }
            s->pc = ra; return 0;
        }
        /* 0xF4E4 = FRET (far return). Pops PC and XPC unconditionally.
         * Per binutils tic54x-opc.c (FL_FAR flag) and SPRU172C Table 2-15:
         *   FRET[D]: XPC = TOS, ++SP, PC = TOS, ++SP
         * Symmetric with the FCALL/FCALLD push, which is unconditional too.
         * ⚠️ The pop must NOT be made conditional on PMST bit 4: that bit is AVIS
         * (address visibility) per SPRU131G, not APTS, and has no stack
         * semantics. Gating on it made FRET skip the XPC pop when AVIS=0 and
         * unbalanced the stack against FCALL FAR, which always pushes 2. */
        if (op == 0xF4E4 || op == 0xF4E5) {
            /* F4E5 = FRETE: same pop, plus 0 -> INTM (SPRU172C). It used to fall
             * into the F4E0-F4FF NOP catch-all; no PROM0..3 site uses it. */
            uint16_t ra, nx;
            uint16_t prev_xpc = s->xpc;
            if (c54x_far_spru()) {
                nx = data_read(s, s->sp); s->sp++;   /* TOS -> XPC */
                ra = data_read(s, s->sp); s->sp++;   /* TOS -> PC  */
            } else {
                ra = data_read(s, s->sp); s->sp++;
                nx = data_read(s, s->sp); s->sp++;
            }
            if (op == 0xF4E5) s->st1 &= ~ST1_INTM;
                if (g_vec28_tracing) {
                    g_vec28_trace_pops++;
                    fprintf(stderr, "[c54x] VEC28-STACK-TRACE #%u FRET(2w,symmetric) PC=0x%04x "
                            "popped=0x%04x words=2 SP_before=0x%04x SP_after=0x%04x "
                            "insn=%u\n", g_vec28_trace_pops, s->pc, ra,
                            (uint16_t)(s->sp - 2), s->sp, s->insn_count);
                    if (s->sp >= (uint16_t)(g_vec28_sp_entry + 2)) {
                        fprintf(stderr, "[c54x] VEC28-STACK-TRACE DISARMED SP=0x%04x "
                                "back at or above pre-interrupt level 0x%04x\n",
                                s->sp, g_vec28_sp_entry);
                        g_vec28_tracing = false;
                    }
                }
            if (nx > 3)   /* PROM0..3 = 4 pages; page 3 is legitimate (masked & 3) */
                C54_DBG("XPC-OOR", "FRET xpc=0x%04x PC=0x%04x SP=0x%04x insn=%u",
                        nx, s->pc, s->sp, s->insn_count);
            s->xpc = nx & 3;
            {
                static uint64_t fret_count;
                fret_count++;
                if (fret_count <= 30 || (fret_count % 1000) == 0)
                    C54_LOG("FRET #%llu PC=0x%04x -> ra=0x%04x XPC=%u→%u SP=0x%04x",
                            (unsigned long long)fret_count,
                            s->pc, ra, prev_xpc, s->xpc, s->sp);
            }
            s->pc = ra;
            return 0;
        }
        /* IDLE 1/2/3: 0xF4E1, 0xF5E1, 0xF6E1, 0xF7E1 (mask 0xFCFF) */
        if ((op & 0xFCFF) == 0xF4E1) {
            int level = ((op >> 8) & 0x3) + 1;
            static int idle_log = 0;
            if (idle_log < 20)
                C54_LOG("IDLE%d @0x%04x INTM=%d IMR=0x%04x SP=0x%04x insns=%u XPC=%d",
                        level, s->pc, !!(s->st1 & ST1_INTM),
                        s->imr, s->sp, s->insn_count, s->xpc);
            idle_log++;
            if (s->pc >= 0x8000 && s->pc < 0x8020) {
                return consumed + s->lk_used;
            }
            s->idle = true;
            return 0;
        }
        /* ================================================================
         * F[4-7]xx generic accumulator family — promoted from F4 block
         * to handle F5/F6/F7 variants. Handlers use bits 8/9 for src/dst,
         * with masks FCE0/FCFF/FEFF naturally covering all 4 combinations
         * (A->A, B->A, A->B, B->B). The matching handler bodies remain
         * inside the F4 block as dead code (never reached for arith ops
         * because of the early return here). 2026-04-28.
         * ================================================================ */
            /* F483/F583: SAT src (mask FEFF, 1 word) */
            if ((op & 0xFEFF) == 0xF483) {
                int src = (op >> 8) & 1;
                int64_t *acc = src ? &s->b : &s->a;
                int64_t val = sext40(*acc);
                uint16_t ovbit = src ? ST0_OVB : ST0_OVA;
                if (val > 0x7FFFFFFFLL) { *acc = sext40(0x7FFFFFFFLL); s->st0 |= ovbit; }
                else if (val < -0x80000000LL) { *acc = sext40(-0x80000000LL); s->st0 |= ovbit; }
                else s->st0 &= ~ovbit;          /* SPRU172C 4-153: "Affects OVsrc" */
                return consumed + s->lk_used;
            }

            /* F484/F584: NEG src[,dst] (mask FCFF, 1 word) */
            if ((op & 0xFCFF) == 0xF484) {
                int src, dst; c54x_f4_srcdst(op, &src, &dst);   /* FIX_F4XX_SRCDST */
                int64_t val = sext40(src ? s->b : s->a);
                c54x_neg_abs_ovf(s, dst, -val);   /* exact: 2^39 for 80 0000 0000h */
                return consumed + s->lk_used;
            }

            /* F485/F585: ABS src[,dst] (mask FCFF, 1 word) */
            if ((op & 0xFCFF) == 0xF485) {
                int src, dst; c54x_f4_srcdst(op, &src, &dst);   /* FIX_F4XX_SRCDST */
                int64_t val = sext40(src ? s->b : s->a);
                if (val < 0) val = -val;
                c54x_neg_abs_ovf(s, dst, val);
                return consumed + s->lk_used;
            }

            /* F48C/F58C: MPYA dst (mask FEFF, 1 word).
             * SPRU172C: `MPYA dst` -> dst = T x A(32-16). It ASSIGNS, it does not
             * accumulate; MAC accumulates, MPY/MPYA assigns. The Smem form
             * `MPYA Smem` (0x3100) and `SQUR A,dst` (0xF48D) below both assign too.
             * [2026-08-22] Where accumulating bites: the TOA chain in PROM0
             *   0x7944 add *AR4,A ; 0x7945 sub #2,A ; 0x7947 mpya A ; 0x7948 add B
             *   ... 0x795a stl B -> a_sync_demod[D_TOA]
             * A is non-zero at 0x7947, so accumulating falsifies the published TOA.
             * (At 0x7920 `mpya B` the difference is nil: B holds d_fb_mode = 0.) */
            if ((op & 0xFEFF) == 0xF48C) {
                int dst = (op >> 8) & 1;
                int64_t prod = (int64_t)(int16_t)s->t * (int64_t)(int16_t)((s->a >> 16) & 0xFFFF);
                if (s->st1 & ST1_FRCT) prod <<= 1;
                if (dst) s->b = sext40(prod); else s->a = sext40(prod);
                return consumed + s->lk_used;
            }

            /* F48D/F58D: SQUR A,dst (mask FEFF, 1 word) */
            if ((op & 0xFEFF) == 0xF48D) {
                int dst = (op >> 8) & 1;
                int16_t ah = (int16_t)((s->a >> 16) & 0xFFFF);
                int64_t prod = (int64_t)ah * (int64_t)ah;
                if (s->st1 & ST1_FRCT) prod <<= 1;
                if (dst) s->b = sext40(prod); else s->a = sext40(prod);
                return consumed + s->lk_used;
            }

            /* F48E/F58E: EXP src (mask FEFF, 1 word)
             * Count leading sign bits of accumulator, store in T */
            if ((op & 0xFEFF) == 0xF48E) {
                int src = (op >> 8) & 1;
                int64_t val = sext40(src ? s->b : s->a);
                int exp = 0;
                if (val == 0) { exp = 0; }              /* SPRU172C 4-58: src = 0 -> T = 0 */
                else if (val == -1) { exp = 31; }
                else {
                    uint64_t uv = (val < 0) ? ~val : val;
                    uv &= 0xFFFFFFFFFFULL;
                    /* Count leading zeros from bit 38 down */
                    for (int i = 38; i >= 0; i--) {
                        if (uv & (1ULL << i)) break;
                        exp++;
                    }
                    exp -= 8; /* EXP = leading sign bits - 8 */
                }
                s->t = (uint16_t)(int16_t)exp;
                return consumed + s->lk_used;
            }

            /* F486/F586: MAX src (mask FEFF, 1 word) — keep max of A,B.
             * binutils tic54x-opc.c: "max" 1,1,1, 0xF486, 0xFEFF (0xF492 is roltc).
             */
            if ((op & 0xFEFF) == 0xF486) {
                /* FIX_MAXMIN_DST — MAX dst, bit 8 (0=A, 1=B). SPRU172C 4-99:
                 * dst = max(A,B); C=0 when the max is A, C=1 otherwise.
                 * ⚠️ Ignoring the destination bit and always writing A means
                 * `max B` never updates B. [2026-09-17] That froze the argmax of
                 * the SCH correlator (0x84e8 `max B`, B stayed 0): the peak landed
                 * at the edge (index 43), the 78 bits were misframed and the SB CRC
                 * failed. */
                int64_t sa = sext40(s->a), sb = sext40(s->b);
                int a_is_max = (sa > sb);     /* [2026-09-21] equal: dst = B, C = 1 (isa_test 128 for MIN) */
                int64_t mx = a_is_max ? sa : sb;
                if ((op >> 8) & 1) s->b = sext40(mx); else s->a = sext40(mx);
                if (a_is_max) s->st0 &= ~ST0_C; else s->st0 |= ST0_C;
                return consumed + s->lk_used;
            }

            /* F487/F587: MIN src (mask FEFF, 1 word) — keep min of A,B.
             * binutils: "min" 1,1,1, 0xF487, 0xFEFF (0xF493 is cmpl). */
            if ((op & 0xFEFF) == 0xF487) {
                /* FIX_MAXMIN_DST — MIN dst, bit 8. SPRU172C 4-100:
                 * dst = min(A,B); C=0 when the min is A, C=1 otherwise. Same
                 * destination bit as MAX above. */
                int64_t sa = sext40(s->a), sb = sext40(s->b);
                int a_is_min = (sa < sb);     /* [2026-09-21] equal: dst = B, C = 1 (isa_test 128) */
                int64_t mn = a_is_min ? sa : sb;
                if ((op >> 8) & 1) s->b = sext40(mn); else s->a = sext40(mn);
                if (a_is_min) s->st0 &= ~ST0_C; else s->st0 |= ST0_C;
                return consumed + s->lk_used;
            }

            /* ⛔ The MAC/MACR/MASR/MACA/MASA/MACAR/MPYA/LD-T/LD-ASM and BITT
             * handlers do NOT belong here. Their opcodes have hi4 = 2 or 3, so
             * inside `case 0xF:` they are unreachable. The live bodies are in
             * c54x_mac_bit_family() (called from `case 0x2:` and `case 0x3:`) and,
             * for BITT, in `case 0x3:` under FIX_BITT_CASE3. Do not move them
             * back: they would be dead code again, and any probe placed with them
             * would go silent without saying so. */

            /* F492/F592: ROLTC src (rotate left through TC, mask FEFF, 1 word).
             * binutils: "roltc" 1,1,1, 0xF492, 0xFEFF.
             * SPRU172C semantics: src bit 31 -> TC, src << 1, src bit 0 <- old TC. */
            if ((op & 0xFEFF) == 0xF492) {
                /* SPRU172C 4-149: 32-bit rotate. (TC) -> src(0); src(30-0) -> src(31-1);
                 * src(31) -> C (the CARRY, TC is only read); 0 -> src(39-32).
                 * [2026-09-20] Was a 40-bit rotate that wrote bit 31 back into TC.
                 * Example ROLTC A, A = 00 0000 5555, TC = 1 -> 00 0000 AAAB, C = 0. */
                int src = (op >> 8) & 1;
                int64_t *acc = src ? &s->b : &s->a;
                int64_t v = *acc & 0xFFFFFFFFLL;
                int new_tc = (int)((v >> 31) & 1);          /* bit 31 -> C */
                int old_tc = (s->st0 & ST0_TC) ? 1 : 0;
                *acc = (int64_t)(((uint32_t)v << 1) | (uint32_t)old_tc);
                if (new_tc) s->st0 |= ST0_C; else s->st0 &= ~ST0_C;
                /* BITT-WATCH, leg 2 of 2 (same gate and window as leg 1 in the
                 * BITT handler). Shows whether the TC set by `bitt` really enters
                 * the accumulator: `after` must equal `before<<1 | old_tc`. An
                 * old_tc always 0 while leg 1 counts TC=1 means TC is overwritten
                 * between the two; both legs seeing TC=0 means the source is empty
                 * and the fault is upstream. */
                if (s->pc >= 0x9ab8 && s->pc <= 0x9ad2) {
                    static int rw = -1;
                    if (rw < 0) rw = calypso_gate("CALYPSO_BITT_WATCH", 0);
                    if (rw) {
                        static unsigned long long n, nz_out, tc_in;
                        n++;
                        if (*acc & 0xFFFFFFFFFFLL) nz_out++;
                        if (old_tc) tc_in++;
                        if (n <= 40 || (n % 5000) == 0)
                            fprintf(stderr, "[c54x] ROLTC-WATCH #%llu pc=0x%04x "
                                    "%c avant=0x%010llx old_tc=%d -> apres=0x%010llx "
                                    "new_tc=%d | cumul: acc_non_nul=%llu/%llu "
                                    "tc_entrant=%llu\n",
                                    n, s->pc, src ? 'B' : 'A',
                                    (unsigned long long)(v & 0xFFFFFFFFFFULL), old_tc,
                                    (unsigned long long)(*acc & 0xFFFFFFFFFFLL),
                                    new_tc, nz_out, n, tc_in);
                    }
                }
                return consumed + s->lk_used;
            }

            /* F49E/F59E: SUBC src (mask FEFF, 1 word) — conditional subtract for division */
            if ((op & 0xFEFF) == 0xF49E) {
                int src = (op >> 8) & 1;
                int64_t *acc = src ? &s->b : &s->a;
                int64_t val = sext40(*acc);
                if (val >= 0) { *acc = sext40((val << 1) + 1); }
                else { *acc = sext40(val << 1); }
                return consumed + s->lk_used;
            }

            /* F48F/F58F: NORM src[, dst] (mask FEFF, 1 word)
             * Per SPRU172C p.4-118: if the two MSBs of src accumulator
             * are different (not sign-extended), shift src left by 1
             * and decrement T. Otherwise do nothing. Used by the FB-det
             * correlator to normalize results; the loop exits when
             * NORM stops shifting (MSBs match = value is normalized). */
            /* FIX_NORM_SD — NORM is `norm src[,dst]`, opcode 0xF48F mask 0xFCFF
             * (tic54x-opc.c): bit 9 = src, bit 8 = dst, hence FOUR encodings
             * F48F/F58F/F68F/F78F. A 0xFEFF mask catches only F48F/F58F and reads
             * F58F as NORM B where it is NORM A,B, while F68F/F78F fall into the
             * F7 "LD #k8" block, which does not exist in this ISA.
             * [2026-09-17] Measured with c54x_exe --arm, step by step: 0x75cf
             * `f78f` NORM B wrote T=0xff8f, BRC became 0xff88 and the FB division
             * (0x75db SUBC) ran 65000 times per frame. PROM0..3 hold 78 x f78f and
             * 157 x f48f. */
            if ((op & 0xFCFF) == 0xF48F) {
                int src, dst;
                c54x_f4_srcdst(op, &src, &dst);
                int64_t val = sext40(src ? s->b : s->a);
                int bit39 = (val >> 39) & 1;
                int bit38 = (val >> 38) & 1;
                /* FIX_NORM_T — a real C54x NORM shifts the accumulator by T (the
                 * exponent produced by EXP) in one cycle and does NOT write T
                 * (SPRU172C). Shifting by 1 bit and decrementing T instead leaves
                 * isolated exp;norm pairs (correlator reference 0x796c/0x796d, SNR
                 * division 0x79b1) never rescaled to full scale: the reference
                 * collapses to {0,-1} and the SNR is tiny. The firmware confirms
                 * it: 0x79ca stores #1 into T then runs `rptb norm A`, a
                 * data-dependent variable shift that only makes sense if NORM
                 * shifts by T. */
                {
                    /* SPRU172C 4-122: the shift is T(5-0), a 6-bit two's complement
                     * count (-16..31). Example NORM B, A with T = 0FF9h shifts by
                     * 39h = -7, not by 4089. */
                    int t = s->t & 0x3F;
                    if (t & 0x20) t -= 64;
                    if (t >= 0) val = sext40(val << t);
                    else        val = sext40(val >> (-t));   /* arithmetic shift (SXM) */
                    if (dst) s->b = val; else s->a = val;
                    /* Do NOT write T: SPRU172C has NORM read T, never set it. */
                }
                norm_tc(s, bit39, bit38);
                return consumed + s->lk_used;
            }

            /* F490/F590: ROR src (mask FEFF, 1 word) */
            if ((op & 0xFEFF) == 0xF490) {
                /* SPRU172C 4-150: 32-bit rotate. C -> src(31); src(31-1) -> src(30-0);
                 * src(0) -> C; 0 -> src(39-32). [2026-09-20] Was a 40-bit rotate. */
                int src = (op >> 8) & 1;
                int64_t *acc = src ? &s->b : &s->a;
                uint32_t v = (uint32_t)(*acc & 0xFFFFFFFFULL);
                uint32_t c = c54x_carry_in(s);
                uint32_t lsb = v & 1;
                *acc = (int64_t)((v >> 1) | (c << 31));
                if (lsb) s->st0 |= ST0_C; else s->st0 &= ~ST0_C;
                return consumed + s->lk_used;
            }

            /* F491/F591: ROL src (mask FEFF, 1 word) */
            if ((op & 0xFEFF) == 0xF491) {
                /* SPRU172C 4-148: 32-bit rotate. C -> src(0); src(30-0) -> src(31-1);
                 * src(31) -> C; 0 -> src(39-32). [2026-09-20] Was a 40-bit rotate:
                 * example ROL A with A = 00 B000 1234, C = 0 gives 00 6000 2468, C = 1. */
                int src = (op >> 8) & 1;
                int64_t *acc = src ? &s->b : &s->a;
                uint32_t v = (uint32_t)(*acc & 0xFFFFFFFFULL);
                uint32_t c = c54x_carry_in(s);
                uint32_t msb = (v >> 31) & 1;
                *acc = (int64_t)((v << 1) | c);
                if (msb) s->st0 |= ST0_C; else s->st0 &= ~ST0_C;
                return consumed + s->lk_used;
            }

            /* F488-F48B (mask FCFF, 1 word): MACA T,src[,dst] / MACAR / MASA T / MASAR.
             * SPRU172C 4-86, 4-95: dst = src +/- T x A(32-16), rounded for the R
             * forms. The multiplier is ALWAYS A's high word, whatever src is, and
             * the base is src (not dst). [2026-09-20] The old MACA multiplied T by
             * src's high word and accumulated into dst; MACAR/MASA/MASAR had no
             * handler and fell into the F7 "LD #k8" block, which wrote T. Manual
             * example MACA T, B, B (A=1234 0000, B=2 0000, T=0444, FRCT=1) gives
             * B = 00 009D 4BA0. */
            if ((op & 0xFCFC) == 0xF488) {
                int src, dst; c54x_f4_srcdst(op, &src, &dst);
                int64_t ahi  = (int64_t)(int16_t)((s->a >> 16) & 0xFFFF);
                int64_t prod = (int64_t)(int16_t)s->t * ahi;
                if (s->st1 & ST1_FRCT) prod <<= 1;
                int64_t base = sext40(src ? s->b : s->a);
                int64_t r = (op & 2) ? base - prod : base + prod;   /* F48A/B subtract */
                if (op & 1) r = (r + 0x8000) & ~(int64_t)0xFFFF;    /* F489/B round */
                if (dst) s->b = sext40(r); else s->a = sext40(r);
                return consumed + s->lk_used;
            }

            /* F493/F593: CMPL src (complement, mask FCFF, 1 word). binutils:
             * "cmpl" 1,1,2, 0xF493, 0xFCFF, {OP_SRC,OPT|OP_DST}. The FCFF mask
             * (not FEFF) is what admits the SRC=B variant through bit 9. */
            if ((op & 0xFCFF) == 0xF493) {
                /* [2026-10-15] src = bit 9, dst = bit 8 (c54x_f4_srcdst, binutils
                 * SRC/DST), as for NEG/ABS/RND: `CMPL A, B` (F593) complemented
                 * B into B. isa_test 39. The ROM only uses F493 (src = dst = A). */
                int src, dst; c54x_f4_srcdst(op, &src, &dst);
                int64_t v = sext40(~(src ? s->b : s->a) & 0xFFFFFFFFFFULL);
                if (dst) s->b = v; else s->a = v;
                return consumed + s->lk_used;
            }

            /* F49F/F59F: RND src (round, mask FCFF, 1 word). binutils:
             * "rnd" 1,1,2, 0xF49F, 0xFCFF, {OP_SRC,OPT|OP_DST}. */
            if ((op & 0xFCFF) == 0xF49F) {
                /* [2026-09-21] src/dst bits as for NEG/ABS (c54x_f4_srcdst):
                 * `RND A, B` (F59F) rounded B into B. isa_test 170. */
                int src, dst; c54x_f4_srcdst(op, &src, &dst);
                int64_t v = sext40((src ? s->b : s->a) + 0x8000);
                if (dst) s->b = v; else s->a = v;
                return consumed + s->lk_used;
            }

            /* F480/F580: ADD src,ASM,dst (mask FCFF, 1 word) */
            if ((op & 0xFCFF) == 0xF480) {
                int src, dst; c54x_f4_srcdst(op, &src, &dst);   /* FIX_F4XX_SRCDST */
                int64_t sv = sext40(src ? s->b : s->a);
                /* ADD src,ASM,dst: dst = dst + (src << ASM) (SPRU172C). The ROM's FR encoder (RPE,
                 * PDROM 0xeac1 `add b,asm,a` = xM << (6 - exp)) relies on the ASM shift; without it
                 * every pulse comes out at 3/4 (~0) and uplink speech is reduced to noise.
                 * CALYPSO_ADDASM_ANCIEN=1: shift ignored. */
                static int addasm_ancien = -1;
                if (addasm_ancien < 0) addasm_ancien = calypso_gate("CALYPSO_ADDASM_ANCIEN", 0);
                if (!addasm_ancien) { int sh = asm_shift(s); if (sh >= 0) sv <<= sh; else sv >>= -sh; sv = sext40(sv); }
                if (dst) s->b = sext40(s->b + sv); else s->a = sext40(s->a + sv);
                return consumed + s->lk_used;
            }

            /* F481/F581: SUB src,ASM,dst (mask FCFF, 1 word) */
            if ((op & 0xFCFF) == 0xF481) {
                int src, dst; c54x_f4_srcdst(op, &src, &dst);   /* FIX_F4XX_SRCDST */
                int64_t sv = sext40(src ? s->b : s->a);
                /* SUB src,ASM,dst: dst = dst - (src << ASM) (SPRU172C). Same ASM shift as ADD
                 * src,ASM,dst above (the ROM's FR encoder relies on it).
                 * CALYPSO_ADDASM_ANCIEN=1: shift ignored. */
                static int addasm_ancien = -1;
                if (addasm_ancien < 0) addasm_ancien = calypso_gate("CALYPSO_ADDASM_ANCIEN", 0);
                if (!addasm_ancien) { int sh = asm_shift(s); if (sh >= 0) sv <<= sh; else sv >>= -sh; sv = sext40(sv); }
                if (dst) s->b = sext40(s->b - sv); else s->a = sext40(s->a - sv);
                return consumed + s->lk_used;
            }

            /* F482/F582: LD src,ASM,dst (mask FCFF, 1 word).
             * The ASM shift (ST1[4:0], two's complement, -16..15; SPRU172C "LD src, ASM [, dst]:
             * dst = src << ASM") is applied. The ROM's uplink burst transmitter (PROM0 0x8613/0x8625
             * `ld b,asm` after `ld DP+0x48,asm`) accumulates its bits through this shift; without it
             * the 16 BULDATA words of the burst all come out as 0xff06.
             * CALYPSO_LDASM_ANCIEN=1: shift ignored (dst receives src as is). */
            if ((op & 0xFCFF) == 0xF482) {
                int src, dst; c54x_f4_srcdst(op, &src, &dst);   /* FIX_F4XX_SRCDST */
                int64_t sv = sext40(src ? s->b : s->a);
                static int ldasm_ancien = -1;
                if (ldasm_ancien < 0) { const char *e = getenv("CALYPSO_LDASM_ANCIEN"); ldasm_ancien = (e && *e == '1') ? 1 : 0; }
                if (!ldasm_ancien) { int sh = asm_shift(s); if (sh >= 0) sv <<= sh; else sv >>= -sh; }
                if (dst) s->b = sext40(sv); else s->a = sext40(sv);
                return consumed + s->lk_used;
            }

            /* CMPR CC, ARx -- binutils tic54x-opc.c {"cmpr", 0xF4A8, mask 0xFCF8, {OP_CC3, OP_ARX}}:
             * compares ARx (bits 2-0) with AR0, 16 bits unsigned, and sets TC; CC = bits 9-8: 0 EQ, 1 LT,
             * 2 GT, 3 NEQ (SPRU172C). The ROM's uplink burst transmitter (PROM0 0x8608 `cmpr eq,ar2 ;
             * rc tc`, AR0 = end of buffer) depends on it: with a stale TC it returns at once and never
             * writes the 16 BULDATA words of the burst (RACH, NB, TCH). 78 CMPR sites in PROM0.
             * CALYPSO_CMPR_ANCIEN=1: F4A8-F4AF treated as `unimpl` (NOP, TC unchanged). */
            if ((op & 0xFCF8) == 0xF4A8) {
                static int cmpr_ancien = -1;
                if (cmpr_ancien < 0) { const char *e = getenv("CALYPSO_CMPR_ANCIEN"); cmpr_ancien = (e && *e == '1') ? 1 : 0; }
                if (!cmpr_ancien) {
                    unsigned cc = (op >> 8) & 3, arx = op & 7;
                    uint16_t x = s->ar[arx], r = s->ar[0];
                    bool tc = cc == 0 ? (x == r) : cc == 1 ? (x < r) : cc == 2 ? (x > r) : (x != r);
                    if (tc) s->st0 |= ST0_TC; else s->st0 &= ~ST0_TC;
                    return consumed + s->lk_used;
                }
            }

            /* F4xx accumulator shift/load (1-word, mask FCE0):
             * F400: ADD src,shift,dst  F420: SUB  F440: LD  F460: SFTA */
            if ((op & 0xFCE0) == 0xF400) {
                int src, dst; c54x_f4_srcdst(op, &src, &dst);   /* FIX_F4XX_SRCDST */
                int shift = op & 0x1F; if (shift > 15) shift -= 32;
                int64_t sv = sext40(src ? s->b : s->a);
                if (shift >= 0) sv <<= shift; else sv >>= (-shift);
                int64_t dv = sext40(dst ? s->b : s->a);
                c54x_carry_add40(s, dv, sv);                     /* [2026-09-21] isa_test 6 */
                if (dst) s->b = sext40(dv + sv); else s->a = sext40(dv + sv);
                return consumed + s->lk_used;
            }

            if ((op & 0xFCE0) == 0xF420) {
                int src, dst; c54x_f4_srcdst(op, &src, &dst);   /* FIX_F4XX_SRCDST */
                int shift = op & 0x1F; if (shift > 15) shift -= 32;
                int64_t sv = sext40(src ? s->b : s->a);
                if (shift >= 0) sv <<= shift; else sv >>= (-shift);
                int64_t dv = sext40(dst ? s->b : s->a);
                c54x_carry_sub40(s, dv, sv);                     /* [2026-09-21] isa_test 227 */
                if (dst) s->b = sext40(dv - sv); else s->a = sext40(dv - sv);
                return consumed + s->lk_used;
            }

            if ((op & 0xFCE0) == 0xF440) {
                int src, dst; c54x_f4_srcdst(op, &src, &dst);   /* FIX_F4XX_SRCDST */
                int shift = op & 0x1F; if (shift > 15) shift -= 32;
                int64_t sv = sext40(src ? s->b : s->a);
                if (shift >= 0) sv <<= shift; else sv >>= (-shift);
                if (dst) s->b = sext40(sv); else s->a = sext40(sv);
                return consumed + s->lk_used;
            }

            if ((op & 0xFCE0) == 0xF460) {
                /* SFTA body lives in c54x_sfta_exec() (FIX_SFTA_CARRY): two
                 * identical sites call it, so a fix applied to one cannot make
                 * them diverge. */
                c54x_sfta_exec(s, op);
                return consumed + s->lk_used;
            }

            /* ⚠️ SFTL must exclude the RSBX/SSBX encodings. Per binutils
             * tic54x-opc.c, a low-byte high nibble of 0xB (bits 7:4 = 1011) is
             * ALWAYS rsbx/ssbx for every hi8 in {F4,F5,F6,F7} (bit 9 = ST0/ST1,
             * bit 8 = rsbx/ssbx) and NEVER a legal SFTL shift amount. The 0xFCE0
             * mask below leaves bit 4 don't-care and is not keyed on hi8, so
             * without the explicit exclusion it swallows 0xF4Bx/F5Bx/F6Bx/F7Bx
             * (including RSBX INTM=0xF6BB and SSBX INTM=0xF7BB) as a bogus
             * accumulator shift, BEFORE the real hi8==0xF6/0xF7 handlers further
             * down ever run, and INTM is never cleared.
             * The exclusion is unconditional. */
            /* [2026-09-20] 0xF4A0-0xF4A7 is LD #k3, ARP (binutils {"ld", 0xF4A0,
             * 0xFFF8, {OP_k3, OP_ARP}}), NOT an SFTL: SFTL is 0xF0E0/0xFCE0 and is
             * handled with AND/OR/XOR src,shift,dst. Manual example LD 3, ARP -> ARP = 3. */
            if ((op & 0xFFF8) == 0xF4A0) {
                s->st0 = (s->st0 & ~ST0_ARP_MASK) | ((op & 7) << ST0_ARP_SHIFT);
                return consumed + s->lk_used;
            }

        /* F494/F594: SFTC src (mask FEFF, 1 word).
         * Per SPRU172C p.4-264: shift src left by 1 if src(31)==src(30)
         * and src!=0. Used by FB-det normalisation around PC=0x10e5..0x10f4
         * — without it the correlator sums never normalise. */
        if ((op & 0xFEFF) == 0xF494) {
            int src = (op >> 8) & 1;
            int64_t *acc = src ? &s->b : &s->a;
            int64_t val = sext40(*acc);
            if (val != 0) {
                int b31 = (val >> 31) & 1;
                int b30 = (val >> 30) & 1;
                if (b31 == b30) *acc = sext40(val << 1);
            }
            return consumed + s->lk_used;
        }

        if (hi8 == 0xF4) {
            /* F4xx: unconditional branch/call and special instructions.
             * Some F4xx instructions are 1-word (FRET, FRETE, RETE, TRAP, NOP, etc.)
             * Must check specific opcodes BEFORE the 2-word switch. */

            /* NOP — F495 per SPRU172C p.4-121 */
            if (op == 0xF495) {
                return 1; /* 1-word NOP */
            }
            /* TRAP K — F4C0-F4DF per SPRU172C p.4-195:
             * SP-1, PC+1 → TOS, vector(IPTR*128 + K*4) → PC */
            if ((op & 0xFFE0) == 0xF4C0) {
                int k = op & 0x1F;
                s->sp--;
                data_write(s, s->sp, (uint16_t)(s->pc + 1));
                uint16_t iptr = (s->pmst >> PMST_IPTR_SHIFT) & 0x1FF;
                s->pc = (iptr * 0x80) + k * 4;
                C54_LOG("TRAP-FIRED #%d → PC=0x%04x (from PC=0x%04x) [XPC non sauve : corruption vs RETE pop-2 si fire]", k, s->pc,
                        (uint16_t)(s->pc - (iptr * 0x80 + k * 4) + 1 - 1));
                return 0;
            }

            /* F4xx arithmetic instructions (1-word, per tic54x-opc.c).
             * These MUST be checked before the 2-word branch/call switch. */
            {
                /* F483/F583: SAT src (mask FEFF, 1 word) */
                if ((op & 0xFEFF) == 0xF483) {
                    int src = (op >> 8) & 1;
                    int64_t *acc = src ? &s->b : &s->a;
                    int64_t val = sext40(*acc);
                    if (val > 0x7FFFFFFFLL) *acc = sext40(0x7FFFFFFFLL);
                    else if (val < -0x80000000LL) *acc = sext40(-0x80000000LL);
                    return consumed + s->lk_used;
                }
                /* F484/F584: NEG src[,dst] (mask FCFF, 1 word) */
                if ((op & 0xFCFF) == 0xF484) {
                    int src, dst; c54x_f4_srcdst(op, &src, &dst);   /* FIX_F4XX_SRCDST */
                    int64_t val = sext40(src ? s->b : s->a);
                    if (dst) s->b = sext40(-val); else s->a = sext40(-val);
                    return consumed + s->lk_used;
                }
                /* F485/F585: ABS src[,dst] (mask FCFF, 1 word) */
                if ((op & 0xFCFF) == 0xF485) {
                    int src, dst; c54x_f4_srcdst(op, &src, &dst);   /* FIX_F4XX_SRCDST */
                    int64_t val = sext40(src ? s->b : s->a);
                    if (val < 0) val = -val;
                    if (dst) s->b = sext40(val); else s->a = sext40(val);
                    return consumed + s->lk_used;
                }
                /* F48C/F58C: MPYA dst (mask FEFF, 1 word)
                 * Multiply T * A(high), accumulate into dst */
                if ((op & 0xFEFF) == 0xF48C) {
                    int dst = (op >> 8) & 1;
                    int64_t prod = (int64_t)(int16_t)s->t * (int64_t)(int16_t)((s->a >> 16) & 0xFFFF);
                    if (s->st1 & ST1_FRCT) prod <<= 1;
                    if (dst) s->b = sext40(s->b + prod); else s->a = sext40(s->a + prod);
                    return consumed + s->lk_used;
                }
                /* F48D/F58D: SQUR A,dst (mask FEFF, 1 word) */
                if ((op & 0xFEFF) == 0xF48D) {
                    int dst = (op >> 8) & 1;
                    int16_t ah = (int16_t)((s->a >> 16) & 0xFFFF);
                    int64_t prod = (int64_t)ah * (int64_t)ah;
                    if (s->st1 & ST1_FRCT) prod <<= 1;
                    if (dst) s->b = sext40(prod); else s->a = sext40(prod);
                    return consumed + s->lk_used;
                }
                /* F48E/F58E: EXP src (mask FEFF, 1 word)
                 * Count leading sign bits of accumulator, store in T */
                if ((op & 0xFEFF) == 0xF48E) {
                    int src = (op >> 8) & 1;
                    int64_t val = sext40(src ? s->b : s->a);
                    int exp = 0;
                    if (val == 0 || val == -1) { exp = 31; }
                    else {
                        uint64_t uv = (val < 0) ? ~val : val;
                        uv &= 0xFFFFFFFFFFULL;
                        /* Count leading zeros from bit 38 down */
                        for (int i = 38; i >= 0; i--) {
                            if (uv & (1ULL << i)) break;
                            exp++;
                        }
                        exp -= 8; /* EXP = leading sign bits - 8 */
                    }
                    s->t = (uint16_t)(int16_t)exp;
                    return consumed + s->lk_used;
                }
                /* F48F/F58F: NORM — handled below (real implementation, not NOP) */
                /* F492/F592: MAX src (mask FEFF, 1 word) — keep max of A,B */
                if ((op & 0xFEFF) == 0xF492) {
                    int64_t sa = sext40(s->a), sb = sext40(s->b);
                    if (sa < sb) { s->a = s->b; s->st0 |= ST0_C; }
                    else { s->st0 &= ~ST0_C; }
                    return consumed + s->lk_used;
                }
                /* F493/F593: MIN src (mask FEFF, 1 word) — keep min of A,B */
                if ((op & 0xFEFF) == 0xF493) {
                    int64_t sa = sext40(s->a), sb = sext40(s->b);
                    if (sa > sb) { s->a = s->b; s->st0 |= ST0_C; }
                    else { s->st0 &= ~ST0_C; }
                    return consumed + s->lk_used;
                }
                /* F49E/F59E: SUBC src (mask FEFF, 1 word) — conditional subtract for division */
                if ((op & 0xFEFF) == 0xF49E) {
                    int src = (op >> 8) & 1;
                    int64_t *acc = src ? &s->b : &s->a;
                    int64_t val = sext40(*acc);
                    if (val >= 0) { *acc = sext40((val << 1) + 1); }
                    else { *acc = sext40(val << 1); }
                    return consumed + s->lk_used;
                }
                /* F48F/F58F: NORM src[, dst] (mask FEFF, 1 word)
                 * Per SPRU172C p.4-118: if the two MSBs of src accumulator
                 * are different (not sign-extended), shift src left by 1
                 * and decrement T. Otherwise do nothing. Used by the FB-det
                 * correlator to normalize results; the loop exits when
                 * NORM stops shifting (MSBs match = value is normalized). */
              /* Widened NORM mask. binutils: norm 0xF48F/0xFCFF, {OP_SRC, OP_DST}
               * -- bit 9 = source, bit 8 = destination, hence FOUR encodings
               * 0xF48F 0xF58F 0xF68F 0xF78F. A `(op & 0xFEFF)` test frees bit 8
               * only, so 0xF68F and 0xF78F go undecoded.
               * [2026-08-23] PROM0 holds 29 NORM, of which 10 were missed: 0x75cf
               * (the EXP/NORM sequence that builds B just before 0x75d3
               * STLM B,BRC, hence the absurd BRC and the ~50000-iteration SUBC
               * loop), 0x796d and 0x79ad (correlator reference, SNR division), and
               * 0x9a08 0x9a27 0x9a4c in the Viterbi region.
               * ⚠️ Global effect: correlator, division and Viterbi all shift. */
              if ((op & 0xFCFF) == 0xF48F && (op & 0xFEFF) != 0xF48F) {
                  /* the two encodings the narrow mask missed */
                  int nsrc = (op >> 9) & 1, ndst = (op >> 8) & 1;
                  int64_t nv = sext40(nsrc ? s->b : s->a);
                  int b39 = (nv >> 39) & 1, b38 = (nv >> 38) & 1;
                  int16_t t = (int16_t)s->t;
                  nv = (t >= 0) ? sext40(nv << t) : sext40(nv >> (-t));
                  if (ndst) s->b = nv; else s->a = nv;
                  norm_tc(s, b39, b38);
                  return consumed + s->lk_used;
              }
              if ((op & 0xFEFF) == 0xF48F) {
                  int src = (op >> 8) & 1;
                  int64_t val = sext40(src ? s->b : s->a);
                    int bit39 = (val >> 39) & 1;
                    int bit38 = (val >> 38) & 1;
                    /* Second copy: NORM shifts by T, not by 1 bit, and does not
                     * write T. */
                    {
                        int16_t t = (int16_t)s->t;
                        if (t >= 0) val = sext40(val << t);
                        else        val = sext40(val >> (-t));
                        if (src) s->b = val; else s->a = val;
                    }
                    norm_tc(s, bit39, bit38);
                    return consumed + s->lk_used;
                }
                /* F49F: DELAY (pipeline flush, NOP) */
                if (op == 0xF49F) { return consumed + s->lk_used; }
                /* F490/F590: ROR src (mask FEFF, 1 word) */
                if ((op & 0xFEFF) == 0xF490) {
                    int src = (op >> 8) & 1;
                    int64_t *acc = src ? &s->b : &s->a;
                    uint16_t c = c54x_carry_in(s); /* carry */
                    uint16_t lsb = *acc & 1;
                    *acc = sext40(((uint64_t)(*acc & 0xFFFFFFFFFFULL) >> 1) | ((uint64_t)c << 39));
                    if (lsb) s->st0 |= ST0_C; else s->st0 &= ~ST0_C;
                    return consumed + s->lk_used;
                }
                /* F491/F591: ROL src (mask FEFF, 1 word) */
                if ((op & 0xFEFF) == 0xF491) {
                    int src = (op >> 8) & 1;
                    int64_t *acc = src ? &s->b : &s->a;
                    uint16_t c = c54x_carry_in(s);
                    uint16_t msb = (*acc >> 39) & 1;
                    *acc = sext40(((*acc << 1) & 0xFFFFFFFFFFULL) | c);
                    if (msb) s->st0 |= ST0_C; else s->st0 &= ~ST0_C;
                    return consumed + s->lk_used;
                }
                /* F488/F588: MACA T,src[,dst] (mask FCFF, 1 word) */
                if ((op & 0xFCFF) == 0xF488) {
                    int src, dst; c54x_f4_srcdst(op, &src, &dst);   /* FIX_F4XX_SRCDST */
                    int64_t prod = (int64_t)(int16_t)s->t * (int64_t)(int16_t)((src ? s->b : s->a) >> 16);
                    if (s->st1 & ST1_FRCT) prod <<= 1;
                    if (dst) s->b = sext40(s->b + prod); else s->a = sext40(s->a + prod);
                    return consumed + s->lk_used;
                }
                /* F486/F586: CMPL src (complement, mask FEFF, 1 word) */
                if ((op & 0xFEFF) == 0xF486) {
                    int src = (op >> 8) & 1;
                    int64_t *acc = src ? &s->b : &s->a;
                    *acc = sext40(~(*acc) & 0xFFFFFFFFFFULL);
                    return consumed + s->lk_used;
                }
                /* F487/F587: RND src (round, mask FEFF, 1 word) */
                if ((op & 0xFEFF) == 0xF487) {
                    int src = (op >> 8) & 1;
                    int64_t *acc = src ? &s->b : &s->a;
                    *acc = sext40(*acc + 0x8000);
                    return consumed + s->lk_used;
                }
                /* F480/F580: ADD src,ASM,dst (mask FCFF, 1 word) */
                if ((op & 0xFCFF) == 0xF480) {
                    int src, dst; c54x_f4_srcdst(op, &src, &dst);   /* FIX_F4XX_SRCDST */
                    int64_t sv = sext40(src ? s->b : s->a);
                    /* ADD src,ASM,dst: dst = dst + (src << ASM) (SPRU172C), 2nd copy of the
                     * handler above (the ROM's FR encoder relies on the ASM shift).
                     * CALYPSO_ADDASM_ANCIEN=1: shift ignored. */
                    static int addasm_ancien = -1;
                    if (addasm_ancien < 0) addasm_ancien = calypso_gate("CALYPSO_ADDASM_ANCIEN", 0);
                    if (!addasm_ancien) { int sh = asm_shift(s); if (sh >= 0) sv <<= sh; else sv >>= -sh; sv = sext40(sv); }
                    if (dst) s->b = sext40(s->b + sv); else s->a = sext40(s->a + sv);
                    return consumed + s->lk_used;
                }
                /* F481/F581: SUB src,ASM,dst (mask FCFF, 1 word) */
                if ((op & 0xFCFF) == 0xF481) {
                    int src, dst; c54x_f4_srcdst(op, &src, &dst);   /* FIX_F4XX_SRCDST */
                    int64_t sv = sext40(src ? s->b : s->a);
                    /* SUB src,ASM,dst: dst = dst - (src << ASM) (SPRU172C), 2nd copy of the
                     * handler above. CALYPSO_ADDASM_ANCIEN=1: shift ignored. */
                    static int addasm_ancien = -1;
                    if (addasm_ancien < 0) addasm_ancien = calypso_gate("CALYPSO_ADDASM_ANCIEN", 0);
                    if (!addasm_ancien) { int sh = asm_shift(s); if (sh >= 0) sv <<= sh; else sv >>= -sh; sv = sext40(sv); }
                    if (dst) s->b = sext40(s->b - sv); else s->a = sext40(s->a - sv);
                    return consumed + s->lk_used;
                }
                /* F482/F582: LD src,ASM,dst (mask FCFF, 1 word) -- 2nd copy, same ASM shift as the
                 * 1st one (see above). */
                if ((op & 0xFCFF) == 0xF482) {
                    int src, dst; c54x_f4_srcdst(op, &src, &dst);   /* FIX_F4XX_SRCDST */
                    int64_t sv = sext40(src ? s->b : s->a);
                    static int ldasm_ancien2 = -1;
                    if (ldasm_ancien2 < 0) { const char *e = getenv("CALYPSO_LDASM_ANCIEN"); ldasm_ancien2 = (e && *e == '1') ? 1 : 0; }
                    if (!ldasm_ancien2) { int sh = asm_shift(s); if (sh >= 0) sv <<= sh; else sv >>= -sh; }
                    if (dst) s->b = sext40(sv); else s->a = sext40(sv);
                    return consumed + s->lk_used;
                }
                /* F4xx accumulator shift/load (1-word, mask FCE0):
                 * F400: ADD src,shift,dst  F420: SUB  F440: LD  F460: SFTA */
                if ((op & 0xFCE0) == 0xF400) {
                    int src, dst; c54x_f4_srcdst(op, &src, &dst);   /* FIX_F4XX_SRCDST */
                    int shift = op & 0x1F; if (shift > 15) shift -= 32;
                    int64_t sv = sext40(src ? s->b : s->a);
                    if (shift >= 0) sv <<= shift; else sv >>= (-shift);
                    if (dst) s->b = sext40(s->b + sv); else s->a = sext40(s->a + sv);
                    return consumed + s->lk_used;
                }
                if ((op & 0xFCE0) == 0xF420) {
                    int src, dst; c54x_f4_srcdst(op, &src, &dst);   /* FIX_F4XX_SRCDST */
                    int shift = op & 0x1F; if (shift > 15) shift -= 32;
                    int64_t sv = sext40(src ? s->b : s->a);
                    if (shift >= 0) sv <<= shift; else sv >>= (-shift);
                    if (dst) s->b = sext40(s->b - sv); else s->a = sext40(s->a - sv);
                    return consumed + s->lk_used;
                }
                if ((op & 0xFCE0) == 0xF440) {
                    int src, dst; c54x_f4_srcdst(op, &src, &dst);   /* FIX_F4XX_SRCDST */
                    int shift = op & 0x1F; if (shift > 15) shift -= 32;
                    int64_t sv = sext40(src ? s->b : s->a);
                    if (shift >= 0) sv <<= shift; else sv >>= (-shift);
                    if (dst) s->b = sext40(sv); else s->a = sext40(sv);
                    return consumed + s->lk_used;
                }
                if ((op & 0xFCE0) == 0xF460) {
                    /* SFTA body lives in c54x_sfta_exec() (FIX_SFTA_CARRY). */
                    c54x_sfta_exec(s, op);
                    return consumed + s->lk_used;
                }
                /* ⚠️ Second copy of the SFTL guard, inside the hi8==0xF4 block.
                 * Here there is NO RSBX/SSBX exclusion, so 0xF4Bx reaches this
                 * SFTL path and returns before the RSBX handler below. See the
                 * unconditional guard higher up for the encoding rule (low-byte
                 * high nibble 0xB is always rsbx/ssbx per tic54x-opc.c, never a
                 * legal shift amount). */
                if ((op & 0xFFF8) == 0xF4A0) {      /* LD #k3, ARP (see the first copy) */
                    s->st0 = (s->st0 & ~ST0_ARP_MASK) | ((op & 7) << ST0_ARP_SHIFT);
                    return consumed + s->lk_used;
                }
            }
                        /* F4Bx: RSBX -- reset bit in ST0 (bit 9=0, bit 8=0).
             * Per tic54x-opc.c: RSBX 0xF4B0 mask 0xFDF0. */
            if ((op & 0xFFF0) == 0xF4B0) {
                int bit = op & 0x0F;
                s->st0 &= ~(1 << bit);
                return consumed + s->lk_used;
            }
            /* F494/F594: SFTC src (mask FEFF, 1 word).
             * Per SPRU172C p.4-264: shift src left by 1 if src(31)==src(30)
             * and src!=0. Used by FB-det normalisation around PC=0x10e5..0x10f4
             * — without it the correlator sums never normalise. */
            if ((op & 0xFEFF) == 0xF494) {
                int src = (op >> 8) & 1;
                int64_t *acc = src ? &s->b : &s->a;
                int64_t val = sext40(*acc);
                if (val != 0) {
                    int b31 = (val >> 31) & 1;
                    int b30 = (val >> 30) & 1;
                    if (b31 == b30) *acc = sext40(val << 1);
                }
                return consumed + s->lk_used;
            }
            /* Remaining F4xx: unhandled — treat as 1-word NOP */
            C54_LOG("F4xx unhandled: 0x%04x PC=0x%04x", op, s->pc);
            return consumed + s->lk_used;
        }
        if (hi8 == 0xF0 || hi8 == 0xF1) {
            /* ═══════════════════════════════════════════════════════════════
             * FIX_F1XX_ALU_LK — ADD/SUB/LD/AND/OR/XOR #lk with DST=B.
             *
             * The long-immediate ALU handlers exist, but they are nested inside
             * `if (hi8 == 0xF2)` and `if (hi8 == 0xF3)`, so an `0xF1xx` opcode can
             * never reach them, and this block holds no FCF0/FCE0/FCFF mask test,
             * only exact matches (F072/F073/F074...). The family with DST=B (bit 8)
             * was therefore not decoded at all.
             *
             * [2026-08-03] Measured with the CHAIN-B05F probe on the task
             * dispatcher:
             *     0xb060  LD          A = 0x000018   (correct)
             *     0xb062  f130 7fff   A = 0x005294   <- A clobbered
             *     0xb066  f843     -> 0xb077          bailout, 33 of 33 passes
             * `0xF130` is `AND #0x7fff, A, B` (subop=3, src_b=0 -> A, dst_b=1 -> B):
             * it must write B and leave A untouched. With the fix, 0xa5cd (RX arming)
             * runs for the first time, the dispatch chain gets past 0xb066 to 0xb070
             * and then the CALA; no observable was lost (SI 8 vs 7, camp 46 vs
             * 30/42, CHAN_REQ 15 vs 10/14).
             *
             * ⚠️ Still unexplained: where `0x5294` comes from. It is constant and
             * independent of the input, and an undecoded opcode should leave A alone
             * rather than write a constant into it. This fix makes the decoding
             * correct; it does not prove it was the only cause. If A is still
             * clobbered with the fix on, the fallback itself is at fault.
             *
             * ⚠️ Scope: this touches EVERY 0xF1xx instruction of the firmware, not
             * just the task dispatcher. The only bench that reaches SMS runs with
             * CALYPSO_DSP_RUN_C54X=0, so this path is never exercised there.
             *
             * The implementation is deliberately a conformant copy of the
             * `hi8 == 0xF3` branch (mask FCF0): same arithmetic, same shift and
             * sign handling, same src/dst selection. Any divergence between the two
             * would be one more bug, not an improvement. */
            if ((op & 0xFCF0) == 0xF000 ||  /* ADD #lk, SHIFT, src, [dst] */
                (op & 0xFCF0) == 0xF010 ||  /* SUB */
                (op & 0xFCF0) == 0xF020 ||  /* LD  */
                (op & 0xFCF0) == 0xF030 ||  /* AND */
                (op & 0xFCF0) == 0xF040 ||  /* OR  */
                (op & 0xFCF0) == 0xF050) {  /* XOR */
                op2 = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
                consumed = 2;
                int subop     = (op >> 4) & 0xF;
                int shift_raw = op & 0xF;
                /* [2026-09-20] SHFT is a 4-bit UNSIGNED field (0..15) for all six
                 * forms, ADD included: SPRU172C 4-4 "ADD #lk [, SHFT], src [, dst]",
                 * and the manual's own example ADD #4568h, 8, A, B gives
                 * B = A + (lk << 8). The signed reading turned SHFT=8..15 into a
                 * right shift. The ISA suite (tools/isa_test) caught it. */
                int shift     = shift_raw;
                /* ─────────────────────────────────────────────────────────────
                 * FIX_LK_SHFT — the shift field of these #lk forms is FOUR bits
                 * (`op & 0xF`) and must not be given the sign rule of a five-bit
                 * field: -16..15 does not fit in four bits. At 0x7d19
                 * (`f02f 0001` = LD #1, SHFT=15, A) the signed reading turns the
                 * shift into -1, lk_val becomes 1 >> 1 = 0 and the accumulator
                 * stays zero.
                 *
                 * Authority: tic54x-opc.c gives these forms OP_SHFT (unsigned),
                 * not OP_SHIFT:
                 *   { "ld",  0xF020, 0xFEF0, {OP_lk, OPT|OP_SHFT,  OP_DST} }
                 *   { "sub"/"and"/"or"/"xor" ... OPT|OP_SHFT ... }
                 *   { "add", 0xF000, 0xFCF0, {OP_lk, OPT|OP_SHIFT, ...} }  <- only
                 * hence the subop 1..5 range: ADD (subop 0) keeps the signed rule.
                 *
                 * Physical argument: the sequence is the C54x reciprocal idiom
                 *     0x7d19  ld   #0x0001, A
                 *     0x7d1b  sfta A
                 *     0x7d1c  rpt  #15
                 *     0x7d1d  subc @0x0b, A     ; 16-step division
                 * SUBC needs a left-aligned dividend or the quotient is zero by
                 * construction, so loading 1 in order to shift it RIGHT would make
                 * the whole block dead. Shifted left by 15: 0x8000 / 0x0481 = 28,
                 * a Q15 reciprocal, which is what the firmware expects.
                 *
                 * [2026-09-19] Validated on the deterministic replay fed with real
                 * SCH bursts (ptrkrysik capture, BSIC 32):
                 *   without: 0x7d19 `LD #1,SHFT=15,A` yields A = 0 -> zero dividend
                 *            -> zero quotient -> zero T -> empty coefficients
                 *   with:    A = 0x0010000000, quotient 13737 then 11987, and the
                 *            0x01dbf46a attractor -- reproduced identically by the
                 *            synthetic fixture and by the real capture -- is gone. */
                {
                    if (subop >= 1 && subop <= 5 && (shift_raw & 0x8)) {
                        /* Coverage counter: an A/B comparison is only meaningful if
                         * the fix actually fires on the bench under test. Count the
                         * cases where it changes the result (shift_raw >= 8, the only
                         * range where signed and unsigned diverge) and report
                         * periodically. */
                        static unsigned long chg = 0;
                        if (++chg == 1 || (chg % 20000) == 0)
                            fprintf(stderr, "[c54x] FIX_LK_SHFT ACTIF #%lu "
                                    "pc=0x%04x op=0x%04x subop=%d shift %d -> %d insn=%u\n",
                                    chg, s->pc, op, subop,
                                    shift_raw - 16, shift_raw, s->insn_count);
                        shift = shift_raw;          /* OP_SHFT: unsigned, 0..15 */
                    }
                }
                int src_b     = (op >> 9) & 1;
                int dst_b     = (op >> 8) & 1;
                int64_t src   = src_b ? s->b : s->a;
                /* ADD/SUB/LD take a signed lk; AND/OR/XOR an unsigned one. */
                int64_t lk_base = (subop <= 2) ? (int64_t)(int16_t)op2
                                               : (int64_t)(uint16_t)op2;
                int64_t lk_val  = (shift >= 0) ? (lk_base << shift)
                                               : (lk_base >> (-shift));
                int64_t result = src;
                switch (subop) {
                case 0x0: c54x_carry_add40(s, src, lk_val); result = src + lk_val; break;  /* [retenue] C */   /* ADD */
                case 0x1: c54x_carry_sub40(s, src, lk_val); result = src - lk_val; break;  /* [retenue] C */   /* SUB */
                case 0x2: result = lk_val;       break;   /* LD (src ignored) */
                case 0x3: result = src & lk_val; break;   /* AND */
                case 0x4: result = src | lk_val; break;   /* OR  */
                case 0x5: result = src ^ lk_val; break;   /* XOR */
                }
                if (dst_b) s->b = sext40(result); else s->a = sext40(result);
                {   /* Bounded trace, 40 lines: an ISA fix has to be observable
                     * rather than assumed. */
                    static unsigned _n = 0;
                    if (_n < 40) {
                        _n++;
                        fprintf(stderr,
                                "[c54x] FIX_F1XX_ALU_LK #%u pc=0x%04x op=0x%04x lk=0x%04x "
                                "subop=%d shift=%d src=%s dst=%s -> %s=0x%06llx insn=%u\n",
                                _n, s->pc, op, op2, subop, shift,
                                src_b ? "B" : "A", dst_b ? "B" : "A",
                                dst_b ? "B" : "A",
                                (unsigned long long)((dst_b ? s->b : s->a) & 0xFFFFFFULL),
                                s->insn_count);
                    }
                }
                return consumed + s->lk_used;
            }
            /* ⚠️ Do not add a catch-all `if (hi8 == 0xF1) { FIRS }` here: per
             * binutils tic54x-opc.c the real FIRS is 0xE000 mask 0xFF00, handled
             * in the 0xE0 case, never 0xF1xx. A catch-all doing
             * `s->a = sext40((int64_t)sum << 16)` zeroes A_low unconditionally, and
             * STL A,*AR2- at PC=0x9ac0 then writes 0 into MMR_IMR, clearing the
             * interrupt mask and wedging the DSP. 0xF1xx belongs to SFTL/AND/OR/XOR
             * (mask FCE0, base 0xF0E0), e.g. 0x9abd `f1fe` = SFTL A,-2,B. */
            /* F073: B pmad — unconditional branch (2-word).
             * Per tic54x-opc.c: 0xF073 mask 0xFFFF. */
            if (op == 0xF073) {
                op2 = prog_fetch(s, s->pc + 1);
                s->pc = op2;
                return 0;
            }
            /* F074: CALL pmad — unconditional call (2-word).
             * Per tic54x-opc.c: call 0xF074 mask 0xFFFF.
             * Push PC+2 (return address), branch to pmad.
             * NOTE: RETE is 0xF4EB (already handled above), NOT F074. */
            if (op == 0xF074) {
                op2 = prog_fetch(s, s->pc + 1);
                s->sp--;
                data_write(s, s->sp, (uint16_t)(s->pc + 2));
                s->pc = op2;
                return 0;
            }
  ×7
            /* F072: RPTB pmad — block repeat (2-word, non-delayed).
             * Per tic54x-opc.c: 0xF072 mask 0xFFFF.
             * RSA = PC+2, REA = pmad. */
            if (op == 0xF072) {
                op2 = prog_fetch(s, s->pc + 1);
                consumed = 2;
                s->rea = op2;
                s->rsa = (uint16_t)(s->pc + 2);
                s->rptb_active = true;
                s->st1 |= ST1_BRAF;
                return consumed + s->lk_used;
            }
            /* F07x: RPT/RPTZ/misc (F072-F074 handled above) */
            if (op == 0xF070) {
                /* F070: RPT #lku — repeat next instruction lku+1 times (2-word) */
                op2 = prog_fetch(s, s->pc + 1);
                consumed = 2;
                s->rpt_count = op2;
                s->rpt_active = true; s->rpt_fresh = true;
                s->pc += 2;
                return 0;
            }
            /* RPTZ needs the binutils mask 0xFEFF (bit 8 = accumulator), not an
             * exact equality:
             *     { "rptz", 2, ..., 0xF071, 0xFEFF, {OP_DST, OP_lku} }
             * 0xF071 = RPTZ A, 0xF171 = RPTZ B. [2026-08-23] With an exact test the
             * B variant fell through to `unimpl:` at 8 sites in PROM0, with three
             * consequences at once: B was not zeroed, RC was not armed (the loop ran
             * ONCE instead of lk+1 times), and 1 word was consumed instead of 2, so
             * the #lk word executed as an instruction and desynchronised the stream. */
            if ((op & 0xFEFF) == 0xF071 && op != 0xF071) {
                op2 = prog_fetch(s, s->pc + 1);
                consumed = 2;
                if ((op >> 8) & 1) s->b = 0; else s->a = 0;
                s->rpt_count = op2;
                s->rpt_active = true; s->rpt_fresh = true;
                s->pc += 2;
                return 0;
            }
            if (op == 0xF071) {
                /* F071: RPTZ dst, #lku — zero accumulator and repeat (2-word) */
                op2 = prog_fetch(s, s->pc + 1);
                consumed = 2;
                int dst = (op >> 8) & 1; /* bit 8 via FEFF mask */
                if (dst) s->b = 0; else s->a = 0;
                s->rpt_count = op2;
                s->rpt_active = true; s->rpt_fresh = true;
                s->pc += 2;
                return 0;
            }
            if ((op & 0xFFF0) == 0xF070) {
                /* F075-F07F: undefined, treat as 1-word NOP */
                return consumed + s->lk_used;
            }
            /* F0Bx/F1Bx are NOT RSBX/SSBX: per binutils tic54x-opc.c those are 0xF4B0/0xF5B0 mask 0xFDF0.
             * F0B0-F1BF is `OR A,SHIFT,dst` with SHIFT=-16..-1 (mask FCE0, base F0A0), handled by the
             * alu_op >= 8 block below. [2026-10-03] This catch-all skipped the OR and flipped an ST1 bit
             * instead. Measured on the uplink FIRE parity store (PROM0 0x8bc4 `ld *+ar3(11),16,b`,
             * 0x8bc6 `f1b0` = `or a,-16,b`, then sth/stl b, stl a): the routine 0x9013 returns
             * A = 0x673a2f3ae7, the correct 40-bit parity, but B stayed 0 so the 24 high parity bits
             * were stored as 0 -> 56 coded bits of every SDCCH block wrong. CALYPSO_F0BX_SBIT=1
             * restores the old behaviour. */
            static int f0bx_sbit = -1;
            if (f0bx_sbit < 0) { const char *e = getenv("CALYPSO_F0BX_SBIT"); f0bx_sbit = (e && *e == '1') ? 1 : 0; }
            if (f0bx_sbit && (op & 0x00F0) == 0x00B0) {
                int bit = op & 0x0F;
                int set = (op >> 8) & 1;
                int st = (op >> 5) & 1;
                if (st == 0) { if (set) s->st0 |= (1<<bit); else s->st0 &= ~(1<<bit); }
                else         { if (set) s->st1 |= (1<<bit); else s->st1 &= ~(1<<bit); }
                return consumed + s->lk_used;
            }
            /* F0xx/F1xx ALU with #lk immediate (2-word).
             * Per tic54x-opc.c: bits 7:4 = op (0=ADD,1=SUB,2=LD,3=AND,4=OR,5=XOR),
             * bit 8 = SRC (ADD/SUB/AND/OR/XOR) or DST (LD), bit 9 = DST,
             * bits 3:0 = shift. Second word = lk. */
            {
                uint8_t alu_op = (op >> 4) & 0xF;
                if (alu_op <= 5) {
                    op2 = prog_fetch(s, s->pc + 1);
                    consumed = 2;
                    int shift = op & 0xF;
                    int src_sel = (op >> 8) & 1;
                    int dst_sel = (op >> 9) & 1;
                    int64_t src_val = src_sel ? s->b : s->a;
                    int64_t *dst = (alu_op == 2)
                        ? (src_sel ? &s->b : &s->a)
                        : (dst_sel ? &s->b : &s->a);
                    int64_t lk_val;
                    if (alu_op <= 2)
                        lk_val = (int64_t)(int16_t)op2 << shift;
                    else
                        lk_val = (int64_t)(uint16_t)op2 << shift;
                    switch (alu_op) {
                    case 0: *dst = sext40(src_val + lk_val); break; /* ADD */
                    case 1: *dst = sext40(src_val - lk_val); break; /* SUB */
                    case 2: *dst = sext40(lk_val); break;           /* LD  */
                    case 3: *dst = src_val & lk_val; break;         /* AND */
                    case 4: *dst = src_val | lk_val; break;         /* OR  */
                    case 5: *dst = src_val ^ lk_val; break;         /* XOR */
                    }
                    return consumed + s->lk_used;
                }
                if (alu_op == 6) {
                    /* F06x: ADD/SUB/LD/AND/OR/XOR #lk,16 + MPY/MAC #lk */
                    uint8_t sub6 = op & 0xF;
                    op2 = prog_fetch(s, s->pc + 1);
                    consumed = 2;
                    /* [2026-10-15] binutils: add/sub/and/or/xor #lk,16,SRC,DST and mac #lk,SRC,DST
                     * are 0xF06x mask FCFF with SRC = bit 9, DST = bit 8 (same as the F2/F3 branches);
                     * ld #lk,16,DST (F062) and mpy #lk,DST (F066) are mask FEFF, DST = bit 8.
                     * The swapped reading ran F16x (`sub #94,16,a,b`, `and #7,16,a,b`, ...: 33 ROM
                     * sites in PROM1/PROM2) as src=B, dst=A. isa_test 101 (MAC #345h,A,B).
                     * CALYPSO_F16X_ANCIEN=1 restores the swapped reading. */
                    static int f16x_ancien = -1;
                    if (f16x_ancien < 0) f16x_ancien = calypso_gate("CALYPSO_F16X_ANCIEN", 0);
                    int src_sel = f16x_ancien ? ((op >> 8) & 1) : ((op >> 9) & 1);
                    int dst_sel = f16x_ancien ? ((op >> 9) & 1) : ((op >> 8) & 1);
                    int dst8    = (op >> 8) & 1;   /* LD #lk,16 / MPY #lk : DST = bit 8 */
                    int64_t src_val = src_sel ? s->b : s->a;
                    int64_t *dst = dst_sel ? &s->b : &s->a;
                    switch (sub6) {
                    case 0: c54x_carry_add40(s, src_val, (int64_t)(int16_t)op2 << 16);   /* [synthesis] C, see carry */
                            *dst = sext40(src_val + ((int64_t)(int16_t)op2 << 16)); break;
                    case 1: c54x_carry_sub40(s, src_val, (int64_t)(int16_t)op2 << 16);   /* [synthesis] C, see carry */
                            *dst = sext40(src_val - ((int64_t)(int16_t)op2 << 16)); break;
                    case 2: dst = dst8 ? &s->b : &s->a;
                            *dst = sext40((int64_t)(int16_t)op2 << 16); break;
                    case 3: *dst = src_val & ((int64_t)(uint16_t)op2 << 16); break;
                    case 4: *dst = src_val | ((int64_t)(uint16_t)op2 << 16); break;
                    case 5: *dst = src_val ^ ((int64_t)(uint16_t)op2 << 16); break;
                    case 6: /* MPY #lk, dst */
                            dst = dst8 ? &s->b : &s->a;
                            { int64_t p = (int64_t)(int16_t)s->t * (int64_t)(int16_t)op2;
                              if (s->st1 & ST1_FRCT) p <<= 1;
                              *dst = sext40(p); } break;
                    case 7: /* MAC #lk, src[,dst] */
                            { int64_t p = (int64_t)(int16_t)s->t * (int64_t)(int16_t)op2;
                              if (s->st1 & ST1_FRCT) p <<= 1;
                              *dst = sext40(src_val + p); } break;
                    default: break;
                    }
                    return consumed + s->lk_used;
                }
                if (alu_op >= 8) {
                    /* F08x-F0Fx: accumulator-to-accumulator ops (1-word).
                     * bits 7:5 = op (100=AND,101=OR,110=XOR,111=SFTL)
                     * bits 4:0 = shift (signed 5-bit), bit 9 = SRC, bit 8 = DST
                     * (binutils tic54x convention).
                     * ⚠️ Swapping the two makes 0xf1fe (SFTL A,-2,B) compute
                     * A = B>>2 instead of B = A>>2; with B=0 that zeroes A, and
                     * STL A,*AR2- then writes 0 into the IMR. Measured with the
                     * A-AT-PC tracer: 8454 fires with A_low=0 and
                     * last_writer=0xf1fe at PC=0x9abd. */
                    int src_sel = (op >> 9) & 1;   /* bit 9 = SRC */
                    int dst_sel = (op >> 8) & 1;   /* bit 8 = DST */
                    int64_t sv = src_sel ? s->b : s->a;
                    int64_t *dst = dst_sel ? &s->b : &s->a;
                    int shift = op & 0x1F;
                    if (shift > 15) shift -= 32;
                    uint8_t aop = (op >> 5) & 0x7;
                    int64_t shifted;
                    if (aop == 7) { if (shift >= 0) shifted = sv << shift; else shifted = sv >> (-shift); }
                    else          shifted = c54x_logic_shifted(sv, shift);
                    /* First operand of AND/OR/XOR is the DESTINATION
                     * (TI SPRU172C); see the block comment below. */
                    int64_t first = *dst;
                    switch (aop) {
                    /* ═══════════════════════════════════════════════════════
                     * AND/OR/XOR READ THE DESTINATION.
                     *
                     * `*dst = sv OP shifted` computes `dst = src OP (src<<SHIFT)`
                     * and throws the destination away. That is right only when
                     * D == S, wrong as soon as D != S.
                     *
                     * TI SPRU172C (Mnemonic Instruction Set, March 2001):
                     *     AND src [,SHIFT] [,dst]   dst = dst & src << SHIFT
                     *     OR  src [,SHIFT] [,dst]   dst = dst | src << SHIFT
                     *     XOR src [,SHIFT] [,dst]   dst = dst ^ src << SHIFT
                     *     Execution: (src or [dst]) OP (src) << SHIFT -> dst
                     * Form 4 encoding (1 word), verified bit by bit:
                     *     15..10 = 111100   bit9 = S   bit8 = D
                     *     7..6 = 10  bit5 = 1  4..0 = SHIFT
                     *
                     * [2026-08-04] Measured case: `0xf1a5` = 111100 0 1 10 1 00101
                     * -> src=A, dst=B, SHIFT=5, i.e. `or A, 5, B`. At 0x9719 B held
                     * 0x8000 (1<<B_BLUD, "data block present", set at 0x96dd); the
                     * old computation destroyed it, `stl *AR3,B` wrote 0x0000 into
                     * a_cd[0] and the ARM never saw a block. The real scope is every
                     * read-modify-write in the firmware, far beyond a_cd.
                     *
                     * ⚠️ `case 7` (SFTL) stays as it is: SPRU172C gives
                     *     SFTL src, SHIFT [,dst]  ->  dst = src << SHIFT
                     * SFTL is a shift and does NOT read the destination.
                     *
                     * ⚠️ Distinct from FIX_F1XX_ALU_LK, which covers only mask
                     * 0xFCF0 subops 0..5, the 2-word long-immediate forms. Here the
                     * subop is 0xA, a 1-word form.
                     * ═══════════════════════════════════════════════════════ */
                    case 4: *dst = c54x_logic_result(sext40(first) & sext40(shifted)); break;
                    case 5: *dst = c54x_logic_result(sext40(first) | sext40(shifted)); break;
                    case 6: *dst = c54x_logic_result(sext40(first) ^ sext40(shifted)); break;
                    case 7: c54x_sftl_exec(s, op); break;   /* SFTL: 32-bit, sets C */
                    default: break;
                    }
                    return consumed + s->lk_used;
                }
            }
            goto unimpl;
        }
        /* F272/F274/F273: RPTBD/CALLD/RETD — must check BEFORE LMS */
        if (op == 0xF272) {
            /* RPTBD pmad — delayed block repeat (2 words).
             * Delayed: 2 delay slots after the 2-word instruction.
             * RSA = PC + 4 (skip RPTBD + 2 delay slot words). */
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            s->rea = op2;
            s->rsa = (uint16_t)(s->pc + 4);
            s->rptb_active = true;
            s->st1 |= ST1_BRAF;
            { static int _rb=0; if (_rb<20) { C54_LOG("RPTBD PC=0x%04x REA=0x%04x RSA=0x%04x BRC=%d", s->pc, s->rea, s->rsa, s->brc); _rb++; } }
            return consumed + s->lk_used;
        }
        if (op == 0xF274) {
            /* CALLD pmad — delayed call (2 words, 2 delay slots).
             * Pushes PC+4 (return past CALLD and its 2 delay slots), then arms
             * delayed_pc/delay_slots so the 2 slots execute BEFORE the branch.
             * ⚠️ Branching immediately (s->pc = op2; return 0) skips the delay
             * slots; when a slot holds a push or a pop, the stack drifts by one
             * word, POPM ST0 picks up an orphan PC, DP turns to garbage and the
             * CALA lands in the 0x70c3 black hole. */
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            s->sp--;
            data_write(s, s->sp, (uint16_t)(s->pc + 4));
            s->delayed_pc  = op2;
            s->delay_slots = 2;
            return consumed + s->lk_used;
        }
        if (op == 0xF273) {
            /* BD pmad — delayed branch (2 words, 2 delay slots), NO stack access.
             * Per tic54x-opc.c: bd 0xF273 mask 0xFFFF. The real RETD is 0xFE00
             * (hi8==0xFE, handled further down with its pop and delay slots).
             * ⚠️ Two ways to get this wrong, both seen: treating it as RETD adds a
             * spurious pop, and branching immediately skips the 2 delay slots.
             * Either drifts SP by one word, POPM ST0 then reads an orphan PC and
             * the dispatcher CALAD ends in the 0x70c3 black hole. Like B (F073):
             * a branch, no stack, but with delay_slots = 2. */
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            s->delayed_pc  = op2;
            s->delay_slots = 2;
            return consumed + s->lk_used;
        }
        /* F2xx dispatch =====================================================
         *
         * Per binutils tic54x-opc.c the ALU masks FCF0/FCFF/FCE0 span
         * F0xx/F1xx/F2xx/F3xx with bit 9 = SRC and bit 8 = DST. F2xx was the one
         * gap: F0/F1 have a legacy handler (reversed convention, bit 8 = src, kept
         * for compatibility because the firmware is aligned on it), F3 has its own
         * dispatch, and F2 fell through to `unimpl`, which turned 0xf210 into a
         * tight loop at PC=0xfbd9. That opcode is `SUB #8,B,A` (op2=0x0008),
         * followed by BC fbe2, ALEQ: the pre-correlator wait loop. Confirmed on
         * three silicon ROM dumps (3416, 3606 and the local one).
         *
         * Coverage:
         *   - F260-F267 mask FCFF: ALU #lk,16 + MAC
         *   - F200/F210/F220/F230/F240/F250 mask FCF0: ADD/SUB/LD/AND/OR/XOR
         *     #lk,shift
         *   - F280-F2FF mask FCE0: 1-word AND/OR/XOR/SFTL src,shift,dst
         *
         * F272/F273/F274 (exact-match RPTBD/BD/CALLD) are handled above and stay
         * out of this dispatch. */
        if (hi8 == 0xF2) {
            /* F260-F267 : 2-word ALU #lk,16 + MAC #lk (mask FCFF) */
            if ((op & 0xFCFF) == 0xF060 ||  /* ADD */
                (op & 0xFCFF) == 0xF061 ||  /* SUB */
                (op & 0xFCFF) == 0xF062 ||  /* LD  */
                (op & 0xFCFF) == 0xF063 ||  /* AND */
                (op & 0xFCFF) == 0xF064 ||  /* OR  */
                (op & 0xFCFF) == 0xF065 ||  /* XOR */
                (op & 0xFCFF) == 0xF067) {  /* MAC */
                op2 = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
                consumed = 2;
                int sub = op & 0x7;
                int src_b = (op >> 9) & 1;
                int dst_b = (op >> 8) & 1;
                int64_t src = src_b ? s->b : s->a;
                int64_t result = src;
                switch (sub) {
                case 0x0: c54x_carry_add40(s, src, (int64_t)(int16_t)op2 << 16); result = src + ((int64_t)(int16_t)op2 << 16); break;  /* [retenue] C */
                case 0x1: c54x_carry_sub40(s, src, (int64_t)(int16_t)op2 << 16); result = src - ((int64_t)(int16_t)op2 << 16); break;  /* [retenue] C */
                case 0x2: result = ((int64_t)(int16_t)op2 << 16); break;
                case 0x3: result = src & (((int64_t)op2) << 16); break;
                case 0x4: result = src | (((int64_t)op2) << 16); break;
                case 0x5: result = src ^ (((int64_t)op2) << 16); break;
                case 0x7: {
                    int64_t prod = (int64_t)(int16_t)s->t * (int64_t)(int16_t)op2;
                    if (s->st1 & ST1_FRCT) prod <<= 1;
                    result = src + prod; break;
                }
                }
                if (dst_b) s->b = sext40(result); else s->a = sext40(result);
                return consumed + s->lk_used;
            }
            /* F200/F210/F220/F230/F240/F250 : 2-word ALU #lk,shift (mask FCF0) */
            if ((op & 0xFCF0) == 0xF000 ||  /* ADD */
                (op & 0xFCF0) == 0xF010 ||  /* SUB  <- 0xF210 matches here */
                (op & 0xFCF0) == 0xF020 ||  /* LD   */
                (op & 0xFCF0) == 0xF030 ||  /* AND  */
                (op & 0xFCF0) == 0xF040 ||  /* OR   */
                (op & 0xFCF0) == 0xF050) {  /* XOR  */
                op2 = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
                consumed = 2;
                int subop = (op >> 4) & 0xF;
                int shift_raw = op & 0xF;
                int shift = shift_raw;          /* SHFT: 4-bit unsigned (SPRU172C), see the F0/F1 copy */
                int src_b = (op >> 9) & 1;
                int dst_b = (op >> 8) & 1;
                int64_t src = src_b ? s->b : s->a;
                int64_t lk_signed = (subop <= 2) ? (int64_t)(int16_t)op2
                                                 : (int64_t)(uint16_t)op2;
                int64_t lk_val = (shift >= 0) ? (lk_signed << shift)
                                              : (lk_signed >> (-shift));
                int64_t result = src;
                switch (subop) {
                case 0x0: c54x_carry_add40(s, src, lk_val); result = src + lk_val; break;  /* [retenue] C */
                case 0x1: c54x_carry_sub40(s, src, lk_val); result = src - lk_val; break;  /* [retenue] C */
                case 0x2: result = lk_val; break;
                case 0x3: result = src & lk_val; break;
                case 0x4: result = src | lk_val; break;
                case 0x5: result = src ^ lk_val; break;
                }
                if (dst_b) s->b = sext40(result); else s->a = sext40(result);
                return consumed + s->lk_used;
            }
            /* F280-F2FF : 1-word shift class (AND/OR/XOR/SFTL src,shift,dst) FCE0 */
            if ((op & 0xFCE0) == 0xF080 ||  /* AND */
                (op & 0xFCE0) == 0xF0A0 ||  /* OR  */
                (op & 0xFCE0) == 0xF0C0 ||  /* XOR */
                (op & 0xFCE0) == 0xF0E0) {  /* SFTL */
                int sub = (op >> 5) & 0x7;
                int src_b = (op >> 9) & 1;
                int dst_b = (op >> 8) & 1;
                int shift_raw = op & 0x1F;
                int shift = (shift_raw & 0x10) ? (shift_raw - 32) : shift_raw;
                int64_t src = src_b ? s->b : s->a;
                int64_t result = src;
                switch (sub) {
                case 0x4: { int64_t dst_in = dst_b ? s->b : s->a;
                            int64_t sh = c54x_logic_shifted(src, shift);
                            result = c54x_logic_result(dst_in & sh);   /* dst = dst OP (src << SHIFT), SPRU172C */ break; }
                case 0x5: { int64_t dst_in = dst_b ? s->b : s->a;
                            int64_t sh = c54x_logic_shifted(src, shift);
                            result = c54x_logic_result(dst_in | sh);   /* dst = dst OP (src << SHIFT), SPRU172C */ break; }
                case 0x6: { int64_t dst_in = dst_b ? s->b : s->a;
                            int64_t sh = c54x_logic_shifted(src, shift);
                            result = c54x_logic_result(dst_in ^ sh);   /* dst = dst OP (src << SHIFT), SPRU172C */ break; }
                case 0x7:   /* SFTL src,SHIFT,DST: 32-bit logical shift, sets C */
                    c54x_sftl_exec(s, op);
                    return consumed + s->lk_used;
                }
                if (dst_b) s->b = sext40(result); else s->a = sext40(result);
                return consumed + s->lk_used;
            }
            /* F2xx unmapped: log once, then NOP. This log firing means an F2xx
             * bit pattern is still uncovered. */
            { static int f2_unm = 0;
              if (f2_unm++ < 20)
                  C54_LOG("F2xx unmapped op=0x%04x PC=0x%04x (NOP)", op, s->pc); }
            return consumed + s->lk_used;
        }
        /* ⚠️ No LMS handler belongs in the F2xx/F3xx range. Per binutils
         * tic54x-opc.c, LMS is { "lms", 1,2,2, 0xE100, 0xFF00, {OP_Xmem,OP_Ymem} },
         * i.e. hi8 == 0xE1, and that handler already exists. An LMS catch-all here
         * steals every F3xx instruction before the F3 dispatch sees it: for 0xF3E1
         * (SFTL B,1,B, 4872 sites in the firmware) it computed a junk Ymem value
         * and wrote it through data_write(s, AR1, ...), which with AR1=0 lands on
         * MMR_IMR. Measured: IMR writes 0x0000 -> {0x0540, 0x0525, 0x082b, 0xfd57,
         * 0xfacf, ...}, all at PC=0x8eb9 op=0xf3e1 with XPC=0, i.e. genuine PROM0
         * execution. F2xx outside F272/F273/F274 falls through to the F-class NOP
         * fallback; the firmware does not appear to use it. */
        /* F8xx: branches, RPT, BANZ, CALL, RET variants */
        if (hi8 == 0xF8) {
            uint8_t sub = (op >> 4) & 0xF;
            /* F820 (624 sites) and F830 (543 sites) are BC pmad,cond per
             * tic54x-opc.c (bc = 0xF800 mask 0xFF00). The dispatcher at
             * PROM0 0xb968-0xb9a4 relies on these branching when the ACC
             * comparison succeeds. Cond 0x20 = C set, cond 0x30 = ?
             * (we treat both via ACC compare for now since dispatcher uses
             * cmp-style behaviour). The full F8xx range is BC per binutils
             * but historically the firmware tolerates the legacy decode
             * for the other sub-codes — surgical override here only.
             *
             * ⚠️ Switching F82x/F83x to strict SPRU172C condition evaluation
             * (cond 0x20 = NTC, cond 0x30 = TC) wedged the DSP (stuck at
             * PC=0xcc51 or 0xfa95 depending on the run, task 24 dropping to 0).
             * Check that BITF (0x61) really sets TC before trying it again. */
            if (sub == 0x2 || sub == 0x3) {
                op2 = prog_fetch(s, s->pc + 1);
                consumed = 2;
                int64_t acc_signed = (s->a & 0x8000000000LL)
                                     ? (s->a | ~0xFFFFFFFFFFLL) : s->a;
                bool take = false;
                /* Real TC semantics (SPRU172C: F820 = bc ntc, F830 = bc tc) are used
                 * ONLY when the previous instruction actually sets TC, i.e. cmpm or
                 * bitf (0x60xx/0x61xx, mask 0xFE00); everywhere else the inherited
                 * ACC heuristic stays, because applying TC-strict across the board
                 * wedges the dispatcher sites.
                 * Without it the bootloader poll at 0xb427 froze: `bc ntc` after
                 * `cmpm #2` must leave on TC=1 (data == 2) towards bacc 0x7000,
                 * while the ACC heuristic (take when A != 0) never let it out.
                 *
                 * CALYPSO_C54X_BCTC_SM (default OFF) extends the same real BC TC
                 * semantics to 0xde0d..0xde26 only: that handshake state-machine
                 * loop ends on F830 de0d = BC TC, nothing in it sets TC (6d91 is a
                 * MAR, f5a9 an RPT fallback), so on real silicon TC=0, the branch is
                 * not taken, the loop runs ONCE and falls to 0xde28 and the setter
                 * at 0xde9c. Under the ACC heuristic (A=0 here via f0e1) it spins
                 * forever. */
                static int bctc_sm = -1;
                if (bctc_sm < 0) bctc_sm = calypso_gate("CALYPSO_C54X_BCTC_SM", 0);
                /* CALYPSO_C54X_BCTC_NORM=1: strict TC also after a NORM. Two sites in the whole
                 * ROM (PROM0 0x90a0, PROM3 0x38d60). At 0x90a0, `bitf *ar2,#8000h ; ld #1,a ;
                 * norm a ; bc 0x90a8,ntc` sets the "speech frame present" marker of the uplink
                 * TCH bursts (a_du_0 B_BLUD): the ACC heuristic (A != 0 -> jump) always clears
                 * it, and the 0xb711 gate then builds no speech burst. To be used with
                 * CALYPSO_NORM_TC=0 (NORM does not touch TC). */
                static int bctc_norm = -1;
                if (bctc_norm < 0) bctc_norm = calypso_gate("CALYPSO_C54X_BCTC_NORM", 0);
                /* CALYPSO_C54X_BCTC_ETENDU=1: strict TC also after the other instructions that
                 * SET TC on silicon -- CMPR (0xF4A8/0xFCF8, 234 sites followed by a bc tc/ntc in
                 * the ROM), BIT Xmem (0x96xx, 34), BITT (0x34xx). */
                static int bctc_etendu = -1;
                if (bctc_etendu < 0) bctc_etendu = calypso_gate("CALYPSO_C54X_BCTC_ETENDU", 0);
                bool tc_strict = ((g_prev_op & 0xFE00) == 0x6000) || c54x_tc_par_rmw() ||
                                 (bctc_sm && s->pc >= 0xde0d && s->pc <= 0xde26) ||
                                 (bctc_norm && (g_prev_op & 0xFCFF) == 0xF48F) ||
                                 (bctc_etendu && ((g_prev_op & 0xFCF8) == 0xF4A8 ||
                                                  (g_prev_op & 0xFF00) == 0x9600 ||
                                                  (g_prev_op & 0xFF00) == 0x3400 ||
                                                  /* cmpm/bitf then an ST #k / STM #k (0x76xx/0x77xx),
                                                   * which do not touch TC -- PROM0 0xb2c1..0xb2c7:
                                                   * half of the I/Q double buffer 0x0cce/0x0d2e, 50 %
                                                   * wrong choices under the ACC heuristic. */
                                                  (((g_prev_op & 0xFE00) == 0x7600) &&
                                                   (g_prev2_op & 0xFE00) == 0x6000)));
                /* CALYPSO_C54X_BCTC_COMBO=1: strict TC when the condition combines
                 * TC/NTC with C/NC and/or BIO/NBIO (low nibble != 0, e.g. f83b =
                 * BC pmad,TC,NC,BIO). The ACC heuristic only targets f820/f830; ROM:
                 * f824 x1, f825 x3, f82a x1. The group 2 fields are ANDed (SPRU172C);
                 * BIO is not modelled (true). */
                static int bctc_combo = -1;
                if (bctc_combo < 0) bctc_combo = calypso_gate("CALYPSO_C54X_BCTC_COMBO", 0);
                if (bctc_combo && (op & 0x0F) != 0) {
                    bool tc = (s->st0 & ST0_TC) != 0;
                    bool c  = (s->st0 & ST0_C) != 0;
                    take = (sub == 0x2) ? !tc : tc;
                    if ((op & 0x0C) == 0x0C) take = take && c;
                    else if ((op & 0x0C) == 0x08) take = take && !c;
                    if (take) { s->pc = op2; return 0; }
                    return consumed + s->lk_used;
                }
                if (tc_strict) {
                    bool tc = (s->st0 & ST0_TC) != 0;
                    take = (sub == 0x2) ? !tc : tc;     /* 0x2=NTC, 0x3=TC */
                } else {
                    if (sub == 0x2)      take = (acc_signed != 0);
                    else /* sub==0x3 */  take = (acc_signed == 0);
                    /* CALYPSO_BCTC_SONDE=1: sites where the ACC heuristic diverges from the
                     * real TC -- per PC, with the previous instruction (summary at exit). */
                    static int sonde = -1;
                    if (sonde < 0) { sonde = calypso_gate("CALYPSO_BCTC_SONDE", 0); if (sonde) atexit(bctc_sonde_resume); }
                    if (sonde) {
                        bool tc = (s->st0 & ST0_TC) != 0;
                        bool vrai = (sub == 0x2) ? !tc : tc;
                        bctc_sonde_note(s->pc, g_prev_op, vrai != take);
                    }
                }
                if (take) { s->pc = op2; return 0; }
                return consumed + s->lk_used;
            }
            /* Per tic54x-opc.c:
             *   F880-F8FF mask FF80 = FB pmad (FAR branch unconditional)
             * The low 7 bits of the opcode word encode the target XPC bits.
             * Calypso uses 2-bit XPC, so & 0x3 is sufficient.
             *
             * ⚠️ Treating this range as plain B pmad keeps XPC at 0 forever and
             * the DSP never reaches the PROM1 user code. */
            if ((op & 0xFF80) == 0xF880) {
                op2 = prog_fetch(s, s->pc + 1);
                consumed = 2;
                uint8_t new_xpc = (op & 0x7F) & 0x03;
                static uint64_t fb_total;
                fb_total++;
                if (fb_total <= 30 || (fb_total % 5000) == 0) {
                    C54_LOG("FB FAR #%llu PC=0x%04x → XPC=%u PC=0x%04x (was XPC=%u)",
                            (unsigned long long)fb_total, s->pc,
                            new_xpc, op2, s->xpc);
                }
                s->xpc = new_xpc;
                s->pc  = op2;
                return 0;
            }
            /* F88x..F8Bx (mask FF80=0): historic plain B pmad (NEAR), kept
             * for sub-codes that fall outside the FAR mask above. */
            if (sub >= 0x8 && sub <= 0xB) {
                op2 = prog_fetch(s, s->pc + 1);
                s->pc = op2;
                return 0;
            }
            /* F86x/F87x: BANZ *ARn, pmad — branch if ARn != 0 (2 words) */
            if (sub == 0x6 || sub == 0x7) {
                op2 = prog_fetch(s, s->pc + 1);
                int ar_idx = op & 0x07;
                if (s->ar[ar_idx] != 0) {
                    s->ar[ar_idx]--;
                    s->pc = op2;
                    return 0;
                }
                return 2;  /* skip 2 words, fall through */
            }
            /* F84x/F85x: BC pmad with an ACC condition (2 words). These are BC, NOT
             * BANZ: BANZ is 0x6Cxx, a disjoint encoding (SPRU172C). The low byte is
             * the condition: cc&0x40 selects the ACC group, cc&0x08 picks B over A,
             * cc&0x07 the test {2:GEQ 3:LT 4:NEQ 5:EQ 6:GT 7:LEQ}.
             * ⚠️ Decoding them as `BANZ ARn` tests and decrements an AR instead of
             * the accumulator. The firmware at 0x772f emits F844 = BC 0x7737,ANEQ;
             * branching on AR4 != 0 lands on the POPM ST0 at 0x7737 without its
             * PSHM ST0 (0x770d is a disjoint path), so SP climbs by one per turn
             * until it collapses. BC pushes nothing, the stack stays intact.
             * Sub-codes 0x2/0x3 keep the dialect decode above, and this is disjoint
             * from the 0x7000 catch-all (STM #lk,SP). */
            if (sub == 0x4 || sub == 0x5) {
                op2 = prog_fetch(s, s->pc + 1);
                consumed = 2;
                uint8_t cc = op & 0xFF;
                int64_t acc  = (cc & 0x08) ? s->b : s->a;
                int64_t accs = (acc & 0x8000000000LL) ? (acc | ~0xFFFFFFFFFFLL) : acc;
                bool take;
                switch (cc & 0x07) {
                case 0x2: take = (accs >= 0); break;   /* AGEQ */
                case 0x3: take = (accs <  0); break;   /* ALT  */
                case 0x4: take = (accs != 0); break;   /* ANEQ */
                case 0x5: take = (accs == 0); break;   /* AEQ  */
                case 0x6: take = (accs >  0); break;   /* AGT  */
                case 0x7: take = (accs <= 0); break;   /* ALEQ */
                default:  take = false;       break;   /* 0/1 reserved */
                }
                if (take) { s->pc = op2; return 0; }
                return consumed + s->lk_used;
            }
            /* F8Cx-F8Fx: CALL/CALLD pmad (2 words) */
            if (sub >= 0xC) {
                op2 = prog_fetch(s, s->pc + 1);
                s->sp--;
                data_write(s, s->sp, (uint16_t)(s->pc + 2));
                s->pc = op2;
                return 0;
            }
            /* F80x-F81x: BANZ pmad, Smem (2 words)
             * Per SPRU172C + tic54x-opc.c: entire F8xx range is BANZ.
             * Sind operand selects AR via op[2:0] (nar). Test pre-mod
             * value; resolve_smem applies Sind post-mod. Same off-by-ARP
             * fix as 0x6C00 / 0x6E00 BANZ/BANZD. */
            if (sub <= 0x1) {
                int nar = op & 0x07;
                uint16_t old_ar = s->ar[nar];
                addr = resolve_smem(s, op, &ind);
                op2 = prog_fetch(s, s->pc + 1);
                consumed = 2;
                if (old_ar != 0) {
                    s->pc = op2;
                    return 0;
                }
                return consumed + s->lk_used;
            }
            /* Fallback: RPT Smem (F8xx sub not handled above) */
            addr = resolve_smem(s, op, &ind);
            s->rpt_count = data_read(s, addr);
            s->rpt_active = true; s->rpt_fresh = true;
            s->pc += consumed;
            return 0;
        }
        /* F3xx: dispatch per binutils tic54x-opc.c (verified against
         * insn_template struct include/opcode/tic54x.h:85-150).
         *
         * 8 sub-families:
         *   F300-F31F  INTR k                                 1-word
         *   F320-F32F  unmapped                               (NOP fallback)
         *   F330-F35F  AND/OR/XOR #lk,SHIFT,SRC,DST  mask FCF0 2-word
         *   F360-F367  ADD/SUB/AND/OR/XOR/MAC #lk var. FCFF   2-word
         *   F368-F37F  unmapped                               (NOP fallback)
         *   F380-F39F  AND  src,SHIFT,DST            mask FCE0 1-word
         *   F3A0-F3BF  OR   src,SHIFT,DST            mask FCE0 1-word
         *   F3C0-F3DF  XOR  src,SHIFT,DST            mask FCE0 1-word
         *   F3E0-F3FF  SFTL src,SHIFT,DST            mask FCE0 1-word
         *
         * Dispatch order: most-specific masks first (FCFF -> FCF0 -> FCE0).
         *
         * ⚠️ An "F320+ -> LD #k9, DP" fallback here mis-decodes 364 firmware sites
         * and wedges the DSP at PC=0x8eb9 (0xF3E1 SFTL B,1,B). */
        if (hi8 == 0xF3) {
            /* ⚠️ No "INTR k" handler belongs at 0xF300-0xF31F. Per binutils
             * tic54x-opc.c, INTR is { "intr", 1,1,1, 0xF7C0, 0xFFE0, ... }: base
             * 0xF7C0, not 0xF300. The F3xx range belongs to the ALU #lk class
             * (mask 0xFCF0), so F310 and friends must reach the FCF0 dispatch
             * below. Measured with an INTR handler in place: 0xe9a0 `f310`, meant
             * to be `SUB #5,B,B`, pushed PC+1 and jumped into the vector table, so
             * B stayed 0, the following STLM B,AR3 at 0xe9a2 fired 10243 times with
             * AR3 = 0, BANZ fc54,*AR3- looped forever at fc50..fc6d and the INT3
             * ISR never returned, leaving INTM at 1.
             * No F7Cx site has been observed in any run, so no real INTR handler
             * has been added there yet. */

            /* F360-F367: 2-word with mask FCFF (#lk<<16 variants).
             * Most-specific mask, check first. */
            if ((op & 0xFCFF) == 0xF060 ||  /* ADD #lk<<16, src, [dst] */
                (op & 0xFCFF) == 0xF061 ||  /* SUB */
                (op & 0xFCFF) == 0xF063 ||  /* AND */
                (op & 0xFCFF) == 0xF064 ||  /* OR  */
                (op & 0xFCFF) == 0xF065 ||  /* XOR */
                (op & 0xFCFF) == 0xF067) {  /* MAC #lk, src, [dst] */
                op2 = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
                consumed = 2;
                int sub = op & 0x7;
                int src_b = (op >> 9) & 1;
                int dst_b = (op >> 8) & 1;
                int64_t src = src_b ? s->b : s->a;
                int64_t result = src;
                switch (sub) {
                case 0x0: c54x_carry_add40(s, src, (int64_t)(int16_t)op2 << 16); result = src + ((int64_t)(int16_t)op2 << 16); break;  /* [retenue] C */
                case 0x1: c54x_carry_sub40(s, src, (int64_t)(int16_t)op2 << 16); result = src - ((int64_t)(int16_t)op2 << 16); break;  /* [retenue] C */
                case 0x3: result = src & (((int64_t)op2) << 16); break;
                case 0x4: result = src | (((int64_t)op2) << 16); break;
                case 0x5: result = src ^ (((int64_t)op2) << 16); break;
                case 0x7: { /* MAC: dst = src + T * lk */
                    int64_t prod = (int64_t)(int16_t)s->t * (int64_t)(int16_t)op2;
                    if (s->st1 & ST1_FRCT) prod <<= 1;
                    result = src + prod;
                    break;
                }
                }
                if (dst_b) s->b = sext40(result); else s->a = sext40(result);
                return consumed + s->lk_used;
            }

            /* F300-F35F: 2-word with mask FCF0 (ALU #lk + 4-bit shift).
             * ADD (sub=0), SUB (sub=1), LD (sub=2), AND (sub=3), OR (sub=4),
             * XOR (sub=5).
             *
             * ⚠️ ADD/SUB/LD belong here too; without them 0xf310 = SUB #lk,B,B at
             * PC=0xe9a0 is mis-decoded, B stays 0, AR3 becomes 0 and the loop at
             * fc50 never ends. */
            if ((op & 0xFCF0) == 0xF000 ||  /* ADD #lk, SHIFT, src, [dst] */
                (op & 0xFCF0) == 0xF010 ||  /* SUB  <- 0xf310 matches here */
                (op & 0xFCF0) == 0xF020 ||  /* LD  (binutils mask FEF0, no src) */
                (op & 0xFCF0) == 0xF030 ||  /* AND */
                (op & 0xFCF0) == 0xF040 ||  /* OR */
                (op & 0xFCF0) == 0xF050) {  /* XOR */
                op2 = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
                consumed = 2;
                int subop = (op >> 4) & 0xF;
                int shift_raw = op & 0xF;
                int shift = shift_raw;          /* SHFT: 4-bit unsigned (SPRU172C), see the F0/F1 copy */
                int src_b = (op >> 9) & 1;
                int dst_b = (op >> 8) & 1;
                int64_t src = src_b ? s->b : s->a;
                /* ADD/SUB/LD : lk signed-extended ; AND/OR/XOR : lk unsigned. */
                int64_t lk_val;
                if (subop <= 2) {
                    int64_t lk_signed = (int64_t)(int16_t)op2;
                    lk_val = (shift >= 0) ? (lk_signed << shift)
                                          : (lk_signed >> (-shift));
                } else {
                    int64_t lk_unsigned = (int64_t)(uint16_t)op2;
                    lk_val = (shift >= 0) ? (lk_unsigned << shift)
                                          : (lk_unsigned >> (-shift));
                }
                int64_t result = src;
                switch (subop) {
                case 0x0: c54x_carry_add40(s, src, lk_val); result = src + lk_val; break;  /* [retenue] C */   /* ADD */
                case 0x1: c54x_carry_sub40(s, src, lk_val); result = src - lk_val; break;  /* [retenue] C */   /* SUB */
                case 0x2: result = lk_val; break;         /* LD (src ignored) */
                case 0x3: result = src & lk_val; break;   /* AND */
                case 0x4: result = src | lk_val; break;   /* OR  */
                case 0x5: result = src ^ lk_val; break;   /* XOR */
                }
                if (dst_b) s->b = sext40(result); else s->a = sext40(result);
                return consumed + s->lk_used;
            }

            /* F380-F3FF: 1-word AND/OR/XOR/SFTL src,SHIFT,DST (mask FCE0).
             * Sub-opcode in bits 7-5: 100=AND, 101=OR, 110=XOR, 111=SFTL. */
            if ((op & 0xFCE0) == 0xF080 ||  /* AND */
                (op & 0xFCE0) == 0xF0A0 ||  /* OR  */
                (op & 0xFCE0) == 0xF0C0 ||  /* XOR */
                (op & 0xFCE0) == 0xF0E0) {  /* SFTL */
                int sub = (op >> 5) & 0x7;
                int src_b = (op >> 9) & 1;
                int dst_b = (op >> 8) & 1;
                int shift_raw = op & 0x1F;
                int shift = (shift_raw & 0x10) ? (shift_raw - 32) : shift_raw;
                int64_t src = src_b ? s->b : s->a;
                int64_t result = src;
                switch (sub) {
                case 0x4: { /* AND src,SHIFT,DST: DST = SRC & (DST_in << shift) */
                    int64_t dst_in = dst_b ? s->b : s->a;
                    int64_t sh = c54x_logic_shifted(src, shift);
                    result = c54x_logic_result(dst_in & sh);   /* dst = dst OP (src << SHIFT), SPRU172C */
                    break;
                }
                case 0x5: { /* OR */
                    int64_t dst_in = dst_b ? s->b : s->a;
                    int64_t sh = c54x_logic_shifted(src, shift);
                    result = c54x_logic_result(dst_in | sh);   /* dst = dst OP (src << SHIFT), SPRU172C */
                    break;
                }
                case 0x6: { /* XOR */
                    int64_t dst_in = dst_b ? s->b : s->a;
                    int64_t sh = c54x_logic_shifted(src, shift);
                    result = c54x_logic_result(dst_in ^ sh);   /* dst = dst OP (src << SHIFT), SPRU172C */
                    break;
                }
                case 0x7:   /* SFTL src,SHIFT,DST: 32-bit logical shift, sets C */
                    c54x_sftl_exec(s, op);
                    return consumed + s->lk_used;
                }
                if (dst_b) s->b = sext40(result); else s->a = sext40(result);
                return consumed + s->lk_used;
            }

            /* F320-F32F + F368-F37F: unmapped per binutils. NOP fallback +
             * log-once for diagnostic. 9 firmware sites total. */
            {
                static int unmapped_log = 0;
                if (unmapped_log++ < 20)
                    C54_LOG("F3xx unmapped op=0x%04x PC=0x%04x (NOP)",
                            op, s->pc);
            }
            return consumed + s->lk_used;
        }
        /* F6xx: various — LD/ST acc-acc, ABDST, SACCD, etc. */
        if (hi8 == 0xF6) {
            uint8_t sub = (op >> 4) & 0xF;
            if (sub == 0x2) {
                /* F62x: LD A, dst_shift, B or LD B, dst_shift, A */
                int dst = op & 1;
                if (dst) s->b = s->a; else s->a = s->b;
                return consumed + s->lk_used;
            }
            if (sub == 0x6) {
                /* F66x: LD A/B with shift to other acc */
                int dst = op & 1;
                if (dst) s->b = s->a; else s->a = s->b;
                return consumed + s->lk_used;
            }
            if (sub == 0xB) {
                /* F6Bx: RSBX -- reset bit in ST1 (bit 9=1, bit 8=0).
                 * Per tic54x-opc.c: RSBX 0xF4B0 mask 0xFDF0 covers F6Bx. */
                int bit = op & 0x0F;
                rsbx_intm_check(s, op);  /* INTM-clear probe */
                s->st1 &= ~(1 << bit);
                return consumed + s->lk_used;
            }
            /* Delayed branches/calls/returns from PROM (per tic54x-opc.c).
             * MUST be checked BEFORE the MVDD catch-all because they share
             * the high nibbles 0xE/0x9. Without these the DSP cannot return
             * from interrupt service routines: without RETED in particular, INTM
             * stays at 1 forever, every later INT3 is blocked and the ARM/DSP frame
             * loop stalls.
             *
             * All delayed forms execute 2 delay-slot words before the jump
             * commits; we arm the existing delayed_pc/delay_slots machinery
             * (the same one RCD uses) so the slots run with the right PC. */
            if (op == 0xF6EB) {
                /* RETED — return from interrupt, enable interrupts, delayed.
                 * Pop PC, clear INTM, then run 2 delay slots before jumping. */
                uint16_t ra = data_read(s, s->sp); s->sp++;
                s->st1 &= ~ST1_INTM;
                s->delayed_pc  = ra;
                s->delay_slots = 2;
                if (g_vec28_tracing) {
                    g_vec28_trace_pops++;
                    fprintf(stderr, "[c54x] VEC28-STACK-TRACE #%u RETED(1w,no-xpc-pop) PC=0x%04x "
                            "popped=0x%04x words=1 SP_before=0x%04x SP_after=0x%04x "
                            "insn=%u\n", g_vec28_trace_pops, s->pc, ra,
                            (uint16_t)(s->sp - 1), s->sp, s->insn_count);
                    if (s->sp >= (uint16_t)(g_vec28_sp_entry + 2)) {
                        fprintf(stderr, "[c54x] VEC28-STACK-TRACE DISARMED SP=0x%04x "
                                "back at or above pre-interrupt level 0x%04x\n",
                                s->sp, g_vec28_sp_entry);
                        g_vec28_tracing = false;
                    }
                }
                {
                    static uint64_t reted_count;
                    reted_count++;
                    if (reted_count <= 20 || (reted_count % 100) == 0)
                        C54_LOG("RETED-FIRED #%llu PC=0x%04x -> ra=0x%04x SP=0x%04x INTM=0 [XPC non poppe : si fire post-XPC-fix = reopener drain, fix en pending-XPC]",
                                (unsigned long long)reted_count,
                                s->pc, ra, s->sp);
                }
                return consumed + s->lk_used;
            }
            if (op == 0xF69B) {
                /* RETFD — fast return, delayed. SPRU172C p.4-147: RETF[D] also
                 * does 0 -> INTM (it is "enable interrupts and fast return").
                 * No F69B word in PROM0..3/PDROM. */
                uint16_t ra = data_read(s, s->sp); s->sp++;
                s->st1 &= ~ST1_INTM;
                s->delayed_pc  = ra;
                s->delay_slots = 2;
                return consumed + s->lk_used;
            }
            if (op == 0xF6E2 || op == 0xF6E3) {
                /* CALAD probe at 0x8353, the site known to self-loop: dumps XPC and
                 * the full A on the first hit. CALAD preserves XPC per SPRU172C, so
                 * XPC=1 at entry means the firmware was already on the far page,
                 * while XPC=0 means it handed a far pointer to a near call. */
                if (s->pc == 0x8353) {
                    static int p8353_first = 0;
                    if (!p8353_first) {
                        p8353_first = 1;
                        C54_LOG("PROBE-CALAD-8353-FIRST insn=%u XPC=%u "
                                "A=%010llx (A_G=0x%02x A_H=0x%04x A_L=0x%04x) "
                                "B=%010llx SP=0x%04x PMST=0x%04x",
                                s->insn_count, s->xpc & 0x3,
                                (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                                (uint8_t)((s->a >> 32) & 0xFF),
                                (uint16_t)((s->a >> 16) & 0xFFFF),
                                (uint16_t)(s->a & 0xFFFF),
                                (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                                s->sp, s->pmst);
                    }
                }
                /* BACCD A / CALAD A — delayed branch/call to acc(low).
                 * 1-word op + 2 delay slots. CALAD pushes PC+3 (skip op +
                 * 2 delay slots) per TI convention (cf. CALLD which pushes
                 * PC+4 for its 2-word form). Branch is armed via the
                 * delayed_pc/delay_slots mechanism so the 2 slots run
                 * before PC commits to tgt. */
                uint16_t tgt = (uint16_t)(s->a & 0xFFFF);
                bool is_call = (op == 0xF6E3);
                static uint64_t bcd_total;
                bcd_total++;
                /* Pre-load context: dump the 8 words preceding PC (in OVLY
                 * the executor reads from DARAM, mirror that). Lets us see
                 * which LD/MAR sequence was supposed to put a valid target
                 * in A before the CALAD/BACCD. */
                int pre_ovly = (s->pmst & PMST_OVLY) && s->pc >= 0x80 && s->pc < 0x2800;
                uint16_t pre[8];
                for (int i = 0; i < 8; i++) {
                    uint16_t a = (uint16_t)(s->pc - 8 + i);
                    pre[i] = pre_ovly ? s->data[a] : s->prog[a];
                }
                if (bcd_total <= 60 || (bcd_total % 5000) == 0) {
                    C54_LOG("BCD/CAD F6E%c #%llu PC=0x%04x tgt=0x%04x A=%010llx SP=0x%04x DP=0x%03x mem[%c PC-8..-1]=%04x %04x %04x %04x %04x %04x %04x %04x%s",
                            is_call ? '3' : '2',
                            (unsigned long long)bcd_total,
                            s->pc, tgt,
                            (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                            s->sp,
                            (s->st0 & 0x1FF),
                            pre_ovly ? 'D' : 'P',
                            pre[0], pre[1], pre[2], pre[3],
                            pre[4], pre[5], pre[6], pre[7],
                            is_call ? " CALAD" : " BACCD");
                }
                if (is_call) {
                    uint16_t ret_pc = (uint16_t)(s->pc + 3);
                    s->sp = (s->sp - 1) & 0xFFFF;
                    data_write(s, s->sp, ret_pc);
                }
                s->delayed_pc  = tgt;
                s->delay_slots = 2;
                return consumed + s->lk_used;
            }
            if (op == 0xF6E4 || op == 0xF6E5) {
                /* FRETD / FRETED — far return, delayed.
                 * Pops XPC and PC unconditionally (FL_FAR); FRETED also clears INTM.
                 * ⚠️ Do not gate the pop on PMST bit 4: that bit is AVIS, not APTS,
                 * and carries no stack semantics. */
                s->xpc = data_read(s, s->sp); s->sp++;
                if (s->xpc > 3) s->xpc &= 3;
                uint16_t ra = data_read(s, s->sp); s->sp++;
                if (op == 0xF6E5) s->st1 &= ~ST1_INTM;
                if (g_vec28_tracing) {
                    g_vec28_trace_pops++;
                    fprintf(stderr, "[c54x] VEC28-STACK-TRACE #%u FRETD-FRETED(2w,symmetric) PC=0x%04x "
                            "popped=0x%04x words=2 SP_before=0x%04x SP_after=0x%04x "
                            "insn=%u\n", g_vec28_trace_pops, s->pc, ra,
                            (uint16_t)(s->sp - 2), s->sp, s->insn_count);
                    if (s->sp >= (uint16_t)(g_vec28_sp_entry + 2)) {
                        fprintf(stderr, "[c54x] VEC28-STACK-TRACE DISARMED SP=0x%04x "
                                "back at or above pre-interrupt level 0x%04x\n",
                                s->sp, g_vec28_sp_entry);
                        g_vec28_tracing = false;
                    }
                }
                s->delayed_pc  = ra;
                s->delay_slots = 2;
                return consumed + s->lk_used;
            }
            if (op == 0xF6E6 || op == 0xF6E7) {
                /* FBACCD A / FCALAD A — far delayed branch/call to A.
                 * A(22:16) -> XPC, A(15:0) -> target; the XPC update is immediate,
                 * like FRETED. FCALAD pushes XPC first, then the return PC+3, the
                 * order FRETD pops in. 2 delay slots. */
                uint16_t tgt = (uint16_t)(s->a & 0xFFFF);
                uint8_t  new_xpc = (uint8_t)((s->a >> 16) & 0xFF);
                if (new_xpc > 3) new_xpc &= 3;
                bool is_call = (op == 0xF6E7);
                static uint64_t fbcd_total;
                fbcd_total++;
                if (fbcd_total <= 10 || (fbcd_total % 5000) == 0) {
                    C54_LOG("FBCD/FCAD F6E%c #%llu PC=0x%04x tgt=0x%04x newXPC=%u A=%010llx SP=0x%04x%s",
                            is_call ? '7' : '6',
                            (unsigned long long)fbcd_total,
                            s->pc, tgt, new_xpc,
                            (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                            s->sp,
                            is_call ? " FCALAD" : " FBACCD");
                }
                if (is_call) {
                    /* FCALAD (F6E7): pushes XPC and the return PC unconditionally
                     * (FL_FAR). ⚠️ Not gated on PMST bit 4, which is AVIS. */
                    c54x_far_push(s, (uint16_t)(s->pc + 3));
                }
                s->xpc         = new_xpc;
                s->delayed_pc  = tgt;
                s->delay_slots = 2;
                return consumed + s->lk_used;
            }
            if (sub >= 0x8) {
                /* F68x-F6Fx: MVDD Xmem, Ymem — dual data-memory operand move
                 * Encoding: 1111 0110 XXXX YYYY
                 *   bit 7   = Xmod (0=inc, 1=dec)
                 *   bits 6:4 = Xar  (source AR register)
                 *   bit 3   = Ymod (0=inc, 1=dec)
                 *   bits 2:0 = Yar  (dest AR register) */
                int xar = (op >> 4) & 0x07;
                int yar = op & 0x07;
                uint16_t val = data_read(s, s->ar[xar]);
                data_write(s, s->ar[yar], val);
                if ((op >> 7) & 1) s->ar[xar]--; else s->ar[xar]++;
                if ((op >> 3) & 1) s->ar[yar]--; else s->ar[yar]++;
                return consumed + s->lk_used;
            }
            /* Other F6xx: treat as NOP for now */
            return consumed + s->lk_used;
        }
        /* F5xx: SSBX or RPT #k */
        if (hi8 == 0xF5) {
            /* F5Bx: SSBX -- set bit in ST0 (bit 9=0, bit 8=1).
             * Per tic54x-opc.c: SSBX 0xF5B0 mask 0xFDF0. */
            if ((op & 0xFFF0) == 0xF5B0) {
                int bit = op & 0x0F;
                s->st0 |= (1 << bit);
                return consumed + s->lk_used;
            }
            /* Note: 0xF5E2/F5E3 (BACC B / CALA B) are handled earlier alongside
             * their F4 counterparts, so they never reach this F5xx block. */
            /* RPT #k (short immediate) — kept as fallback, must advance PC. */
            s->rpt_count = op & 0xFF;
            s->rpt_active = true; s->rpt_fresh = true;
            s->pc += 1;
            return 0;
        }
        /* DIAG: log F7xx executions before the (buggy) LD #k8 dispatch.
         * Per tic54x-opc.c the F7xx range contains SSBX ST1 (0xF7Bx) and
         * other instructions, NOT LD #k8 (which is at E800-E9FF).
         * Caps at 5 per distinct sub-opcode to avoid spam. */
        if (hi8 == 0xF7) {
            static int f7xx_seen[256] = {0};
            int sub_idx = op & 0xFF;
            if (++f7xx_seen[sub_idx] <= 100 || (f7xx_seen[sub_idx] % 1000) == 0) {
                C54_LOG("F7xx EXEC op=0x%04x PC=0x%04x XPC=%d insn=%u",
                        op, s->pc, s->xpc, s->insn_count);
            }
        }
        /* F7Bx: SSBX bit, ST1 (incl. SSBX INTM at F7BB).
         * Per binutils tic54x-opc.c: opcode "ssbx" 0xF5B0 mask 0xFDF0,
         * where bit 9 selects ST0 (0xF5Bx) vs ST1 (0xF7Bx).
         * Symmetric counterpart of RSBX ST1 (F6Bx) handler above.
         * MUST be tested before the F7xx LD #k8 dispatch (which is
         * itself incorrect — per SPRU172C, LD #k8 lives at E800-E9FF). */
        if ((op & 0xFFF0) == 0xF7B0) {
            int bit = op & 0x0F;
            bool is_intm = (bit == 11);
            s->st1 |= (1 << bit);
            if (is_intm)
                C54_LOG("*** SSBX INTM (F7BB) *** PC=0x%04x ST1=0x%04x insn=%u",
                        s->pc, s->st1, s->insn_count);
            return consumed + s->lk_used;
        }
        /* F7xx: LD/ST #k to various registers */
        if (hi8 == 0xF7) {
            uint8_t sub = (op >> 4) & 0xF;
            uint16_t k = op & 0xFF;
            /* F7C0..F7DF = INTR k (handled elsewhere if implemented),
             * F7E0       = RESET (exact opcode, 0xFFFF mask per tic54x-opc.c)
             * F7E1..F7FF = reserved/undefined per SPRU172C.
             *   The old LD #k8 dispatch here corrupted BRC at op=0xF7E3
             *   (= sub 0xE, k=0xE3) inside the DSP idle loop @ PC=0x9b1d,
             *   making RPTB count wrong → DSP stuck in 0x9aXX..0x9bXX
             *   block. Silicon treats reserved opcodes as NOP, not LD. */
            /* FIX_F7_DELAYED — F7E2/F7E3 = BACCD B / CALAD B, F7E6/F7E7 =
             * FBACCD B / FCALAD B (tic54x-opc.c: baccd 0xF6E2 mask 0xFEFF, bit 8 =
             * accumulator). The block below files them under "reserved -> NOP",
             * which makes a CALAD B skip the call entirely; PROM0..3 hold 51 sites
             * of f7e3. Same semantics as the A variants in the F6 block: return
             * PC+3, 2 delay slots. */
            if (op == 0xF7E2 || op == 0xF7E3) {
                uint16_t tgt = (uint16_t)(s->b & 0xFFFF);
                if (op == 0xF7E3) {
                    uint16_t ret_pc = (uint16_t)(s->pc + 3);
                    s->sp = (s->sp - 1) & 0xFFFF;
                    data_write(s, s->sp, ret_pc);
                }
                s->delayed_pc  = tgt;
                s->delay_slots = 2;
                return consumed + s->lk_used;
            }
            if (op == 0xF7E6 || op == 0xF7E7) {
                uint16_t tgt = (uint16_t)(s->b & 0xFFFF);
                uint8_t  new_xpc = (uint8_t)((s->b >> 16) & 0xFF);
                if (new_xpc > 3) new_xpc &= 3;
                if (op == 0xF7E7) {
                    c54x_far_push(s, (uint16_t)(s->pc + 3));
                }
                s->xpc         = new_xpc;
                s->delayed_pc  = tgt;
                s->delay_slots = 2;
                return consumed + s->lk_used;
            }
            /* FIX_INTR_RESET — INTR K (0xF7C0 mask 0xFFE0) and RESET (0xF7E0
             * exact), binutils tic54x-opc.c, SPRU172C p.4-64 / p.4-140.
             * No PROM0..3/PDROM word at an instruction slot is F7Cx or F7E0
             * (the few F7Cx words in PROM1/PROM3/PDROM are CALL/BC/RPTBD
             * operands), so the ROM path is unchanged. The former
             * "F7Cx = LD #k8u, BK" case below is not a C54x encoding. */
            if ((op & 0xFFE0) == 0xF7C0) {
                /* INTR K: SP-1 -> SP, PC+1 -> TOS, vector(IPTR*128 + K*4) -> PC,
                 * 1 -> INTM, IFR bit of a maskable vector (K >= 16) cleared. */
                int kv = op & 0x1F;
                uint16_t iptr = (s->pmst >> PMST_IPTR_SHIFT) & 0x1FF;
                s->sp = (s->sp - 1) & 0xFFFF;
                data_write(s, s->sp, (uint16_t)(s->pc + 1));
                s->st1 |= ST1_INTM;
                if (kv >= 16) s->ifr &= (uint16_t)~(1u << (kv - 16));
                s->pc = (uint16_t)(iptr * 0x80 + kv * 4);
                return 0;
            }
            if (op == 0xF7E0) {
                /* RESET (software): (IPTR)<<7 -> PC; OVA=OVB=0, C=TC=1, ARP=0,
                 * DP=0; SXM=XF=INTM=1, ASM=BRAF=HM=C16=FRCT=CMPT=CPL=OVM=0;
                 * IFR=0. IPTR itself is not reloaded by the instruction. */
                uint16_t iptr = (s->pmst >> PMST_IPTR_SHIFT) & 0x1FF;
                s->st0 = ST0_C | ST0_TC;
                s->st1 = ST1_INTM | ST1_SXM | ST1_XF;
                s->ifr = 0;
                s->rpt_active = false;
                s->delay_slots = 0;
                s->pc = (uint16_t)(iptr * 0x80);
                return 0;
            }
            if (sub == 0xE || sub == 0xF) {
                /* F7E0..F7FF : RESET (0xF7E0 exact) + reserved.
                 * Treat as NOP — don't touch BRC. RESET (0xF7E0) would
                 * soft-reset the DSP; if firmware ever issues it we'd
                 * jump to vec 0 = 0xFF80. For now leave as NOP — has
                 * not been observed as a legitimate firmware path. */
                return consumed + s->lk_used;
            }
            switch (sub) {
            case 0x0: /* F70x: LD #k8, ASM */
                s->st1 = (s->st1 & ~ST1_ASM_MASK) | (k & ST1_ASM_MASK);
                break;
            case 0x1: /* F71x: LD #k8, AR0 */
                s->ar[0] = k; break;
            case 0x2: /* F72x: LD #k8, AR1 */
                s->ar[1] = k; break;
            case 0x⟨1⟩: s->ar[⟨2⟩] = k; break;  ×5
    ⟨⟩ = (3,2) (4,3) (5,4) (6,5) (7,6)
            case 0x8: /* F78x: LD #k8, T */
                s->t = (s->st1 & ST1_SXM) ? (uint16_t)(int8_t)k : k; break;
            case 0x9: /* F79x: LD #k8, DP */
                s->st0 = (s->st0 & ~ST0_DP_MASK) | (k & ST0_DP_MASK);
                g_last_ldp_pc = s->pc; g_last_ldp_val = (k & ST0_DP_MASK); g_last_ldp_kind = 1;
                break;
            case 0xA: /* F7Ax: LD #k8, ARP */
                s->st0 = (s->st0 & ~ST0_ARP_MASK) | ((k & 7) << ST0_ARP_SHIFT); break;
            case 0xB: s->ar[7] = k; break; /* F7Bx: LD #k8, AR7 */
            case 0xC: /* F7Cx: LD #k8u, BK */
                /* Second BK write site (LD #k8,BK): names the writer and the value.
                 * BK=0 breaks circular addressing and sends AR2 running away
                 * (0xfa98/0xf17c). Remove together with the MMR_BK probe. */
                {
                    static uint32_t bkw2_n = 0;
                    if (bkw2_n < 40) {
                        fprintf(stderr, "[c54x] BK-WR (F7Cx LD#k) 0x%04x→0x%04x PC=0x%04x "
                                "%s insn=%u\n", s->bk, k, s->pc,
                                (k == 0) ? "<<< BK=0 (casse circular!)" : "", s->insn_count);
                        bkw2_n++;
                    }
                }
                s->bk = k; break;
            case 0xD: sp_abs_track(s, k, 1); s->sp = k; break;  /* LD #k8u, SP */
            }
            return consumed + s->lk_used;
        }
        /* F9xx encoding split per tic54x-opc.c:
         *   F900-F97F mask FF00 = CC pmad cond (NEAR conditional call)
         *   F980-F9FF mask FF80 = FCALL pmad   (FAR call unconditional)
         * The bit 7 of the opcode low byte distinguishes them. */
        if (hi8 == 0xF9) {
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            /* FCALL FAR: pushes XPC and the return PC unconditionally (FL_FAR).
             * Per binutils tic54x-opc.c (fcall 0xF980 mask 0xFF80, FL_FAR) and
             * SPRU172C, a FAR call always saves XPC for FRET to restore.
             * ⚠️ Do not gate the push on PMST bit 4 (AVIS, no stack semantics):
             * the firmware's 281 FCALL FAR sites would push PC only, against 142
             * FRET sites popping both PC and XPC. */
            if ((op & 0x80) != 0) {
                uint8_t new_xpc = (op & 0x7F) & 0x03;
                static uint64_t fcall_total;
                fcall_total++;
                c54x_far_push(s, (uint16_t)(s->pc + 2));
                if (fcall_total <= 30 || (fcall_total % 5000) == 0) {
                    C54_LOG("FCALL FAR #%llu PC=0x%04x → XPC=%u PC=0x%04x (was XPC=%u SP=0x%04x)",
                            (unsigned long long)fcall_total, s->pc,
                            new_xpc, op2, s->xpc, s->sp);
                }
                s->xpc = new_xpc;
                s->pc  = op2;
                return 0;
            }
            /* The condition is the low byte (binutils condition_codes[]), decoded
             * by c54x_cond_true(). ⚠️ Reading (op>>4)&0xF instead takes the wrong
             * field: CC TC/NEQ/LT and friends then evaluate wrongly, the power scan
             * at 0xb1xx loses its pushes and the resulting over-pop ends in the
             * 0x70c3 self-CALA. */
            bool take = c54x_cond_true(s, (uint8_t)(op & 0x7F));
            if (take) {
                s->sp--;
                data_write(s, s->sp, (uint16_t)(s->pc + 2));
                /* CC leak tracer */
                {
                    static uint32_t cc_targets[64];
                    static uint32_t cc_counts[64];
                    static int cc_n = 0;
                    static uint32_t total_cc = 0;
                    bool found = false;
                    for (int i = 0; i < cc_n; i++) {
                        if (cc_targets[i] == op2) { cc_counts[i]++; found = true; break; }
                    }
                    if (!found && cc_n < 64) { cc_targets[cc_n] = op2; cc_counts[cc_n++] = 1; }
                    if ((++total_cc % 100) == 0) {
                        C54_LOG("F9xx CC TOP TARGETS (SP=0x%04x total=%u):", s->sp, total_cc);
                        for (int i = 0; i < cc_n && i < 10; i++)
                            C54_LOG("  CC→0x%04x count=%u", cc_targets[i], cc_counts[i]);
                    }
                }
                s->pc = op2;
                return 0;
            }
            return consumed + s->lk_used;
        }
        /* FAxx encoding split per tic54x-opc.c:
         *   FA80-FAFF mask FF80 = FBD pmad (FAR branch delayed)
         *   FA00-FA7F = various NEAR delayed ops (treated as branch). */
        if (hi8 == 0xFA) {
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            if ((op & 0x80) != 0) {
                /* FBD FAR delayed branch — XPC change, no push */
                uint8_t new_xpc = (op & 0x7F) & 0x03;
                static uint64_t fbd_total;
                fbd_total++;
                if (fbd_total <= 30 || (fbd_total % 5000) == 0) {
                    C54_LOG("FBD FAR #%llu PC=0x%04x → XPC=%u PC=0x%04x (was XPC=%u, delayed 2 slots)",
                            (unsigned long long)fbd_total, s->pc,
                            new_xpc, op2, s->xpc);
                }
                s->xpc = new_xpc;
                s->delayed_pc  = op2;
                s->delay_slots = 2;
                return consumed + s->lk_used;
            }
            /* FA20/FA30 are BCD pmad,NTC/TC, the delayed siblings of F820/F830,
             * and use exactly the BC technique above: evaluate the real condition
             * ONLY when the preceding opcode is CMPM or BITF (0x60xx/0x61xx), which
             * set TC reliably; otherwise keep the inherited branch-always.
             * ⚠️ Evaluating TC/NTC for all of FA00-FA7F wedges the firmware in
             * stuck loops, because most FAxx callers do not set TC before
             * branching. Delayed: 2 delay slots, same mechanism as FBD FAR. */
            {
                uint8_t fa_sub = (op >> 4) & 0xF;
                if (fa_sub == 0x2 || fa_sub == 0x3) {
                    /* same extension as the BC F82x/F83x (BCTC_NORM / BCTC_ETENDU) */
                    static int fa_norm = -1, fa_etendu = -1;
                    if (fa_norm < 0) fa_norm = calypso_gate("CALYPSO_C54X_BCTC_NORM", 0);
                    if (fa_etendu < 0) fa_etendu = calypso_gate("CALYPSO_C54X_BCTC_ETENDU", 0);
                    bool fa_tc_strict = (g_prev_op & 0xFE00) == 0x6000 || c54x_tc_par_rmw() ||
                        (fa_norm && (g_prev_op & 0xFCFF) == 0xF48F) ||
                        (fa_etendu && ((g_prev_op & 0xFCF8) == 0xF4A8 || (g_prev_op & 0xFF00) == 0x9600 ||
                                       (g_prev_op & 0xFF00) == 0x3400 ||
                                       (((g_prev_op & 0xFE00) == 0x7600) && (g_prev2_op & 0xFE00) == 0x6000)));
                    /* CALYPSO_C54X_BCD_TC=1: real TC on ALL BCD TC/NTC (12 sites in the ROM). The
                     * FR codec tests the sign of P[1] with `bit *ar2,0` then, nine instructions
                     * later (after rpt subc), `bcd ...,tc` chooses whether to negate the reflection
                     * coefficient (Schur, PDROM 0xe763 encoder and 0xedc5): the "always jump"
                     * fallback contradicts it 47 % of the time (BCTC probe) -> LPC with a random
                     * sign, uplink speech spectrum flipped (1 kHz rendered at ~3 kHz). */
                    static int fa_tc_tous = -1;
                    if (fa_tc_tous < 0) fa_tc_tous = calypso_gate("CALYPSO_C54X_BCD_TC", 0);
                    if (fa_tc_tous) fa_tc_strict = true;
                    if (fa_tc_strict) {
                        bool tc = (s->st0 & ST0_TC) != 0;
                        bool take = (fa_sub == 0x2) ? !tc : tc;  /* 2=NTC,3=TC */
                        if (take) {
                            s->delayed_pc  = op2;
                            s->delay_slots = 2;
                        }
                        return consumed + s->lk_used;
                    }
                }
            }
            /* Accumulator conditions are really evaluated, and the delay honoured.
             *
             * [2026-08-22] Measured with the TOA-TRACE probe on a clean native run:
             *     @0x7918 BCD 0x7923 if B!=0 ... B=0
             *     @0x7923 *** BRANCH TAKEN ***
             * `BNEQ` with B=0 branched anyway, every time, because the fallback
             * below treats EVERY non-TC FAxx as an unconditional branch. The FB0
             * route (0x791c..0x7921), which computes
             * TOA = (d[0x3fb4]-3)*48 + d[0x0c3d], was therefore never taken and the
             * TOA came from the FB1 route with cpt1[0x3fb3] frozen at 296, outside
             * the 0..1249 range osmocom expects (prim_fbsb.c, BITS_PER_TDMA=1250).
             * BCD is also DELAYED: setting `s->pc = op2` immediately skipped the
             * slot 0x791a `stm #0x0030` that loads T = 48, the TOA step.
             *
             * Scope: only accumulator conditions (cc & 0x40), which test A or B and
             * are reliable. TC and carry keep the previous behaviour, since those
             * are the ones that wedge the firmware. The two sets are disjoint
             * (fa_sub 2/3 implies cc & 0x40 == 0). */
            {
                uint8_t _cc = (uint8_t)(op & 0x7F);
                if (_cc & 0x40) {
                    if (c54x_cond_true(s, _cc)) {
                        s->delayed_pc  = op2;
                        s->delay_slots = 2;
                    }
                    return consumed + s->lk_used;   /* not taken: fall through */
                }
            }
            /* CALYPSO_C54X_BCD_CARRY=1: BCD on the carry (C 0x0C / NC 0x08) evaluated for real,
             * with its 2 delay slots. The fallback below ALWAYS jumps and WITHOUT delay slots: in
             * the ROM's FR codec (PDROM 0xe458, unpacking of the 260 bits: `rol b ; bcd 0xe463,nc`)
             * no 1 bit would ever be stored -> null FR parameters -> mute speech decoder. */
            {
                static int bcd_c = -1;
                if (bcd_c < 0) bcd_c = calypso_gate("CALYPSO_C54X_BCD_CARRY", 0);
                uint8_t _cc = (uint8_t)(op & 0x7F);
                if (bcd_c && (_cc & 0x0C) && !(_cc & 0x70)) {
                    if (c54x_cond_true(s, _cc)) {
                        s->delayed_pc  = op2;
                        s->delay_slots = 2;
                    }
                    return consumed + s->lk_used;
                }
            }
            /* CALYPSO_BCTC_SONDE=1: where the "always jump" fallback is used (PC, condition). */
            {
                static int sonde = -1;
                if (sonde < 0) { sonde = calypso_gate("CALYPSO_BCTC_SONDE", 0); if (sonde) atexit(bctc_sonde_resume); }
                if (sonde) bctc_sonde_note(s->pc, (uint16_t)(0xFA00 | (op & 0x7F)), !c54x_cond_true(s, (uint8_t)(op & 0x7F)));
            }
            /* NEAR FAxx fallback: simplified treat as branch (unchanged,
             * proven-safe default for every case not handled above). */
            s->pc = op2;
            return 0;
        }
        /* FBxx encoding split per tic54x-opc.c:
         *   FB80-FBFF mask FF80 = FCALLD pmad (FAR call delayed)
         *   FB00-FB7F mask FF00 = CCD pmad cond (NEAR conditional call delayed) */
        if (hi8 == 0xFB) {
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            /* FCALLD FAR: pushes XPC and the return PC+4 unconditionally
             * (FL_FAR delayed). Per binutils, fcalld 0xFB80 mask 0xFF80,
             * FL_FAR|FL_DELAY. ⚠️ Not gated on PMST bit 4, which is AVIS. */
            if ((op & 0x80) != 0) {
                uint8_t new_xpc = (op & 0x7F) & 0x03;
                static uint64_t fcalld_total;
                fcalld_total++;
                c54x_far_push(s, (uint16_t)(s->pc + 4));
                if (fcalld_total <= 30 || (fcalld_total % 5000) == 0) {
                    C54_LOG("FCALLD FAR #%llu PC=0x%04x → XPC=%u PC=0x%04x (was XPC=%u SP=0x%04x, delayed)",
                            (unsigned long long)fcalld_total, s->pc,
                            new_xpc, op2, s->xpc, s->sp);
                }
                s->xpc = new_xpc;
                s->delayed_pc  = op2;
                s->delay_slots = 2;
                return consumed + s->lk_used;
            }
            /* Condition decoded from the low byte by c54x_cond_true(), as for CC
             * above. */
            bool take = c54x_cond_true(s, (uint8_t)(op & 0x7F));
            if (take) {
                /* CCD is DELAYED: arm delay_slots=2 and delayed_pc, as CALLD
                 * (f274) does. ⚠️ Branching immediately skips the 2 delay slots,
                 * and a push in a slot is then lost, giving the over-pop that ends
                 * in the 0x70c3 self-CALA. The pushed return is pc+4 (past CCD and
                 * its slots); when not taken, PC advances by consumed (2). */
                s->sp--;
                data_write(s, s->sp, (uint16_t)(s->pc + 4));
                s->delayed_pc  = op2;
                s->delay_slots = 2;
                return consumed + s->lk_used;
            }
            return consumed + s->lk_used;
        }
        /* FCxx: LD #k, 16, B */
        /* FCxx: RC cond / RET -- return conditional (1-word).
         * Per tic54x-opc.c: RET=0xFC00, RC=0xFC00 mask 0xFF00. */
        if (hi8 == 0xFC) {
            uint8_t cc = op & 0xFF;
            bool cond = false;
            /* Evaluate condition per tic54x-opc.c encoding:
             * CC1=0x40: accumulator test, CCB=0x08: use B (else A)
             * EQ=0x05, NEQ=0x04, LT=0x03, LEQ=0x07, GT=0x06, GEQ=0x02
             * OV=0x70, NOV=0x60, TC=0x30, NTC=0x20, C=0x0C, NC=0x08 */
            if (cc == 0x00) cond = true; /* UNC */
            else if (cc & 0x40) {
                /* Accumulator condition */
                int64_t acc = (cc & 0x08) ? sext40(s->b) : sext40(s->a);
                uint8_t test = cc & 0x07;
                bool ov = (cc & 0x08) ? (s->st0 & (1<<9)/*OVB*/) : (s->st0 & (1<<8)/*OVA*/);
                if ((cc & 0x70) == 0x70) cond = ov;        /* AOV/BOV */
                else if ((cc & 0x70) == 0x60) cond = !ov;  /* ANOV/BNOV */
                else {
                    switch (test) {
                    case 0x05: cond = (acc == 0); break;  /* EQ */
                    case 0x04: cond = (acc != 0); break;  /* NEQ */
                    case 0x03: cond = (acc < 0); break;   /* LT */
                    case 0x07: cond = (acc <= 0); break;  /* LEQ */
                    case 0x06: cond = (acc > 0); break;   /* GT */
                    case 0x02: cond = (acc >= 0); break;  /* GEQ */
                    default: cond = true; break;
                    }
                }
            }
            else if ((cc & 0x30) == 0x30) cond = (s->st0 & ST0_TC) != 0; /* TC */
            else if ((cc & 0x30) == 0x20) cond = !(s->st0 & ST0_TC);     /* NTC */
            else if ((cc & 0x0C) == 0x0C) cond = (s->st0 & ST0_C) != 0;  /* C */
            else if ((cc & 0x0C) == 0x08) cond = !(s->st0 & ST0_C);      /* NC */
            else cond = true; /* unknown: take it */
            if (cond) {
                uint16_t ra = data_read(s, s->sp); s->sp++;
                {
                    static int rc_log = 0;
                    if (rc_log < 50)
                        C54_LOG("RC/RET PC=0x%04x cc=0x%02x -> ra=0x%04x SP=0x%04x",
                                s->pc, cc, ra, s->sp);
                    rc_log++;
                }
                if (g_vec28_tracing) {
                    g_vec28_trace_pops++;
                    fprintf(stderr, "[c54x] VEC28-STACK-TRACE #%u RC/RET(1w) PC=0x%04x "
                            "popped=0x%04x words=1 SP_before=0x%04x SP_after=0x%04x "
                            "insn=%u\n", g_vec28_trace_pops, s->pc, ra,
                            (uint16_t)(s->sp - 1), s->sp, s->insn_count);
                    if (s->sp >= (uint16_t)(g_vec28_sp_entry + 2)) {
                        fprintf(stderr, "[c54x] VEC28-STACK-TRACE DISARMED SP=0x%04x "
                                "back at or above pre-interrupt level 0x%04x\n",
                                s->sp, g_vec28_sp_entry);
                        g_vec28_tracing = false;
                    }
                }
                /* POST-BOOTSTUB-RET: a RET taken from the boot stub
                 * (PC in 0x0000..0x0008) is the exit of the task-switch trampoline
                 * 0x701b/0x701d -> 0x0000, and the popped return address is the PC
                 * of the task taking control. */
                if (s->pc <= 0x0008) {
                    static unsigned bsr;
                    bsr++;
                    if (bsr <= 200 || (bsr % 50) == 0) {
                        fprintf(stderr,
                                "[c54x] POST-BOOTSTUB-RET #%u PC=0x%04x -> task=0x%04x "
                                "SP_new=0x%04x B=0x%010llx INTM=%d insn=%u\n",
                                bsr, s->pc, ra, s->sp,
                                (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                                !!(s->st1 & ST1_INTM), s->insn_count);
                    }
                    /* DEEP-TRAIL (CALYPSO_DEBUG=BOOTSTUB_TRAIL): for the first 5
                     * POST-BOOTSTUB-RET events, dump pc_ring[-64..-1] to expose the
                     * caller chain leading to the stack-underflow loop. */
                    if (bsr <= 5 && calypso_debug_enabled("BOOTSTUB_TRAIL")) {
                        fprintf(stderr,
                            "[c54x] BOOTSTUB DEEP-TRAIL #%u (last 64 PCs):\n",
                            bsr);
                        for (int row = 0; row < 8; row++) {
                            fprintf(stderr, "[c54x] BS-DEEP[%3d..%3d] :",
                                    -64 + row*8, -57 + row*8);
                            for (int col = 0; col < 8; col++) {
                                int idx = -64 + row*8 + col;
                                fprintf(stderr, " %04x",
                                    pc_ring[(pc_ring_idx + idx) & 255]);
                            }
                            fprintf(stderr, "\n");
                        }
                        /* Also dump 16 stack words starting at SP. */
                        fprintf(stderr, "[c54x] BS-DEEP stack[SP..SP+15] :");
                        for (int i = 0; i < 16; i++) {
                            fprintf(stderr, " %04x",
                                s->data[(s->sp + i) & 0xFFFF]);
                        }
                        fprintf(stderr, "\n");
                    }
                }
                s->pc = ra;
                return 0;
            }
            return consumed + s->lk_used;
        }
        /* FDxx: LD #k, A (no shift) */
        if (hi8 == 0xFD) {
            int8_t k = (int8_t)(op & 0xFF);
            s->a = sext40((int64_t)k);
            return consumed + s->lk_used;
        }
        /* FExx: RCD cond / RETD -- return conditional delayed (1-word).
         * Per tic54x-opc.c: RETD=0xFE00, RCD=0xFE00 mask 0xFF00.
         * Simplified: immediate return (delay slots skipped). */
        if (hi8 == 0xFE) {
            uint8_t cc = op & 0xFF;
            bool cond = false;
            /* Evaluate condition per tic54x-opc.c encoding:
             * CC1=0x40: accumulator test, CCB=0x08: use B (else A)
             * EQ=0x05, NEQ=0x04, LT=0x03, LEQ=0x07, GT=0x06, GEQ=0x02
             * OV=0x70, NOV=0x60, TC=0x30, NTC=0x20, C=0x0C, NC=0x08 */
            if (cc == 0x00) cond = true; /* UNC */
            else if (cc & 0x40) {
                /* Accumulator condition */
                int64_t acc = (cc & 0x08) ? sext40(s->b) : sext40(s->a);
                uint8_t test = cc & 0x07;
                bool ov = (cc & 0x08) ? (s->st0 & (1<<9)/*OVB*/) : (s->st0 & (1<<8)/*OVA*/);
                if ((cc & 0x70) == 0x70) cond = ov;        /* AOV/BOV */
                else if ((cc & 0x70) == 0x60) cond = !ov;  /* ANOV/BNOV */
                else {
                    switch (test) {
                    case 0x05: cond = (acc == 0); break;  /* EQ */
                    case 0x04: cond = (acc != 0); break;  /* NEQ */
                    case 0x03: cond = (acc < 0); break;   /* LT */
                    case 0x07: cond = (acc <= 0); break;  /* LEQ */
                    case 0x06: cond = (acc > 0); break;   /* GT */
                    case 0x02: cond = (acc >= 0); break;  /* GEQ */
                    default: cond = true; break;
                    }
                }
            }
            else if ((cc & 0x30) == 0x30) cond = (s->st0 & ST0_TC) != 0; /* TC */
            else if ((cc & 0x30) == 0x20) cond = !(s->st0 & ST0_TC);     /* NTC */
            else if ((cc & 0x0C) == 0x0C) cond = (s->st0 & ST0_C) != 0;  /* C */
            else if ((cc & 0x0C) == 0x08) cond = !(s->st0 & ST0_C);      /* NC */
            else cond = true; /* unknown: take it */
            if (cond) {
                /* RCD is DELAYED: per SPRU172C the next 2 instructions after RCD
                 * execute before the return takes effect. Pop the return address
                 * now, let PC advance normally so those 2 instructions run as delay
                 * slots, and the main loop then forces PC = delayed_pc.
                 * ⚠️ Skipping the delay slots breaks FB detection: slots such as
                 * `LD #0, B` at PROM0 0x75ea never run, the accumulator stays stale
                 * and the dispatcher at 0x7700 loops forever. */
                uint16_t ra = data_read(s, s->sp); s->sp++;
                s->delayed_pc  = ra;
                s->delay_slots = 2;
                {
                    static int rcd_log = 0;
                    if (rcd_log < 50)
                        C54_LOG("RCD/RETD PC=0x%04x cc=0x%02x -> ra=0x%04x SP=0x%04x (delayed)",
                                s->pc, cc, ra, s->sp);
                    rcd_log++;
                }
                if (g_vec28_tracing) {
                    g_vec28_trace_pops++;
                    fprintf(stderr, "[c54x] VEC28-STACK-TRACE #%u RCD/RETD(1w,delayed) PC=0x%04x "
                            "popped=0x%04x words=1 SP_before=0x%04x SP_after=0x%04x "
                            "insn=%u\n", g_vec28_trace_pops, s->pc, ra,
                            (uint16_t)(s->sp - 1), s->sp, s->insn_count);
                    if (s->sp >= (uint16_t)(g_vec28_sp_entry + 2)) {
                        fprintf(stderr, "[c54x] VEC28-STACK-TRACE DISARMED SP=0x%04x "
                                "back at or above pre-interrupt level 0x%04x\n",
                                s->sp, g_vec28_sp_entry);
                        g_vec28_tracing = false;
                    }
                }
                return consumed + s->lk_used;
            }
            return consumed + s->lk_used;
        }
        /* FFxx is XC 2,cond — handled above with FDxx. No ADD here. */
        goto unimpl;

    case 0xE:
        /* Exxxx: single-word ALU, status, misc */
        /* ================================================================
         * 0xE0-0xE3 IS NOT `CMPS`. The real CMPS is 0x8E00/0xFE00, decoded
         * further down. Real encodings per SPRU172C (XXXXYYYY in bits 7:0):
         *     0xE0 FIRS  Xmem,Ymem,pmad   ** 2 WORDS **
         *     0xE1 LMS   Xmem,Ymem        1 word
         *     0xE2 SQDST Xmem,Ymem        1 word
         *     0xE3 ABDST Xmem,Ymem        1 word
         * ⚠️ The serious damage is counting 0xE0 as one word, which
         * desynchronises the stream. The word count is fixed first; E1-E3 are
         * left inert rather than made to write A/B/TC/TRN wrongly, since wrong
         * semantics are worse than none. Faithful LMS/SQDST/ABDST remain to be
         * written if a probe shows they matter.
         * ================================================================ */
        {
            if ((op & 0xFC00) == 0xE000) {
                static unsigned _e0_n = 0;
                if (hi8 == 0xE0) {
                    uint16_t pmad0 = prog_fetch(s, s->pc + 1);
                    consumed = 2;
                    /* FIRS-COEF (CALYPSO_FIRS_COEF, default OFF): where are the
                     * coefficients? pmad=0x0061 sits BELOW the OVLY window, which
                     * starts at 0x80, so prog_read falls back to s->prog[], where
                     * nothing is loaded under 0x7000 and reads give 0xF4E4.
                     * 0x0060-0x007F is the C54x DARAM scratch pad, so both spaces
                     * are dumped at FIRS time: a non-zero data[] means the OVLY
                     * alias should reach down to 0x60, a non-zero prog[] means MVDP
                     * already deposited them, and both zero means the table lives
                     * somewhere else. Read-only. */
                    {
                        static int _fc = -1;
                        if (_fc < 0) {
                            _fc = calypso_gate("CALYPSO_FIRS_COEF", 0);
                            fprintf(stderr, "[c54x] FIRS-COEF %s : dump prog[pmad..+5] "
                                    "ET data[pmad..+5] a chaque FIRS (pmad attendu 0x0061, "
                                    "sous la fenetre OVLY qui debute a 0x0080)\n",
                                    _fc ? "ACTIVE" : "INACTIVE (defaut)");
                        }
                        if (_fc) {
                            static unsigned _fcn = 0;
                            if (_fcn < 12) {
                                _fcn++;
                                char pb[96], db[96], sb2[96];
                                int po = 0, dof = 0, so = 0;
                                int nzp = 0, nzd = 0, nzs = 0;
                                /* Source of the table: the MVDD at 0x833c copies
                                 * data[0x2cbf] downwards into data[0x0060] upwards.
                                 * An empty source means an empty destination. */
                                for (int k = 0; k < 7; k++) {
                                    uint16_t sv = s->data[0x2CB9 + k];
                                    if (sv) nzs++;
                                    so += snprintf(sb2 + so, sizeof(sb2) - so, " %04x", sv);
                                }
                                /* Is the burst buffer alive when the DSP reads it?
                                 * The whole chain (0x8202 -> 0x2cba -> MVDD ->
                                 * 0x0061 -> FIRS) yields zero when its head, the
                                 * 0x2a00 buffer, is empty. An empty buffer HERE is
                                 * not a badly written one: DARAM-WR-JUDGE measured
                                 * coherence 0.998 at write time, under lock.
                                 * Also reports the spread of the two correlator
                                 * buffers (A=0x2c56, B=0x2c88, 50 words each,
                                 * BRC=0x31): distinct==1 means it is not
                                 * accumulating by shifting and the argmax is
                                 * meaningless. */
                                int dstA = 0, dstB = 0;
                                int16_t mnA = 32767, mxA = -32768;
                                int16_t mnB = 32767, mxB = -32768;
                                {
                                    int16_t va[50], vb[50];
                                    for (int k = 0; k < 50; k++) {
                                        va[k] = (int16_t)s->data[0x2C56 + k];
                                        vb[k] = (int16_t)s->data[0x2C88 + k];
                                        if (va[k] < mnA) mnA = va[k];
                                        if (va[k] > mxA) mxA = va[k];
                                        if (vb[k] < mnB) mnB = vb[k];
                                        if (vb[k] > mxB) mxB = vb[k];
                                    }
                                    for (int k = 0; k < 50; k++) {
                                        int dupA = 0, dupB = 0;
                                        for (int j = 0; j < k; j++) {
                                            if (va[j] == va[k]) dupA = 1;
                                            if (vb[j] == vb[k]) dupB = 1;
                                        }
                                        if (!dupA) dstA++;
                                        if (!dupB) dstB++;
                                    }
                                }
                                int nzb = 0; unsigned long eb = 0;
                                for (int k = 0; k < 304; k++) {
                                    int16_t s16 = (int16_t)s->data[0x2A00 + k];
                                    if (s16) nzb++;
                                    eb += (unsigned long)(s16 < 0 ? -(long)s16 : (long)s16);
                                }
                                for (int k = 0; k < 6; k++) {
                                    uint16_t a = (uint16_t)(pmad0 + k);
                                    uint16_t pv = s->prog[c54x_prog_xlate(s, a)];
                                    uint16_t dv = s->data[a];
                                    if (pv && pv != 0xF4E4) nzp++;
                                    if (dv) nzd++;
                                    po  += snprintf(pb + po,  sizeof(pb) - po,  " %04x", pv);
                                    dof += snprintf(db + dof, sizeof(db) - dof, " %04x", dv);
                                }
                                fprintf(stderr, "[c54x] FIRS-COEF #%u PC=0x%04x pmad=0x%04x "
                                        "OVLY=%d | AR2=0x%04x(*=0x%04x) AR3=0x%04x AR5=0x%04x"
                                        " | prog[]=%s (utiles=%d) | data[]=%s (utiles=%d)"
                                        " | SOURCE data[0x2CB9..]=%s (utiles=%d)"
                                        " | BURST 0x2a00 : %d/304 non nuls, energie=%lu,"
                                        " *AR2=0x%04x *AR3=0x%04x"
                                        " | CORR A(0x2c56) distinct=%d/50 [%d..%d]"
                                        " B(0x2c88) distinct=%d/50 [%d..%d]"
                                        " | VERDICT=%s insn=%u\n",
                                        _fcn, s->pc, pmad0, !!(s->pmst & PMST_OVLY),
                                        s->ar[2], s->data[s->ar[2]], s->ar[3], s->ar[5],
                                        pb, nzp, db, nzd, sb2, nzs,
                                        nzb, eb, s->data[s->ar[2]], s->data[s->ar[3]],
                                        dstA, (int)mnA, (int)mxA, dstB, (int)mnB, (int)mxB,
                                        nzd ? "SCRATCH-PAD (etendre OVLY a 0x60)"
                                            : (nzp ? "PROGRAMME (MVDP a depose)"
                                                   : "TABLE INTROUVABLE dans les deux espaces"),
                                        s->insn_count);
                            }
                        }
                    }
                    /* FIRS sits on the SCH soft-bit production path:
                     *   0x8492 rpt #5 ; 0x8493 firs 0x0061 ; 0x8497 sth *AR6+,B
                     * Semantics per SPRU172C:
                     *     B = B + A(32-16) x Pmem[pmad] ; A = (Xmem+Ymem)<<16
                     * Under RPT, pmad auto-increments at every repetition. */
                    int xmod = (op >> 6) & 0x03;
                    int xar  = ((op >> 4) & 0x03) + 2;
                    int ymod = (op >> 2) & 0x03;
                    int yar  = ( op       & 0x03) + 2;

                    {
                        /* pmad auto-increment: a second `rpt ; firs` at the SAME
                         * site must restart from pmad instead of continuing at
                         * _fpm+1. The SB path has two such sites, 0x8478 and
                         * 0x8493, 1704 iterations per burst. */
                        static uint16_t _fpm = 0;
                        if (!s->rpt_active || s->rpt_fresh) { _fpm = pmad0; s->rpt_fresh = false; }
                        else                                { _fpm++; }

                        uint16_t xv = data_read(s, s->ar[xar]);
                        uint16_t yv = data_read(s, s->ar[yar]);
                        int16_t  coef = (int16_t)prog_fetch(s, _fpm);

                        int64_t prod = (int64_t)(int16_t)((s->a >> 16) & 0xFFFF)
                                     * (int64_t)coef;
                        if (s->st1 & ST1_FRCT) prod <<= 1;
                        s->b = sext40(s->b + prod);
                        s->a = sext40(((int64_t)(int16_t)xv
                                     + (int64_t)(int16_t)yv) << 16);
                        {   static unsigned _fn2 = 0;
                            if (_fn2 < 16) {
                                _fn2++;
                                fprintf(stderr, "[c54x] FIRS #%u PC=0x%04x pmad=0x%04x "
                                        "coef(prog)=0x%04x data[pmad]=0x%04x OVLY=%d "
                                        "Xar=AR%d@0x%04x=0x%04x "
                                        "Yar=AR%d@0x%04x=0x%04x -> A=0x%010llx "
                                        "B=0x%010llx insn=%u\n",
                                        _fn2, s->pc, _fpm, (unsigned)(uint16_t)coef,
                                        (unsigned)s->data[_fpm],
                                        !!(s->pmst & PMST_OVLY),
                                        xar, s->ar[xar], xv, yar, s->ar[yar], yv,
                                        (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                                        (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                                        s->insn_count);
                            }
                        }
                    }
                    /* ⚠️ On the C54x FIRS advances Xmem and Ymem in every case,
                     * so the post-modifications are unconditional. Leaving them
                     * out froze AR2/AR3 at 0x2a8e/0x2a9a at s->pc == 0x8478 over
                     * six executions; the stage's RPTB loop (BRC=0x8d, 142
                     * iterations) then carried AR3 out of the burst buffer up to
                     * 0x2ceb, where it overwrote the correlation reference.
                     * Hardware watchpoint, six triggers in perfect alternation:
                     *   0x7c50 : 0x223c -> 0x0200  (installs the reference)
                     *   0x847a : 0x0200 -> 0x223c  (overwrites it, through AR3)
                     * hence a frozen correlator buffer A, a meaningless argmax and
                     * an SCH that never decodes. */
                    c54x_par_postmod(s, xar, xmod);
                    c54x_par_postmod(s, yar, ymod);
                    return consumed + s->lk_used;
                }
                if (hi8 == 0xE3) {
                    /* [retenue] ABDST Xmem,Ymem (SPRU172C, 1 word, isa_test 1):
                     *     B = B + |A(32-16)| ; A = (Xmem - Ymem) << 16
                     * A(32-16) is the 17-bit field, sign bit = A(32). Both ARs
                     * are post-modified like FIRS. C from the subtraction. */
                    int xmod = (op >> 6) & 0x03, xar = ((op >> 4) & 0x03) + 2;
                    int ymod = (op >> 2) & 0x03, yar = ( op       & 0x03) + 2;
                    uint16_t xv = data_read(s, s->ar[xar]);
                    uint16_t yv = data_read(s, s->ar[yar]);
                    int64_t ah = (s->a >> 16) & 0x1FFFF;
                    if (ah & 0x10000) ah -= 0x20000;
                    s->b = sext40(s->b + (ah < 0 ? -ah : ah));
                    int sxm = (s->st1 & ST1_SXM) != 0;
                    int64_t x40 = (sxm ? (int64_t)(int16_t)xv : (int64_t)xv) << 16;
                    int64_t y40 = (sxm ? (int64_t)(int16_t)yv : (int64_t)yv) << 16;
                    c54x_carry_sub40(s, x40, y40);
                    s->a = sext40(x40 - y40);
                    c54x_par_postmod(s, xar, xmod);
                    c54x_par_postmod(s, yar, ymod);
                    return consumed + s->lk_used;
                }
                /* [2026-10-15] LMS Xmem,Ymem (binutils 0xE100/0xFF00, 1 word). SPRU172C:
                 *     A = A + (Xmem)<<16 + 2^15      (no clearing of bits 15-0)
                 *     B = B + (Xmem) x (Ymem)        (FRCT honoured)
                 * Xmem sign-extended per SXM, OVA/OVB on 32-bit overflow, OVM saturates.
                 * Was inert. isa_test 98. CALYPSO_LMS_ANCIEN=1 keeps it inert. */
                static int lms_ancien = -1;
                if (lms_ancien < 0) lms_ancien = calypso_gate("CALYPSO_LMS_ANCIEN", 0);
                if (hi8 == 0xE1 && !lms_ancien) {
                    int xmod = (op >> 6) & 0x03, xar = ((op >> 4) & 0x03) + 2;
                    int ymod = (op >> 2) & 0x03, yar = ( op       & 0x03) + 2;
                    uint16_t xv = data_read(s, s->ar[xar]);
                    uint16_t yv = data_read(s, s->ar[yar]);
                    int64_t xs = (s->st1 & ST1_SXM) ? (int64_t)(int16_t)xv : (int64_t)xv;
                    int64_t ra = sext40(s->a + (xs << 16) + 0x8000);
                    int64_t p  = (int64_t)(int16_t)xv * (int64_t)(int16_t)yv;
                    if (s->st1 & ST1_FRCT) p <<= 1;
                    int64_t rb = sext40(s->b + p);
                    if (ra != sat32(ra)) { s->st0 |= ST0_OVA; if (s->st1 & ST1_OVM) ra = sat32(ra); }
                    if (rb != sat32(rb)) { s->st0 |= ST0_OVB; if (s->st1 & ST1_OVM) rb = sat32(rb); }
                    s->a = ra; s->b = rb;
                    c54x_par_postmod(s, xar, xmod);
                    c54x_par_postmod(s, yar, ymod);
                    return consumed + s->lk_used;
                }
                if (_e0_n < 40) {
                    _e0_n++;
                    C54_LOG("E0-FAM non implemente op=0x%04x (%s) PC=0x%04x "
                            "consumed=%d insn=%u", op,
                            hi8 == 0xE0 ? "FIRS" : hi8 == 0xE1 ? "LMS" :
                            hi8 == 0xE2 ? "SQDST" : "ABDST",
                            s->pc, consumed, s->insn_count);
                }
                return consumed + s->lk_used;
            }
        }
        if ((op & 0xFE00) == 0xEA00) {
            /* EAxx: LD #k9, DP — Load Data Page pointer (1-word).
             * Per tic54x-opc.c: ld 0xEA00 mask 0xFE00, 1 word. */
            uint16_t k9 = op & 0x01FF;
            uint16_t old_dp = s->st0 & ST0_DP_MASK;
            s->st0 = (s->st0 & ~ST0_DP_MASK) | k9;
            g_last_ldp_pc = s->pc; g_last_ldp_val = k9; g_last_ldp_kind = 2;
            {
                static uint64_t dpc;
                dpc++;
                if (dpc <= 80 || (dpc % 5000) == 0 || k9 == 0x83) {
                    C54_LOG("DP-SET EAxx #%llu PC=0x%04x DP 0x%03x → 0x%03x %s",
                            (unsigned long long)dpc, s->pc,
                            old_dp, k9,
                            k9 == 0x83 ? "*** 0x83 (CALAD-zone base 0x4180) ***" : "");
                }
            }
            return consumed + s->lk_used;
        }
        if (hi8 == 0xEC) {
            /* ECxx: RPT #k8u — repeat next instruction k8u+1 times.
             * Per tic54x-opc.c: rpt 0xEC00 mask 0xFF00, single word.
             * Must advance PC past RPT now and return 0 so the dispatcher
             * re-executes the NEXT instruction (not RPT itself). */
            s->rpt_count = op & 0xFF;
            s->rpt_active = true; s->rpt_fresh = true;
            s->pc += 1;
            return 0;
        }
        if (hi8 == 0xE5) {
            /* E5xx: MVDD Xmem, Ymem  (per tic54x-opc.c, NOT MVMM)
             * 1-word, 2-cycle dual-operand data-to-data move:
             *   *Ymem = *Xmem
             * Per tic54x.h:
             *   XMEM = (op & 0xF0) >> 4
             *   YMEM = op & 0x0F
             *   XMOD/YMOD = (nibble & 0xC) >> 2  (0=*AR,1=*AR-,2=*AR+,3=*AR+0%)
             *   XARX/YARX = (nibble & 0x3) + 2   (AR2..AR5 only) */
            uint8_t xnib = (op >> 4) & 0xF;
            uint8_t ynib = op & 0xF;
            int xar = (xnib & 0x3) + 2;
            int yar = (ynib & 0x3) + 2;
            int xmod = (xnib & 0xC) >> 2;
            int ymod = (ynib & 0xC) >> 2;
            uint16_t xa = s->ar[xar];
            uint16_t ya = s->ar[yar];
            uint16_t v = data_read(s, xa);
            data_write(s, ya, v);
            /* Post-modify both ARs per their mod field */
            switch (xmod) {
                case 0: break;                        /* *AR     */
                case 1: s->ar[xar] = xa - 1; break;   /* *AR-    */
                case 2: s->ar[xar] = xa + 1; break;   /* *AR+    */
                case 3: s->ar[xar] = c54x_circ_ref(xa, +(int16_t)s->ar[0], s->bk); break; /* *AR+0% circular, modulo BK */
            }
            switch (ymod) {
                case 0: break;
                case 1: s->ar[yar] = ya - 1; break;
                case 2: s->ar[yar] = ya + 1; break;
                case 3: s->ar[yar] = c54x_circ_ref(ya, +(int16_t)s->ar[0], s->bk); break; /* *AR+0% circular, modulo BK */
            }
            return consumed + s->lk_used;
        }
        /* ================================================================
         *     ST src,Ymem || LD Xmem,T      encoding 111001S0 XXXXYYYY
         *     = 0xE400 mask 0xFD00 (so 0xE4xx AND 0xE6xx, with b8 = 0)
         *
         * ⚠️ Decoding 0xE4 as a 2-word "BITF Smem,#lk" swallows one word per
         * site and desynchronises the instruction stream. The real BITF is
         * 0x6100/0xFF00, decoded elsewhere in this file.
         *
         * ⚠️ Do NOT widen the mask to 0xFC00: 0xE5xx = MVDD and 0xE7xx = MVMM
         * are different instructions (b8 discriminates) and are really used --
         * 79 MVDD in PROM0, including the correlator body in PDROM.
         *
         * Semantics (SPRU172C p.4-178, syntax 2):
         *     Ymem = (src << ASM) >> 16   ;   T = Xmem
         * S = b9 (0xE4 -> A, 0xE6 -> B). ONE word.
         * ================================================================ */
        if ((op & 0xFD00) == 0xE400) {
            int s_acc = (op >> 9) & 1;
            int xmod  = (op >> 6) & 3;
            int xar   = ((op >> 4) & 3) + 2;
            int ymod  = (op >> 2) & 3;
            int yar   = ( op       & 3) + 2;
            uint16_t yaddr = s->ar[yar];
            uint16_t xval  = data_read(s, s->ar[xar]);
            int64_t  sv    = s_acc ? s->b : s->a;
            int      ash   = asm_shift(s);
            int64_t  sh    = (ash >= 0) ? (sv << ash) : (sv >> (-ash));

            data_write(s, yaddr, (uint16_t)((sh >> 16) & 0xFFFF));
            s->t = xval;                      /* LD Xmem, T */
            c54x_par_postmod(s, xar, xmod);
            c54x_par_postmod(s, yar, ymod);
            return consumed + s->lk_used;
        }
        if (hi8 == 0xE7) {
            /* E7xx: MVMM mmrx, mmry  (per tic54x-opc.c)
             * 1-word, 2-cycle, MMR-to-MMR move using a constrained set
             * (MMRX/MMRY operand types). */
            int src = (op >> 4) & 0xF;
            int dst = op & 0xF;
            uint16_t val;
            if (src <= 7) val = s->ar[src];
            else if (src == 8) val = s->sp;
            else val = data_read(s, src + 0x10);
            if (dst <= 7) s->ar[dst] = val;
            else if (dst == 8) { sp_abs_track(s, val, 2); s->sp = val; }  /* MVMM SP-dest */
            else data_write(s, dst + 0x10, val);
            return consumed + s->lk_used;
        }
        if (hi8 == 0xE8 || hi8 == 0xE9) {
            /* E8xx/E9xx: LD #k8u, dst — Load 8-bit unsigned immediate (1-word).
             * Per tic54x-opc.c: ld 0xE800 mask 0xFE00.
             * bit 8 = dst (0=A, 1=B), bits 7:0 = k8u.
             * ⚠️ Decoding this as CC (a 2-word conditional call) overflows the
             * stack by pushing return addresses in a loop. */
            int dst = (op >> 8) & 1;
            uint8_t k = op & 0xFF;
            /* [2026-09-20] K is an 8-bit UNSIGNED short immediate whatever SXM says:
             * SPRU172C example LD #248, B gives B = 00 0000 00F8. */
            int64_t v = (int64_t)k;
            /* Per SPRU172C, LD #k8 loads the immediate into the LOW bits
             * (sext40(v)), not v<<16. [2026-07-23] With the <<16, 0x39 landed in
             * bits 16-23: in the mask-ROM terminal, 0xb408 LD #0x39 followed by
             * 0xb409 ADD #0x4387 gave A=0x394387, so STLM wrote AR7=0x4387 (the
             * IDLE slot, data[0x4387]=0xab38) instead of 0x0039+0x4387=0x43C0 (the
             * go-live slot, data[0x43c0]=0xa4c7), and the terminal BACC at 0xb40f
             * jumped into idle. CALYPSO_LDK8_SHIFT16=1 restores the old form. */
            static int _ldk8sh = -1;
            if (_ldk8sh < 0) _ldk8sh = calypso_gate("CALYPSO_LDK8_SHIFT16", 0);
            int64_t _ldv = _ldk8sh ? (v << 16) : v;
            if (dst) s->b = sext40(_ldv);
            else     s->a = sext40(_ldv);
            return consumed + s->lk_used;
        }
        if (hi8 == 0xE1) {
            /* E1xx: single-word acc ops — NEG, ABS, CMPL, SAT, EXP, etc. */
            uint8_t sub = op & 0xFF;
            switch (sub) {
            case 0xE0: s->a = ~s->a; s->a = sext40(s->a); break;  /* CMPL A */
            case 0xE1: s->b = ~s->b; s->b = sext40(s->b); break;  /* CMPL B */
            case 0xE2: s->a = -s->a; s->a = sext40(s->a); break;  /* NEG A */
            case 0xE3: s->b = -s->b; s->b = sext40(s->b); break;  /* NEG B */
            /* SAT: -2^31 sign-extended (see calypso_c54x.c, OVM post-saturation). */
            case 0xE4: /* SAT A */ if (s->st0 & ST0_OVA) s->a = (s->a < 0) ? (getenv("CALYPSO_SAT_NEG_ANCIEN") && *getenv("CALYPSO_SAT_NEG_ANCIEN") == '1' ? (int64_t)0xFF80000000LL : -0x80000000LL) : 0x7FFFFFFFLL; break;
            case 0xE5: /* SAT B */ if (s->st0 & ST0_OVB) s->b = (s->b < 0) ? (getenv("CALYPSO_SAT_NEG_ANCIEN") && *getenv("CALYPSO_SAT_NEG_ANCIEN") == '1' ? (int64_t)0xFF80000000LL : -0x80000000LL) : 0x7FFFFFFFLL; break;
            case 0xE8: /* ABS A */ s->a = (s->a < 0) ? -s->a : s->a; s->a = sext40(s->a); break;
            case 0xE9: /* ABS B */ s->b = (s->b < 0) ? -s->b : s->b; s->b = sext40(s->b); break;
            case 0xEA: /* ROR A */ { uint16_t c = s->st0 & ST0_C ? 1 : 0; if (s->a & 1) s->st0 |= ST0_C; else s->st0 &= ~ST0_C; s->a = (s->a >> 1) | ((int64_t)c << 39); s->a = sext40(s->a); } break;
            case 0xEB: /* ROL A */ { uint16_t c = s->st0 & ST0_C ? 1 : 0; if (s->a & ((int64_t)1<<39)) s->st0 |= ST0_C; else s->st0 &= ~ST0_C; s->a = (s->a << 1) | c; s->a = sext40(s->a); } break;
            default:
                /* EXP A/B etc — return 0 for now */
                break;
            }
            return consumed + s->lk_used;
        }
        if (hi8 == 0xEF) {
            /* EFxx: RPTZ dst, #lk — Zero accumulator and repeat (2 words)
             * Per SPRU172C: dst = 0; RPT #lk
             * Encoding: 1110 1111 xxxx xxxx + lk_word */
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            int rptz_dst = (op >> 0) & 1;
            if (rptz_dst) s->b = 0; else s->a = 0;
            s->rpt_count = op2;
            s->rpt_active = true; s->rpt_fresh = true;
            s->pc += 2;
            return 0;
        }
        if (hi8 == 0xEB) {
            /* EBxx: RPTB[D] pmad — Block repeat (2 words)
             * Per SPRU172C: REA = pmad, RSA = PC+2, BRAF=1 */
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            s->rea = op2;
            s->rsa = (uint16_t)(s->pc + 2);
            s->rptb_active = true;
            s->st1 |= ST1_BRAF;
            return consumed + s->lk_used;
        }
        if (hi8 == 0xE6) {
            /* E6xx: SFTA/SFTL acc, #shift (single-word immediate shift) */
            int shift = op & 0x1F;
            if (shift & 0x10) shift |= ~0x1F;  /* sign extend 5-bit */
            int dst = (op >> 5) & 1;
            int logical = (op >> 6) & 1;
            int64_t *acc = dst ? &s->b : &s->a;
            if (logical) {
                uint64_t u = (uint64_t)(*acc) & 0xFFFFFFFFFFULL;
                if (shift >= 0) *acc = sext40((int64_t)(u << shift));
                else            *acc = sext40((int64_t)(u >> (-shift)));
            } else {
                if (shift >= 0) *acc = sext40(*acc << shift);
                else            *acc = sext40(*acc >> (-shift));
            }
            return consumed + s->lk_used;
        }
        if (hi8 == 0xEE) {
            /* FRAME #k8 — stack-frame pointer adjust: SP = SP + sign_ext(k8).
             * Per tic54x-opc.c { "frame", 1,1,1, 0xEE00, 0xFF00, {OP_k8} }: ONE
             * word. FRAME -N allocates the frame (SP goes down), FRAME +N frees it.
             *
             * ⚠️ Decoding 0xEExx as a 2-word "BCD pmad,cond" is wrong on both
             * counts: the length desynchronises everything downstream, and the
             * semantics are SP += k8, not a branch (bc is 0xF8, cc 0xF9, bcd 0xFA).
             * PROM0 has 124 0xEExx sites, including the boot path, where
             * FRAME #-1 / FRAME #+1 pairs are prologue and epilogue. With SP never
             * adjusted, pops outnumber pushes, POPM ST0 at 0x94f3 picks up the
             * orphan 0x80fd, DP becomes 0x0fd, the dispatcher LUT reads garbage and
             * the self-CALA at 0x70c3 writes 0x70c4 into d_fb_det/a_pm, poisoning
             * rxlev and TOA. */
            int8_t k = (int8_t)(op & 0xFF);
            s->sp = (uint16_t)(s->sp + k);
            return consumed + s->lk_used;
        }
        if ((op & 0xFFE0) == 0xED00) {
            /* ED00-ED1F: LD #k5, ASM — load 5-bit immediate into ASM field of ST1.
             * Per tic54x-opc.c: ld 0xED00 mask 0xFFE0, 1 word.
             * NOT BCD (which is 0xFA00 mask 0xFF00). */
            uint8_t k5 = op & 0x1F;
            s->st1 = (s->st1 & ~ST1_ASM_MASK) | k5;
            return consumed + s->lk_used;
        }
        if (hi8 == 0xED) {
            /* EDxx (not ED00-ED1F): BCD pmad, cond (conditional branch delayed, 2 words) */
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            uint8_t cond = op & 0xFF;
            bool take = false;
            if (cond == 0x00) take = true;            /* UNC */
            else if (cond == 0x08) take = (s->b < 0);
            else if (cond == 0x02) take = (s->a != 0);
            else if (cond == 0x0A) take = (s->b != 0);
            else if (cond == 0x03) take = (s->a == 0);
            else if (cond == 0x0B) take = (s->b == 0);
            else if (cond == 0x04) take = (s->a > 0);
            else if (cond == 0x0C) take = (s->b > 0);
            else if (cond == 0x40) take = (s->st0 & ST0_TC) != 0;
            else if (cond == 0x41) take = !(s->st0 & ST0_TC);
            else take = true;
            if (take) { s->pc = op2; return 0; }
            return consumed + s->lk_used;
        }
        goto unimpl;

    case 0x6: case 0x7:
        /* 7Exx: READA Smem — read prog[A_low] → data[Smem]
         * Per tic54x-opc.c: reada 0x7E00 mask 0xFF00 (1 word).
         * Per SPRU131G : program address = (XPC[6:0] | A[15:0]). A.high is
         * NOT used as XPC source — XPC reg is. prog_read already implements
         * this via c54x_prog_xlate for addr ≥ 0x8000.
         * Under RPT, the prog address auto-increments each iteration;
         * accumulator A is preserved (mirrored through mvpd_src).
         *
         * ⚠️ Do not make A.high override XPC as a 23-bit address: SPRU131G says
         * otherwise and it moves no symptom. */
        if (hi8 == 0x7E) {
            /* READA-ITER: why the table loader's second loop makes TWO copies
             * instead of three. [2026-08-04] The loader at 0xb4b6 runs two loops:
             *   0xb4bc  rpt #0x4d ; reada *AR1+  -> 77 copies, 0x4387..0x43d3,
             *                                       exact (77 = 0x4d + 1)
             *   0xb4c4  rpt #0x02 ; reada *AR1+  -> 2 copies, 0x43d5 and 0x43d6,
             *                                       one short of the expected 3
             * RPT therefore counts correctly in one case and one short in the
             * other, so RPT itself is not the culprit. Over 76 passes,
             * data[0x43d7] -- entry 2 of the task dispatch table -- is never
             * initialised.
             * The probe prints AR1 before resolve_smem, the resolved address and
             * the repeat state: three lines with the third outside 0x43d7 means
             * the `*AR1+` pointer slips, two lines mean the third iteration never
             * happens and the repeat/instruction interaction is at fault.
             * Limited to the second loop's PC (0xb4c5), 60 lines. */
            uint16_t _ar1_before = s->ar[1];
            addr = resolve_smem(s, op, &ind);
            if (s->pc == 0xb4c5) {
                static unsigned _ri;
                if (_ri < 60) {
                    _ri++;
                    fprintf(stderr, "[c54x] READA-ITER #%u AR1 0x%04x -> 0x%04x "
                            "addr=0x%04x A_low=0x%04x mvpd_src=0x%04x "
                            "rpt_active=%d rpt_count=%u rpt_fresh=%d insn=%u\n",
                            _ri, _ar1_before, s->ar[1], addr,
                            (unsigned)(s->a & 0xFFFF), s->mvpd_src,
                            s->rpt_active, s->rpt_count, s->rpt_fresh,
                            s->insn_count);
                }
            }
            /* ⚠️ Under RPT the first iteration must start from A_low, the source
             * base, not from the mvpd_src left by a previous READA. Otherwise
             * `RPT #N ; READA *ARx+` copies from the wrong ROM region and fills
             * the dispatch table at 0x4380+ with garbage (0xf074), so the BACC
             * lands in the LUT instead of the real FB handler at 0xab38. */
            uint16_t psrc;
            if (!s->rpt_active || s->rpt_fresh) {
                psrc = (uint16_t)(s->a & 0xFFFF);
                s->rpt_fresh = false;
            } else {
                psrc = s->mvpd_src;
            }
            uint16_t v = prog_read(s, psrc);
            data_write(s, addr, v);
            s->mvpd_src = psrc + 1;
            { /* Cap of 200: the first loop (rpt #0x4d, 77 copies) consumes a cap
                 * of 20 entirely and hides the second one (rpt #0x02 into the
                 * dispatch table at 0x43d5). */
                static int reada_log = 0; if (reada_log++ < 200)
                C54_LOG("READA: prog[0x%04x]=0x%04x → data[0x%04x] PC=0x%04x rpt=%d insn=%u",
                        psrc, v, addr, s->pc, s->rpt_count, s->insn_count); }
            return consumed + s->lk_used;
        }
        /* 7Fxx: WRITA Smem — write data[Smem] → prog[A_low] (mirror of READA) */
        if (hi8 == 0x7F) {
            addr = resolve_smem(s, op, &ind);
            uint16_t pdst = s->rpt_active ? s->mvpd_src : (uint16_t)(s->a & 0xFFFF);
            prog_write(s, pdst, data_read(s, addr));
            s->mvpd_src = pdst + 1;
            return consumed + s->lk_used;
        }
        /* 6Dxx: MAR Smem — modify address register (side effects only) */
        if (hi8 == 0x6D) {
            addr = resolve_smem(s, op, &ind);
            /* MAR only modifies AR via addressing mode, no data access */
            return consumed + s->lk_used;
        }
        /* 76xx: ST #lk, Smem  (2 or 3 words) — store 16-bit literal to data
         * memory. Per binutils tic54x-opc.c {st, 2,2,2, 0x7600, 0xFF00,
         * {OP_lk, OP_Smem}} and tic54x-dis.c get_insn_size = words +
         * has_lkaddr (extra word when Smem mode in 0xC..0xF).
         *
         * Encoding (verified via tic54x-dis.c:192-204):
         *   word 0 = opcode (0x76xx)
         *   word 1 = lkaddr  (Smem extension, only if mode in 0xC..0xF)
         *   word N = opcode2 (the #lk value being stored, last extension)
         *
         * ⚠️ Do not decode this as a 1-word LDM MMR,dst: the real LDM is 0x48xx
         * mask 0xFE00, handled in the 0x4 group. With a 1-word decode, PC advances
         * by 1 instead of 2 or 3 and the literal executes as a stray opcode; a
         * stray 0x4F00 (DST B,Lmem with DP=0, i.e. MMR_IMR) zeroes the IMR for
         * good, masks INT3 and BRINT0, and parks the DSP in the RPTB at
         * e9ab..e9b6 waiting for a frame interrupt that is never served. */
        if (hi8 == 0x76) {
            static unsigned hit76_log;
            addr = resolve_smem(s, op, &ind);
            op2 = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
            consumed = 2;
            if (hit76_log++ < 30) {
                if (calypso_debug_enabled("HIT-76")) fprintf(stderr,
                        "[c54x] HIT-76 PC=0x%04x op=0x%04x addr=0x%04x "
                        "lk=0x%04x lk_used=%d insn=%u\n",
                        s->pc, op, addr, op2, s->lk_used, s->insn_count);
            }
            data_write(s, addr, op2);
            return consumed + s->lk_used;
        }
        /* 77xx: STM #lk, MMR (2 words) */
        if (hi8 == 0x77) {
            uint8_t mmr = resolve_mmr(s, op);   /* direct or *ARx / *ARx- / *ARx+ */
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            /* WATCH-ST1-WRITE: MMR 0x07 is ST1. Logs every STM #lk,ST1, including
             * the ones that leave INTM alone but redefine the whole ST1 word:
             * value written, bit 11 (INTM) and the delta against the current ST1.
             * First 200 entries for boot, then one in 100. */
            if (mmr == 0x07) {
                static unsigned st1w;
                st1w++;
                if (st1w <= 200 || (st1w % 100) == 0) {
                    int new_intm = !!(op2 & (1 << 11));
                    int cur_intm = !!(s->st1 & ST1_INTM);
                    if (calypso_debug_enabled("ST1-WR")) fprintf(stderr,
                            "[c54x] ST1-WR #%u STM #0x%04x,ST1 PC=0x%04x "
                            "cur=0x%04x->0x%04x INTM:%d->%d insn=%u XPC=%d\n",
                            st1w, op2, s->pc, s->st1, op2,
                            cur_intm, new_intm, s->insn_count, s->xpc);
                }
            }
            data_write(s, mmr, op2);
            return consumed + s->lk_used;
        }
        /* @BEQUILLE — 0x72 MVDM stays mis-decoded (generic STL fallthrough).
         *
         *   masks   : an upstream AR3 setup bug. 0x72xx is MVDM MMR,dmad and is
         *             really 2 words; decoded as a 1-word STL it under-consumes
         *             the operand word.
         *   remove  : once the 0xee38 deadlock below is fixed, i.e. once AR3 is
         *             set up correctly before that point.
         *
         * [2026-06-02] Measured both ways. With the mis-decode: at PC=0xf564
         * op=0x7215 = MVDM data[0x0014] -> AR5, in the heart of the FB dispatch
         * loop (0xf561..0xf588), the operand 0x0014 executes as an opcode, the
         * loop desynchronises every iteration, AR5 (the task handler pointer) is
         * never loaded, the FB task is never dispatched and d_fb_det is never
         * armed. With the ISA-correct 2-word MVDM: the dispatch does reach
         * PC=0xee38 (task_md=5 read on both pages), but AR3 there points OUTSIDE
         * the I/Q buffer (0x2b97 > 0x2b28), the correlation runs on zeros, the BSP
         * stops delivering and INT3 stops firing (3860 interrupts down to 4) --
         * a worse deadlock. The mis-decoded STL was compensating for the broken
         * AR3 setup. 0x73 (MVMD, the save direction) IS fixed below. */
        /* PORTR 0x74 / PORTW 0x75 are 3 words when Smem is absolute.
         * tic54x-opc.c: portr {2,2,2,0x7400,0xFF00,{OP_PA,OP_Smem}},
         * portw {2,2,2,0x7500,0xFF00,{OP_Smem,OP_PA}}. Base 2 words (opcode + PA),
         * plus one when Smem uses absolute/long addressing (binutils
         * get_insn_size = words + has_lkaddr). resolve_smem reads the absolute
         * Smem address at pc+1 and sets lk_used; the PA follows at pc+1+lk_used,
         * the same convention as CMPM/BITF below.
         * ⚠️ The generic `(op & 0xF800)==0x7000` catch-all below swallows them as
         * a 1-word STL, loses the PA word and shifts alignment by one word.
         * Measured against binutils-2.21.1: PROM0 0xb416
         * `portw *(0x000e),0xf900` sized as 2 words makes its PA 0xf900 be read as
         * a phantom CC at 0xb418, which enters the epilogue at 0x76f8 bare (POPM
         * ST1 with no PSHM ST1), over-pops SP and collapses it, leaving
         * d_fb_det = 0. The firmware holds 128 `75f8` and 25 `74f8` sites.
         * The real I/O semantics are handled below; this fixes the LENGTH. */
        if ((op & 0xFF00) == 0x7500) {        /* PORTW Smem, PA */
            addr = resolve_smem(s, op, &ind); /* applies the AR post-modify, sets lk_used when absolute */
            uint16_t pa = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
            /* RIF (calypso_rif.c): SPCR/SPCX/DXR are really written. SPCR is how
             * the firmware opens RINT_MASK or RDMA_MASK, i.e. how it chooses the
             * transfer mode of CAL207 §3.7.1. */
            /* A5 coprocessor (0x2800..0x2818), see calypso_a5.c. */
            if (calypso_a5_portw(s, pa, data_read(s, addr))) {
                consumed = 2;
                return consumed + s->lk_used;
            }
            if (calypso_rif_portw(s, pa, data_read(s, addr))) {
                consumed = 2;
                return consumed + s->lk_used;
            }
            {   /* DSP-side DMA window; see PORTR below */
                uint16_t wv = data_read(s, addr);
                if (calypso_rhea_dma_xio(true, pa, &wv, s->pc)) {
                    consumed = 2;
                    return consumed + s->lk_used;
                }
                if (calypso_xio_misc(true, pa, &wv, s->pc)) {
                    consumed = 2;
                    return consumed + s->lk_used;
                }
            }
            (void)pa; (void)addr;             /* any other port: not modelled */
            consumed = 2;                     /* opcode + PA */
            return consumed + s->lk_used;
        }
        if ((op & 0xFF00) == 0x7400) {        /* PORTR PA, Smem */
            addr = resolve_smem(s, op, &ind);
            uint16_t pa = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
            /* PORTR-ANY (read-only, unconditional): counts every PORTR hit
             * whatever the PA, to tell "opcode never reached" from "reached but
             * PA is not 0xF430/0x0034". Capped at 30. */
            {
                static unsigned _pany = 0;
                if (_pany++ < 30)
                    fprintf(stderr, "[c54x] PORTR-ANY #%u PA=0x%04x addr=0x%04x PC=0x%04x insn=%u\n",
                            _pany, pa, addr, s->pc, s->insn_count);
            }
            /* RIF (calypso_rif.c, CAL207 §12) is the only target the firmware
             * polls here: [2026-08-03] all 30 PORTR-ANY samples had PA=0x0003,
             * i.e. SPCR. */
            {
                uint16_t rv;
                if (calypso_a5_portr(s, pa, &rv)) {
                    data_write(s, addr, rv);
                    consumed = 2;
                    return consumed + s->lk_used;
                }
                if (calypso_rif_portr(s, pa, &rv)) {
                    data_write(s, addr, rv);
                    consumed = 2;
                    return consumed + s->lk_used;
                }
                /* DMA window as seen from the DSP (XIO:FC00..FCFF, CAL207 §11.1).
                 * The ARM hands the RIF channels to the DSP (measured
                 * ALLOC_CONFIG=0x000C), so the transfer configuration goes through
                 * here. */
                rv = 0;
                if (calypso_rhea_dma_xio(false, pa, &rv, s->pc)) {
                    data_write(s, addr, rv);
                    consumed = 2;
                    return consumed + s->lk_used;
                }
                /* API Control (F900) and the DSP INTH (FA00); see calypso_xio.c */
                rv = 0;
                if (calypso_xio_misc(false, pa, &rv, s->pc)) {
                    data_write(s, addr, rv);
                    consumed = 2;
                    return consumed + s->lk_used;
                }
            }
            (void)pa; (void)addr;
            consumed = 2;
            return consumed + s->lk_used;
        }

        /* 0x73xx: MVMD MMR,dmad — MMR -> data[dmad], 2 words, the SAVE direction.
         * It reads the MMR (SP is 0x18, aliased at data 0x0018) and writes
         * data[dmad], leaving SP and the MMR unchanged.
         * [2026-07-23] Measured with the SP-CORRUPT watchpoint: 0xa4f8 op=0x7318 =
         * MVMD SP,0x3f6e saves SP, but the generic STL fallthrough LOADED SP from
         * data[0x3f6e], which holds garbage (SP=0xc905 and similar), derailing
         * after every POPD. Only 0x73 is fixed here; 0x72 (MVDM, the load
         * direction) keeps its documented crutch above. */
        if ((op & 0xFF00) == 0x7300) {
            int mmr = op & 0x7F;
            uint16_t dmad = prog_fetch(s, s->pc + 1);
            data_write(s, dmad, data_read(s, mmr));
            consumed = 2;
            return consumed + s->lk_used;
        }

        /* 0x70xx: MVKD dmad,Smem — data[dmad] -> data[Smem].
         * binutils tic54x-opc.c {mvkd,2,2,2,0x7000,0xFF00,{OP_dmad,OP_Smem}}.
         * Base 2 words (opcode + dmad), plus one when Smem is absolute/long
         * (mode 0xC..0xF, has_lkaddr). Operand order: dmad FIRST at pc+1, the
         * absolute-Smem lk SECOND at pc+2.
         *
         * ⚠️ The generic `(op & 0xF800)==0x7000` catch-all below decodes 0x70xx as
         * a 1-word STL (+lk) and under-consumes the dmad word. [2026-06-23] Traced
         * with the AR3-TRIP probe: at PROM0 0xb3cc (`70f8 4356 00e3`) the third
         * word 0x00e3 then executed as a stray `ADD *AR3(lk)`, and 0xb3d1=0x00db as
         * `ADD *AR3+0%,A`, adding AR0 (about SP) to AR3 every turn until AR3 swept
         * memory and overwrote data[0x0c36], the task pointer, giving CALA 0 and
         * the boot-stub return loop. The ADD decode itself was faithful; the bug
         * was the LENGTH of the MVKD ahead of it. */
        if ((op & 0xFF00) == 0x7000) {
            uint16_t dmad = prog_fetch(s, s->pc + 1);
            int mode = (op & 0x80) ? ((op >> 3) & 0x0F) : -1;
            uint16_t smem_addr;
            s->lk_used = false;
            /* WORD ORDER IN LONG ADDRESSING: lk FIRST (pc+1), dmad SECOND (pc+2),
             * like PORTW `75f8 4356 2800` and ST `76f8 4356 0000` in this same core
             * and like the binutils disassembler. The reverse order would turn
             * `70f8 0012 0014` (0x76c0, power measurement) into AR4 <- AR2 instead
             * of AR2 <- AR4: the PM routine would read a stale AR2 and write its
             * results at 0x3d0b..0x3d89, over the TCH variables -- including the
             * SACCH buffer pointer 0x3d89 sent to the API RAM (SACCH never decoded,
             * LOS in call, d_debug_ptr overwritten).
             * CALYPSO_MVKD_DMAD_AVANT=1 selects the reverse order (A/B). */
            static int dmad_avant = -1;
            if (dmad_avant < 0) { const char *e = calypso_getenv("CALYPSO_MVKD_DMAD_AVANT"); dmad_avant = (e && *e == '1'); }
            if (mode >= 0xC && !dmad_avant) dmad = prog_fetch(s, s->pc + 2);
            if (mode >= 0xC) {                 /* absolute/long Smem: lk @pc+1 (see above) */
                uint16_t lk = prog_fetch(s, s->pc + (dmad_avant ? 2 : 1));
                int nar = op & 0x07;
                if (mode == 0xC) {                      /* *ARx(lk), no modify */
                    smem_addr = (uint16_t)(s->ar[nar] + lk);
                } else if (mode == 0xD || mode == 0xE) { /* *+ARx(lk)[%] premod */
                    s->ar[nar] = (uint16_t)(s->ar[nar] + lk);
                    smem_addr = s->ar[nar];
                } else {                                 /* 0xF: *(lk) absolute */
                    smem_addr = lk;
                }
                s->st0 = (s->st0 & ~ST0_ARP_MASK) | (nar << ST0_ARP_SHIFT);
                s->lk_used = true;
            } else {                           /* direct or non-absolute indirect */
                smem_addr = resolve_smem(s, op, &ind);  /* plus the AR post-modify */
            }
            /* SPRU172C MVKD: dmad -> DAR; under RPT, DAR + 1 after each transfer ("move consecutive
             * words"). Re-reading dmad at every repetition would make `rpt #15 ; mvkd 0x2c00,*ar7+`
             * (PROM0 0x9aaa, TCH Viterbi metrics history read by the CHED 0x9ad3) copy data[0x2c00]
             * 16 times. Same mechanism as READA/MVPD/MACP (rpt_fresh, mvpd_src).
             * CALYPSO_MVK_RPT_ANCIEN=1 = dmad re-read at every repetition (MVKD and MVDK). */
            {   static int mvk_ancien = -1;
                if (mvk_ancien < 0) { const char *e = getenv("CALYPSO_MVK_RPT_ANCIEN"); mvk_ancien = (e && *e == '1') ? 1 : 0; }
                if (!mvk_ancien && s->rpt_active) {
                    if (s->rpt_fresh) s->rpt_fresh = false; else dmad = s->mvpd_src;
                    s->mvpd_src = (uint16_t)(dmad + 1);
                }
            }
            data_write(s, smem_addr, data_read(s, dmad));
            consumed = 2;
            return consumed + (s->lk_used ? 1 : 0);
        }

        /* 0x71xx: MVDK Smem,dmad — data[Smem] -> data[dmad], the mirror of MVKD.
         * binutils tic54x-opc.c {mvdk,2,2,2,0x7100,0xFF00,{OP_Smem,OP_dmad}}.
         * Same encoding as MVKD 0x70 (Smem in the opcode low byte, dmad at pc+1,
         * the absolute-Smem lk at pc+2); only the direction differs.
         *
         * ⚠️ The `(op & 0xF800)==0x7000` catch-all decodes these as a 1-word STL and
         * under-consumes the dmad word. [2026-06-24] At PROM0 0xb3db..0xb3e3 three
         * 3-word MVDK (`71f8 4356 00e3`, `71f8 4357 00db`, `71f8 4355 00d3`) restore
         * what the three symmetric MVKD at 0xb3cc saved before three CALLs; with the
         * short decode the operands 00e3/00db/00d3 execute as stray ADDs (0x00db is
         * `ADD *AR3+0%,A`) on every frame, corrupting AR3 and A and losing the
         * restore. 0x71 is data-to-data, not MMR, so it is independent of the 0x72
         * crutch above. */
        if ((op & 0xFF00) == 0x7100) {
            uint16_t dmad = prog_fetch(s, s->pc + 1);
            int mode = (op & 0x80) ? ((op >> 3) & 0x0F) : -1;
            uint16_t smem_addr;
            s->lk_used = false;
            /* WORD ORDER IN LONG ADDRESSING: lk FIRST (pc+1), dmad SECOND (pc+2),
             * like PORTW `75f8 4356 2800` and ST `76f8 4356 0000` in this same core
             * and like the binutils disassembler. The reverse order would turn
             * `70f8 0012 0014` (0x76c0, power measurement) into AR4 <- AR2 instead
             * of AR2 <- AR4: the PM routine would read a stale AR2 and write its
             * results at 0x3d0b..0x3d89, over the TCH variables -- including the
             * SACCH buffer pointer 0x3d89 sent to the API RAM (SACCH never decoded,
             * LOS in call, d_debug_ptr overwritten).
             * CALYPSO_MVKD_DMAD_AVANT=1 selects the reverse order (A/B). */
            static int dmad_avant = -1;
            if (dmad_avant < 0) { const char *e = calypso_getenv("CALYPSO_MVKD_DMAD_AVANT"); dmad_avant = (e && *e == '1'); }
            if (mode >= 0xC && !dmad_avant) dmad = prog_fetch(s, s->pc + 2);
            if (mode >= 0xC) {                 /* absolute/long Smem: lk @pc+1 (see above) */
                uint16_t lk = prog_fetch(s, s->pc + (dmad_avant ? 2 : 1));
                int nar = op & 0x07;
                if (mode == 0xC) {                      /* *ARx(lk), no modify */
                    smem_addr = (uint16_t)(s->ar[nar] + lk);
                } else if (mode == 0xD || mode == 0xE) { /* *+ARx(lk)[%] premod */
                    s->ar[nar] = (uint16_t)(s->ar[nar] + lk);
                    smem_addr = s->ar[nar];
                } else {                                 /* 0xF: *(lk) absolute */
                    smem_addr = lk;
                }
                s->st0 = (s->st0 & ~ST0_ARP_MASK) | (nar << ST0_ARP_SHIFT);
                s->lk_used = true;
            } else {                           /* direct or non-absolute indirect */
                smem_addr = resolve_smem(s, op, &ind);  /* +post-modify AR */
            }
            /* SPRU172C MVDK: same DAR incrementing under RPT (see MVKD). */
            {   static int mvk_ancien = -1;
                if (mvk_ancien < 0) { const char *e = getenv("CALYPSO_MVK_RPT_ANCIEN"); mvk_ancien = (e && *e == '1') ? 1 : 0; }
                if (!mvk_ancien && s->rpt_active) {
                    if (s->rpt_fresh) s->rpt_fresh = false; else dmad = s->mvpd_src;
                    s->mvpd_src = (uint16_t)(dmad + 1);
                }
            }
            data_write(s, dmad, data_read(s, smem_addr));   /* MVDK : dmad <- Smem */
            consumed = 2;
            return consumed + (s->lk_used ? 1 : 0);
        }

        /* 0x72 MVDM dmad,MMR (MMR <- data[dmad]) and 0x73 MVMD MMR,dmad
         * (data[dmad] <- MMR): 2 words (opcode + dmad), MMR in the low byte, mapped
         * to data 0x00-0x1f which data_read/data_write route to the registers.
         * These unblock the go-live subroutine at 0xaad5 (7211 434f MVDM
         * data[0x434f] -> AR1; 7210 434e; 7310 434e MVMD AR0 -> data[0x434e]): with
         * the 1-word STL catch-all, A stays 0, the BC at 0xa4cd (AEQ, A==0) never
         * releases and RSBX INTM at 0xa51b is never reached. [2026-07-23] With the
         * fix, D_TASK_MD reads go from 0 to 1859 and the DSP reaches the frame
         * dispatcher.
         * ⚠️ This fix only holds once the upstream AR3 setup is right, i.e. with
         * MVKD 0x70 and MVDK 0x71 fixed; applied before them it deadlocks at
         * 0xee38. */
        if ((op & 0xFF00) == 0x7200) {       /* MVDM dmad, MMR */
            uint16_t dmad = prog_fetch(s, s->pc + 1);
            uint16_t mmr  = op & 0x00FF;
            data_write(s, mmr, data_read(s, dmad));
            consumed = 2;
            return consumed;
        }
        if ((op & 0xFF00) == 0x7300) {       /* MVMD MMR, dmad */
            uint16_t dmad = prog_fetch(s, s->pc + 1);
            uint16_t mmr  = op & 0x00FF;
            data_write(s, dmad, data_read(s, mmr));
            consumed = 2;
            return consumed;
        }

        /* [2026-10-15] 0x7C MVPD pmad,Smem (Smem <- Pmem(pmad)) and 0x7D MVDP
         * Smem,pmad (Pmem(pmad) <- Smem). binutils tic54x-opc.c
         *   { "mvpd", 2,2,2, 0x7C00, 0xFF00, {OP_pmad,OP_Smem} }
         *   { "mvdp", 2,2,2, 0x7D00, 0xFF00, {OP_Smem,OP_pmad} }
         * 2 words (+1 when Smem is absolute/long: lk at pc+1, pmad after it, the
         * same order as PORTW/ST #lk here). SPRU172C: under RPT, PAR = pmad is
         * incremented after each transfer (rpt_fresh/mvpd_src, as READA/MACP).
         * ⚠️ They fell into the 0x78-0x7F catch-all below as a 1-word STH src,
         * Smem: the high half of A/B was stored instead of the program word, and
         * the pmad word then executed as an opcode. PROM0 has 13 MVPD sites
         * (0xa7a0, 0xa912, 0xad8d, 0xb8e3, 0xbc93, ... `7c90/7cd0/7c91 pmad`, table
         * loads *AR0+ / *AR1+). CALYPSO_MVPD_ANCIEN=1 restores the old STH decode. */
        static int mvpd_ancien = -1;
        if (mvpd_ancien < 0) mvpd_ancien = calypso_gate("CALYPSO_MVPD_ANCIEN", 0);
        if ((op & 0xFE00) == 0x7C00 && !mvpd_ancien) {
            addr = resolve_smem(s, op, &ind);
            uint16_t pmad = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
            uint16_t padr;
            if (!s->rpt_active || s->rpt_fresh) { padr = pmad; s->rpt_fresh = false; }
            else padr = s->mvpd_src;
            if (hi8 == 0x7C) data_write(s, addr, prog_read(s, padr));   /* MVPD */
            else             prog_write(s, padr, data_read(s, addr));   /* MVDP */
            s->mvpd_src = (uint16_t)(padr + 1);
            consumed = 2;
            return consumed + s->lk_used;
        }

        /* LD / ST operations */
        if ((op & 0xF800) == 0x7000) {
            /* 70xx: STL src, Smem */
            int src_acc = (op >> 9) & 1;
            addr = resolve_smem(s, op, &ind);
            int64_t acc = src_acc ? s->b : s->a;
            data_write(s, addr, (uint16_t)(acc & 0xFFFF));
            return consumed + s->lk_used;
        }
        /* FIX_MACP_MACD — macp Smem,pmad,src (0x7800/0xFE00) and
         * macd Smem,pmad,src (0x7A00/0xFE00), bit 8 = src, TWO words plus the Smem
         * lk. SPRU172C: src = src + Smem x Pmem(pmad); T = Smem; MACD also copies
         * Smem into Smem+1. Under RPT, pmad increments each turn, tracked like
         * READA/MVPD through rpt_fresh and mvpd_src.
         * ⚠️ They otherwise fall into the 1-word "STH" block below and the pmad
         * word executes as an instruction: [2026-09-17] on the SB trace, 0x7e2a
         * `7892 7a1c` under RPT had `7a1c` taken for a MACD. */
        if ((op & 0xFC00) == 0x7800) {
            addr = resolve_smem(s, op, &ind);
            uint16_t pmad = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
            consumed = 2;
            uint16_t psrc;
            if (!s->rpt_active || s->rpt_fresh) { psrc = pmad; s->rpt_fresh = false; }
            else psrc = s->mvpd_src;
            uint16_t sval = data_read(s, addr);
            int64_t prod = (int64_t)(int16_t)sval * (int64_t)(int16_t)prog_fetch(s, psrc);
            if (s->st1 & ST1_FRCT) prod <<= 1;
            if (op & 0x0100) s->b = sext40(s->b + prod);
            else             s->a = sext40(s->a + prod);
            s->t = sval;
            if (op & 0x0200) data_write(s, (uint16_t)(addr + 1), sval);   /* MACD */
            s->mvpd_src = (uint16_t)(psrc + 1);
            return consumed + s->lk_used;
        }
        if ((op & 0xF800) == 0x7800) {
            /* 78xx-7Fxx: STH src, Smem
             * Note: BANZ (0x78xx per doc) shares this range but is handled
             * via F84x (BANZ with condition) in the F8xx group. */
            int src_acc = (op >> 9) & 1;
            addr = resolve_smem(s, op, &ind);
            int64_t acc = src_acc ? s->b : s->a;
            data_write(s, addr, (uint16_t)((acc >> 16) & 0xFFFF));
            return consumed + s->lk_used;
        }
        /* 0x6000-0x60FF: CMPM Smem, lk  (compare memory with long immediate)
         * Per tic54x-opc.c: { "cmpm", 2,2,2, 0x6000, 0xFF00 }
         * Sets TC = (data[Smem] == lk).
         *
         * The DSP bootloader at PROM0 0xb41c / 0xb424 polls
         *   CMPM *(0x0fff), 4   then  CMPM *(0x0fff), 2
         * waiting for the ARM-side BL_CMD_STATUS write. ⚠️ Folded into the generic
         * 0x6000-0x67FF "LD" path it sets the accumulator and never updates TC, so
         * the following BC NTC always branches back and loops forever. */
        if ((op & 0xFF00) == 0x6000) {
            addr = resolve_smem(s, op, &ind);
            uint16_t cmp_val = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
            uint16_t mem_val = data_read(s, addr);
            if (mem_val == cmp_val) s->st0 |= ST0_TC;
            else                    s->st0 &= ~ST0_TC;
            consumed = 2;  /* opcode + cmp_val (smem extra lk added via lk_used) */
            return consumed + s->lk_used;
        }
        /* 0x6100-0x61FF: BITF Smem, lk — bit-field test, TC = (Smem & lk)!=0 */
        if ((op & 0xFF00) == 0x6100) {
            addr = resolve_smem(s, op, &ind);
            uint16_t mask = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
            uint16_t mem_val = data_read(s, addr);
            bool tc_before = (s->st0 & ST0_TC) != 0;
            if (mem_val & mask) s->st0 |= ST0_TC;
            else                s->st0 &= ~ST0_TC;
            bool tc_after = (s->st0 & ST0_TC) != 0;
            consumed = 2;
            /* FBWATCH: exact capture at the foreground poll sites 0xf7af/0xf7b7 --
             * resolved address, value read and TC -- to name the flag that is never
             * true. */
            if (g_fbwatch_on > 0 && (s->pc == 0xf7af || s->pc == 0xf7b7)
                && s->insn_count > 100000) {   /* after the first wire, at insn 32768 */
                static unsigned wbf = 0;
                if (wbf++ < 30)
                    fprintf(stderr, "[c54x] FBWATCH-BITF pc=0x%04x addr=0x%04x "
                            "mem=0x%04x mask=0x%04x -> TC=%d insn=%u\n",
                            s->pc, addr, mem_val, mask, tc_after, s->insn_count);
            }
            /* BITF instrumentation: does BITF really set TC? Many calls with
             * tc_after=1 rarely means the mask/mem_val pattern never yields TC=1,
             * so BC NTC always branches and `ST #1, d_task_d` at PROM 0x9ab1 is
             * never reached. First 200 hits, then one in 1000. */
            {
                static uint64_t bitf_total;
                static uint64_t bitf_tc_set;
                static uint64_t bitf_tc_clear;
                bitf_total++;
                if (tc_after) bitf_tc_set++;
                else          bitf_tc_clear++;
                if (bitf_total <= 200 || (bitf_total % 1000) == 0) {
                    if (calypso_debug_enabled("BITF-PROBE")) fprintf(stderr,
                            "[c54x] BITF-PROBE #%llu PC=0x%04x addr=0x%04x "
                            "mem=0x%04x mask=0x%04x tc_before=%d tc_after=%d "
                            "(total=%llu set=%llu clear=%llu)\n",
                            (unsigned long long)bitf_total, s->last_exec_pc,
                            addr, mem_val, mask, tc_before, tc_after,
                            (unsigned long long)bitf_total,
                            (unsigned long long)bitf_tc_set,
                            (unsigned long long)bitf_tc_clear);
                }
            }
            return consumed + s->lk_used;
        }
        /* FIX_MPY_MAC_LK — mpy Smem,#lk,dst (0x6200/0xFE00, bit 8 = dst) and
         * mac Smem,#lk,src[,dst] (0x6400/0xFC00, bit 9 = src, bit 8 = dst): TWO
         * words plus the Smem lk. SPRU172C: dst = Smem x lk; dst = src + Smem x lk;
         * T = Smem.
         * ⚠️ They otherwise fall into the 1-word "LD" block below and the #lk word
         * executes as an instruction: [2026-09-17] on the SB trace, 0x762c
         * `6283 36f6` and 0x7692 `6283 5a82` (cos 45). */
        if ((op & 0xF800) == 0x6000 && (op & 0x0600) != 0) {
            addr = resolve_smem(s, op, &ind);
            uint16_t lk = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
            consumed = 2;
            uint16_t sval = data_read(s, addr);
            int64_t prod = (int64_t)(int16_t)sval * (int64_t)(int16_t)lk;
            if (s->st1 & ST1_FRCT) prod <<= 1;
            if ((op & 0xFE00) == 0x6200) {                    /* mpy */
                if (op & 0x0100) s->b = sext40(prod); else s->a = sext40(prod);
            } else {                                          /* mac 0x64..0x67 */
                int64_t base = (op & 0x0200) ? s->b : s->a;
                if (op & 0x0100) s->b = sext40(base + prod); else s->a = sext40(base + prod);
            }
            s->t = sval;
            return consumed + s->lk_used;
        }
        if ((op & 0xF800) == 0x6000) {
            /* 60xx-67xx: LD Smem, dst (other variants — fallback) */
            int dst_acc = (op >> 9) & 1;
            int shift = (op >> 8) & 1;
            addr = resolve_smem(s, op, &ind);
            uint16_t val = data_read(s, addr);
            int64_t v = (s->st1 & ST1_SXM) ? (int16_t)val : val;
            if (shift) v <<= 16;  /* LD Smem, 16, dst */
            if (dst_acc) s->b = sext40(v); else s->a = sext40(v);
            return consumed + s->lk_used;
        }
        /* 0x6800-0x6BFF + 0x6Cxx + 0x6Exx: companion to the 0x6F00 fix below.
         * Per binutils tic54x-opc.c (verified against insn_template struct):
         *   0x6800 ANDM  #lk, Smem      data[Smem] = data[Smem] & lk     (2-word)
         *   0x6900 ORM   #lk, Smem      data[Smem] = data[Smem] | lk     (2-word)
         *   0x6A00 XORM  #lku, Smem     data[Smem] = data[Smem] ^ lku    (2-word)
         *   0x6B00 ADDM  #lk, Smem      data[Smem] = data[Smem] + lk     (2-word)
         *   0x6C00 BANZ  pmad, Sind     if (ARx != 0) PC = pmad          (2-word)
         *   0x6E00 BANZD pmad, Sind     same as BANZ but with 2 delay slots
         *
         * ⚠️ Without these, the fallback at (op & 0xF800) == 0x6800 below decodes
         * them all as a 1-word LD Smem,T, PC drifts by one word and the lk or pmad
         * operand executes as a stray instruction. The ROM holds 1259
         * ANDM/ORM/XORM/ADDM sites and 304 BANZ/BANZD sites, 1563 in all. */
        if ((op & 0xFF00) == 0x6800) {
            /* ANDM #lk, Smem */
            addr = resolve_smem(s, op, &ind);
            uint16_t lk = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
            uint16_t v = data_read(s, addr);
            data_write(s, addr, v & lk);
            consumed = 2;
            return consumed + s->lk_used;
        }
        if ((op & 0xFF00) == 0x6900) {
            /* ORM #lk, Smem */
            addr = resolve_smem(s, op, &ind);
            uint16_t lk = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
            uint16_t v = data_read(s, addr);
            data_write(s, addr, v | lk);
            consumed = 2;
            return consumed + s->lk_used;
        }
        if ((op & 0xFF00) == 0x6A00) {
            /* XORM #lku, Smem */
            addr = resolve_smem(s, op, &ind);
            uint16_t lku = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
            uint16_t v = data_read(s, addr);
            data_write(s, addr, v ^ lku);
            consumed = 2;
            return consumed + s->lk_used;
        }
        if ((op & 0xFF00) == 0x6B00) {
            /* ADDM #lk, Smem — add signed lk to memory (wrap mod 2^16) */
            addr = resolve_smem(s, op, &ind);
            int16_t lk = (int16_t)prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
            uint16_t v = data_read(s, addr);
            uint16_t res = (uint16_t)((int16_t)v + lk);
            /* [2026-10-15] SPRU172C ADDM: "Affected by OVM and SXM". The ALU
             * works on the operands aligned in bits 31-16 (sign-extended when
             * SXM=1) and stores bits 31-16, so with OVM=1 a 16-bit overflow
             * saturates to 7FFFh / 8000h. Manual example: ADDM 0FFF8h,*AR4+
             * with OVM=SXM=1 on 8007h -> 8000h (isa_test 11). With OVM=0 the
             * stored word is the plain wrap, i.e. unchanged behaviour.
             * CALYPSO_ADDM_OVM=0 restores the unconditional wrap. */
            static int addm_ovm = -1;
            if (addm_ovm < 0) { const char *e = getenv("CALYPSO_ADDM_OVM"); addm_ovm = (e && *e == '0') ? 0 : 1; }
            if (addm_ovm && (s->st1 & ST1_OVM)) {
                int32_t a = (s->st1 & ST1_SXM) ? (int32_t)(int16_t)v  : (int32_t)v;
                int32_t b = (s->st1 & ST1_SXM) ? (int32_t)lk : (int32_t)(uint16_t)lk;
                int32_t r = a + b;
                if (r > 0x7FFF) res = 0x7FFF;
                else if (r < -0x8000) res = 0x8000;
            }
            data_write(s, addr, res);
            consumed = 2;
            /* TODO: C/OVA flag effects per SPRU172C (not verified, untouched) */
            return consumed + s->lk_used;
        }
        if ((op & 0xFF00) == 0x6C00) {
            /* [2026-10-15] lk-indexed Sind (MOD 12..15, e.g. *AR3(-1)): the
             * value tested is the generated address ARx+lk, not ARx -- SPRU172C
             * BANZ example 3: `BANZ 2000h,*AR3(-1)` with AR3=1 falls through to
             * PC+3. The ROM uses it as a compare idiom: PROM2 0x1801
             * `stlm a,ar1 ; nop ; nop ; banz 0x980a,*ar1(0xfff0)` branches iff
             * AR1 != 16 (also *ar1(0xffff) after cala, PROM0 0x565f/0x596a/0x59d5).
             * See c54x_banz_value. */
            /* BANZ pmad, Sind — branch if ARx (selected by ARF in op[2:0])
             * is non-zero. Tests the PRE-modify value; resolve_smem applies the
             * post-mod whether or not the branch is taken.
             * ⚠️ Reading ARP from ST0 here takes the PREVIOUS instruction's nar and
             * tests the wrong AR; see the resolve_smem comment. */
            int nar = op & 0x07;
            uint16_t pre = s->ar[nar];
            uint16_t ea = resolve_smem(s, op, &ind);
            uint16_t pmad = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
            pre = c54x_banz_value(op, pre, ea);
            consumed = 2;
            if (pre != 0) {
                s->pc = pmad;
                return 0;
            }
            return consumed + s->lk_used;
        }
        if ((op & 0xFF00) == 0x6E00) {
            /* BANZD pmad, Sind — delayed BANZ (2 slots after the 2-word op).
             * Same off-by-ARP fix as BANZ above. */
            int nar = op & 0x07;
            uint16_t pre = s->ar[nar];
            uint16_t ea = resolve_smem(s, op, &ind);
            uint16_t pmad = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
            pre = c54x_banz_value(op, pre, ea);
            consumed = 2;
            if (pre != 0) {
                s->delayed_pc  = pmad;
                s->delay_slots = 2;
            }
            return consumed + s->lk_used;
        }
        /* 0x6F00-0x6FFF: Extended ADD/SUB/LD/STH/STL Smem, SHIFT, DST/SRC (2-word).
         * Per binutils tic54x-opc.c (verified against insn_template struct
         * include/opcode/tic54x.h:85-150):
         *   word0 = 0x6F00 mask 0xFF00 (Smem in low 7 bits)
         *   word1 = sub-opcode in bits 7:5, SRC=bit 9, DST/SRC1=bit 8,
         *           SHIFT=signed 5-bit in bits 4:0
         *     bits 7:5 = 000 → ADD Smem,SHIFT,SRC,[DST]
         *     bits 7:5 = 001 → SUB Smem,SHIFT,SRC,[DST]
         *     bits 7:5 = 010 → LD  Smem,SHIFT,DST
         *     bits 7:5 = 011 → STH SRC1,SHIFT,Smem
         *     bits 7:5 = 100 → STL SRC1,SHIFT,Smem
         *
         * ⚠️ Without this handler, the fallback at (op & 0xF800) == 0x6800 below
         * decodes 0x6Fxx as a 1-word LD Smem,T, PC drifts by one word and the
         * second word executes as a stray instruction. The ROM has 544 such sites.
         * That is what wedged PC=0x8353 in a CALAD A self-loop: 0x6F07 0x0C41
         * mis-decoded let 0x0C41 run as a stray SUB Smem,TS,A, giving A_low=0xFFFA
         * and then A_low=0x8353 after the following ADD. */
        if ((op & 0xFF00) == 0x6F00) {
            addr = resolve_smem(s, op, &ind);
            op2 = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
            int sub = (op2 >> 5) & 0x7;
            int shift_raw = op2 & 0x1F;
            int shift = (shift_raw & 0x10) ? (shift_raw - 32) : shift_raw;
            int dst_b = (op2 >> 8) & 1;   /* bit 8 = DST/SRC1 */
            int src_b = (op2 >> 9) & 1;   /* bit 9 = SRC (ADD/SUB only) */
            consumed = 2;

            switch (sub) {
            case 0: { /* ADD Smem,SHIFT,SRC,[DST]: DST = SRC + (data[Smem]<<shift) */
                uint16_t mv = data_read(s, addr);
                int64_t v = (s->st1 & ST1_SXM) ? (int64_t)(int16_t)mv : (int64_t)mv;
                v = (shift >= 0) ? (v << shift) : (v >> (-shift));
                int64_t src = src_b ? s->b : s->a;
                int64_t result = sext40(src + v);
                if (dst_b) s->b = result; else s->a = result;
                break;
            }
            case 1: { /* SUB Smem,SHIFT,SRC,[DST]: DST = SRC - (data[Smem]<<shift) */
                uint16_t mv = data_read(s, addr);
                int64_t v = (s->st1 & ST1_SXM) ? (int64_t)(int16_t)mv : (int64_t)mv;
                v = (shift >= 0) ? (v << shift) : (v >> (-shift));
                int64_t src = src_b ? s->b : s->a;
                int64_t result = sext40(src - v);
                if (dst_b) s->b = result; else s->a = result;
                break;
            }
            case 2: { /* LD Smem,SHIFT,DST: DST = data[Smem] << shift (SXM-aware) */
                uint16_t mv = data_read(s, addr);
                int64_t v = (s->st1 & ST1_SXM) ? (int64_t)(int16_t)mv : (int64_t)mv;
                v = (shift >= 0) ? (v << shift) : (v >> (-shift));
                if (dst_b) s->b = sext40(v); else s->a = sext40(v);
                break;
            }
            case 3: { /* STH SRC1,SHIFT,Smem: data[Smem] = (SRC1 high 16) << shift */
                int64_t src = dst_b ? s->b : s->a;
                int16_t high = (int16_t)((src >> 16) & 0xFFFF);
                int64_t shifted = (shift >= 0) ? ((int64_t)high << shift)
                                               : ((int64_t)high >> (-shift));
                data_write(s, addr, (uint16_t)(shifted & 0xFFFF));
                break;
            }
            case 4: { /* STL SRC1,SHIFT,Smem: data[Smem] = (SRC1 low) << shift */
                int64_t src = dst_b ? s->b : s->a;
                int64_t shifted = (shift >= 0) ? (src << shift) : (src >> (-shift));
                data_write(s, addr, (uint16_t)(shifted & 0xFFFF));
                break;
            }
            default:
                { static int unk6f = 0; if (unk6f++ < 10)
                    C54_LOG("0x6F unknown sub=%d op=0x%04x op2=0x%04x PC=0x%04x",
                            sub, op, op2, s->pc); }
                break;
            }
            return consumed + s->lk_used;
        }
        if ((op & 0xF800) == 0x6800) {
            /* Unreachable in practice: every 0x68xx-0x6Fxx is taken by a specific
             * handler above (ANDM/ORM/XORM/ADDM/BANZ/BANZD, the extended 0x6F00)
             * or by the 0x6Dxx MAR. This generic "LD Smem,T" fallback used to catch
             * all 2107 of them and drift PC at every one. It is kept as a net: if
             * it ever triggers, the handler for that 0xNN00 prefix is incomplete. */
            addr = resolve_smem(s, op, &ind);
            s->t = data_read(s, addr);
            return consumed + s->lk_used;
        }
        goto unimpl;

    case 0x1: {
        /* 1xxx: LD / LDU / LDR Smem, DST  (per tic54x-opc.c, all mask FE00):
         *   0x1000  LD  Smem, DST          — signed load (SXM-aware)
         *   0x1200  LDU Smem, DST          — unsigned load (zero-extend)
         *   0x1400  LD  Smem, TS, DST      — load shifted by T low bits
         *   0x1600  LDR Smem, DST          — load with rounding
         *
         * ⚠️ The bootloader at PROM0 0xb429 does `LDU *(0x0ffe), A`
         * (op=0x12f8 + lk=0x0ffe) to read BL_ADDR_LO, then BACC A to that target.
         * Decoding case 0x1 as SUB leaves A=0 and the BACC falls into the boot-stub
         * NOPs. */
        addr = resolve_smem(s, op, &ind);
        int dst = (op >> 8) & 1;
        int sub = (op >> 9) & 0x07;  /* selects LD/LDU/LD,TS/LDR within case 1 */
        uint16_t val = data_read(s, addr);
        {   /* LD-TRACE (CALYPSO_DISPATCH_PROBE, read-only), limited to the task
             * dispatcher block 0xb05f..0xb078, so a few dozen lines at most.
             *
             * [2026-08-03] At 0xb060 (`10e1 0000` = LD *AR1(0), A), AR1 holds 0x0814
             * (d_task_d, page W1) and the cell it points at holds 0x0018 (24, ALLC),
             * both confirmed by the CHAIN-B05F probe, yet the accumulator comes out
             * at 0x5294. The chain then compares 0x5294 against the constants
             * 12/30/34, matches none and bails out to 0xb077, which is why index
             * resolution is never reached and RX arming is never requested.
             *
             * Reading it: addr==0x0814 and val==0x0018 with a different final
             * accumulator means the fault is in the accumulator write (sub, dst,
             * sign extension); a different addr means resolve_smem after all. */
            uint16_t _pc = s->last_exec_pc;
            if (_pc >= 0xb05f && _pc <= 0xb078) {
                static int _lt = -1; static unsigned _ltn = 0;
                if (_lt < 0) _lt = calypso_gate("CALYPSO_DISPATCH_PROBE", 0);
                if (_lt && _ltn < 60) {
                    _ltn++;
                    fprintf(stderr,
                            "[dispatch] LD-TRACE pc=0x%04x op=0x%04x dst=%s sub=%d "
                            "addr=0x%04x val_read=0x%04x data[addr]=0x%04x "
                            "A_before=0x%06llx SXM=%d insn=%u\n",
                            _pc, op, dst ? "B" : "A", sub, addr, val,
                            s->data[addr],
                            (unsigned long long)(s->a & 0xFFFFFFULL),
                            (s->st1 & ST1_SXM) ? 1 : 0, s->insn_count);
                    fflush(stderr);
                }
            }
        }
        int64_t v;
        switch (sub) {
        case 0x0:  /* 0x1000: LD Smem, DST — signed (SXM honoured) */
            v = (s->st1 & ST1_SXM) ? (int16_t)val : (uint16_t)val;
            break;
        case 0x1: { /* 0x1200: LDU Smem, DST — always zero-extended */
            v = (uint16_t)val;
            break;
        }
        case 0x2: { /* 0x1400: LD Smem, TS, DST — shift by T[5:0] (signed) */
            int8_t ts = (int8_t)((s->t & 0x3F) | ((s->t & 0x20) ? 0xC0 : 0));
            int64_t base = (s->st1 & ST1_SXM) ? (int16_t)val : (uint16_t)val;
            v = (ts >= 0) ? (base << ts) : (base >> -ts);
            break;
        }
        case 0x3: { /* 0x1600: LDR Smem, DST — load with rounding (+0x8000) */
            v = (s->st1 & ST1_SXM) ? (int16_t)val : (uint16_t)val;
            v = (v << 16) + 0x8000;
            /* [2026-10-15] SPRU172C LDR : (Smem)<<16 + 2^15 -> dst, example LDR *AR1,A gives
             * A = 00 FEDC 8000 : the 2^15 stays in bit 15 (as for RND and LMS). Clearing it made
             * LDR a plain LD Smem,16 and dropped the rounding constant that the MAC chains
             * after it rely on. isa_test 96. CALYPSO_LDR_ANCIEN=1 restores the clearing. */
            static int ldr_ancien = -1;
            if (ldr_ancien < 0) ldr_ancien = calypso_gate("CALYPSO_LDR_ANCIEN", 0);
            if (ldr_ancien) v &= 0xFFFFFFFF0000LL;  /* clear low 16 after rounding */
            if (dst) s->b = sext40(v); else s->a = sext40(v);
            return consumed + s->lk_used;
        }
        /* sub 4..7 are the LOGICAL half of the family; without them they fall into
         * the `default` below and execute as a LD. Encodings per SPRU172C (tables
         * 2-7/2-8/2-9, SUBC p.4-192). Smem is ZERO-extended to 40 bits: the TI
         * example for AND (p.4-12) gives A=00 00FF 1200 & Smem=0x1500 ->
         * A=00 0000 1000.
         * [2026-07-28] Measured: 0x1860 (AND) read as a LD set A=15 instead of
         * A&15, hence T=31 and a `LD Smem,TS` shifting by +31 that saturated the
         * accumulator (A=0x80000000) and flattened the demodulator output. */
        case 0x4: { /* 0x1800: AND Smem, src — src = src & Smem */
            uint64_t cur = (uint64_t)(dst ? s->b : s->a) & 0xFFFFFFFFFFULL;
            uint64_t r = cur & (uint64_t)(uint16_t)val;
            if (dst) s->b = sext40((int64_t)r); else s->a = sext40((int64_t)r);
            return consumed + s->lk_used;
        }
        case 0x5: { /* 0x1A00: OR Smem, src — src = src | Smem */
            uint64_t cur = (uint64_t)(dst ? s->b : s->a) & 0xFFFFFFFFFFULL;
            uint64_t r = cur | (uint64_t)(uint16_t)val;
            if (dst) s->b = sext40((int64_t)r); else s->a = sext40((int64_t)r);
            return consumed + s->lk_used;
        }
        case 0x6: { /* 0x1C00: XOR Smem, src — src = src ^ Smem */
            uint64_t cur = (uint64_t)(dst ? s->b : s->a) & 0xFFFFFFFFFFULL;
            uint64_t r = cur ^ (uint64_t)(uint16_t)val;
            if (dst) s->b = sext40((int64_t)r); else s->a = sext40((int64_t)r);
            return consumed + s->lk_used;
        }
        case 0x7: { /* 0x1E00: SUBC Smem, src — conditional subtract (division) */
            int64_t src = dst ? sext40((int64_t)s->b) : sext40((int64_t)s->a);
            int64_t d = src - ((int64_t)(uint16_t)val << 15);
            int64_t r = (d >= 0) ? ((d << 1) + 1) : (src << 1);
            /* SPRU172C: SUBC "Affects C" -- C = 1 when the subtraction is positive or zero
             * (manual examples: 4 - 1<<15 -> C=0; RPT #15 SUBC 41h/7 -> C=1). */
            if (d >= 0) s->st0 |= ST0_C; else s->st0 &= ~ST0_C;
            if (dst) s->b = sext40(r); else s->a = sext40(r);
            return consumed + s->lk_used;
        }
        default:
            v = (s->st1 & ST1_SXM) ? (int16_t)val : (uint16_t)val;
            break;
        }
        if (dst) s->b = sext40(v); else s->a = sext40(v);
        /* LDU-PTR probe (CALYPSO_DEBUG=LDU-PTR): at the site that loads A for the
         * CALA to 0 (PC=0xfa7e by default, CALYPSO_TRACE_LDU_PC to move it), dumps
         * the effective address, the value read, the indirect flag and the AR/DP,
         * to name the zero cell: an uninitialised table pointer or a wrong EA. */
        {
            static int ldu_trace_pc = -1;
            if (ldu_trace_pc < 0) {
                const char *e = calypso_getenv("CALYPSO_TRACE_LDU_PC");
                ldu_trace_pc = (e && *e) ? (int)strtol(e, NULL, 0) : 0xfa7e;
            }
            if (s->pc == (uint16_t)ldu_trace_pc) {
                C54_DBG("LDU-PTR",
                    "LDU-PTR PC=0x%04x op=0x%04x sub=%d EA=0x%04x val=0x%04x ind=%d "
                    "DP=0x%03x AR0=%04x AR1=%04x AR2=%04x AR3=%04x AR4=%04x "
                    "AR5=%04x AR6=%04x AR7=%04x insn=%u",
                    s->pc, op, sub, addr, val, ind, (s->st0 & 0x1FF),
                    s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                    s->ar[4], s->ar[5], s->ar[6], s->ar[7],
                    (unsigned)s->insn_count);
            }
        }
        /* CALAD-zone LD trace: every LD/LDU/LDR that targets A while
         * executing in DARAM near the CALAD cluster. Reveals what
         * address/value is feeding A right before each CALAD A. */
        if (dst == 0 && (s->pmst & PMST_OVLY) &&
            s->pc >= 0x10b0 && s->pc < 0x1100) {
            static uint64_t ldA_total;
            ldA_total++;
            if (ldA_total <= 60 || (ldA_total % 5000) == 0) {
                C54_LOG("LD-A-TRACE #%llu PC=0x%04x op=0x%04x sub=%d addr=0x%04x val=0x%04x A_after=0x%04x DP=0x%03x",
                        (unsigned long long)ldA_total,
                        s->pc, op, sub, addr, val,
                        (uint16_t)(s->a & 0xFFFF),
                        (s->st0 & 0x1FF));
            }
        }
        return consumed + s->lk_used;
    }

    case 0x0: {
        /* 0xxx: ADD / ADDS / ADD,TS / SUB / SUBS / SUB,TS  (mask FE00):
         *   0x0000 ADD  Smem, SRC1 (no shift, SXM honoured)
         *   0x0200 ADDS Smem, SRC1 (no shift, zero-extended)
         *   0x0400 ADD  Smem, TS, SRC1
         *   0x0800 SUB  Smem, SRC1
         *   0x0A00 SUBS Smem, SRC1
         *   0x0C00 SUB  Smem, TS, SRC1
         * ⚠️ Plain ADD/SUB take no shift; shifting by 16 unconditionally is wrong.
         */
        addr = resolve_smem(s, op, &ind);
        int dst = (op >> 8) & 1;
        int sub = (op >> 9) & 0x07;  /* 0..7 */
        uint16_t val = data_read(s, addr);
        int64_t v;
        bool is_sub = (sub & 0x4) != 0;
        bool is_unsigned = (sub == 1 || sub == 5);  /* ADDS / SUBS */
        bool ts_shift = (sub == 2 || sub == 6);     /* ,TS variants */
        /* sub 3 = ADDC (0x0600) and sub 7 = SUBB (0x0E00) carry the carry; the
         * generic ADD/SUB path drops it. SPRU172C:
         *   ADDC Smem, src : src = src + Smem + C
         *   SUBB Smem, src : src = src - Smem - C
         * binutils: addc 0x0600/0xFE00, subb 0x0E00/0xFE00, one word each.
         * This follows the manual literally (- C); some SUBB implementations
         * subtract the borrow (~C) instead. */
        bool with_carry = (sub == 3 || sub == 7);
        v = is_unsigned ? (uint16_t)val
                        : ((s->st1 & ST1_SXM) ? (int16_t)val : (uint16_t)val);
        if (ts_shift) {
            int8_t ts = (int8_t)((s->t & 0x3F) | ((s->t & 0x20) ? 0xC0 : 0));
            v = (ts >= 0) ? (v << ts) : (v >> -ts);
        }
        {
            /* [2026-09-21] SUBB subtracts the BORROW, the logical inverse of C
             * (SPRU172C example: A=6, C=0, SUBB 6 -> A=-1 ; B=..06, C=1, SUBB 6
             * -> B=..00). isa_test 229/230. ADDC adds C. */
            int64_t c = with_carry ? ((s->st0 & ST0_C) ? 1 : 0) : 0;
            if (is_sub && with_carry) c = 1 - c;
            /* [retenue] SPRU172C ADD/ADDS/ADDC/SUB/SUBS/SUBB "Affects C": carry
             * out of bit 39 (add) / no borrow (sub), carry-in included.
             * isa_test 9, 12. */
            {   int64_t acc0 = dst ? s->b : s->a;
                if (is_sub) c54x_carry_sub40(s, acc0, v + c);
                else        c54x_carry_add40(s, acc0, v + c); }
            if (is_sub) {
                if (dst) s->b = sext40(s->b - v - c);
                else     s->a = sext40(s->a - v - c);
            } else {
                if (dst) s->b = sext40(s->b + v + c);
                else     s->a = sext40(s->a + v + c);
            }
        }
        /* CALAD-zone ADD/SUB trace: same scope as LD-A-TRACE. */
        if (dst == 0 && (s->pmst & PMST_OVLY) &&
            s->pc >= 0x10b0 && s->pc < 0x1100) {
            static uint64_t addA_total;
            addA_total++;
            if (addA_total <= 30 || (addA_total % 5000) == 0) {
                C54_LOG("ADDSUB-A-TRACE #%llu PC=0x%04x op=0x%04x sub=%d addr=0x%04x val=0x%04x A_after=%010llx",
                        (unsigned long long)addA_total,
                        s->pc, op, sub, addr, val,
                        (unsigned long long)(s->a & 0xFFFFFFFFFFULL));
            }
        }
        return consumed + s->lk_used;
    }

    case 0x3:
        {   /* ⚠️ MAC/bit handlers, called BEFORE any resolve_smem: each one does
             * its own, and calling this later would double post-increment the ARs. */
            int _h = c54x_mac_bit_family(s, op, consumed);
            if (_h >= 0) return _h;
        }
        /* 3xxx: MAC / MAS, but SQURA and BITT first. */
        addr = resolve_smem(s, op, &ind);
        {
            uint16_t val = data_read(s, addr);
            /* SQURA Smem,src (0x3800/0xFE00, bit 8 = src): src = src + Smem*Smem.
             * ⚠️ The blind MAC of case 0x3 runs it as `acc += T*Smem`, so the energy
             * (a sum of squares) is computed from the wrong operand and sign and A
             * stays <= 0 at PROM0 0x76ff/0x7700. The RCD LEQ at 0x75e8 then takes
             * the early exit, skipping the body that pushes ST1, and POPM ST1 at
             * 0x7706 over-pops (first orphan pop at insn 146) until SP collapses.
             * SQURA accumulates a SQUARE, a non-negative contribution, so A > 0 and
             * the RCD is not taken. */
            /* ═════════════════════════════════════════════════════════════════
             * FIX_BITT_CASE3 — BITT must be decoded here, in `case 0x3:`.
             *
             * `bitt` is 0x34xx, so hi4 = 3. A handler for it placed in `case 0xF:`
             * is unreachable by construction and 0x348e falls into the blind MAC of
             * this case instead.
             *
             * TI SPRU172C: `BITT Smem` -> `TC = Smem(15 - T(3-0))`, encoding
             * `0011 0100 IAAAAAAA`, "Status Bits: Affects TC" AND NOTHING ELSE. The
             * blind MAC is therefore wrong twice over: TC is never set and stays
             * stale, and accumulator A is corrupted by an accumulation that should
             * not happen.
             *
             * [2026-08-04] Measured: the bit assembler at 0x9ab8..0x9ad2
             * (`bitt *AR6-` -> `roltc A` -> `stl *AR2-,A`) packed mush into
             * 0x2c3c..0x2c47, which the `mvdd` at 0x9723 published into a_cd[3..].
             * The ROLTC-WATCH probe measured incoming TC at 1445/5000 (about 29%),
             * i.e. noise rather than a bit test.
             * ⚠️ A was non-zero and plausible-looking (0x33c7eed1bb...) and still
             * pure MAC mush: a non-zero value is not a correct value.
             *
             * SQURA just below was extracted from the same blind MAC for the same
             * reason.
             * ═════════════════════════════════════════════════════════════════ */
            /* ADD Smem, 16, src [, dst] : dst = src + (Smem << 16).
             * Encoding 0011 11SD IAAAAAAA (binutils {"add", 0x3C00, 0xFC00,
             * {Smem, 16, SRC, DST}}, SPRU172C 4-4). [2026-09-20] Had no handler
             * and fell into the blind MAC below (acc += T*Smem). Sites: SCH
             * decoder 0x9a7d `3d81 add *AR1,16,A,B` (branch metric s0+s1), SB
             * equalizer 0x847f/0x849b/0x837a/0x8390 `3f89 add *AR1-,16,B`. */
            if ((op & 0xFC00) == 0x3C00) {
                int64_t srcv = ((op >> 9) & 1) ? s->b : s->a;
                int64_t r = sext40(srcv + c54x_smem16_shifted(s, val));
                if ((op >> 8) & 1) s->b = r; else s->a = r;
                return consumed + s->lk_used;
            }
            if ((op & 0xFF00) == 0x3400) {
                int bitt_idx = 15 - (s->t & 0xF);
                if ((val >> bitt_idx) & 1) s->st0 |= ST0_TC;
                else                       s->st0 &= ~ST0_TC;
                return consumed + s->lk_used;
            }

            if ((op & 0xFE00) == 0x3800) {
                int64_t sq = (int64_t)(int16_t)val * (int64_t)(int16_t)val;
                if (s->st1 & ST1_FRCT) sq <<= 1;
                int sdst = (op >> 8) & 1;
                if (sdst) s->b = sext40(s->b + sq);
                else      s->a = sext40(s->a + sq);
                /* SPRU172C: "SQURA Smem, src : src = src + Smem * Smem, T = Smem".
                 * ⚠️ Without the T write, every later instruction that uses T (MAC,
                 * LD Smem,TS, ...) works on a stale value. */
                s->t = val;
                return consumed + s->lk_used;
            }
            /* [2026-10-03] SQURS (0x3A00/0xFE00) and POLY (0x3600/0xFF00) had no handler and fell into
             * the blind MAC below (`acc += T*Smem`). SPRU172C:
             *   SQURS Smem,src : T = Smem ; src = src - Smem*Smem
             *   POLY  Smem     : B = Smem << 16 ; A = rnd(A(32-16) * T + B)   (B = the OLD B)
             * The speech-frame code runs SQURS once per TCH/F frame (opcode histogram of the replay).
             * CALYPSO_SQURS_MAC=1 restores the old decoding. */
            /* Correct SQURS = DEFAULT. It feeds the SB (0x7e40 `squrs @0x22,a`, residual noise E - c^2
             * of the SCH correlation; depends on the sign-extended negative saturation of
             * calypso_c54x.c) and, with MVKD/MVDK under RPT, gives a correct speech BFI (replay:
             * 147/2875 instead of 2875/2875; bench: 200/3000, all on really bad frames or frames
             * stolen by the FACCH). CALYPSO_SQURS_MAC=1 forces the MAC decoding. */
            static int squrs_mac = -1;
            if (squrs_mac < 0) { const char *e = getenv("CALYPSO_SQURS_MAC"); squrs_mac = (e && *e == '1') ? 1 : 0; }
            if (!squrs_mac && (op & 0xFE00) == 0x3A00) {
                int64_t sq = (int64_t)(int16_t)val * (int64_t)(int16_t)val;
                if (s->st1 & ST1_FRCT) sq <<= 1;
                if ((op >> 8) & 1) s->b = sext40(s->b - sq);
                else               s->a = sext40(s->a - sq);
                s->t = val;
                return consumed + s->lk_used;
            }
            static int poly_mac = -1;   /* CALYPSO_POLY_MAC=1: MAC decoding of POLY alone */
            if (poly_mac < 0) { const char *e = getenv("CALYPSO_POLY_MAC"); poly_mac = (e && *e == '1') ? 1 : 0; }
            if (!poly_mac && (op & 0xFF00) == 0x3600) {
                int64_t ahi = (sext40(s->a) >> 16) & 0x1FFFF;          /* A(32-16), 17-bit operand */
                if (ahi & 0x10000) ahi -= 0x20000;
                int64_t prod = ahi * (int64_t)(int16_t)s->t;
                if (s->st1 & ST1_FRCT) prod <<= 1;
                int64_t old_b = s->b;
                s->a = sext40(((prod + old_b) + 0x8000) & ~0xFFFFLL);
                s->b = sext40(c54x_smem16_shifted(s, val));
                return consumed + s->lk_used;
            }
            int dst = (op >> 8) & 1;
            int64_t product = (int64_t)(int16_t)s->t * (int64_t)(int16_t)val;
            if (s->st1 & ST1_FRCT) product <<= 1;
            if (dst) s->b = sext40(s->b + product);
            else     s->a = sext40(s->a + product);
        }
        return consumed + s->lk_used;

    case 0x2:
        {   /* ⚠️ MAC/bit handlers, called BEFORE any resolve_smem: each one does
             * its own, and calling this later would double post-increment the ARs. */
            int _h = c54x_mac_bit_family(s, op, consumed);
            if (_h >= 0) return _h;
        }
        /* 2xxx: MPY, SQUR, MAS, MAC variants */
        {
            int sub = (op >> 8) & 0xF;
            addr = resolve_smem(s, op, &ind);
            uint16_t val = data_read(s, addr);
            int64_t product;
            int dst;
            switch (sub) {
            case 0x0: case 0x1: /* MPY Smem, A/B */
                product = (int64_t)(int16_t)s->t * (int64_t)(int16_t)val;
                if (s->st1 & ST1_FRCT) product <<= 1;
                if (sub & 1) s->b = sext40(product);
                else         s->a = sext40(product);
                return consumed + s->lk_used;
            /* binutils: mpyr 0x2200/0xFE00, mpyu 0x2400/0xFE00, squr 0x2600/0xFE00,
             * so `sub = (op >> 8) & 0xF` is 2/3 for MPYR, 4/5 for MPYU and 6/7 for
             * SQUR. ⚠️ Placing SQUR at 4/5 puts it on MPYU and drops 2/3 and 6/7
             * into `default:` (MAS). What matters is T:
             *   MPYU (SPRU172C): dst = uns(T)*uns(Smem), T UNCHANGED -- writing
             *        `s->t = val` there is a stray write
             *   SQUR: dst = Smem*Smem AND T = Smem
             *   MPYR: dst = rnd(T*Smem), an assignment, T unchanged -- not an
             *        accumulate-and-subtract like MAS
             * [2026-08-23] MPYR has two sites, 0x8166 and 0x816c, inside the very
             * routine that loads T for the MPY at 0x81e4. */
            case 0x2: case 0x3: /* MPYR Smem, dst : dst = rnd(T * Smem) */
                product = (int64_t)(int16_t)s->t * (int64_t)(int16_t)val;
                if (s->st1 & ST1_FRCT) product <<= 1;
                /* @BEQUILLE [2026-09-21] CALYPSO_HACK_SOFT_SCALE=<k> : diagnostic only.
                 * The NB soft-bit scaler (0x8166/0x816c/0x8175) multiplies the
                 * equaliser output by a scale T that comes out as 2 (or 0) on the
                 * synthetic cell, so rnd(T*soft)>>13 is 0..3 and the 129-entry
                 * quantiser table is indexed at 0: every soft bit is stored +1.
                 * Shifting the product by k here says whether a k-bit larger
                 * scale is all that separates the decoder from the SI. */
                { static int hk = -1; if (hk < 0) { const char *e = getenv("CALYPSO_HACK_SOFT_SCALE"); hk = (e && *e) ? atoi(e) : 0; }
                  if (hk && (s->pc == 0x8166 || s->pc == 0x816c || s->pc == 0x8175)) product <<= hk; }
                product = (product + 0x8000) & ~0xFFFFLL;   /* [2026-09-21] rnd(): +2^15 then bits 15-0 cleared (isa_test 132: MPYR 0,B -> 0x6260000) */
                if (sub & 1) s->b = sext40(product);
                else         s->a = sext40(product);
                return consumed + s->lk_used;      /* T UNCHANGED */
            case 0x4: case 0x5: /* MPYU Smem, dst : dst = uns(T) * uns(Smem) */
                product = (int64_t)(uint16_t)s->t * (int64_t)(uint16_t)val;
                if (s->st1 & ST1_FRCT) product <<= 1;
                if (sub & 1) s->b = sext40(product);
                else         s->a = sext40(product);
                return consumed + s->lk_used;      /* T UNCHANGED */
            case 0x6: case 0x7: /* SQUR Smem, dst : dst = Smem*Smem ; T = Smem */
                product = (int64_t)(int16_t)val * (int64_t)(int16_t)val;
                if (s->st1 & ST1_FRCT) product <<= 1;
                s->t = val;
                if (sub & 1) s->b = sext40(product);
                else         s->a = sext40(product);
                return consumed + s->lk_used;
            case 0x8: case 0x9: /* MPYA Smem (A = T * Smem, B += A) or variants */
                product = (int64_t)(int16_t)s->t * (int64_t)(int16_t)val;
                if (s->st1 & ST1_FRCT) product <<= 1;
                if (sub & 1) { s->a += s->b; s->b = sext40(product); }
                else         { s->b += s->a; s->a = sext40(product); }
                return consumed + s->lk_used;
            case 0xA: case 0xB: /* MACA[R] Smem, A/B (A += B * Smem then B = T * Smem) */
                dst = sub & 1;
                product = (int64_t)(int16_t)s->t * (int64_t)(int16_t)val;
                if (s->st1 & ST1_FRCT) product <<= 1;
                if (dst) { s->a = sext40(s->a + s->b); s->b = sext40(product); }
                else     { s->b = sext40(s->b + s->a); s->a = sext40(product); }
                s->t = val;
                return consumed + s->lk_used;
            default:
                /* MAS variants and others */
                product = (int64_t)(int16_t)s->t * (int64_t)(int16_t)val;
                if (s->st1 & ST1_FRCT) product <<= 1;
                dst = sub & 1;
                if (dst) s->b = sext40(s->b - product);
                else     s->a = sext40(s->a - product);
                return consumed + s->lk_used;
            }
        }

    case 0x4:
        /* 0x4xxx group — per binutils tic54x-opc.c:
         *   0x40-0x43  SUB Smem,16,src[,dst]    (mask 0xFC00)
         *   0x44-0x45  LD  Smem,16,dst          (mask 0xFE00)
         *   0x4600     LD  Smem,DP              (mask 0xFF00)
         *   0x4700     RPT Smem                 (mask 0xFF00)
         *   0x48-0x49  LDM MMR,dst              (mask 0xFE00)
         *   0x4A00     PSHM MMR                 (mask 0xFF00)
         *   0x4B00     PSHD Smem                (mask 0xFF00)
         *   0x4C00     LTD Smem                 (mask 0xFF00)
         *   0x4D00     DELAY Smem               (mask 0xFF00)
         *   0x4E-0x4F  DST src,Lmem             (mask 0xFE00) */
        {
            uint8_t op8 = hi8;            /* (op >> 8) & 0xFF */
            int dst_b = op8 & 0x01;        /* bit8 = src/dst select (A=0, B=1) */
            int64_t *acc_dst = dst_b ? &s->b : &s->a;

            if (op8 >= 0x40 && op8 <= 0x43) {
                /* SUB Smem, 16, src [, dst] : dst = src - (Smem << 16).
                 * Encoding 0100 00SD IAAAAAAA: bit 9 = src, bit 8 = dst (binutils
                 * {"sub", 0x4000, 0xFC00, {Smem, 16, SRC, DST}}, SPRU172C 4-187).
                 * [2026-09-20] src was ignored (dst used as src): the SCH decoder's
                 * `4191 sub *AR1+,16,A,B` at 0x9a7f computed B - s1 instead of
                 * A - s1, so the second branch metric (s0-s1) was garbage. */
                addr = resolve_smem(s, op, &ind);
                int64_t val = c54x_smem16_shifted(s, data_read(s, addr));
                int64_t srcv = ((op >> 9) & 1) ? s->b : s->a;
                *acc_dst = sext40(srcv - val);
                return consumed + s->lk_used;
            }
            if (op8 == 0x44 || op8 == 0x45) {
                /* LD Smem << 16, dst */
                addr = resolve_smem(s, op, &ind);
                int64_t val = c54x_smem16_shifted(s, data_read(s, addr));
                *acc_dst = sext40(val);
                return consumed + s->lk_used;
            }
            if (op8 == 0x46) {
                /* LD Smem, DP — load DP from low 9 bits of Smem */
                addr = resolve_smem(s, op, &ind);
                uint16_t val = data_read(s, addr);
                s->st0 = (s->st0 & ~ST0_DP_MASK) | (val & ST0_DP_MASK);
                g_last_ldp_pc = s->pc; g_last_ldp_val = (val & ST0_DP_MASK); g_last_ldp_kind = 3;
                return consumed + s->lk_used;
            }
            if (op8 == 0x47) {
                /* RPT Smem loads the SINGLE repeat counter (RC), not the block
                 * counter (BRC). SPRU172C: "RPT Smem: Repeat single, RC = Smem";
                 * binutils: rpt 0x4700/0xFF00, one word.
                 * ⚠️ Writing s->brc instead leaves rpt_count at its previous value,
                 * so the RPT has no effect at all, and corrupts BRC on the way.
                 * Like the RPT #k8u handler (0xEC00): advance PC and return 0 so
                 * the dispatcher re-executes the NEXT instruction, not the RPT. */
                addr = resolve_smem(s, op, &ind);
                uint16_t val = data_read(s, addr);
                s->rpt_count = val;
                s->rpt_active = true; s->rpt_fresh = true;
                /* + lk_used: with an absolute Smem (47f8 = RPT *(lk)) the
                 * instruction is TWO words. A bare `pc += 1` would execute the lk
                 * address as the repeated instruction: PROM0 0x724a `47f8 3fd8`
                 * (RPT *(0x3fd8), then NOP) -- in the tail of the frame IRQ, the
                 * interrupted context already restored -- would execute 0x3fd8
                 * (`*AR0+0%`): AR0 1 -> 2, saved again by the 0x013b prologue and
                 * handed back to the RPTB loop 0xe93e..0xe957 (circular buffer
                 * `*AR5+0%`, BK=16). AR5 would then drift linearly, the next calls
                 * of 0xe931 (BRC 0x77) overwrite data[0x2d0d] (pointer set at
                 * 0x2d0e by 0xe68a), AR5 = 0 + 8 at the CALLD 0xea0c, and 0xeac9
                 * `MVDD *AR2+,*AR5+` under RPT #16 write SP (SP-CORRUPT at
                 * pc=0xeac9 on every TCH call). */
                s->pc += 1 + s->lk_used;
                return 0;
            }
            if (op8 == 0x48 || op8 == 0x49) {
                /* LDM MMR, dst — load accumulator from a memory-mapped reg */
                int mmr = op & 0x7F;
                uint16_t val = data_read(s, mmr);
                /* SPRU172C LDM : (MMR) -> dst(15-0), 0 -> dst(39-16) : zero-extended, whatever SXM says. */
                *acc_dst = c54x_logic40() ? sext40((int16_t)val) : (int64_t)(uint16_t)val;
                return consumed + s->lk_used;
            }
            if (op8 == 0x4A) {
                /* PSHM MMR — push memory-mapped reg onto stack */
                int mmr = op & 0x7F;
                uint16_t val = data_read(s, mmr);
                if (mmr == MMR_ST0) st0_ring_rec(s, val, 'P'); /* push ST0 (C-sweep) */
                s->sp = (s->sp - 1) & 0xFFFF;
                data_write(s, s->sp, val);
                return consumed + s->lk_used;
            }
            if (op8 == 0x4B) {
                /* PSHD Smem — push data memory onto stack */
                addr = resolve_smem(s, op, &ind);
                uint16_t val = data_read(s, addr);
                s->sp = (s->sp - 1) & 0xFFFF;
                data_write(s, s->sp, val);
                return consumed + s->lk_used;
            }
            if (op8 == 0x4C) {
                /* LTD Smem — T = mem[Smem]; mem[Smem+1] = mem[Smem] */
                addr = resolve_smem(s, op, &ind);
                uint16_t val = data_read(s, addr);
                s->t = val;
                data_write(s, (addr + 1) & 0xFFFF, val);
                return consumed + s->lk_used;
            }
            if (op8 == 0x4D) {
                /* DELAY Smem — mem[Smem+1] = mem[Smem] (delay-line shift) */
                addr = resolve_smem(s, op, &ind);
                uint16_t val = data_read(s, addr);
                data_write(s, (addr + 1) & 0xFFFF, val);
                return consumed + s->lk_used;
            }
            if (op8 == 0x4E || op8 == 0x4F) {
                /* DST src, Lmem — store accumulator to long memory.
                 * Lmem = even-aligned 32-bit pair: mem[L]=high, mem[L+1]=low */
                addr = resolve_smem(s, op, &ind) & 0xFFFE;
                int64_t v = *acc_dst;
                data_write(s, addr,         (uint16_t)((v >> 16) & 0xFFFF));
                data_write(s, (addr+1)&0xFFFF, (uint16_t)(v & 0xFFFF));
                return consumed + s->lk_used;
            }
        }
        return consumed + s->lk_used;

    case 0x5:
        /* 5xxx: shifts — SFTA, SFTL and the dual long-word family.
         * ⚠️ 0x56xx/0x57xx are not MVPD (which is 0x8Cxx): decoding them as MVPD
         * makes resolve_smem write MMR_SP and corrupts the stack. */
        {
            /* Dual long-word DADST/DSADT Lmem,dst (1 word). SPRU172C encoding:
             * 0101 101D = DADST (0x5A/5B), 0101 111D = DSADT (0x5E/5F); D (bit 8)
             * is dst (0=A, 1=B); bits 7:0 are the Lmem operand. Semantics, checked
             * against the worked examples in SPRU172C:
             *   C16=1 (dual-16, unsaturated):
             *     DADST: dst(39-16)=Lmem.hi+T ; dst(15-0)=Lmem.lo-T
             *     DSADT: dst(39-16)=Lmem.hi-T ; dst(15-0)=Lmem.lo+T
             *   C16=0 (double precision, SXM):
             *     DADST: dst=Lmem+((T<<16)|T) ; DSADT: dst=Lmem-((T<<16)|T)
             * Lmem post-modification is +-2 (long operand, via resolve_lmem), and
             * C16 is read at execution time from ST1, tracked by SSBX/RSBX C16.
             * ⚠️ Without this handler these opcodes fall into the SFTA/SFTL block
             * below, which flattens the FCCH correlator into "dst >>= ASM" and
             * leaves d_fb_det at 0 (SHADOW-DADST probe: shiftLike=1, walked=0). */
            uint8_t dl_hi = (op >> 8) & 0xFF;
            if (dl_hi >= 0x50 && dl_hi <= 0x5F) {
                /* Full dual long-word family (SPRU172C):
                 *   0x50-53 DADD(00SD)  0x54-55 DSUB(010S)  0x56-57 DLD(011D)
                 *   0x58-59 DRSUB(100S) 0x5A-5B DADST(101D) 0x5C-5D DSUBT(110D)
                 *   0x5E-5F DSADT(111D)
                 * 32-bit Lmem via resolve_lmem (post-mod +-2), branching on
                 * ST1.C16. Checked bit by bit against the SPRU172C worked examples
                 * for DADD, DADST and DSADT. */
                uint16_t laddr  = resolve_lmem(s, op);
                uint16_t lhi    = data_read(s, laddr);
                /* Lmem: high word AT the address, low word at address ^ 1 (SPRU172C
                 * DADD example, AR3 = 0101h: hi = data[0101h], lo = data[0100h]). */
                uint16_t llo    = data_read(s, (uint16_t)(laddr ^ 1));
                int      c16    = (s->st1 & ST1_C16) != 0;
                int      sxm    = (s->st1 & ST1_SXM) != 0;
                int16_t  lhi16  = (int16_t)lhi, llo16 = (int16_t)llo;
                uint32_t lmem32 = ((uint32_t)lhi << 16) | llo;
                int64_t  lmem40 = sxm ? (int64_t)(int32_t)lmem32 : (int64_t)(uint32_t)lmem32;
                /* [retenue] SPRU172C: the whole family except DLD "Affects C".
                 * C16=0: 40-bit carry/borrow. C16=1: taken from the HIGH-part
                 * operation (add: carry out of bit 15, sub: no borrow).
                 * isa_test 51 (DRSUB, C16=1, both halves borrow -> C=0). */
#define DL_C16_ADD(x, y) do { if ((((uint32_t)(uint16_t)(x) + (uint16_t)(y)) >> 16) & 1) \
                                   s->st0 |= ST0_C; else s->st0 &= ~ST0_C; } while (0)
#define DL_C16_SUB(x, y) do { if ((uint16_t)(x) >= (uint16_t)(y)) \
                                   s->st0 |= ST0_C; else s->st0 &= ~ST0_C; } while (0)
                if (dl_hi <= 0x53) {                 /* DADD Lmem,src[,dst] : dst = src + Lmem */
                    int64_t *src = ((op >> 9) & 1) ? &s->b : &s->a;
                    int64_t *dst = ((op >> 8) & 1) ? &s->b : &s->a;
                    if (!c16) c54x_carry_add40(s, *src, lmem40);
                    else      DL_C16_ADD(*src >> 16, lhi);
                    if (!c16) *dst = sext40(*src + lmem40);
                    else { int32_t hi = (int32_t)(int16_t)((*src >> 16) & 0xFFFF) + lhi16;
                           int32_t lo = (int32_t)(int16_t)(*src & 0xFFFF) + llo16;
                           *dst = sext40(((int64_t)hi << 16) | ((uint32_t)lo & 0xFFFF)); }
                } else if (dl_hi <= 0x55) {          /* DSUB Lmem,src : src = src - Lmem */
                    int64_t *src = ((op >> 8) & 1) ? &s->b : &s->a;
                    if (!c16) c54x_carry_sub40(s, *src, lmem40);
                    else      DL_C16_SUB(*src >> 16, lhi);
                    if (!c16) *src = sext40(*src - lmem40);
                    else { int32_t hi = (int32_t)(int16_t)((*src >> 16) & 0xFFFF) - lhi16;
                           int32_t lo = (int32_t)(int16_t)(*src & 0xFFFF) - llo16;
                           *src = sext40(((int64_t)hi << 16) | ((uint32_t)lo & 0xFFFF)); }
                } else if (dl_hi <= 0x57) {          /* DLD Lmem,dst : dst = Lmem */
                    int64_t *dst = ((op >> 8) & 1) ? &s->b : &s->a;
                    if (!c16) *dst = sext40(lmem40);
                    else *dst = sext40(((int64_t)lhi16 << 16) | (uint16_t)llo);
                } else if (dl_hi <= 0x59) {          /* DRSUB Lmem,src : src = Lmem - src */
                    int64_t *src = ((op >> 8) & 1) ? &s->b : &s->a;
                    if (!c16) c54x_carry_sub40(s, lmem40, *src);
                    else      DL_C16_SUB(lhi, *src >> 16);
                    if (!c16) *src = sext40(lmem40 - *src);
                    else { int32_t hi = lhi16 - (int32_t)(int16_t)((*src >> 16) & 0xFFFF);
                           int32_t lo = llo16 - (int32_t)(int16_t)(*src & 0xFFFF);
                           *src = sext40(((int64_t)hi << 16) | ((uint32_t)lo & 0xFFFF)); }
                } else {                              /* DADST/DSUBT/DSADT Lmem,dst : use T */
                    int64_t *dst = ((op >> 8) & 1) ? &s->b : &s->a;
                    int16_t t16 = (int16_t)s->t;
                    int64_t r;
                    if (c16) {
                        int sgn_hi = (dl_hi <= 0x5B) ? +1 : -1;   /* DADST hi+T; DSUBT/DSADT hi-T */
                        int sgn_lo = (dl_hi <= 0x5D) ? -1 : +1;   /* DADST/DSUBT lo-T; DSADT lo+T */
                        int32_t hi = (int32_t)lhi16 + sgn_hi * (int32_t)t16;
                        int32_t lo = (int32_t)llo16 + sgn_lo * (int32_t)t16;
                        r = ((int64_t)hi << 16) | ((uint32_t)lo & 0xFFFF);
                        if (sgn_hi > 0) DL_C16_ADD(lhi, t16); else DL_C16_SUB(lhi, t16);
                    } else {
                        uint32_t tt32 = ((uint32_t)(uint16_t)t16 << 16) | (uint16_t)t16;
                        int64_t tt40 = sxm ? (int64_t)(int32_t)tt32 : (int64_t)(uint32_t)tt32;
                        r = (dl_hi <= 0x5B) ? (lmem40 + tt40) : (lmem40 - tt40); /* DADST adds, the others subtract */
                        if (dl_hi <= 0x5B) c54x_carry_add40(s, lmem40, tt40);
                        else               c54x_carry_sub40(s, lmem40, tt40);
                    }
                    *dst = sext40(r);
                }
#undef DL_C16_ADD
#undef DL_C16_SUB
                return consumed + s->lk_used;
            }
            int dst = (op >> 8) & 1;
            int64_t *acc = dst ? &s->b : &s->a;
            int sub = (op >> 9) & 0x7;
            if (sub <= 1) {
                /* 50xx/51xx: SFTA src, ASM shift */
                int shift = asm_shift(s);
                if (shift >= 0) *acc = sext40(*acc << shift);
                else            *acc = sext40(*acc >> (-shift));
            } else if (sub == 2 || sub == 3) {
                /* 54xx/55xx: SFTA src, #shift (immediate in Smem) */
                addr = resolve_smem(s, op, &ind);
                int shift = (int16_t)data_read(s, addr);
                if (shift >= 0) *acc = sext40(*acc << shift);
                else            *acc = sext40(*acc >> (-shift));
            } else if (sub == 4 || sub == 5) {
                /* 58xx/59xx: SFTL src, ASM shift (logical) */
                int shift = asm_shift(s);
                uint64_t u = (uint64_t)(*acc) & 0xFFFFFFFFFFULL;
                if (shift >= 0) *acc = sext40((int64_t)(u << shift));
                else            *acc = sext40((int64_t)(u >> (-shift)));
            } else if (sub == 6 || sub == 7) {
                /* 5Cxx/5Dxx/5Exx/5Fxx: SFTL with Smem or other */
                addr = resolve_smem(s, op, &ind);
                int shift = (int16_t)data_read(s, addr);
                uint64_t u = (uint64_t)(*acc) & 0xFFFFFFFFFFULL;
                if (shift >= 0) *acc = sext40((int64_t)(u << shift));
                else            *acc = sext40((int64_t)(u >> (-shift)));
            }
        }
        return consumed + s->lk_used;

    case 0x8: case 0x9:
        /* 8xxx/9xxx: Memory moves, PORTR/PORTW */

        /* FIX_ADDSUB_XSHFT — per binutils and SPRU172C 4-4 / 4-187, 0x9000
         * (mask FE00) is ADD Xmem,SHFT,src (src += Xmem << SHFT) and 0x9200 is
         * SUB Xmem,SHFT,src (src -= Xmem << SHFT), with bit 8 = src, SHFT in bits
         * 3:0 and SXM applied.
         * ⚠️ Run as MAC Xmem,Ymem instead they corrupt the SB path, which has such
         * a site at 0x988e (`9086`). */
        if ((op & 0xFC00) == 0x9000) {
            uint16_t xa = resolve_xmem(s, op);
            uint16_t xv = data_read(s, xa);
            int shft = op & 0xF;
            int64_t v = ((s->st1 & ST1_SXM) ? (int64_t)(int16_t)xv : (int64_t)xv) << shft;
            int64_t *srcp = (op & 0x0100) ? &s->b : &s->a;
            /* [retenue] SPRU172C: ADD/SUB Xmem,SHFT affect C. isa_test 5. */
            if (op & 0x0200) c54x_carry_sub40(s, *srcp, v);
            else             c54x_carry_add40(s, *srcp, v);
            *srcp = sext40((op & 0x0200) ? (*srcp - v) : (*srcp + v));
            return consumed + s->lk_used;
        }

        /* 94xx: MVDK Smem, dmad — Move data(Smem) to data(dmad) (2 words) */
        if (hi8 == 0x94) {
            addr = resolve_smem(s, op, &ind);
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            data_write(s, op2, data_read(s, addr));
            return consumed + s->lk_used;
        }
        /* 95xx: MVKD dmad, Smem — Move data(dmad) to data(Smem) (2 words) */
        if (hi8 == 0x95) {
            addr = resolve_smem(s, op, &ind);
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            data_write(s, addr, data_read(s, op2));
            return consumed + s->lk_used;
        }
        /* 96xx: MVDP Smem, pmad — Move data to program (2 words) */
        if (hi8 == 0x96) {
            addr = resolve_smem(s, op, &ind);
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            {   uint16_t _mv = data_read(s, addr);
                scratchwr_note(s, (uint16_t)op2, _mv, "prog");
                s->prog[op2] = _mv; }
            return consumed + s->lk_used;
        }

        /* Per binutils tic54x-opc.c:
         *   { "stl", 1,3,3, 0x9800, 0xFE00, {OP_SRC1,OP_SHFT,OP_Xmem} }
         *   { "sth", 1,3,3, 0x9A00, 0xFE00, {OP_SRC1,OP_SHFT,OP_Xmem} }
         * so 0x98/0x99 write the LOW half and 0x9A/0x9B the HIGH half, with bit 8
         * selecting A or B. ⚠️ Swapping them makes every shifted STL/STH in the
         * firmware write the wrong half of the accumulator, and the pattern is hot
         * in post-MAC scaling code.
         * The SHFT field is not decoded here; the dedicated FIX_STL_STH_SHFT
         * handler higher up does that. */
        if (hi8 == 0x98 || hi8 == 0x99) {
            /* STL src, SHFT, Xmem — store LOW (acc & 0xFFFF).
             * ⚠️ The operand is Xmem and must go through resolve_xmem: resolve_smem
             * maps a low byte of 0x00-0x1F with bit 7 = 0 into MMR space and
             * clobbers SP/IMR/IFR. Observed at PC=0x8a46 op=0x9918, which stomped
             * SP to 0. */
            addr = resolve_xmem(s, op);
            int src = hi8 & 1;
            int64_t acc = src ? s->b : s->a;
            data_write(s, addr, (uint16_t)(acc & 0xFFFF));
            return consumed + s->lk_used;
        }
        if (hi8 == 0x9A || hi8 == 0x9B) {
            /* STH src, SHFT, Xmem — store HIGH (acc >> 16). Xmem through
             * resolve_xmem, as for STL above. */
            addr = resolve_xmem(s, op);
            int src = hi8 & 1;
            int64_t acc = src ? s->b : s->a;
            data_write(s, addr, (uint16_t)((acc >> 16) & 0xFFFF));
            return consumed + s->lk_used;
        }

        /* 0x9C-0x9F range: SACCD/SRCCD/STRCD — conditional stores */

        /* FIX_XCCD — per SPRU172C 4-152/4-165/4-186: 0x9C = STRCD Xmem,cond
         * (Xmem = T), 0x9D = SRCCD Xmem,cond (Xmem = BRC), 0x9E/0x9F = SACCD
         * src,Xmem,cond with src in bit 8, not bit 9. The 4-bit cond field has
         * bit 3 = accumulator tested (0 = A, 1 = B) and bits 2:0 = 101 EQ, 100 NEQ,
         * 110 GT, 010 GEQ, 011 LT, 111 LEQ.
         * ⚠️ Treating the whole 0x9Cxx range as SACCD tests src instead of the
         * condition's accumulator and reads the condition code backwards. SB sites:
         * 0x847e and 0x849a `9e1e` (SACCD A), 0x831f `9f06` (SACCD B). */
        if ((op & 0xFC00) == 0x9C00) {
            uint16_t xaddr = resolve_xmem(s, op);
            int cond = op & 0x0F;
            int64_t ca = sext40((cond & 0x8) ? s->b : s->a);
            int take;
            switch (cond & 0x7) {
            case 0x5: take = (ca == 0); break;
            case 0x4: take = (ca != 0); break;
            case 0x6: take = (ca > 0);  break;
            case 0x2: take = (ca >= 0); break;
            case 0x3: take = (ca < 0);  break;
            case 0x7: take = (ca <= 0); break;
            default:  take = 0; break;
            }
            uint16_t val;
            if (!take)                        val = data_read(s, xaddr);
            else if (hi8 == 0x9C)             val = s->t;
            else if (hi8 == 0x9D)             val = s->brc;
            else {
                int64_t src = (op & 0x0100) ? s->b : s->a;
                int ash = asm_shift(s);
                int64_t sh = (ash >= 0) ? (src << ash) : (src >> (-ash));
                val = (uint16_t)((sh >> 16) & 0xFFFF);
            }
            data_write(s, xaddr, val);
            return consumed + s->lk_used;
        }
        /* POPM MMR — pop top-of-stack into MMR (1-word).
         * Per tic54x-opc.c: { "popm", 0x8A00, 0xFF00, {OP_MMR} }.
         * Per SPRU172C section 4: the value at SP is popped into the MMR, SP++.
         *
         * ⚠️ 0x8Axx is POPM, not MVDK Smem,dmad (which is 0x7100 mask 0xFF00).
         * Decoded as MVDK, the firmware's symmetric PSHM/POPM pattern (PROM0
         * 0x7013..0x7023 saves and restores 6 MMRs around a CALA) never restores
         * anything after the CALA, ST1 is never restored, INTM stays at 1 forever
         * and IRQ vectoring stops. */
        if ((op & 0xFF00) == 0x8A00) {
            uint16_t mmr = op & 0x7F;
            uint16_t val = data_read(s, s->sp);
            s->sp = (s->sp + 1) & 0xFFFF;
            /* POPM-ST1 probe (CALYPSO_DEBUG=POPM-ST1): ST1 is MMR 0x07. Tells
             * "POPM ST1 never executed" from "executed, but the popped value
             * already has INTM=1", which restores 1 and never clears it. Silent by
             * default. */
            if (mmr == 0x07) {
                C54_DBG("POPM-ST1",
                        "POPM ST1 val=0x%04x INTM_bit=%u PC=0x%04x SP=0x%04x insn=%u",
                        val, !!(val & ST1_INTM), s->pc, s->sp, s->insn_count);
            }
            data_write(s, mmr, val);
            return consumed + s->lk_used;
        }
        /* 0x88xx-0x89xx: STLM src, MMR  (1-word!)
         * Per tic54x-opc.c: { "stlm", 1,2,2, 0x8800, 0xFE00, ... }
         *   bits 9-15 = fixed (0x44)
         *   bit 8     = src (0 = A, 1 = B)
         *   bits 0-6  = MMR address (0x00..0x7F)
         *
         * ⚠️ Critical for the DSP bootloader at PROM0 0xb42d (`STLM B, AR1`):
         * decoded as a 2-word MVDM, the emulator eats the next opcode
         * (0xb42e = 0xf84c, a BC), then enters 0xb431 (MACR family) with an
         * uninitialised T, producing A=0x10, which the BACC A at 0xb430 uses as
         * its jump target and drops the DSP into the boot-stub NOPs at PC=0x0010
         * instead of continuing the bootloader handshake. */
        if (hi8 == 0x88 || hi8 == 0x89) {
            int src = (op >> 8) & 1;  /* 0 = A, 1 = B */
            int mmr = resolve_mmr(s, op);   /* direct or *ARx / *ARx- / *ARx+ */
            uint16_t val = src ? (uint16_t)(s->b & 0xFFFF)
                               : (uint16_t)(s->a & 0xFFFF);
            data_write(s, (uint16_t)mmr, val);  /* MMRs alias addr 0x00..0x1F */
            return consumed + s->lk_used;
        }
        /* 0x8Bxx: POPD Smem — pop top-of-stack into data memory, the mirror of
         * PSHD (0x4B): PSHM/POPM are 0x4A/0x8A, PSHD/POPD are 0x4B/0x8B.
         * ⚠️ Missing, it falls through as a 1-word NOP. The overlay frame handler
         * at 0x013b does `POPD *(0x3fcd)` (pops the return of CALL 0x013b), 24
         * PSHM, `PSHD *(0x3fcd)` (pushes the return back), RET. Without POPD the
         * return is buried under the saves, data[0x3fcd] is 0 and RET goes to 0.
         * resolve_smem sets lk_used for the absolute mode (0xf8), giving the
         * correct 2-word length. */
        if (hi8 == 0x8B) {
            addr = resolve_smem(s, op, &ind);
            uint16_t val = data_read(s, s->sp);
            s->sp = (s->sp + 1) & 0xFFFF;
            data_write(s, addr, val);
            return consumed + s->lk_used;
        }
        if (hi8 == 0x80) {
            /* Per binutils tic54x-opc.c:
             *   { "stl", 1,2,2, 0x8000, 0xFE00, {OP_SRC1,OP_Smem}, 0, REST }
             * 0x80xx/0x81xx = STL src,Smem (1 word, no shift), bit 8 = src, so
             * 0x8000-0x80FF is STL A,Smem. ⚠️ Stubbing this as a NOP silently drops
             * every STL A in the firmware and leaves the DARAM variables it should
             * write at stale values. */
            addr = resolve_smem(s, op, &ind);
            data_write(s, addr, (uint16_t)(s->a & 0xFFFF));
            return consumed + s->lk_used;
        }
        if (hi8 == 0x8C) {
            /* Per binutils tic54x-opc.c:
             *   { "mvpd", 2,2,2, 0x7C00, 0xFF00, {OP_pmad,OP_Smem}, 0, REST }
             *   { "st",   1,2,2, 0x8C00, 0xFF00, {OP_T,OP_Smem},    0, REST }
             * MVPD is 0x7C; 0x8C is ST T,Smem (1 word, store T to data memory),
             * the pattern used after a MAC to persist T. Run traces show zero
             * 0x7Cxx sites, the PROM0 overlay being done by the DSP bootloader
             * rather than by MVPD. */
            addr = resolve_smem(s, op, &ind);
            data_write(s, addr, s->t);
            return consumed + s->lk_used;
        }
        /* 0x8E/0x8F : CMPS src, Smem — Compare, Select & Store Maximum
         * (SPRU172C p.4-35). Opcode 1000 111 S I AAAAAAA = 0x8E00/0xFE00,
         * bit8 = src (0=A, 1=B). ONE word (+1 when Smem is long-offset/absolute
         * -> lk_used).
         *   if src(31–16) > src(15–0):  src(31–16)→Smem ; TRN<<=1,TRN(0)=0 ; TC=0
         *   else:                       src(15–0)→Smem  ; TRN<<=1,TRN(0)=1 ; TC=1
         * Compares the two 16-bit two's-complement halves of the accumulator,
         * stores the larger, and records the winner in TRN and TC. Core of the
         * FCCH peak search (Viterbi).
         *
         * ⚠️ 0x8E/0x8F are not MVDP (0x7D) or PORTR (0x74). Decoded as 2-word
         * instructions, each consecutive CMPS A / CMPS B pair (op=8e94 op2=8f93 in
         * the FB-det region 0xa0xx) has its second CMPS swallowed as a phantom
         * pmad, which desynchronises the correlator and leaves d_fb_det unarmed.
         * The I/Q data arrives by DARAM DMA (data[0x2a00]), never through a PORTR
         * opcode. */
        if (hi8 == 0x8E || hi8 == 0x8F) {
            addr = resolve_smem(s, op, &ind);
            int src = (op >> 8) & 1;
            int64_t acc = src ? s->b : s->a;
            int16_t hi = (int16_t)((acc >> 16) & 0xFFFF);
            int16_t lo = (int16_t)(acc & 0xFFFF);
            s->trn = (uint16_t)(s->trn << 1);
            if (hi > lo) {
                data_write(s, addr, (uint16_t)hi);
                s->trn &= ~0x0001u;
                s->st0 &= ~ST0_TC;
            } else {
                data_write(s, addr, (uint16_t)lo);
                s->trn |= 0x0001u;
                s->st0 |= ST0_TC;
            }
            return consumed + s->lk_used;
        }
        if (hi8 == 0x9F) {
            /* PORTW Smem, PA — write I/O port */
            addr = resolve_smem(s, op, &ind);
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            /* Log I/O port writes */
            {
                uint16_t wval = data_read(s, addr);
                static int portw_log = 0;
                if (portw_log < 30) {
                    C54_LOG("PORTW PA=0x%04x val=0x%04x PC=0x%04x", op2, wval, s->pc);
                    portw_log++;
                }
            }
            return consumed + s->lk_used;
        }
        /* 85xx: MVPD pmad, Smem (prog→data, different encoding) */
        if (hi8 == 0x85) {
            addr = resolve_smem(s, op, &ind);
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            data_write(s, addr, prog_read(s, op2));
            return consumed + s->lk_used;
        }
        /* 0x86/0x87: STH src,ASM,Smem — store the HIGH half (acc >> 16) shifted
         * by ASM into Smem, ONE word, bit 8 = src (0=A, 1=B), mask 0xFE00.
         * Exact mirror of the 0x84 handler (STL A,ASM,Smem) but storing the high
         * word. resolve_smem applies the indirect Smem post-increment, so do NOT
         * modify the AR again here.
         *
         * ⚠️ These are not MVDM (0x72) or MVMD (0x73). Decoded as 2-word
         * MVDM/MVMD they eat the following opcode and never touch the Smem AR, so
         * AR3 stays frozen at 0 in the FB correlator loop: at 0x7e71 op=0x8693
         * (STH A,ASM,*AR3+) AR3 must post-increment to sweep the I/Q buffer at
         * 0x2a00+. */
        if (hi8 == 0x86 || hi8 == 0x87) {
            addr = resolve_smem(s, op, &ind);
            int shift = asm_shift(s);
            int src = hi8 & 1;            /* 0x86→A, 0x87→B */
            int64_t v = src ? s->b : s->a;
            if (shift >= 0) v <<= shift; else v >>= (-shift);
            data_write(s, addr, (uint16_t)((v >> 16) & 0xFFFF));  /* STH = high word */
            return consumed + s->lk_used;
        }
        /* Store family, per tic54x-opc.c and SPRU172C, bit 8 = src (0=A, 1=B):
         *   stl 0x8000 / 0xFE00 -> 0x80..0x81 STL src,Smem       (no shift)
         *   sth 0x8200 / 0xFE00 -> 0x82..0x83 STH src,Smem       (no shift)
         *   stl 0x8400 / 0xFE00 -> 0x84..0x85 STL src,ASM,Smem   (ASM shift)
         *   sth 0x8600 / 0xFE00 -> 0x86..0x87 STH src,ASM,Smem   (ASM shift)
         * The ASM variants go through asm_shift(), which follows the manual
         * (ASM = ST1[4:0], signed, -16 <= ASM <= 15).
         * ⚠️ The no-shift variants must NOT call asm_shift, and 0x81 stores B, not
         * A. Getting either wrong makes every STL B / STH through indirect *ARn
         * write the wrong value, including d_burst_d (0x0829/0x083D) and d_task_d
         * (0x0828/0x083C) of the NDB, which the ARM then reports as "EMPTY" and
         * mismatched burst ids in prim_rx_nb.c::l1s_nb_resp. */

        /* 0x81xx: STL B, Smem  (src=B, no shift) */
        if (hi8 == 0x81) {
            addr = resolve_smem(s, op, &ind);
            data_write(s, addr, (uint16_t)(s->b & 0xFFFF));
            return consumed + s->lk_used;
        }
        /* 0x82xx: STH A, Smem  (src=A, no shift) */
        if (hi8 == 0x82) {
            addr = resolve_smem(s, op, &ind);
            data_write(s, addr, (uint16_t)((s->a >> 16) & 0xFFFF));
            return consumed + s->lk_used;
        }
        /* 89xx: ST src, Smem with shift or MVDK variants */
        if (hi8 == 0x89) {
            addr = resolve_smem(s, op, &ind);
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            data_write(s, op2, data_read(s, addr));
            return consumed + s->lk_used;
        }
        /* 0x8B is POPD Smem, handled above; this is unreachable and stays a
         * 1-word no-op. It is NOT a long-address MVDK. */
        if (hi8 == 0x8B) {
            return 1;
        }
        /* 8Dxx: MVDD Smem, Smem */
        if (hi8 == 0x8D) {
            addr = resolve_smem(s, op, &ind);
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            data_write(s, op2, data_read(s, addr));
            return consumed + s->lk_used;
        }
        /* ⚠️ 0x83 is STH B,Smem, not WRITA (which is 0x7F), and 0x84 is
         * STL A,ASM,Smem, not READA (which is 0x7E). */
        /* 0x83xx: STH B, Smem  (src=B, no shift) */
        if (hi8 == 0x83) {
            addr = resolve_smem(s, op, &ind);
            data_write(s, addr, (uint16_t)((s->b >> 16) & 0xFFFF));
            return consumed + s->lk_used;
        }
        /* 0x84xx: STL A, ASM, Smem (src=A, with ASM shift). The 0x85 (STL B) and
         * 0x86/0x87 (STH A/B) ASM variants have their own handlers above. */
        if (hi8 == 0x84) {
            addr = resolve_smem(s, op, &ind);
            int shift = asm_shift(s);
            int64_t v = s->a;
            if (shift >= 0) v <<= shift; else v >>= (-shift);
            data_write(s, addr, (uint16_t)(v & 0xFFFF));
            return consumed + s->lk_used;
        }
        /* 91xx: MVKD dmad, Smem (another encoding) */
        if (hi8 == 0x91) {
            addr = resolve_smem(s, op, &ind);
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            data_write(s, addr, data_read(s, op2));
            return consumed + s->lk_used;
        }
        /* 97xx: ST #lk, Smem (2-word). 0x96xx is caught above as MVDP. */
        if (hi8 == 0x97) {
            addr = resolve_smem(s, op, &ind);
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            data_write(s, addr, op2);
            return consumed + s->lk_used;
        }
        goto unimpl;

    case 0xA: case 0xB:
        /* Axx/Bxx: STLM, LDMM, misc accumulator ops */

        /* ---- Dual-operand MAC/MAS Xmem, Ymem, dst (1-word) ----
         * MAC:  dst += T * Xmem; T = Ymem
         * MACR: dst += rnd(T * Xmem); T = Ymem
         * MAS:  dst -= T * Xmem; T = Ymem
         * MASR: dst -= rnd(T * Xmem); T = Ymem
         * Encoding: OOOO OOOD XXXX YYYY (1 word)
         *   Xmem: AR[ARP], post-mod by bit4 (0=inc,1=dec)
         *   Ymem: AR[bits2:0], post-mod by bit3 (0=inc,1=dec)
         *   D: 0=A, 1=B
         * hi8 mapping per SPRU172C:
         *   0xA4/0xA5: MAC[R] Xmem,Ymem,A   0xA6/0xA7: MAC[R] Xmem,Ymem,B
         *   0xB4/0xB5: MAS[R] Xmem,Ymem,A   0xB6/0xB7: MAS[R] Xmem,Ymem,B
         *   0xB0/0xB1: MAC[R] Xmem,Ymem,A (alt)  0xB2/0xB3 already handled
         */
        if (hi8 == 0xA4 || hi8 == 0xA5 || hi8 == 0xA6 || hi8 == 0xA7 ||
            hi8 == 0xB4 || hi8 == 0xB5 || hi8 == 0xB6 || hi8 == 0xB7 ||
            hi8 == 0xB0 || hi8 == 0xB1 || hi8 == 0xB2 || hi8 == 0xB3) {
            /* 2-bit Xmem/Ymem decoding (SPRU131G tables 5-6/5-8, same as
             * resolve_xmem): Xmod[7:6] Xar[5:4] Ymod[3:2] Yar[1:0], AR = field + 2
             * (AR2..AR5), mod 0 = *AR, 1 = *AR-, 2 = *AR+, 3 = *AR+0% circular
             * (BK, +AR0).
             * ⚠️ A raw 3-bit split ((op>>4)&7 / op&7 with a 1-bit post-mod) picks
             * the wrong ARs: op=0xb4f5 at PC=0xf170 (steady-state TOA correlator)
             * then reads Xmem=AR7 and Ymem=AR5, both outside the buffer, instead of
             * Xmem=AR5 (*AR5+0%) and Ymem=AR3 (*AR3-), so Ymem reads 0, T becomes 0
             * and a_sync_demod[D_TOA] ends up garbage. */
            int xar_d  = ((op >> 4) & 0x03) + 2;
            int yar_d  = (op & 0x03) + 2;
            int xmod_d = (op >> 6) & 0x03;
            int ymod_d = (op >> 2) & 0x03;
            uint16_t xval_d = data_read(s, s->ar[xar_d]);
            uint16_t yval_d = data_read(s, s->ar[yar_d]);
            {   /* MAC-PROBE (CALYPSO_MAC_PROBE, read-only, capped): traces the TOA
                 * correlator at the dual MAC around PC=0xf170, showing Xmem (AR5,
                 * I/Q) and Ymem (AR3, reference), their addresses and values, plus
                 * T. Tells a DC reference (AR3) from an unfed I/Q buffer (AR5). */
                static int _mp = -1; static unsigned _mpn = 0;
                if (_mp < 0) _mp = calypso_gate("CALYPSO_MAC_PROBE", 0);
                if (_mp && s->pc >= 0xf150 && s->pc <= 0xf190 && _mpn < 80) {
                    _mpn++;
                    fprintf(stderr, "[c54x] MAC-PROBE PC=0x%04x op=0x%04x "
                            "Xar=AR%d@0x%04x=0x%04x Yar=AR%d@0x%04x=0x%04x "
                            "T=0x%04x AR3=0x%04x AR5=0x%04x insn=%u\n",
                            s->pc, op, xar_d, s->ar[xar_d], xval_d,
                            yar_d, s->ar[yar_d], yval_d, s->t,
                            s->ar[3], s->ar[5], s->insn_count);
                }
            }
            /* Post-modify (SPRU131G Table 5-8) */
            switch (xmod_d) {
            case 1: s->ar[xar_d]--; break;
            case 2: s->ar[xar_d]++; break;
            case 3: s->ar[xar_d] = c54x_circ_ref(s->ar[xar_d], +(int16_t)s->ar[0], s->bk); break;
            }
            switch (ymod_d) {
            case 1: s->ar[yar_d]--; break;
            case 2: s->ar[yar_d]++; break;
            case 3: s->ar[yar_d] = c54x_circ_ref(s->ar[yar_d], +(int16_t)s->ar[0], s->bk); break;
            }
            /* The dual family multiplies its TWO MEMORY operands together, not T
             * by Xmem. binutils:
             *   0xa400-0xa5ff mpy   X,Y,dst       dst = X*Y      (assignment)
             *   0xa600-0xa7ff macsu X,Y,src1      src += u(X)*s(Y)
             *   0xb000-0xb3ff mac   X,Y,src,dst   dst = src + X*Y
             *   0xb400-0xb7ff macr  same, rounded
             * `T * Xmem ; T = Ymem` is the SINGLE-operand MAC semantics
             * (MAC Smem,src -> src += T*Smem). The C54x side effect here is
             * T <- Xmem, not Ymem.
             * [2026-08-23] Measured in the SCH coefficient loop (0x81f3 LD #0,A;
             * RPT; MAC *AR2+,*AR4+; RPT; MAC *AR3+,*AR5+; 0x8202 STH A): at
             * pc=0x81f5 op=0xb08a, T was 0x0000 at the first MAC, so the product
             * was zero, A stayed zero, STH wrote 0 and 810 zeros landed in
             * 0x2cba..0x2cbf, which MVDD copied on as null FIRS coefficients --
             * while the inputs were sane.
             * ⚠️ Global effect: this is the backbone of all signal processing. */
            {
                int is_mpy   = (hi8 == 0xA4 || hi8 == 0xA5);
                int is_macsu = (hi8 == 0xA6 || hi8 == 0xA7);
                int is_macr  = (hi8 >= 0xB4 && hi8 <= 0xB7);
                int64_t p = is_macsu
                    ? (int64_t)(uint16_t)xval_d * (int64_t)(int16_t)yval_d
                    : (int64_t)(int16_t)xval_d * (int64_t)(int16_t)yval_d;
                if (s->st1 & ST1_FRCT) p <<= 1;
                /* [2026-10-15] SPRU172C MACR Xmem,Ymem : dst = rnd(src + X*Y), the RESULT is
                 * rounded (as for MACR Smem). Behind the same opt-in as the Smem forms
                 * (CALYPSO_MAC=1); default keeps the product rounding. */
                if (is_macr && c54x_mac_ancien()) p = (p + 0x8000) & ~0xFFFFLL;   /* [2026-09-21] rnd() clears bits 15-0 */
                if (is_macr && !c54x_mac_ancien()) p += 0x8000;
                int dstb, srcb;
                if (is_mpy || is_macsu) {          /* mask 0xFE00: bit 8 = acc */
                    dstb = hi8 & 1; srcb = dstb;
                } else {                            /* mask 0xFC00: b9 = src, b8 = dst */
                    srcb = (op >> 9) & 1; dstb = (op >> 8) & 1;
                }
                int64_t base = srcb ? s->b : s->a;
                int64_t res  = is_mpy ? p : (base + p);
                if (is_macr) res &= ~(int64_t)0xFFFF;   /* rnd(): +0x8000 then clear the low half */
                if (dstb) s->b = sext40(res);
                else      s->a = sext40(res);
                s->t = xval_d;                      /* C54x side effect: T <- Xmem */
                return consumed + s->lk_used;
            }
        }

        /* [2026-09-21] 0xA1xx is NOT SQDST. SQDST Xmem,Ymem is 0xE2xx (binutils
         * { "sqdst", 0xE200, 0xFF00 }); 0xA000-0xA1FF is ADD Xmem,Ymem,dst with
         * bit 8 = dst, so every `add Xmem,Ymem,B` (a1xx) was run as SQDST:
         * A <- Ymem<<16 (clobbered), B += (AH-Xmem)^2, T <- Xmem.
         * Measured on the NB equaliser (PROM0 0x8251 `a189 add *AR2+,*AR3+,B`,
         * 206 times per burst): the 3-tap sum just built in A was replaced by
         * the derotated Q sample, so the "soft bits" were the raw Q plane
         * (clean but one symbol early and inverted), the TSC residual check
         * saw no correlation (count 6) and the quantiser output +1 everywhere.
         * Also hits 0x8565, 0x81fa, 0x7f03, 0x7fab-0x80ec (tap threshold /
         * SB path). The block is kept for reference, never entered. */
        if (0 && hi8 == 0xA1) {
            /* 2-bit Xmem/Ymem per SPRU131G tables 5-6/5-8. */
            int xar_sq  = ((op >> 4) & 0x03) + 2;
            int yar_sq  = (op & 0x03) + 2;
            int xmod_sq = (op >> 6) & 0x03;
            int ymod_sq = (op >> 2) & 0x03;
            uint16_t xval_sq = data_read(s, s->ar[xar_sq]);
            uint16_t yval_sq = data_read(s, s->ar[yar_sq]);
            switch (xmod_sq) { case 1: s->ar[xar_sq]--; break; case 2: s->ar[xar_sq]++; break;
                case 3: s->ar[xar_sq] = c54x_circ_ref(s->ar[xar_sq], +(int16_t)s->ar[0], s->bk); break; }
            switch (ymod_sq) { case 1: s->ar[yar_sq]--; break; case 2: s->ar[yar_sq]++; break;
                case 3: s->ar[yar_sq] = c54x_circ_ref(s->ar[yar_sq], +(int16_t)s->ar[0], s->bk); break; }
            int16_t ah_sq = (int16_t)((s->a >> 16) & 0xFFFF);
            int32_t diff = (int32_t)ah_sq - (int32_t)(int16_t)xval_sq;
            int64_t sq = (int64_t)diff * (int64_t)diff;
            if (s->st1 & ST1_FRCT) sq <<= 1;
            s->b = sext40(s->b + sq);
            s->a = sext40((int64_t)(int16_t)yval_sq << 16);
            s->t = xval_sq;
            return consumed + s->lk_used;
        }

        /* POLY Xmem, Ymem — Polynomial evaluation (1-word dual-operand)
         * Encoding: 1011 110D XXXX YYYY (0xBC=A, 0xBD=B)
         *           1011 111D XXXX YYYY (0xBE/0xBF variants — ABDST or POLY)
         * Per SPRU172C: B += AH * T (with round); A = Xmem << 16; T = Ymem */
        if (hi8 == 0xBC || hi8 == 0xBD || hi8 == 0xBE || hi8 == 0xBF) {
            /* 2-bit Xmem/Ymem decoding per SPRU131G tables 5-6/5-8. */
            int xar_p  = ((op >> 4) & 0x03) + 2;
            int yar_p  = (op & 0x03) + 2;
            int xmod_p = (op >> 6) & 0x03;
            int ymod_p = (op >> 2) & 0x03;
            uint16_t xval_p = data_read(s, s->ar[xar_p]);
            uint16_t yval_p = data_read(s, s->ar[yar_p]);
            switch (xmod_p) { case 1: s->ar[xar_p]--; break; case 2: s->ar[xar_p]++; break;
                case 3: s->ar[xar_p] = c54x_circ_ref(s->ar[xar_p], +(int16_t)s->ar[0], s->bk); break; }
            switch (ymod_p) { case 1: s->ar[yar_p]--; break; case 2: s->ar[yar_p]++; break;
                case 3: s->ar[yar_p] = c54x_circ_ref(s->ar[yar_p], +(int16_t)s->ar[0], s->bk); break; }
            /* 0xBC00-0xBFFF is dual MASR, not POLY. binutils:
             *   { "masr", 0xBC00, 0xFC00, {OP_Xmem,OP_Ymem,OP_SRC,OP_DST} }
             *   { "poly", 0x3600, 0xFF00, {OP_Smem} }   <- POLY lives elsewhere
             * SPRU172C: dst = rnd(src - Xmem*Ymem) AND T = Xmem.
             * ⚠️ Running a POLY here (B += rnd(AH*T); A = Xmem<<16; T = Ymem) picks
             * the wrong accumulator, the wrong product and the wrong T, at 119
             * sites. */
            {
                int64_t pr = (int64_t)(int16_t)xval_p * (int64_t)(int16_t)yval_p;
                if (s->st1 & ST1_FRCT) pr <<= 1;
                int srcr = (op >> 9) & 1, dstr = (op >> 8) & 1;
                int64_t baser = srcr ? s->b : s->a;
                int64_t resr;
                if (c54x_mac_ancien()) {
                    pr = (pr + 0x8000) & ~0xFFFFLL;     /* round: bits 15-0 cleared [2026-09-21] */
                    resr = baser - pr;
                } else {   /* [2026-10-15] SPRU172C MASR Xmem,Ymem : dst = rnd(src - X*Y) (CALYPSO_MAC=1) */
                    resr = (baser - pr + 0x8000) & ~0xFFFFLL;
                }
                if (dstr) s->b = sext40(resr);
                else      s->a = sext40(resr);
                s->t = xval_p;
                return consumed + s->lk_used;
            }
        }

        /* B8-BB: MAS/MASR Xmem, Ymem (subtract variants) or POLY-like */
        if (hi8 == 0xB8 || hi8 == 0xB9 || hi8 == 0xBA || hi8 == 0xBB) {
            /* 0xBA is LDMM, handled below. */
            if (hi8 == 0xBA) goto ba_handler;
            /* 2-bit Xmem/Ymem decoding per SPRU131G tables 5-6/5-8. */
            int xar_b8  = ((op >> 4) & 0x03) + 2;
            int yar_b8  = (op & 0x03) + 2;
            int xmod_b8 = (op >> 6) & 0x03;
            int ymod_b8 = (op >> 2) & 0x03;
            uint16_t xval_b8 = data_read(s, s->ar[xar_b8]);
            uint16_t yval_b8 = data_read(s, s->ar[yar_b8]);
            switch (xmod_b8) { case 1: s->ar[xar_b8]--; break; case 2: s->ar[xar_b8]++; break;
                case 3: s->ar[xar_b8] = c54x_circ_ref(s->ar[xar_b8], +(int16_t)s->ar[0], s->bk); break; }
            switch (ymod_b8) { case 1: s->ar[yar_b8]--; break; case 2: s->ar[yar_b8]++; break;
                case 3: s->ar[yar_b8] = c54x_circ_ref(s->ar[yar_b8], +(int16_t)s->ar[0], s->bk); break; }
            /* Dual MAS: binutils { "mas", 0xB800, 0xFC00,
             * {OP_Xmem,OP_Ymem,OP_SRC,OP_DST} }, SPRU172C:
             *     dst = src - Xmem*Ymem   AND   T = Xmem
             * ⚠️ Computing `T * Xmem` then `T = Ymem` is the SINGLE-operand MAS
             * semantics applied to the dual form, the same defect already fixed for
             * mac/mpy; 122 sites. */
            int64_t prod_b8 = (int64_t)(int16_t)xval_b8 * (int64_t)(int16_t)yval_b8;
            if (s->st1 & ST1_FRCT) prod_b8 <<= 1;
            int src_b8 = (op >> 9) & 1;
            int dst_b8 = (op >> 8) & 1;
            int64_t base_b8 = src_b8 ? s->b : s->a;
            if (dst_b8) s->b = sext40(base_b8 - prod_b8);
            else        s->a = sext40(base_b8 - prod_b8);
            s->t = xval_b8;
            return consumed + s->lk_used;
        }
ba_handler:
        if (hi8 == 0xAA || hi8 == 0xAB) {
            /* 0xAA/0xAB belong to the parallel LD||MACR family, and are NOT
             * STLM src,MMR (that is 0x88/0x89). One word, no effect. This stub
             * sits before the LD||MAC handler below and shadows it for those two
             * opcodes.
             * [2026-10-15] isa_test 93 (SPRU172C LD *AR4+,A || MACR *AR5+,B):
             * the stub loads nothing and advances no pointer. It now falls
             * through to the LD||MAC handler; CALYPSO_LD_MACR_STUB=1 restores it. */
            static int ld_macr_stub = -1;
            if (ld_macr_stub < 0) ld_macr_stub = calypso_gate("CALYPSO_LD_MACR_STUB", 0);
            if (ld_macr_stub) return 1;
        }
        if (hi8 == 0xBA) {
            /* LDMM MMR, dst — load the MMR value into the accumulator.
             * Per SPRU172C: dst[15:0] = MMR, dst[31:16] = sign extension (SXM) or 0.
             * ⚠️ `sext40(v << 16)` puts the MMR in the wrong half: the boot stub at
             * 0x0000 (LDMM SP,B) must return B = the current SP, and with the shift
             * B[15:0] is 0 for every caller that reads B as a 16-bit SP. */
            uint16_t mmr = op & 0x7F;
            int dst = (op >> 4) & 1;
            int64_t v = (int64_t)(int16_t)data_read(s, mmr);
            if (dst) s->b = sext40(v);
            else     s->a = sext40(v);
            return consumed + s->lk_used;
        }
        /* Parallel LD||MAC family: 0xA800-0xAFFF, ONE word.
         * binutils (tic54x_paroptab):
         *   0xA800/0xFE00  ld Xmem,dst || mac  Ymem
         *   0xAA00/0xFE00  ld Xmem,dst || macr Ymem
         *   0xAC00/0xFE00  ld Xmem,dst || mas  Ymem
         *   0xAE00/0xFE00  ld Xmem,dst || masr Ymem
         * SPRU172C: dst = Xmem << 16; the OTHER accumulator gets +/- T*Ymem
         * (rounded for the R variants); T IS UNCHANGED. bit 8 selects dst.
         *
         * ⚠️ The wrong decodings all cost a word: 0xA8/0xA9 as a 2-word `AND #lk`
         * (the real one is 0xF030), 0xAC/0xAD as a 2-word `MACP Smem,pmad` (really
         * 0x7800), 0xAE/0xAF as a 2-word `MACD Smem,pmad` (really 0x7A00, and it
         * also wrote T). Those three swallow one word too many at each of 233
         * sites; 0xAA/0xAB as a silent stub is the right length but does nothing,
         * at 193 sites. */
        if ((op & 0xF800) == 0xA800) {
            int xar_l  = ((op >> 4) & 0x03) + 2;
            int yar_l  = ( op       & 0x03) + 2;
            int xmod_l = (op >> 6) & 0x03;
            int ymod_l = (op >> 2) & 0x03;
            uint16_t xv_l = data_read(s, s->ar[xar_l]);
            uint16_t yv_l = data_read(s, s->ar[yar_l]);
            int dst_l  = (op >> 8) & 1;              /* accumulator of the LD   */
            int soust  = ((op >> 10) & 1);           /* 0xAC/0xAE = subtract    */
            int arrondi= ((op >> 9)  & 1);           /* 0xAA/0xAE = round       */
            int64_t p_l = (int64_t)(int16_t)s->t * (int64_t)(int16_t)yv_l;
            if (s->st1 & ST1_FRCT) p_l <<= 1;
            /* SPRU172C: dst_ = rnd(dst_ +/- T*Ymem) -- the RESULT is rounded, as for
             * MACR/MASR Smem; the old product rounding is kept under
             * c54x_mac_ancien() (default) like the single-operand MACR. */
            int ld_rnd_ancien = c54x_mac_ancien();
            if (arrondi && ld_rnd_ancien) p_l = (p_l + 0x8000) & ~0xFFFFLL;   /* [2026-09-21] rnd() clears bits 15-0 */
            int64_t *acc_ld = dst_l ? &s->b : &s->a;   /* receives the LD  */
            int64_t *acc_op = dst_l ? &s->a : &s->b;   /* receives the MAC */
            int64_t r_l = soust ? (*acc_op - p_l) : (*acc_op + p_l);
            if (arrondi && !ld_rnd_ancien) r_l = (r_l + 0x8000) & ~0xFFFFLL;
            *acc_op = sext40(r_l);
            *acc_ld = sext40((int64_t)(int16_t)xv_l << 16);
            c54x_par_postmod(s, xar_l, xmod_l);
            c54x_par_postmod(s, yar_l, ymod_l);
            return consumed + s->lk_used;            /* one word, T unchanged */
        }
        if (hi8 == 0xA8 || hi8 == 0xA9) {
            /* A8xx/A9xx: AND #lk, src[, dst] (2-word) */
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            int dst = op & 1;
            int64_t *acc = dst ? &s->b : &s->a;
            *acc = sext40(*acc & ((int64_t)op2 << 16));
            return consumed + s->lk_used;
        }
        /* 0xA000-0xA1FF is one instruction. binutils has a single entry on that
         * range:
         *     { "add", 1,3,3, 0xA000, 0xFE00, {OP_Xmem,OP_Ymem,OP_DST} }
         *   ADD Xmem, Ymem, dst  ->  dst = (Xmem << 16) + (Ymem << 16)
         * ⚠️ The accumulator operations it is easily confused with all live in
         * 0xF0xx-0xF4xx (ld 0xF482, neg 0xF484, abs 0xF485, sat 0xF483,
         * sfta 0xF460, sftl 0xF0E0); none of them is at 0xA0.
         *
         * [2026-08-22] Measured on the last instruction of the correlator loop
         * body (RPTB 0x84b0, REA=0x84c6):
         *     0x84c6 : 0xA09A = add *AR3+, *AR4+, A
         * Low byte 0x9A gives Xar=AR3 (*AR+) and Yar=AR4 (*AR+), the correlator's
         * two INPUT pointers (0x2a27 and 0x2ae7 in the burst buffer); this is the
         * instruction that slides the window by one word per lag. Decoded as an
         * accumulator op it became a 77-bit SFTL, no pointer advanced, all 50 lags
         * correlated the same window (distinct = 3/50 and 5/50), the argmax became
         * meaningless and the SCH never decoded. gr-gsm confirms the semantics:
         * the reference restarts at each lag and the input slides.
         * ⚠️ Global effect: ADD Xmem,Ymem,dst is not used only by the correlator. */
        if ((op & 0xFE00) == 0xA000) {
            int xmod = (op >> 6) & 0x03;
            int xar  = ((op >> 4) & 0x03) + 2;
            int ymod = (op >> 2) & 0x03;
            int yar  = ( op       & 0x03) + 2;
            int64_t *dst = ((op >> 8) & 1) ? &s->b : &s->a;
            int16_t xv = (int16_t)data_read(s, s->ar[xar]);
            int16_t yv = (int16_t)data_read(s, s->ar[yar]);
            *dst = sext40(((int64_t)xv + (int64_t)yv) << 16);
            c54x_par_postmod(s, xar, xmod);
            c54x_par_postmod(s, yar, ymod);
            return consumed + s->lk_used;
        }
        if (hi8 == 0xA5) {
            /* CMPS src, Smem — compare and select (Viterbi) */
            addr = resolve_smem(s, op, &ind);
            uint16_t val = data_read(s, addr);
            int src = (op >> 4) & 1;
            int64_t acc = src ? s->b : s->a;
            int64_t cmp = (int64_t)(int16_t)val << 16;
            /* TRN shift left, TC set based on comparison */
            s->trn <<= 1;
            if (acc >= cmp) {
                s->st0 |= ST0_TC;
                s->trn |= 1;
            } else {
                s->st0 &= ~ST0_TC;
                if (src) s->b = cmp; else s->a = cmp;
            }
            return consumed + s->lk_used;
        }
        /* AExx/AFxx: MACD Smem, pmad, dst — MAC + data move (2 words)
         * dst += T * Smem, then data(Smem) → data(dmad)
         * pmad in second word auto-increments during RPT */
        if (hi8 == 0xAE || hi8 == 0xAF) {
            addr = resolve_smem(s, op, &ind);
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            uint16_t sval = data_read(s, addr);
            int64_t prod = (int64_t)(int16_t)s->t * (int64_t)(int16_t)sval;
            if (s->st1 & ST1_FRCT) prod <<= 1;
            int dst = (hi8 & 0x01);
            if (dst) s->b = sext40(s->b + prod);
            else     s->a = sext40(s->a + prod);
            /* Data move: read from prog[pmad], write to data[addr] */
            uint16_t psrc = s->rpt_active ? s->mvpd_src : op2;
            data_write(s, addr, prog_fetch(s, psrc));
            s->mvpd_src = psrc + 1;
            s->t = sval;  /* T = old Smem value (before overwrite) */
            return consumed + s->lk_used;
        }
        /* ACxx/ADxx: MACP Smem, pmad, dst — MAC + program fetch (2 words) */
        if (hi8 == 0xAC || hi8 == 0xAD) {
            addr = resolve_smem(s, op, &ind);
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            uint16_t sval = data_read(s, addr);
            int64_t prod = (int64_t)(int16_t)s->t * (int64_t)(int16_t)sval;
            if (s->st1 & ST1_FRCT) prod <<= 1;
            int dst = (hi8 & 0x01);
            if (dst) s->b = sext40(s->b + prod);
            else     s->a = sext40(s->a + prod);
            /* Coeff fetch from program memory */
            uint16_t psrc = s->rpt_active ? s->mvpd_src : op2;
            s->t = prog_fetch(s, psrc);
            s->mvpd_src = psrc + 1;
            return consumed + s->lk_used;
        }
        /* ⚠️ 0xB3 is MACR Xmem,Ymem,B (1 word, handled with the MAC family above),
         * not a 2-word `LD #lk,dst`: the extra word drifts PC inside the RPTBD
         * routine at 0x820e..0x820f and loops forever at 0x821a. */
        /* ADD #lk, src[, dst] */
        if (hi8 == 0xA2) {
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            int dst = op & 1;
            int64_t v = (s->st1 & ST1_SXM) ? (int16_t)op2 : op2;
            if (dst) s->b = sext40(s->b + (v << 16));
            else     s->a = sext40(s->a + (v << 16));
            return consumed + s->lk_used;
        }
        /* SUB #lk */
        if (hi8 == 0xA3) {
            op2 = prog_fetch(s, s->pc + 1);
            consumed = 2;
            int dst = op & 1;
            int64_t v = (s->st1 & ST1_SXM) ? (int16_t)op2 : op2;
            if (dst) s->b = sext40(s->b - (v << 16));
            else     s->a = sext40(s->a - (v << 16));
            return consumed + s->lk_used;
        }
        goto unimpl;

    case 0xC: case 0xD:
        /* ================================================================
         * Parallel family: ST src,Ymem || <op> Xmem,dst (ONE word,
         * SPRU172C 4-177..4-185).
         *
         * The whole 0xC000-0xDFFF range is ONE family: binutils has four "st"
         * entries of mask 0xFC00 plus those at 0xD000/D400/D800/DC00, all flagged
         * FL_PAR. ⚠️ Splitting it into unrelated instructions costs words, because
         * some of the impostors are 2 words: RPTB is really 0xF072, RPTBD 0xF272,
         * PSHD 0x4B00, RPT Smem 0x4700, SACCD 0x9E00, DELAY 0x4D00 -- none of them
         * lives here.
         *
         * [2026-08-22] Measured: the FB/SB correlator kernel (PDROM
         * 0xf16c..0xf17e, `rptbd 0xf178` loop) is built on this family --
         *     0xf172 c780 = ST||SUB    0xf173 ce91 = ST||MPY
         *     0xf176 c709 = ST||SUB    0xf178 ce98 = ST||MPY
         * Decoded as 2-word RPTB they swallowed the next word AND overwrote
         * rea/rsa/rptb_active/BRAF of the live rptbd loop containing them, so the
         * correlation surface was computed from a different program than the
         * silicon runs: false peak, false argmax, a_sync_demod[D_TOA] an arbitrary
         * word (TOA = -7791 / -3691 / +8026 / -28903, outside 0..156) and
         * d_fb_det = 1 about once in 95.
         *
         * Semantics (SPRU172C, encoding `1100xxSD`/`1101RSD` XXXXYYYY):
         *   store : Ymem = (src << ASM) >> 16          (src = b9, NOT b8)
         *   op    : C0-C3 dst += Xmem<<16 | C4-C7 dst = Xmem<<16 - dst
         *           C8-CB dst  = Xmem<<16 | CC-CF dst = T*Xmem
         *           D0-D7 dst += T*Xmem   | D8-DF dst -= T*Xmem  (R = b10, round)
         *   Xmem is read BEFORE the store ("If src is equal to dst, the value
         *   stored in Ymem is the value of src before the execution").
         *   This family NEVER writes T.
         *
         * ⚠️ Global effect: the family also serves the filters, the Viterbi and the
         * I/Q buffers.
         * ================================================================ */
        {
            int s_acc = (op >> 9) & 1;        /* S = b9: source acc of the ST  */
            int d_acc = (op >> 8) & 1;        /* D = b8: destination of the op */
            int xmod  = (op >> 6) & 3;
            int xar   = ((op >> 4) & 3) + 2;  /* Xmem : AR2..AR5 */
            int ymod  = (op >> 2) & 3;
            int yar   = ( op       & 3) + 2;  /* Ymem : AR2..AR5 */
            uint16_t xaddr = s->ar[xar];
            uint16_t yaddr = s->ar[yar];
            uint16_t xval  = data_read(s, xaddr);   /* read BEFORE the store */
            int64_t  sv    = s_acc ? s->b : s->a;
            int64_t *dstp  = d_acc ? &s->b : &s->a;
            int      ash   = asm_shift(s);
            int64_t  sh    = (ash >= 0) ? (sv << ash) : (sv >> (-ash));
            int64_t  xs    = (int64_t)(int16_t)xval;
            int64_t  prod;

            /* ST src,Ymem : Ymem = (src << ASM) >> 16 */
            data_write(s, yaddr, (uint16_t)((sh >> 16) & 0xFFFF));

            switch ((op >> 10) & 7) {         /* bits 12:10 = sub-class */
            /* FIX_PAR_ST_DSTBAR — per SPRU172C 4-177/4-185,
             * "|| ADD Xmem, dst : dst = dst_ + Xmem << 16" and
             * "|| SUB Xmem, dst : dst = (Xmem << 16) - dst_", where dst_ is the
             * OTHER accumulator (dst = A means dst_ = B), not dst itself.
             * The SB demodulator (0x76e4/0x76f5 `c0dc`) uses it 896 times per
             * burst. */
            case 0: { /* C0-C3 : || ADD Xmem,dst */
                int64_t other = d_acc ? s->a : s->b;
                *dstp = sext40(other + (xs << 16));
                break; }
            case 1: { /* C4-C7 : || SUB Xmem,dst  ->  Xmem<<16 - dst_ */
                int64_t other = d_acc ? s->a : s->b;
                *dstp = sext40((xs << 16) - other);
                break; }
            case 2:  /* C8-CB : || LD Xmem,dst */
                *dstp = sext40(xs << 16);
                break;
            case 3:  /* CC-CF : || MPY Xmem,dst */
                prod = (int64_t)(int16_t)s->t * xs;
                if (s->st1 & ST1_FRCT) prod <<= 1;
                *dstp = sext40(prod);
                break;
            default: /* D0-D7 MAC[R] (b11=0) , D8-DF MAS[R] (b11=1) */
                prod = (int64_t)(int16_t)s->t * xs;
                if (s->st1 & ST1_FRCT) prod <<= 1;
                if (op & 0x0400) {            /* R = b10: round */
                    prod += 0x8000;
                    prod &= ~0xFFFFLL;
                }
                *dstp = sext40((op & 0x0800) ? (*dstp - prod)
                                             : (*dstp + prod));
                break;
            }
            /* T is NOT modified by this family. */
            c54x_par_postmod(s, xar, xmod);
            c54x_par_postmod(s, yar, ymod);
            return consumed + s->lk_used;
        }

    default:
        break;
    }

unimpl:
    s->unimpl_count++;
    if (s->unimpl_count <= 200 || op != s->last_unimpl) {
        C54_LOG("UNIMPL @0x%04x: 0x%04x (hi8=0x%02x) [#%u]",
                s->pc, op, hi8, s->unimpl_count);
        s->last_unimpl = op;
    }
    return consumed + s->lk_used;
}

/* The main execution loop and the DSP idle fast-forward
 * (CALYPSO_DSP_IDLE_FF, CALYPSO_DSP_IDLE_RANGE) live in calypso_c54x.c. */

4.43 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/c54x_internal.h

20540 octets, 529 lignes → 529 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * c54x_internal.h - internal header of the C54x core.
 *
 * The core is split by role:
 *   calypso_c54x.c  init / reset / c54x_run loop / public API
 *   c54x_exec.c     c54x_exec_one and the instruction families
 *   c54x_decode.c   operand resolution (Smem/Lmem/Xmem), conditions
 *   c54x_mem.c      data and program memory, overlay, locks
 *   c54x_irq.c      IFR/IMR, frame interrupt, interrupt dispatch
 *   c54x_probes.c   probes and traces (diagnostics only)
 *
 * This header carries what those files share: includes, constants, types and
 * core global state. It is STRICTLY internal.
 *
 * File-scope anonymous structs were given names (c54x_<var>_s): two anonymous
 * definitions are DIFFERENT types from one translation unit to the next, which
 * makes them impossible to share.
 */
#ifndef CALYPSO_C54X_INTERNAL_H
#define CALYPSO_C54X_INTERNAL_H


#include "calypso_c54x.h"
extern int c54x_rapide;   /* fast path, see calypso_c54x.h */
/* Switch for the PURE probes (those that only read the state and log or
 * count). Hundreds of PC tests, static counters and diagnostic prog_fetch
 * calls per instruction cost too much for real time (13+ M insn/s needed),
 * so they sit behind this switch. Resolved ONCE (c54x_sondes_resoudre, called
 * by c54x_init and at the top of c54x_run): CALYPSO_SONDES=1, or CALYPSO_DEBUG
 * non-empty, or -vvvv and above in c54x_exe (main.c sets it before c54x_init).
 * Default: off.
 *   -1 = not resolved yet (treated as off by C54X_SONDES)
 * Whatever MODIFIES the state (@BEQUILLE, realignments, forcings, OVM, timer,
 * IRQ) and whatever feeds other processes (/dev/shm, side-bands) stays OUTSIDE
 * this switch, executed as is and in the same order. With CALYPSO_SONDES=1 the
 * output is identical to the unswitched one. */
extern int c54x_sondes;
void c54x_sondes_resoudre(void);
#define C54X_SONDES (__builtin_expect(c54x_sondes > 0, 0))
/* Counter of TAKEN (vectored) interrupts, all paths: replay at the top of
 * c54x_run, c54x_irq_level_check, c54x_interrupt_ex. Pure diagnostic counter,
 * always maintained (one increment per taken interrupt); it tells the
 * ANNEAU-EXEC ring whether an interrupt was taken in the current instruction. */
extern unsigned g_c54x_it_prises;
#include "calypso_rif.h"
#include "calypso_rhea_dma.h"
#include "hw/arm/calypso/calypso_xio.h"   /* SAM/HOM arbitration */
#include "calypso_mailbox.h"
#include "calypso_dma.h"
#include "calypso_arm2dsp.h"
#include "hw/arm/calypso/calypso_invariants.h"
#include "calypso_bsp.h"
#include "hw/arm/calypso/calypso_trf6151.h"
#include "calypso_full_pcb.h"  /* daram_lock, api_ram_lock */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>   /* DARAM-SANITY: coherence/dphi of the corr buffer */
/* DARAM-FNSTAMP counters, defined in calypso_bsp.c. */
extern unsigned calypso_daram_last_fn;
extern unsigned calypso_daram_wr_count;

extern int calypso_rxfb_fired;   /* [probe golive] defined in calypso_bsp.c */

static int g_boot_trace = 0;
/* VEC28-STACK-TRACE, gated by CALYPSO_TRACE_VEC28_STACK. READ-ONLY diagnostic:
 * traces the software stack from a vec28 interrupt entry (2-word PC+XPC push by
 * c54x_interrupt_ex) through every RET-family instruction until SP returns to
 * its pre-interrupt level, to show whether a 1-word RET/RCD/RETED pop consumes
 * the orphaned XPC word (expected 0x0000) as if it were a return PC. Observes
 * only; no DSP state is modified. */
static int g_vec28_trace_en = -1;
static bool g_vec28_tracing = false;
static uint16_t g_vec28_sp_entry = 0;
static unsigned g_vec28_trace_pops = 0;

#include "hw/arm/calypso/calypso_debug.h"

/* Legacy C54_LOG: gated by CALYPSO_DEBUG containing "C54X" or "ALL".
 * For per-probe gating use C54_DBG("PROBE_NAME", fmt, ...). */
#define C54_LOG(fmt, ...) \
    do { if (calypso_debug_enabled("C54X")) \
        fprintf(stderr, "[c54x] " fmt "\n", ##__VA_ARGS__); } while (0)

/* ================================================================
 * Helpers
 * ================================================================ */

/* Sign-extend 40-bit accumulator */
static inline int64_t sext40(int64_t v)
{
    if (v & ((int64_t)1 << 39))
        v |= ~(((int64_t)1 << 40) - 1);
    else
        v &= ((int64_t)1 << 40) - 1;
    return v;
}

/* Saturate 40-bit to 32-bit (OVM mode) */
static inline int64_t sat32(int64_t v)
{
    if (v > 0x7FFFFFFF) return 0x7FFFFFFF;
    if (v < (int64_t)(int32_t)0x80000000) return (int64_t)(int32_t)0x80000000;
    return v;
}

/* Get ARP from ST0 */
static inline int arp(C54xState *s)
{
    return (s->st0 >> ST0_ARP_SHIFT) & 7;
}

/* Get DP from ST0 */
static inline uint16_t dp(C54xState *s)
{
    return s->st0 & ST0_DP_MASK;
}

/* Get ASM from ST1 (5-bit signed) */
static inline int asm_shift(C54xState *s)
{
    int v = s->st1 & ST1_ASM_MASK;
    if (v & 0x10) v |= ~0x1F;  /* sign extend */
    return v;
}

/* ---- shared constants and types (original order preserved) ---- */
#define C54_LOG(fmt, ...) \
    do { if (calypso_debug_enabled("C54X")) \
        fprintf(stderr, "[c54x] " fmt "\n", ##__VA_ARGS__); } while (0)
typedef struct {
    uint64_t per_pc[0x10000];
    uint64_t total;
    uint64_t last_log_insn;
    uint64_t last_summary_insn;
    uint16_t last_exec_pc;
} WatchWriteState;
#define AR_HIST_MAX 64
typedef struct {
    uint16_t pc;
    uint16_t op_last;
    uint16_t val_last;
    uint32_t count;
} ArEntry;
typedef struct { uint16_t pc, op, val, sp; char kind; } St0Ev; /* kind P=push p=pop L=ldp */
#define ST0_RING_N 24
struct sp_evt { uint16_t pc; uint16_t op; int16_t delta; uint16_t sp; };
struct shadow_ent { uint16_t pc; uint16_t op; uint16_t sp; char kind; };
#define SHADOW_N 512
#define STKSLOT_LO 0x1100
#define STKSLOT_HI 0x1140
#define STKSLOT_N  (STKSLOT_HI - STKSLOT_LO + 1)
#define SP_ABS_HIST_MAX 128
typedef struct {
    uint16_t pc;
    uint16_t value_last;
    uint32_t count;
    uint8_t  site;   /* 0=MMR_SP, 1=LDK8, 2=MVMM */
} SpAbsEntry;
#define MVPD_BUCKET_BITS 7      /* 0x80 = 128 words per bucket */
#define MVPD_BUCKET_SZ   (1u << MVPD_BUCKET_BITS)
#define MVPD_RANGE_LO    0x0080
#define MVPD_RANGE_HI    0x2800
#define MVPD_BUCKETS_N   (((MVPD_RANGE_HI - MVPD_RANGE_LO) + MVPD_BUCKET_SZ - 1) / MVPD_BUCKET_SZ)
#define CORR_PC_LO 0x8d00
#define CORR_PC_HI 0x9000   /* exclusive */
#define CORR_READ_HIST_MAX 128
typedef struct { uint16_t addr; uint32_t count; } CorrReadEntry;
#define STUCK_HIST_SIZE 64
typedef struct {
    uint16_t pc;
    uint8_t  xpc;
    uint32_t count;
} StuckHistEntry;
#define INT3_BRANCH_TRACE_MAX 1024
typedef struct {
    uint16_t pc;        /* PC of branch insn */
    uint16_t op;        /* opcode word 0 */
    uint16_t next_pc;   /* PC after exec (= branch taken target OR fall-through) */
    uint32_t insn_offset; /* delta from cycle start (first occurrence) */
    uint32_t repeat;    /* consecutive identical (pc,op,next_pc) collapsed count */
} Int3BranchEvent;
enum { RR_MMRS, RR_LOW, RR_APIRAM, RR_TARGET, RR_WRAP, RR_OTHER, RR_NUM };
#define NOP_RING_N 32
typedef struct {
    uint16_t src_pc;
    uint8_t  src_xpc;
    uint16_t op;
    uint16_t tgt_pc;
    uint8_t  tgt_xpc;
    int64_t  a_val;
    uint64_t insn;
    char     type[8];   /* "B", "BACC", "CALA", "FB", "FCALL", "FBACC", "FCALA", "RET", "FRET", "OTHER" */
} XferLog;
typedef struct {
    uint16_t pc;
    uint8_t  xpc;
    uint16_t op;
    int64_t  old_a;
    int64_t  new_a;
    uint64_t insn;
} AWriteLog;
#define RMAP_PCS 48
typedef struct { uint16_t addr, val, pc, op; } StkwEv;
#define STKW_RING_N 48
#define WMAP_PCS 64
#define WMAP_TICK 2000
#define DSP_IDLE_FF_MAX_RANGES 4
typedef struct {
    uint16_t pc;
    uint16_t op_last;
    uint32_t dec_count;
    int32_t  delta_sum;   /* negative = net drain */
} SpDecEntry;
#define SP_HIST_MAX 512
#define SP_RING_SZ 4096  /* must be a power of two; 4096 covers the thousands
                          * of instructions preceding the SP spiral */
typedef struct {
    unsigned insn;
    uint16_t pc;
    uint16_t sp;
    uint16_t op;
    uint16_t _pad;
} SpRingEntry;

/* ---- named structures for the core global state ---- */
struct c54x_g_fb_det_timing_s {
    /* Timing trackers, updated in data_write over 0x2bc0..0x2bff */
    uint64_t last_compute_insn;
    uint16_t last_compute_addr;
    uint64_t last_clear_insn;
    uint16_t last_clear_addr;
    uint64_t last_pattern_insn;
    uint16_t last_pattern_addr;
    /* Stats captured when 0x8f51 fires */
    uint64_t fb_det_total;
    uint64_t fb_det_ar4_in_zone;
    uint64_t fb_det_ar4_outside;
    uint64_t fb_det_dar4_zero;
    uint64_t fb_det_dar4_sentinel;
    uint64_t fb_det_dar4_other;
    /* Sweep tracking: one sweep = 50 consecutive 0x8f51 fires with AR3 walking
     * 0..0x3A3 at stride +19. A wrap (ar3 < last_ar3) starts a new sweep. */
    uint16_t last_ar3_at_fire;
    uint64_t sweep_id;
    uint64_t sweep_nonzero_count;
};
struct c54x_g_throughput_s {
    uint64_t last_logged_insn;
    struct timespec last_logged_ts;
};
struct c54x_g_read_stats_s {
    uint64_t cumulative[RR_NUM];
    uint64_t snapshot[RR_NUM];
    uint64_t trigger_count;
};
struct c54x_g_rmap_s { uint16_t pc; uint32_t n; uint16_t a[8]; uint8_t na;
                uint16_t amn, amx; };
struct c54x_g_wmap_s { uint16_t pc; uint32_t n; uint16_t v[8]; uint8_t nv;
                uint16_t mn, mx; uint16_t addr0; uint32_t n0; };
struct c54x_g_sp_ledger_s {
    uint64_t sp_pushes;        /* SP delta < 0 events */
    uint64_t sp_pops;          /* SP delta > 0 events */
    int64_t  net_words;        /* total words pushed - total words popped */
    uint64_t irq_entries;      /* count of c54x_interrupt_ex actual dispatches */
    uint64_t irq_words_pushed; /* words written by IRQ entry path (1 or 2 per APTS) */
    uint64_t last_dump_insn;
};
struct c54x_g_sp_trail_s {
    unsigned insn;
    uint16_t old_sp, new_sp, exec_pc, exec_op, a_low;
};

/* ---- core global state, defined in one of the .c files ---- */
extern uint16_t g_last_d_burst_d;
extern struct c54x_g_fb_det_timing_s g_fb_det_timing;
extern ArEntry  g_ar_hist[8][AR_HIST_MAX];
extern unsigned g_ar_used[8];
extern unsigned g_ar_total[8];
extern unsigned g_ar_mask;
extern int      g_ar_enabled;
extern unsigned g_ar_log_cap;
extern int64_t  g_a_last_value;
extern uint16_t g_a_last_writer_pc;
extern uint16_t g_a_last_writer_op;
extern unsigned g_a_last_writer_insn;
extern int      g_a_trace_enabled;
extern uint16_t g_a_trace_pc;
extern unsigned g_a_trace_hits;
extern unsigned g_a_trace_log_cap;
extern uint16_t g_ar6_last_value;
extern uint16_t g_ar6_last_writer_pc;
extern uint16_t g_ar6_last_writer_op;
extern unsigned g_ar6_last_writer_insn;
extern int      g_ar6_at_enabled;
extern uint16_t g_ar6_at_pc;
extern unsigned g_ar6_at_win_lo;
extern unsigned g_ar6_at_win_hi;
extern unsigned g_ar6_at_hits;
extern unsigned g_ar6_at_log_cap;
extern uint64_t g_rsbx_intm_hits;
extern int      g_rsbx_intm_enabled;
extern uint64_t g_last_intr_insn;
extern int      g_last_intr_vec;
extern uint16_t g_last_intr_fg_pc;
extern uint16_t g_last_intr_fg_dp;
extern uint16_t g_last_ldp_pc;
extern uint16_t g_last_ldp_val;
extern int      g_last_ldp_kind;
extern uint16_t g_prev_pc;
extern uint16_t g_prev_op;
extern uint16_t g_prev2_op;
extern int g_tc_fiable;      /* TC set by CMPM/BITF and untouched since (c54x_exec.c) */
extern uint16_t g_last_st0w_pc;
extern uint16_t g_last_st0w_val;
extern uint16_t g_last_st0w_op;
extern uint16_t g_last_st0w_xpc;
extern uint16_t g_last_st0w_prev;
extern St0Ev    g_st0_ring[ST0_RING_N];
extern unsigned g_st0_ring_idx;
extern int      g_st0_ring_on;
extern uint16_t g_disp_lut_ea;
extern uint16_t g_disp_lut_val;
extern struct sp_evt g_spring[64];
extern uint32_t g_spring_idx;
extern struct shadow_ent g_shadow[SHADOW_N];
extern int  g_shadow_depth;
extern int  g_shadow_on;
extern uint64_t g_orphan_hits;
extern uint64_t g_mismatch_hits;
extern uint16_t g_stkslot_wpc[STKSLOT_N];
extern uint16_t g_stkslot_wop[STKSLOT_N];
extern uint8_t  g_stkslot_written[STKSLOT_N];
extern SpAbsEntry g_sp_abs_hist[SP_ABS_HIST_MAX];
extern unsigned   g_sp_abs_used;
extern unsigned   g_sp_abs_total;
extern int        g_sp_abs_enabled;
extern unsigned   g_sp_abs_log_cap;
extern uint32_t g_mvpd_buckets[MVPD_BUCKETS_N];  /* 128 words each */;
extern int      g_mvpd_trace_enabled;
extern unsigned g_mvpd_boot_limit;
extern int      g_mvpd_dumped;
extern CorrReadEntry g_corr_read_hist[CORR_READ_HIST_MAX];
extern unsigned    g_corr_read_used;
extern int         g_corr_trace_enabled;
extern unsigned    g_corr_entry_count;
extern unsigned    g_corr_entry_log_cap;
extern uint64_t    g_corr_read_total;
extern uint16_t    g_corr_last_pc;
extern int g_fbdb_probe_enabled;
extern unsigned g_fbdb_probe_log_cap;
extern unsigned g_fbdb_probe_count_b;
extern unsigned g_fbdb_probe_count_a;
extern unsigned g_fbdb_probe_count_fbf3;
extern unsigned g_addr3dc0_wr_count;
extern unsigned g_addr3dc0_rd_count;
extern StuckHistEntry g_stuck_hist[STUCK_HIST_SIZE];
extern unsigned g_stuck_hist_used;
extern int g_stuck_probe_enabled;
extern int g_stuck_active;
extern uint32_t g_stuck_duration;
extern uint64_t g_stuck_start_insn;
extern unsigned g_stuck_dump_count;
extern int g_force_intm_oneshot_enabled;
extern int g_force_intm_oneshot_done;
extern uint64_t g_force_intm_oneshot_insn;
extern uint16_t g_force_intm_at_pc;
extern Int3BranchEvent g_int3_trace[INT3_BRANCH_TRACE_MAX];
extern unsigned g_int3_trace_count;
extern int g_int3_trace_overflow;
extern int g_int3_cycle_active;
extern uint64_t g_int3_cycle_id;
extern uint16_t g_int3_cycle_entry_pc;
extern uint64_t g_int3_cycle_entry_insn;
extern int g_int3_trace_enabled;
extern struct c54x_g_throughput_s g_throughput;
extern struct c54x_g_read_stats_s g_read_stats;
extern XferLog   g_xfer_ring[NOP_RING_N];
extern unsigned  g_xfer_idx;
extern AWriteLog g_awrite_ring[NOP_RING_N];
extern unsigned  g_awrite_idx;
extern int       g_nop_tripped;
extern int      g_fbwatch_on;
extern struct c54x_g_rmap_s g_rmap[RMAP_PCS];
extern int      g_rmap_n;
extern uint32_t g_rmap_tot;
extern int      g_rmap_on;
extern uint16_t g_rmap_pclo, g_rmap_pchi;
extern StkwEv   g_stkw_ring[STKW_RING_N];
extern unsigned g_stkw_idx;
extern int      g_orphan_on;
extern FILE *g_flow_f;
extern long  g_flow_budget;
extern int   g_flow_armed;
extern struct c54x_g_wmap_s g_wmap[WMAP_PCS];
extern int      g_wmap_n;
extern uint32_t g_wmap_tot;
extern int      g_wmap_on;
extern uint16_t g_wmap_lo, g_wmap_hi, g_wmap_lo2, g_wmap_hi2;
extern int      g_dio_on;
extern uint64_t g_dio_after;
extern unsigned g_dio_n;
extern uint16_t g_dio_pclo, g_dio_pchi;
extern struct c54x_g_sp_ledger_s g_sp_ledger;
extern uint16_t pc_ring[256];
extern int pc_ring_idx;
extern bool g_frame_it_level;
extern struct c54x_g_sp_trail_s g_sp_trail[256];
extern unsigned g_sp_trail_idx;
extern uint16_t g_sp_low;
extern uint16_t g_sp_low_pc;
extern unsigned g_sp_low_hits_at_pc;
extern unsigned g_sp_low_distinct_pcs;
extern SpDecEntry g_sp_dec_hist[SP_HIST_MAX];
extern unsigned   g_sp_dec_used;
extern unsigned   g_sp_dec_total_events;
extern uint16_t   g_sp_dec_arm_threshold;
extern uint16_t   g_sp_dec_dump_threshold;
extern int        g_sp_dec_enabled;
extern int        g_sp_dec_armed;
extern int        g_sp_dec_dumped;
extern unsigned   g_sp_dec_arm_insn;
extern uint16_t   g_sp_dec_arm_sp;
extern SpRingEntry g_sp_ring[SP_RING_SZ];
extern unsigned    g_sp_ring_head;
extern uint64_t    g_sp_ring_total;
extern int         g_sp_ring_enabled;
extern unsigned    g_sp_ring_dump_count;
extern unsigned    g_sp_ring_dump_max;
extern int         g_sp_ring_trig_mode;
extern unsigned    g_sp_ring_insn_min;
extern int g_bootstub_dumped;
extern bool g_c54x_early_booted;
extern uint32_t g_arm_taskmd5_insn;
extern uint16_t g_arm_taskmd5_ea;

/* ---- functions shared between the core files ---- */
uint16_t prog_fetch(C54xState *s, uint16_t pc);
uint16_t prog_read(C54xState *s, uint32_t addr);
uint16_t c54x_ovly_bas(void);

/* Inline prog_fetch for the hot path (5.5 calls per instruction, each
 * doubled by a call to c54x_ovly_bas). EXACT copy of prog_fetch +
 * c54x_prog_xlate (c54x_mem.c): OVLY alias to data[] above the floor, window
 * 0x8000-0xDFFF banked by XPC, the rest unbanked. The OVLY floor is read from
 * g_c54x_ovly_bas, set by the first call of c54x_ovly_bas() (which keeps its
 * arming message at the same instant). The prog_fetch function stays exported
 * (pont.c calls it); c54x_mem.c defines it after an #undef. */
extern uint16_t g_c54x_ovly_bas;   /* 0 = not resolved yet */
/* 0xE000-0xFFFF banked in pages 1 and 2 (32K PROM1/PROM2), see c54x_prog_xlate */
extern int g_c54x_xpc_haut;        /* -1 = not resolved yet */
int c54x_xpc_haut_init(void);
static inline int c54x_xpc_haut_banque(uint16_t xpc)
{
    if ((xpc & 0x7f) != 1 && (xpc & 0x7f) != 2) return 0;
    return __builtin_expect(g_c54x_xpc_haut >= 0, 1) ? g_c54x_xpc_haut : c54x_xpc_haut_init();
}
static inline uint16_t c54x_prog_fetch_vite(C54xState *s, uint16_t pc)
{
    if (s->pmst & PMST_OVLY) {
        uint16_t bas = __builtin_expect(g_c54x_ovly_bas != 0, 1)
                       ? g_c54x_ovly_bas : c54x_ovly_bas();
        if (pc >= bas && pc < 0x2800)
            return s->data[pc];
    }
    if (pc >= 0x8000 && (pc < 0xE000 || c54x_xpc_haut_banque(s->xpc)))
        return s->prog[(((uint32_t)s->xpc << 16) | pc) & (C54X_PROG_SIZE - 1)];
    return s->prog[pc];
}
#define prog_fetch(s, pc) c54x_prog_fetch_vite((s), (pc))

void a_track_init_lazy(void);
void a_track_iter(C54xState *s, uint16_t prev_pc, uint16_t prev_op);
void ar6_at_init_lazy(void);
void ar6_at_iter(C54xState *s, uint16_t prev_pc, uint16_t prev_op);
void awrite_log_push(uint16_t pc, uint8_t xpc, uint16_t op, int64_t old_a, int64_t new_a, uint64_t insn);
int c54x_exec_one(C54xState *s);
void c54x_fire_tint(C54xState *s);
void c54x_ifr_clear(C54xState *s, uint16_t mask, const char *site);
bool calypso_fix_enabled(const char *name);
const char *classify_xfer_op(uint16_t op);
void corr_entry_track(uint16_t pc, void *s_void);
void corr_read_dump(const char *trig);
void corr_trace_init_lazy(void);
uint16_t data_read(C54xState *s, uint16_t addr);
void data_write(C54xState *s, uint16_t addr, uint16_t val);
void fbdb_probe_check_pc(uint16_t pc, void *s_void);
void force_intm_oneshot_check(C54xState *s);
bool frame_it_level_on(void);
bool frame_it_prio_on(void);
void int3_cycle_end_good(C54xState *s, uint16_t return_addr);
void int3_cycle_start(C54xState *s, uint16_t target_pc);
void int3_cycle_track_branch(C54xState *s, uint16_t exec_pc, uint16_t exec_op, int consumed);
void mvpd_trace_dump_if_due(unsigned insn);
void mvpd_trace_init_lazy(void);
void nop_guard_dump(C54xState *s, uint16_t pc, uint8_t xpc);
int pc_in_nop_region(const C54xState *s, uint16_t pc, uint8_t xpc);
void stuck_probe_check(C54xState *s);
void xfer_log_push(uint16_t src_pc, uint8_t src_xpc, uint16_t op, uint16_t tgt_pc, uint8_t tgt_xpc, int64_t a_val, uint64_t insn);

void ar_write_track(C54xState *s, unsigned idx, uint16_t new_val);
uint16_t c54x_circ_ref(uint16_t ar, int step, uint16_t bk);
bool c54x_cond_true(C54xState *s, uint8_t cc);
bool c54x_irq_level_check(C54xState *s);
void c54x_par_postmod(C54xState *s, int ar, int mod);
uint32_t c54x_prog_xlate(const C54xState *s, uint16_t addr16);
void corr_read_record(uint16_t addr);
void fbdb_probe_read_3dc0(uint16_t addr, uint16_t val, uint16_t pc, unsigned insn);
void fbdb_probe_write_3dc0(uint16_t addr, uint16_t old_val, uint16_t new_val, uint16_t pc, unsigned insn);
void mvpd_trace_record(uint16_t addr);
void read_stats_record(uint16_t addr);
void read_stats_trigger_check(C54xState *s);
uint16_t resolve_lmem(C54xState *s, uint16_t opcode);
uint16_t resolve_mmr(C54xState *s, uint16_t opcode);
uint16_t resolve_smem(C54xState *s, uint16_t opcode, bool *indirect);
uint16_t resolve_xmem(C54xState *s, uint16_t op);
void rsbx_intm_check(C54xState *s, uint16_t op);
void scratchwr_note(C54xState *s, uint16_t a, uint16_t v, const char *espace);
void sp_abs_track(C54xState *s, uint16_t new_val, uint8_t site);
void st0_ring_rec(C54xState *s, uint16_t val, char kind);
void throughput_tick(uint64_t insn_count);
bool watch_write_zone_check(C54xState *s, uint16_t addr, uint16_t val, const char *name, uint16_t lo, uint16_t hi, WatchWriteState *st);

void prog_write(C54xState *s, uint32_t addr, uint16_t val);

uint16_t c54x_revcarry(C54xState *s, uint16_t ar, uint16_t ar0, int sub);

#endif /* CALYPSO_C54X_INTERNAL_H */

4.44 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/c54x_irq.c

12565 octets, 239 lignes → 239 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * c54x_irq.c - interrupts: IFR/IMR, frame IT, level hold.
 *
 * Split out of calypso_c54x.c on 2026-09-18 (one file per role).
 * File map in c54x_internal.h.
 */
#include "c54x_internal.h"
#include "hw/arm/calypso/calypso_debug.h"

void calypso_inth_arm_ack(void);
/* @BEQUILLE - FRAME_IT_LEVEL  (CALYPSO_FRAME_IT_LEVEL, EQ1, default OFF)
 *   masque  : the firmware's INTM=0 window, too rare to catch the frame IT.
 *             Re-asserts IFR bit 12 on EVERY instruction until vector 28 is
 *             taken, because the c54x IFR latches events and has no level mode.
 *   retirer : once the firmware's INTM duty cycle is wide enough to catch the
 *             IT as it comes.
 */
bool frame_it_level_on(void)
{
    static int c = -1;
    if (c < 0) { const char *e = calypso_getenv("CALYPSO_FRAME_IT_LEVEL"); c = (e && *e == '1') ? 1 : 0; }
    return c;
}
/* @BEQUILLE - FRAME_IT_PRIO  (CALYPSO_FRAME_IT_PRIO, EQ1, default OFF)
 *   masque  : interrupt priority. Forces b=12 instead of ctz(pend) so the frame
 *             IT wins over BRINT0/bit 5; on the c54x, priority is fixed by the
 *             vector number and is not configurable.
 *   retirer : once BRINT0 and the frame IT no longer compete for the same
 *             window (BSP delivery at the right rate).
 */
bool frame_it_prio_on(void)
{
    static int c = -1;
    if (c < 0) { const char *e = calypso_getenv("CALYPSO_FRAME_IT_PRIO"); c = (e && *e == '1') ? 1 : 0; }
    return c;
}

bool c54x_irq_level_check(C54xState *s)
{
    static int en = -1;
    if (en < 0) { const char *_d = calypso_getenv("CALYPSO_DSP"); en = (calypso_getenv("CALYPSO_C54X_IRQ_LEVEL") || (_d && !strcmp(_d, "c54x"))) ? 1 : 0; }
    if (!en) return false;
    /* Level hold: keep bit 12 pending in the IFR until the frame IT has been
     * vectored (vec 28), so the next INTM=0 window picks it up. */
    if (g_frame_it_level && frame_it_level_on()) {
        s->ifr |= (1u << 12);
    }
    /* [2026-08-04] INT10n (bit 14) is a LEVEL line: the DMA line.
     *
     * CAL000 5.1 gives the sense of each line:
     *     INT0n  (level) -> RIF receive        INT8n  (edge) -> TPU frame
     *     INT1n  (level) -> RIF transmit       INT9n  (edge) -> TPU programmable
     *     INT10n (level) -> DMA interrupt      INT7n  (edge) -> CYPHER
     * Posting bit 14 once on the edge loses the event whenever INTM is 1 at
     * that instant: over the 15 logged vec=30 requests, IMR=0x52ed (bit 14
     * unmasked) and IFR=0x4000 (bit 14 pending) with INTM=1 all 15 times, and
     * "IRQ-LEVEL take" never fires. The DSP then never services DMA
     * completion, its internal queues overflow (116 writes at 0x434e/0x434f)
     * and it raises DSP_ERR_DMA_PEND.
     *
     * The level source is the channel IRQ_STATE, which CAL207 11.3.5 describes
     * as "cleared after being read": the line drops when the firmware reads the
     * register, as on silicon. */
    if (calypso_rhea_dma_irq_level()) {
        s->ifr |= (1u << C54X_IT_DMA_BIT);
    }
    /* Pure probes LEVELCHK-DBG, LEVELCHK-EMPIRICAL, LEVELCHK-WINDOW (called
     * at every instruction): behind the C54X_SONDES switch. The interrupt
     * take that follows is unaffected. */
    if (C54X_SONDES) {
    /* [2026-07-22] LEVELCHK-DBG (gated CALYPSO_AR0_DEBUG): with IMR != 0 (window
     * armed), report which gate keeps the frame IT from being taken - INTM vs
     * IPTR vs pend=0. */
    {
        static int lcdbg = -1;
        if (lcdbg < 0) lcdbg = calypso_gate("CALYPSO_AR0_DEBUG", 0);
        if (lcdbg && s->imr && s->insn_count > 4000) {   /* skip boot-reset noise */
            static unsigned lc = 0;
            uint16_t iptr = (s->pmst >> PMST_IPTR_SHIFT) & 0x1FF;
            uint16_t pend = (uint16_t)(s->ifr & s->imr);
            if (lc++ < 150)
                fprintf(stderr, "[c54x] LEVELCHK-DBG INTM=%d delay=%d IPTR=0x%03x "
                        "IFR=0x%04x IMR=0x%04x pend=0x%04x PC=0x%04x insn=%u -> %s\n",
                        !!(s->st1 & ST1_INTM), s->delay_slots, iptr,
                        s->ifr, s->imr, pend, s->pc, s->insn_count,
                        (s->st1 & ST1_INTM) ? "BLOCK:INTM" :
                        s->delay_slots ? "BLOCK:delay" :
                        (iptr == 0x1FF) ? "BLOCK:IPTR=0x1FF" :
                        (!pend) ? "BLOCK:pend=0(IFR&IMR)" : "WOULD-TAKE!");
        }
    }
    /* [2026-07-23] LEVELCHK-EMPIRICAL (unconditional, capped): "IRQ-LEVEL take"
     * never fires in native runs although INTM-TRANS shows IFR=0x1020/0x1030
     * (bit 5 BRINT0 + bit 12 frame pending) right at the INTM 1->0 (RETE)
     * moments. Traces every early-return path of this function so the blocking
     * gate is observed rather than inferred. */
    {
        static unsigned _lcn = 0;
        static uint32_t _last_insn = 0xFFFFFFFFu;
        bool _intm = !!(s->st1 & ST1_INTM);
        bool _delay = s->delay_slots != 0;
        uint16_t _iptr = (s->pmst >> PMST_IPTR_SHIFT) & 0x1FF;
        uint16_t _pend = (uint16_t)(s->ifr & s->imr);
        /* [2026-07-23] DEDUP: while RPT is active the same instruction is
         * re-executed hundreds of times at the same PC WITHOUT insn_count
         * advancing, which burns the whole cap on one repeat loop. Log only on
         * a new insn_count so the cap covers distinct instructions. */
        if (_pend && _iptr != 0x1FF && _lcn < 5000 && s->insn_count != _last_insn) {
            _last_insn = s->insn_count;
            _lcn++;
            fprintf(stderr, "[c54x] LEVELCHK-EMPIRICAL #%u PC=0x%04x INTM=%d delay=%d "
                    "IPTR=0x%03x IFR=0x%04x IMR=0x%04x pend=0x%04x insn=%u idle=%d "
                    "rpt_active=%d rpt_count=%u -> %s\n",
                    _lcn, s->pc, _intm, _delay, _iptr, s->ifr, s->imr, _pend, s->insn_count, s->idle,
                    s->rpt_active, s->rpt_count,
                    _intm ? "BLOCKED:INTM=1" : _delay ? "BLOCKED:delay_slots" :
                    (_iptr == 0x1FF) ? "BLOCKED:IPTR=0x1FF" : "WOULD-DISPATCH");
        }
    }
    /* [2026-07-30] LEVELCHK-WINDOW - count the window, not the boring case.
     *
     * LEVELCHK-EMPIRICAL is capped at 5000 lines and burns them all on
     * BLOCKED:INTM=1 before insn=14M, while the question is the opposite one:
     * when INTM drops, is the pending IT seen, and is it taken? Measured
     * 2026-07-30: "IRQ-LEVEL take" = 0 over 52M instructions for 739 traced
     * INTM 1->0 transitions, with pend=0x1020 (bit 5 BRINT0 + bit 12 frame)
     * permanently set; the window lasts about one instruction (1->0 at
     * insn 187431877, 0->1 at 187431878).
     *
     * Counters, not lines: the summary shows at a glance whether the window is
     * even SEEN here. w_intm0 staying at 0 while INTM-TRANS counts 1->0
     * transitions means this function is not called inside the window - an
     * ordering problem in the execution loop, not a guard. */
    {
        static unsigned long long w_calls = 0, w_intm0 = 0, w_pend = 0, w_ready = 0;
        static unsigned w_log = 0;
        uint16_t _p = (uint16_t)(s->ifr & s->imr);
        uint16_t _ip = (uint16_t)((s->pmst >> PMST_IPTR_SHIFT) & 0x1FF);
        w_calls++;
        if (!(s->st1 & ST1_INTM)) {
            w_intm0++;
            if (_p) {
                w_pend++;
                if (s->delay_slots == 0 && _ip != 0x1FF) w_ready++;
                if (w_log < 20) {
                    w_log++;
                    fprintf(stderr, "[c54x] LEVELCHK-WINDOW #%u FENETRE OUVERTE "
                            "PC=0x%04x pend=0x%04x IFR=0x%04x IMR=0x%04x delay=%d "
                            "IPTR=0x%03x insn=%u -> %s\n",
                            w_log, s->pc, _p, s->ifr, s->imr, s->delay_slots, _ip,
                            s->insn_count,
                            (s->delay_slots == 0 && _ip != 0x1FF) ? "PRISE"
                              : (s->delay_slots ? "BLOQUE:delay" : "BLOQUE:IPTR=0x1FF"));
                }
            }
        }
        if ((w_calls % 2000000ULL) == 0)
            fprintf(stderr, "[c54x] LEVELCHK-WINDOW resume : appels=%llu intm0=%llu "
                    "intm0+pend=%llu prises=%llu insn=%u\n",
                    (unsigned long long)w_calls, (unsigned long long)w_intm0,
                    (unsigned long long)w_pend, (unsigned long long)w_ready,
                    s->insn_count);
    }
    }   /* C54X_SONDES : LEVELCHK-* */

    if ((s->st1 & ST1_INTM) || s->delay_slots != 0) return false;
    /* Do not vector until the ROM has relocated IPTR: reset value 0x1ff puts the
     * vector table at 0xff80, which is garbage. Wait for a relocated IPTR
     * (typically 0x001). */
    if (((s->pmst >> PMST_IPTR_SHIFT) & 0x1FF) == 0x1FF) return false;
    uint16_t pend = (uint16_t)(s->ifr & s->imr);
    if (!pend) return false;
    int b = __builtin_ctz(pend);          /* lowest set bit = highest priority */
    /* Priority: the frame IT (bit 12 / vec 28) outranks lower bits such as
     * BRINT0 (bit 5), which would steal the rare window and re-mask INTM. */
    if (frame_it_prio_on() && (pend & (1u << 12))) {
        b = 12;
    }
    int vec = b + 16;                     /* C54x: maskable IMR bit b -> vector b+16 */
    /* Bit 3 is the DSP timer: the frame IT is raised on bit 12 / vector 28 at
     * the source, so vec = b + 16 holds with no exception. */
    c54x_ifr_clear(s, (uint16_t)(1u << b), "vector-level");
    if (b == 12) g_frame_it_level = false;   /* frame IT vectored: release the level hold */
    s->sp--; data_write(s, s->sp, (uint16_t)s->pc);
    /* [2026-07-22] Push XPC ONLY in extended mode (xpc != 0), as a standard c54x
     * does. The firmware leaves the ISR through POPM ST1 + RCD (pops 1 word =
     * PC), not RETE (pops 2). With xpc == 0 (no paging), pushing XPC leaves an
     * orphan word that is never popped: SP drifts +1 per IT until it wraps, the
     * bootstub RET at 0xab38 pops mem[0x5ac8]=0 instead of mem[0x5ac7], and the
     * PC=0 storm follows. Legacy behaviour: CALYPSO_IT_PUSH_XPC_ALWAYS=1. */
    {
        static int always = -1;
        if (always < 0) { const char *e = calypso_getenv("CALYPSO_IT_PUSH_XPC_ALWAYS");
                          always = (e && *e != 0) ? 1 : 0; }
        if (always || s->xpc != 0) { s->sp--; data_write(s, s->sp, s->xpc); }
    }
    s->st1 |= ST1_INTM;
    s->xpc = 0;
    uint16_t iptr = (s->pmst >> PMST_IPTR_SHIFT) & 0x1FF;
    s->pc = (uint16_t)((iptr * 0x80) + vec * 4);
    g_c54x_it_prises++;   /* ANNEAU-EXEC diagnostic */
    static unsigned lvln = 0;
    if (lvln++ < 60)
        fprintf(stderr, "[c54x] IRQ-LEVEL take bit=%d vec=%d -> PC=0x%04x "
                "IPTR=0x%03x IMR=0x%04x IFR=0x%04x insn=%u\n",
                b, vec, s->pc, iptr, s->imr, s->ifr, s->insn_count);
    return true;
}


/* [2026-07-28] Unified gate for emulation fixes.
 *   CALYPSO_FIXES=FIX_ONE,FIX_TWO   enables the named fixes
 *   CALYPSO_FIXES=all               enables them all
 *   (unset)                         none; original behaviour strictly unchanged
 *
 * Protocol: land all safe fixes behind this gate at once, test under full load
 * (camp + LU + SMS, not a bare boot), and once a fix is confirmed delete the
 * CONDITION, not the fix - drop the `if (calypso_fix_enabled("FIX_..."))` and
 * its braces so the code becomes unconditional and the name leaves the gate
 * list. This is a temporary airlock, never a configuration option: an airlock
 * empties. Never let a validated fix grow old in it. */
/* [2026-08-04] FIX_F4XX_SRCDST - the F4xx/F5xx family had src and dst swapped.
 *
 * TI SPRU172C (Mnemonic Instruction Set, March 2001), instruction pages:
 *     ADD form 9 : 15..10 = 111101   bit9 = S   bit8 = D   7..5 = 000  SHIFT
 *     LD  form   : 15..10 = 111101   bit9 = S   bit8 = D   7..5 = 010  SHIFT
 *     (same field layout for SUB, SFTA, SFTL, NEG, ABS, MACA, *,ASM,*)
 * So bit 9 = SRC and bit 8 = DST, as in the F0-F3 family (OR form 4), where the
 * one-word block already had it right. 22 handlers had the opposite assignment
 * (`src = (op>>8)&1, dst = (op>>9)&1`).
 *
 * The blast radius is why the escape gate exists: CALYPSO_FIX_F4XX_SRCDST=0
 * restores the pre-2026-08-04 behaviour exactly, to isolate a regression
 * without touching code. Default 1: the fix matches the vendor documentation.
 *
 * WARNING: not validated under load. No bench currently combines "c54x active"
 * with "full LU" (shunt_legit forces CALYPSO_DSP_RUN_C54X=0). Remove the gate
 * only once a camp->LU run has exercised it. */

4.45 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/c54x_mem.c

207091 octets, 3987 lignes → 3980 lignes (4 groupes compactés)

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * c54x_mem.c - data and program memory: data_read/write, prog_fetch, overlay.
 *
 * Split out of calypso_c54x.c on 2026-09-18; file map in c54x_internal.h.
 */
#include "c54x_internal.h"
#include "hw/arm/calypso/calypso_debug.h"
#include "calypso_bsp.h"

static uint16_t data_read_locked(C54xState *s, uint16_t addr);
/* DEMODIO: forward declaration - the helper is defined further down, but its
 * caller data_read comes first. */
static void dio_note(C54xState *s, const char *rw, uint16_t addr, uint16_t val);

/* FBWATCH: FB-dispatch probe behind its own env (CALYPSO_FBWATCH=1), resolved
 * once into a static int so the hot path costs one int test. Deliberately NOT
 * routed through calypso_debug_enabled: the master gate must stay 0 to keep the
 * 127 debug gates short-circuited and QEMU real-time. Declared here because
 * data_read_locked uses it. Watches page reads, d_fb_det and the canary. */
int      g_fbwatch_on = -1;

uint16_t data_read(C54xState *s, uint16_t addr)
{
    /* Mailbox monitor: logs the value as found in memory on entry. The few cells
     * synthesized further down (FB-STREAM) show up on the write side anyway.
     * Placed BEFORE the fast path: c54x_exe runs with c54x_rapide and
     * CALYPSO_MAILBOX=1 must still produce its file. Monitor off, the cost is
     * one int load (calypso_mbx_actif) per access. */
    calypso_mbx(MBX_DSP_RD, addr, s->data[addr], 0, s->pc, 0, s->insn_count);
    if (c54x_rapide)                       /* see calypso_c54x.h */
        return data_read_locked(s, addr);

    /* FEED-DST - which buffer does the demod actually read? Gate
     * CALYPSO_FEED_DST (default 0), read-only, capped.
     *
     * [2026-08-04] The DMA delivers real IQ (295/296 non-zero words in steady
     * state) yet the DSP still publishes 89 % zeros in a_cd, so check whether the
     * demod reads where the DMA writes before blaming the opcode decoder.
     *
     *   zone A = 0x0cce..0x0df5: DMA destination (AAD=0x99c -> api_ram[0x4ce],
     *            DSP word 0x0800+0x4ce), where the real IQ lands.
     *   zone B = 0x4c00..0x4d27: correlator buffer (CALYPSO_BSP_DARAM_ADDR).
     *
     * A>0 and B=0 -> the demod reads the DMA, the fault is downstream
     * (decoder/ALU). A=0 and B>0 -> it reads a buffer the DMA never feeds, which
     * explains the zeros with no ALU bug. Both 0 -> neither, and the question
     * changes.
     *
     * Counts READS, not frames: an A/B ratio says how many words were read
     * where, not which buffer the demod prefers. The listed PCs are the first 8
     * distinct ones seen, not the most frequent. */
    {
        static int fd_on = -1;
        if (fd_on < 0) {
            fd_on = calypso_gate("CALYPSO_FEED_DST", 0);
            /* Arming trace: without it a silent probe is indistinguishable
             * from a disabled one. ARMEE with no A/B line after it means the
             * demod reads neither 0x0cce nor 0x4c00 - a result, not a failure. */
            fprintf(stderr, "[c54x] FEED-DST %s (CALYPSO_FEED_DST=%d) — "
                    "A=0x0cce..0x0df5 (dest DMA), B=0x4c00..0x4d27 (correlateur)\n",
                    fd_on ? "ARMEE" : "eteinte", fd_on);
        }
        if (fd_on) {
            int zone = (addr >= 0x0cce && addr <= 0x0df5) ? 0
                     : (addr >= 0x4c00 && addr <= 0x4d27) ? 1 : -1;
            if (zone >= 0) {
                static unsigned long long n_rd[2];
                static uint16_t pcs[2][8];
                static unsigned  pcn[2][8];
                static int       npc[2];
                n_rd[zone]++;
                int k;
                for (k = 0; k < npc[zone]; k++)
                    if (pcs[zone][k] == s->pc) { pcn[zone][k]++; break; }
                if (k == npc[zone] && npc[zone] < 8) {
                    pcs[zone][k] = s->pc; pcn[zone][k] = 1; npc[zone]++;
                }
                unsigned long long tot = n_rd[0] + n_rd[1];
                static unsigned n_print;
                if ((tot == 1 || (tot % 20000) == 0) && n_print < 20) {
                    n_print++;
                    fprintf(stderr, "[c54x] FEED-DST A(DMA 0x0cce)=%llu "
                            "B(corr 0x4c00)=%llu\n", n_rd[0], n_rd[1]);
                    for (int z = 0; z < 2; z++) {
                        fprintf(stderr, "[c54x] FEED-DST   %s PC:",
                                z == 0 ? "A" : "B");
                        for (int j = 0; j < npc[z]; j++)
                            fprintf(stderr, " 0x%04x(%u)", pcs[z][j], pcn[z][j]);
                        fprintf(stderr, "%s\n", npc[z] ? "" : " (aucune lecture)");
                    }
                    fflush(stderr);
                }
            }
        }
    }

    /* FB-STREAM (gates CALYPSO_FB_STREAM, CALYPSO_FB_STREAM_CELL / _CELLQ):
     * injects a fresh FCCH sample on every read of the demod sample cell, which
     * gives 0x2a00 a real window independent of cadence; models the on-chip DMA.
     * The I/Q cells are configurable because WATCH-9F00-RD measured the demod
     * reading 0x9260/0x9261, not the 0x9213/0x9215 default. */
    static uint16_t _fscI = 0, _fscQ = 0;
    if (_fscI == 0) {
        const char *c = calypso_getenv("CALYPSO_FB_STREAM_CELL");  _fscI = c ? (uint16_t)strtol(c, NULL, 0) : 0x9213;
        const char *q = calypso_getenv("CALYPSO_FB_STREAM_CELLQ"); _fscQ = q ? (uint16_t)strtol(q, NULL, 0) : 0x9215;
    }
    if (s->pc >= 0x9f00 && s->pc <= 0x9fb8 && (addr == _fscI || addr == _fscQ)) {
        static int _fs = -1;
        /* @BEQUILLE - FB_STREAM (read side)  (CALYPSO_FB_STREAM, default OFF)
         *   masque  : the missing on-chip DMA. Injects a fresh sample on every
         *             read of the cell instead of a peripheral filling the
         *             buffer.
         *   retirer : once the buffer is fed by the real path (BSP -> DARAM).
         *   Note: inert with CORR_ENTRY=0x94f5 - cells 0x9260/61 are never read
         *   there (measured 2026-07-28 by WATCH-9F00-RD). */
        if (_fs < 0) _fs = calypso_gate("CALYPSO_FB_STREAM", 0);
        if (_fs) {
            static uint16_t _si, _sq; static int _hv = 0; uint16_t _rv;
            if (addr == _fscI) { _hv = calypso_bsp_fb_stream_next(&_si, &_sq) ? 1 : 0; _rv = _hv ? _si : s->data[addr]; }
            else { _rv = _hv ? _sq : s->data[addr]; }
            static unsigned _sl = 0;
            if (_sl++ < 24)
                fprintf(stderr, "[c54x] FB-STREAM addr=0x%04x -> 0x%04x (cell 0x%04x) hv=%d PC=0x%04x\n",
                        addr, _rv, s->data[addr], _hv, s->pc);
            return _rv;
        }
    }
    if (s->pc >= 0x9f00 && s->pc <= 0x9fb8) {
        static int _r9 = -1;
        if (_r9 < 0) _r9 = calypso_gate("CALYPSO_WATCH_9F00_RD", 0);
        if (_r9) {
            /* WATCH-9F00-RD (gate CALYPSO_WATCH_9F00_RD): the demod stage at
             * 0x9f00..0x9fb8 writes its result into the 0x2a00 workzone, so its
             * input is whatever it reads. Logs every read on that path, with no
             * exclusion, to locate the real source I/Q buffer. */
            static unsigned _n9 = 0;
            if (_n9++ < 200)
                fprintf(stderr, "[c54x] WATCH-9F00-RD PC=0x%04x reads addr=0x%04x val=0x%04x insn=%u\n",
                        s->pc, addr, s->data[addr], s->insn_count);
        }
    }
    /* Correlator read tracer (env-gated CALYPSO_CORRELATOR_TRACE=1): records
     * addr only while PC is in [CORR_PC_LO..CORR_PC_HI), the FB-det range.
     * Cost when OFF: one compare plus one branch. */
    if (g_corr_trace_enabled > 0 && s->pc >= CORR_PC_LO && s->pc < CORR_PC_HI) {
        corr_read_record(addr);
    }
    /* IQ-READ tracer: who reads the BSP DMA buffer [0x2a00..0x2b27]? Confirms
     * that the FB correlator consumes the real I/Q the BSP wrote, and at which
     * PC (the actual correlator site). Cap 60, ~zero cost outside the zone. */
    if (addr >= 0x2a00 && addr < 0x2b28 && s->data[addr] != 0) {
        static unsigned iqr = 0, iqseen = 0;
        iqseen++;
        /* Boot (first 60) plus 1-in-8000 past insn>50M, so the sample covers
         * what the correlator reads at FB-det time (insn ~71M) and not just the
         * stale boot buffer. */
        if (iqr < 60 || (s->insn_count > 50000000u && (iqseen % 8000) == 0)) {
            uint16_t val = s->data[addr];
            /* A/B (complex correlation accumulators, 40-bit sign-extended)
             * plus the values under the other pointers during the I/Q read. */
            int64_t a = (s->a & 0x8000000000LL) ? (int64_t)(s->a | ~0xFFFFFFFFFFLL) : (int64_t)s->a;
            int64_t b = (s->b & 0x8000000000LL) ? (int64_t)(s->b | ~0xFFFFFFFFFFLL) : (int64_t)s->b;
            fprintf(stderr, "[c54x] IQ-READ #%u addr=0x%04x val=0x%04x PC=0x%04x A=%lld B=%lld "
                    "T=%04x s=%p | AR3=%04x[%04x] AR4=%04x[%04x] AR5=%04x[%04x] insn=%u\n",
                    iqr, addr, val, s->pc, (long long)a, (long long)b, s->t, (void*)s,
                    s->ar[3], s->data[s->ar[3]], s->ar[4], s->data[s->ar[4]],
                    s->ar[5], s->data[s->ar[5]], s->insn_count);
            iqr++;
        }
        /* SPAN write-vs-read: at detection time, dump once the contiguous span
         * data[0x2a00..0x2a1f] (what the correlator can read) and bsp_buf[0..31]
         * (what the BSP wrote).
         *   both waveform    -> buffer fine, bug is AR3/correlator (reads [0] only)
         *   span [0] then DC -> PORTR delivery copies only [0] (stride/len)
         *   bsp_buf DC       -> the BSP cs16 conversion truncates. */
        static int span_done = 0;
        if (!span_done && s->insn_count > 60000000u) {
            span_done = 1;
            fprintf(stderr, "[c54x] SPAN-READ  data[0x2a00..0x2a1f] insn=%u:", s->insn_count);
            for (int _i = 0; _i < 32; _i++) fprintf(stderr, " %04x", s->data[0x2a00 + _i]);
            fprintf(stderr, "\n[c54x] SPAN-WRITE bsp_buf[0..31] (bsp_len=%d):", s->bsp_len);
            for (int _i = 0; _i < 32 && _i < s->bsp_len; _i++) fprintf(stderr, " %04x", s->bsp_buf[_i]);
            fprintf(stderr, "\n");
        }
    }
    /* MTTCG: guards DARAM access, since the DSP and ARM-OVLY can race.
     * Without MTTCG the mutex is uncontended and costs almost nothing. */
    qemu_mutex_lock(&calypso_pcb_daram_lock);
    uint16_t v = data_read_locked(s, addr);
    qemu_mutex_unlock(&calypso_pcb_daram_lock);
    dio_note(s, "R", addr, v);
    return v;
}

static void flow_log(const char *rw, uint16_t addr, uint16_t val, uint16_t pc, unsigned insn);

/* RMAP: aggregated map of the addresses READ by the PCs of a range. Mirror of
 * WMAP. */
struct c54x_g_rmap_s g_rmap[RMAP_PCS];
int      g_rmap_n;
uint32_t g_rmap_tot;
int      g_rmap_on = -1;
uint16_t g_rmap_pclo, g_rmap_pchi;

static void rmap_dump(void)
{
    fprintf(stderr, "[c54x] RMAP PC 0x%04x..0x%04x  lectures=%u  PCs=%d%s\n",
            g_rmap_pclo, g_rmap_pchi, g_rmap_tot, g_rmap_n,
            g_rmap_n >= RMAP_PCS ? "  *** SATUREE ***" : "");
    for (int i = 0; i < g_rmap_n; i++) {
        fprintf(stderr, "[c54x] RMAP   PC=0x%04x n=%-7u lit 0x%04x..0x%04x  ex:",
                g_rmap[i].pc, g_rmap[i].n, g_rmap[i].amn, g_rmap[i].amx);
        for (int k = 0; k < g_rmap[i].na && k < 8; k++)
            fprintf(stderr, " %04x", g_rmap[i].a[k]);
        fprintf(stderr, "\n");
    }
}

static void rmap_note(uint16_t addr, uint16_t pc)
{
    if (g_rmap_on < 0) {
        const char *e = calypso_getenv("CALYPSO_RMAP");
        g_rmap_on = (e && atoi(e) > 0) ? 1 : 0;
        const char *lo = calypso_getenv("CALYPSO_RMAP_PCLO"), *hi = calypso_getenv("CALYPSO_RMAP_PCHI");
        g_rmap_pclo = lo ? (uint16_t)strtoul(lo, NULL, 0) : 0x9f00;
        g_rmap_pchi = hi ? (uint16_t)strtoul(hi, NULL, 0) : 0x9fff;
        if (g_rmap_on)
            fprintf(stderr, "[c54x] RMAP armed PC 0x%04x..0x%04x\n", g_rmap_pclo, g_rmap_pchi);
    }
    if (!g_rmap_on || pc < g_rmap_pclo || pc > g_rmap_pchi) return;

    int i;
    for (i = 0; i < g_rmap_n; i++) if (g_rmap[i].pc == pc) break;
    if (i == g_rmap_n) {
        if (g_rmap_n >= RMAP_PCS) return;
        g_rmap_n++;
        g_rmap[i].pc = pc; g_rmap[i].n = 0; g_rmap[i].na = 0;
        g_rmap[i].amn = 0xffff; g_rmap[i].amx = 0;
    }
    g_rmap[i].n++;
    if (addr < g_rmap[i].amn) g_rmap[i].amn = addr;
    if (addr > g_rmap[i].amx) g_rmap[i].amx = addr;
    if (g_rmap[i].na < 8) {
        int seen = 0;
        for (int k = 0; k < g_rmap[i].na; k++) if (g_rmap[i].a[k] == addr) { seen = 1; break; }
        if (!seen) g_rmap[i].a[g_rmap[i].na++] = addr;
    }
    if (++g_rmap_tot % 5000 == 0) rmap_dump();
}

static uint16_t data_read_locked(C54xState *s, uint16_t addr)
{
    /* All the read probes (DTASKD-WATCH .. FBWATCH-PAGE-RD: RMAP, FLOWTRACE,
     * read_stats, histograms, WATCH-READ, ...) only read and count: behind the
     * C54X_SONDES switch. The semantics (timers, MMR, API RAM, FORCE_3FAD_KERNEL
     * crutch) follow below. */
    if (C54X_SONDES) {
    {   /* DTASKD-WATCH, leg 4/4 - CALYPSO_DTASKD_WATCH=1, default 0. Read-only,
         * capped. Legs 1-2 are in calypso_trx.c, leg 3 in data_write_locked.
         *
         * [2026-08-03] Measured with CALYPSO_DISPATCH_PROBE=1: the DSP task
         * queue only ever receives one handler, 0xb5a1, 32559 times; the
         * index->handler helper 0xa9ea runs exactly once over the whole run,
         * with index 42 (disarm). Index 41 (RX arm, 0xa5cd) is never requested
         * and none of its call sites are reached, so the NB/CCCH path is never
         * queued. The question becomes whether the DSP even READS the cell where
         * the ARM drops the task: if the ROM never reads d_task_d it never learns
         * an NB task exists, and everything else follows.
         *
         * Cells (W pages, MCU->DSP side; dsp_api.h:22-23):
         *     W p0 = data[0x0800]     W p1 = data[0x0814]
         * d_task_md (data[0x0804]/[0x0818]) is watched too: it is the task the
         * DSP does consume today, hence the control witness. Without it a silent
         * leg 4 is ambiguous between "does not read d_task_d" and "reads nothing
         * in page W at all".
         *
         * Result:
         *   md read, d read     -> the DSP sees the NB task, the block is later.
         *   md read, d NOT read -> root cause: the ROM ignores d_task_d here.
         *   neither             -> page W is not read at all; go back up to the
         *                          selector 0xaad5 and whatever feeds 0xaac3. */
        static int _dw = -1;
        if (_dw < 0) {
            _dw = calypso_gate("CALYPSO_DTASKD_WATCH", 0);
            if (_dw) {
                fprintf(stderr, "[dtaskd] patte 4/4 armee (lectures DSP page W) : "
                        "d_task_d=data[0x0800]/[0x0814]  d_task_md=data[0x0804]/[0x0818]\n");
                fflush(stderr);
            }
        }
        if (_dw && (addr == 0x0800 || addr == 0x0814 ||
                    addr == 0x0804 || addr == 0x0818)) {
            static unsigned long long _nd = 0, _nmd = 0;
            int is_d = (addr == 0x0800 || addr == 0x0814);
            unsigned long long _n = is_d ? ++_nd : ++_nmd;
            if (_n <= 30 || (_n % 5000) == 0) {
                fprintf(stderr,
                        "[dtaskd] DSP<RD  %-9s data[0x%04x] = 0x%04x  "
                        "(d_task_d lus=%llu  d_task_md lus=%llu)  PC=0x%04x insn=%u\n",
                        is_d ? "d_task_d" : "d_task_md", addr, s->data[addr],
                        _nd, _nmd, s->last_exec_pc, s->insn_count);
                fflush(stderr);
            }
        }
    }
    rmap_note(addr, s->pc);
    {   /* WZREAD: reads of the MAC kernel input cell (gate CALYPSO_WZWRITE). */
        static int _wr = -1; static unsigned _wrn = 0;
        if (_wr < 0) _wr = calypso_gate("CALYPSO_WZWRITE", 0);
        if (_wr && addr == 0x2c00 && s->pc == 0xa07c && _wrn < 40) {
            /* Only the MAC kernel read matters here; 0x9aba is the
             * normalization loop, noisy and already characterized. */
            _wrn++;
            fprintf(stderr, "[c54x] WZREAD  data[0x%04x] = 0x%04x PC=0x%04x op=0x%04x "
                    "AR2=%04x AR3=%04x AR4=%04x AR5=%04x insn=%u\n",
                    addr, s->data[addr], s->pc, prog_fetch(s, s->pc),
                    s->ar[2], s->ar[3], s->ar[4], s->ar[5], s->insn_count);
        }
    }
    {   /* DMAWATCH (read side): DMA sub-register bank 0x0054..0x0057. */
        static int _dw2 = -1; static unsigned _dwr = 0;
        if (_dw2 < 0) _dw2 = calypso_gate("CALYPSO_DMAWATCH", 0);
        if (_dw2 && addr >= 0x0054 && addr <= 0x0057 && _dwr < 40) {
            _dwr++;
            const char *_nm = (addr==0x0054) ? "DMPREC?(modele:DMSA)" :
                              (addr==0x0055) ? "DMSA?(modele:DMSDI)" :
                              (addr==0x0056) ? "DMSDI?" : "DMSDN";
            fprintf(stderr, "[c54x] DMAWATCH RD 0x%04x %-20s = 0x%04x PC=0x%04x insn=%u\n",
                    addr, _nm, s->data[addr], s->pc, s->insn_count);
        }
    }
    {   /* DEMODRD: the demod sample reads at PC 0x9fb5. */
        static int _dr = -1; static unsigned _drn = 0;
        if (_dr < 0) _dr = calypso_gate("CALYPSO_DEMODRD", 0);
        if (_dr && _drn < 60 && s->pc == 0x9fb5) {   /* sample reads only */
            _drn++;
            fprintf(stderr, "[c54x] DEMODRD PC=0x%04x XPC=%u op=0x%04x lit data[0x%04x]=0x%04x "
                    "AR0=%04x AR1=%04x AR2=%04x AR3=%04x AR4=%04x AR5=%04x AR6=%04x AR7=%04x "
                    "BK=%04x | 5 mots: %04x %04x %04x %04x %04x | insn=%u\n",
                    s->pc, (unsigned)s->xpc, prog_fetch(s, s->pc), addr, s->data[addr],
                    s->ar[0], s->ar[1], s->ar[2], s->ar[3], s->ar[4], s->ar[5],
                    s->ar[6], s->ar[7], s->bk,
                    s->data[(uint16_t)(addr+0)], s->data[(uint16_t)(addr+1)],
                    s->data[(uint16_t)(addr+2)], s->data[(uint16_t)(addr+3)],
                    s->data[(uint16_t)(addr+4)], s->insn_count);
        }
    }
    {   /* SLOTSRC-RD: which address holds the shared-RET stub 0xab38? */
        static int _sr = -1; static unsigned _srn = 0;
        if (_sr < 0) _sr = calypso_gate("CALYPSO_SLOTSRC", 0);
        if (_sr && _srn < 40 && s->data[addr] == 0xab38) {
            _srn++;
            fprintf(stderr, "[c54x] SLOTSRC-RD data[0x%04x] = 0xab38 (STUB) lu PC=0x%04x insn=%u\n",
                    addr, s->pc, s->insn_count);
        }
    }
    flow_log("R", addr, s->data[addr], s->pc, s->insn_count);
    read_stats_record(addr);
    /* MEM-WATCH-2B80 (gate CALYPSO_MEM_WATCH_2B80): the FB correlator at
     * PC=0xee38 reads data[0x2b97] through AR3 (STM hardcoded in the ROM), in
     * [0x2b80,0x2c00) - a region distinct from the BSP DMA buffer
     * [0x2a00,0x2b28). Logs every read in that window (value, PC), cap 200, to
     * tell whether the region is ever populated at all. */
    if (addr >= 0x2b80 && addr < 0x2c00) {
        static int mw2b80_en = -1;
        if (mw2b80_en < 0) mw2b80_en = calypso_gate("CALYPSO_MEM_WATCH_2B80", 0);
        if (mw2b80_en) {
            static unsigned mw2b80_n = 0;
            if (mw2b80_n < 200) {
                mw2b80_n++;
                fprintf(stderr, "[c54x] MEM-WATCH-2B80-RD data[0x%04x]=0x%04x "
                        "PC=0x%04x insn=%u\n", addr, s->data[addr], s->pc,
                        s->insn_count);
            }
        }
    }
    /* Frame-IT probe: the frozen value of the polled flags and who polls them.
     * The value read (never changing) is what the BSP must produce or toggle. */
    if (addr == 0x006e || addr == 0x585f || addr == 0x8a44) {
        static uint32_t fr_n = 0;
        if (fr_n < 24) {
            fprintf(stderr, "[c54x] FLAGRD data[0x%04x]=0x%04x PC=0x%04x A=0x%04x "
                    "TC=%d insn=%u\n", addr, s->data[addr], s->pc,
                    (uint16_t)(s->a & 0xFFFF), !!(s->st0 & ST0_TC), s->insn_count);
            fr_n++;
        }
        /* One-shot: dump the overlay of the 0x010b loop plus the interrupt
         * state (IFR/IMR/INTM) during the spin, which separates a dead source
         * from an IMR-masked one from a stuck INTM. */
        static int dumped_010b = 0;
        if (addr == 0x006e && s->pc == 0x010b && !dumped_010b) {
            dumped_010b = 1;
            fprintf(stderr, "[c54x] SPIN-IT @0x010b IFR=0x%04x IMR=0x%04x INTM=%d "
                    "(INT3 bit3 IFR=%d IMR=%d ; BRINT0 bit5 IFR=%d IMR=%d) insn=%u\n",
                    s->ifr, s->imr, !!(s->st1 & ST1_INTM),
                    !!(s->ifr&(1<<3)), !!(s->imr&(1<<3)),
                    !!(s->ifr&(1<<5)), !!(s->imr&(1<<5)), s->insn_count);
            fprintf(stderr, "[c54x] OVERLAY-DUMP prog[0x0100..0x0118] (loop poll 0x006e):\n");
            for (uint16_t a = 0x0100; a <= 0x0118; a++)
                fprintf(stderr, "[c54x]   prog[0x%04x]=0x%04x\n", a, prog_fetch(s, a));
        }
    }
    /* D_TASK_MD-RD probe : trace DSP reads of d_task_md (write page 0
     * @ data[0x0804], write page 1 @ data[0x0818]). The DSP dispatcher
     * reads task_md then branches to FB / SB / ALLC / etc. routines.
     * If only one PC reads it, that's the single dispatcher. Capped 30. */
    if ((addr == 0x0804 || addr == 0x0818) && calypso_debug_enabled("D_TASK_MD-RD")) {
        /* Prints EA, page, value and the delta since the ARM wrote 5. If the
         * value read after that write (delta>0) is not 5, the 5 does not reach
         * this EA: compare ARM5_EA (written by the ARM) with EA (read by the DSP).
         *  - same EA, value != 5 -> ordering (read before write in the frame)
         *  - EA != ARM5_EA (page stride) -> w_page/r_page flip parity. */
        fprintf(stderr,
                "[c54x] D_TASK_MD-RD EA=0x%04x page=%d val=0x%04x "
                "ARM5_EA=0x%04x dArm5=%lld PC=0x%04x insn=%u\n",
                addr, (addr == 0x0804) ? 0 : 1, s->data[addr],
                g_arm_taskmd5_ea,
                (long long)((int64_t)s->insn_count - (int64_t)g_arm_taskmd5_insn),
                s->pc, s->insn_count);
    }
    /* WATCH-RD-ADDR (generic; CALYPSO_DEBUG=WATCH-RD plus
     * CALYPSO_WATCH_RD_ADDR=0xNNNN): logs every READ of an arbitrary data
     * address with PC, value and insn - who reads a dispatcher-pointer cell and
     * with which value (0 before it is populated vs valid after). Mirror of
     * WATCH-WR. */
    {
        static int watch_rd_addr = -1;
        if (watch_rd_addr < 0) {
            const char *e = calypso_getenv("CALYPSO_WATCH_RD_ADDR");
            watch_rd_addr = (e && *e) ? (int)strtol(e, NULL, 0) : 0;
        }
        if (watch_rd_addr && addr == (uint16_t)watch_rd_addr) {
            C54_DBG("WATCH-RD",
                "WATCH-RD data[0x%04x] = 0x%04x PC=0x%04x DP=0x%03x insn=%u",
                addr, s->data[addr], s->pc, (s->st0 & 0x1FF),
                (unsigned)s->insn_count);
        }
    }
    /* DISP-POLL (CALYPSO_DEBUG=DISP-POLL): the dispatcher busy-loop
     * (d1xx <-> da0d) polls the flag area DARAM[0x60..0x70]. Shows, in steady
     * state, which slot it reads and whether the flag ever goes non-zero.
     * Throttled to 1 in 40000 so the busy-loop cannot flood the log, plus every
     * non-zero value - that is the event that matters. */
    if (addr >= 0x0060 && addr <= 0x0070 && calypso_debug_enabled("DISP-POLL")) {
        static uint64_t poll_n = 0;
        uint16_t v = s->data[addr];
        if (v != 0 || (poll_n++ % 40000) == 0)
            fprintf(stderr,
                "[c54x] DISP-POLL-RD data[0x%04x]=0x%04x PC=0x%04x INTM=%d insn=%u%s\n",
                addr, v, s->pc, !!(s->st1 & ST1_INTM), s->insn_count,
                v ? "  <-- NON-ZERO (flag posé !)" : "");
    }
    /* FBDB-PROBE read 0x3DC0 (= SARAM flag polled by fc63 BITF).
     * Env CALYPSO_FBDB_PROBE=1. Logs first 30 reads + each 10000th. */
    if (addr == 0x3DC0 && g_fbdb_probe_enabled > 0) {
        fbdb_probe_read_3dc0(addr, s->data[addr], s->pc, s->insn_count);
    }
    /* D_BURST_D_W probe: does the DSP read db_w->d_burst_d?
     * 0x0801 (W page 0 + offset 1), 0x0815 (W page 1 + offset 1).
     *   reads see 0,1,2,3 in sequence -> the ARM fills db_w correctly
     *   reads always see 0            -> the ARM never sets burst_id
     *   never read                    -> the DSP does not use db_w for the
     *                                    burst sequence. */
    if (addr == 0x0801 || addr == 0x0815) {
        static uint64_t dbw_total[2];
        static uint64_t dbw_per_val[2][16];
        static uint64_t dbw_last_log[2];
        static uint16_t dbw_last_val[2];
        int page = (addr == 0x0815) ? 1 : 0;
        uint16_t cur_val = s->data[addr] & 0xF;
        dbw_total[page]++;
        if (cur_val < 16) dbw_per_val[page][cur_val]++;
        bool changed = (cur_val != dbw_last_val[page]);
        dbw_last_val[page] = cur_val;
        if (dbw_total[page] <= 100 || changed
            || (s->insn_count - dbw_last_log[page]) > 1000000) {
            fprintf(stderr,
                    "[c54x] D_BURST_D_W-RD page=%d #%llu addr=0x%04x "
                    "val=0x%04x exec_pc=0x%04x insn=%u\n",
                    page, (unsigned long long)dbw_total[page], addr,
                    s->data[addr], s->last_exec_pc, s->insn_count);
            dbw_last_log[page] = s->insn_count;
        }
        /* Summary every 50000 reads: histogram of the values read */
        if ((dbw_total[page] % 50000) == 0) {
            if (calypso_debug_enabled("D_BURST_D_W-SUMMARY")) fprintf(stderr,
                    "[c54x] D_BURST_D_W-SUMMARY page=%d total=%llu "
                    "val[0]=%llu [1]=%llu [2]=%llu [3]=%llu other=%llu\n",
                    page, (unsigned long long)dbw_total[page],
                    (unsigned long long)dbw_per_val[page][⟨1⟩],  ×4
    ⟨⟩ = (0) (1) (2) (3)
                    (unsigned long long)(dbw_total[page]
                        - dbw_per_val[page][0] - dbw_per_val[page][1]
                        - dbw_per_val[page][2] - dbw_per_val[page][3]));
        }
    }
    /* PC histogram to identify the PM routine. Two ranges:
     *   [0x3fb0..0x3fbf] = BSP buffer (I/Q samples)
     *   [0x3dcf..0x3dd5] = dominant scratch buffer (78k + 52k reads measured)
     * Counts per PC and dumps the top 10 every 50k reads in each range, which
     * separates "PM broken" from "PM never called". */
    if (addr >= 0x3fb0 && addr <= 0x3fbf) {
        static uint32_t pc_hist_3fb[65536];
        static uint32_t total_3fb;
        pc_hist_3fb[s->pc]++;
        total_3fb++;
        if ((total_3fb % 50000) == 0) {
            uint32_t top_pc[10] = {0};
            uint32_t top_cnt[10] = {0};
            for (uint32_t p = 0; p < 65536; p++) {
                uint32_t c = pc_hist_3fb[p];
                if (c == 0) continue;
                for (int i = 0; i < 10; i++) {
                    if (c > top_cnt[i]) {
                        for (int j = 9; j > i; j--) {
                            top_pc[j]  = top_pc[j-1];
                            top_cnt[j] = top_cnt[j-1];
                        }
                        top_pc[i]  = p;
                        top_cnt[i] = c;
                        break;
                    }
                }
            }
            if (calypso_debug_enabled("PC-HIST-3FB")) fprintf(stderr, "[c54x] PC-HIST-3FB total=%u :", total_3fb);
            for (int i = 0; i < 10 && top_cnt[i]; i++) {
                fprintf(stderr, " %04x:%u", top_pc[i], top_cnt[i]);
            }
            fprintf(stderr, "\n");
        }
    }
    if (addr >= 0x3dcf && addr <= 0x3dd5) {
        static uint32_t pc_hist_3dd[65536];
        static uint32_t total_3dd;
        pc_hist_3dd[s->pc]++;
        total_3dd++;
        if ((total_3dd % 50000) == 0) {
            uint32_t top_pc[10] = {0};
            uint32_t top_cnt[10] = {0};
            for (uint32_t p = 0; p < 65536; p++) {
                uint32_t c = pc_hist_3dd[p];
                if (c == 0) continue;
                for (int i = 0; i < 10; i++) {
                    if (c > top_cnt[i]) {
                        for (int j = 9; j > i; j--) {
                            top_pc[j]  = top_pc[j-1];
                            top_cnt[j] = top_cnt[j-1];
                        }
                        top_pc[i]  = p;
                        top_cnt[i] = c;
                        break;
                    }
                }
            }
            if (calypso_debug_enabled("PC-HIST-3DD")) fprintf(stderr, "[c54x] PC-HIST-3DD total=%u :", total_3dd);
            for (int i = 0; i < 10 && top_cnt[i]; i++) {
                fprintf(stderr, " %04x:%u", top_pc[i], top_cnt[i]);
            }
            fprintf(stderr, "\n");
        }
    }
    /* CANARY-READ probe: with CALYPSO_BSP_INJECT_CANARY=1 the BSP overwrites
     * every sample with 0xCAFE. Any address from which the DSP reads 0xCAFE is
     * on its sample-read path, i.e. the right target for CALYPSO_BSP_DARAM_ADDR.
     * Capped at 100 lines. */
    if (addr < 0x4000) {
        uint16_t v = s->data[addr];
        if (v == 0xCAFE) {
            static unsigned canary_log;
            const unsigned LIMIT = 100;
            if (canary_log < LIMIT) {
                fprintf(stderr,
                        "[c54x] CANARY-READ #%u addr=0x%04x PC=0x%04x "
                        "AR2=%04x AR3=%04x AR4=%04x AR5=%04x insn=%u\n",
                        canary_log, addr, s->pc,
                        s->ar[2], s->ar[3], s->ar[4], s->ar[5],
                        s->insn_count);
                canary_log++;
                if (canary_log == LIMIT)
                    fprintf(stderr, "[c54x] CANARY-READ capped at %u\n", LIMIT);
            }
        }
    }

    /* Watch the mailbox slots that the firmware polls at PROM0 0xb41a
     * (LDU *(0x0ffe), A then BACC A) and 0xb41c (CMPM *(0x0fff), 4).
     * If these stay zero / 0x10 forever, ARM never wrote them. */
    if (addr == 0x0ffe || addr == 0x0fff || addr == 0x0ffc || addr == 0x0ffd) {
        static unsigned watch_count;
        static uint16_t last_vd[4] = { 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF };
        int widx = (addr == 0x0fff) ? 0 : (addr == 0x0ffe) ? 1 : (addr == 0x0ffd) ? 2 : 3;
        uint16_t vd = s->data[addr];
        watch_count++;
        /* Log on change: first 60 lines plus every value CHANGE. The bootloader
         * spin at 0xb424 reads data[0x0fff]=0x0001 1.36 billion times, so a
         * 1-in-10000 sample emitted 135k lines (20 MB), stalled QEMU and lost
         * osmocon. This keeps the exact signal - the IDLE 0x0001 -> command
         * 0x0002/0x0004 transition - without the noise. */
        if (watch_count <= 60 || vd != last_vd[widx]) {
            uint16_t va = s->api_ram ? s->api_ram[addr - C54X_API_BASE] : 0xDEAD;
            fprintf(stderr,
                    "[c54x] WATCH-READ #%u data[0x%04x] data=0x%04x api_ram=0x%04x api_set=%d PC=0x%04x insn=%u\n",
                    watch_count, addr, vd, va, s->api_ram ? 1 : 0, s->pc, s->insn_count);
        }
        last_vd[widx] = vd;
    }
    /* Wait-loop diagnostic: 0x3dd0 was found to absorb ~99.5 % of DARAM
     * reads after the first ~500k reads - the DSP is stuck polling it.
     * Log the first PCs and then sample once per million reads so we can
     * trace the loop without flooding the log. */
    if (addr == 0x3dd0) {
        static unsigned wait_log;
        static unsigned wait_seen;
        wait_seen++;
        if (wait_log < 20 || (wait_seen % 1000000) == 0) {
            wait_log++;
            fprintf(stderr,
                    "[c54x] WAIT-3DD0 #%u data[0x3dd0]=0x%04x PC=0x%04x AR2=%04x AR3=%04x insn=%u\n",
                    wait_seen, s->data[0x3dd0], s->pc,
                    s->ar[2], s->ar[3], s->insn_count);
        }
    }
    /* d_fb_det watch. The DSP word address is 0x08F8:
     * ARM 0xFFD001F0 (BASE_API_NDB 0xFFD001A8 + 36 words * 2)
     *   = DSP word 0x0800 + 0x1F0/2 = 0x08F8.
     * 0x01F0 is the ARM byte offset, not a DSP word address. */
    if (addr == 0x08F8) {
        static unsigned fb_read;
        if (fb_read++ < 30) {
            fprintf(stderr,
                    "[c54x] WATCH-READ d_fb_det[0x08F8]=0x%04x PC=0x%04x insn=%u\n",
                    s->data[0x08F8], s->pc, s->insn_count);
        }
    }
    /* BOOT-POLL-RD: traces data reads in the boot polling loop (DARAM
     * 0x00ed..0x00ff = OVLY mirror of PROM0[0x70ed..0x70ff]; DSP_ROM_MAP puts
     * the boot polling loop at 0x7026-0x71FF, where it writes the API RAM
     * tables). Identifies which address the firmware polls to decide to leave,
     * hence which signal the modelled peripherals must emit. Cap 300. */
    if (s->pc >= 0x00ed && s->pc <= 0x010f) {
        static unsigned bpr_log;
        const unsigned LIMIT = 300;
        if (bpr_log < LIMIT) {
            uint16_t v;
            const char *region;
            if (addr >= C54X_API_BASE && addr < C54X_API_BASE + C54X_API_SIZE) {
                v = s->api_ram ? s->api_ram[addr - C54X_API_BASE] : 0;
                region = "api";
            } else if (addr < 0x4000) {
                v = s->data[addr];
                region = "daram";
            } else {
                v = s->data[addr];
                region = "mmr/oth";
            }
            fprintf(stderr,
                    "[c54x] BOOT-POLL-RD #%u PC=0x%04x op=0x%04x [%s 0x%04x]=0x%04x "
                    "AR1=%04x AR2=%04x AR3=%04x SP=%04x insn=%u\n",
                    bpr_log, s->pc, s->prog[s->pc], region, addr, v,
                    s->ar[1], s->ar[2], s->ar[3], s->sp, s->insn_count);
            bpr_log++;
            if (bpr_log == LIMIT) {
                fprintf(stderr, "[c54x] BOOT-POLL-RD log capped at %u\n", LIMIT);
            }
        }
    }

    /* CORR-RD: traces data reads in the FB-det correlator inner body
     * (PROM0[0x9aba..0x9abf] = RPTBD body of the publish routine at 0x9aaf+).
     * addr in [0x3fb0..0x3fbf] -> right zone (BSP RX), the value is a sample.
     * addr anywhere else -> addressing bug: the correlator reads an empty
     * buffer, A stays 0, it publishes 0 and never locks. Cap 200. */
    if (s->pc >= 0x9aba && s->pc <= 0x9abf) {
        static unsigned cr_log;
        const unsigned LIMIT = 200;
        if (cr_log < LIMIT) {
            uint16_t v;
            const char *region;
            if (addr >= C54X_API_BASE && addr < C54X_API_BASE + C54X_API_SIZE) {
                v = s->api_ram ? s->api_ram[addr - C54X_API_BASE] : 0;
                region = "api";
            } else if (addr < 0x4000) {
                v = s->data[addr];
                region = "daram";
            } else {
                v = s->data[addr];
                region = "mmr/oth";
            }
            fprintf(stderr,
                    "[c54x] CORR-RD #%u PC=0x%04x [%s 0x%04x]=0x%04x "
                    "AR2=%04x AR3=%04x AR4=%04x AR5=%04x A_lo=%04x B_lo=%04x insn=%u\n",
                    cr_log, s->pc, region, addr, v,
                    s->ar[2], s->ar[3], s->ar[4], s->ar[5],
                    (uint16_t)(s->a & 0xFFFF),
                    (uint16_t)(s->b & 0xFFFF),
                    s->insn_count);
            cr_log++;
            if (cr_log == LIMIT) {
                fprintf(stderr, "[c54x] CORR-RD log capped at %u\n", LIMIT);
            }
        }
    }
    /* IDLE-DISP: the DSP gets stuck in the PROM0 loop at 0xCC62..0xCC6F polling
     * task slots. Dumps (PC, addr, value, AR2..AR5) for the first N reads to see
     * which location the dispatcher inspects before deciding to branch out.
     * Captures DARAM, API RAM and MMR reads so the poll address cannot be
     * missed. Capped. */
    if (s->pc >= 0xCC62 && s->pc <= 0xCC6F) {
        static unsigned idle_rd_log;
        const unsigned LIMIT = 200;
        if (idle_rd_log < LIMIT) {
            uint16_t v;
            const char *region;
            if (addr >= C54X_API_BASE && addr < C54X_API_BASE + C54X_API_SIZE) {
                v = s->api_ram ? s->api_ram[addr - C54X_API_BASE] : 0;
                region = "api";
            } else if (addr < 0x4000) {
                v = s->data[addr];
                region = "daram";
            } else {
                v = s->data[addr];
                region = "mmr/other";
            }
            fprintf(stderr,
                    "[c54x] IDLE-DISP RD #%u PC=0x%04x [%s 0x%04x]=0x%04x "
                    "AR2=%04x AR3=%04x AR4=%04x AR5=%04x insn=%u\n",
                    idle_rd_log, s->pc, region, addr, v,
                    s->ar[2], s->ar[3], s->ar[4], s->ar[5], s->insn_count);
            idle_rd_log++;
            if (idle_rd_log == LIMIT) {
                fprintf(stderr,
                        "[c54x] IDLE-DISP RD log capped at %u — pattern should be visible above\n",
                        LIMIT);
            }
        }
    }

    /* UPPER-DARAM RD HIST: histogram of reads in [0x4000..0xFFFF], the
     * companion of the low-DARAM histogram below. Shows which upper addresses
     * the DSP polls for its samples, hence the right target for
     * CALYPSO_BSP_DARAM_ADDR. Top 16 every 100k reads; skips the first 1M insn
     * to drop the boot noise. */
    if (addr >= 0x4000 && s->insn_count > 1000000) {
        static unsigned uhist[0xC000]; /* 0x4000..0xFFFF = 48K words */
        static unsigned ureads;
        uhist[addr - 0x4000]++;
        ureads++;
        if ((ureads % 100000) == 0) {
            unsigned best[16] = {0}; uint16_t baddr[16] = {0};
            for (unsigned a = 0; a < 0xC000; a++) {
                unsigned c = uhist[a];
                if (c <= best[15]) continue;
                int p = 15;
                while (p > 0 && best[p-1] < c) {
                    best[p] = best[p-1]; baddr[p] = baddr[p-1]; p--;
                }
                best[p] = c; baddr[p] = (uint16_t)(0x4000 + a);
            }
            if (calypso_debug_enabled("UPPER-DARAM")) fprintf(stderr,
                    "[c54x] UPPER-DARAM RD HIST (reads=%u): ", ureads);
            for (int i = 0; i < 16 && best[i]; i++)
                fprintf(stderr, "%04x:%u ", baddr[i], best[i]);
            fprintf(stderr, "\n");
        }
    }
    /* === DARAM discovery histogram ===
     * Track ALL data reads from DARAM (addr < 0x4000) regardless of PC.
     * The FB handler runs from both PROM0 (0xBD47) and DARAM overlay,
     * so filtering by PC misses critical reads. */
    if (addr < 0x4000 && addr >= 0x20) {  /* skip MMRs 0x00-0x1F */
        static unsigned hist[0x4000]; /* 16 KW DARAM */
        static unsigned reads;
        if (addr < 0x4000) {
            hist[addr]++;
            reads++;
            if ((reads % 50000) == 0) {
                /* find top-16 */
                unsigned best[16] = {0}; uint16_t baddr[16] = {0};
                for (uint16_t a = 0; a < 0x4000; a++) {
                    unsigned c = hist[a];
                    if (c <= best[15]) continue;
                    int p = 15;
                    while (p > 0 && best[p-1] < c) {
                        best[p] = best[p-1]; baddr[p] = baddr[p-1]; p--;
                    }
                    best[p] = c; baddr[p] = a;
                }
                if (calypso_debug_enabled("DARAM")) fprintf(stderr,
                        "[c54x] DARAM RD HIST (FB-det, reads=%u): ",
                        reads);
                for (int i = 0; i < 16 && best[i]; i++)
                    fprintf(stderr, "%04x:%u ", baddr[i], best[i]);
                fprintf(stderr, "\n");
            }
        }
    }
    /* FB-det / dispatcher subroutine trace. The 0x7e80..0x7eb8 wrapper calls
     * into 0x81a5/0x81c8 with AR5=0x0e4c (the FB sample buffer); both ranges are
     * covered so wrapper polls and inner correlator reads are caught. Skips the
     * boot init phase. */
    if ((s->pc >= 0x7e80 && s->pc <= 0x7ec0) ||
        (s->pc >= 0x81a0 && s->pc <= 0x82ff)) {
        static int fbdet_rd_log = 0;
        if (s->insn_count > 50000000 && fbdet_rd_log < 2000) {
            uint16_t v;
            if (addr >= C54X_API_BASE && addr < C54X_API_BASE + C54X_API_SIZE)
                v = s->api_ram ? s->api_ram[addr - C54X_API_BASE] : 0;
            else
                v = s->data[addr];
            C54_LOG("FBDET RD [0x%04x]=0x%04x PC=0x%04x AR2=%04x AR3=%04x AR4=%04x AR5=%04x insn=%u",
                    addr, v, s->pc, s->ar[2], s->ar[3], s->ar[4], s->ar[5], s->insn_count);
            fbdet_rd_log++;
        }
    }
    /* Log AR0..AR7 when entering FB-det subroutines to understand
     * what each AR points at (sample buffer? coeffs? status?). */
    if ((s->pc == 0x81a5 || s->pc == 0x81c8) && s->insn_count > 50000000) {
        static int ar_log = 0;
        if (ar_log < 10) {
            C54_LOG("FB-CALL PC=0x%04x AR0=%04x AR1=%04x AR2=%04x AR3=%04x "
                    "AR4=%04x AR5=%04x AR6=%04x AR7=%04x SP=%04x BK=%04x",
                    s->pc, s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                    s->ar[4], s->ar[5], s->ar[6], s->ar[7], s->sp, s->bk);
            ar_log++;
        }
    }
    /* d_spcx_rif (NDB word 2 = api 0xD6 = DSP data 0x08D6) */
    if (addr == 0x08D6) {
        static int spcx_rd = 0;
        if (spcx_rd < 32) {
            C54_LOG("d_spcx_rif RD = 0x%04x PC=0x%04x insn=%u",
                    s->api_ram ? s->api_ram[0xD6] : s->data[addr],
                    s->pc, s->insn_count);
            spcx_rd++;
        }
    }
    /* Log reads from API RAM at 0x08D4 (d_dsp_page). */
    if (addr == 0x08D4) {
        static int dsp_page_log = 0;
        if (dsp_page_log < 50) {
            C54_LOG("d_dsp_page RD = 0x%04x PC=0x%04x insn=%u SP=0x%04x",
                    s->api_ram ? s->api_ram[addr - 0x0800] : s->data[addr],
                    s->pc, s->insn_count, s->sp);
            dsp_page_log++;
        }
        /* FBWATCH producer side: does the DSP re-read d_dsp_page every frame? */
        if (g_fbwatch_on < 0) g_fbwatch_on = calypso_gate("CALYPSO_FBWATCH", 0);
        if (g_fbwatch_on) {
            static unsigned wpg = 0;
            if (wpg++ < 60)
                fprintf(stderr, "[c54x] FBWATCH-PAGE-RD #%u val=0x%04x PC=0x%04x insn=%u\n",
                        wpg, s->api_ram ? s->api_ram[addr - 0x0800] : s->data[addr],
                        s->pc, s->insn_count);
        }
    }
    }   /* C54X_SONDES: read probes */
    /* Timer registers (0x0024-0x0026) - read returns current value */
    if (addr == TIM_ADDR) return s->data[TIM_ADDR];
    if (addr == PRD_ADDR) return s->data[PRD_ADDR];
    if (addr == TCR_ADDR) {
        /* TCR: PSC is read from bits 9:6, rest from stored value */
        uint16_t tcr = s->data[TCR_ADDR] & ~TCR_PSC_MASK;
        tcr |= (s->timer_psc & 0xF) << TCR_PSC_SHIFT;
        return tcr;
    }

    /* MMR region */
    if (addr < 0x20) {
        switch (addr) {
        case MMR_IMR:  return s->imr;
        case MMR_IFR:
        {
            static int ifr_log = 0;
            if ((s->ifr & 0x0020) && ifr_log < 10) {
                /* bit 5 = BRINT0 per C54X header (vec 21). */
                C54_LOG("IFR READ=0x%04x (BRINT0 pending) PC=0x%04x", s->ifr, s->pc);
                ifr_log++;
            }
            return s->ifr;
        }
        case MMR_ST0:  return s->st0;
        case MMR_ST1:  return s->st1;
        case MMR_AL:   return (uint16_t)(s->a & 0xFFFF);
        case MMR_AH:   return (uint16_t)((s->a >> 16) & 0xFFFF);
        case MMR_AG:   return (uint16_t)((s->a >> 32) & 0xFF);
        case MMR_BL:   return (uint16_t)(s->b & 0xFFFF);
        case MMR_BH:   return (uint16_t)((s->b >> 16) & 0xFFFF);
        case MMR_BG:   return (uint16_t)((s->b >> 32) & 0xFF);
        case MMR_T:    return s->t;
        case MMR_TRN:  return s->trn;
        case MMR_AR0: case MMR_AR1: case MMR_AR2: case MMR_AR3:
        case MMR_AR4: case MMR_AR5: case MMR_AR6: case MMR_AR7:
            return s->ar[addr - MMR_AR0];
        case MMR_SP:   return s->sp;
        case MMR_BK:   return s->bk;
        case MMR_BRC:  return s->brc;
        case MMR_RSA:  return s->rsa;
        case MMR_REA:  return s->rea;
        case MMR_PMST: return s->pmst;
        case MMR_XPC:  return s->xpc;
        default: return 0;
        }
    }

    /* API RAM (shared with ARM) */
    if (addr >= C54X_API_BASE && addr < C54X_API_BASE + C54X_API_SIZE) {
        if (s->api_ram) {
            uint16_t val = s->api_ram[addr - C54X_API_BASE];
            /* Log ALL API reads during interrupt handler (first 100) */
            static int api_rd_log = 0;
            if (api_rd_log < 100 && s->insn_count > 66000) {
                C54_LOG("API RD [0x%04x] = 0x%04x PC=0x%04x insn=%u",
                        addr, val, s->pc, s->insn_count);
                api_rd_log++;
            }
            return val;
        }
    }

    /* Log data reads during SINT17 handler (PC in 0xFFC0-0xFFFF) */
    if (s->pc >= 0xFFC0 && s->insn_count > 66090) {
        static int handler_rd_log = 0;
        if (handler_rd_log < 30) {
            C54_LOG("H_RD [0x%04x]=0x%04x PC=0x%04x", addr, s->data[addr], s->pc);
            handler_rd_log++;
        }
    }

    /* PROBE-3FAD-GATE (gate CALYPSO_PROBE_3FAD_GATE, cap 200, no cost when OFF):
     * captures the exact value read by BITF *(0x3fad),#0x8000 at 0x8753, the only
     * lock on the FB dispatcher (CC 0xa0a0 -> kernel 0xa076). Nothing else blocks
     * the sweep, so bit15 of 0x3fad alone decides.
     *   val & 0x8000      -> TC=1, the kernel runs.
     *   val & 0x8000 == 0 while RX-FBFLAGS set it -> a per-frame clearer
     *   (76f8 3fad 0000, XPC=0 at 0xace8/0xad04/0xad24 or XPC=2 overlay at
     *   0x28040/0x282d0) wiped it between the BSP write and the DSP sweep. */
    if (addr == 0x3fad && s->pc == 0x8753 && calypso_rxfb_fired) {  /* gated on RX-FBFLAGS having actually set 3fad bit15 */
        static int p3_en = -1; static unsigned p3_n = 0;
        if (p3_en < 0) p3_en = calypso_gate("CALYPSO_PROBE_3FAD_GATE", 0);
        if (p3_en && p3_n < 200) {
            p3_n++;
            fprintf(stderr, "[c54x] PROBE-3FAD-GATE @0x8753 val=0x%04x bit15=%d "
                    "(=>TC/kernel) task_md0=0x%04x xpc=%d insn=%u\n",
                    s->data[0x3fad], !!(s->data[0x3fad] & 0x8000),
                    s->data[0x0804], s->xpc, s->insn_count);
        }
        /* @BEQUILLE - FORCE_3FAD_KERNEL  (CALYPSO_FORCE_3FAD_KERNEL, default OFF)
         *   masque  : the producer of the "burst ready" flag data[0x3fad] bit15.
         *             A per-frame clearer (76f8 3fad 0000) wipes it between the
         *             BSP write and the DSP sweep; this re-sets it on the read
         *             path at 0x8753 so the BITF sees TC=1 -> CC 0xa0a0 ->
         *             kernel 0xa076.
         *   retirer : once the native setter (RX/BRINT0 chain) holds bit15 until
         *             the BITF at 0x8753, i.e. when PROBE_3FAD_GATE sees bit15=1
         *             without this gate.
         *   NB      : conditioned on calypso_rxfb_fired, set by CALYPSO_RX_FBFLAGS.
         */
        { static int _fk = -1; if (_fk < 0) _fk = calypso_gate("CALYPSO_FORCE_3FAD_KERNEL", 0);
          if (_fk && calypso_rxfb_fired) s->data[0x3fad] |= 0x8000; }
    }

    return s->data[addr];
}

static void data_write_locked(C54xState *s, uint16_t addr, uint16_t val);

/* Stack-write ring (ORPHAN trace): records data writes into the stack area so
 * that, at the POPM ST0 at 0xf48b, the writer of the popped slot can be named
 * (clobber below SP, circular / dual-operand family). No writer at dump time
 * means nobody wrote the slot in this frame: the slot is stale and SP is
 * misaligned (a POP with no matching PUSH). */
StkwEv   g_stkw_ring[STKW_RING_N];
unsigned g_stkw_idx  = 0;
int      g_orphan_on = -1;
static void stkw_rec(C54xState *s, uint16_t addr, uint16_t val)
{
    if (g_orphan_on < 0) g_orphan_on = calypso_gate("CALYPSO_ORPHAN", 0);  /* own env, outside CALYPSO_DEBUG: the master gate stays 0 */
    if (!g_orphan_on) return;
    if (addr < 0x1000 || addr > 0x6000) return;   /* stack area (SP drifts 0x1100 -> 0x56xx) */
    StkwEv *e = &g_stkw_ring[g_stkw_idx % STKW_RING_N];
    e->addr = addr; e->val = val; e->pc = s->pc; e->op = prog_fetch(s, s->pc);
    g_stkw_idx++;
    /* Track-value: names the CALL/push that lays down the orphan value (default
     * 0x3125, override with CALYPSO_TRACK_STKVAL=0xNNNN), to be correlated with
     * RCD at 0x765c and POPM at 0xf48b in insn order. */
    {
        static int tval = -2;
        if (tval == -2) {
            const char *te = calypso_getenv("CALYPSO_TRACK_STKVAL");
            tval = (te && *te) ? (int)strtol(te, NULL, 0) : 0x3125;
        }
        if (tval >= 0 && val == (uint16_t)tval)
            fprintf(stderr, "[c54x] STK-PUSH val=0x%04x addr=0x%04x PC=0x%04x op=0x%04x SP=0x%04x insn=%u\n",
                    val, addr, s->pc, prog_fetch(s, s->pc), s->sp, s->insn_count);
    }
}

/* SCRATCH-WR (CALYPSO_SCRATCH_WR, default OFF, read-only) - is the FIRS
 * coefficient table ever written?
 * Measured: prog[0x61..0x66]=0xF4E4 (filler) and data[0x61..0x66]=0 at execution
 * time. Program space 0x0000-0x6FFF is covered by no ROM file (PROM0 starts at
 * 0x7000), and that is exactly where pmad=0x61 falls.
 *   no write   -> a ROM segment is missing
 *   late write -> an ordering problem
 * Every write in BOTH spaces is stamped with insn and PC, with a periodic
 * summary so that "nothing written" cannot be read as "probe silent". */
static int scratchwr_on(void)
{
    static int g = -1;
    if (g < 0) {
        g = calypso_gate("CALYPSO_SCRATCH_WR", 0);
        fprintf(stderr, "[c54x] SCRATCH-WR %s : ecritures dans 0x0060-0x007F "
                "(scratch-pad DARAM + espace PROGRAMME via MVDP) — zone des "
                "coefficients du FIRS (pmad=0x0061)\n",
                g ? "ACTIVE" : "INACTIVE (defaut)");
    }
    return g;
}

void scratchwr_note(C54xState *s, uint16_t a, uint16_t v, const char *espace)
{
    if (!scratchwr_on()) return;
    /* The cap is PER ADDRESS, never global: a global 40-line cap is consumed by
     * data[0x74] around insn 5000-10000, so the coefficient writes (MVDD at insn
     * ~1376674) never print and the cap reads as an absence. */
    int zone = -1;
    if (a >= 0x0060 && a <= 0x007F)      zone = 0;   /* destination */
    else if (a >= 0x2CB0 && a <= 0x2CBF) zone = 1;   /* table SOURCE */
    if (zone < 0) return;

    static unsigned long long cum[2] = {0, 0};
    static unsigned long long cum_coef = 0;   /* 0x0061..0x0066 only */
    static unsigned long long cum_src  = 0;   /* 0x2CB9..0x2CBF only */
    static unsigned char shown[2][0x20];

    cum[zone]++;
    if (a >= 0x0061 && a <= 0x0066) cum_coef++;
    if (a >= 0x2CB9 && a <= 0x2CBF) cum_src++;

    unsigned idx = (zone == 0) ? (unsigned)(a - 0x0060) : (unsigned)(a - 0x2CB0);
    if (idx < 0x20 && shown[zone][idx] < 3) {
        shown[zone][idx]++;
        fprintf(stderr, "[c54x] SCRATCH-WR %s[0x%04x] <- 0x%04x PC=0x%04x insn=%u%s\n",
                espace, a, v, s->pc, s->insn_count,
                (a >= 0x0061 && a <= 0x0066) ? "  <<<< COEFFICIENTS (destination)" :
                (a >= 0x2CB9 && a <= 0x2CBF) ? "  <<<< SOURCE de la table" : "");
    }
    if (((cum[0] + cum[1]) % 500) == 0)
        fprintf(stderr, "[c54x] SCRATCH-WR bilan : scratch(0x60-0x7F)=%llu "
                "source(0x2CB0-0x2CBF)=%llu | dont coefficients(0x61-0x66)=%llu "
                "source utile(0x2CB9-0x2CBF)=%llu\n",
                (unsigned long long)cum[0], (unsigned long long)cum[1],
                (unsigned long long)cum_coef, (unsigned long long)cum_src);
}

/* origin of the last TOA written by the ROM (see the probe on 0x08fa) */
int g_toa_grille = -1, g_toa_valeur = 0;
unsigned g_toa_seq = 0;

uint16_t g_piege_pile_sp; int g_piege_pile_on;   /* see data_write */
void data_write(C54xState *s, uint16_t addr, uint16_t val)
{
    /* STACK TRAP: a `ret` reading 0 at its return address (e.g. PROM1 0x1e9bd, SP=0x5abb) right
     * after an interrupt sends PC -> 0x0000 and loses the DSP. Between the entry of an interrupt
     * and its RETE, every write into the 4 words above the SP of the interrupted code (where it
     * keeps its return addresses) is traced, with the ring. g_piege_pile_on: calypso_c54x.c
     * (interrupt entry), c54x_exec.c (RETE). CALYPSO_C54X_PIEGE_PILE=0 disables. */
    {
        extern uint16_t g_piege_pile_sp; extern int g_piege_pile_on;
        static int on = -1;
        if (on < 0) on = calypso_gate("CALYPSO_C54X_PIEGE_PILE", 1);
        if (on && g_piege_pile_on && (uint16_t)(addr - g_piege_pile_sp) < 4u && s->insn_count > 3000000) {
            static unsigned n;
            if (n++ < 12) {
                printf("  [piege-pile] data[0x%04x] %04x -> %04x pendant l'IT (SP interrompu 0x%04x) pc=0x%04x xpc=%u op=0x%04x "
                       "SP=%04x DP=%03x ST1=%04x AR0..7=%04x %04x %04x %04x %04x %04x %04x %04x insn=%u [%u]\n",
                       addr, s->data[addr], val, g_piege_pile_sp, s->pc, s->xpc, s->prog[s->pc], s->sp, s->st0 & 0x1ff,
                       s->st1, s->ar[0], s->ar[1], s->ar[2], s->ar[3], s->ar[4], s->ar[5], s->ar[6], s->ar[7],
                       s->insn_count, n);
                if (n <= 2) { extern void c54x_anneau_vider_stdout(const char *etiquette, unsigned n_max);
                              c54x_anneau_vider_stdout("piege-pile", 200); }
            }
        }
    }
    /* GUARD 0x3d89: write pointer of the 232-word circular buffer at 0x4d00
     * (init 0x90c0, advanced at 0x9173, copies 28 words of the DMA buffer
     * 0x0cce on every TCH frame). If it lands in 0x08xx the buffer overlaps the
     * W/R pages and the NDB of the API RAM (d_debug_ptr 0x08dc overwritten ->
     * STL into SP from 0xb522, DSP lost). The first writes outside
     * 0x4d00..0x4dff are logged, with enough context to trace the cause. */
    if (addr == 0x3d89 && (val & 0xff00) != 0x4d00) {
        static unsigned n;
        if (n++ < 12)
            printf("  [garde-3d89] data[0x3d89] <- 0x%04x (etait 0x%04x) PC=0x%04x DP=0x%03x ST0=0x%04x "
                    "ST1=0x%04x SP=0x%04x IMR=0x%04x IFR=0x%04x AR2=0x%04x BK=0x%04x insn=%u\n",
                    val, s->data[0x3d89], s->pc & 0xffff, s->st0 & 0x1ff, s->st0, s->st1, s->sp,
                    s->imr, s->ifr, s->ar[2], s->bk, s->insn_count), fflush(stdout);
    }
    /* [etat-tch] data[0x3fba]: state of the PROM0 TCH machine (be3e: list = 0xde27 + state*12 +
     * (fn%26)/2). -1 (init: b456, c442, c8e1, PDROM e210); a dispatcher run without st #0 then
     * reads a list outside the table (entry 0x01f6, DSP stuck). Logged: who writes it, from
     * where, with d_ctrl_tch/d_task_d. */
    if (addr == 0x3fba && val != s->data[0x3fba] && s->insn_count > 3000000) {
        static unsigned n;
        if (n++ < 80) {
            const uint16_t *w0 = &s->data[0x0800], *w1 = &s->data[0x0814];
            printf("  [etat-tch] 3fba %d -> %d PC=0x%04x xpc=%u | W0 task_d=%u ctrl_tch=%04x d_fn=%04x | W1 task_d=%u ctrl_tch=%04x d_fn=%04x | 4364=%u 3fb8=%04x insn=%u\n",
                   (int16_t)s->data[0x3fba], (int16_t)val, s->pc & 0xffff, s->xpc, w0[0], w0[9], w0[8], w1[0], w1[9], w1[8],
                   s->data[0x4364], s->data[0x3fb8], s->insn_count);
        }
    }
    /* [base-tch] base of the TCH state table (b928: 0xde57 if data[0x3fab] bit8, otherwise 0xde27
     * at b930). 0xde57 + state 4 -> list 0xbc5a (outside the table). Logged: who sets 0xde57 and
     * with which page (0x3fc1, word 9 = d_ctrl_tch, bit15 = B_SUBCHANNEL -> b829 orm #256,*(0x3fab)). */
    if (addr == 0x4366 && val == 0xde57 && s->insn_count > 3000000) {
        static unsigned n;
        if (n++ < 40) {
            uint16_t pg = s->data[0x3fc1];
            printf("  [base-tch] 4366 <- de57 PC=0x%04x 3fab=%04x 3fc1=%04x mot9=%04x mot0=%04x 3fba=%d 4364=%u insn=%u\n",
                   s->pc & 0xffff, s->data[0x3fab], pg, s->data[(uint16_t)(pg + 9)], s->data[pg],
                   (int16_t)s->data[0x3fba], s->data[0x4364], s->insn_count);
        }
    }
    if (addr == 0x3fab && (val & 0x100) && !(s->data[0x3fab] & 0x100) && s->insn_count > 3000000) {
        static unsigned n;
        if (n++ < 40) {
            uint16_t pg = s->data[0x3fc1];
            printf("  [base-tch] 3fab bit8 <- 1 (%04x) PC=0x%04x 3fc1=%04x mot9=%04x mot0=%04x 3fba=%d 4364=%u insn=%u\n",
                   val, s->pc & 0xffff, pg, s->data[(uint16_t)(pg + 9)], s->data[pg], (int16_t)s->data[0x3fba],
                   s->data[0x4364], s->insn_count);
        }
    }
    if (c54x_rapide) {                     /* see calypso_c54x.h */
        /* the one functional substitution of the slow path: a_pm from the
         * measured downlink magnitude (CALYPSO_PM_RSSI, default 1) */
        static int rssi_on = -1;
        if (rssi_on < 0) rssi_on = calypso_gate("CALYPSO_PM_RSSI", 1);
        if (rssi_on && ((addr >= 0x0834 && addr <= 0x0836) || (addr >= 0x0848 && addr <= 0x084A)))
            val = calypso_bsp_rssi_apm_suivante(NULL);   /* next window, in order (calypso_bsp.c) */
        if (addr == 0x0831 || addr == 0x0845) val = calypso_bsp_air_dpm(val);   /* D_PM, realistic air */
        /* mailbox monitor also on the fast path (see data_read) */
        calypso_mbx(MBX_DSP_WR, addr, val, s->data[addr], s->pc, 0, s->insn_count);
        data_write_locked(s, addr, val);
        return;
    }
    scratchwr_note(s, addr, val, "data");
    {   /* HANDLER-WATCH - CALYPSO_DISPATCH_PROBE=1, read-only.
         *
         * data[0x43d8] is the CURRENT handler slot; the dispatcher does
         *     0xb01c  ld   *(0x43d8), A
         *     0xb01e  cala A
         * and [2026-08-03] it always calls 0xab38, the shared RET of empty slots.
         * The whole RX chain therefore reduces to who writes this cell, and why
         * never 0xa5cd.
         *
         * Watching the WRITE rather than a PC is deliberate: a PC watch is easy
         * to aim at the operand instead of the instruction, and watching the cell
         * also catches writers that have not been identified yet. */
        static int _hw = -1;
        if (_hw < 0) _hw = calypso_gate("CALYPSO_DISPATCH_PROBE", 0);
        if (_hw && addr == 0x43d8) {
            static unsigned long long _n = 0;
            uint16_t old = s->data[addr];
            if (old != val || _n < 8) {
                _n++;
                fprintf(stderr, "[dispatch] *** data[0x43d8] : 0x%04x -> 0x%04x "
                        "par PC=0x%04x%s  (data[0x43b0]=0x%04x) insn=%u\n",
                        old, val, s->pc,
                        (val == 0xa5cd) ? "  <<< ARMEMENT RX INSTALLE !" :
                        (val == 0xab38) ? "  (RET partage = slot vide)" : "",
                        s->data[0x43b0], s->insn_count);
            }
        }
    }
    /* Mailbox monitor (calypso_mailbox.h). Must stay FIRST: s->data[addr] still
     * holds the old value here. Costs one inline int test when off, and
     * data_write is a hot path. */
    calypso_mbx(MBX_DSP_WR, addr, val, s->data[addr], s->pc, 0, s->insn_count);

    /* WATCH-ACD (gate CALYPSO_WATCH_ACD): does the DSP write a_cd by opcode?
     * A_CD-WR (watch_write_zone_check, 0x09d2..0x09e0) already covers the zone
     * ungated and logs the first 500 writes; it is the reference, WATCH-ACD only
     * a targeted duplicate. */
    if (addr >= 0x09D0 && addr <= 0x09DE) {
        static int _wac = -1;
        if (_wac < 0) _wac = calypso_gate("CALYPSO_WATCH_ACD", 0);
        if (_wac) { static unsigned _nac = 0;
            if (_nac++ < 60)
                fprintf(stderr, "[c54x] WATCH-ACD DSP-opcode-write data[0x%04x]=0x%04x (was 0x%04x) PC=0x%04x insn=%u\n",
                        addr, val, s->data[addr], s->pc, s->insn_count); }
    }
    /* WATCH-2A00 (gate CALYPSO_WATCH_2A00): traces every OPCODE write to the IQ
     * buffer 0x2a00..0x2a07. feed_iq writes s->data[] directly, bypassing
     * data_write, so anything seen here is a DSP opcode writer; s=%p lets the
     * pointer be compared with feed_iq's. */
    if (addr >= 0x2a00 && addr <= 0x2a07) {
        static int _w2a = -1;
        if (_w2a < 0) _w2a = calypso_gate("CALYPSO_WATCH_2A00", 0);
        if (_w2a) {
            static unsigned _n2a = 0;
            if (_n2a++ < 80)
                fprintf(stderr, "[c54x] WATCH-2A00 opcode-write data[0x%04x]=0x%04x "
                        "(was 0x%04x) PC=0x%04x s=%p insn=%u\n",
                        addr, val, s->data[addr], s->pc, (void*)s, s->insn_count);
        }
    }
    /* WATCH-9200 (gate CALYPSO_WATCH_9200): the demod (0x9fab-0x9fb5) reads
     * 0x9210-0x9218 / 0x9260-0x9261 as its IQ source, but they stay constant
     * (0xff06/0x04a3), leaving workzone 0x2a00 flat. Traces every opcode write
     * to the region to find whether anything feeds it per frame, and from where. */
    if ((addr >= 0x9210 && addr <= 0x9220) || (addr >= 0x9260 && addr <= 0x9262)) {
        static int _w92 = -1;
        if (_w92 < 0) _w92 = calypso_gate("CALYPSO_WATCH_9200", 0);
        if (_w92) {
            static unsigned _n92 = 0;
            if (_n92++ < 80)
                fprintf(stderr, "[c54x] WATCH-9200 opcode-write data[0x%04x]=0x%04x (was 0x%04x) PC=0x%04x insn=%u\n",
                        addr, val, s->data[addr], s->pc, s->insn_count);
        }
    }
    /* WATCH-RESULT (gate CALYPSO_WATCH_RESULT): traces OPCODE writes to the
     * native FB result cells, confirmed against the osmocom NDB layout -
     * d_fb_det 0x08F8, a_sync_demod TOA/PM/ANGLE/SNR 0x08FA..0x08FD. Shows what
     * the native correlator produces even while d_fb_det stays 0. */
    if (addr >= 0x08F8 && addr <= 0x08FD) {
        static int _wr = -1;
        if (_wr < 0) _wr = calypso_gate("CALYPSO_WATCH_RESULT", 0);
        if (_wr) {
            static unsigned _nr = 0;
            const char *_nm = (addr==0x08F8)?"d_fb_det":(addr==0x08F9)?"d_fb_mode":
                              (addr==0x08FA)?"TOA":(addr==0x08FB)?"PM":
                              (addr==0x08FC)?"ANGLE":"SNR";
            if (_nr++ < 120)
                fprintf(stderr, "[c54x] WATCH-RESULT data[0x%04x]=%-8s 0x%04x (was 0x%04x) PC=0x%04x insn=%u\n",
                        addr, _nm, val, s->data[addr], s->pc, s->insn_count);
        }
    }
    /* WATCH-0810 (gate CALYPSO_WATCH_0810): traces every DSP-side write to
     * data[0x0810] (d_ctrl_system, B_TASK_ABORT is bit15) with the writing PC.
     * Companion of ARM-WRITE-0810 on the trx side: together they say who re-sets
     * bit15 after the ARM clears it. The CTRLSYS wire writes s->data[] directly,
     * bypassing data_write, so its absence here identifies it as the re-setter.
     * Cap 200. */
    if (addr == 0x0810) {
        static int w810 = -1;
        if (w810 < 0) w810 = calypso_gate("CALYPSO_WATCH_0810", 0);
        if (w810) {
            static unsigned n810 = 0;
            if (n810++ < 200)
                fprintf(stderr, "[c54x] WATCH-0810 DSP-write data[0x0810]=0x%04x "
                        "(was 0x%04x) PC=0x%04x insn=%u\n",
                        val, s->data[0x0810], s->pc, s->insn_count);
        }
    }
    /* WRITE-WATCH (read-only, cap 80): who writes 0x434f (FIFO write pointer),
     * 0x434e (FIFO read pointer) and 0x3f6d (go-live soft vector), and from which
     * PC. Separates an active write from a leftover reset value: whether the FIFO
     * is really initialized, and whether the soft vector is actively pointed at
     * 0xa4df. */
    if (addr == 0x434f || addr == 0x434e || addr == 0x3f6d) {
        static unsigned ww = 0;
        if (ww++ < 80)
            fprintf(stderr, "[c54x] WRITE-WATCH data[0x%04x] <- 0x%04x PC=0x%04x insn=%u\n",
                    addr, val, s->pc, s->insn_count);
    }
    /* F70-SETBIT1 (read-only, cap 40): bit1 of data[0x3f70] is the flag that
     * leaves the wait loop (tested at 0xa4d4, RET if TC). Logs only when bit1
     * (0x0002) is written; never firing means the DSP never raises "frame ready".
     * Known writers of 0x0002: 0x710c, 0xa5bd, 0xb3ef, 0xde9c. */
    if (addr == 0x3f70 && (val & 0x0002)) {
        static unsigned f70 = 0;
        if (f70++ < 40)
            fprintf(stderr, "[c54x] F70-SETBIT1 data[0x3f70] 0x%04x->0x%04x PC=0x%04x insn=%u\n",
                    s->data[0x3f70], val, s->pc, s->insn_count);
    }
    /* VEC-INSTALL (read-only, cap 200): the runtime vector table lives in DARAM
     * at 0x0080 (IPTR=1, OVLY). Measured cold: vec19 (FRAME) at 0xcc and vec21
     * (BRINT0) at 0xd4 are RETE (0xf4eb) / NOP stubs, while vec17/20/22/24..
     * carry real handlers (FB 0xf8xx). Watches the 8 words of those two slots
     * plus any word 0 of the table [0x80..0xFC] receiving an FB branch (0xf8xx),
     * to say whether the firmware ever installs a real branch there, and when. */
    if ((addr >= 0x00cc && addr <= 0x00cf) || (addr >= 0x00d4 && addr <= 0x00d7) ||
        ((addr >= 0x0080 && addr <= 0x00ff) && ((addr & 3) == 0) &&
         (val & 0xFF80) == 0xF880)) {
        static unsigned vi = 0;
        if (vi++ < 200) {
            int vec = (addr - 0x0080) / 4;
            int word = (int)((addr - 0x0080) & 3);
            fprintf(stderr, "[c54x] VEC-INSTALL vec%d@0x%04x w%d <- 0x%04x %s PC=0x%04x insn=%u\n",
                    vec, addr, word, val,
                    (vec==19?"<FRAME":(vec==21?"<BRINT0":"")),
                    s->pc, s->insn_count);
        }
    }
    /* TASKTAB-WR (read-only): who seeds data[0x4c5b/0x4c5c], the task table read
     * by LD *(0x4c5c),A followed by CALA, expected to come from the FRAME handler
     * at 0xA04C. No write means never seeded: CALA with A=0 lands in the boot
     * stub. */
    if (addr >= 0x4c5b && addr <= 0x4c5d) {
        static unsigned ttab_n = 0;
        if (ttab_n++ < 80)
            fprintf(stderr, "[c54x] TASKTAB-WR data[0x%04x] <- 0x%04x PC=0x%04x insn=%u\n",
                    addr, val, s->pc, s->insn_count);
    }
    /* ARM<->DSP coherence sentinel (CALYPSO_FBDET_SENTINEL=1): forces every DSP
     * write of d_fb_det (0x08f8) to 0xDEAD. The ARM reads that word through
     * arm=0x01f0 -> s->dsp->data[0x08f8] (calypso_trx.c), so the "ARM RD
     * d_fb_det" probe (TRX token) then shows:
     *   ARM reads 0xDEAD -> same cell, memory is coherent; look at PM/threshold.
     *   ARM reads 0x0000 -> ARM<->DSP desync. */
    {
        static int sent = -1;
        if (sent < 0) { const char *e = calypso_getenv("CALYPSO_FBDET_SENTINEL"); sent = e ? atoi(e) : 0;
            if (sent==1) fprintf(stderr, "[c54x] FBDET-SENTINEL=1 FORCE : data[0x08f8] forcé à 0xDEAD\n");
            else if (sent==2) fprintf(stderr, "[c54x] FBDET-SENTINEL=2 MONITOR : logge la vraie valeur écrite à 0x08f8 (pas de force)\n"); }
        {
            /* Force a_pm (rxlev) on the array the ARM actually reads:
             * calypso_trx.c reads s->dsp->data[off/2+0x800], not api_ram. The
             * DSP writes 0 there (no real measurement), so the calibrated
             * trf6151 value is substituted to keep rxlev stable. */
            {
                /* Hardware RSSI integrator model - the native pm_meas. On
                 * real Calypso the PM task (md=1) does NOT compute a_pm from
                 * the samples: the DSP zero-fills the result page (PROM0
                 * 0xb446, `stl *AR1+,A` in rptb 80), then an integrator reads a
                 * power register on the ABB/RF side and posts a_pm. That
                 * register is not in the modelled ADC, so it is derived from
                 * the real downlink magnitude the BSP measures. The
                 * MAV_REF -> RF_REF anchor is frontend calibration, like the
                 * trf6151 gain, not a decreed value: two different signals give
                 * two different a_pm. */
                static int rssi_on = -1;
                if (rssi_on < 0) {
                    rssi_on = calypso_gate("CALYPSO_PM_RSSI", 1);
                }
                /* db_r layout: d_task_ra(7), a_serv_demod[4](8..11),
                 * a_pm[3](12..14), a_sch[5](15..19). a_pm read page 0 = word 12
                 * = data[0x28+12+0x800] = data[0x834..0x836]; page 1 =
                 * data[0x3C+12+0x800] = data[0x848..0x84A]. 0x830/0x844 are
                 * a_serv_demod, not a_pm. */
                if (rssi_on && ((addr >= 0x0834 && addr <= 0x0836) ||
                            (addr >= 0x0848 && addr <= 0x084A))) {
                    val = calypso_bsp_rssi_apm_suivante(NULL);   /* next window, in order (calypso_bsp.c) */
                }
                if (addr == 0x0831 || addr == 0x0845) val = calypso_bsp_air_dpm(val);   /* D_PM, realistic air */
            }
        }
        if (sent && addr == 0x08f8) {
            uint16_t orig = val;
            if (sent == 1) val = 0xDEAD;   /* FORCE mode (coherence test) */
            static unsigned sn = 0;
            if (sn++ < 40 || (sn % 2000) == 0)
                fprintf(stderr, "[c54x] FBDET-SENTINEL #%u DSP write d_fb_det[0x08f8] orig=0x%04x%s PC=0x%04x insn=%u\n",
                        sn, orig, (sent==1) ? " ->0xDEAD" : " (monitor)", s->pc, s->insn_count);
        }
    }
    stkw_rec(s, addr, val);   /* ORPHAN: stack-write ring */
    /* ORPHAN: tracks direct stores in [0x1100..0x1140] (legitimate vector vs
     * untouched). Slots above SP_base are never reached by a push. */
    if (addr >= STKSLOT_LO && addr <= STKSLOT_HI) {
        int _si = addr - STKSLOT_LO;
        g_stkslot_wpc[_si] = s->pc;
        g_stkslot_wop[_si] = prog_fetch(s, s->pc);
        g_stkslot_written[_si] = 1;
    }
    /* MVPD overlay occupancy : count writes to [0x0080..0x27FF] during
     * boot phase. Env-gated CALYPSO_MVPD_TRACE=1. */
    if (g_mvpd_trace_enabled > 0) {
        mvpd_trace_record(addr);
    }
    /* FBMODE-WR: who writes d_fb_mode (0x08f9) and with which value (wide vs
     * narrow search)? Switching to narrow after the boot reject means no more
     * cold acquisition. */
    if (addr == 0x08f9) {
        static unsigned fm = 0;
        if (fm < 40) {
            fprintf(stderr, "[c54x] FBMODE-WR #%u d_fb_mode <- 0x%04x PC=0x%04x insn=%u\n",
                    fm, val, s->pc, s->insn_count);
            fm++;
        }
    }
    /* FBWATCH (env CALYPSO_FBWATCH, resolved once, outside the master gate) */
    if (g_fbwatch_on < 0) g_fbwatch_on = calypso_gate("CALYPSO_FBWATCH", 0);
    if (g_fbwatch_on) {
        /* (1) who writes the FB dispatch flag (data[0x60..0x70] / 0x3dc0..2)? */
        if ((addr >= 0x0060 && addr <= 0x0070) || (addr >= 0x3dc0 && addr <= 0x3dc2)) {
            static unsigned wf = 0;
            if (wf++ < 80)
                fprintf(stderr, "[c54x] FBWATCH-FLAG data[0x%04x] <- 0x%04x PC=0x%04x insn=%u%s\n",
                        addr, val, s->pc, s->insn_count, val ? "  *** NON-ZERO ***" : "");
        }
        /* (3) does the DSP ever write an FB detection (d_fb_det 0x08f8 non-zero)? */
        if (addr == 0x08f8 && val) {
            static unsigned wd = 0;
            if (wd++ < 40)
                fprintf(stderr, "[c54x] FBWATCH-DET d_fb_det <- 0x%04x PC=0x%04x insn=%u\n",
                        val, s->pc, s->insn_count);
        }
        /* (5) the producer flag: who writes data[0x585f], the foreground/ISR
         * status word? bit7 (0x0080) is polled by the foreground at 0xf7af,
         * bit8 (0x0100) by the ISRs. Never written, or bit7 never set, means the
         * flag has no producer. */
        if (addr == 0x585f) {
            static unsigned w5 = 0;
            if (w5++ < 80)
                fprintf(stderr, "[c54x] FBWATCH-585F data[0x585f] <- 0x%04x PC=0x%04x insn=%u%s\n",
                        val, s->pc, s->insn_count, (val & 0x0080) ? "  *** BIT7 SET ***" : "");
        }
        /* is the dispatch table populated? data[0x4c5b] = BACC A target at 0x7127 */
        if (addr == 0x4c5b) {
            static unsigned wt = 0;
            if (wt++ < 20)
                fprintf(stderr, "[c54x] FBWATCH-INITTAB-WR data[0x4c5b] <- 0x%04x PC=0x%04x insn=%u\n",
                        val, s->pc, s->insn_count);
        }
    }
    /* ANGLE-WR: who writes a_sync_demod ANGLE (0x08fc), TOA (0x08fa) and SNR
     * (0x08fd), the output of the real FCCH frequency detector. Captures A/B
     * (complex correlation) and the reference pointers AR3/4/5 at the store, to
     * name the actual frequency-correlation site. Cap 40. */
    /* [2026-09-19] A_SCH-WR: which R page does the ROM write its SB result to,
     * and what does d_dsp_page say at that instant? a_sch[0,1,3,4] sit at
     * 0x0837..0x083b (R page 0) and 0x084b..0x084f (R page 1) per calypso_api.h
     * (API_R_PAGE + RP_A_SCH). If the ROM writes the page the firmware is not
     * reading, the firmware decodes a stale cell -- which is what an SB result
     * that is wrong yet repeatable looks like. */
    if ((addr >= 0x0837 && addr <= 0x083b) || (addr >= 0x084b && addr <= 0x084f)) {
        static unsigned sw = 0;
        if (val != 0 && sw < 120) {
            int pg = (addr >= 0x084b);
            fprintf(stderr, "[c54x] A_SCH-WR #%u page=%d [0x%04x]<-0x%04x "
                    "d_dsp_page=0x%04x PC=0x%04x insn=%u\n",
                    sw, pg, addr, val, s->data[0x08d4], s->pc, s->insn_count);
            sw++;
        }
    }
    if (addr >= 0x08fa && addr <= 0x08fd) {
        static unsigned aw = 0, awz = 0;
        /* [2026-09-19] Only the NON-ZERO stores are of interest, and the cap of
         * 40 never reached them: measured, the first 40 writes are all 0x0000
         * from a single clearing site (PC 0xb2cf/b2d2/b2d5/b2d8, the four cells
         * in four consecutive instructions), so the probe filled up on the
         * memset and never showed the detector. Zeros are counted and reported
         * once every 2000 instead. */
        if (val == 0) {
            if (++awz % 2000 == 1)
                fprintf(stderr, "[c54x] ANGLE-WR (mise a zero x%u, PC=0x%04x)\n",
                        awz, s->pc);
        } else if (aw < 200) {
            int64_t a = (s->a & 0x8000000000LL) ? (int64_t)(s->a | ~0xFFFFFFFFFFLL) : (int64_t)s->a;
            int64_t b = (s->b & 0x8000000000LL) ? (int64_t)(s->b | ~0xFFFFFFFFFFLL) : (int64_t)s->b;
            const char *nm = addr==0x08fa?"TOA":addr==0x08fb?"PM":addr==0x08fc?"ANGLE":"SNR";
            fprintf(stderr, "[c54x] ANGLE-WR #%u %s[0x%04x]<-0x%04x PC=0x%04x A=%lld B=%lld T=%04x | "
                    "AR2=%04x[%04x] AR3=%04x[%04x] AR4=%04x[%04x] AR5=%04x[%04x] insn=%u\n",
                    aw, nm, addr, val, s->pc, (long long)a, (long long)b, s->t,
                    s->ar[2], s->data[s->ar[2]], s->ar[3], s->data[s->ar[3]],
                    s->ar[4], s->data[s->ar[4]], s->ar[5], s->data[s->ar[5]], s->insn_count);
            aw++;
        }
    }
    /* MTTCG lock: see data_read above. */
    qemu_mutex_lock(&calypso_pcb_daram_lock);
    data_write_locked(s, addr, val);
    qemu_mutex_unlock(&calypso_pcb_daram_lock);
}

/* FLOWTRACE: chronological R/W log of [0x2800,0x3000) into
 * /tmp/calypso_flow.txt, with a line budget in CALYPSO_FLOWTRACE. */
FILE *g_flow_f = NULL;
long  g_flow_budget = -2;
int   g_flow_armed = 0;
static void flow_log(const char *rw, uint16_t addr, uint16_t val, uint16_t pc, unsigned insn)
{
    if (g_flow_budget == -2) {
        const char *e = calypso_getenv("CALYPSO_FLOWTRACE");
        g_flow_budget = (e && *e) ? atol(e) : -1;
        if (g_flow_budget > 0) {
            g_flow_f = fopen("/tmp/calypso_flow.txt", "w");
            fprintf(stderr, "[c54x] FLOWTRACE armed budget=%ld -> /tmp/calypso_flow.txt\n", g_flow_budget);
        }
    }
    if (g_flow_budget <= 0 || !g_flow_f || !g_flow_armed) return;
    if (addr < 0x2800 || addr >= 0x3000) return;
    fprintf(g_flow_f, "%s %04x %04x pc=%04x insn=%u\n", rw, addr, val, pc, insn);
    if (--g_flow_budget == 0) { fflush(g_flow_f); fclose(g_flow_f); g_flow_f = NULL;
        fprintf(stderr, "[c54x] FLOWTRACE done -> /tmp/calypso_flow.txt\n"); }
}


/* WMAP: aggregated map of the writers of a data[] range. An aggregate, not a
 * stream: no line cap, so no window can be missed by truncation. Periodic output
 * gives PC, count, distinct values, min/max. */
struct c54x_g_wmap_s g_wmap[WMAP_PCS];
int      g_wmap_n;
uint32_t g_wmap_tot;
int      g_wmap_on = -1;
uint16_t g_wmap_lo, g_wmap_hi, g_wmap_lo2, g_wmap_hi2;

static void wmap_dump(void)
{
    fprintf(stderr, "[c54x] WMAP plages 0x%04x..0x%04x + 0x%04x..0x%04x  ecritures=%u  ecrivains=%d%s\n",
            g_wmap_lo, g_wmap_hi, g_wmap_lo2, g_wmap_hi2, g_wmap_tot, g_wmap_n,
            g_wmap_n >= WMAP_PCS ? "  *** TABLE SATUREE, ecrivains manquants ***" : "");
    for (int i = 0; i < g_wmap_n; i++) {
        fprintf(stderr, "[c54x] WMAP   PC=0x%04x n=%-7u @0x%04x(n=%u) distinct=%s%d  min=0x%04x max=0x%04x  ex:",
                g_wmap[i].pc, g_wmap[i].n, g_wmap[i].addr0, g_wmap[i].n0,
                g_wmap[i].nv >= 8 ? ">=" : "", g_wmap[i].nv,
                g_wmap[i].mn, g_wmap[i].mx);
        for (int k = 0; k < g_wmap[i].nv && k < 8; k++)
            fprintf(stderr, " %04x", g_wmap[i].v[k]);
        fprintf(stderr, "%s\n", g_wmap[i].n0 < 2 ? "   <= (trop peu d echantillons)"
                : g_wmap[i].nv == 1 ? "   <= CONSTANTE dans le temps"
                                    : "   <= VARIE dans le temps = PORTE DE LA DONNEE");
    }
}

/* Heartbeat: proves the probe is alive even at zero writes. Called on every
 * write outside the range; prints once per 5e6 global writes, repeating the
 * total INSIDE the range (0 there is a real zero). */
static void wmap_heartbeat(void)
{
    static uint64_t k;
    if (!g_wmap_on) return;
    if (++k % 5000000ULL) return;
    fprintf(stderr, "[c54x] WMAP heartbeat: writes DSP=%llu, dans plages=%u"
            " (0 = la plage n est jamais ecrite par une instruction DSP)\n",
            (unsigned long long)k, g_wmap_tot);
}

static void wmap_note(uint16_t addr, uint16_t val, uint16_t pc)
{
    if (g_wmap_on < 0) {
        const char *e = calypso_getenv("CALYPSO_WMAP");
        g_wmap_on = (e && atoi(e) > 0) ? 1 : 0;
        const char *lo = calypso_getenv("CALYPSO_WMAP_LO"), *hi = calypso_getenv("CALYPSO_WMAP_HI");
        g_wmap_lo = lo ? (uint16_t)strtoul(lo, NULL, 0) : 0x2c00;
        g_wmap_hi = hi ? (uint16_t)strtoul(hi, NULL, 0) : 0x2c1f;
        const char *lo2 = calypso_getenv("CALYPSO_WMAP_LO2"), *hi2 = calypso_getenv("CALYPSO_WMAP_HI2");
        g_wmap_lo2 = lo2 ? (uint16_t)strtoul(lo2, NULL, 0) : 0xffff;
        g_wmap_hi2 = hi2 ? (uint16_t)strtoul(hi2, NULL, 0) : 0x0000;
        if (g_wmap_on)
            fprintf(stderr, "[c54x] WMAP armed 0x%04x..0x%04x\n", g_wmap_lo, g_wmap_hi);
    }
    if (!g_wmap_on) return;
    if (!((addr >= g_wmap_lo  && addr <= g_wmap_hi) ||
          (addr >= g_wmap_lo2 && addr <= g_wmap_hi2))) { wmap_heartbeat(); return; }

    int i;
    for (i = 0; i < g_wmap_n; i++) if (g_wmap[i].pc == pc) break;
    if (i == g_wmap_n) {
        if (g_wmap_n >= WMAP_PCS) return;
        g_wmap_n++;
        g_wmap[i].pc = pc; g_wmap[i].n = 0; g_wmap[i].nv = 0;
        g_wmap[i].mn = 0xffff; g_wmap[i].mx = 0;
        g_wmap[i].addr0 = addr; g_wmap[i].n0 = 0;   /* fixed witness cell */
    }
    g_wmap[i].n++;
    if (val < g_wmap[i].mn) g_wmap[i].mn = val;
    if (val > g_wmap[i].mx) g_wmap[i].mx = val;
    /* The signal criterion is variation over TIME at a FIXED address. */
    if (addr != g_wmap[i].addr0) return;
    g_wmap[i].n0++;
    if (g_wmap[i].nv < 8) {
        int seen = 0;
        for (int k = 0; k < g_wmap[i].nv; k++) if (g_wmap[i].v[k] == val) { seen = 1; break; }
        if (!seen) g_wmap[i].v[g_wmap[i].nv++] = val;
    }
    /* Low threshold plus a periodic tick, so that ABSENCE is measurable. */
    if (++g_wmap_tot % WMAP_TICK == 0) wmap_dump();
}


/* DEMODIO: per-access trace of the demod loop (PC range + insn threshold). */
int      g_dio_on = -1;
uint64_t g_dio_after;
unsigned g_dio_n;
uint16_t g_dio_pclo, g_dio_pchi;

static void dio_init(void)
{
    const char *e = calypso_getenv("CALYPSO_DEMODIO");
    g_dio_on = (e && atoi(e) > 0) ? 1 : 0;
    const char *a = calypso_getenv("CALYPSO_DEMODIO_AFTER");
    g_dio_after = a && *a ? strtoull(a, NULL, 0) : 40000000ULL;
    const char *lo = calypso_getenv("CALYPSO_DEMODIO_PCLO"), *hi = calypso_getenv("CALYPSO_DEMODIO_PCHI");
    g_dio_pclo = lo ? (uint16_t)strtoul(lo, NULL, 0) : 0x9f95;
    g_dio_pchi = hi ? (uint16_t)strtoul(hi, NULL, 0) : 0x9fe2;
    if (g_dio_on)
        fprintf(stderr, "[c54x] DEMODIO armed PC 0x%04x..0x%04x apres insn=%llu\n",
                g_dio_pclo, g_dio_pchi, (unsigned long long)g_dio_after);
}

static void dio_note(C54xState *s, const char *rw, uint16_t addr, uint16_t val)
{
    if (g_dio_on < 0) dio_init();
    if (!g_dio_on) return;
    if (s->pc < g_dio_pclo || s->pc > g_dio_pchi) return;
    if (s->insn_count < g_dio_after) return;
    if (g_dio_n >= 160) return;
    int64_t a = (s->a & 0x8000000000LL) ? (int64_t)(s->a | ~0xFFFFFFFFFFLL) : (int64_t)s->a;
    int64_t b = (s->b & 0x8000000000LL) ? (int64_t)(s->b | ~0xFFFFFFFFFFLL) : (int64_t)s->b;
    /* ASM and raw ST1 are printed because the loop at 0x9fab-0x9fb8 builds the
     * correlator reference into 0x2a00+ with
     *     0x9fb5  ld  *AR6-0%, TS, A     (A = table(+-1) << T, T in [16,31])
     *     0x9fb6  sfta A, +8
     *     0x9fb7  sfta A, -8             (the two cancel out)
     *     0x9fb8  sth *AR4+, A, ASM      (0x8694 = the ASM variant)
     * and it only ever writes 0x0000 / 0xffff, i.e. {0, -1} instead of {+K, -K}:
     * a reference with |DC|/rms ~ 0.7, matching the 0.60 measured on
     * daram_2a00.cfile, which is why no correlation peaks. The high word of
     * (A >> |ASM|) is 0 for any magnitude below 2^16 but 0xffff for any negative
     * value (sign extension), so the asymmetry depends entirely on ASM and T. T
     * was already printed; ASM must be measured HERE, since an ASM read at
     * another PC belongs to another window. */
    int _asm = s->st1 & 0x1F; if (_asm & 0x10) _asm |= ~0x1F;
    fprintf(stderr, "[c54x] DEMODIO %s PC=0x%04x op=0x%04x addr=0x%04x val=0x%04x "
            "A=%lld B=%lld T=0x%04x ASM=%d ST1=0x%04x AR2=%04x AR3=%04x AR4=%04x AR5=%04x insn=%u\n",
            rw, s->pc, prog_fetch(s, s->pc), addr, val, (long long)a, (long long)b,
            s->t, _asm, s->st1, s->ar[2], s->ar[3], s->ar[4], s->ar[5], s->insn_count);
    g_dio_n++;
}

static void data_write_locked(C54xState *s, uint16_t addr, uint16_t val)
{
    /* [2026-09-19] A_SCH-WR2: the twin probe in data_write() never fired on
     * a_sch while its ANGLE-WR neighbour did, so these writes take the OTHER
     * path. Here the ADDRESS is the function's own argument, so the attribution
     * is exact -- unlike a before/after snapshot per instruction, which the ARM
     * writing the same shared mapping from another process can fool. */
    /* [2026-09-19] TOA-LAW: the firmware wants a ToA of 23 and never sees it;
     * the measured values form a ladder of 48 (48, 96, 144, 192...). The ROM
     * stores a_sync[TOA] at 0x795a and the stored word equals B, with T holding
     * the constant 48 loaded at 0x7953. Capture B, T and the pointers on every
     * such store to get the law instead of guessing it. */
    /* ANGLE-LAW: same treatment as the TOA, which has TWO producers separated
     * by AR2 (0x0cce = a grid, anything else = a real measurement). The angle is
     * written by the ROM at 0x798b; if it also has two populations, one of them
     * may measure correctly while the wrong one is consumed. */
    /* BUF-0cce: the TOA (grid of 48) and the angle (uncorrelated noise) are
     * BOTH degraded when AR2=0x0cce, i.e. what the correlator reads there does
     * not carry the signal. Direct test -- the FCCH is a pure tone, so at
     * 1 sample/symbol it rotates by +pi/2 per sample with a coherence ~1.
     * Coherence and dphi are measured ON THE BUFFER CONTENT at the moment the
     * FB task returns its result. */
    /* Pure write probes BUF-0cce, ANGLE-LAW: behind the C54X_SONDES switch. */
    if (C54X_SONDES) {
    if ((addr == 0x08fc || addr == 0x08fa) && val != 0 && s->ar[2] == 0x0cce) {
        static unsigned nb;
        if (nb < 20) {
            double ar=0, ai=0, den=0; int ns=148;
            const uint16_t BUF = 0x0cce;
            for (int k = 1; k < ns; k++) {
                double i0=(int16_t)s->data[BUF + 2*(k-1)], q0=(int16_t)s->data[BUF + 2*(k-1) + 1];
                double i1=(int16_t)s->data[BUF + 2*k],     q1=(int16_t)s->data[BUF + 2*k + 1];
                ar += i1*i0 + q1*q0;  ai += q1*i0 - i1*q0;
                den += sqrt((i0*i0+q0*q0)*(i1*i1+q1*q1));
            }
            double coh = den > 0 ? sqrt(ar*ar+ai*ai)/den : 0;
            double dphi = atan2(ai, ar);
            fprintf(stderr, "[c54x] BUF-0cce %s=%d : coherence=%.3f dphi=%+.3f "
                    "(FCCH pure vise coh~1, dphi=+1.571)\n",
                    addr == 0x08fa ? "TOA" : "ANGLE", (int)(int16_t)val, coh, dphi);
            nb++;
        }
    }
    if (addr == 0x08fc && val != 0) {
        static unsigned na;
        if (na < 300)
            fprintf(stderr, "[c54x] ANGLE-LAW <-%6d  AR2=%04x AR3=%04x AR4=%04x "
                    "AR5=%04x A=%lld B=%lld T=%04x PC=0x%04x\n",
                    (int)(int16_t)val, s->ar[2], s->ar[3], s->ar[4], s->ar[5],
                    (long long)s->a, (long long)s->b, s->t, s->pc);
        na++;
    }
    }   /* C54X_SONDES: BUF-0cce, ANGLE-LAW */
    if (addr == 0x08fa) {
        static unsigned nt;
        /* Two TOA producers come out of the SAME instruction 0x795a, separated
         * by AR2: 0x0cce gives 100% multiples of 48 (a grid, not a measurement),
         * any other pointer gives fine values. The origin is flagged to know
         * which one the SB task consumes. */
        if (s->pc == 0x795a) {
            g_toa_grille = (s->ar[2] == 0x0cce);
            g_toa_valeur = (int)(int16_t)val;
            g_toa_seq++;
        }
        /* The seam. A TOA falling 100% on a multiple of 48 samples = 96 words
         * = one DMA page means the buffer carries a discontinuity at every page
         * boundary. The words on both sides of the boundaries (96 and 192) of
         * buffer 0x0cce are printed: on a continuous GMSK burst, |IQ| is almost
         * constant and nothing should jump there. */
        /* g_toa_* above feeds the bridge (pont.c): stays active. COUTURE and
         * TOA-LAW are pure probes. */
        if (C54X_SONDES) {
        if (val != 0 && nt < 12 && s->pc == 0x795a) {
            const uint16_t B0 = 0x0cce;
            fprintf(stderr, "[c54x] COUTURE TOA=%d | p1 94,95 -> 96,97 : "
                    "%04x %04x | %04x %04x || p2 190,191 -> 192,193 : "
                    "%04x %04x | %04x %04x\n", (int)(int16_t)val,
                    s->data[B0 + ⟨1⟩], s->data[B0 + ⟨2⟩],  ×3
    ⟨⟩ = (94,95) (96,97) (190,191)
                    s->data[B0 + 192], s->data[B0 + 193]);
        }
        if (nt < 400)
            fprintf(stderr, "[c54x] TOA-LAW <-%5d  B=%lld T=%d A=%lld "
                    "AR2=%04x AR3=%04x AR4=%04x AR5=%04x PC=0x%04x\n",
                    (int)(int16_t)val, (long long)s->b, (int)(int16_t)s->t,
                    (long long)s->a, s->ar[2], s->ar[3], s->ar[4], s->ar[5], s->pc);
        nt++;
        }   /* C54X_SONDES: COUTURE, TOA-LAW */
    }
    /* A_SERV-WR: read_sb_result (prim_fbsb.c:148) reads
     * dsp_api.db_r->a_serv_demod[], i.e. the R PAGE at words 8..11 =
     * 0x0830..0x0833 (page 0) and 0x0844..0x0847 (page 1) -- NOT the NDB
     * a_sync_demod 0x08fa..0x08fd that read_fb_result reads. If nobody writes
     * those cells, the SB reads dead data: st->snr stays zero, the test
     * `snr > AFC_SNR_THRESHOLD` always fails and ALL the AFC measurements are
     * flagged invalid -- the sliding average then never emits. */
    /* SRC-3fa4: the four SB results come out of four work cells, read by the
     * ROM at 0xb1e7..0xb1f5:
     *   0x3fa4 -> TOA    0x3fa5 -> PM    0x3fa7 -> ANGLE    0x3fa6 -> SNR
     * The AFC needs the SNR to exceed AFC_SNR_THRESHOLD=2560 for 8 valid
     * measurements per window of 40. The probe traces who writes 0x3fa6, and
     * with what. */
    /* SNR-BINAIRE: the published SNR is either 16384 (0x4000, the "good
     * signal" constant the ROM also sets at 0x798d) or a one- or two-digit
     * value -- nothing in between. To tell a good burst from a bad one, the
     * coherence of buffer 0x0cce is measured AT WRITE TIME, together with the
     * multiframe phase: an FCCH (fn%51 in {0,10,20,30,40}) is a pure tone,
     * coherence ~1 and dphi=+pi/2 at 1 sample/symbol. */
    /* DECALAGE: the SNR verdict is NOT correlated with the buffer content;
     * when a real FCCH is there the DSP sees it perfectly (coherence 0.999,
     * dphi +1.565 for +1.571 theoretical), so the signal and the estimator are
     * fine and it is the INSTANT of the evaluation that is shifted relative to
     * the deposit. Each evaluation is paired with the last DMA deposit and the
     * frames separating them are counted. */
    /* CRC-CELL: the SB status is formed at 0x98b3/0x98b4 -- A takes 0x8000
     * (B_BLUD alone) then 0x8100, instruction 0x98b4 having 0x2bf8 and 0x0c08
     * as operands. If data[0x0c08] carries 0x0100, it is the CRC verdict cell,
     * and its writer is the decoder itself. */
    /* Pure probes CRC-CELL .. DISPTAB-WR (including dio_note, wmap_note,
     * flow_log): behind the switch. */
    if (C54X_SONDES) {
    if (addr == 0x0c08) {
        static unsigned n7;
        if (n7 < 60) {
            fprintf(stderr, "[c54x] CRC-CELL [0x0c08] <- 0x%04x  PC=0x%04x "
                    "A=%lld B=%lld T=%04x AR2=%04x AR3=%04x insn=%u\n",
                    val, s->pc, (long long)s->a, (long long)s->b, s->t,
                    s->ar[2], s->ar[3], s->insn_count);
            n7++;
        }
    }
    if (addr == 0x3fa6) {
        extern unsigned g_depot_fn, g_depot_seq;
        static unsigned n6;
        if (n6 < 60) {
            unsigned fn_eval = calypso_daram_last_fn;
            fprintf(stderr, "[c54x] DECALAGE snr=%-6d eval_fn=%-7u p51=%-2u | "
                    "dernier depot fn=%-7u p51=%-2u | ecart=%d trames\n",
                    (int)(int16_t)val, fn_eval, fn_eval % 51u,
                    g_depot_fn, g_depot_fn % 51u, (int)(fn_eval - g_depot_fn));
            n6++;
        }
    }
    if (addr == 0x3fa6) {
        static unsigned n5;
        if (n5 < 400) {
            double ar=0, ai=0, den=0; const uint16_t BUF = 0x0cce; int ns=148;
            for (int k = 1; k < ns; k++) {
                double i0=(int16_t)s->data[BUF + 2*(k-1)], q0=(int16_t)s->data[BUF + 2*(k-1) + 1];
                double i1=(int16_t)s->data[BUF + 2*k],     q1=(int16_t)s->data[BUF + 2*k + 1];
                ar += i1*i0 + q1*q0;  ai += q1*i0 - i1*q0;
                den += sqrt((i0*i0+q0*q0)*(i1*i1+q1*q1));
            }
            double coh = den > 0 ? sqrt(ar*ar+ai*ai)/den : 0;
            unsigned fn = calypso_daram_last_fn;
            fprintf(stderr, "[c54x] SNR-BINAIRE snr=%-6d fn=%-7u p51=%-2u "
                    "coherence=%.3f dphi=%+.3f %s\n",
                    (int)(int16_t)val, fn, fn % 51u, coh, atan2(ai, ar),
                    (fn % 51u) % 10 == 0 && (fn % 51u) <= 40 ? "<- trame FCCH" : "");
            n5++;
        }
    }
    if (addr >= 0x3fa4 && addr <= 0x3fa7) {
        static unsigned n4;
        if (n4 < 80) {
            static const char *q[4] = { "TOA(3fa4)", "PM(3fa5)", "SNR(3fa6)", "ANGLE(3fa7)" };
            fprintf(stderr, "[c54x] SRC-3fa4 %-11s <- %6d  PC=0x%04x A=%lld B=%lld "
                    "T=%04x AR2=%04x AR3=%04x insn=%u\n",
                    q[addr - 0x3fa4], (int)(int16_t)val, s->pc,
                    (long long)s->a, (long long)s->b, s->t, s->ar[2], s->ar[3],
                    s->insn_count);
            n4++;
        }
    }
    if ((addr >= 0x0830 && addr <= 0x0833) || (addr >= 0x0844 && addr <= 0x0847)) {
        static unsigned n3;
        if (n3 < 600) {
            static const char *nm[4] = { "TOA", "PM", "ANGLE", "SNR" };
            int pg = (addr >= 0x0844);
            int idx = addr - (pg ? 0x0844 : 0x0830);
            fprintf(stderr, "[c54x] A_SERV-WR page=%d %-5s [0x%04x] <- %6d "
                    "PC=0x%04x AR2=%04x insn=%u\n",
                    pg, nm[idx & 3], addr, (int)(int16_t)val, s->pc, s->ar[2],
                    s->insn_count);
            n3++;
        }
    }
    if ((addr >= 0x0837 && addr <= 0x083b) || (addr >= 0x084b && addr <= 0x084f)) {
        static unsigned n2;
        if (n2 < 60) {
            fprintf(stderr, "[c54x] A_SCH-WR2 [0x%04x] <- 0x%04x PC=0x%04x "
                    "A=%lld B=%lld T=%04x AR2=%04x AR3=%04x insn=%u\n",
                    addr, val, s->pc, (long long)s->a, (long long)s->b, s->t,
                    s->ar[2], s->ar[3], s->insn_count);
            n2++;
        }
    }
    { static long _dwl_n = 0; static int _dwl_on = -1; if (_dwl_on < 0) _dwl_on = calypso_getenv("CALYPSO_DWL_PROVE") ? 1 : 0;
      if (_dwl_on && (_dwl_n++ % 100000) == 0)
        fprintf(stderr, "[c54x] DWL-PROVE appel #%ld addr=0x%04x pc=0x%04x\n", _dwl_n, addr, s->pc); }
    {   /* FBCNT-WATCH - CALYPSO_FBROUTE=1 (same gate as FBROUTE, of which this
         * is the direct sequel). Read-only, capped.
         *
         * [2026-08-03] Since the DMA delivers samples the FB routine finally
         * runs (FBROUTE: ENTER twice, high-water 0x794e - the zone was never
         * entered before), which makes the guard at 0x79e3 measurable:
         *     at 0x7720, DP=0x083 -> dma(0x7e) = data[0x41fe] = 1, 2, 3, 3, 3, 3
         *     the guard requires == 4.
         * The counter tops out at 3, exactly one short. A candidate explanation
         * is that the DMA delivers the burst in 4 pages (ALGTH=192 -> 96 words,
         * burst=296) while the counter counts 3; this watch says who writes the
         * cell and when, which confirms or kills it.
         *
         * Known limit: 0x41fe is the address resolved at 0x7720 (DP=0x083). The
         * guard is at 0x79e3, which is never reached, so there is no proof it
         * reads the same cell. The inference is reasonable (same direct
         * addressing at 0x7e, same routine, DP constant over 6 samples) but
         * UNPROVEN. An incoherent writer in this watch indicts that inference
         * first, not the counter. */
        static int _fc = -1;
        if (_fc < 0) _fc = calypso_gate("CALYPSO_FBROUTE", 0);
        if (_fc && addr == 0x41fe) {
            static unsigned _n = 0;
            if (_n < 60) {
                _n++;
                fprintf(stderr,
                        "[c54x] FBCNT-WR #%u data[0x41fe] 0x%04x -> 0x%04x "
                        "PC=0x%04x A=0x%06llx insn=%u\n",
                        _n, s->data[0x41fe], val, s->last_exec_pc,
                        (unsigned long long)(s->a & 0xFFFFFFULL), s->insn_count);
                fflush(stderr);
            }
        }
    }
    {   /* WATCH-3FB4 (gate CALYPSO_WATCH_3FB4, read-only, capped): state/slot
         * index of the FB search. The coarse TOA (mode 0, branch A at 0x791c) is
         * (data[0x3fb4]-3)*T, so this says whether the index advances
         * (0->1->2...) or stays stuck at 0. DSP writers: 0x7744, 0x7776. */
        static int _w4 = -1;
        if (_w4 < 0) _w4 = calypso_gate("CALYPSO_WATCH_3FB4", 0);
        if (_w4 && addr == 0x3fb4) {
            static unsigned _n4 = 0;
            if (_n4 < 80) {
                _n4++;
                fprintf(stderr, "[c54x] WATCH-3FB4 data[0x3fb4] 0x%04x -> 0x%04x PC=0x%04x "
                        "T=0x%04x A=0x%06llx insn=%u\n", s->data[0x3fb4], val, s->pc,
                        s->t, (unsigned long long)(s->a & 0xFFFFFFULL), s->insn_count);
                fflush(stderr);
            }
        }
    }
    {   /* DTASKD-WATCH, leg 3/3 - CALYPSO_DTASKD_WATCH=1, default 0. Read-only,
         * capped. Legs 1 and 2 are in calypso_trx.c.
         *
         * The firmware console shows, every cycle, l1s_nb_cmd() requesting a
         * reception and l1s_nb_resp() printing EMPTY (prim_rx_nb.c:74), whose
         * test is
         *     if (dsp_api.db_r->d_task_d == 0) { puts("EMPTY\n"); return 0; }
         *
         * Trap: db_w->d_task_d and db_r->d_task_d are NOT the same cell. They are
         * two distinct firmware structures (osmocom-bb dsp_api.h:20-23, d_task_d
         * at offset 0 in both):
         *     db_w = T_DB_MCU_TO_DSP @ 0xFFD00000 (p0) / 0xFFD00028 (p1)
         *     db_r = T_DB_DSP_TO_MCU @ 0xFFD00050 (p0) / 0xFFD00078 (p1)
         * So EMPTY does not mean the ARM write was lost; it means the DSP never
         * copied the task into its RESPONSE page. This leg settles that.
         *
         * Matching DSP words (api = data[0x0800 + ARM_offset/2]):
         *     W p0 = data[0x0800]   W p1 = data[0x0814]   <- written by the ARM
         *     R p0 = data[0x0828]   R p1 = data[0x083C]   <- must be written here
         *
         * DISPATCH-PROBE already shows AR2 alternating between 0x0828 and 0x083c,
         * exactly the two R page bases: the DSP holds the right pointer, the
         * question is whether it writes the cell.
         *
         * The periodic summary prints the counters even at zero, so no line is
         * ambiguous between "the DSP does not write" and "the probe is off". */
        static int _dw = -1;
        if (_dw < 0) {
            _dw = calypso_gate("CALYPSO_DTASKD_WATCH", 0);
            if (_dw) {
                fprintf(stderr, "[dtaskd] patte 3/3 armee (ecritures DSP) : "
                        "R p0=data[0x0828] R p1=data[0x083C]\n");
                fflush(stderr);
            }
        }
        if (_dw && (addr == 0x0828 || addr == 0x083C)) {
            static unsigned long long _n0 = 0, _n1 = 0, _nz = 0;
            unsigned long long _n = (addr == 0x0828) ? ++_n0 : ++_n1;
            if (val) _nz++;
            if (_n <= 40 || (_n % 5000) == 0) {
                fprintf(stderr,
                        "[dtaskd] DSP>WR  R p%d  data[0x%04x] <- 0x%04x  "
                        "(non_nuls=%llu  p0=%llu p1=%llu)  PC=0x%04x insn=%u\n",
                        (addr == 0x0828) ? 0 : 1, addr, val,
                        _nz, _n0, _n1, s->last_exec_pc, s->insn_count);
                fflush(stderr);
            }
        }
    }
    dio_note(s, "W", addr, val);
    wmap_note(addr, val, s->pc);
    flow_log("W", addr, val, s->pc, s->insn_count);
    {   /* WZWRITE: who fills the MAC kernel input cell 0x2c00? */
        static int _wz = -1; static unsigned _wzn = 0;
        if (_wz < 0) _wz = calypso_gate("CALYPSO_WZWRITE", 0);
        if (_wz && addr == 0x2c00 && (s->pc == 0x9fd5 || s->pc == 0x9ab1) && _wzn < 40) {
            /* Filtered on the producing PC: the MAC init sites
             * 0xa03d/0xa042/0xa079 are excluded, they used to saturate the cap
             * and hide the bursts that follow. */
            _wzn++;
            fprintf(stderr, "[c54x] WZWRITE data[0x%04x] <- 0x%04x PC=0x%04x op=0x%04x "
                    "A=0x%010llx AR2=%04x AR3=%04x AR4=%04x AR5=%04x insn=%u\n",
                    addr, val, s->pc, prog_fetch(s, s->pc),
                    (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                    s->ar[2], s->ar[3], s->ar[4], s->ar[5], s->insn_count);
        }
    }
    {   /* DMAQ-RESET (gate CALYPSO_DMAQ, cap 30): does the DMA queue write
         * pointer return to its base without having been consumed - producer
         * pushes, a reset wipes, consumer finds the queue empty? */
        static int _dqr = -1; static unsigned _dqrn = 0;
        if (_dqr < 0) _dqr = calypso_gate("CALYPSO_DMAQ", 0);
        if (_dqr && _dqrn < 30 && (addr == 0x433f || addr == 0x433e)
            && val == 0x4330 && s->data[addr] != 0x4330) {
            _dqrn++;
            fprintf(stderr, "[c54x] DMAQ-RESET data[0x%04x] 0x%04x -> 0x4330 "
                    "PC=0x%04x (la file contenait %u element(s)) insn=%u\n",
                    addr, s->data[addr], s->pc,
                    (unsigned)((s->data[0x433f] - s->data[0x433e]) & 0xFFFF),
                    s->insn_count);
        }
    }
    {   /* ERRWATCH: who sets d_error_status (0x08D5), and with which bit? */
        static int _ew = -1; static unsigned _ewn = 0;
        if (_ew < 0) _ew = calypso_gate("CALYPSO_ERRWATCH", 0);
        /* Writes of 0 are ignored: they are the boot cleanup and used to
         * consume the whole cap. */
        if (_ew && addr == 0x08D5 && val != 0 && _ewn < 60) {
            _ewn++;
            const char *_b = (val & 0x0800) ? "STACK_OV" : (val & 0x0400) ? "DMA_UL_PEND" :
                             (val & 0x0200) ? "DMA_UL_PROG" : (val & 0x0100) ? "DMA_UL_TASK" :
                             (val & 0x0080) ? "VM" : (val & 0x0020) ? "DMA_PEND" :
                             (val & 0x0010) ? "DMA_TASK" : (val & 0x0008) ? "DMA_PROG" :
                             (val & 0x0004) ? "IQ_SAMPLES" : (val & 0x0001) ? "RHEA" : "(clear)";
            fprintf(stderr, "[c54x] ERRWATCH data[0x%04x] 0x%04x -> 0x%04x [%s] "
                    "PC=0x%04x op=0x%04x A=0x%06llx AR1=%04x AR2=%04x AR6=%04x insn=%u\n",
                    addr, s->data[addr], val, _b, s->pc, prog_fetch(s, s->pc),
                    (unsigned long long)(s->a & 0xFFFFFFULL),
                    s->ar[1], s->ar[2], s->ar[6], s->insn_count);
        }
    }
    {   /* DMAWATCH (write side). */
        static int _dw3 = -1; static unsigned _dww = 0;
        if (_dw3 < 0) _dw3 = calypso_gate("CALYPSO_DMAWATCH", 0);
        if (_dw3 && addr >= 0x0054 && addr <= 0x0057 && _dww < 40) {
            _dww++;
            fprintf(stderr, "[c54x] DMAWATCH WR 0x%04x <- 0x%04x (etait 0x%04x) PC=0x%04x insn=%u\n",
                    addr, val, s->data[addr], s->pc, s->insn_count);
        }
    }
    {   /* BOOTCMD, DSP side: who overwrites the boot command cell? */
        static int _bc2 = -1; static unsigned _bc2n = 0;
        if (_bc2 < 0) _bc2 = calypso_gate("CALYPSO_BOOTCMD", 0);
        if (_bc2 && addr >= 0x0FFC && addr <= 0x0FFF && _bc2n < 40) {
            _bc2n++;
            fprintf(stderr, "[c54x] BOOTCMD DSP data[0x%04x] 0x%04x -> 0x%04x PC=0x%04x insn=%u%s\n",
                    addr, s->data[addr], val, s->pc, s->insn_count,
                    (addr == 0x0FFF) ? "   <<<< CELLULE DE COMMANDE" : "");
        }
    }
    {   /* DISPTAB-WR: who fills the dispatch table 0x4380..0x43cf? */
        static int _dt = -1; static unsigned _dtn = 0;
        if (_dt < 0) _dt = calypso_gate("CALYPSO_DISPTAB", 0);
        if (_dt && addr >= 0x4380 && addr <= 0x43cf && _dtn < 60) {
            _dtn++;
            fprintf(stderr, "[c54x] DISPTAB-WR data[0x%04x] <- 0x%04x PC=0x%04x insn=%u\n",
                    addr, val, s->pc, s->insn_count);
        }
    }
    }   /* C54X_SONDES : CRC-CELL .. DISPTAB-WR */
    {
        /* @BEQUILLE - DEMOD_NOCLOBBER  (CALYPSO_DEMOD_NOCLOBBER, default OFF)
         *   masque  : the emulated demod stage (PC 0x9fb8 = I, 0x9fe2 = Q) fills
         *             the correlator input buffer [0x2a00,0x2b28) with constant
         *             pairs (0000,52ed) and overwrites the real FCCH laid down
         *             by feed_iq; the real branch is a demod that consumes the
         *             RX I/Q instead of producing constants. Here the write is
         *             DROPPED (return) instead of the producer being fixed.
         *   retirer : as soon as the demod stage 0x9f95-0x9fe2 reads a real I/Q
         *             source, or FB_IQ_OWNS=1 makes feed_iq the sole owner of
         *             0x2a00.
         */
        static int _nc = -1;
        if (_nc < 0) { const char *e = calypso_getenv("CALYPSO_DEMOD_NOCLOBBER"); _nc = (e && atoi(e) > 0) ? 1 : 0; }
        if (_nc && addr >= 0x2a00 && addr < 0x2b28 &&
            (s->pc == 0x9fb8 || s->pc == 0x9fe2)) {
            static unsigned _ncn = 0;
            if (_ncn++ < 8)
                fprintf(stderr, "[c54x] DEMOD-NOCLOBBER skip PC=0x%04x data[0x%04x] <- 0x%04x "
                        "(ecriture demod ignoree ; l alimentation vient de rx_burst sauf si FB_IQ_OWNS=1)\n", s->pc, addr, val);
            return;
        }
    }
    /* [2026-09-23] Sondes pures B4 .. WATCH-VEC : derriere l'interrupteur. */
    if (C54X_SONDES) {
    /* B4 (gate CALYPSO_B4): watchpoint on d_fb_det (0x08f8) - separates "the DSP
     * writes 0" (correlator concludes negative) from "never written" (path not
     * reached). Two different bugs. */
    if (addr == 0x08f8) {
        static int _b4 = -1; static unsigned _b4n = 0;
        if (_b4 < 0) _b4 = calypso_gate("CALYPSO_B4", 0);
        if (_b4 && _b4n < 64) {
            _b4n++;
            fprintf(stderr, "[c54x] B4-DFBDET-WR data[0x08f8] 0x%04x -> 0x%04x PC=0x%04x xpc=%u insn=%u\n",
                    s->data[0x08f8], val, s->pc, s->xpc, s->insn_count);
        }
    }
    /* B1 boot-copy watch (gate CALYPSO_B1): writes into the reference table
     * [0x2c00,0x2c10) with the source PC, to confirm the 0x76f8 -> 0x2c00 boot
     * copy and name its writer. */
    if (addr >= 0x2c00 && addr < 0x2c10) {
        static int _b1w = -1; static unsigned _b1wn = 0;
        if (_b1w < 0) _b1w = calypso_gate("CALYPSO_B1", 0);
        if (_b1w && _b1wn < 64 && val != 0) {
            _b1wn++;
            fprintf(stderr, "[c54x] B1-BOOTCOPY-WR data[0x%04x] 0x%04x -> 0x%04x PC=0x%04x xpc=%u insn=%u\n",
                    addr, s->data[addr], val, s->pc, s->xpc, s->insn_count);
        }
    }
    /* MEM-WATCH-2B80 (gate CALYPSO_MEM_WATCH_2B80): twin of the read-side probe
     * in data_read_locked. Logs every write in [0x2b80,0x2c00), cap 200, to say
     * whether a boot copy ever populates the region before the correlator reads
     * it. */
    if (addr >= 0x2b80 && addr < 0x2c00) {
        static int mw2b80_wen = -1;
        if (mw2b80_wen < 0) mw2b80_wen = calypso_gate("CALYPSO_MEM_WATCH_2B80", 0);
        if (mw2b80_wen) {
            static unsigned mw2b80_wn = 0;
            if (mw2b80_wn < 200) {
                mw2b80_wn++;
                fprintf(stderr, "[c54x] MEM-WATCH-2B80-WR data[0x%04x] 0x%04x -> 0x%04x "
                        "PC=0x%04x insn=%u\n", addr, s->data[addr], val, s->pc,
                        s->insn_count);
            }
        }
    }
    /* STATE-WR: transitions of the handler pointer data[0x3f5e] (L1 state
     * machine) together with the API control cells 0x0908/0x0909 at the same
     * instant. Shows whether the scheduler advances the state or stays pinned on
     * 0x7013 (FB), and from which PC. */
    if (addr == 0x3f5e) {
        static unsigned stwr = 0;
        if (stwr++ < 80)
            fprintf(stderr, "[c54x] STATE-WR data[0x3f5e] 0x%04x -> 0x%04x PC=0x%04x "
                    "api[0x908]=0x%04x api[0x909]=0x%04x api[0x945]=0x%04x insn=%u\n",
                    s->data[0x3f5e], val, s->pc, s->data[0x0908], s->data[0x0909],
                    s->data[0x0945], s->insn_count);
    }

    /* TASKPTR-WR: who writes data[0x0c36], the task pointer CALA'd at 0xb3a5
     * (null -> derail)? Catches DSP writes including indirect ones through AR. */
    if (addr == 0x0c36) {
        static unsigned tw = 0;
        if (tw++ < 60)
            fprintf(stderr, "[c54x] TASKPTR-WR data[0x0c36] 0x%04x -> 0x%04x PC=0x%04x "
                    "XPC=%u AR[0..7]=%04x,%04x,%04x,%04x,%04x,%04x,%04x,%04x insn=%u\n",
                    s->data[0x0c36], val, s->pc, s->xpc,
                    s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                    s->ar[4], s->ar[5], s->ar[6], s->ar[7], s->insn_count);
    }

    /* STATE435B-WR (cap 40): who writes data[0x435b], the state word of the
     * go-live state machine at 0xa4e4, whose bits 0x10/0x40/0x100 gate both its
     * progress and the INTM enable. 0 keeps the state machine blocked. */
    if (addr == 0x435b) {
        static unsigned sw=0;
        if (sw++ < 40)
            fprintf(stderr, "[c54x] STATE435B-WR data[0x435b] 0x%04x -> 0x%04x PC=0x%04x A=0x%06llx insn=%u\n",
                    s->data[0x435b], val, s->pc,
                    (unsigned long long)(s->a & 0xFFFFFFULL), s->insn_count);
    }
    /* WATCH-09BC-WR (cap 40, read-only): the go-live gate at 0xa544
     * (BITF *(0x09bc),1 ; BC 0xa549 if NTC) skips the only native BACC
     * (0xa546-0xa548, through d[0x3fe0]=0x70ce) towards the operational bootstrap
     * 0x7102 CALL 0xd247 unless bit0 of data[0x09bc] is already set. A full scan
     * of PROM0-3 found no clear ORM/SET of that bit (only two ANDM clears at
     * 0x70fc/0x712d, plus an ambiguous site at 0xcf53). This confirms at runtime
     * who writes 0x09bc and with what, and whether bit0 is ever set natively. */
    if (addr == 0x09bc) {
        static unsigned w9 = 0;
        if (w9++ < 40)
            fprintf(stderr, "[c54x] WATCH-09BC-WR data[0x09bc] 0x%04x -> 0x%04x (bit0 %d->%d) "
                    "PC=0x%04x A=0x%06llx insn=%u\n",
                    s->data[0x09bc], val, s->data[0x09bc] & 1, val & 1, s->pc,
                    (unsigned long long)(s->a & 0xFFFFFFULL), s->insn_count);
    }
    /* WATCH-000B-WR (cap 40, read-only): data[0x000b] is tested twice by the
     * background dispatcher (0xdeb6: BITF *(0x000b),0x4000 ; 0xdec2: BITF
     * *(0x000b),0x2000 - bits 14 and 13). Confirms at runtime whether anything
     * writes it natively. If it is never written, BITF always reads 0, TC is
     * always false and that path never unblocks the loop. */
    if (addr == 0x000b) {
        static unsigned wb = 0;
        if (wb++ < 40)
            fprintf(stderr, "[c54x] WATCH-000B-WR data[0x000b] 0x%04x -> 0x%04x "
                    "PC=0x%04x A=0x%06llx insn=%u\n",
                    s->data[0x000b], val, s->pc,
                    (unsigned long long)(s->a & 0xFFFFFFULL), s->insn_count);
    }
    /* WATCH-0810-WR (cap 40): data[0x0810] = db_w->d_ctrl_system (MCU->DSP write
     * page, offset 16), named through osmocom-bb dsp_api.h:75 "Control Register
     * for RESET/RESUME". Bit15 = B_TASK_ABORT (l1_environment.h:365), set by the
     * ARM in l1s_reset(). The DSP tests it at 0xa53c/0xa53f
     * (BITF *(AR1+0x10),0x8000 with AR1=0x0800): bit15 SET falls through
     * a541-a544-a546-0x09bc-0xd247, the operational bootstrap; bit15 CLEAR jumps
     * to 0xa575 and never bootstraps. Says if and when the ARM writes the bit,
     * and its value when the DSP reads it. */
    if (addr == 0x0810) {
        static unsigned w810 = 0;
        if (w810++ < 40)
            fprintf(stderr, "[c54x] WATCH-0810-WR data[0x0810] 0x%04x -> 0x%04x "
                    "(bit15/B_TASK_ABORT %d->%d) PC=0x%04x insn=%u\n",
                    s->data[0x0810], val, !!(s->data[0x0810] & 0x8000), !!(val & 0x8000),
                    s->pc, s->insn_count);
    }
    /* DISPATCH-CELL-RESEED (read-only): data[0x43d8]/[0x3fd4]/[0x4368] measured
     * constant (0xab38/0xc1fa/0xaff9) over the whole observed runtime. Watches
     * writes to prove or refute that they are ever reseeded to anything else. */
    if (addr == 0x43d8 || addr == 0x3fd4 || addr == 0x4368) {
        static unsigned _ndcr = 0;
        if (_ndcr++ < 30)
            fprintf(stderr, "[c54x] DISPATCH-CELL-RESEED data[0x%04x] 0x%04x -> 0x%04x "
                    "PC=0x%04x insn=%u\n", addr, s->data[addr], val, s->pc, s->insn_count);
    }
    /* FBDET-WR (read-only, uncapped): data[0x08F8] = d_fb_det, the NDB field the
     * ARM firmware reads to learn whether the correlator found an FCCH. Critical
     * field, so every write is logged. */
    if (addr == 0x08F8) {
        fprintf(stderr, "[c54x] FBDET-WR data[0x08F8] 0x%04x -> 0x%04x PC=0x%04x insn=%u\n",
                s->data[0x08F8], val, s->pc, s->insn_count);
    }
    /* READY-WR (cap 40): who populates the go-live readiness. 0xaad5 reads
     * *data[0x434e] = *0x4340 and *data[0x434f] = *0x7f75; A=0 makes 0xa4cd skip
     * the enable. Watches both the pointers (0x434e/0x434f) and the targets
     * (0x4340/0x7f75). */
    if (addr == 0x434e || addr == 0x434f || addr == 0x4340 || addr == 0x7f75) {
        static unsigned rw=0;
        if (rw++ < 40)
            fprintf(stderr, "[c54x] READY-WR data[0x%04x] 0x%04x -> 0x%04x PC=0x%04x A=0x%06llx insn=%u\n",
                    addr, s->data[addr], val, s->pc,
                    (unsigned long long)(s->a & 0xFFFFFFULL), s->insn_count);
    }
    /* OVLD-WR (cap 30): writes to the overlay ISR cells 0x0154/0x0155/0x013b. */
    if (addr == 0x0155 || addr == 0x013b || addr == 0x0154) {
        static unsigned ov=0;
        if (ov++ < 30)
            fprintf(stderr, "[c54x] OVLD-WR data[0x%04x] 0x%04x -> 0x%04x PC=0x%04x A=0x%06llx insn=%u\n",
                    addr, s->data[addr], val, s->pc,
                    (unsigned long long)(s->a & 0xFFFFFFULL), s->insn_count);
    }
    /* 3FCD-WR (cap 40): who writes data[0x3fcd], the target of the RET at
     * 0x0157 - the frame handler 0x013b does PSHD *(0x3fcd) then RET, so it jumps
     * to data[0x3fcd]. 0 here derails. */
    if (addr == 0x3fcd || addr == 0x3fce || addr == 0x3fcf) {
        static unsigned f3=0;
        if (f3++ < 40)
            fprintf(stderr, "[c54x] 3FCD-WR data[0x%04x] 0x%04x -> 0x%04x PC=0x%04x A=0x%06llx XPC=%u insn=%u\n",
                    addr, s->data[addr], val, s->pc,
                    (unsigned long long)(s->a & 0xFFFFFFULL), s->xpc, s->insn_count);
    }
    /* DISPPTR-WR: who writes data[0x43c0], the dispatch pointer read by the
     * 0xb40e/0xb40f BACC trampoline, measured at 0xf074 - a table address, hence
     * a derail. */
    if (addr == 0x43c0) {
        static unsigned dw = 0;
        if (dw++ < 60)
            fprintf(stderr, "[c54x] DISPPTR-WR data[0x43c0] 0x%04x -> 0x%04x PC=0x%04x "
                    "A=0x%010llx XPC=%u insn=%u\n",
                    s->data[0x43c0], val, s->pc,
                    (unsigned long long)(s->a & 0xFFFFFFFFFFULL), s->xpc, s->insn_count);
    }

    /* DISPVAL-WR (cap 60): who writes the value 0xf074 (the LUT base, the faulty
     * BACC target) into the dispatch table 0x4300-0x43ff? Names whoever plants
     * the corrupt handler pointer. */
    if (val == 0xf074 && addr >= 0x4300 && addr < 0x4400) {
        static unsigned dvw = 0;
        if (dvw++ < 60)
            fprintf(stderr, "[c54x] DISPVAL-WR data[0x%04x] <- 0xf074 PC=0x%04x "
                    "A=0x%010llx XPC=%u insn=%u\n",
                    addr, s->pc,
                    (unsigned long long)(s->a & 0xFFFFFFFFFFULL), s->xpc, s->insn_count);
    }

    /* SLOT4387-WR (cap 40): does anything ever rewrite the terminal dispatch slot
     * data[0x4387] with a real handler rather than the 0xab38 idle RET? */
    if (addr == 0x4387) {
        static unsigned s4=0;
        if (s4++<12) {
            /* Full context: dump AR/A/B/DP/ST0 and the 6 program words
             * around the store, to reconstruct the index computation of the
             * 0xab10 table. */
            fprintf(stderr, "[c54x] SLOT4387-WR data[0x4387] <- 0x%04x PC=0x%04x insn=%u\n"
                    "         A=0x%06llx B=0x%06llx T=0x%04x DP=0x%03x ST0=0x%04x\n"
                    "         AR0=%04x AR1=%04x AR2=%04x AR3=%04x AR4=%04x AR5=%04x AR6=%04x AR7=%04x\n"
                    "         prog[pc-3..pc+2]=%04x %04x %04x %04x %04x %04x\n",
                    val, s->pc, s->insn_count,
                    (unsigned long long)(s->a & 0xFFFFFFULL), (unsigned long long)(s->b & 0xFFFFFFULL),
                    s->t, (s->st0 & ST0_DP_MASK), s->st0,
                    s->ar[0],s->ar[1],s->ar[2],s->ar[3],s->ar[4],s->ar[5],s->ar[6],s->ar[7],
                    prog_fetch(s,(uint16_t)(s->pc-3)), prog_fetch(s,(uint16_t)(s->pc-2)),
                    prog_fetch(s,(uint16_t)(s->pc-1)), prog_fetch(s,s->pc),
                    prog_fetch(s,(uint16_t)(s->pc+1)), prog_fetch(s,(uint16_t)(s->pc+2)));
        }
    }
    /* SEED-WR (cap 40): who writes the stack base data[0x5ac8..0x5acc], the
     * return address popped by the RET of the no-op handler 0xab38? Never written
     * means the scheduler-return seed is never laid down: the idle path is
     * correct but its init is missing. */
    if (addr >= 0x5ac8 && addr <= 0x5acc) {
        static unsigned sw = 0;
        if (sw++ < 40)
            fprintf(stderr, "[c54x] SEED-WR data[0x%04x] <- 0x%04x PC=0x%04x insn=%u\n",
                    addr, val, s->pc, s->insn_count);
    }
    /* VECWATCH (gate CALYPSO_AR0_DEBUG, cap 40): catches the value 0x71f4 (the
     * go-live trampoline vector) written ANYWHERE, from insn 0.
     *   written at an address other than 0x5ac8 -> stray write (SP/addressing)
     *   never written                           -> reset init is not modelled. */
    if (val == 0x71f4) {
        static int vw_en = -1;
        if (vw_en < 0) vw_en = calypso_gate("CALYPSO_AR0_DEBUG", 0);
        if (vw_en) {
            static unsigned vw = 0;
            if (vw++ < 40)
                fprintf(stderr, "[c54x] VECWATCH 0x71f4 -> data[0x%04x] PC=0x%04x "
                        "SP=0x%04x AR0=0x%04x AR1=0x%04x insn=%u\n",
                        addr, s->pc, s->sp, s->ar[0], s->ar[1], s->insn_count);
        }
    }

    /* === NDB-CTL-WR : trace ARM-side writes to NDB control flags in
     * [data[0x08F8]..data[0x0900]] = d_fb_det, d_fb_mode, a_sync_demod[],
     * d_sb_ext, etc. The firmware writes mode flags before scheduling
     * SB task - finding which flag toggles SB vs FB tells the DSP
     * dispatcher selector. Capped 50. */
    {
        bool ndb_ctl = (addr >= 0x08F8 && addr <= 0x0900);
        /* Filter on s->pc to identify ARM-side writes : ARM has no PC
         * concept here (it writes via MMIO callback), so s->pc reflects
         * the DSP PC at the moment. ARM-side calls land via calypso_dsp_write
         * which does direct s->data[] write, NOT data_write_locked -> so
         * this probe sees only DSP-side writes to NDB. Both paths are
         * useful to discriminate. */
        if (ndb_ctl) {
            static unsigned ndb_log = 0;
            if (ndb_log++ < 50) {
                if (calypso_debug_enabled("NDB-CTL-WR")) fprintf(stderr,
                        "[c54x] NDB-CTL-WR data[0x%04x] <- 0x%04x "
                        "(was 0x%04x) PC=0x%04x insn=%u\n",
                        addr, val, s->data[addr], s->pc, s->insn_count);
            }
        }
    }

    /* SYNC-DEMOD-WR: traces writes to the a_sync_demod cells [0x08FA..0x08FD]
     * (TOA/PM/ANGLE/SNR per the NDB layout, D_TOA=0 D_PM=1 D_ANGLE=2 D_SNR=3) and
     * to a_serv_demod on both read pages (0x0830..0x0833, 0x0844..0x0847).
     * Cap 200 entries. */
    if ((addr >= 0x08FA && addr <= 0x08FD) ||
        (addr >= 0x0830 && addr <= 0x0833) ||
        (addr >= 0x0844 && addr <= 0x0847)) {
        {   /* No PC filter: the writers 0x821a/0x8213/0x8217, once dismissed
             * as stale-AR garbage, may be the real TOA writers the firmware
             * reads. */
            static unsigned sd_log;
            const unsigned LIMIT = 200;
            if (sd_log < LIMIT) {
                const char *name = (addr==0x08FA||addr==0x0830||addr==0x0844) ? "TOA"
                                 : (addr==0x08FB||addr==0x0831||addr==0x0845) ? "PM"
                                 : (addr==0x08FC||addr==0x0832||addr==0x0846) ? "ANGLE"
                                 : "SNR";
                fprintf(stderr,
                        "[c54x] SYNC-DEMOD-WR #%u %s[0x%04x] <- 0x%04x "
                        "(was 0x%04x) PC=0x%04x op=0x%04x "
                        "A=0x%010llx B=0x%010llx "
                        "AR2=%04x AR3=%04x AR4=%04x AR5=%04x insn=%u\n",
                        sd_log, name, addr, val, s->data[addr],
                        s->pc, s->prog[s->pc],
                        (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                        (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                        s->ar[2], s->ar[3], s->ar[4], s->ar[5],
                        s->insn_count);
                sd_log++;
                if (sd_log == LIMIT)
                    fprintf(stderr,
                            "[c54x] SYNC-DEMOD-WR log capped at %u\n", LIMIT);
            }
        }
    }

    /* FB-DET-WR: traces writes to d_fb_det (DSP word 0x08F8) by PC. Measured
     * d_fb_det stuck at 0x1255 (96 times) with no varied lock signature, so the
     * question is whether one site always writes 0x1255 or several sites write
     * and the ARM only consumes that one. Each event snapshots the PC, the B
     * accumulator (source of the STH/STL) and the previous opcode (addressing
     * context). Cap 300. */
    if (addr == 0x08F8) {
        static unsigned fbdet_log;
        const unsigned LIMIT = 300;
        if (fbdet_log < LIMIT) {
            fprintf(stderr,
                    "[c54x] FB-DET-WR #%u data[0x08F8] <- 0x%04x "
                    "PC=0x%04x op=0x%04x prev=0x%04x "
                    "B=0x%010llx A=0x%010llx SP=0x%04x "
                    "AR2=%04x AR3=%04x AR4=%04x AR5=%04x insn=%u\n",
                    fbdet_log, val,
                    s->pc, s->prog[s->pc],
                    s->prog[(uint16_t)(s->pc - 1)],
                    (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                    (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                    s->sp,
                    s->ar[2], s->ar[3], s->ar[4], s->ar[5],
                    s->insn_count);
            fbdet_log++;
            if (fbdet_log == LIMIT)
                fprintf(stderr, "[c54x] FB-DET-WR log capped at %u\n", LIMIT);
        }
    }

    /* === A_SCH-WR probe : trace DSP writes to a_sch[0..4] in both db_r
     * pages. If DSP never writes these cells, firmware reads stale RAM
     * -> random BSIC in FBSB_CONF. If DSP writes garbage, the SCH demod
     * path is broken upstream. Helps discriminate the SB sync root cause.
     * Capped at 50 logged hits to avoid spam. */
    {
        bool a_sch_p0 = (addr >= 0x0837 && addr <= 0x083B);  /* page 0 a_sch[0..4] */
        bool a_sch_p1 = (addr >= 0x084B && addr <= 0x084F);  /* page 1 a_sch[0..4] */
        if (a_sch_p0 || a_sch_p1) {
            static unsigned a_sch_log = 0;
            if (a_sch_log++ < 50) {
                if (calypso_debug_enabled("A_SCH-WR")) fprintf(stderr,
                        "[c54x] A_SCH-WR data[0x%04x] <- 0x%04x page=%d "
                        "idx=%d PC=0x%04x insn=%u\n",
                        addr, val,
                        a_sch_p0 ? 0 : 1,
                        (int)(addr - (a_sch_p0 ? 0x0837 : 0x084B)),
                        s->pc, s->insn_count);
            }
        }
    }

    /* === BLOB-WR diagnostic for dsp_blobs/ test harness ===
     * Logs writes either targeting scratch [0x2000..0x200F] (dsp-deadbeef
     * etc.) or carrying a known blob signature value. Self-throttled to
     * 5000 hits per process. Zero impact when no blob test is running. */
    {
        static unsigned blob_wr_count = 0;
        bool is_scratch = (addr >= 0x2000 && addr <= 0x200F);
        bool is_magic = (val == 0xCAFE || val == 0xBEEF || val == 0xDEAD ||
                         val == 0x2B2B || val == 0x4906 || val == 0x1B00 ||
                         val == 0x7080 || val == 0x4000);
        if ((is_scratch || is_magic) && blob_wr_count < 5000) {
            blob_wr_count++;
            fprintf(stderr,
                    "[c54x] BLOB-WR data[0x%04x] <- 0x%04x PC=0x%04x insn=%u\n",
                    addr, val, s->pc, s->insn_count);
        }
    }

    /* WATCH-VEC (env CALYPSO_WATCH_VEC): writes to the interrupt vector table
     * (0x0080-0x00FF) and to the 0x013b dispatch (0x0138-0x013c). Separates never
     * written, written then dropped, and corrupted. */
    {
        static int wv = -1;
        if (wv < 0) { const char *e = calypso_getenv("CALYPSO_WATCH_VEC"); wv = (e && *e != 0) ? 1 : 0; }
        if (wv && ((addr >= 0x0080 && addr <= 0x00FF) || (addr >= 0x0138 && addr <= 0x013C))) {
            static unsigned wvn = 0;
            if (wvn++ < 100)
                fprintf(stderr, "[c54x] WATCH-VEC data[0x%04x] <- 0x%04x (was 0x%04x) "
                        "PC=0x%04x op=0x%04x insn=%u\n",
                        addr, val, s->data[addr], s->pc, prog_fetch(s, s->pc), s->insn_count);
        }
    }

    }   /* C54X_SONDES : B4 .. WATCH-VEC */

    /* The DROM LUT column is read-only (SPRU172C: with PMST.DROM=1 the DSP ROM
     * in data space is read-only).
     *
     * The dispatcher reads a per-task LUT at data[(DP<<7)|0x07] = 0x9187, 0x9207,
     * 0x9287... (always offset 0x07, DP = task number). A walking `STH B,*AR2+`
     * in the RPTB body [0x815E..0x8176] corrupted 0x9207 (0xff72 -> 0xf6b7), so
     * CALAD-A became 0x70c3 - the CALA-A opcode itself in PROM0 - instead of the
     * MAC routine 0x8239. Each self-call pushed one word until SP reached MMR_SP
     * (0x0018) and the push aliased SP itself.
     *
     * Only the offset-0x07 column is protected. A whole-DROM guard breaks the
     * firmware scratch at other offsets (0x00/0x42/0x60...), which then reads
     * back stale and wedges the DSP at 0xebf0.
     *
     * The guard is deliberately NOT conditioned on PMST.DROM: on a 459M-insn run
     * PMST=0x70c4 appeared 149 times - PMST (MMR 0x1D) itself clobbered by the
     * self-CALA at 0x70c3 - which cleared DROM (bit 0x08) and disabled the guard,
     * re-corrupting the LUT: a self-sustaining loop (148838 self-CALAs). The
     * firmware never legitimately runs with DROM=0 (legitimate PMST is
     * 0xffa8/0xffb8, DROM=1 in both). Drops silently, like silicon. */
    if (addr >= 0x9000 && addr <= 0xDFFF && (addr & 0x7F) == 0x07) {
        static unsigned drom_w_attempts = 0;
        if (drom_w_attempts++ < 40) {
            if (calypso_debug_enabled("DROM-W-DROP")) fprintf(stderr,
                    "[c54x] DROM-W-DROP data[0x%04x] <- 0x%04x (was 0x%04x) "
                    "PC=0x%04x insn=%u pmst=0x%04x (LUT col, read-only)\n",
                    addr, val, s->data[addr], s->pc, s->insn_count, s->pmst);
        }
        return;
    }

    /* Pure probes FBDB, COEFFS-WR: behind the switch. The INVARIANT that
     * follows stays active (it can report/stop). */
    if (C54X_SONDES) {
    /* FBDB-PROBE write to 0x3DC0 (= SARAM flag polled by fc63 BITF).
     * Env CALYPSO_FBDB_PROBE=1. Logs old->new + which bits set, with focus
     * on bit 4 (= 0x0010) since that's the bit fc63 tests via BITF. */
    if (addr == 0x3DC0 && g_fbdb_probe_enabled > 0) {
        fbdb_probe_write_3dc0(addr, s->data[addr], val, s->pc, s->insn_count);
    }
    /* COEFFS-WR: watch-writes over [0x2bc0..0x2bff] (64 words). Measured
     * sequence init -> clear -> use, by cluster:
     *   0x8216 (23 hits)  real coefficients (f320, a660, ...)
     *   0x9ace (64 hits)  partial clear
     *   0x9abf (113 hits) uniform 0x0001 pattern
     * then all-zero from insn ~4M on, still all-zero when the correlator reads at
     * 0x8f51. The per-cluster timing trackers stay out of the helper because they
     * are cluster-specific; watch_write_zone_check factors out the per-PC counter,
     * the throttled log and the summary. */
    if (addr >= 0x2bc0 && addr <= 0x2bff) {
        uint16_t exec_pc = s->last_exec_pc;
        if (exec_pc == 0x8216 || exec_pc == 0x8217 || exec_pc == 0x8218) {
            g_fb_det_timing.last_compute_insn = s->insn_count;
            g_fb_det_timing.last_compute_addr = addr;
        } else if (exec_pc == 0x9ace) {
            g_fb_det_timing.last_clear_insn = s->insn_count;
            g_fb_det_timing.last_clear_addr = addr;
        } else if (exec_pc == 0x9abf) {
            g_fb_det_timing.last_pattern_insn = s->insn_count;
            g_fb_det_timing.last_pattern_addr = addr;
        }
    }
    {
        static WatchWriteState wws_coeffs;
        watch_write_zone_check(s, addr, val, "COEFFS", 0x2bc0, 0x2bff, &wws_coeffs);
    }
    }   /* C54X_SONDES : FBDB, COEFFS-WR */
    /* INVARIANT (gate CALYPSO_INVARIANTS, default off): correlator pointers.
     * AR4 (write pointer) must take more than 2 distinct values, otherwise it is
     * a 2-word loop. AR5 (I/Q read pointer) must stay inside the buffer
     * [0x2a00..0x2b27], otherwise the correlator reads outside it and the kernel
     * at 0xa076 is never reached. */
    if (addr >= 0x2bc0 && addr <= 0x2bff) {
        static uint32_t pw;
        static uint16_t ar4_seen[8];
        static int ar4_n;
        uint16_t a4 = s->ar[4], a5 = s->ar[5];
        if (ar4_n < 8) {
            int f = 0;
            for (int i = 0; i < ar4_n; i++) { if (ar4_seen[i] == a4) { f = 1; break; } }
            if (!f) ar4_seen[ar4_n++] = a4;
        }
        if (++pw == 2000) {
            calypso_invariant("correlator_ar4_sweeps", ar4_n > 2,
                              "AR4 (write ptr) : %d valeur(s) distincte(s) sur %u writes",
                              ar4_n, pw);
        }
        calypso_invariant("correlator_ar5_in_iq_buffer",
                          a5 >= 0x2a00 && a5 <= 0x2b27,
                          "AR5 (read ptr I/Q) = 0x%04x HORS buffer [0x2a00..0x2b27]", a5);
    }
    /* [2026-09-23] Sondes pures A_CD-WR .. DARAM[0x40..0x90] : derriere
     * l'interrupteur. */
    if (C54X_SONDES) {
    /* A_CD-WR: tracks whether the DSP CCCH demod (DSP_TASK_ALLC) writes its
     * results into a_cd[15]. */
    {
        static WatchWriteState wws_a_cd;
        /* Bounds are 0x09D2..0x09E0. Four independent anchors put the base at
         * 0x09D2: (1) dsp_api.h preprocessed with the real l1_environment.h
         * macros gives offset 254 words = 0xFE; (2) ndb.h, reversed from the
         * loaded DSP binary, gives the same 0xFE; (3) B_BLUD is tested on a_cd[0]
         * (prim_tch.c:667) and the DSP writes 0x8000 at 0x09D2; (4) the 12-word
         * block copy targets 0x09D5 while the ARM reads 23 bytes at &a_cd[3].
         * Consistent with d_dsp_state=0x08E2 and NDB_D_FB_DET=0x08F8. A window of
         * 0x09D0..0x09DE instead watches two dead a_ramp words and blinds
         * a_cd[13] and a_cd[14]. */
        watch_write_zone_check(s, addr, val, "A_CD", 0x09d2, 0x09e0, &wws_a_cd);
    }
    /* BLK-SRC - the SOURCE of the block copy, 0x2c3c..0x2c47.
     *
     * The a_cd header is repaired (B_BLUD survives the `or` at 0x9719: measured
     * 86 times 0x8000, never 0x0000 at 0x09d2), but the payload stays null, and it
     * does not come from the header. Firmware disassembly at 0x971e..0x9723:
     *     0x971e  stm #0x2c3c    ; AR2 <- source
     *     0x9720  stm #0x09d5    ; AR3 <- a_cd[3]
     *     0x9722  rpt #0x0b      ; 12 times
     *     0x9723  mvdd           ; *AR2+ -> *AR3+
     * The 12 payload words are therefore a copy of 0x2c3c..0x2c47. If a_cd is
     * null, either that zone is null or nobody writes it.
     *
     *   no line at all   -> nobody writes the zone: the demod does not publish
     *                       its result, look upstream (the 0x81xx loop writes
     *                       through *AR5+, not a fixed address, so follow AR5)
     *   lines with val=0 -> the demod writes, but writes nothing: the fault is
     *                       in the computation, not the transport
     *   non-zero values  -> the zone is good and the `mvdd`, or its timing, is
     *                       at fault.
     *
     * Same helper as A_CD-WR, hence the same emission condition
     * (`total <= 500 || exec_pc != last || delta_insn > 100000`): never derive a
     * rate from the number of lines, read the cumulative SUMMARY.
     * `exec_pc` names the PRECEDING instruction, not the writer; correct for it
     * mentally with `cur_pc`. */
    {
        static WatchWriteState wws_blk_src;
        watch_write_zone_check(s, addr, val, "BLK-SRC", 0x2c3c, 0x2c47, &wws_blk_src);
    }
    /* DISP-TBL - the task dispatch table, 0x43d0..0x43dc.
     *
     * Structure established by disassembling PROM0:
     *   0xb4be  stm #0x43d5 ; ld #0xab35 ; rpt #0x02 ; reada *AR1+
     *           -> copies THREE words prog[0xab35..0xab37] to data[0x43d5..0x43d7]
     *   0xbb00  st *(0x43d8), #0xab38      -> entry 3 = shared RET, set by init
     *   0xb0e5  add #0x43d5, A ; ld *AR3, A ; cala A
     *           -> dispatch on table[idx], guarded by 0 <= idx <= 3
     *   0xb01c  ld *(0x43d8), A ; cala A
     *           -> reads entry 3 specifically
     *
     * So the 35000 `CALAA tgt=0xab38` are NOT a broken dispatcher: 0x43d8 is the
     * entry of task 3, which the loader deliberately does not copy (it copies
     * only 3) and which init sets to a RET. A no-op by design. Tasks 0..2 are
     * dispatched through 0xb0e5..0xb0ec.
     *
     * The index at 0xb0e5 is NOT d_task_md. It comes from accumulator A after the
     * `cala` at 0xb0d7, i.e. the return value of the previous dispatch; do not
     * compare it with the 1/5 the ARM writes at 0x0804.
     *
     * The probe says what the four entries really hold and who writes them,
     * including through indirect addressing that a literal scan of the ROM cannot
     * see. Plausible handler addresses in 0x43d5..0x43d7 mean the table is sound
     * and the index is at fault; 0 or RETs mean the loader is.
     *
     * Same helper as A_CD-WR, hence the same emission condition - read the
     * cumulative SUMMARY, never the number of lines. */
    {
        static WatchWriteState wws_disp_tbl;
        /* Window widened to 0x43d0..0x43dc: 0x43d5..0x43d9 saw only 2 of the 3
         * expected copies and could not say whether the third was missing or
         * landed outside the zone through a pointer offset. */
        watch_write_zone_check(s, addr, val, "DISP-TBL", 0x43d0, 0x43dc, &wws_disp_tbl);
    }
    /* TRAMPO - the vector 30 trampoline, data[0x0158..0x015f].
     *
     * Chain established link by link, by measurement:
     *   1. the DMA raises INT10n at end of transfer   -> 15500 calls, ok
     *   2. the DSP ENTERS vector 30                   -> seen with
     *      CALYPSO_DEBUG=C54X, `vec=30` present, ok
     *   3. the trampoline at 0x0158 runs only 8 times -> not ok
     *   4. its body 0x728a never runs                 -> not ok
     * The DSP queues therefore overflow (PEND: 116 writes at 0x434e/0x434f,
     * TASK: 0) and it raises DSP_ERR_DMA_PEND.
     *
     * The probe says who writes the trampoline cell, when, and with what. The API
     * window overlaps PROGRAM space (CAL000 7.2.1: mixed data program memory, API
     * overlay over the program area), so a data write there installs CODE.
     *   no write              -> the trampoline is never installed and the 8
     *                            executions come from a leftover; find out why
     *                            0xa5cd does not install it.
     *   writes then overwrite -> something destroys it between two takes of the
     *                            vector; the writing PC names it.
     *
     * Same helper as A_CD-WR: same emission condition. Read the cumulative
     * SUMMARY, never the number of lines. */
    {
        static WatchWriteState wws_trampo;
        watch_write_zone_check(s, addr, val, "TRAMPO", 0x0158, 0x015f, &wws_trampo);
        /* A_CD-BY-BURST: correlates a_cd[] writes with the current d_burst_d.
         * A DSP running bursts 0->1->2->3 gives ~25 % of the writes per
         * burst_id; zero writes with burst=3 means the DSP never writes the end
         * of the sequence, which is why the ARM nb_resp bails. */
        if (addr >= 0x09d0 && addr <= 0x09de) {
            static uint64_t a_cd_by_burst[16];
            static uint64_t a_cd_corr_total;
            static uint64_t a_cd_corr_last_log;
            uint16_t b = g_last_d_burst_d & 0xF;
            a_cd_by_burst[b]++;
            a_cd_corr_total++;
            if (a_cd_corr_total - a_cd_corr_last_log >= 1000) {
                a_cd_corr_last_log = a_cd_corr_total;
                if (calypso_debug_enabled("A_CD-BY-BURST")) fprintf(stderr,
                        "[c54x] A_CD-BY-BURST total=%llu "
                        "burst[0]=%llu [1]=%llu [2]=%llu [3]=%llu other=%llu\n",
                        (unsigned long long)a_cd_corr_total,
                        (unsigned long long)a_cd_by_burst[⟨1⟩],  ×4
    ⟨⟩ = (0) (1) (2) (3)
                        (unsigned long long)(a_cd_corr_total -
                                             a_cd_by_burst[0] - a_cd_by_burst[1] -
                                             a_cd_by_burst[2] - a_cd_by_burst[3]));
            }
        }
    }
    /* D_BURST_D probe: watches d_burst_d at 0x0829 (page 0) and 0x083D (page 1)
     * with a per-PC counter, a transition matrix and a histogram.
     *   0,1,2,3 in sequence -> the DSP signals correctly, the bug is ARM-side
     *   0,1,2 but never 3   -> the DSP stalls on the 4th burst
     *   no write at all     -> the DSP never writes this cell
     */
    if (addr == 0x0829 || addr == 0x083D) {
        static uint64_t db_total[2];
        static uint64_t db_per_pc[2][0x10000];
        static uint16_t db_prev[2];
        static uint64_t db_trans[2][16][16];
        static uint64_t db_last_log[2];
        static uint64_t db_last_summary[2];
        int page = (addr == 0x083D) ? 1 : 0;
        uint16_t exec_pc = s->last_exec_pc;
        uint16_t prev_val = db_prev[page];
        uint16_t curr_val = val & 0xF;
        db_total[page]++;
        db_per_pc[page][exec_pc]++;
        if (prev_val < 16 && curr_val < 16) {
            db_trans[page][prev_val][curr_val]++;
        }
        db_prev[page] = curr_val;
        g_last_d_burst_d = curr_val;  /* propagated to A_CD-BY-BURST */
        bool should_log = db_total[page] <= 200
            || (s->insn_count - db_last_log[page]) > 100000;
        if (should_log) {
            if (calypso_debug_enabled("D_BURST_D-WR")) fprintf(stderr,
                    "[c54x] D_BURST_D-WR page=%d #%llu addr=0x%04x val=0x%04x "
                    "exec_pc=0x%04x prev=%u curr=%u insn=%u\n",
                    page, (unsigned long long)db_total[page], addr, val,
                    exec_pc, prev_val, curr_val, s->insn_count);
            db_last_log[page] = s->insn_count;
        }
        if (s->insn_count - db_last_summary[page] >= 5000000) {
            db_last_summary[page] = s->insn_count;
            if (calypso_debug_enabled("D_BURST_D-SUMMARY")) fprintf(stderr,
                    "[c54x] D_BURST_D-SUMMARY page=%d total=%llu trans:",
                    page, (unsigned long long)db_total[page]);
            for (int p = 0; p < 8; p++) {
                for (int c = 0; c < 8; c++) {
                    if (db_trans[page][p][c]) {
                        fprintf(stderr, " %u->%u=%llu",
                                p, c, (unsigned long long)db_trans[page][p][c]);
                    }
                }
            }
            fprintf(stderr, "\n");
        }
    }
    /* D_TASK_D probe: watches d_task_d at 0x0828 (page 0) and 0x083C (page 1),
     * the read side of db_buf_r. The ARM L1 prim_rx_nb reads this field through
     * dsp_api.db_r->d_task_d and bails with puts("EMPTY") when it is 0; 60 EMPTY
     * lines were measured on the deterministic synth=1 bench. Traces who writes
     * it, when and with what, to separate:
     *   - the DSP never touches 0x0828/0x083C
     *   - the DSP writes 0 (clear/init only)
     *   - the DSP writes 24 (DSP_TASK_ALLC) but the ARM reads before the write
     */
    if (addr == 0x0828 || addr == 0x083C) {
        static uint64_t dt_total[2];
        static uint16_t dt_prev[2];
        static uint64_t dt_last_log[2];
        int page = (addr == 0x083C) ? 1 : 0;
        uint16_t exec_pc = s->last_exec_pc;
        uint16_t prev_val = dt_prev[page];
        uint16_t curr_val = val;
        dt_total[page]++;
        dt_prev[page] = curr_val;
        bool should_log = dt_total[page] <= 200
            || (s->insn_count - dt_last_log[page]) > 100000;
        if (should_log) {
            if (calypso_debug_enabled("D_TASK_D-WR")) fprintf(stderr,
                    "[c54x] D_TASK_D-WR page=%d #%llu addr=0x%04x val=0x%04x "
                    "exec_pc=0x%04x prev=0x%04x insn=%u\n",
                    page, (unsigned long long)dt_total[page], addr, val,
                    exec_pc, prev_val, s->insn_count);
            dt_last_log[page] = s->insn_count;
        }
    }
    /* DATA-W-MMR : log every write into the low MMR window (addr <= 0x1F)
     * with full attribution context. Goal : disambiguate the IMR-W trace
     * cascade observed at PC=0x8eb9 (op=0xf3e1) and PC=0x9ad0 (op=0x8192).
     * The writer_kind field tells us *which path* triggered the write
     * (opcode family / IRQ ack / ARM MMIO / resolve_smem side effect).
     * Cap at 200 distinct events to avoid log flood. */
    if (addr <= 0x1F) {
        static unsigned mmrw_log;
        if (mmrw_log++ < 200) {
            const char *wk_name[] = {
                "UNK", "F3", "8x", "77", "76", "PSHM",
                "RET", "IRQ_ACK", "ARM_MMIO", "RES_AR", "OTHER"
            };
            uint8_t wk = s->writer_kind;
            const char *wkn = (wk < sizeof(wk_name)/sizeof(wk_name[0]))
                              ? wk_name[wk] : "??";
            if (calypso_debug_enabled("DATA-W-MMR")) fprintf(stderr,
                    "[c54x] DATA-W-MMR addr=0x%02x val=0x%04x "
                    "exec_pc=0x%04x cur_pc=0x%04x cur_op=0x%04x "
                    "xpc=%d wk=%s "
                    "AR0=%04x AR1=%04x AR2=%04x AR3=%04x "
                    "AR4=%04x AR5=%04x AR6=%04x AR7=%04x "
                    "SP=%04x DP=%d INTM=%d insn=%u\n",
                    addr, val,
                    s->last_exec_pc, s->pc, s->prog[s->pc],
                    s->xpc, wkn,
                    s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                    s->ar[4], s->ar[5], s->ar[6], s->ar[7],
                    s->sp, dp(s),
                    !!(s->st1 & ST1_INTM),
                    s->insn_count);
        }
    }

    /* WATCH-WR-ADDR (CALYPSO_DEBUG=WATCH-WR plus CALYPSO_WATCH_WR_ADDR): logs
     * every write to an arbitrary data address, to trace who writes - or never
     * writes - a SARAM dispatcher-pointer cell that falls to 0, turning CALA into
     * a jump to 0 and landing in the boot stub.
     * Accepts a LIST of addresses separated by commas or spaces
     * (CALYPSO_WATCH_WR_ADDR=0x098b,0x098d), max 8: the DSP task dispatcher
     * (0xddfd/0xde01) tests two masks, d[0x098b] and d[0x098d], both permanently
     * zero, and they must be watched in the SAME run to be comparable. */
    {
        static int wr_init = 0;
        static uint16_t wr_list[8];
        static int wr_n = 0;
        if (!wr_init) {
            wr_init = 1;
            const char *e = calypso_getenv("CALYPSO_WATCH_WR_ADDR");
            if (e && *e) {
                const char *p = e;
                while (*p && wr_n < 8) {
                    while (*p == ',' || *p == ' ' || *p == '\t') p++;
                    if (!*p) break;
                    char *fin = NULL;
                    long v = strtol(p, &fin, 0);
                    if (fin == p) break;          /* nothing parseable: stop */
                    wr_list[wr_n++] = (uint16_t)v;
                    p = fin;
                }
            }
        }
        for (int _i = 0; _i < wr_n; _i++) {
            if (addr == wr_list[_i]) {
                C54_DBG("WATCH-WR",
                    "WATCH-WR data[0x%04x] <- 0x%04x (was 0x%04x) PC=0x%04x "
                    "DP=0x%03x insn=%u",
                    addr, val, s->data[addr], s->pc, (s->st0 & 0x1FF),
                    (unsigned)s->insn_count);
                break;
            }
        }
    }

    /* WATCH-WR-PLAGE: chronological log of EVERY write in the data range
     * [CALYPSO_WATCH_WR_LO, CALYPSO_WATCH_WR_HI], capped at CALYPSO_WATCH_WR_N
     * lines (default 600). Establishes by measurement the order of the writes
     * into the soft-bit buffer 0x2a00-0x2a8d - who writes what, in which order,
     * and whether zeros arrive AFTER the values - instead of inferring it from an
     * aggregate count.
     * PC rule (doc/SONDES.md): the dispatcher has already advanced the PC when
     * the probe observes it, so the line prints pc=<seen> and ecr=<seen-1>, the
     * likely writer.
     * Output goes to stderr unconditionally (not through C54_DBG) so it stays
     * readable without CALYPSO_DEBUG; inert until LO/HI are set. */
    {
        static int wrp_init = 0;
        static long wrp_lo = -1, wrp_hi = -1, wrp_max = 600;
        static long wrp_seen = 0;
        if (!wrp_init) {
            wrp_init = 1;
            const char *lo = calypso_getenv("CALYPSO_WATCH_WR_LO");
            const char *hi = calypso_getenv("CALYPSO_WATCH_WR_HI");
            const char *nn = calypso_getenv("CALYPSO_WATCH_WR_N");
            if (lo && *lo) wrp_lo = strtol(lo, NULL, 0);
            if (hi && *hi) wrp_hi = strtol(hi, NULL, 0);
            if (nn && *nn) wrp_max = strtol(nn, NULL, 0);
            if (wrp_lo >= 0 && wrp_hi < 0) wrp_hi = wrp_lo;
        }
        if (wrp_lo >= 0 && addr >= (uint16_t)wrp_lo && addr <= (uint16_t)wrp_hi) {
            wrp_seen++;
            if (wrp_seen <= wrp_max)
                fprintf(stderr, "[c54x] WR-PLAGE #%ld data[0x%04x] idx=%d <- 0x%04x "
                        "(was 0x%04x) pc=0x%04x ecr=0x%04x insn=%u\n",
                        wrp_seen, addr, (int)(addr - (uint16_t)wrp_lo), val,
                        s->data[addr], s->pc, (uint16_t)(s->pc - 1),
                        (unsigned)s->insn_count);
            else if (wrp_seen == wrp_max + 1)
                fprintf(stderr, "[c54x] WR-PLAGE ... plafond %ld atteint, suite muette\n",
                        wrp_max);
        }
    }

    /* WATCH-3FBE (env CALYPSO_WATCH_3FBE=1, zero cost otherwise): writes over
     * [0x3fb0..0x3fbf], which includes 0x3fbe, the slot popped as 0 at the
     * bootstub entry (insn=3995013). The BSP DMA bypasses data_write_locked
     * (direct s->data[]), so this hook sees only DSP instruction writes
     * (STL/STM/STLM/STH). No write before insn=3995013 means the firmware never
     * pushes to that address: the SP trajectory diverged (a RETD at 0x8ed1 with
     * no matching CALL). */
    if (addr >= 0x3fb0 && addr <= 0x3fbf) {
        static int      w3fbe_enabled = -1;
        static unsigned w3fbe_total = 0;
        if (w3fbe_enabled < 0) {
            const char *e = cdbg_env("WATCH-3FBE");
            w3fbe_enabled = (e && *e == '1') ? 1 : 0;
            if (w3fbe_enabled) {
                fprintf(stderr,
                    "[c54x] WATCH-3FBE enabled — range [0x3fb0..0x3fbf] "
                    "(DSP-side writes only, BSP DMA bypassed)\n");
            }
        }
        if (w3fbe_enabled > 0) {
            w3fbe_total++;
            if (w3fbe_total <= 100 || (w3fbe_total % 5000) == 0) {
                fprintf(stderr,
                    "[c54x] WATCH-3FBE #%u addr=0x%04x val=0x%04x "
                    "(was 0x%04x) PC=0x%04x insn=%u\n",
                    w3fbe_total, addr, val, s->data[addr],
                    s->pc, s->insn_count);
            }
        }
    }

    /* WATCH-WRITE 0x3dd2: the cell 0x75db polls in a loop (37M reads in 15 s).
     * Identifies who writes it, and who does not.
     *   no write        -> a compute block never fires
     *   boot-only write -> init is fine, the steady-state set is missing
     *   periodic writes whose value never matches the test at 0x75db
     *                   -> the bug is in the upstream computation. */
    if (addr == 0x3dd2) {
        static unsigned w3dd2;
        w3dd2++;
        if (w3dd2 <= 100 || (w3dd2 % 1000) == 0) {
            fprintf(stderr,
                    "[c54x] WATCH-WRITE 0x3dd2 #%u <- 0x%04x (was 0x%04x) "
                    "PC=0x%04x insn=%u INTM=%d\n",
                    w3dd2, val, s->data[addr], s->pc, s->insn_count,
                    !!(s->st1 & ST1_INTM));
        }
    }
    /* WATCH-WRITE on the same mailbox slots tracked in data_read. Whoever writes
     * them - DSP or ARM through the api_ram alias - gets logged, so the source of
     * the value the firmware polls can be attributed. */
    if (addr == 0x0ffe || addr == 0x0fff || addr == 0x01F0) {
        static unsigned wcount;
        if (wcount++ < 30) {
            if (calypso_debug_enabled("WATCH-WRITE")) fprintf(stderr,
                    "[c54x] WATCH-WRITE data[0x%04x] <- 0x%04x  (was 0x%04x) "
                    "PC=0x%04x insn=%u\n",
                    addr, val, s->data[addr], s->pc, s->insn_count);
        }
    }
    /* Dispatcher pointer at data[0x3f65] - `LD *(0x3f65),A; CALA A` at
     * DARAM 0x008a-0x008c. When this slot holds 0xfff8/0x0000/garbage the
     * CALA jumps into PROM1 vec or boot stub NOPs and the SP runs away.
     * Trace every write so we can identify who populates / corrupts it. */
    if (addr == 0x3f65) {
        static unsigned dpw;
        if (dpw++ < 100) {
            if (calypso_debug_enabled("DISP-PTR")) fprintf(stderr,
                    "[c54x] DISP-PTR data[0x3f65] <- 0x%04x (was 0x%04x) "
                    "PC=0x%04x insn=%u\n",
                    val, s->data[addr], s->pc, s->insn_count);
        }
    }
    /* Dispatcher poll addresses - log ANY write so we identify the
     * code path that should populate them. Currently 0 PORTR PA=0xF430
     * fires because dispatcher reads 0 here forever. */
    if (addr == 0x4359 || addr == 0x3fab) {
        static unsigned dispw;
        if (dispw++ < 50) {
            if (calypso_debug_enabled("DISP-WRITE")) fprintf(stderr,
                    "[c54x] DISP-WRITE data[0x%04x] <- 0x%04x (was 0x%04x) "
                    "PC=0x%04x insn=%u\n",
                    addr, val, s->data[addr], s->pc, s->insn_count);
        }
    }
    /* CALAD source zone 0x4180-0x41FF - LD-A-TRACE shows the firmware
     * reads 0x4189 (DP=0x83) but our emulation has it as 0. Log every
     * write to this range so we can tell whether (a) anyone is meant to
     * populate it and we missed the path, or (b) DP=0x83 is itself a
     * symptom upstream of an unrelated bug. */
    if (addr >= 0x4180 && addr <= 0x41FF) {
        static unsigned cwz;
        if (cwz++ < 5000) {
            if (calypso_debug_enabled("CALAD-ZONE-W")) fprintf(stderr,
                    "[c54x] CALAD-ZONE-W data[0x%04x] <- 0x%04x (was 0x%04x) "
                    "PC=0x%04x insn=%u\n",
                    addr, val, s->data[addr], s->pc, s->insn_count);
        }
    }
    /* Dedicated watch on 0x4189 - never capped. The LD-A loop reads this
     * slot in the CALAD trap; we want to know if/when *anyone* finally
     * writes a non-zero value, and from which PC. */
    if (addr == 0x4189) {
        fprintf(stderr,
                "[c54x] *** WR-0x4189 *** data[0x4189] <- 0x%04x (was 0x%04x) PC=0x%04x insn=%u\n",
                val, s->data[addr], s->pc, s->insn_count);
    }
    /* DARAM[0x40..0x90] watch - dispatcher flag area. The PROM0 idle dispatcher
     * (0xCC62..0xCC6F) polls data[0x62] and other slots in [0x60..0x70]; forcing
     * data[0x62]=1 was measured to make the DSP escape and reach 0x770c, so this
     * range gates the runtime task pipeline. ARM-side writes go to the API page
     * mirror at +0x0800 (calypso_trx.c calypso_dsp_write) and never to DARAM
     * 0x40..0x90, so any value here comes from DSP stores (ST/STH/STM/...) or
     * stays zero forever. Every write is captured with PC, INTM and insn; INTM
     * separates ISR-context writes from main code. */
    if (addr >= 0x0040 && addr <= 0x0090) {
        static unsigned daram_disp_w;
        if (daram_disp_w++ < 1000) {
            if (calypso_debug_enabled("DISP-FLAG-W")) fprintf(stderr,
                    "[c54x] DISP-FLAG-W data[0x%04x] <- 0x%04x (was 0x%04x) "
                    "PC=0x%04x INTM=%d IFR=0x%04x insn=%u\n",
                    addr, val, s->data[addr], s->pc,
                    !!(s->st1 & ST1_INTM), s->ifr, s->insn_count);
            if (daram_disp_w == 1000) {
                if (calypso_debug_enabled("DISP-FLAG-W")) fprintf(stderr,
                        "[c54x] DISP-FLAG-W log capped at 1000 — pattern visible above\n");
            }
        }
    }
    }   /* C54X_SONDES : A_CD-WR .. DARAM[0x40..0x90] */
    /* Timer registers (0x0024-0x0026) - before MMR check */
    if (addr == TCR_ADDR) {
        /* TRB: write 1 -> reload TIM from PRD, PSC from TDDR */
        if (val & TCR_TRB) {
            s->data[TIM_ADDR] = s->data[PRD_ADDR];
            s->timer_psc = val & TCR_TDDR_MASK;
        }
        /* Store TCR without TRB (TRB is write-only, always reads 0) */
        s->data[TCR_ADDR] = val & ~TCR_TRB;
        return;
    }
    if (addr == TIM_ADDR) { s->data[TIM_ADDR] = val; return; }
    if (addr == PRD_ADDR) { s->data[PRD_ADDR] = val; return; }

    /* MMR region */
    if (addr < 0x20) {
        /* === BOOT-MMR-WR probe : reach+effect test for boot init STMs ===
         * The DSP boot is supposed to STM #imm into SP, IMR, AR0..7, BK,
         * BRC, PMST shortly after the jump from 0xb418->0x76f8. Observed
         * runtime says SP/IMR/AR4/AR5 never receive their init values, so
         * either (a) PC never reaches the STM, or (b) the STM handler writes
         * to the wrong target. This probe answers BOTH : every write to
         * MMR 0..0x1E during boot phase is logged with PC + opcode + delta,
         * so we can see what got written, when, by what instruction. */
        if (s->insn_count <= 300000) {
            static unsigned bmw_log;
            const unsigned LIMIT = 800;
            if (bmw_log < LIMIT) {
                static const char *names[0x20] = {
                    "IMR","IFR","??02","??03","??04","??05","ST0","ST1",
                    "AL","AH","AG","BL","BH","BG","T","TRN",
                    "AR0","AR1","AR2","AR3","AR4","AR5","AR6","AR7",
                    "SP","BK","BRC","RSA","REA","PMST","XPC","??1F",
                };
                uint16_t old_val = (addr == MMR_IMR) ? s->imr
                                 : (addr == MMR_IFR) ? s->ifr
                                 : (addr == MMR_SP)  ? s->sp
                                 : (addr >= MMR_AR0 && addr <= MMR_AR7) ? s->ar[addr - MMR_AR0]
                                 : s->data[addr];
                fprintf(stderr,
                        "[c54x] BOOT-MMR-WR #%u insn=%u PC=%04x op=%04x "
                        "MMR[%02x %s] %04x → %04x\n",
                        bmw_log, s->insn_count, s->pc, s->prog[s->pc],
                        (unsigned)addr, names[addr],
                        old_val, val);
                bmw_log++;
                if (bmw_log == LIMIT) {
                    fprintf(stderr,
                            "[c54x] BOOT-MMR-WR log capped at %u\n", LIMIT);
                }
            }
        }
        switch (addr) {
        case MMR_IMR:
            /* IMR-ARM (read-only, cap 80): full history of IMR writes
             * (old -> new) with bit12 = vec28 (scheduler), bit3 = vec19,
             * bit5 = vec21. Measured IMR=0x52fd (bit12 set) cleared by 0xb37e,
             * so this names who writes 0x52fd and whether a re-arm follows. */
            if ((uint16_t)val != s->imr) {
                static unsigned ia = 0;
                if (ia++ < 80)
                    fprintf(stderr, "[c54x] IMR-ARM 0x%04x -> 0x%04x (b12/vec28=%d "
                            "b3/vec19=%d b5/vec21=%d) PC=0x%04x op=0x%04x insn=%u\n",
                            s->imr, (uint16_t)val,
                            !!(val&(1<<12)), !!(val&(1<<3)), !!(val&(1<<5)),
                            s->pc, s->prog[s->pc], s->insn_count);
            }
            if (val != s->imr) {
                static unsigned imr_log = 0;
                /* Always log transitions TO zero (mask-everything) - that
                 * is the cascade root suspected in 2026-05-08 v2 diag :
                 * IMR=0 -> INT3 IFR pending forever -> RPTB at 0xe9ac never
                 * exits. We need the PC + opcode of every IMR=0 write,
                 * uncapped, so we can identify the buggy code path. */
                bool to_zero = (val == 0);
                if (imr_log++ < 50 || to_zero) {
                    if (calypso_debug_enabled("IMR-W")) fprintf(stderr,
                            "[c54x] IMR-W %s 0x%04x → 0x%04x PC=0x%04x "
                            "op=0x%04x prev_op=0x%04x SP=0x%04x INTM=%d "
                            "AR0=0x%04x AR1=0x%04x AR2=0x%04x AR3=0x%04x "
                            "AR4=0x%04x AR5=0x%04x AR6=0x%04x AR7=0x%04x "
                            "B=0x%010llx insn=%u\n",
                            to_zero ? "*ZERO*" : "      ",
                            s->imr, val, s->pc,
                            s->prog[s->pc],
                            s->prog[(uint16_t)(s->pc - 1)],
                            s->sp,
                            !!(s->st1 & ST1_INTM),
                            s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                            s->ar[4], s->ar[5], s->ar[6], s->ar[7],
                            (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                            s->insn_count);
                }
            }
            {
                /* The IMR bit9 toggle (0xddf9 ANDM / 0xde84 ORM) is driven by
                 * the TPU - dynamic masking per burst window - and must not be
                 * pinned statically: IMR stays whatever the firmware and TPU
                 * write. The only re-arm kept is the dynamic bit5/BRINT0 fix
                 * (KEEP-IMR in calypso_c54x.c), which restores bit5 only when it
                 * drops. */
            }
            /* [imr0] (stdout, 40 lines; the [c54x] traces of stderr are filtered by verbosite.c):
             * an IMR=0000 followed by IDLE leaves the DSP asleep forever. Logged: who zeroes the
             * IMR, from where, with which return stack. */
            /* [imrW]: ALL the IMR writes after boot (60 lines), with DP and the words of the DP
             * page that routine 1:e4d1 (IMR save / restore) reads and writes. */
            if (s->insn_count > 1000000 && (uint16_t)val != s->imr) {
                static unsigned nw;
                if (nw++ < 60) {
                    uint16_t dpb = (uint16_t)((s->st0 & 0x1ff) << 7);
                    printf("  [imrW] IMR 0x%04x -> 0x%04x PC=%u:0x%04x op=0x%04x DP=0x%03x d[+05]=%04x d[+12]=%04x "
                           "d[+30]=%04x d[+50]=%04x A=%010llx B=%010llx AR2=%04x insn=%u\n",
                           s->imr, (uint16_t)val, s->xpc, s->pc, s->prog[s->pc], s->st0 & 0x1ff,
                           s->data[(uint16_t)(dpb | 0x05)], s->data[(uint16_t)(dpb | 0x12)],
                           s->data[(uint16_t)(dpb | 0x30)], s->data[(uint16_t)(dpb | 0x50)],
                           (unsigned long long)(s->a & 0xFFFFFFFFFFULL), (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                           s->ar[2], s->insn_count);
                }
            }
            /* IMR GUARD OF THE FR CODEC. Right after an uplink FACCH, the PDROM FR encoder (called
             * at 0xe3fc, return 0xe3fe) can write A through "STL A,*AR3+%" (0xe7ad, op 0x81d3) with
             * AR3 ~ 0: IMR 0x7012 -> 0, all DSP interrupts masked, voice port stopped (rint 0),
             * 27 ms frames, then ">!PM 202" in a loop and the cell lost. The codec never writes
             * the IMR: that is a corrupted pointer (AR3 traced below). The write is ignored if it
             * comes from the codec PDROM (0xe000..0xefff, XPC 0). CALYPSO_C54X_GARDE_IMR=0 lets
             * it through. */
            {
                static int garde = -1;
                if (garde < 0) garde = calypso_gate("CALYPSO_C54X_GARDE_IMR", 1);
                if (garde && s->xpc == 0 && s->pc >= 0xe000 && s->pc <= 0xefff && s->insn_count > 1000000) {
                    static unsigned ng;
                    if (ng++ < 40)
                        printf("  [garde-imr] IMR 0x%04x -> 0x%04x IGNORE (codec PDROM pc=0x%04x op=0x%04x AR3=%04x BK=%04x insn=%u) [%u]\n",
                               s->imr, (uint16_t)val, s->pc, s->prog[s->pc], s->ar[3], s->bk, s->insn_count, ng);
                    return;
                }
            }
            if (val == 0 && s->imr != 0) {
                static unsigned n0;
                if (n0++ < 40)
                    printf("  [imr0] IMR 0x%04x -> 0 PC=0x%04x op=0x%04x XPC=%d SP=0x%04x ret=%04x %04x %04x %04x "
                           "AR0..3=%04x %04x %04x %04x A=%010llx B=%010llx insn=%u\n",
                           s->imr, s->pc, s->prog[s->pc], s->xpc, s->sp,
                           s->data[s->sp], s->data[(uint16_t)(s->sp + 1)], s->data[(uint16_t)(s->sp + 2)],
                           s->data[(uint16_t)(s->sp + 3)], s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                           (unsigned long long)(s->a & 0xFFFFFFFFFFULL), (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                           s->insn_count);
                if (n0 <= 6 && s->insn_count > 1000000) {   /* hors boot : le chemin qui y mene */
                    extern void c54x_anneau_vider_stdout(const char *etiquette, unsigned n_max);
                    c54x_anneau_vider_stdout("imr0-chemin", 512);
                }
            }
            s->imr = val; return;
        case MMR_IFR: {
            /* IFR-CLEAR-W (unconditional, capped): does the go-live code write
             * IFR directly (write-1-to-clear) on bit5 (BRINT0) / bit12 (frame)
             * without ever dispatching - a software acknowledge that throws the
             * pending interrupt away instead of servicing it? */
            static unsigned _ifrw = 0;
            if ((val & 0x1020) && _ifrw < 100) {
                _ifrw++;
                fprintf(stderr, "[c54x] IFR-CLEAR-W #%u val=0x%04x (clears bit5=%d bit12=%d) "
                        "ifr_before=0x%04x -> after=0x%04x PC=0x%04x insn=%u\n",
                        _ifrw, val, !!(val & 0x20), !!(val & 0x1000),
                        s->ifr, (uint16_t)(s->ifr & ~val), s->pc, s->insn_count);
            }
            {   /* IFR-CLEAR-WHO: reports the pending interrupts this clear loses. */
                uint16_t _av = s->ifr;
                s->ifr &= (uint16_t)~val;
                uint16_t _pd = (uint16_t)(_av & ~s->ifr & s->imr);
                if (_pd) { static unsigned _n = 0;
                    if (_n++ < 40 || (_n % 5000) == 0)
                        fprintf(stderr, "[c54x] IFR-CLEAR-WHO #%u site=mmio-write "
                                "perdus=0x%04x IFR 0x%04x->0x%04x IMR=0x%04x "
                                "INTM=%d PC=0x%04x insn=%u\\n", _n, _pd, _av,
                                s->ifr, s->imr, (s->st1 & ST1_INTM) ? 1 : 0,
                                s->pc, s->insn_count);
                }
            }
            return;
        }
        case MMR_ST0:  s->st0 = val;
            /* Whole-ST0 restore (POPM ST0 / STLM): the non-LDP path that
             * changes DP. This is where DP becomes 0x087. */
            g_last_st0w_pc = s->pc; g_last_st0w_val = val;
            g_last_st0w_op = prog_fetch(s, s->pc); g_last_st0w_xpc = s->xpc;
            g_last_st0w_prev = g_prev_pc;
            st0_ring_rec(s, val, 'p'); /* pop/write ST0 */
            if (C54X_SONDES && g_orphan_on > 0 && (s->pc == 0xf48b || s->pc == 0x7737 || (val & 0x1FF) == 0x124)) {   /* [2026-09-23] sonde : lit g_spring, alimente sous C54X_SONDES */
                /* POPM ST0 at 0xf48b: the slot just popped is data[sp-1]. Look
                 * up its last writer in the stack ring. NO-WRITER means a stale
                 * slot, i.e. SP misaligned by a POP with no matching PUSH. */
                uint16_t slot = (uint16_t)(s->sp - 1);
                fprintf(stderr, "[c54x] ORPHAN@%04x SP=0x%04x slot=0x%04x val=0x%04x(DP=%03x)",
                        s->pc, s->sp, slot, val, (unsigned)(val & 0x1FF));
                int found = 0;
                unsigned rn = g_stkw_idx < STKW_RING_N ? g_stkw_idx : STKW_RING_N;
                for (unsigned i = 0; i < rn; i++) {
                    StkwEv *e = &g_stkw_ring[(g_stkw_idx - 1 - i) % STKW_RING_N];
                    if (e->addr == slot) {
                        fprintf(stderr, "  WRITER@%04x op=%04x val=%04x", e->pc, e->op, e->val);
                        found = 1; break;
                    }
                }
                if (!found)
                    fprintf(stderr, "  NO-WRITER → slot STALE → SP désaligné (POP sans PUSH)");
                fprintf(stderr, " insn=%u\n", s->insn_count);
                /* SP event ring: recent pushes and pops, which expose the
                 * unbalanced RET-family return (pop delta > 0 with no matching
                 * push) that shifts SP. */
                fprintf(stderr, "[c54x]   ORPHAN-SP-RING (anciens→récents, pc:op±delta) :");
                for (int k = 28; k >= 1; k--) {
                    struct sp_evt *e = &g_spring[(g_spring_idx - k) & 63];
                    fprintf(stderr, " %04x:%04x%+d", e->pc, e->op, e->delta);
                }
                fprintf(stderr, "\n");
            }
            return;
        case MMR_ST1:  s->st1 = val; return;
        case MMR_AL:   s->a = (s->a & ~0xFFFF) | val; return;
        case MMR_AH:   s->a = (s->a & ~((int64_t)0xFFFF << 16)) | ((int64_t)val << 16); return;
        case MMR_AG:   s->a = (s->a & 0xFFFFFFFF) | ((int64_t)(val & 0xFF) << 32); return;
        case MMR_BL:   s->b = (s->b & ~0xFFFF) | val; return;
        case MMR_BH:   s->b = (s->b & ~((int64_t)0xFFFF << 16)) | ((int64_t)val << 16); return;
        case MMR_BG:   s->b = (s->b & 0xFFFFFFFF) | ((int64_t)(val & 0xFF) << 32); return;
        case MMR_T:    s->t = val; return;
        case MMR_TRN:  s->trn = val; return;
        case MMR_AR0: case MMR_AR1: case MMR_AR2: case MMR_AR3:
        case MMR_AR4: case MMR_AR5: case MMR_AR6: case MMR_AR7:
            ar_write_track(s, addr - MMR_AR0, val);  /* unified probe AR0..AR7 */
            s->ar[addr - MMR_AR0] = val; return;
        case MMR_SP:
            if (val >= 0x0800 && val < 0x0900) {
                if (calypso_debug_enabled("SP-GUARD")) fprintf(stderr,
                        "[c54x] SP-GUARD: refused MMR_SP write 0x%04x "
                        "(API mailbox); keeping 0x%04x PC=0x%04x\n",
                        val, s->sp, s->pc);
                return;
            }
            /* SP GUARD OF THE CODEC. PDROM 0xf15c `st #0x001f, *(0x18)` in DIRECT addressing with
             * DP=0 gives SP=0x001f (and 0xf152 `st #0x20, *(0x16)` -> AR6): DSP gone to 0x00c0
             * forever, 28 ms frames, sync lost. Those ST target a page of codec variables: DP is
             * what is wrong (DP traced here). A stack below 0x0060 (MMR, scratch) makes no sense:
             * the write is ignored when it comes from the PDROM (0xe000..0xffff, XPC 0).
             * CALYPSO_C54X_GARDE_SP=0 disables. */
            {
                static int garde_sp = -1;
                if (garde_sp < 0) garde_sp = calypso_gate("CALYPSO_C54X_GARDE_SP", 1);
                if (garde_sp && val < 0x0060 && s->xpc == 0 && s->pc >= 0xe000 && s->insn_count > 1000000) {
                    static unsigned ngs;
                    if (ngs++ < 40)
                        printf("  [garde-sp] SP 0x%04x -> 0x%04x IGNORE (PDROM pc=0x%04x op=0x%04x DP=0x%03x AR6=%04x insn=%u) [%u]\n",
                               s->sp, val, s->pc, s->prog[s->pc], s->st0 & 0x1ff, s->ar[6], s->insn_count, ngs);
                    return;
                }
            }
            sp_abs_track(s, val, 0);  /* site 0: MMR_SP via STL/STM/STLM */
            s->sp = val;
            return;
        case MMR_BK:
            /* Who writes BK? BK=0 breaks circular addressing and sends AR2
             * running away (0xfa98/0xf17c); this names the writer and the value. */
            {
                static uint32_t bkw_n = 0;
                if (bkw_n < 40) {
                    fprintf(stderr, "[c54x] BK-WR (MMR) 0x%04x→0x%04x PC=0x%04x op=0x%04x "
                            "%s insn=%u\n", s->bk, val, s->pc, prog_fetch(s, s->pc),
                            (val == 0) ? "<<< BK=0 (casse circular!)" : "", s->insn_count);
                    bkw_n++;
                }
            }
            s->bk = val; return;
        case MMR_BRC:  s->brc = val; return;
        case MMR_RSA:  s->rsa = val; return;
        case MMR_REA:  s->rea = val; return;
        case MMR_PMST:
            {
                /* PMST-WR (read-only, ungated): separates "relocated IPTR 0x140
                 * lost" from "never emitted". First 300 writes, plus always when
                 * IPTR is 0x140. */
                {
                    static unsigned pmst_n = 0;
                    uint16_t niptr = (val >> PMST_IPTR_SHIFT) & 0x1FF;
                    if (pmst_n < 300 || niptr == 0x140) {
                        pmst_n++;
                        fprintf(stderr, "[c54x] PMST-WR #%u val=0x%04x IPTR=0x%03x PC=0x%04x insn=%u%s\n",
                                pmst_n, val, niptr, s->pc, s->insn_count,
                                niptr == 0x140 ? "  <<< IPTR=0x140 EMITTED" : "");
                    }
                }
                static unsigned pmst_wr_attempts = 0;
                if (pmst_wr_attempts++ < 100)
                    C54_LOG("PMST WR attempt #%u: val=0x%04x cur=0x%04x PC=0x%04x insn=%u",
                            pmst_wr_attempts, val, s->pmst, s->pc, s->insn_count);
            }
            if (val != s->pmst) {
                uint16_t old_iptr = (s->pmst >> PMST_IPTR_SHIFT) & 0x1FF;
                uint16_t new_iptr = (val >> PMST_IPTR_SHIFT) & 0x1FF;
                {
                    static unsigned pmst_log = 0;
                    if (pmst_log++ < 100)
                        C54_LOG("PMST change 0x%04x → 0x%04x (IPTR=0x%03x→0x%03x OVLY=%d) PC=0x%04x SP=0x%04x insn=%u #%u/100",
                                s->pmst, val, old_iptr, new_iptr, !!(val & PMST_OVLY), s->pc, s->sp, s->insn_count, pmst_log);
                }

                static uint16_t last_dumped_iptr = 0xFFFF;
                static unsigned vecdump_count = 0;
                /* Cap at 8 dumps total - firmware may oscillate between 2-3
                 * IPTR values thousands of times during a session, and each
                 * dump emits 32 fprintf lines. Without cap : 250k+ log lines
                 * = saturates host I/O = bridge stops emitting CLK INDs =
                 * BTS shutdown "No more clock from transceiver". */
                if (new_iptr != last_dumped_iptr && vecdump_count < 8) {
                    vecdump_count++;
                    last_dumped_iptr = new_iptr;
                    uint32_t base = (uint32_t)new_iptr << 7;
                    uint16_t saved_pmst = s->pmst;
                    s->pmst = val;
                    C54_LOG("VECDUMP IPTR=0x%03x base=0x%04x (32 vectors) #%u/8:",
                            new_iptr, (uint16_t)base, vecdump_count);
                    for (int vec = 0; vec < 32; vec++) {
                        uint32_t a = base + vec * 4;
                        uint16_t w⟨1⟩ = prog_read(s, a + ⟨2⟩);  ×4
    ⟨⟩ = (0,0) (1,1) (2,2) (3,3)
                        fprintf(stderr,
                                "[c54x] vec %2d @ 0x%04x : %04x %04x %04x %04x\n",
                                vec, (uint16_t)a, w0, w1, w2, w3);
                    }
                    s->pmst = saved_pmst;
                }
            }
            s->pmst = val; return;
        case MMR_XPC:
            {
                static int xpc_log = 0;
                if (xpc_log++ < 50)
                    C54_LOG("MMR_XPC WR val=0x%04x (was %d) PC=0x%04x SP=0x%04x insn=%u",
                            val, s->xpc, s->pc, s->sp, s->insn_count);
            }
            s->xpc = val & 3;
            return;
        default: return;
        }
    }

    /* DMA controller (calypso_dma.c): intercepts before the older decoding below
     * and returns true when it has handled the write. Off by default
     * (CALYPSO_DMA), in which case it returns false and nothing changes.
     * Warning: the two decodings disagree on the mapping - SPRU131 puts DMPREC at
     * 0x54, the older code puts DMSA there. See calypso_dma.h. */
    if (calypso_dma_mmr_write(s, addr, val)) {
        return;
    }

    /* DMA sub-register bank (C54x DMA controller).
     * DMSA (0x0054): sets the sub-register address.
     * DMSDI (0x0055): writes sub-register data, auto-increments DMSA.
     * DMSDN (0x0057): writes sub-register data, no auto-increment.
     * DMA channel 0 sub-registers (BSP receive DMA):
     *   sub 0x00=DMSRC0, 0x01=DMDST0, 0x02=DMCTR0, 0x03=DMMCR0 */
    if (addr == 0x0054) {
        s->dma_subaddr = val;
        s->data[0x0054] = val;
        return;
    }
    if (addr == 0x0055 || addr == 0x0057) {
        uint16_t sa = s->dma_subaddr;
        if (sa < 24) {  /* 6 channels x 4 regs */
            s->dma_subregs[sa] = val;
            int ch = sa / 4;
            int reg = sa % 4;
            static const char *rnames[] = {"SRC","DST","CTR","MCR"};
            C54_LOG("DMA ch%d %s = 0x%04x (sub 0x%02x) PC=0x%04x",
                    ch, rnames[reg], val, sa, s->pc);
        }
        s->data[addr] = val;
        if (addr == 0x0055) s->dma_subaddr++;  /* auto-increment */
        return;
    }

    /* McBSP sub-register bank (serial port extended config).
     * SPSA (0x0038): sub-address. SPSD (0x0039): sub-data. */
    if (addr == 0x0038 || addr == 0x0039) {
        if (addr == 0x0038) s->spsa = val;
        else {
            C54_LOG("McBSP sub[0x%02x] = 0x%04x PC=0x%04x", s->spsa, val, s->pc);
        }
        s->data[addr] = val;
        return;
    }

    /* API RAM (shared with ARM) */
    if (addr >= C54X_API_BASE && addr < C54X_API_BASE + C54X_API_SIZE) {
        uint16_t woff = addr - C54X_API_BASE;
        /* SAM / HOM arbitration - CAL000 7.2.1, CAL207 9.1.
         *
         * "In HOM mode (Host Only Mode), the API RAM is dedicated to external
         * access under the control of either the ARM or the DMA controller." In
         * HOM the DSP therefore has no access to this window; the model had no
         * notion of it and both sides wrote unarbitrated.
         *
         * Measured: the DSP firmware switches HOM<->SAM ONCE PER FRAME
         * (API_CONF=0x0002 at 0xa693, back to 0x0000 at 0xa4e7).
         *
         * Two deliberately separate stages:
         *   - observation (CALYPSO_API_HOM_WATCH, default 1): counts and logs the
         *     DSP writes made during HOM, changing nothing.
         *   - enforcement (CALYPSO_API_HOM_STRICT, default 0): DROPS the write,
         *     as the silicon would. A real behaviour change, hence opt-in and to
         *     be validated under load.
         * The documentation does not say what the DSP reads in HOM, so the read
         * path is left alone. */
        {
            static int watch = -1, strict = -1;
            if (watch < 0) {
                watch  = calypso_gate("CALYPSO_API_HOM_WATCH", 1);
                strict = calypso_gate("CALYPSO_API_HOM_STRICT", 0);
                if (strict)
                    fprintf(stderr, "[c54x] API_HOM_STRICT=1 : les ecritures DSP "
                            "dans la fenetre API sont ABANDONNEES pendant HOM "
                            "(CAL000 §7.2.1). Changement de comportement — a "
                            "valider sous charge.\n");
            }
            if ((watch || strict) && calypso_xio_api_hom()) {
                static unsigned long long n_hom = 0;
                if (n_hom++ == 0 || (n_hom % 5000) == 0)
                    fprintf(stderr, "[c54x] API-HOM : ecriture DSP dans la fenetre "
                            "API pendant HOM #%llu — data[0x%04x] <- 0x%04x "
                            "PC=0x%04x (%s)\n", n_hom, addr, val, s->pc,
                            strict ? "ABANDONNEE" : "laissee passer, observation");
                if (strict)
                    return;
            }
        }
        if (s->api_ram)
            s->api_ram[woff] = val;
        {   /* FBDET-API, DSP side: api_ram writes to d_fb_det / a_sync_demod. */
            static int _fa = -1; static unsigned _fan = 0;
            if (_fa < 0) _fa = calypso_gate("CALYPSO_FBDET_API", 0);
            if (_fa && woff >= 0xF8 && woff <= 0xFD && _fan < 40) {
                _fan++;
                fprintf(stderr, "[c54x] FBDET-API DSP api_ram[0x%02x] (mot 0x%04x, %s)"
                        " <- 0x%04x PC=0x%04x insn=%u\n", woff, addr,
                        woff == 0xF8 ? "d_fb_det" : "a_sync_demod",
                        val, s->pc, s->insn_count);
            }
        }
        /* === DSP->ARM STATUS / DEMOD probe (CCCH chain tracing) ===
         * Track DSP writes to the four critical mailbox regions :
         *   (1) a_pm[3]  + a_serv_demod[4]  on read page 0  : woff 0x30..0x36
         *   (2) a_pm[3]  + a_serv_demod[4]  on read page 1  : woff 0x44..0x4A
         *   (3) a_cd[15] CCCH demod result (CLAUDE.md DWARF) : woff 0x1C2..0x1D0
         *   (4) a_cd[15] CCCH demod result (shunt DWARF v2)  : woff 0x1D2..0x1E0
         * If a_serv_demod_WP* never appears -> DSP never advances past
         * cell-search. If a_cd ranges stay silent -> CCCH demod never
         * publishes (= bridge GMSK not converging, or task-md chain
         * never reaches CCCH). */
        {
            bool in_serv = (woff >= 0x0030 && woff <= 0x0036)
                        || (woff >= 0x0044 && woff <= 0x004A);
            bool in_acd  = (woff >= 0x01C2 && woff <= 0x01E0);
            if (in_serv || in_acd) {
                static unsigned cd_log;
                const unsigned LIMIT = 400;
                if (cd_log < LIMIT) {
                    const char *tag;
                    if (woff >= 0x0030 && woff <= 0x0032) tag = "A_PM_WP0";
                    else if (woff >= 0x0033 && woff <= 0x0036) tag = "A_SERV_DEMOD_WP0";
                    else if (woff >= 0x0044 && woff <= 0x0046) tag = "A_PM_WP1";
                    else if (woff >= 0x0047 && woff <= 0x004A) tag = "A_SERV_DEMOD_WP1";
                    else tag = "A_CD";
                    fprintf(stderr,
                            "[c54x] DSP-API-WR #%u %s woff=0x%04x val=0x%04x "
                            "PC=0x%04x AR2=%04x AR3=%04x AR4=%04x AR5=%04x insn=%u\n",
                            cd_log, tag, woff, val, s->pc,
                            s->ar[2], s->ar[3], s->ar[4], s->ar[5],
                            s->insn_count);
                    cd_log++;
                    if (cd_log == LIMIT) {
                        fprintf(stderr,
                                "[c54x] DSP-API-WR log capped at %u\n", LIMIT);
                    }
                }
            }
        }
        /* Notify the ARM-side mailbox watcher (calypso_trx) so it can
         * pulse IRQ_API, mirror to dsp_ram, and run the d_fb_det hook.
         * Without this, DSP writes to NDB cells are invisible to ARM. */
        if (s->api_write_cb)
            s->api_write_cb(s->api_write_cb_opaque, woff, val);
        /* Stack-corruption watch: stack push landing in the NDB
         * mailbox region [0x0800..0x08FF]. Only fires when SP has
         * already been corrupted into that range. */
        if (addr == s->sp && addr >= 0x0800 && addr < 0x0900) {
            if (calypso_debug_enabled("STACK-IN-NDB")) fprintf(stderr,
                    "[c54x] STACK-IN-NDB addr=0x%04x val=0x%04x SP=0x%04x "
                    "PC=0x%04x insn=%u op[pc-2..pc+1]=%04x %04x %04x %04x\n",
                    addr, val, s->sp, s->pc, s->insn_count,
                    s->prog[(uint16_t)(s->pc - 2)],
                    s->prog[(uint16_t)(s->pc - 1)],
                    s->prog[s->pc],
                    s->prog[(uint16_t)(s->pc + 1)]);
        }
        /* Always log writes to d_dsp_page (0x08D4 ; 0x08E2 = d_dsp_state) */
        if (addr == 0x08D4) {
            C54_LOG("DSP WR d_dsp_page = 0x%04x PC=0x%04x insn=%u op[pc-2..pc+1]=%04x %04x %04x %04x",
                    val, s->pc, s->insn_count,
                    s->prog[(uint16_t)(s->pc - 2)],
                    s->prog[(uint16_t)(s->pc - 1)],
                    s->prog[s->pc],
                    s->prog[(uint16_t)(s->pc + 1)]);
        }

        /* d_spcx_rif (NDB word 2 = DSP data 0x08D6) - BSP serial port config */
        if (addr == 0x08D6) {
            C54_LOG("DSP WR d_spcx_rif = 0x%04x PC=0x%04x insn=%u op[pc-2..pc+1]=%04x %04x %04x %04x",
                    val, s->pc, s->insn_count,
                    s->prog[(uint16_t)(s->pc - 2)],
                    s->prog[(uint16_t)(s->pc - 1)],
                    s->prog[s->pc],
                    s->prog[(uint16_t)(s->pc + 1)]);
        }
        /* Full a_sync_demod + d_fb_mode write watch: every cell, no PC filter,
         * so real fb-det writes and stomp candidates are both caught. Writes from
         * PC=0x06xx are tagged [STOMP?] for easy grepping.
         * Three discriminating outcomes for d_fb_mode - the binary "FB matched"
         * flag the ARM actually tests:
         *   never written  -> the "FB confirmed" path is never reached
         *   written as 0   -> the DSP scans but never crosses the threshold
         *   written non-0 while the ARM reads 0 -> a coherence bug. */
        if (addr == 0x08F9 || addr == 0x08FA ||
            addr == 0x08FB || addr == 0x08FC || addr == 0x08FD) {
            static unsigned ts_log[5] = {0};
            static uint16_t prev_d_fb_mode = 0xFFFF;
            int idx = (addr == 0x08F9) ? 0 :
                      (addr == 0x08FA) ? 1 :
                      (addr == 0x08FB) ? 2 :
                      (addr == 0x08FC) ? 3 : 4;
            const char *name = (idx == 0) ? "d_fb_mode"  :
                               (idx == 1) ? "a_sync_TOA" :
                               (idx == 2) ? "a_sync_PM"  :
                               (idx == 3) ? "a_sync_ANG" : "a_sync_SNR";
            ts_log[idx]++;
            bool transition = (idx == 0) &&
                              (prev_d_fb_mode != 0xFFFF) &&
                              (prev_d_fb_mode != val) &&
                              (val != 0 || prev_d_fb_mode != 0);
            bool stomp_zone = (s->pc >= 0x0600 && s->pc < 0x0700);
            bool log_it = transition ||
                          (idx == 0 && val != 0) ||
                          (val != 0 && ts_log[idx] <= 50) ||
                          (ts_log[idx] % 1000) == 0;
            if (log_it) {
                C54_LOG("DSP WR %s = 0x%04x (s=%d) PC=0x%04x%s insn=%u #%u%s",
                        name, val, (int)(int16_t)val, s->pc,
                        stomp_zone ? " [STOMP?]" : "",
                        s->insn_count, ts_log[idx],
                        transition ? " *TRANSITION*" : "");
            }
            if (idx == 0) prev_d_fb_mode = val;
        }
        /* d_fb_det (NDB word 36 = DSP data 0x08F8). The firmware FB-det path
         * treats the correlator output as Q15 signed. Logs every write (thinned
         * past 200) and dumps the adjacent NDB cells [0x08F0..0x0900] so the
         * correlator output, the flag and the a_sync_demod fields can be read
         * together. */
        if (addr == 0x08F8) {
            static unsigned fbd_log = 0;
            /* Filter out stack-stomp at d_fb_det: only PCs known to be
             * actual fb-det correlator stores (0x8d33, 0x8eb9, 0x8f51) get
             * the full per-write log + NDB+DARAM dumps. Other PCs (e.g.
             * 0xb906 push site, 0x7763/0x7764 SP-overflow) get a counted
             * one-line tag so we don't lose visibility on them, but they
             * stop polluting the watch stream. */
            bool real_fbdet = (s->pc == 0x8d33 || s->pc == 0x8eb9 ||
                               s->pc == 0x8f51);
            /* FBDET-DIVERSITY: count distinct values per 1M-insn window.
             * 1 = DSP pegged on stale data. >5 = real scan. Discriminates
             * "BSP delivers fresh I/Q" from "DSP recorrelates same window". */
            if (real_fbdet) {
                static uint16_t recent_vals[8] = {0};
                static unsigned next_window = 1000000;
                static int n_distinct = 0;
                int seen = 0;
                for (int i = 0; i < 8; i++) {
                    if (recent_vals[i] == val) { seen = 1; break; }
                }
                if (!seen) {
                    recent_vals[n_distinct & 7] = val;
                    n_distinct++;
                }
                if (s->insn_count >= next_window) {
                    C54_LOG("FBDET-DIVERSITY window=%uM distinct=%d",
                            next_window / 1000000, n_distinct);
                    n_distinct = 0;
                    for (int i = 0; i < 8; i++) recent_vals[i] = 0;
                    next_window = (s->insn_count / 1000000 + 1) * 1000000;
                }
            }
            if (real_fbdet && (fbd_log < 200 || (fbd_log % 1000) == 0)) {
                C54_LOG("DSP WR d_fb_det = 0x%04x (s=%d) PC=0x%04x insn=%u op[pc-2..pc+1]=%04x %04x %04x %04x",
                        val, (int)(int16_t)val, s->pc, s->insn_count,
                        s->prog[(uint16_t)(s->pc - 2)],
                        s->prog[(uint16_t)(s->pc - 1)],
                        s->prog[s->pc],
                        s->prog[(uint16_t)(s->pc + 1)]);
                C54_LOG("  NDB[0x08F0..0x0900]: %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x",
                        s->data[0x08F0], s->data[0x08F1], s->data[0x08F2], s->data[0x08F3],
                        s->data[0x08F4], s->data[0x08F5], s->data[0x08F6], s->data[0x08F7],
                        val,             s->data[0x08F9], s->data[0x08FA], s->data[0x08FB],
                        s->data[0x08FC], s->data[0x08FD], s->data[0x08FE], s->data[0x08FF],
                        s->data[0x0900]);
                if (fbd_log < 5) {
                    C54_LOG("  DARAM[0x3FB0..0x3FBF]: %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x",
                            s->data[0x3FB0], s->data[0x3FB1], s->data[0x3FB2], s->data[0x3FB3],
                            s->data[0x3FB4], s->data[0x3FB5], s->data[0x3FB6], s->data[0x3FB7],
                            s->data[0x3FB8], s->data[0x3FB9], s->data[0x3FBA], s->data[0x3FBB],
                            s->data[0x3FBC], s->data[0x3FBD], s->data[0x3FBE], s->data[0x3FBF]);
                }
            } else if (!real_fbdet) {
                static unsigned other_pc_count = 0;
                other_pc_count++;
                if (other_pc_count == 1 || other_pc_count == 100 ||
                    other_pc_count == 10000 || other_pc_count == 1000000) {
                    C54_LOG("d_fb_det NON-FBDET-PC write #%u val=0x%04x PC=0x%04x SP=0x%04x",
                            other_pc_count, val, s->pc, s->sp);
                }
            }
            /* d_fb_det zero-override trace. Measured race window: the DSP
             * writes a high SNR (e.g. 0x7902, 0x7766) at the fb-det PCs, then
             * something zeroes d_fb_det before the ARM reads it; the ARM sees 200
             * times 0x0000, finds no FB and retries L1CTL_FBSB_REQ forever.
             * Captures EVERY write of 0 to 0x08F8 with full context, to identify
             * the zeroing PCs and reconstruct the condition (threshold check,
             * post-correlation reset, error path). Cap 200. */
            if (val == 0) {
                static unsigned zero_log = 0;
                if (zero_log < 200) {
                    C54_DBG("FBDET", "D_FB_DET ZERO-WR #%u PC=0x%04x op=0x%04x prev=0x%04x "
                            "A=%010llx B=%010llx T=0x%04x ST0=0x%04x ST1=0x%04x insn=%u",
                            zero_log + 1,
                            s->pc, s->prog[s->pc],
                            s->data[0x08F8],
                            (unsigned long long)(s->a & 0xFFFFFFFFFFLL),
                            (unsigned long long)(s->b & 0xFFFFFFFFFFLL),
                            s->t, s->st0, s->st1, s->insn_count);
                    zero_log++;
                }
            }
            /* Transition trace : non-zero -> zero (the override moment).
             * Logs whenever d_fb_det was non-zero just before this write
             * but the new write makes it zero. Cap 100. */
            if (val == 0 && s->data[0x08F8] != 0) {
                static unsigned override_log = 0;
                if (override_log < 100) {
                    C54_DBG("FBDET", "D_FB_DET OVERRIDE #%u prev=0x%04x → 0 PC=0x%04x op=0x%04x "
                            "A=%010llx ST0=0x%04x insn=%u",
                            override_log + 1,
                            s->data[0x08F8], s->pc, s->prog[s->pc],
                            (unsigned long long)(s->a & 0xFFFFFFFFFFLL),
                            s->st0, s->insn_count);
                    override_log++;
                }
            }
            /* Non-zero SET trace plus SET->CLEAR delta, the mirror of ZERO-WR.
             * Captures every write of a non-zero value to 0x08F8 to identify who
             * sets "FB found" and how many cycles it survives before a clear.
             * A delta below 100 insn points at an opcode bug hitting immediately;
             * a delta of thousands of insn is a legitimate timing race between
             * the DSP set and the ARM read. */
            {
                static uint64_t last_set_insn;
                static uint16_t last_set_val;
                static uint16_t last_set_pc;
                static unsigned set_log_n = 0;
                static unsigned delta_log_n = 0;
                if (val != 0) {
                    /* SET event */
                    if (set_log_n < 500) {
                        C54_DBG("FBDET", "D_FB_DET SET #%u val=0x%04x PC=0x%04x op=0x%04x "
                                "prev=0x%04x A=%010llx insn=%u",
                                set_log_n + 1,
                                val, s->pc, s->prog[s->pc],
                                s->data[0x08F8],
                                (unsigned long long)(s->a & 0xFFFFFFFFFFLL),
                                s->insn_count);
                        set_log_n++;
                    }
                    last_set_insn = s->insn_count;
                    last_set_val = val;
                    last_set_pc = s->pc;
                } else if (s->data[0x08F8] != 0 && last_set_insn != 0) {
                    /* CLEAR after non-zero - log delta */
                    uint64_t delta = (uint64_t)s->insn_count - last_set_insn;
                    if (delta_log_n < 100) {
                        C54_LOG("D_FB_DET SET-TO-CLEAR-DELTA #%u "
                                "set_PC=0x%04x set_insn=%llu set_val=0x%04x "
                                "clear_PC=0x%04x clear_insn=%u delta=%llu cycles",
                                delta_log_n + 1,
                                last_set_pc, (unsigned long long)last_set_insn,
                                last_set_val, s->pc, s->insn_count,
                                (unsigned long long)delta);
                        delta_log_n++;
                    }
                }
            }
            fbd_log++;
        }
    }

    /* Log DARAM writes to code target area and count total */
    if (addr >= 0x0020 && addr < 0x0800) {
        static int dw_total = 0;
        dw_total++;
        if (addr >= 0x1200 && addr <= 0x1240) {
            C54_LOG("DARAM WR [0x%04x] = 0x%04x PC=0x%04x insn=%u",
                    addr, val, s->pc, s->insn_count);
        }
        if (dw_total == 1 || dw_total == 100 || dw_total == 1000 || dw_total == 10000)
            C54_LOG("DARAM write count: %d (last: [0x%04x]=0x%04x)", dw_total, addr, val);
    }

    /* Frame-IT probe: who writes the flags the wedged DSP polls (data[0x006e],
     * data[0x585f])? Separates an ISR relocation from a hardware write. No write
     * at all, or a value never matching what is expected, means the flag is never
     * set and the DSP deadlocks. */
    if (addr == 0x006e || addr == 0x585f || addr == 0x8a44) {
        static uint32_t fw_n = 0;
        if (fw_n < 80) {
            fprintf(stderr, "[c54x] FLAGWR data[0x%04x] 0x%04x→0x%04x PC=0x%04x "
                    "INTM=%d insn=%u\n", addr, s->data[addr], val, s->pc,
                    !!(s->st1 & ST1_INTM), s->insn_count);
            fw_n++;
        }
    }

    /* SBSLOT-WR (read-only, cap 300): who writes the SB slots
     * a_serv_demod[D_TOA] in db_r (p0 data[0x0830], p1 data[0x0844]) and a_sch[3]
     * (p0 data[0x083a], p1 data[0x084e])? Separates a scatter write from another
     * writer from never written. Logs PC, opcode, A low and value. */
    if (addr == 0x0830 || addr == 0x0844 || addr == 0x083a || addr == 0x084e) {
        static unsigned sbw_n = 0;
        if (sbw_n < 300) {
            const char *what = (addr == 0x0830) ? "SERV_TOA_p0" :
                               (addr == 0x0844) ? "SERV_TOA_p1" :
                               (addr == 0x083a) ? "A_SCH3_p0"   : "A_SCH3_p1";
            fprintf(stderr, "[c54x] SBSLOT-WR %s data[0x%04x] 0x%04x->0x%04x "
                    "PC=0x%04x op=0x%04x A=0x%010llx insn=%u\n",
                    what, addr, s->data[addr], val, s->pc,
                    prog_fetch(s, s->pc),
                    (unsigned long long)(s->a & 0xFFFFFFFFFFULL), s->insn_count);
            sbw_n++;
        }
    }

    /* Who writes d_fb_mode (0x08f9) with garbage (> 1)? The detector runs but
     * d_fb_mode was measured at 0x435b instead of 0 or 1, so the window or the
     * scaling is wrong; a runaway AR2 with BK=0 would corrupt it. Names the
     * corrupter. */
    if (addr == 0x08f9 && val > 1) {   /* garbage only (value other than 0/1) */
        static uint32_t fm_n = 0;
        if (fm_n < 40) {
            fprintf(stderr, "[c54x] FBMODE-GARBAGE data[0x08f9] 0x%04x→0x%04x PC=0x%04x "
                    "op=0x%04x SP=0x%04x AR2=%04x AR3=%04x BK=%04x %s insn=%u\n",
                    s->data[0x08f9], val, s->pc, prog_fetch(s, s->pc),
                    s->sp, s->ar[2], s->ar[3], s->bk,
                    (s->sp == 0x08f9) ? "<<< SP-PUSH" :
                    (s->ar[2] == 0x08f9 || s->ar[2] == 0x08f8) ? "<<< AR2-STORE" : "?",
                    s->insn_count);
            fm_n++;
        }
    }

    /* DISPATCH_INSTALL, "init" mode - the crutch grafts onto the init routine
     * instead of fighting it.
     *
     * In the older "dispatch" mode it reinstalled data[0x43d8] on every pass of
     * the dispatcher (exec_pc == 0xb01c) while the init routine at 0xbb00 put its
     * 0xab38 back. Measured [2026-07-30]: 2894 rewrites by 0xbb00 (~1447
     * re-inits) and 156550 POST-BOOTSTUB-RET at PC=0x0000; the same evening's A/B
     * showed zero of both without the crutch. The storm was self-inflicted by the
     * fight, not a firmware defect.
     *
     * Here the INIT write is intercepted instead: 0xbb00 writes 0xab38, that
     * value is seen as-is by the mailbox monitor and by WATCH-WR (both placed
     * above, so the trace stays honest), and the substitution happens just before
     * the store. One write per init, no conflict, deterministic ordering.
     *
     * @BEQUILLE - DISPATCH_INSTALL_AT=init  (with CALYPSO_DISPATCH_INSTALL=0xNNNN)
     *   masque  : the routine that should install a real handler in 0x43d8; the
     *             slot no longer holds what the ROM put there.
     *   retirer : as soon as it is known who must populate 0x43d8. The gate
     *             already stays silent if a value other than 0xab38 appears, so
     *             it will not hide a real installer showing up.
     */
    if (addr == 0x43d8) {
        static int _dg = -1, _dgv = -1; static unsigned _dgn = 0;
        if (_dg < 0) {
            const char *m = calypso_getenv("CALYPSO_DISPATCH_INSTALL_AT");
            _dg = (m && strcmp(m, "init") == 0) ? 1 : 0;
            const char *v = calypso_getenv("CALYPSO_DISPATCH_INSTALL");
            _dgv = (v && *v) ? (int)strtoul(v, NULL, 0) : -1;
            if (_dg && _dgv >= 0)
                fprintf(stderr, "[c54x] DISPATCH-GRAFT arme : toute ecriture de 0xab38 "
                        "dans data[0x43d8] devient 0x%04x (mode init, BEQUILLE — le "
                        "mode dispatch est desactive)\n", (unsigned)_dgv);
        }
        if (_dg && _dgv >= 0 && val == 0xab38) {
            if (_dgn++ < 20)
                fprintf(stderr, "[c54x] DISPATCH-GRAFT #%u : l'init (PC=0x%04x) ecrit "
                        "0xab38 dans data[0x43d8] -> greffe 0x%04x insn=%u\n",
                        _dgn, s->pc, (unsigned)_dgv, s->insn_count);
            val = (uint16_t)_dgv;
        }
    }

    s->data[addr] = val;
}

/* 23-bit program address translation : honors XPC for >=0x8000 (extended
 * program memory / banked area), passes through for <0x8000 (common bank 0).
 * Shared by prog_fetch and prog_read so they cannot diverge again.
 * OVLY (DARAM mirror) is handled at call site because it routes to s->data[]. */
inline uint32_t c54x_prog_xlate(const C54xState *s, uint16_t addr16)
{
    /* Only the 0x8000-0xDFFF window is banked by XPC (PROM0/2/3 overlay).
     * 0xE000-0xFFFF is fixed ROM (PROM1, mirrored at prog[0xE000+]) and is not
     * banked on silicon. Applying XPC above 0xE000 makes PC=0xee00 with XPC=3
     * fetch prog[0x3ee00], past the 8K of PROM3, which reads as empty (op=0x0000)
     * and sends the post-correlator FB stage running away. High ROM must ignore
     * XPC. */
    /* EXCEPT IN PAGES 1 AND 2: PROM1 (0x18000) and PROM2 (0x28000) are 32K and cover their own
     * 0xE000-0xFFFF, unrelated to the PDROM (15886 bytes differ out of 16384). 1:bc82 `call 0xe4d1`
     * (PROM1 routine "ld #150,dp") must not execute PDROM 0xe4d1, the middle of the FR codec
     * unpacking, which with AR5=0xffff does `stl b,*ar5+` then `andm #0x1f8,*ar5` on the IMR
     * (MMR 0): IMR=0, mute DSP, PM=0 on every TCH frame, L1 RECOVERY and network loss in call.
     * PROM3 (8K) keeps the page 0 fallback. CALYPSO_XPC_HAUT=0: page 0 fallback for all pages. */
    if (addr16 >= 0x8000 && (addr16 < 0xE000 || c54x_xpc_haut_banque(s->xpc))) {
        return (((uint32_t)s->xpc << 16) | addr16) & (C54X_PROG_SIZE - 1);
    }
    return addr16;   /* 0x0000-0x7FFF on-chip + 0xE000-0xFFFF PDROM in page 0 (and 3) */
}

int g_c54x_xpc_haut = -1;
int c54x_xpc_haut_init(void)
{
    if (g_c54x_xpc_haut < 0) {
        g_c54x_xpc_haut = calypso_gate("CALYPSO_XPC_HAUT", 1);
        fprintf(stderr, "[c54x] XPC-HAUT %s : 0xE000-0xFFFF %s\n", g_c54x_xpc_haut ? "ACTIF" : "INACTIF",
                g_c54x_xpc_haut ? "banque par XPC en pages 1 et 2 (PROM1/PROM2)" : "toujours en page 0");
    }
    return g_c54x_xpc_haut;
}

/* Out-of-line definition, kept for the external callers (pont.c); the core
 * goes through the inline copy of c54x_internal.h. */
#undef prog_fetch
uint16_t prog_fetch(C54xState *s, uint16_t pc)
{
    if ((s->pmst & PMST_OVLY) && pc >= c54x_ovly_bas() && pc < 0x2800)
        return s->data[pc];
    return s->prog[c54x_prog_xlate(s, pc)];
}

/* Floor of the OVLY alias. 0x0000-0x005F are mapped registers, 0x0060-0x007F is
 * the DARAM scratch pad. The SCH filter bank fills data[0x0060..0x0066] with MVDD
 * (prologue 0x8336) then reads those coefficients back with FIRS at
 * pmad=0x0061..0x0064, i.e. through PROGRAM space, which only works if the
 * scratch pad is visible there. With the floor at 0x80 the read fell through to
 * unloaded ROM and returned 0xF4E4 (measured: FIRS #1 pmad=0x0063,
 * coef(prog)=0xf4e4). Gate CALYPSO_OVLY_SCRATCH, default 1.
 * Warning: GLOBAL effect - the alias serves everything executed or read in
 * overlay. */
uint16_t g_c54x_ovly_bas = 0;   /* cache for c54x_prog_fetch_vite */
uint16_t c54x_ovly_bas(void)
{
    static int g = -1;
    if (g < 0) {
        g = calypso_gate("CALYPSO_OVLY_SCRATCH", 1);
        g_c54x_ovly_bas = g ? 0x0060 : 0x0080;
        fprintf(stderr, "[c54x] OVLY-SCRATCH %s : plancher de l alias programme "
                "a 0x%04x (scratch-pad DARAM 0x0060-0x007F %s)\n",
                g ? "ACTIF" : "INACTIF", g ? 0x0060 : 0x0080,
                g ? "VISIBLE en programme" : "hors alias");
    }
    return g ? 0x0060 : 0x0080;
}

uint16_t prog_read(C54xState *s, uint32_t addr)
{
    uint16_t addr16 = addr & 0xFFFF;
    if ((s->pmst & PMST_OVLY) && addr16 >= c54x_ovly_bas() && addr16 < 0x2800)
        return s->data[addr16];
    return s->prog[c54x_prog_xlate(s, addr16)];
}

void prog_write(C54xState *s, uint32_t addr, uint16_t val)
{
    uint16_t addr16 = addr & 0xFFFF;
    /* PROM1 (0xE000-0xFFFF) is ROM - reject writes */
    if (addr16 >= 0xE000) return;
    if ((s->pmst & PMST_OVLY) && addr16 >= c54x_ovly_bas() && addr16 < 0x2800)
        s->data[addr16] = val;
    if (addr16 >= 0x8000) {
        uint32_t ext = ((uint32_t)s->xpc << 16) | addr16;
        ext &= (C54X_PROG_SIZE - 1);
        s->prog[ext] = val;
    }
    s->prog[addr16] = val;
}

/* ================================================================
 * Addressing mode helpers
 * ================================================================ */

/* Canonical C54x circular addressing (SPRU172, tic54x-dis.c): step is +/-1 or
 * +/-AR0, with |step| <= BK. BK=0 means linear - STM #0,BK is deliberate and
 * must not wrap.
 *
 * "base = ar - (ar % bk)" puts the base on a grid of multiples of BK. The C54x
 * does not work that way: the buffer starts at an address whose low N bits are
 * zero, with 2^N >= BK; the index is ARn & (2^N-1) and the base ARn & ~(2^N-1).
 *
 * Measured [2026-07-29] on the firmware DMA request queue (BK=14):
 *     ring at 0x4340   old:     0x4340 % 14 = 10 -> base 0x4336, index 10
 *                      correct: mask 0x0F        -> base 0x4340, index 0
 * The emulator therefore placed the ring at 0x4336..0x4343, straddling 0x433e and
 * 0x433f - the two pointers of the OTHER queue. Queue 2's pointer wandered
 * through 0x433d..0x433f and overwrote queue 1: corrupt accounting, then
 * `orm *(0x3f92), #0x0008` at 0xaa83 = DSP_ERR_DMA_PROG, reported to the ARM
 * ("DSP Error Status: 8", 605 times).
 *
 * Warning: like any ISA fix the effect is GLOBAL - circular addressing also
 * serves the filters and the I/Q buffers. CALYPSO_CIRC_BASE_MOD=1 restores the
 * old computation for A/B comparison. */
uint16_t c54x_circ_ref(uint16_t ar, int step, uint16_t bk)
{
    if (bk == 0) return (uint16_t)(ar + step);

    static int _vieux = -1;
    if (_vieux < 0) _vieux = calypso_gate("CALYPSO_CIRC_BASE_MOD", 0);

    uint16_t base, masque;
    int idx;

    if (_vieux) {
        base = (uint16_t)(ar - (ar % bk));
        idx  = (int)(ar % bk) + step;
    } else {
        unsigned n = 0;
        while ((1u << n) < (unsigned)bk) n++;   /* N such that 2^N >= BK */
        masque = (uint16_t)((1u << n) - 1u);
        base   = (uint16_t)(ar & (uint16_t)~masque);
        idx    = (int)(ar & masque) + step;
    }
    if (idx >= (int)bk) idx -= bk;
    else if (idx < 0)   idx += bk;
    return (uint16_t)(base + idx);
}

/* AR post-modification for the dual-operand and parallel families
 * (SPRU131G Table 5-8):
 *   00 = *ARi (none)   01 = *ARi-   10 = *ARi+   11 = *ARi+0% (circular, BK)
 * Uses c54x_circ_ref, whose base is aligned on 2^N, not on a grid of multiples
 * of BK. */
void c54x_par_postmod(C54xState *s, int ar, int mod)
{
    switch (mod) {
    case 1: s->ar[ar]--; break;
    case 2: s->ar[ar]++; break;
    case 3: s->ar[ar] = c54x_circ_ref(s->ar[ar], +(int16_t)s->ar[0], s->bk); break;
    default: break;
    }
}

/* ================================================================
 * Bit-reversed (reverse carry) addressing
 *     mode 7 = *ARn+0B      mode 4 = *ARn-0B
 *
 * These two modes used to be treated as a flat +/-AR0. The probe that shows they
 * matter: PDROM 0xf1b3 `mar *AR2+0B` (smem=0xBA -> mod=7, AR2), in the FB/SB
 * correlator region, right after the correlation loop (0xf16c..0xf17e) and the
 * 512-word `norm` pass (0xf195..0xf19b) - the bit reversal of an FFT.
 *
 * An FFT whose reverse carry is ignored yields plausible magnitudes and phase but
 * a wrong BIN ORDER: the angle converges (measured: -6 Hz, stable) while the peak
 * POSITION is wrong or pinned to the window edge (measured: TOA=39, the r39 edge,
 * or aberrant).
 *
 * Semantics (SPRU131G): the carry propagates towards the LOW-order bits instead
 * of the high ones. Exact loop-free equivalent: reverse the bits of both
 * operands, add normally, reverse the result.
 *
 * Warning: GLOBAL effect - these modes serve every FFT/IFFT in the firmware.
 * ================================================================ */
static inline uint16_t c54x_bitrev16(uint16_t v)
{
    v = (uint16_t)(((v & 0xAAAAu) >> 1) | ((v & 0x5555u) << 1));
    v = (uint16_t)(((v & 0xCCCCu) >> 2) | ((v & 0x3333u) << 2));
    v = (uint16_t)(((v & 0xF0F0u) >> 4) | ((v & 0x0F0Fu) << 4));
    v = (uint16_t)(((v & 0xFF00u) >> 8) | ((v & 0x00FFu) << 8));
    return v;
}

uint16_t c54x_revcarry(C54xState *s, uint16_t ar, uint16_t ar0, int sub)
{
    uint16_t plat = (uint16_t)(sub ? (ar - ar0) : (ar + ar0));
    uint16_t r;
    r = c54x_bitrev16(ar);
    r = (uint16_t)(sub ? (r - c54x_bitrev16(ar0)) : (r + c54x_bitrev16(ar0)));
    r = c54x_bitrev16(r);
    {   /* Probe: proves the mode is actually exercised and shows the gap with
         * the flat computation. Capped - never derive a rate from it. */
        static unsigned n = 0;
        if (n < 24) {
            n++;
            fprintf(stderr, "[c54x] BITREV #%u PC=0x%04x AR=0x%04x %c AR0=0x%04x "
                    "-> 0x%04x (plat: 0x%04x) insn=%u\n",
                    n, s->pc, ar, sub ? '-' : '+', ar0, r, plat, s->insn_count);
        }
    }
    return r;
}

4.46 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/c54x_probes.c

61043 octets, 1422 lignes → 1420 lignes (1 groupes compactés)

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * c54x_probes.c - probes, traces and instrumentation (diagnostics only)
 *
 * Split out of calypso_c54x.c on 2026-09-18 (per-role file split).
 * File map in c54x_internal.h.
 */
#include "c54x_internal.h"
#include "hw/arm/calypso/calypso_debug.h"

/* ================================================================
 * Memory access
 * ================================================================ */

/* Forward decl: used by data_write() VECDUMP at MMR_PMST. */
uint16_t prog_read(C54xState *s, uint32_t addr);
uint16_t (prog_fetch)(C54xState *s, uint16_t pc);   /* parenthesised: prog_fetch is also a macro (c54x_internal.h) */

/* Propagated by D_BURST_D probe, consumed by A_CD-BY-BURST correlation. */
uint16_t g_last_d_burst_d;

/* === Generic watch-write zone helper ===
 *
 * One monitored memory range: per-PC counter, throttled log, periodic summary.
 * Callers in c54x_mem.c: COEFFS, A_CD, BLK-SRC, DISP-TBL, TRAMPO.
 *
 * Cost: 512 KB of static storage per zone (per_pc[0x10000] * uint64_t). */

bool watch_write_zone_check(C54xState *s, uint16_t addr, uint16_t val,
                                   const char *name,
                                   uint16_t lo, uint16_t hi,
                                   WatchWriteState *st)
{
    if (addr < lo || addr > hi) return false;
    uint16_t exec_pc = s->last_exec_pc;
    st->per_pc[exec_pc]++;
    st->total++;
    bool should_log =
        st->total <= 500
        || exec_pc != st->last_exec_pc
        || (s->insn_count - st->last_log_insn) > 100000;
    if (should_log) {
        const char *wk_name[] = {
            "UNK", "F3", "8x", "77", "76", "PSHM",
            "RET", "IRQ_ACK", "ARM_MMIO", "RES_AR", "OTHER"
        };
        uint8_t wk = s->writer_kind;
        const char *wkn = (wk < sizeof(wk_name)/sizeof(wk_name[0]))
                          ? wk_name[wk] : "??";
        fprintf(stderr,
                "[c54x] %s-WR #%llu addr=0x%04x val=0x%04x "
                "exec_pc=0x%04x cur_pc=0x%04x cur_op=0x%04x wk=%s "
                "AR3=%04x AR4=%04x AR5=%04x insn=%u\n",
                name, (unsigned long long)st->total, addr, val,
                exec_pc, s->pc, s->prog[s->pc], wkn,
                s->ar[3], s->ar[4], s->ar[5], s->insn_count);
        st->last_exec_pc = exec_pc;
        st->last_log_insn = s->insn_count;
    }
    if (s->insn_count - st->last_summary_insn >= 5000000) {
        st->last_summary_insn = s->insn_count;
        /* Keep the -WR- infix: `<NAME>-WR-SUMMARY` must stay consistent with
         * the per-hit `<NAME>-WR #N` lines and with the regex tests. */
        fprintf(stderr, "[c54x] %s-WR-SUMMARY insn=%u total=%llu",
                name, s->insn_count, (unsigned long long)st->total);
        for (int p = 0; p < 0x10000; p++) {
            if (st->per_pc[p]) {
                fprintf(stderr, " pc[0x%04x]=%llu",
                        p, (unsigned long long)st->per_pc[p]);
            }
        }
        fprintf(stderr, "\n");
    }
    return true;
}

/* === FB-det timing/content stats ===
 *
 * Filled by c54x_mem.c over the ~928 fires of PC 0x8f51:
 *   - AR4 inside/outside [0x2bc0..0x2bff] -> addressing vs timing
 *   - insn delta since the last write, per cluster (compute/clear/pattern)
 *   - val[AR4] histogram: zero / 0xfffe sentinel / other
 *
 * Reading the counters:
 *   ar4_in_zone < 100%            addressing bug, AR4 points outside the zone
 *   delta_clear < delta_compute   timing race, the clear wins
 *   delta_compute >> 1M           compute never lands in the fire window
 *   other > 0                     some sweeps do see data
 */
struct c54x_g_fb_det_timing_s g_fb_det_timing;

/* === Generic ARn write tracer with provenance (AR0..AR7) ===
 * Gate: AR-TRACE. Env `CALYPSO_AR_TRACE` is a hex bitmask of the AR indices
 * to trace, default 0xFF (all); 0x14 = AR2 + AR4, 0x04 = AR2 only.
 *
 * Hooked from `case MMR_AR0..AR7` in data_write_locked, so it observes MMR
 * loads only, never auto-modify; deltas in [-3, 3] are skipped as increment
 * noise. A write of 0 is flagged separately (suspect MMR clobber via
 * STL A,*AR-).
 *
 * Opcode classification names where the value comes from:
 *   STM-#lk    ROM immediate, a deliberate firmware update
 *   MVDM-mem   load from memory
 *   MVMM       copy of another AR
 *   STM Smem   indirect memory load
 *   STLM-A     from accumulator A */
ArEntry  g_ar_hist[8][AR_HIST_MAX];
unsigned g_ar_used[8]    = {0};
unsigned g_ar_total[8]   = {0};
unsigned g_ar_mask       = 0;
int      g_ar_enabled    = -1;
unsigned g_ar_log_cap    = 50;

void ar_write_track(C54xState *s, unsigned idx, uint16_t new_val)
{
    /* AR3-PRELOAD: read-only, always on, capped at 120 lines. Logs AR3 loads
     * landing in or around the I/Q buffer [0x2a00..0x2c00), whose end is
     * 0x2b28. Only MMR loads (STM/STLM/MVDM) reach ar_write_track, never
     * auto-increment, so an OUT-OF-BUF load (>= 0x2b28) points at the loading
     * instruction, while the absence of one means an out-of-range AR3 comes
     * from increments instead (loop too long / buffer too short). */
    if (idx == 3 && new_val >= 0x2a00 && new_val < 0x2c00) {
        static unsigned ar3p_n = 0;
        if (ar3p_n < 120) {
            uint16_t op = prog_fetch(s, s->pc);
            fprintf(stderr, "[c54x] AR3-PRELOAD PC=0x%04x op=0x%04x AR3 %04x->%04x "
                    "%s insn=%u\n", s->pc, op, s->ar[3], new_val,
                    new_val >= 0x2b28 ? "*** OUT-OF-BUF ***" : "(in-buf)",
                    s->insn_count);
            ar3p_n++;
        }
    }
    if (g_ar_enabled < 0) {
        const char *e = calypso_getenv("CALYPSO_AR_TRACE");
        g_ar_mask = (e && *e) ? (unsigned)strtoul(e, NULL, 0) : 0xFFu;
        g_ar_enabled = calypso_debug_enabled("AR-TRACE") ? 1 : 0;
        if (g_ar_enabled) {
            fprintf(stderr,
                "[c54x] AR-TRACE enabled, mask=0x%02x (AR0..AR7), "
                "log_cap=%u hist_max=%u\n",
                g_ar_mask, g_ar_log_cap, AR_HIST_MAX);
        }
    }
    if (g_ar_enabled <= 0) return;
    if (idx >= 8) return;
    if (!(g_ar_mask & (1u << idx))) return;

    uint16_t old_val = s->ar[idx];
    int32_t delta = (int32_t)new_val - (int32_t)old_val;
    if (delta >= -3 && delta <= 3) return;  /* skip auto-modify noise */
    g_ar_total[idx]++;

    uint16_t op = prog_fetch(s, s->pc);
    const char *kind = "MISC";
    if ((op & 0xFF80) == 0x7700) kind = "STM-#lk";
    else if ((op & 0xFF00) == 0x8400) kind = "STLM-A";
    else if ((op & 0xFF00) == 0x8600) kind = "MVDM-mem";
    else if ((op & 0xFF00) == 0x8800) kind = "MVMM";
    else if ((op & 0xF800) == 0x8000) kind = "STL-A";

    unsigned i;
    for (i = 0; i < g_ar_used[idx]; i++) {
        if (g_ar_hist[idx][i].pc == s->pc) {
            g_ar_hist[idx][i].count++;
            g_ar_hist[idx][i].val_last = new_val;
            g_ar_hist[idx][i].op_last = op;
            break;
        }
    }
    if (i == g_ar_used[idx] && g_ar_used[idx] < AR_HIST_MAX) {
        g_ar_hist[idx][i].pc = s->pc;
        g_ar_hist[idx][i].op_last = op;
        g_ar_hist[idx][i].val_last = new_val;
        g_ar_hist[idx][i].count = 1;
        g_ar_used[idx]++;
    }
    if (g_ar_total[idx] <= g_ar_log_cap) {
        fprintf(stderr,
            "[c54x] AR%u-W #%u %s @insn=%u PC=0x%04x op=0x%04x  "
            "AR%u %04x → %04x (Δ=%+d)  A=%010llx SP=%04x\n",
            idx, g_ar_total[idx], kind, s->insn_count, s->pc, op,
            idx, old_val, new_val, delta,
            (unsigned long long)(s->a & 0xFFFFFFFFFFULL), s->sp);
    }
    /* Semantic distinction, kept explicit in the log:
     * - STM-#lk / LD-#k  deliberate AR update from a ROM immediate, the AR
     *                    changes by firmware design
     * - anything else    side effect of an MMR write where the AR happens to
     *                    point at its own MMR slot (self-alias). A coincidence,
     *                    not an explicit AR update. */
    if (new_val == 0) {
        int deliberate = ((op & 0xFF80) == 0x7700);  /* STM-#lk only */
        fprintf(stderr,
            "[c54x] AR%u-W ZERO %s @insn=%u PC=0x%04x op=0x%04x AR%u←0 "
            "(kind=%s)\n",
            idx,
            deliberate ? "DELIBERATE" : "SIDE-EFFECT",
            s->insn_count, s->pc, op, idx, kind);
    }
}

/* === A accumulator provenance tracer ===
 * Records A's last writer at the top-of-loop chokepoint: A is compared with
 * the previous iteration and, on a change, the writer's PC and opcode are
 * kept. When the trigger PC fires, A and its last writer are dumped, which
 * tells a deliberate A=0 (mask-all by design) from a diverged one.
 *
 * Gate: A-TRACE. Env CALYPSO_A_TRACE_PC sets the trigger PC (hex, 0 when the
 * gate is on but the variable is unset). Zero cost when off. */
int64_t  g_a_last_value      = 0;
uint16_t g_a_last_writer_pc  = 0;
uint16_t g_a_last_writer_op  = 0;
unsigned g_a_last_writer_insn = 0;
int      g_a_trace_enabled   = -1;
uint16_t g_a_trace_pc        = 0xFFFF;
unsigned g_a_trace_hits      = 0;
unsigned g_a_trace_log_cap   = 50;

void a_track_init_lazy(void)
{
    if (g_a_trace_enabled >= 0) return;
    const char *e = calypso_getenv("CALYPSO_A_TRACE_PC");
    if (calypso_debug_enabled("A-TRACE")) {
        g_a_trace_pc = (e && *e) ? (uint16_t)strtoul(e, NULL, 0) : 0;
        g_a_trace_enabled = 1;
        fprintf(stderr,
            "[c54x] A-TRACE enabled, trigger PC=0x%04x log_cap=%u\n",
            g_a_trace_pc, g_a_trace_log_cap);
    } else {
        g_a_trace_enabled = 0;
    }
}

void a_track_iter(C54xState *s, uint16_t prev_pc, uint16_t prev_op)
{
    if (g_a_trace_enabled <= 0) return;
    /* A changed: the instruction that just ran is its writer. */
    if (s->a != g_a_last_value) {
        g_a_last_writer_pc   = prev_pc;
        g_a_last_writer_op   = prev_op;
        g_a_last_writer_insn = s->insn_count;
        g_a_last_value       = s->a;
    }
    /* Trigger : PC about to execute matches target */
    if (s->pc == g_a_trace_pc) {
        g_a_trace_hits++;
        int a_zero = ((s->a & 0xFFFF) == 0);
        /* Log the first N hits for context, plus every A_low=0 hit whatever
         * the count: a silent cap hides late critical events, as it did for
         * the IMR clobber at insn=253328. */
        if (g_a_trace_hits <= g_a_trace_log_cap || a_zero) {
            fprintf(stderr,
                "[c54x] A-AT-PC #%u @insn=%u PC=0x%04x  A=%010llx (low=0x%04x, %s) "
                "last_writer: PC=0x%04x op=0x%04x @insn=%u\n",
                g_a_trace_hits, s->insn_count, s->pc,
                (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                (unsigned)(s->a & 0xFFFF),
                a_zero ? "A_low=0 → STL clobber zone" : "A_low≠0",
                g_a_last_writer_pc, g_a_last_writer_op,
                g_a_last_writer_insn);
        }
    }
}

/* === AR6 windowed snapshot at a trigger PC ===
 * Captures AR6, B and AR6's last writer on each fire of the trigger PC,
 * restricted to [insn_lo, insn_hi] because such a PC fires 10M+ times
 * (0x821a does). At a fire, AR6=0 means the buffer base diverged and the
 * write lands on IMR, AR6=0x16 means AR6 aliases its own MMR slot.
 *
 * The last writer is tracked by top-of-loop comparison, like the A tracer.
 *
 * Gate: AR6-AT. Env:
 *   CALYPSO_AR6_AT_PC=0x821a    trigger PC
 *   CALYPSO_AR6_WIN_LO=3619500  insn window start
 *   CALYPSO_AR6_WIN_HI=3619810  insn window end (one outer-loop iteration)
 *   CALYPSO_AR6_AT_LOG_CAP=200  max log lines (default 200)
 */
uint16_t g_ar6_last_value     = 0;
uint16_t g_ar6_last_writer_pc = 0;
uint16_t g_ar6_last_writer_op = 0;
unsigned g_ar6_last_writer_insn = 0;
int      g_ar6_at_enabled     = -1;
uint16_t g_ar6_at_pc          = 0xFFFF;
unsigned g_ar6_at_win_lo      = 0;
unsigned g_ar6_at_win_hi      = 0;
unsigned g_ar6_at_hits        = 0;
unsigned g_ar6_at_log_cap     = 200;

void ar6_at_init_lazy(void)
{
    if (g_ar6_at_enabled >= 0) return;
    const char *e = calypso_getenv("CALYPSO_AR6_AT_PC");
    if (calypso_debug_enabled("AR6-AT")) {
        g_ar6_at_pc = (e && *e) ? (uint16_t)strtoul(e, NULL, 0) : 0;
        g_ar6_at_enabled = 1;
        const char *lo = calypso_getenv("CALYPSO_AR6_WIN_LO");
        const char *hi = calypso_getenv("CALYPSO_AR6_WIN_HI");
        const char *cap = calypso_getenv("CALYPSO_AR6_AT_LOG_CAP");
        g_ar6_at_win_lo = (lo && *lo) ? (unsigned)strtoul(lo, NULL, 0) : 0;
        g_ar6_at_win_hi = (hi && *hi) ? (unsigned)strtoul(hi, NULL, 0) : 0xFFFFFFFFu;
        g_ar6_at_log_cap = (cap && *cap) ? (unsigned)strtoul(cap, NULL, 0) : 200;
        fprintf(stderr,
            "[c54x] AR6-AT-PC enabled, trigger PC=0x%04x window=[%u..%u] cap=%u\n",
            g_ar6_at_pc, g_ar6_at_win_lo, g_ar6_at_win_hi, g_ar6_at_log_cap);
    } else {
        g_ar6_at_enabled = 0;
    }
}

void ar6_at_iter(C54xState *s, uint16_t prev_pc, uint16_t prev_op)
{
    if (g_ar6_at_enabled <= 0) return;
    /* Track AR6 last writer */
    if (s->ar[6] != g_ar6_last_value) {
        g_ar6_last_writer_pc   = prev_pc;
        g_ar6_last_writer_op   = prev_op;
        g_ar6_last_writer_insn = s->insn_count;
        g_ar6_last_value       = s->ar[6];
    }
    /* Trigger: PC about to execute matches AND we are inside the window. */
    if (s->pc == g_ar6_at_pc &&
        s->insn_count >= g_ar6_at_win_lo &&
        s->insn_count <= g_ar6_at_win_hi) {
        g_ar6_at_hits++;
        if (g_ar6_at_hits <= g_ar6_at_log_cap) {
            uint16_t ar6 = s->ar[6];
            const char *regime;
            if (ar6 == 0)              regime = "AR6=0 → addr=IMR (BUFFER BASE DIVERGENCE)";
            else if (ar6 == 0x16)      regime = "AR6=0x16 → addr=MMR_AR6 (SELF-ALIAS)";
            else if (ar6 < 0x20)       regime = "AR6 in MMR zone";
            else                        regime = "AR6 normal";
            fprintf(stderr,
                "[c54x] AR6-AT-PC #%u @insn=%u PC=0x%04x  AR6=0x%04x (%s) "
                "B=%010llx (high=0x%04x)  last_writer: PC=0x%04x op=0x%04x @insn=%u\n",
                g_ar6_at_hits, s->insn_count, s->pc,
                ar6, regime,
                (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                (unsigned)((s->b >> 16) & 0xFFFF),
                g_ar6_last_writer_pc, g_ar6_last_writer_op,
                g_ar6_last_writer_insn);
        }
    }
}


/* RSBX INTM hit counter (cheap probe). */
uint64_t g_rsbx_intm_hits = 0;
int      g_rsbx_intm_enabled = -1;

/* DISP-ENTRY discriminator: captures the interrupt-preemption context. The
 * C54x does not push ST0/DP on interrupt entry, so the ISR inherits the DP of
 * the code it preempted. Bad dispatcher entries (DP != 0x124) correlating with
 * a recent interrupt therefore mean preemption, not a clobbered DP. */
uint64_t g_last_intr_insn  = 0;      /* insn_count of the last interrupt serviced */;
int      g_last_intr_vec   = -1;     /* vector of the last interrupt */;
uint16_t g_last_intr_fg_pc = 0;      /* preempted foreground PC */;
uint16_t g_last_intr_fg_dp = 0;      /* preempted foreground DP */;
/* Last LDP (the instruction that set DP) and the predecessor PC. */
uint16_t g_last_ldp_pc  = 0;         /* PC of the instruction that set DP */;
uint16_t g_last_ldp_val = 0;         /* DP value written */;
int      g_last_ldp_kind = 0;        /* 1=LDP #k  2=LDP #k9  3=LD Smem,DP */;
uint16_t g_prev_pc = 0;  /* PC of the instruction executed just before */
uint16_t g_prev2_op = 0;
uint16_t g_prev_op = 0;  /* its opcode; makes TC reliable for BC after CMPM/BITF */;
uint16_t g_last_st0w_pc  = 0;        /* PC of the last full ST0 write (POPM ST0 / STLM) */;
uint16_t g_last_st0w_val = 0;        /* ST0 value restored */;
uint16_t g_last_st0w_op  = 0;        /* opcode of the instruction writing ST0 */;
uint16_t g_last_st0w_xpc = 0;        /* XPC at the write (PC 0xf48b is XPC-dependent) */;
uint16_t g_last_st0w_prev = 0;       /* predecessor PC of the write */;

/* === ST0 push/pop ring (gate: DISP-ENTRY) ===============================
 * Records PSHM ST0 (push) and POPM/STLM ST0 (write) in a ring, dumped on a bad
 * dispatcher entry (LUT read != 0xff72). It splits a stale DP three ways:
 *   - last PUSH val=0x3124 but POP=0x3125: the stack slot was clobbered
 *     between push and pop (SP / circular-addressing family)
 *   - last PUSH val=0x3125: DP was already wrong at push time (LDP skipped
 *     upstream)
 *   - no PUSH ST0 paired with the POP: SP misalignment, the pop reads another
 *     slot */
St0Ev    g_st0_ring[ST0_RING_N];
unsigned g_st0_ring_idx = 0;
int      g_st0_ring_on  = -1;
void st0_ring_rec(C54xState *s, uint16_t val, char kind)
{
    if (g_st0_ring_on < 0) g_st0_ring_on = calypso_debug_enabled("DISP-ENTRY") ? 1 : 0;
    if (!g_st0_ring_on) return;
    St0Ev *e = &g_st0_ring[g_st0_ring_idx % ST0_RING_N];
    e->pc = s->pc; e->op = prog_fetch(s, s->pc); e->val = val; e->sp = s->sp; e->kind = kind;
    g_st0_ring_idx++;
}
/* LUT slot read at dispatcher entry 0x834d. Captured silently on purpose:
 * DISP-TRACE's logging shifts the timing and hides the bug it looks for. */
uint16_t g_disp_lut_ea  = 0;
uint16_t g_disp_lut_val = 0;
/* Ring of SP events (push/pop), fed from the single chokepoint of the run
 * loop: every SP change records {pc, op, delta}. Dumped by BLACKHOLE-CALA to
 * name the recurring drain source, a push that is never popped. Array writes
 * only, so the cost is negligible. */
struct sp_evt g_spring[64];
uint32_t g_spring_idx = 0;

/* === SHADOW STACK (push/pop pairing) ===
 * Logical mirror of the DSP stack: every PUSH (CALL/CALLD/PSHM/IRQ, SP-)
 * pushes {pc,op,kind}, every POP (RET/RETD/RETE/FRET/RETED/POPM, SP+) pops and
 * checks the pairing. A POP against an empty shadow is a return with no
 * matching call: the over-pop source, reading pristine stack above SP_base.
 * It names that orphan return (PC/op/SP) instead of its victims.
 * Gate: env CALYPSO_ORPHAN, deliberately outside CALYPSO_DEBUG so the master
 * switch stays off (anti-Heisenbug). kind: 'C'=call 'P'=pshm/pshd 'I'=irq
 * 'R'=reti. Array writes only when off. */
struct shadow_ent g_shadow[SHADOW_N];
int  g_shadow_depth = 0;     /* words currently pushed (logical) */;
int  g_shadow_on   = -1;     /* -1 = gate not resolved yet */;
uint64_t g_orphan_hits = 0;  /* orphan POPs detected */;
uint64_t g_mismatch_hits = 0;/* POPs whose kind does not match */;

/* Tracks direct stores into the stack zone [0x1100..0x1140], ABOVE SP_base.
 * A push never writes these slots (the stack grows below 0x1100), so only a
 * direct ST does. Tells a legitimate vector init (slot written by firmware)
 * from a pristine slot (never written = genuine over-pop garbage). */
uint16_t g_stkslot_wpc[STKSLOT_N];
uint16_t g_stkslot_wop[STKSLOT_N];
uint8_t  g_stkslot_written[STKSLOT_N];

void rsbx_intm_check(C54xState *s, uint16_t op)
{
    if (g_rsbx_intm_enabled < 0) {
        const char *e = cdbg_env("RSBX-INTM");
        g_rsbx_intm_enabled = (e && *e == '1') ? 1 : 0;
        if (g_rsbx_intm_enabled) {
            fprintf(stderr, "[c54x] RSBX-INTM-TRACE enabled (op=0xF6BB)\n");
        }
    }
    if (g_rsbx_intm_enabled <= 0) return;
    if (op == 0xF6BB) {
        g_rsbx_intm_hits++;
        if (g_rsbx_intm_hits <= 20 || (g_rsbx_intm_hits % 1000) == 0) {
            fprintf(stderr,
                "[c54x] RSBX-INTM #%llu @insn=%u PC=0x%04x  ST1 INTM 0x%04x → "
                "(cleared) — IRQ enable path atteint !\n",
                (unsigned long long)g_rsbx_intm_hits, s->insn_count, s->pc,
                s->st1);
        }
    }
}

/* === SP absolute-write tracer ===
 * Logs every SP write that *teleports* SP to an arbitrary value - STL/STM/STLM
 * absolute, FRAME #imm, MVMM register transfer - as opposed to PUSH/POP/
 * CALL/RET, which move SP by one. A site that teleports SP=0x3fbe is the exact
 * corrupter of the bootstub entry observed at insn=3995013.
 *
 * Hooked at the three sites that can do it:
 *   site 0  data_write_locked case MMR_SP (STL/STM/STLM to MMR_SP)
 *   site 1  F7Dx case 0xD, LD #k8u, SP
 *   site 2  MVMM register transfer with dst==8 (SP in the 3-bit MMR encoding)
 *
 * Gate: CALYPSO_DEBUG token SP-ABS, zero cost when off. First N writes are
 * logged verbatim, the rest go to a per-PC histogram (cap SP_ABS_HIST_MAX). */
SpAbsEntry g_sp_abs_hist[SP_ABS_HIST_MAX];
unsigned   g_sp_abs_used     = 0;
unsigned   g_sp_abs_total    = 0;
int        g_sp_abs_enabled  = -1;
unsigned   g_sp_abs_log_cap  = 50;

void sp_abs_track(C54xState *s, uint16_t new_val, uint8_t site)
{
    if (g_sp_abs_enabled < 0) {
        const char *e = cdbg_env("SP-ABS");
        g_sp_abs_enabled = (e && *e == '1') ? 1 : 0;
        if (g_sp_abs_enabled) {
            fprintf(stderr, "[c54x] SP-ABS-TRACE enabled, log_cap=%u hist_max=%u\n",
                    g_sp_abs_log_cap, SP_ABS_HIST_MAX);
        }
    }
    if (g_sp_abs_enabled <= 0) return;
    g_sp_abs_total++;
    /* Per-PC histogram */
    unsigned i;
    for (i = 0; i < g_sp_abs_used; i++) {
        if (g_sp_abs_hist[i].pc == s->pc && g_sp_abs_hist[i].site == site) {
            g_sp_abs_hist[i].count++;
            g_sp_abs_hist[i].value_last = new_val;
            break;
        }
    }
    if (i == g_sp_abs_used && g_sp_abs_used < SP_ABS_HIST_MAX) {
        g_sp_abs_hist[i].pc         = s->pc;
        g_sp_abs_hist[i].value_last = new_val;
        g_sp_abs_hist[i].count      = 1;
        g_sp_abs_hist[i].site       = site;
        g_sp_abs_used++;
    }
    /* Verbatim log first N */
    if (g_sp_abs_total <= g_sp_abs_log_cap) {
        const char *site_name = site == 0 ? "MMR_SP-W" :
                                site == 1 ? "LD-#k8-SP" : "MVMM-SP";
        int32_t delta = (int32_t)new_val - (int32_t)s->sp;
        fprintf(stderr,
            "[c54x] SP-ABS #%u %s @insn=%u PC=0x%04x SP %04x → %04x (Δ=%+d) "
            "A=%010llx AR4=%04x\n",
            g_sp_abs_total, site_name, s->insn_count, s->pc,
            s->sp, new_val, delta,
            (unsigned long long)(s->a & 0xFFFFFFFFFFULL), s->ar[4]);
    }
    /* Flag an SP landing in a suspect zone: 0x3fb0..0x3fbf is the BSP read
     * region, 0x2b80..0x2c00 the I/Q buffer tail. */
    if ((new_val >= 0x3fb0 && new_val <= 0x3fbf) ||
        (new_val >= 0x2b80 && new_val <= 0x2c00)) {
        fprintf(stderr,
            "[c54x] SP-ABS SUSPECT! @insn=%u PC=0x%04x SP←0x%04x (corrupter ?)\n",
            s->insn_count, s->pc, new_val);
    }
}

/* === MVPD overlay occupancy trace ===
 * Buckets writes to data[0x0080..0x27FF] in 0x80-word buckets and dumps the
 * occupancy at the end of the boot phase. It shows which sub-ranges MVPD
 * loads at boot (= overlay code), which decides whether the BSP buffer can
 * live in the read region [0x0000..0x03A3] without overwriting code that is
 * executing.
 * Gates:
 *   CALYPSO_DEBUG token MVPD   enables the trace (default off)
 *   CALYPSO_MVPD_BOOT_LIMIT=N  insn cap for the dump (default 500000) */
uint32_t g_mvpd_buckets[MVPD_BUCKETS_N];  /* 80 buckets */;
int      g_mvpd_trace_enabled = -1;
unsigned g_mvpd_boot_limit    = 0;
int      g_mvpd_dumped        = 0;

void mvpd_trace_init_lazy(void)
{
    if (g_mvpd_trace_enabled >= 0) return;
    const char *e = cdbg_env("MVPD");
    g_mvpd_trace_enabled = (e && *e == '1') ? 1 : 0;
    const char *l = calypso_getenv("CALYPSO_MVPD_BOOT_LIMIT");
    g_mvpd_boot_limit = (l && *l) ? (unsigned)strtoul(l, NULL, 0) : 500000u;
    if (g_mvpd_trace_enabled) {
        fprintf(stderr,
            "[c54x] MVPD-TRACE enabled, range=[0x%04x..0x%04x] bucket_sz=%u "
            "buckets=%u boot_limit=%u\n",
            MVPD_RANGE_LO, MVPD_RANGE_HI, MVPD_BUCKET_SZ, MVPD_BUCKETS_N,
            g_mvpd_boot_limit);
    }
}

void mvpd_trace_record(uint16_t addr)
{
    if (g_mvpd_trace_enabled <= 0) return;
    if (addr < MVPD_RANGE_LO || addr >= MVPD_RANGE_HI) return;
    unsigned b = (addr - MVPD_RANGE_LO) >> MVPD_BUCKET_BITS;
    if (b < MVPD_BUCKETS_N) g_mvpd_buckets[b]++;
}

void mvpd_trace_dump_if_due(unsigned insn)
{
    if (g_mvpd_trace_enabled <= 0) return;
    if (g_mvpd_dumped) return;
    if (insn < g_mvpd_boot_limit) return;
    g_mvpd_dumped = 1;
    fprintf(stderr, "[c54x] MVPD-OCCUPANCY DUMP @insn=%u (boot phase end)\n", insn);
    for (unsigned b = 0; b < MVPD_BUCKETS_N; b++) {
        if (g_mvpd_buckets[b] == 0) continue;
        uint16_t lo = MVPD_RANGE_LO + b * MVPD_BUCKET_SZ;
        uint16_t hi = lo + MVPD_BUCKET_SZ - 1;
        fprintf(stderr,
            "[c54x] MVPD-BUCKET [0x%04x..0x%04x] writes=%u\n",
            lo, hi, g_mvpd_buckets[b]);
    }
    /* Buffer placement verdict: few or no writes in [0x0080..0x03A3]
     * (correlator read region, buckets 0..6) means the range is safe for BSP
     * DMA; otherwise the buffer needs another home. */
    unsigned bucket_0_to_6_total = 0;
    for (unsigned b = 0; b < 7 && b < MVPD_BUCKETS_N; b++)
        bucket_0_to_6_total += g_mvpd_buckets[b];
    fprintf(stderr,
        "[c54x] MVPD-VERDICT correlator_read_region [0x0080..0x03A3] "
        "writes=%u → %s\n",
        bucket_0_to_6_total,
        bucket_0_to_6_total == 0
            ? "EMPTY (safe pour BSP buffer placement ici)"
            : bucket_0_to_6_total < 100
            ? "lightly used (probably safe, audit specifics)"
            : "HEAVILY USED (code overlay, NE PAS placer BSP buffer ici)");
}

/* === Correlator trace ===
 * Captures AR0..AR7, SP and ST0/ST1 on entry into the FB-det correlator, and
 * the data reads performed while inside it, to check empirically that the
 * firmware reads its I/Q input from [0x0000..0x03A3].
 * Gate: CALYPSO_DEBUG token CORRELATOR, zero cost when off.
 *
 * Correlator range: [0x8d00..0x9000) (FB-det handler in PROM0). The upper
 * bound was raised from 0x8F80 to 0x9000 because d_fb_det WATCH-READ showed
 * reads at PC=0x8FAC and 0x8FB5 falling outside the old filter, which made
 * CORR-ENTRY report 0 while the firmware was in fact working in the zone.
 *
 * Reads are kept as a capped list of unique addresses so long runs do not
 * flood the log. */
CorrReadEntry g_corr_read_hist[CORR_READ_HIST_MAX];
unsigned    g_corr_read_used    = 0;
int         g_corr_trace_enabled = -1; /* -1 uninit, 0 off, 1 on */;
unsigned    g_corr_entry_count  = 0;
unsigned    g_corr_entry_log_cap = 100000;  /* uncap: see the per-frame post-+3s */;

/* Set by calypso_trx.c when the ARM writes d_task_md=5 (FB command). The
 * D_TASK_MD-RD probe timestamps the DSP reads against this write, comparing
 * the ARM write EA with the DSP read EA and their order. */
uint32_t g_arm_taskmd5_insn = 0;
uint16_t g_arm_taskmd5_ea   = 0;
uint64_t    g_corr_read_total   = 0;
uint16_t    g_corr_last_pc      = 0xFFFF; /* track PC transitions */;

void corr_trace_init_lazy(void)
{
    if (g_corr_trace_enabled >= 0) return;
    const char *e = cdbg_env("CORRELATOR");
    g_corr_trace_enabled = (e && *e == '1') ? 1 : 0;
    if (g_corr_trace_enabled) {
        fprintf(stderr, "[c54x] CORRELATOR-TRACE enabled, range=[0x%04x..0x%04x) "
                        "hist_max=%u entry_log_cap=%u\n",
                CORR_PC_LO, CORR_PC_HI,
                CORR_READ_HIST_MAX, g_corr_entry_log_cap);
    }
}

/* CORR-ENTRY tracker: called at the top of the loop for every dispatched
 * instruction. Detects a PC transition from outside into the FB-det range and
 * logs the first N entries with the AR/SP/ST context. */
void corr_entry_track(uint16_t pc, void *s_void)
{
    if (g_corr_trace_enabled <= 0) return;
    bool was_in  = (g_corr_last_pc >= CORR_PC_LO && g_corr_last_pc < CORR_PC_HI);
    bool is_in   = (pc >= CORR_PC_LO && pc < CORR_PC_HI);
    g_corr_last_pc = pc;
    if (!was_in && is_in) {
        g_corr_entry_count++;
        if (g_corr_entry_count <= g_corr_entry_log_cap
            || (g_corr_entry_count % 100) == 0) {
            C54xState *s = (C54xState *)s_void;
            fprintf(stderr,
                "[c54x] CORR-ENTRY #%u @PC=0x%04x from=0x%04x SP=0x%04x "
                "ST0=0x%04x ST1=0x%04x AR=[%04x %04x %04x %04x %04x %04x %04x %04x] "
                "A=%010llx B=%010llx T=%04x\n",
                g_corr_entry_count, pc, g_corr_last_pc, s->sp,
                s->st0, s->st1,
                s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                s->ar[4], s->ar[5], s->ar[6], s->ar[7],
                (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                s->t);
        }
    }
}

/* === FBDB/FBF3 + 0x3DC0 probes ========================================
 *
 * Read-only probes around the fc50-fc6f kernel, three observations:
 *   A) B at PC=0xfbd9, before `SUB #8, B, A`: a wrong B upstream yields a
 *      wrong A downstream whatever the F2xx handler does.
 *   B) A at PC=0xfbdb (just after the SUB) and at PC=0xfbf3 (just before
 *      STLM A,AR4): A_low=0 there gives AR4=0, hence AR5=1=MMR_IFR.
 *   C) Whether bit 4 of the flag at 0x3DC0, tested by the BITF at fc63, is
 *      ever set. If no DSP routine sets it, the BCD NTC at fc66 always
 *      branches and fc50-fc6f loops forever without entering its body.
 *
 * Gate: CALYPSO_DEBUG token FBDB. Cost when off: one compare and one branch
 * per opcode. */
int g_fbdb_probe_enabled = -1;
unsigned g_fbdb_probe_log_cap = 100;
unsigned g_fbdb_probe_count_b = 0;
unsigned g_fbdb_probe_count_a = 0;
unsigned g_fbdb_probe_count_fbf3 = 0;
unsigned g_addr3dc0_wr_count = 0;
unsigned g_addr3dc0_rd_count = 0;

static void fbdb_probe_init_lazy(void)
{
    if (g_fbdb_probe_enabled >= 0) return;
    const char *e = cdbg_env("FBDB");
    g_fbdb_probe_enabled = (e && *e == '1') ? 1 : 0;
    if (g_fbdb_probe_enabled) {
        fprintf(stderr,
            "[c54x] FBDB-PROBE enabled : track B@0xfbd9 + A@0xfbdb + A@0xfbf3 "
            "+ ALL r/w to 0x3DC0 (= SARAM flag polled by fc63 BITF)\n");
    }
}

/* Hook called from c54x_run top-of-loop, before c54x_exec_one. */
void fbdb_probe_check_pc(uint16_t pc, void *s_void)
{
    if (g_fbdb_probe_enabled < 0) fbdb_probe_init_lazy();
    if (g_fbdb_probe_enabled <= 0) return;
    C54xState *s = (C54xState *)s_void;
    if (pc == 0xfbd9 && g_fbdb_probe_count_b < g_fbdb_probe_log_cap) {
        g_fbdb_probe_count_b++;
        int64_t b = s->b & 0xFFFFFFFFFFLL;
        fprintf(stderr,
            "[c54x] FBDB-PROBE B@fbd9 #%u: B=0x%010llx (low=0x%04x sign=%d) "
            "A=0x%010llx AR2=0x%04x AR3=0x%04x AR4=0x%04x insn=%u\n",
            g_fbdb_probe_count_b,
            (unsigned long long)b, (unsigned)(b & 0xFFFF),
            (b & 0x8000000000LL) ? -1 : 1,
            (unsigned long long)(s->a & 0xFFFFFFFFFFLL),
            s->ar[2], s->ar[3], s->ar[4], s->insn_count);
    }
    else if (pc == 0xfbdb && g_fbdb_probe_count_a < g_fbdb_probe_log_cap) {
        g_fbdb_probe_count_a++;
        int64_t a = s->a & 0xFFFFFFFFFFLL;
        fprintf(stderr,
            "[c54x] FBDB-PROBE A@fbdb #%u (= after SUB #8,B,A): A=0x%010llx "
            "(low=0x%04x), TC=%d insn=%u\n",
            g_fbdb_probe_count_a, (unsigned long long)a,
            (unsigned)(a & 0xFFFF),
            !!(s->st0 & (1 << 13)), s->insn_count);  /* TC = ST0 bit 13 per SPRU131G */
    }
    else if (pc == 0xfbf3 && g_fbdb_probe_count_fbf3 < g_fbdb_probe_log_cap) {
        g_fbdb_probe_count_fbf3++;
        int64_t a = s->a & 0xFFFFFFFFFFLL;
        fprintf(stderr,
            "[c54x] FBDB-PROBE A@fbf3 #%u (= just before STLM A,AR4): "
            "A=0x%010llx (low=0x%04x) — if low=0 → AR4=0 → AR5=1=MMR_IFR corruption "
            "insn=%u\n",
            g_fbdb_probe_count_fbf3, (unsigned long long)a,
            (unsigned)(a & 0xFFFF), s->insn_count);
    }
}

/* === STUCK-STATE PC+XPC histogram =====================================
 *
 * While the DSP is stuck (INTM=1 AND BRINT0 pending in IFR), record PC+XPC.
 * The plain PC histogram cannot tell XPC pages apart - a bare "fc50" may be
 * the page 0x1F mirror, or page 0x28, 0x38... - so the XPC qualification is
 * what names the real blocking loop.
 *
 * Stuck entry and exit are logged to delimit the window, and the top 20
 * PC+XPC pairs are dumped periodically while it lasts.
 *
 * Gate: CALYPSO_DEBUG token STUCK. Cost when off: one bit test and one
 * branch. */
StuckHistEntry g_stuck_hist[STUCK_HIST_SIZE];
unsigned g_stuck_hist_used = 0;
int g_stuck_probe_enabled = -1;
int g_stuck_active = 0;
uint32_t g_stuck_duration = 0;
uint64_t g_stuck_start_insn = 0;
unsigned g_stuck_dump_count = 0;

static void stuck_probe_init_lazy(void)
{
    if (g_stuck_probe_enabled >= 0) return;
    const char *e = cdbg_env("STUCK");
    g_stuck_probe_enabled = (e && *e == '1') ? 1 : 0;
    if (g_stuck_probe_enabled) {
        fprintf(stderr,
            "[c54x] STUCK-PROBE enabled : capture PC+XPC histogramme quand "
            "INTM=1 + IFR bit5 (BRINT0) pending\n");
    }
}

static void stuck_probe_record(uint16_t pc, uint8_t xpc)
{
    /* Linear scan small hist (cap 64). Insert or increment. */
    for (unsigned i = 0; i < g_stuck_hist_used; i++) {
        if (g_stuck_hist[i].pc == pc && g_stuck_hist[i].xpc == xpc) {
            g_stuck_hist[i].count++;
            return;
        }
    }
    if (g_stuck_hist_used < STUCK_HIST_SIZE) {
        g_stuck_hist[g_stuck_hist_used].pc = pc;
        g_stuck_hist[g_stuck_hist_used].xpc = xpc;
        g_stuck_hist[g_stuck_hist_used].count = 1;
        g_stuck_hist_used++;
    }
    /* If hist full, silently drop new PCs — top hot ones already captured. */
}

static void stuck_probe_dump(uint64_t cur_insn, const char *trig)
{
    /* Bubble sort by count desc (n<=64). */
    for (unsigned k = 0; k < g_stuck_hist_used; k++) {
        unsigned best = k;
        for (unsigned i = k + 1; i < g_stuck_hist_used; i++) {
            if (g_stuck_hist[i].count > g_stuck_hist[best].count) best = i;
        }
        if (best != k) {
            StuckHistEntry tmp = g_stuck_hist[k];
            g_stuck_hist[k] = g_stuck_hist[best];
            g_stuck_hist[best] = tmp;
        }
    }
    if (calypso_debug_enabled("STUCK-HIST")) fprintf(stderr,
        "[c54x] STUCK-HIST [%s] duration=%u insn since insn=%llu (now=%llu) top:\n",
        trig, g_stuck_duration,
        (unsigned long long)g_stuck_start_insn,
        (unsigned long long)cur_insn);
    unsigned n_show = g_stuck_hist_used > 20 ? 20 : g_stuck_hist_used;
    for (unsigned i = 0; i < n_show; i++) {
        fprintf(stderr,
            "[c54x]   #%2u PC=0x%04x XPC=%u  count=%u\n",
            i + 1, g_stuck_hist[i].pc, g_stuck_hist[i].xpc,
            g_stuck_hist[i].count);
    }
}

/* === FORCE-INTM-ONESHOT ================================================
 *
 * Arbitration probe: when INTM=1 AND BRINT0 (IFR bit 5) is pending, clear
 * INTM ONCE to let the dispatch happen, then watch the existing tracers
 * (CORR-ENTRY, a_sync_demod writes, RETE log, INTM-TRANS). Real snr/toa after
 * the dispatch means INTM was the only blocker; garbage or nothing means the
 * downstream path is broken too, or the ISR state is corrupt.
 *
 * This is a probe, NOT a fix: the one-shot observes without masking steady
 * behaviour. If INTM turns out to be the only blocker, the fix belongs in the
 * ISR (why no RETE), not in a systematic INTM clear.
 *
 * Env: CALYPSO_FORCE_INTM_ONESHOT=1. */
int g_force_intm_oneshot_enabled = -1;
int g_force_intm_oneshot_done = 0;
uint64_t g_force_intm_oneshot_insn = 0;
/* CALYPSO_FORCE_INTM_AT_PC=0xfc6f restricts the force to that PC, so it can
 * fire at a safe point (the RET of the fc50 compute kernel, say) instead of
 * mid-compute. The 0xFFFF sentinel means no PC restriction: fire at the first
 * opportunity. */
uint16_t g_force_intm_at_pc = 0xFFFF;

/* @BEQUILLE - FORCE_INTM_ONESHOT (+ FORCE_INTM_AT_PC)  (CALYPSO_FORCE_INTM_ONESHOT=1,
 *              CALYPSO_FORCE_INTM_AT_PC=0xXXXX ; default OFF ; calypso_wire.env:=1)
 *   masks   : the RSBX INTM at 0xa51b that the firmware never executes. With the
 *             PC gate the block ALSO raises the interrupt (s->ifr |= s->imr &
 *             0x3000): it manufactures the event, it does not merely open the
 *             window.
 *   remove  : once INTM reaches 0 through the ROM path (see the INTM-TRANS trace).
 *   NB      : run.sh already reports it as "NON-nominal".
 */
void force_intm_oneshot_check(C54xState *s)
{
    if (g_force_intm_oneshot_enabled < 0) {
        const char *e = calypso_getenv("CALYPSO_FORCE_INTM_ONESHOT");
        /* Strict gate: ON only for =1; =0 or unset means OFF. The one-shot
         * masks the vec28 livelock by clearing INTM once, which is useful
         * until the frame-IT over-fire is fixed at its BSP root. */
        g_force_intm_oneshot_enabled = (e && *e == '1') ? 1 : 0;
        /* Optional PC gate: with CALYPSO_FORCE_INTM_AT_PC=0xXXXX set, fire
         * only when the PC matches, so the force lands at a safe point (the
         * RET at fc6f, the idle dispatcher) instead of mid-compute at fc57.
         * That separates state corruption from a broken downstream path. */
        const char *pc_e = calypso_getenv("CALYPSO_FORCE_INTM_AT_PC");
        if (pc_e && *pc_e) {
            unsigned long pc_val = strtoul(pc_e, NULL, 0);
            if (pc_val <= 0xFFFF) {
                g_force_intm_at_pc = (uint16_t)pc_val;
            }
        }
        if (g_force_intm_oneshot_enabled) {
            if (g_force_intm_at_pc != 0xFFFF) {
                fprintf(stderr,
                    "[c54x] FORCE-INTM-ONESHOT enabled : will clear INTM ONCE "
                    "when INTM=1 + BRINT0 pending + PC=0x%04x (= safe-PC gate, "
                    "départage state-corruption vs aval-cassé)\n",
                    g_force_intm_at_pc);
            } else {
                fprintf(stderr,
                    "[c54x] FORCE-INTM-ONESHOT enabled : will clear INTM ONCE "
                    "when INTM=1 + BRINT0 pending (= sonde aval-sain, no PC gate)\n");
            }
        }
    }
    if (g_force_intm_oneshot_enabled <= 0) return;
    if (g_force_intm_oneshot_done) return;
    int intm_set = !!(s->st1 & ST1_INTM);
    if (!intm_set) return;
    /* [2026-07-22] Raise IFR before clearing INTM. At go-live (0xa4e1) IMR is
     * 0x3000 (frame bit unmasked) but IFR is 0, so no interrupt is pending and
     * clearing INTM alone does nothing: the frame interrupt is never latched
     * (the c54x interrupt model is incomplete). At the target PC, force the
     * frame interrupt pending (IFR |= unmasked bits) and only then clear INTM.
     * Without the PC gate, keep the old behaviour: fire on an already pending
     * interrupt. */
    if (g_force_intm_at_pc != 0xFFFF) {
        if (s->pc != g_force_intm_at_pc) return;
        uint16_t unmasked = s->imr & 0x3000;   /* vec28/frame (bit12) + bit13 */
        if (!unmasked) return;                 /* nothing unmasked to force */
        s->ifr |= unmasked;                    /* raise the frame interrupt */
    } else {
        int it_pending = !!(s->ifr & s->imr);
        if (!it_pending) return;
        if (s->insn_count < 1000000) return;
    }
    /* FIRE one-shot : clear INTM, log context. */
    g_force_intm_oneshot_done = 1;
    g_force_intm_oneshot_insn = s->insn_count;
    fprintf(stderr,
        "[c54x] FORCE-INTM-ONESHOT FIRED @insn=%llu PC=0x%04x XPC=%u SP=0x%04x "
        "ST1=0x%04x IMR=0x%04x IFR=0x%04x%s — clearing INTM to allow dispatch\n",
        (unsigned long long)s->insn_count, s->pc, s->xpc & 0xFF, s->sp,
        s->st1, s->imr, s->ifr,
        (g_force_intm_at_pc != 0xFFFF) ? " (safe-PC gate)" : "");
    s->st1 &= ~ST1_INTM;  /* clear INTM bit 11 */
    fprintf(stderr,
        "[c54x] FORCE-INTM-ONESHOT post-clear : ST1=0x%04x — watch next IRQ "
        "dispatch + CORR-ENTRY + a_sync_demod writes\n", s->st1);
}

/* === INT3 cycle tracer + control-flow signature ========================
 *
 * Answers why an INT3 ISR does not reach its RETE.
 *
 * Per INT3 cycle:
 *   START   INT3 dispatched (vec=19): reset the trace, log cycle_id and entry PC
 *   DURING  every conditional branch executed: (PC, op, target, taken)
 *   END ok  RETE fires: dump the trace tagged GOOD plus the insn count
 *   END bad a new INT3 dispatch before the RETE: dump the previous trace
 *           tagged ORPHAN-NEXT-INT3
 *
 * Diffing a good cycle against an orphan one offline gives the first branch
 * that diverges; the state tested at that branch is the cause.
 *
 * Capped at INT3_BRANCH_TRACE_MAX branches per cycle, overflow is tagged.
 * Gate: CALYPSO_DEBUG token INT3-CYCLE. */
Int3BranchEvent g_int3_trace[INT3_BRANCH_TRACE_MAX];
unsigned g_int3_trace_count = 0;
int g_int3_trace_overflow = 0;
int g_int3_cycle_active = 0;
uint64_t g_int3_cycle_id = 0;
uint16_t g_int3_cycle_entry_pc = 0;
uint64_t g_int3_cycle_entry_insn = 0;
int g_int3_trace_enabled = -1;

static void int3_trace_init_lazy(void)
{
    if (g_int3_trace_enabled >= 0) return;
    const char *e = cdbg_env("INT3-CYCLE");
    g_int3_trace_enabled = (e && *e == '1') ? 1 : 0;
    if (g_int3_trace_enabled) {
        fprintf(stderr,
            "[c54x] INT3-CYCLE-TRACE enabled : par cycle vec=19, log toutes "
            "branches conditionnelles + RETE/orphan tag. Cap=%u branches/cycle.\n",
            INT3_BRANCH_TRACE_MAX);
    }
}

/* Detect conditional branch / call / return family. Returns 1 if op
 * is in a tracked branch family, else 0. */
static int is_int3_traced_branch(uint16_t op)
{
    uint16_t hi = op & 0xFF00;
    if (hi == 0x6C00) return 1;  /* BANZ pmad,Sind */
    if (hi == 0x6E00) return 1;  /* BANZD pmad,Sind */
    if (hi == 0xF800) return 1;  /* BC pmad,cond */
    if (hi == 0xF900) return 1;  /* CC pmad,cond */
    if (hi == 0xFA00) return 1;  /* BCD pmad,cond */
    if (hi == 0xFB00) return 1;  /* CCD pmad,cond */
    if (hi == 0xFC00) {
        /* FC00 unconditional = RET ; FCxx where xx is cond = RC */
        if (op != 0xFC00) return 1;
        return 0;
    }
    if (hi == 0xFE00) {
        if (op != 0xFE00) return 1; /* RCD cond */
        return 0;
    }
    return 0;
}

/* Called from c54x_interrupt_ex when vec=19 (INT3 FRAME) dispatched. */
void int3_cycle_start(C54xState *s, uint16_t target_pc)
{
    if (g_int3_trace_enabled < 0) int3_trace_init_lazy();
    if (g_int3_trace_enabled <= 0) return;
    /* If previous cycle still active = orphan (= didn't RETE before re-entry) */
    if (g_int3_cycle_active) {
        fprintf(stderr,
            "[c54x] INT3-CYCLE #%llu ORPHAN-NEXT-INT3 — previous cycle didn't "
            "RETE, new entry @insn=%llu PC=0x%04x. Trace below (%u branches%s) :\n",
            (unsigned long long)g_int3_cycle_id,
            (unsigned long long)s->insn_count, target_pc,
            g_int3_trace_count,
            g_int3_trace_overflow ? "+ OVERFLOW" : "");
        for (unsigned i = 0; i < g_int3_trace_count; i++) {
            Int3BranchEvent *e = &g_int3_trace[i];
            /* 2-word branches (no lk_used here for simplicity) : BC/CC/BCD/CCD
             * (0xF8-0xFB) and BANZ/BANZD (0x6C/0x6E). 1-word : RET/RC/RCD. */
            uint16_t hi = e->op & 0xFF00;
            bool two_word = (hi >= 0xF800 && hi <= 0xFB00)
                            || hi == 0x6C00 || hi == 0x6E00;
            uint16_t fallthrough = e->pc + (two_word ? 2 : 1);
            fprintf(stderr,
                "[c54x]   #%3u Δ%u PC=0x%04x op=0x%04x → next=0x%04x %s ×%u\n",
                i + 1, e->insn_offset, e->pc, e->op, e->next_pc,
                (e->next_pc == fallthrough) ? "(NOT_TAKEN)" : "(TAKEN)",
                e->repeat);
        }
    }
    g_int3_cycle_id++;
    g_int3_cycle_active = 1;
    g_int3_cycle_entry_pc = target_pc;
    g_int3_cycle_entry_insn = s->insn_count;
    g_int3_trace_count = 0;
    g_int3_trace_overflow = 0;
    if (calypso_debug_enabled("INT3-CYCLE")) fprintf(stderr,
        "[c54x] INT3-CYCLE #%llu START @insn=%llu PC→0x%04x SP=0x%04x "
        "PMST=0x%04x IFR=0x%04x\n",
        (unsigned long long)g_int3_cycle_id,
        (unsigned long long)s->insn_count, target_pc, s->sp,
        s->pmst, s->ifr);
}

/* Called from c54x_run after c54x_exec_one. exec_pc/exec_op are the
 * instruction that just executed; s->pc is the resulting PC. */
void int3_cycle_track_branch(C54xState *s, uint16_t exec_pc,
                                    uint16_t exec_op, int consumed)
{
    if (g_int3_trace_enabled <= 0) return;
    if (!g_int3_cycle_active) return;
    if (!is_int3_traced_branch(exec_op)) return;
    /* Compute next_pc correctly across all branch-handler patterns :
     * 1. Non-delayed branch TAKEN  → handler set s->pc=target, returned consumed=0
     *    → s->pc already = target.
     * 2. Delayed branch TAKEN      → handler armed delay_slots=2 + delayed_pc,
     *    returned consumed>0; main loop hasn't run +=consumed yet
     *    → eventual target = s->delayed_pc.
     * 3. Branch FALL-THROUGH (any) → handler returned consumed>0, s->pc unchanged,
     *    delay_slots not set; main loop will += consumed → next insn
     *    → next = exec_pc + consumed. */
    uint16_t actual_next;
    if (consumed == 0) {
        actual_next = s->pc;
    } else if (s->delay_slots == 2) {
        actual_next = s->delayed_pc;
    } else {
        actual_next = (uint16_t)(exec_pc + consumed);
    }

    /* Dedup-pattern : look up to 4 slots back. Catches consecutive
     * identical (distance 1, AAA), strict alternation (distance 2,
     * ABAB), and short cycles up to length 4 (ABCDABCD). Each iteration
     * of the repeating pattern bumps the matched slot's repeat — total
     * iterations = max(repeat) across slots forming the cycle. */
    for (unsigned back = 1; back <= 4 && back <= g_int3_trace_count; back++) {
        Int3BranchEvent *cand = &g_int3_trace[g_int3_trace_count - back];
        if (cand->pc == exec_pc && cand->op == exec_op && cand->next_pc == actual_next) {
            cand->repeat++;
            return;
        }
    }
    if (g_int3_trace_count >= INT3_BRANCH_TRACE_MAX) {
        g_int3_trace_overflow = 1;
        return;
    }
    Int3BranchEvent *e = &g_int3_trace[g_int3_trace_count++];
    e->pc = exec_pc;
    e->op = exec_op;
    e->next_pc = actual_next;
    e->insn_offset = (uint32_t)(s->insn_count - g_int3_cycle_entry_insn);
    e->repeat = 1;
}

/* Called from RETE handler (L3300 area) BEFORE INTM is cleared. */
void int3_cycle_end_good(C54xState *s, uint16_t return_addr)
{
    if (g_int3_trace_enabled <= 0) return;
    if (!g_int3_cycle_active) return;
    uint64_t duration = s->insn_count - g_int3_cycle_entry_insn;
    fprintf(stderr,
        "[c54x] INT3-CYCLE #%llu RETE-GOOD @insn=%llu duration=%llu PC→0x%04x "
        "branches=%u%s\n",
        (unsigned long long)g_int3_cycle_id,
        (unsigned long long)s->insn_count, (unsigned long long)duration,
        return_addr, g_int3_trace_count,
        g_int3_trace_overflow ? "+ OVERFLOW" : "");
    for (unsigned i = 0; i < g_int3_trace_count; i++) {
        Int3BranchEvent *e = &g_int3_trace[i];
        uint16_t hi = e->op & 0xFF00;
        bool two_word = (hi >= 0xF800 && hi <= 0xFB00)
                        || hi == 0x6C00 || hi == 0x6E00;
        uint16_t fallthrough = e->pc + (two_word ? 2 : 1);
        fprintf(stderr,
            "[c54x]   #%3u Δ%u PC=0x%04x op=0x%04x → next=0x%04x %s ×%u\n",
            i + 1, e->insn_offset, e->pc, e->op, e->next_pc,
            (e->next_pc == fallthrough) ? "(NOT_TAKEN)" : "(TAKEN)",
            e->repeat);
    }
    g_int3_cycle_active = 0;
}

/* Called from c54x_run top-of-loop. */
void stuck_probe_check(C54xState *s)
{
    if (g_stuck_probe_enabled < 0) stuck_probe_init_lazy();
    if (g_stuck_probe_enabled <= 0) return;
    int intm_set = !!(s->st1 & ST1_INTM);
    int brint0_pending = !!(s->ifr & (1 << 5));
    int now_stuck = (intm_set && brint0_pending);
    if (now_stuck && !g_stuck_active) {
        g_stuck_active = 1;
        g_stuck_start_insn = s->insn_count;
        g_stuck_duration = 0;
        g_stuck_hist_used = 0;  /* fresh hist per stuck window */
        fprintf(stderr,
            "[c54x] STUCK-ENTER insn=%llu PC=0x%04x XPC=%u IFR=0x%04x IMR=0x%04x\n",
            (unsigned long long)s->insn_count, s->pc, s->xpc & 0xFF,
            s->ifr, s->imr);
    }
    if (now_stuck) {
        g_stuck_duration++;
        /* Sample every 100 insns to bound hist diversity */
        if ((g_stuck_duration % 100) == 0) {
            stuck_probe_record(s->pc, s->xpc & 0xFF);
        }
        /* Dump periodically while stuck */
        if ((g_stuck_duration % 5000000) == 0 && g_stuck_dump_count < 5) {
            g_stuck_dump_count++;
            stuck_probe_dump(s->insn_count, "periodic-5M");
        }
    } else if (g_stuck_active) {
        g_stuck_active = 0;
        fprintf(stderr,
            "[c54x] STUCK-EXIT insn=%llu duration=%u PC=0x%04x XPC=%u IFR=0x%04x\n",
            (unsigned long long)s->insn_count, g_stuck_duration,
            s->pc, s->xpc & 0xFF, s->ifr);
        if (g_stuck_duration >= 10000) {  /* only dump if long-ish stuck */
            stuck_probe_dump(s->insn_count, "on-exit");
        }
    }
}

/* Hook called from data_write_locked when an absolute write hits 0x3DC0. */
void fbdb_probe_write_3dc0(uint16_t addr, uint16_t old_val,
                                  uint16_t new_val, uint16_t pc, unsigned insn)
{
    if (g_fbdb_probe_enabled <= 0) return;
    g_addr3dc0_wr_count++;
    if (g_addr3dc0_wr_count <= 50) {
        uint16_t set_mask = new_val & ~old_val;  /* bits set by this write */
        fprintf(stderr,
            "[c54x] FBDB-PROBE WR 0x%04x : 0x%04x → 0x%04x (set=0x%04x) "
            "PC=0x%04x insn=%u %s\n",
            addr, old_val, new_val, set_mask, pc, insn,
            (set_mask & 0x0010) ? "*** BIT 4 SET ***" : "");
    }
}

void fbdb_probe_read_3dc0(uint16_t addr, uint16_t val,
                                 uint16_t pc, unsigned insn)
{
    if (g_fbdb_probe_enabled <= 0) return;
    g_addr3dc0_rd_count++;
    if (g_addr3dc0_rd_count <= 30
        || (g_addr3dc0_rd_count % 10000) == 0) {
        fprintf(stderr,
            "[c54x] FBDB-PROBE RD 0x%04x = 0x%04x (bit4=%d) PC=0x%04x insn=%u\n",
            addr, val, !!(val & 0x0010), pc, insn);
    }
}

void corr_read_record(uint16_t addr)
{
    if (g_corr_trace_enabled <= 0) return;
    g_corr_read_total++;
    unsigned i;
    for (i = 0; i < g_corr_read_used; i++) {
        if (g_corr_read_hist[i].addr == addr) {
            g_corr_read_hist[i].count++;
            return;
        }
    }
    if (g_corr_read_used < CORR_READ_HIST_MAX) {
        g_corr_read_hist[g_corr_read_used].addr  = addr;
        g_corr_read_hist[g_corr_read_used].count = 1;
        g_corr_read_used++;
    }
}

void corr_read_dump(const char *trig)
{
    if (g_corr_trace_enabled <= 0) return;
    fprintf(stderr, "[c54x] CORR-READ DUMP[%s] total=%llu uniq=%u\n",
            trig, (unsigned long long)g_corr_read_total, g_corr_read_used);
    /* Sort by count descending (simple selection sort, n<=128). */
    for (unsigned k = 0; k < g_corr_read_used; k++) {
        unsigned best = k;
        for (unsigned i = k + 1; i < g_corr_read_used; i++) {
            if (g_corr_read_hist[i].count > g_corr_read_hist[best].count)
                best = i;
        }
        if (best != k) {
            CorrReadEntry tmp = g_corr_read_hist[k];
            g_corr_read_hist[k] = g_corr_read_hist[best];
            g_corr_read_hist[best] = tmp;
        }
        fprintf(stderr, "[c54x] CORR-READ #%u addr=0x%04x count=%u\n",
                k + 1, g_corr_read_hist[k].addr, g_corr_read_hist[k].count);
    }
}

/* === DSP throughput emission ===
 *
 * Emits `[c54x] INSN-COUNT-STATS total=N delta=N elapsed_ms=N rate=N/s` every
 * 1M instructions. Two tests read it:
 *   - test_dsp_throughput_5x (milestones, static)
 *   - test_dsp_throughput_above_threshold (observability, runtime)
 * The pytest threshold is 50M/s, a x2 margin below the 100M/s measured here. */
#include <time.h>
struct c54x_g_throughput_s g_throughput;

inline void throughput_tick(uint64_t insn_count)
{
    if (insn_count - g_throughput.last_logged_insn < 1000000) return;
    struct timespec now;
    clock_gettime(CLOCK_MONOTONIC, &now);
    if (g_throughput.last_logged_ts.tv_sec == 0 &&
        g_throughput.last_logged_ts.tv_nsec == 0) {
        g_throughput.last_logged_ts = now;
        g_throughput.last_logged_insn = insn_count;
        return;
    }
    int64_t delta_ns =
        (int64_t)(now.tv_sec - g_throughput.last_logged_ts.tv_sec) * 1000000000LL +
        (int64_t)(now.tv_nsec - g_throughput.last_logged_ts.tv_nsec);
    uint64_t delta_insn = insn_count - g_throughput.last_logged_insn;
    uint64_t rate = (delta_ns > 0)
        ? (delta_insn * 1000000000ULL / (uint64_t)delta_ns) : 0;
    if (calypso_debug_enabled("INSN-COUNT-STATS")) fprintf(stderr,
            "[c54x] INSN-COUNT-STATS total=%llu delta=%llu elapsed_ms=%lld rate=%llu/s\n",
            (unsigned long long)insn_count,
            (unsigned long long)delta_insn,
            (long long)(delta_ns / 1000000),
            (unsigned long long)rate);
    g_throughput.last_logged_ts = now;
    g_throughput.last_logged_insn = insn_count;
}

/* === Read-by-range tracking for FB-det path analysis ===
 *
 * Identifies which DARAM zone the FB-det routine reads. Cumulative counters
 * per range, plus a snapshot/delta at each trigger PC, so the delta between
 * two consecutive triggers is the reads accumulated in the window before it.
 *
 * Mutually exclusive ranges:
 *   RR_MMRS   [0x0000..0x005F]  C54x MMR registers
 *   RR_LOW    [0x0060..0x03A3]  linear correlator zone
 *   RR_APIRAM [0x0800..0x27FF]  ARM/DSP shared API RAM
 *   RR_TARGET [0x3FB0..0x3FFF]  where the BSP DMA writes by default
 *   RR_WRAP   [0xFC5D..0xFFED]  correlator wrap zone, BK=176 (AR2/AR7)
 *   RR_OTHER  everything else (overlay 0x80..0x7FF, above 0x4000, ...) */

struct c54x_g_read_stats_s g_read_stats;

inline void read_stats_record(uint16_t addr)
{
    int r;
    if      (addr <= 0x005F)                    r = RR_MMRS;
    else if (addr <= 0x03A3)                    r = RR_LOW;
    else if (addr >= 0x0800 && addr <= 0x27FF)  r = RR_APIRAM;
    else if (addr >= 0x3FB0 && addr <= 0x3FFF)  r = RR_TARGET;
    else if (addr >= 0xFC5D && addr <= 0xFFED)  r = RR_WRAP;
    else                                        r = RR_OTHER;
    g_read_stats.cumulative[r]++;
}

void read_stats_trigger_check(C54xState *s)
{
    /* [2026-05-14] Single trigger PC 0x8f51 (FB-det compute loop, 50 hits per
     * sweep). The wider list {0x8f51, 0x778a, 0x9ac0, 0x9ad0, 0x9b00, 0x821a}
     * did not work: 0x821a sits in the boot mailbox poll loop, 14 insns apart,
     * and ate the 200-line cap before 0x8f51 ever fired; the other PCs were
     * init/reset code, 1-3 hits each over a whole run. The cap is now 5000, to
     * cover several FB-det sweeps. */
    if (s->pc != 0x8f51) return;
    g_read_stats.trigger_count++;
    if (g_read_stats.trigger_count > 5000) return;
    uint64_t delta[RR_NUM];
    for (int r = 0; r < RR_NUM; r++) {
        delta[r] = g_read_stats.cumulative[r] - g_read_stats.snapshot[r];
        g_read_stats.snapshot[r] = g_read_stats.cumulative[r];
    }
    if (calypso_debug_enabled("READ-AMONT")) fprintf(stderr,
            "[c54x] READ-AMONT #%llu PC=0x%04x insn=%u "
            "mmrs=%llu low=%llu apiram=%llu target=%llu wrap=%llu other=%llu\n",
            (unsigned long long)g_read_stats.trigger_count, s->pc, s->insn_count,
            (unsigned long long)delta[RR_MMRS],
            (unsigned long long)delta[RR_LOW],
            (unsigned long long)delta[RR_APIRAM],
            (unsigned long long)delta[RR_TARGET],
            (unsigned long long)delta[RR_WRAP],
            (unsigned long long)delta[RR_OTHER]);
}

/* === NOP-region guard + transfer ring + A-write ring (2026-05-27 Plan B) ===
 * Trip ONCE on first entry into the unmapped prog zone (= PC < 0x7000 in
 * bank 0, outside OVLY DARAM 0x80-0x27FF). At trip, dump :
 *   (a) trigger transfer (the call/branch that landed in NOP zone)
 *   (b) N last control-flow transfers (most recent → oldest)
 *   (c) N last A-writes
 * Together they name the root cause without speculation. */
  ×3
XferLog   g_xfer_ring[NOP_RING_N];
unsigned  g_xfer_idx;
AWriteLog g_awrite_ring[NOP_RING_N];
unsigned  g_awrite_idx;
int       g_nop_tripped;

const char *classify_xfer_op(uint16_t op)
{
    if ((op & 0xFF80) == 0xF880) return "FB";
    if ((op & 0xFF80) == 0xF980) return "FCALL";
    if ((op & 0xFF80) == 0xFA80) return "FBD";
    if ((op & 0xFF80) == 0xFB80) return "FCALLD";
    if (op == 0xF4E2 || op == 0xF5E2) return "BACC";
    if (op == 0xF4E3 || op == 0xF5E3) return "CALA";
    if (op == 0xF4E6 || op == 0xF5E6) return "FBACC";
    if (op == 0xF4E7 || op == 0xF5E7) return "FCALA";
    if (op == 0xF6E6) return "FBACCD";
    if (op == 0xF6E7) return "FCALAD";
    if (op == 0xF4E4) return "FRET";
    if (op == 0xF4EB) return "RETE";
    if (op == 0xF6E4 || op == 0xF6E5) return "FRETD";
    if (op == 0xF073) return "B";
    if (op == 0xF273) return "BD";
    if (op == 0xF074) return "CALL";
    if (op == 0xF274) return "CALLD";
    return "OTHER";
}

void xfer_log_push(uint16_t src_pc, uint8_t src_xpc, uint16_t op,
                          uint16_t tgt_pc, uint8_t tgt_xpc, int64_t a_val,
                          uint64_t insn)
{
    XferLog *e = &g_xfer_ring[g_xfer_idx % NOP_RING_N];
    e->src_pc = src_pc;
    e->src_xpc = src_xpc;
    e->op = op;
    e->tgt_pc = tgt_pc;
    e->tgt_xpc = tgt_xpc;
    e->a_val = a_val;
    e->insn = insn;
    const char *t = classify_xfer_op(op);
    /* strncpy without padding */
    int k = 0;
    while (k < 7 && t[k]) { e->type[k] = t[k]; k++; }
    e->type[k] = '\0';
    g_xfer_idx++;
}

void awrite_log_push(uint16_t pc, uint8_t xpc, uint16_t op,
                            int64_t old_a, int64_t new_a, uint64_t insn)
{
    AWriteLog *e = &g_awrite_ring[g_awrite_idx % NOP_RING_N];
    e->pc = pc;
    e->xpc = xpc;
    e->op = op;
    e->old_a = old_a;
    e->new_a = new_a;
    e->insn = insn;
    g_awrite_idx++;
}

/* NOP-region predicate :
 *   xpc == 0 && pc < 0x7000 && !(OVLY && pc in [0x80, 0x2800])
 * Anything that lands here is in the unmapped prog area = NOP slide. */
/* Forward declaration: pc_in_nop_region below needs the OVLY alias floor,
 * defined in another translation unit. */
uint16_t c54x_ovly_bas(void);

inline int pc_in_nop_region(const C54xState *s, uint16_t pc, uint8_t xpc)
{
    if (xpc != 0) return 0;                 /* upper bank: handled elsewhere */
    if (pc >= 0x7000) return 0;             /* PROM0 + PROM1 mirror = valid */
    if ((s->pmst & PMST_OVLY) && pc >= c54x_ovly_bas() && pc < 0x2800)
        return 0;                           /* OVLY DARAM mapping = valid */
    return 1;
}

void nop_guard_dump(C54xState *s, uint16_t pc, uint8_t xpc)
{
    if (g_nop_tripped) return;
    g_nop_tripped = 1;
    C54_LOG("================================================");
    C54_LOG("NOP-REGION GUARD TRIPPED");
    C54_LOG("  trigger PC=0x%04x XPC=%u  prog[lin]=0x%04x  insn=%u",
            pc, xpc, s->prog[((uint32_t)xpc << 16) | pc], s->insn_count);
    C54_LOG("  state : A=%010llx B=%010llx SP=0x%04x ST1=0x%04x INTM=%d "
            "AR0..7: %04x %04x %04x %04x %04x %04x %04x %04x",
            (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
            (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
            s->sp, s->st1, !!(s->st1 & ST1_INTM),
            s->ar[0], s->ar[1], s->ar[2], s->ar[3],
            s->ar[4], s->ar[5], s->ar[6], s->ar[7]);

    C54_LOG("--- last %d control-flow transfers (oldest → newest) ---", NOP_RING_N);
    unsigned start = g_xfer_idx > NOP_RING_N ? (g_xfer_idx - NOP_RING_N) : 0;
    for (unsigned i = start; i < g_xfer_idx; i++) {
        const XferLog *t = &g_xfer_ring[i % NOP_RING_N];
        C54_LOG("  [%u] %-7s src=(xpc=%u,pc=0x%04x) op=0x%04x → tgt=(xpc=%u,pc=0x%04x) "
                "A=%010llx insn=%llu",
                i, t->type, t->src_xpc, t->src_pc, t->op,
                t->tgt_xpc, t->tgt_pc,
                (unsigned long long)(t->a_val & 0xFFFFFFFFFFULL),
                (unsigned long long)t->insn);
    }

    C54_LOG("--- last %d A-writes (oldest → newest) ---", NOP_RING_N);
    unsigned astart = g_awrite_idx > NOP_RING_N ? (g_awrite_idx - NOP_RING_N) : 0;
    for (unsigned i = astart; i < g_awrite_idx; i++) {
        const AWriteLog *a = &g_awrite_ring[i % NOP_RING_N];
        int64_t do_old = a->old_a & 0xFFFFFFFFFFLL;
        int64_t do_new = a->new_a & 0xFFFFFFFFFFLL;
        C54_LOG("  [%u] PC=0x%04x xpc=%u op=0x%04x  A: %010llx → %010llx "
                "(Δ=%+lld) insn=%llu",
                i, a->pc, a->xpc, a->op,
                (unsigned long long)do_old, (unsigned long long)do_new,
                (long long)(do_new - do_old),
                (unsigned long long)a->insn);
    }
    C54_LOG("================================================");
}

4.47 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_a5.c

11210 octets, 261 lignes → 261 lignes

/* calypso_a5.c - the A5 coprocessor of the Calypso DSP, on the C54x I/O bus.
 *
 * The mask-ROM does not cipher in software: it hands Kc and COUNT to a
 * coprocessor wired on ports 0x2800..0x2818 and reads the key stream back
 * in the completion interrupt. Without this model the ports are silent, the
 * stream stays at zero and the DSP deciphers nothing. As read in PROM0
 * (tools/tic54x-dis.py, base 0x7000):
 *
 *   0xb12c  programming, at every dedicated frame:
 *           PORTW 0x2800 <- 0                      stop
 *           PORTW 0x2803..0x2806 <- a_kc[0..3]      (NDB 0x0a3b..0x0a3e)
 *           PORTW 0x2807 <- a_a5fn[0] = T3<<5 | T2  (page W word 12, via 0x3fc6)
 *           PORTW 0x2808 <- a_a5fn[1] = T1          (page W word 13, via 0x3fc7)
 *           PORTW 0x2800 <- (d_a5mode << 2) | 0x12, then | 0x0001 (start)
 *           IMR |= 0x0800, PORTR 0x2802: bit 0 = accepted, otherwise retry
 *   0xb19d  completion ISR (IFR bit 11, vector 27, relay 0x726c):
 *           PORTR 0x2801 (trace only), PORTR 0x2809..0x2818 -> 0x3f93..0x3fa2,
 *           0x3f92 |= 0x1000 (stream ready), PORTW 0x2800 <- 0
 *   0x856b  downlink: words 0x3f93..0x3f9a, bit 15 first (T=24, LD TS,B
 *           then SFTA B,1; SACCD BLT inverts the soft bit) -> 114 bits
 *   0x85d1  uplink: words 0x3f9b..0x3fa2 XORed onto the packed burst 0x3f8a
 *
 * Kc arrives reversed (firmware dsp_load_ciph_param:
 * a_kc[0] = key[7] | key[6] << 8 ...), COUNT is already T1/T3/T2. The stream
 * is therefore computed with osmo_a5() on the reconstructed fn.
 *
 * CALYPSO_A5=0 disables the model (the ports become silent again, A/B). */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <osmocom/gsm/a5.h>
#include "calypso_c54x.h"
#include "calypso_a5.h"

#define A5_PA_CTRL    0x2800
#define A5_PA_TRACE   0x2801
#define A5_PA_ETAT    0x2802
#define A5_PA_KC0     0x2803
#define A5_PA_CNT0    0x2807
#define A5_PA_CNT1    0x2808
#define A5_PA_FLUX0   0x2809
#define A5_N_FLUX     16

#define A5_IT_VEC     27
#define A5_IT_BIT     11

static struct {
    uint16_t ctrl, kc[4], cnt[2], etat;
    uint16_t flux[A5_N_FLUX];
    unsigned long n_calculs;
    unsigned long n_recales;   /* COUNTs realigned to last deposit + 1 */
    unsigned long n_dl_bsp;    /* downlink stream taken at the BSP fn */
    uint32_t dernier_fn;       /* fn of the last computed A5/1-2 stream (coverage) */
    unsigned dernier_algo;
} a5;

/* For coverage (c54x_exe pont.c tx_tsp_publier): true if the coprocessor has
 * computed an A5/1 or A5/2 stream, *fn = the frame of that stream. */
bool calypso_a5_dernier(uint32_t *fn)
{
    if (!calypso_a5_on() || !a5.dernier_algo)
        return false;
    *fn = a5.dernier_fn;
    return true;
}

bool calypso_a5_on(void)
{
    static int on = -1;
    if (on < 0) {
        const char *e = getenv("CALYPSO_A5");
        on = !(e && *e == '0');
    }
    return on;
}

/* 114 bits 0/1 -> 8 words, bit 15 of the first word = first bit. */
static void empaqueter(const ubit_t *b, uint16_t *w)
{
    memset(w, 0, 8 * sizeof(*w));
    for (int i = 0; i < 114; i++)
        if (b[i])
            w[i / 16] |= (uint16_t)(0x8000u >> (i % 16));
}

static void a5_calculer(C54xState *s, unsigned algo)
{
    uint8_t key[8];
    for (int i = 0; i < 4; i++) {
        key[7 - 2 * i] = (uint8_t)(a5.kc[i] & 0xff);
        key[6 - 2 * i] = (uint8_t)(a5.kc[i] >> 8);
    }
    unsigned t2 = a5.cnt[0] & 0x1f, t3 = (a5.cnt[0] >> 5) & 0x3f, t1 = a5.cnt[1] & 0x7ff;
    /* gsm_gsmtime2fn: T1, T2, T3 -> fn */
    uint32_t fn = 51u * 26u * t1 + 51u * (((int)t3 - (int)t2 + 26 * 3) % 26) + t3;
    /* COUNT ONE FRAME LATE: REALIGN TO THE NEXT DEPOSIT.
     * The firmware writes a_a5fn from l1s.next_time (calypso/dsp.c:553): the
     * frame the BSP will deposit at the NEXT tick, i.e. last deposit + 1.
     * When the ROM programs the coprocessor with the a_a5fn of the PREVIOUS
     * task (read in its phase A before the ARM l1_sync has written the next
     * page W, a race of the two-phase stepping), the offset is 0 and the
     * stream would serve the following burst. CALYPSO_A5_RECALE=0 keeps the
     * raw COUNT. */
    {
        static int recale = -1;
        if (recale < 0) { const char *e = getenv("CALYPSO_A5_RECALE"); recale = !(e && *e == '0'); }
        extern unsigned calypso_daram_last_fn;
        /* ON THE SACCH ONLY. At offset 0 the traffic and FACCH frames carry
         * the right COUNT (speech bit-exact, FACCH identical when compared
         * with tools/comparer_parole.py against bursts deciphered outside the
         * DSP) while the SACCH/TF does not: realigning everything would turn
         * speech into noise and break the FACCH. Only the SACCH/TF frame is
         * realigned (fn%26 == 12 on an even TN, 25 on an odd TN). */
        unsigned p26 = fn % 26u;
        if (recale && fn == calypso_daram_last_fn && (p26 == 12u || p26 == 25u)) {
            fn = (fn + 1u) % (2715648u);
            a5.n_recales++;
            t1 = fn / 1326u; t2 = fn % 26u; t3 = fn % 51u;
        }
    }

    /* DOWNLINK STREAM: THE FN OF THE BURST THE BSP DEPOSITS AT THIS TICK. The race
     * described above leaves a_a5fn one or two frames ahead of the burst depending on
     * the run: SDCCH at "offset +1" (right), TCH at "offset +2" (downlink FACCH (UA)
     * fails Fire, RR Assignment Failure). The BSP timing (calypso_bsp_fn_bts_du_tick)
     * is exact for the frame played at the current tick; it is used for the downlink
     * stream when within 3 frames. The uplink stream keeps the firmware COUNT.
     * CALYPSO_A5_DL_BSP=0: use the firmware COUNT for the downlink too. */
    uint32_t fn_dl = fn;
    {
        static int dl_bsp = -1;
        if (dl_bsp < 0) { const char *e = getenv("CALYPSO_A5_DL_BSP"); dl_bsp = !(e && *e == '0'); }
        extern uint32_t calypso_trx_get_fn(void);
        extern int calypso_bsp_fn_bts_du_tick(uint32_t tick, uint32_t *fn);
        uint32_t ft;
        if (dl_bsp && calypso_bsp_fn_bts_du_tick(calypso_trx_get_fn(), &ft)) {
            int32_t d = (int32_t)ft - (int32_t)fn;
            /* CALYPSO_A5_DL_DECAL: offset added to the BSP fn when a_a5fn differs from it (TCH
             * case; on SDCCH a_a5fn == BSP fn and nothing changes). */
            static int dl_decal = 99;
            if (dl_decal == 99) { const char *e = getenv("CALYPSO_A5_DL_DECAL"); dl_decal = (e && *e) ? atoi(e) : 0; }
            if (d >= -3 && d <= 3 && d != 0) { fn_dl = (uint32_t)((int32_t)ft + dl_decal); a5.n_dl_bsp++; }
            extern unsigned calypso_daram_last_fn;
            static unsigned n_sd;
            if (n_sd < 400 && ft != fn) {
                n_sd++;
                if (n_sd <= 400) printf("  [a5-dl] tick=%u a5fn=%u bsp=%u dernier_depot=%u -> flux DL %u (p26 %u)\n",
                                        calypso_trx_get_fn(), fn, ft, calypso_daram_last_fn, fn_dl, fn_dl % 26u);
            }
        }
    }
    ubit_t dl[114], ul[114], dl2[114];
    if (osmo_a5((int)algo, key, fn, dl, ul) < 0) {
        memset(a5.flux, 0, sizeof a5.flux);
    } else {
        if (fn_dl != fn && osmo_a5((int)algo, key, fn_dl, dl2, NULL) == 0) memcpy(dl, dl2, sizeof dl);
        empaqueter(dl, &a5.flux[0]);
        empaqueter(ul, &a5.flux[8]);
        a5.dernier_fn = fn;
        a5.dernier_algo = algo;
    }
    a5.etat |= 0x0001;
    /* On stdout (dsp.log): the stderr of c54x_exe is not read.
     * The first 8 computations, every Kc change, then a sample every 500:
     * fn of the COUNT against the last BTS frame deposited by the BSP
     * (calypso_daram_last_fn). The COUNT targets the frame of the task,
     * deposited at the next tick: an offset other than +1 means a wrong COUNT. */
    {
        static uint8_t kc_prec[8]; static int kc_vu;
        bool change = !kc_vu || memcmp(kc_prec, key, 8) != 0;
        if (a5.n_calculs < 8 || change || (a5.n_calculs % 500) == 0) {
            extern unsigned calypso_daram_last_fn;
            printf("  [a5] #%lu A5/%u fn=%u (T1=%u T2=%u T3=%u) Kc=%02x%02x%02x%02x%02x%02x%02x%02x%s "
                   "| dernier depot BSP fn=%u (ecart %+d)\n", a5.n_calculs, algo, fn, t1, t2, t3,
                   key[0], key[1], key[2], key[3], key[4], key[5], key[6], key[7],
                   change ? " (NOUVEAU Kc)" : "", calypso_daram_last_fn,
                   (int)(fn - calypso_daram_last_fn));
            fflush(stdout);
        }
        memcpy(kc_prec, key, 8); kc_vu = 1;
    }
    /* HISTOGRAM OF THE OFFSET BY POSITION IN THE 26-MULTIFRAME.
     * Shows WHICH frames are targeted off by one: count of the offset
     * (-1, 0, +1, +2, other) per fn%26, one line every 2000 operations. */
    {
        extern unsigned calypso_daram_last_fn;
        static unsigned long hist[26][5];
        int e = (int)(fn - calypso_daram_last_fn);
        hist[fn % 26u][e >= -1 && e <= 2 ? e + 1 : 4]++;
        if (a5.n_calculs && (a5.n_calculs % 2000) == 0) {
            printf("  [a5-hist] #%lu recales=%lu ecart(-1/0/+1/+2/autre) par fn%%26 :", a5.n_calculs, a5.n_recales);
            for (int p = 0; p < 26; p++)
                if (hist[p][0] + hist[p][1] + hist[p][2] + hist[p][3] + hist[p][4])
                    printf(" %d:%lu/%lu/%lu/%lu/%lu", p, hist[p][0], hist[p][1], hist[p][2], hist[p][3], hist[p][4]);
            printf("\n");
            fflush(stdout);
            memset(hist, 0, sizeof hist);
        }
    }
    a5.n_calculs++;
    /* End of computation: the 0xb19d ISR collects the stream. IFR only -- the
     * core vectors at the next instruction (c54x_irq_level_check), never in
     * the middle of the PORTW in progress. */
    if (s)
        s->ifr |= (uint16_t)(1u << A5_IT_BIT);
}

bool calypso_a5_portw(C54xState *s, uint16_t pa, uint16_t val)
{
    if (!calypso_a5_on() || pa < A5_PA_CTRL || pa > A5_PA_CNT1)
        return false;
    switch (pa) {
    case A5_PA_CTRL: {
        uint16_t avant = a5.ctrl;
        a5.ctrl = val;
        if (val == 0) {
            a5.etat = 0;
        } else if ((val & 0x0001) && !(avant & 0x0001)) {
            unsigned algo = (val >> 2) & 3;
            if (algo == 1 || algo == 2) {
                a5_calculer(s, algo);
            } else {
                memset(a5.flux, 0, sizeof a5.flux);
                a5.etat |= 0x0001;
                if (s) s->ifr |= (uint16_t)(1u << A5_IT_BIT);
            }
        }
        return true;
    }
    case A5_PA_CNT0: a5.cnt[0] = val; return true;
    case A5_PA_CNT1: a5.cnt[1] = val; return true;
    case A5_PA_TRACE:
    case A5_PA_ETAT:
        return true;
    default:
        a5.kc[pa - A5_PA_KC0] = val;
        return true;
    }
}

bool calypso_a5_portr(C54xState *s, uint16_t pa, uint16_t *out)
{
    (void)s;
    if (!calypso_a5_on() || pa < A5_PA_CTRL || pa >= A5_PA_FLUX0 + A5_N_FLUX)
        return false;
    if (pa >= A5_PA_FLUX0)
        *out = a5.flux[pa - A5_PA_FLUX0];
    else if (pa == A5_PA_ETAT)
        *out = a5.etat;
    else if (pa == A5_PA_CTRL)
        *out = a5.ctrl;
    else if (pa == A5_PA_TRACE)
        *out = 0;
    else if (pa <= A5_PA_KC0 + 3)
        *out = a5.kc[pa - A5_PA_KC0];
    else
        *out = a5.cnt[pa - A5_PA_CNT0];
    return true;
}

4.48 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_a5.h

426 octets, 16 lignes → 16 lignes

/* calypso_a5.h - coprocesseur A5 du DSP Calypso (ports XIO 0x2800..0x2818).
 * Voir calypso_a5.c. */
#ifndef CALYPSO_A5_H
#define CALYPSO_A5_H

#include <stdbool.h>
#include <stdint.h>

typedef struct C54xState C54xState;

bool calypso_a5_on(void);
bool calypso_a5_portw(C54xState *s, uint16_t pa, uint16_t val);
bool calypso_a5_portr(C54xState *s, uint16_t pa, uint16_t *out);
bool calypso_a5_dernier(uint32_t *fn);

#endif

4.49 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_arm2dsp.c

17515 octets, 328 lignes → 328 lignes

/*
 * calypso_arm2dsp - ARM->DSP task-post bridge, data only.
 *
 * On real Calypso the ARM (osmocom L1) drives the C54x DSP through the shared
 * API RAM: it commands the FB/FCCH search by writing d_dsp_page bit1
 * (B_GSM_TASK). The DSP runs a software task dispatcher (ROM 0xb41c) polling
 * the task-ready word data[0x0fff]; bit1 (0x0002) set means dequeue and run
 * the posted task (0xb42a) from its own ROM code.
 *
 * This module posts only DATA the DSP already polls, mirrored into api_ram:
 * no PC redirect, no IMR/INTM/SP poke, no vector poke. Keep it that way -
 * forcing the frame IT out of a clean idle context pushes a bogus return PC
 * and self-loops at PC=0x0000.
 *
 * Env (no rebuild):
 *   CALYPSO_ARM2DSP=1               enable (off by default)
 *   CALYPSO_ARM2DSP_TASKWORD=0xfff  DSP task-ready word (default 0x0fff)
 *   CALYPSO_ARM2DSP_TASKBIT=0x2     bit to post (default 0x0002 = dispatcher bit1)
 *   CALYPSO_ARM2DSP_BGEN=1          post d_background_enable/state cells (off by default)
 *   CALYPSO_ARM2DSP_BGEN_A=0x098a   d_background_enable word
 *   CALYPSO_ARM2DSP_BGEN_C=0x098c   d_background_state  word
 *   CALYPSO_ARM2DSP_BGEN_VAL=0x1    value posted into d_background_enable
 *   CALYPSO_ARM2DSP_BGEN_VAL_C=0x1  value posted into d_background_state
 *   CALYPSO_ARM2DSP_BGEN_POLLPC=0xdddb  DSP PC of the phase-SM cell poll
 *   CALYPSO_ARM2DSP_BGEN_ONESHOT=1  post exactly once (single go-live transition)
 */
#include "qemu/osdep.h"
#include "hw/arm/calypso/calypso_debug.h"
#include "calypso_arm2dsp.h"

#include <stdlib.h>
#include <stdio.h>

/* ARM byte offset of d_dsp_page (DSP word 0x08D4). B_GSM_TASK = bit1.
 * Not 0x08E2: that word is d_dsp_state, 14 words further on. */
#define A2D_DSP_PAGE_OFF   0x01A8
#define A2D_B_GSM_TASK     0x0002

/* DSP API region base: words >= 0x0800 are read by the DSP from api_ram. */
#define A2D_API_BASE       0x0800

static int      a2d_on = -1;      /* -1 = unresolved, 0/1 = disabled/enabled     */
static uint16_t a2d_word;         /* DSP task-ready word (default 0x0fff)         */
static uint16_t a2d_bit;          /* task-ready bit to post (default 0x0002)      */

static volatile int a2d_pending;  /* ARM posted B_GSM_TASK, awaiting DSP step     */
static unsigned     a2d_posts;    /* how many task-posts applied                  */
static int          a2d_cont = -1; /* continuous post while d_dsp_page bit1 set     */

/* ---- BGEN: ARM background-enable handshake -----------------------------------
 * The DSP go-live phase-SM (0xdddb -> 0xddeb -> 0xde8b) polls d[0x098a]
 * (d_background_enable) and d[0x098c] (d_background_state). While the ARM
 * leaves them 0 the SM takes the reset branch and never reaches the GO setter
 * 0xde9c (ST #2) that raises d[0x3f70] bit1, the flag the go-live wait-loop
 * test at 0xa4d4 reads to exit. Measured on the live full-grgsm run: 0 hits on
 * 0xde9c, the wait-loop 0xa4ca/0xa4d0 spins 300k+ times toggling INTM, the FB
 * correlator [0x8d00..0x9000] is never entered (0 hits), and the ARM never
 * writes near 0x098a. Real Calypso posts these cells as part of the go-live
 * handshake, so we post them here: data only, exactly ONCE, gated on the ARM
 * having commanded the task (dispatcher bit d[0x0fff] & 0x0002 set). The DSP
 * then reaches 0xde9c, raises d[0x3f70] bit1 itself and leaves the wait-loop
 * natively - one go-live transition, not a per-frame re-fire. Off by default. */
static int          a2d_bgen = -1;       /* -1 unresolved, 0/1 disabled/enabled  */
static uint16_t     a2d_bgen_a;          /* d_background_enable word (0x098a)     */
static uint16_t     a2d_bgen_c;          /* d_background_state  word (0x098c)     */
static uint16_t     a2d_bgen_val;        /* enable value to post     (0x0001)     */
/* Separate value for cell C (0x098c). PROM0 decode:
 *   0xddeb  LD *(0x098a),A ; BC 0xde8a, AEQ   -> 098a == 0 takes the reset branch
 *   0xde86  LD *(0x098c),A ; BC 0xddf5, ANEQ  -> 098c != 0 loops back
 * The two cells have OPPOSITE polarities: reaching 0xde9c needs 098a != 0 AND
 * 098c == 0. Writing 1 into both unlocks the first and locks the second -
 * 466497 iterations measured on 0xde86. Default = a2d_bgen_val so nothing
 * changes without a measurement; set CALYPSO_ARM2DSP_BGEN_VAL_C=0 to test the
 * correct polarity. */
static uint16_t     a2d_bgen_val_c;      /* value for 0x098c (default = val)      */
static uint16_t     a2d_bgen_pollpc;     /* phase-SM poll PC         (0xdddb)     */
static int          a2d_bgen_oneshot = -1; /* 1 = one transition only (default)   */
static int          a2d_bgen_done;       /* one-shot latch                        */
static unsigned     a2d_bgen_posts;      /* how many background-enable posts       */

/* ---- CTRLSYS wire (RANK1): ARM->DSP d_ctrl_system 0x0810 bit15 ----------------
 * The go-live gate at DSP 0xa53c is BITF *(AR1+0x10),0x8000 with AR1=0x0800, i.e.
 * it tests data[0x0810] (d_ctrl_system, dsp_api.h "Control Register RESET/RESUME").
 * bit15 SET  -> a541..a544..0x09bc..0xd247 = bootstrap/operational path (reaches
 *               the FB dispatch we need).
 * bit15 CLEAR-> BC 0xa575 = short-circuit, never bootstrap (current: data[0x0810]=0
 *               -> DSP loops the go-live init -> double IMR, correlator never set up).
 * The real ARM asserts this bit in l1s_reset(), but the emulated ARM->DSP API bridge
 * never propagates it. We model that write HERE (ARM side), not as a DSP-core poke. */
static int          a2d_ctrlsys = -1;    /* -1 unresolved, 0/1 disabled/enabled   */
static uint16_t     a2d_ctrlsys_cell;    /* d_ctrl_system cell      (0x0810)      */
static uint16_t     a2d_ctrlsys_bit;     /* bit to assert           (0x8000)      */
static uint16_t     a2d_ctrlsys_pollpc;  /* DSP PC just before gate (0xa537)      */
static unsigned     a2d_ctrlsys_posts;   /* how many asserts (log cap)            */

static uint16_t a2d_env_u16(const char *name, uint16_t def)
{
    const char *e = calypso_getenv(name);
    if (!e || !*e) {
        return def;
    }
    return (uint16_t)strtoul(e, NULL, 0);
}

/* @BEQUILLE - ARM2DSP (+ _TASKWORD / _TASKBIT)  (CALYPSO_ARM2DSP, atoi>0, default 0)
 *   masque  : the ARM->DSP propagation of dispatcher bit data[0x0fff] bit1 that
 *             the ARM write of d_dsp_page (B_GSM_TASK) should produce through the
 *             shared API RAM.
 *   retirer : when the ARM write of d_dsp_page is really routed into this module
 *             (or when the ROM dispatcher reads the cell the ARM writes).
 *   NB      : calypso_arm2dsp_on_arm_write() has NO caller -> without
 *             CALYPSO_ARM2DSP_CONT, ARM2DSP=1 posts nothing.
 */
static void a2d_resolve(void)
{
    if (a2d_on >= 0) {
        return;
    }
    const char *e = calypso_getenv("CALYPSO_ARM2DSP");
    a2d_on   = (e && atoi(e) > 0) ? 1 : 0;
    a2d_word = a2d_env_u16("CALYPSO_ARM2DSP_TASKWORD", 0x0fff);
    a2d_bit  = a2d_env_u16("CALYPSO_ARM2DSP_TASKBIT", 0x0002);

    /* Background-enable handshake; independent of a2d_on. */
    /* @BEQUILLE - ARM2DSP_BGEN (+ _A / _C / _VAL / _POLLPC / _ONESHOT)
     *              (CALYPSO_ARM2DSP_BGEN, atoi>0 ; :=1 in calypso.env, native,
     *              native_helped, wire ; INDEPENDENT of CALYPSO_ARM2DSP)
     *   masque  : the go-live handshake where the ARM posts d_background_enable
     *             (0x098a) and d_background_state (0x098c). Without it the phase-SM
     *             0xdddb->0xddeb takes the reset branch, 0xde9c is never reached,
     *             d[0x3f70] bit1 stays 0 and the wait-loop 0xa4ca/0xa4d0 spins
     *             forever.
     *   retirer : when the emulated ARM firmware writes 0x098a/0x098c into the API
     *             RAM itself (handshake ported to the ARM side).
     */
    const char *eb = calypso_getenv("CALYPSO_ARM2DSP_BGEN");
    a2d_bgen        = (eb && atoi(eb) > 0) ? 1 : 0;
    a2d_bgen_a      = a2d_env_u16("CALYPSO_ARM2DSP_BGEN_A",      0x098a);
    a2d_bgen_c      = a2d_env_u16("CALYPSO_ARM2DSP_BGEN_C",      0x098c);
    a2d_bgen_val    = a2d_env_u16("CALYPSO_ARM2DSP_BGEN_VAL",    0x0001);
    a2d_bgen_val_c  = a2d_env_u16("CALYPSO_ARM2DSP_BGEN_VAL_C",  a2d_bgen_val);
    a2d_bgen_pollpc = a2d_env_u16("CALYPSO_ARM2DSP_BGEN_POLLPC", 0xdddb);
    const char *eo  = calypso_getenv("CALYPSO_ARM2DSP_BGEN_ONESHOT");
    a2d_bgen_oneshot = (eo && *eo) ? (atoi(eo) > 0 ? 1 : 0) : 1;

    /* CTRLSYS wire (RANK1): model the ARM's l1s_reset() write of d_ctrl_system
     * bit15 so the DSP go-live gate 0xa53c (BITF data[0x0810],#0x8000) falls
     * through to the bootstrap/FB-dispatch path instead of short-circuiting to
     * 0xa575. Cross-validated: minimal correct value is exactly 0x8000. */
    /* @BEQUILLE - ARM2DSP_CTRLSYS (+ _CELL / _VAL / _POLLPC)
     *              (CALYPSO_ARM2DSP_CTRLSYS, atoi>0 ; :=1 under WIRE, :=0 in NATIVE
     *              and NATIVE_HELPED)
     *   masque  : the ARM write of d_ctrl_system (data[0x0810] bit15) that
     *             l1s_reset() performs on real Calypso and that the emulated API
     *             bridge does not propagate. Without it the gate 0xa53c
     *             (BITF #0x8000) short-circuits to 0xa575.
     *   retirer : when the emulated ARM firmware writes 0x0810 through the normal
     *             API path.
     *   WARNING : this writes s->data[] DIRECTLY, so it is invisible to data_write
     *             and hence to CALYPSO_WATCH_0810. Forced on, it triggers
     *             B_TASK_ABORT and breaks the FB return - hence =0 in the native
     *             profiles.
     */
    const char *ec  = calypso_getenv("CALYPSO_ARM2DSP_CTRLSYS");
    a2d_ctrlsys        = (ec && atoi(ec) > 0) ? 1 : 0;
    a2d_ctrlsys_cell   = a2d_env_u16("CALYPSO_ARM2DSP_CTRLSYS_CELL",   0x0810);
    a2d_ctrlsys_bit    = a2d_env_u16("CALYPSO_ARM2DSP_CTRLSYS_VAL",    0x8000);
    a2d_ctrlsys_pollpc = a2d_env_u16("CALYPSO_ARM2DSP_CTRLSYS_POLLPC", 0xa537);

    if (a2d_on) {
        fprintf(stderr,
                "[arm2dsp] enabled (faithful task-post): d_dsp_page(0x%04x) bit1 "
                "-> set data[0x%04x] |= 0x%04x (DSP dispatches via ROM)\n",
                A2D_DSP_PAGE_OFF, a2d_word, a2d_bit);
    }
    if (a2d_bgen) {
        fprintf(stderr,
                "[arm2dsp] BGEN enabled (Fix A): on ARM task-cmd post "
                "data[0x%04x]=0x%04x data[0x%04x]=0x%04x @DSP-PC=0x%04x (oneshot=%d) "
                "-> DSP phase-SM reaches 0xde9c, raises d[0x3f70] bit1\n",
                a2d_bgen_a, a2d_bgen_val, a2d_bgen_c, a2d_bgen_val_c, a2d_bgen_pollpc,
                a2d_bgen_oneshot);
    }
    if (a2d_ctrlsys) {
        fprintf(stderr,
                "[arm2dsp] CTRLSYS enabled (RANK1): assert data[0x%04x] |= 0x%04x "
                "@DSP-PC=0x%04x -> go-live gate 0xa53c falls through to bootstrap/FB\n",
                a2d_ctrlsys_cell, a2d_ctrlsys_bit, a2d_ctrlsys_pollpc);
    }
}

/* ARM->DSP API-RAM write path (calypso_trx.c). offset = ARM byte offset into the
 * DSP API window; value = 16-bit written value. */
void calypso_arm2dsp_on_arm_write(uint16_t offset, uint16_t value)
{
    a2d_resolve();
    if (!a2d_on) {
        return;
    }
    /* The ARM commands the FB task by writing d_dsp_page bit1 (B_GSM_TASK).
     * Each such write re-posts the task (the ARM re-issues it per frame). */
    if (offset == A2D_DSP_PAGE_OFF && (value & A2D_B_GSM_TASK)) {
        a2d_pending = 1;
    }
    /* L1CTL_RESET_REQ FULL (mobile Ctrl-C): the firmware runs l1s_reset_hw(),
     * which writes d_dsp_page = 0 (sync.c:168). That zero is the unique L1-reset
     * signal - in operation the word is always B_GSM_TASK|w_page, never 0. Re-arm
     * the BGEN go-live so the DSP produces FBSB/SI again after the mobile
     * restarts; without it nothing comes back. */
    if (offset == A2D_DSP_PAGE_OFF && value == 0) {
        a2d_bgen_done = 0;
    }
}

/* Once per DSP instruction step (calypso_c54x.c). When the ARM has posted the
 * task, set the DSP task-ready bit so the DSP's own dispatcher runs it. */
void calypso_arm2dsp_on_dsp_step(C54xState *s, uint16_t exec_pc)
{
    a2d_resolve();
    if (!a2d_on && !a2d_bgen && !a2d_ctrlsys) {
        return;
    }

    /* ---- CTRLSYS: assert d_ctrl_system bit15 at the go-live gate --------------
     * When the DSP reaches the instruction just before the 0xa53c BITF gate, make
     * sure data[0x0810] bit15 is set so BITF sets TC and the DSP falls through to
     * the bootstrap/FB-dispatch path (else BC NTC 0xa575 short-circuits). Modeled
     * as the ARM's write; re-asserted on each pass (it persists, since nothing
     * clears it, but this stays correct if the DSP ever does). */
    if (a2d_ctrlsys && exec_pc == a2d_ctrlsys_pollpc &&
        !(s->data[a2d_ctrlsys_cell] & a2d_ctrlsys_bit)) {
        s->data[a2d_ctrlsys_cell] |= a2d_ctrlsys_bit;
        if (a2d_ctrlsys_cell >= A2D_API_BASE && s->api_ram) {
            s->api_ram[a2d_ctrlsys_cell - A2D_API_BASE] |= a2d_ctrlsys_bit;
        }
        if (a2d_ctrlsys_posts++ < 8) {
            fprintf(stderr,
                    "[arm2dsp] CTRLSYS: data[0x%04x] |= 0x%04x (go-live gate "
                    "0xa53c bit15 SET -> bootstrap path) @DSP-PC=0x%04x insn=%u\n",
                    a2d_ctrlsys_cell, a2d_ctrlsys_bit, exec_pc, s->insn_count);
        }
    }

    /* ---- BGEN: background-enable handshake ------------------------------------
     * Post d_background_enable/state the moment the DSP phase-SM is about to poll
     * them (exec_pc == pollpc), gated on the ARM having commanded the task (the
     * dispatcher bit is set in the task-ready word). One-shot by default: a SINGLE
     * go-live transition, not a per-frame re-fire. The latch is cleared on L1 reset
     * (see on_arm_write): a full L1CTL_RESET_REQ re-clears 0x098a/0x098c, and
     * without a re-post the DSP L1S stays stale - no FBSB completion, sync
     * timeout. */
    if (a2d_bgen && exec_pc == a2d_bgen_pollpc &&
        (!a2d_bgen_oneshot || !a2d_bgen_done)) {
        uint16_t taskw = (a2d_word >= A2D_API_BASE && s->api_ram)
                         ? s->api_ram[a2d_word - A2D_API_BASE]
                         : s->data[a2d_word];
        int armed = (s->data[a2d_word] & a2d_bit) || (taskw & a2d_bit);
        if (armed) {
            s->data[a2d_bgen_a] = a2d_bgen_val;
            s->data[a2d_bgen_c] = a2d_bgen_val_c;   /* opposite polarity - see header */
            if (a2d_bgen_a >= A2D_API_BASE && s->api_ram) {
                s->api_ram[a2d_bgen_a - A2D_API_BASE] = a2d_bgen_val;
            }
            /* Both mirrors must carry val_c: inside the API window (>= 0x0800)
             * the DSP reads api_ram, not data[] (calypso_c54x.c:2194). Mirroring
             * val here instead of val_c makes CALYPSO_ARM2DSP_BGEN_VAL_C=0
             * ineffective - the cell the DSP reads stays 1 and 0xde86 loops
             * forever. [2026-08-03] profile native_twl: 100000 hits at pc=0xde86
             * with d[0x098c]=0x0001, phase-SM never reaching 0xde9c. */
            if (a2d_bgen_c >= A2D_API_BASE && s->api_ram) {
                s->api_ram[a2d_bgen_c - A2D_API_BASE] = a2d_bgen_val_c;
            }
            a2d_bgen_done = 1;
            a2d_bgen_posts++;
            if (a2d_bgen_posts <= 8) {
                fprintf(stderr,
                        "[arm2dsp] BGEN post #%u: data[0x%04x]=0x%04x "
                        "data[0x%04x]=0x%04x @DSP-PC=0x%04x d[0x3f70]=0x%04x insn=%u\n",
                        a2d_bgen_posts, a2d_bgen_a, a2d_bgen_val, a2d_bgen_c, a2d_bgen_val_c,
                        exec_pc, s->data[0x3f70], s->insn_count);
            }
        }
    }

    if (!a2d_on) {
        return;
    }
    /* @BEQUILLE - ARM2DSP_CONT  (CALYPSO_ARM2DSP_CONT, EXISTS idiom -> "=0" ENABLES
     *              it, only unset turns it off)
     *   masque  : the missing per-frame re-post. The ROM dispatcher clears the task
     *             bit between passes; with no live ARM path, CONT re-reads d_dsp_page
     *             from the API RAM on every DSP step and re-posts the bit. It is the
     *             ONLY path through which ARM2DSP=1 has any effect.
     *   retirer : as soon as on_arm_write() is called (a2d_pending becomes the
     *             trigger again).
     */
    if (a2d_cont < 0) a2d_cont = calypso_gate("CALYPSO_ARM2DSP_CONT", 0);
    /* CONT mode: re-post on every step while the ARM's B_GSM_TASK is asserted in
     * DSP memory (d_dsp_page word 0x08D4 bit1), so the task-ready bit is set when
     * the dispatcher checks it (b424) despite b419 clearing it. Non-CONT: one post
     * per ARM write (a2d_pending). */
    if (a2d_cont) {
        uint16_t page = s->api_ram ? s->api_ram[0x08D4 - A2D_API_BASE]
                                   : s->data[0x08D4];
        if (!(page & A2D_B_GSM_TASK)) {
            return;
        }
    } else if (!a2d_pending) {
        return;
    }
    a2d_pending = 0;
    a2d_posts++;

    uint16_t before = s->data[a2d_word];
    s->data[a2d_word] |= a2d_bit;
    /* words >= 0x0800 are read by the DSP from api_ram (shared DARAM/API RAM) */
    if (a2d_word >= A2D_API_BASE && s->api_ram) {
        s->api_ram[a2d_word - A2D_API_BASE] |= a2d_bit;
    }

    if (a2d_posts <= 10 || (a2d_posts % 20000) == 0) {
        fprintf(stderr,
                "[arm2dsp] task-post #%u: data[0x%04x] 0x%04x->0x%04x "
                "@DSP-PC=0x%04x insn=%u\n",
                a2d_posts, a2d_word, before, s->data[a2d_word],
                exec_pc, s->insn_count);
    }
}

4.50 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_arm2dsp.h

854 octets, 21 lignes → 21 lignes

#ifndef CALYPSO_ARM2DSP_H
#define CALYPSO_ARM2DSP_H

#include "calypso_c54x.h"

/*
 * calypso_arm2dsp - ARM->DSP task-post bridge. See calypso_arm2dsp.c for the
 * mechanism, the crutch annotations and the full env-variable list.
 */

/* Posts B_GSM_TASK when the ARM writes d_dsp_page in the shared API window.
 * offset = ARM byte offset into the DSP API window; value = the 16-bit value
 * written. Currently has NO caller: the ARM API-RAM write path does not route
 * through this module, so only the continuous-post path does anything. */
void calypso_arm2dsp_on_arm_write(uint16_t offset, uint16_t value);

/* Called once per DSP instruction step from calypso_c54x.c with the DSP state
 * and the PC about to execute. Applies a pending task post. */
void calypso_arm2dsp_on_dsp_step(C54xState *s, uint16_t exec_pc);

#endif /* CALYPSO_ARM2DSP_H */

4.51 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_bsp.c

260166 octets, 4998 lignes → 4989 lignes (2 groupes compactés)

/*
 * Calypso BSP/RIF DMA module — implementation.
 *
 * On real hardware the BSP (Baseband Serial Port) is a synchronous serial
 * link that DMA-feeds I/Q samples from the IOTA RF frontend into C54x
 * DSP DARAM. The DSP code (FB/SB/burst detection in PROM0) reads them
 * from a fixed DARAM buffer and posts results into the NDB.
 *
 * In QEMU, DL bursts arrive via UDP (TRXDv0 from calypso-ipc-device on port 5702).
 * This module owns that socket, decodes the TRXDv0 header, converts hard
 * bits to I/Q samples, and DMA-writes them into DSP DARAM.
 *
 * L1CTL control (DLCI 5) goes through the UART — bursts never touch UART.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#include "qemu/osdep.h"
#include <math.h>
#include "qemu/main-loop.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>   /* inet_aton */
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>   /* calypso_reglage */
#include <poll.h>
#include "qemu/timer.h"
#include "calypso_bsp.h"
#include "calypso_rhea_dma.h"
#include "calypso_rif.h"
#include "calypso_c54x.h"
#include "hw/arm/calypso/calypso_iota.h"
#include "hw/arm/calypso/calypso_invariants.h"
#include "calypso_twl3025.h"
#include "hw/arm/calypso/calypso_trx.h"
#include "calypso_tint0.h"  /* GSM_HYPERFRAME */
#include "calypso_full_pcb.h"  /* DARAM lock helpers */
#include "hw/arm/calypso/calypso_trf6151.h"   /* RF calibration behind the a_pm model (calypso_bsp_rssi_apm) */

int calypso_rxfb_fired = 0;   /* set to 1 once RX-FBFLAGS sets data[0x3fad] bit15 */

/* Forward decls for env-gated helpers pre-warmed in calypso_bsp_init(). */
static uint32_t d_rach_word_offset(void);
static int rach_force_bsic(void);

#include "hw/arm/calypso/calypso_debug.h"
#include "calypso_gmsk.h"

/* calypso_api.h declares it but pulls in all the QEMU state: bare prototype. */
extern uint32_t calypso_trx_get_fn(void);

/* DARAM write stamp, published for the c54x memory dump. */
unsigned calypso_daram_last_fn;
/* fn of the last burst actually handed to the BSP, to measure the gap between
 * the DEPOSIT and the EVALUATION by the DSP task (DECALAGE probe). */
unsigned g_depot_fn, g_depot_seq;
unsigned calypso_daram_wr_count;
#define BSP_LOG(fmt, ...) \
    do { if (calypso_debug_enabled("BSP")) \
        fprintf(stderr, "[BSP] " fmt "\n", ##__VA_ARGS__); } while (0)

#define BSP_TRXD_PORT  6702   /* bridge forwards DL bursts here (5702 is bridge's own) */

/* ==========================================================================
 * DSP I/Q CHAIN
 *
 * Both mechanisms below derive from the REAL DL signal the BSP receives.
 *
 *  1. FB-STREAM — ring of decimated FCCH I/Q samples, served to the native
 *     correlator by the data[0x9213]/[0x9215] read intercept in calypso_c54x.c.
 *     This is the correlator INPUT in native mode (CALYPSO_FB_STREAM=1). It
 *     stays a crutch as long as the BSP -> DARAM chain does not feed the
 *     buffer on its own.
 *
 *  2. MAV / a_pm — mean magnitude of the DL burst, from which a_pm derives.
 *     On real Calypso the PM task does not compute a_pm from samples: the DSP
 *     zero-fills the result page and an integrator reads an ABB/RF power
 *     register. That register is not part of the modelled ADC, so we model it
 *     from the true DL magnitude. The MAV_REF -> RF_REF anchor is frontend
 *     calibration (like the trf6151 gain), not a decreed constant: two
 *     different signals give two different a_pm.
 * ========================================================================== */
#define BSP_FBS_RING 16384              /* power of two */
static int16_t  bsp_fbs[BSP_FBS_RING];
static uint32_t bsp_fbs_wr, bsp_fbs_rd;
static uint16_t bsp_last_mav;           /* MAV(|I|+|Q|) of the last DL burst */

/* Feed the FB-STREAM ring and the magnitude measurement from a raw DL burst
 * (cs16, `n` interleaved I,Q int16). Called on burst reception. */
static void bsp_iq_publish(const int16_t *iq, int n)
{
    if (!iq || n <= 0) {
        return;
    }

    /* Burst MAV -> a_pm (header §2). No sqrt: mean of |I|+|Q|. */
    {
        uint64_t acc = 0;
        for (int i = 0; i < n; i++) {
            int v = iq[i];
            acc += (v < 0) ? (uint32_t)(-v) : (uint32_t)v;
        }
        uint32_t mav = (uint32_t)(acc / (uint32_t)n);
        /* A silent burst does not measure the cell. Clears (bsp_ts0_service,
         * frame without burst) and the zero fills of empty slots also come
         * through here; letting them drive the MAV to ~0 would push the next
         * a_pm to the floor (-100 dBm, which the firmware reports as
         * "rxlev <=-110 (0)"), i.e. C1 = 0 and the cell deemed unusable
         * (LOST_COVERAGE every 5 to 7 s). The last measurement of a real
         * burst is therefore kept. CALYPSO_BSP_MAV_MIN=0 restores the
         * unconditional update. */
        static int mav_min = -1;
        if (mav_min < 0) {
            const char *e = calypso_getenv("CALYPSO_BSP_MAV_MIN");
            mav_min = e ? atoi(e) : 64;
        }
        if (mav >= (uint32_t)mav_min) {
            bsp_last_mav = (mav > 0xffff) ? 0xffff : (uint16_t)mav;
        }
    }

    /* FB-STREAM ring (header §1). */
    {
        static int on = -1, decim = 4;
        if (on < 0) {
            const char *e = calypso_getenv("CALYPSO_FB_STREAM");
            on = (e && atoi(e) > 0) ? 1 : 0;
            const char *d = calypso_getenv("CALYPSO_FB_STREAM_DECIM");
            if (d && *d) {
                decim = atoi(d);
            }
            if (decim < 1) {
                decim = 1;
            }
        }
        if (!on) {
            return;
        }
        /* Skip all-zero frames (fn 0..4 at startup): they pollute the ring the
         * demodulator reads at the leading edge, which would then hit zeros
         * instead of the real FCCH pushed right after. */
        int nonzero = 0;
        for (int i = 0; i < n && i < 64; i++) {
            if (iq[i]) {
                nonzero = 1;
                break;
            }
        }
        if (!nonzero) {
            return;
        }
        for (int k = 0; 2 * (k * decim) + 1 < n; k++) {
            bsp_fbs[bsp_fbs_wr++ & (BSP_FBS_RING - 1)] = iq[2 * (k * decim)];
            bsp_fbs[bsp_fbs_wr++ & (BSP_FBS_RING - 1)] = iq[2 * (k * decim) + 1];
        }
    }
}

bool calypso_bsp_fb_stream_next(uint16_t *outI, uint16_t *outQ)
{
    if (bsp_fbs_rd + 1 >= bsp_fbs_wr) {
        return false;
    }
    *outI = (uint16_t)bsp_fbs[bsp_fbs_rd++ & (BSP_FBS_RING - 1)];
    *outQ = (uint16_t)bsp_fbs[bsp_fbs_rd++ & (BSP_FBS_RING - 1)];
    return true;
}

/* One measurement per window. In idle mode the TI L1 makes several measurements in the
 * same frame (d_task_md = 0x200 + n: the cell ARFCN 1 and the neighbour 277 = DCS 614); if
 * apm_jamais_nul (pont.c) filled them all with the MAV of the LAST delivered packet, the
 * neighbour window (fake-air noise) would come last and drag the cell to the floor with it.
 * The MAV of each measurement window (one-shot, < 150 samples) is therefore kept in frame
 * order; the k-th PM takes the k-th one. */
#define BSP_PM_MAV_N 8
static int faux_air_gain(uint32_t tick, int limite);   /* below, with faux_air_arfcn */
static uint32_t g_tick_courant;        /* (early definition, see bsp_ts0_service) */
static uint16_t g_pm_mav[BSP_PM_MAV_N], g_pm_mav_der[BSP_PM_MAV_N];
static int g_pm_mav_n, g_pm_mav_der_n;
static uint32_t g_pm_mav_tick;
#define BSP_PM_FIFO_N 16
static uint16_t g_pm_mav_cellule = 14000;
static unsigned g_pm_dette;
/* The PM the DSP writes. c54x_mem.c replaces every DSP write into a_pm (page R,
 * 0x834-836 / 0x848-84A) with calypso_bsp_rssi_apm(). bsp_last_mav is only fed by the UDP
 * path (bsp_iq_publish); with pont.c it would stay on noise, so the value comes from the
 * LAST delivered measurement window (the DSP writes its PM right after processing it),
 * with the gain programmed for that window. */
static uint16_t g_pm_der_mav; static int g_pm_der_gain = -999, g_pm_der_ok;                   /* predicted PMs: their windows, delivered later, do not go in the queue */     /* MAV of a cell burst (see calypso_bsp_rssi_apm_suivante) */
static int faux_air_arfcn(uint32_t tick, int limite);
static int faux_air_cellule(void);
static uint16_t g_pm_fifo[BSP_PM_FIFO_N];
static int16_t g_pm_fifo_gain[BSP_PM_FIFO_N];
static uint32_t g_pm_fifo_tick[BSP_PM_FIFO_N];  /* delivery tick: past 3 frames the entry is stale */   /* RX gain of the window (faux_air_gain), -999 unknown */
static uint32_t g_pm_fifo_w, g_pm_fifo_r;
/* Live trace: `touch /dev/shm/apm_trace` -> every frame (pont.c [apmT]) and every delivery
 * in a measurement window ([apmL]) are traced, 4000 lines at most; `rm` to stop. Shows WHEN
 * the DSP writes a_pm / d_task_md and when the MAV queue serves them. */
int calypso_apm_trace_on(void)
{
    static unsigned n_appels, n_lignes; static int on;
    if (n_appels++ % 108 == 0) on = (access("/dev/shm/apm_trace", F_OK) == 0);
    if (!on || n_lignes >= 4000) return 0;
    n_lignes++;
    return 1;
}
/* Rank of the measurement window in the frame. In idle mode the TI L1 measures two
 * frequencies per frame (IBA_I pairs (1, 277)): the 1st window uses the synthesizer
 * programming done at the END of the previous frame (qbit 4851..4983), the 2nd the one at
 * the START of the frame (qbit 323/671), e.g. frame N: 323 -> 1, 4861 -> 614. Looking up
 * "last programming before qbit 5000" would return the same for both, so the lookup is
 * bounded by the window rank. */
static int pm_rang_fenetre(void)
{
    return g_pm_mav_tick == g_tick_courant ? g_pm_mav_n : 0;
}
static int pm_limite_qbit(int rang)
{
    return rang == 0 ? -1 : 2500;    /* -1: previous frames only */
}
static void bsp_pm_mav_noter(const int16_t *iq, int n, double k_air)
{
    if (g_pm_mav_tick != g_tick_courant) {
        if (g_pm_mav_n > 0) { memcpy(g_pm_mav_der, g_pm_mav, sizeof g_pm_mav); g_pm_mav_der_n = g_pm_mav_n; }
        g_pm_mav_tick = g_tick_courant; g_pm_mav_n = 0;
    }
    if (n <= 0) return;
    int lim = pm_limite_qbit(g_pm_mav_n);      /* rank of this window in the frame (before adding) */
    uint64_t acc = 0;
    for (int i = 0; i < n; i++) { int v = iq[i]; acc += (v < 0) ? (uint32_t)(-v) : (uint32_t)v; }
    uint32_t mav = (uint32_t)(acc / (uint32_t)n * k_air);
    uint16_t m16 = mav > 0xffff ? 0xffff : (uint16_t)mav;
    if (m16 >= 5000) g_pm_mav_cellule = m16;   /* last real burst seen in a measurement window */
    g_pm_der_mav = m16; g_pm_der_gain = faux_air_gain(g_tick_courant, lim); g_pm_der_ok = 1;
    if (calypso_apm_trace_on())
        printf("  [apmL] tick=%u fenetre=%d rang=%d mav=%u arfcn=%d gain=%d file=%u\n", g_tick_courant,
               calypso_rhea_dma_get_len_words() / 2, g_pm_mav_n - 1, m16, faux_air_arfcn(g_tick_courant, lim),
               faux_air_gain(g_tick_courant, lim), g_pm_fifo_w - g_pm_fifo_r);
    if (g_pm_mav_n < BSP_PM_MAV_N) g_pm_mav[g_pm_mav_n++] = m16;
    /* Event queue. In idle mode the pump serves the measurement windows AFTER the DONE
     * (PONT_DONE_TOT): the 0 the DSP writes for this PM is only seen by apm_jamais_nul at
     * the next DONE, so a per-frame association would hand it the MAV of the windows of
     * THAT frame (neighbour noise). Each delivered window pushes its MAV; each null PM
     * found in an R page pops the oldest. */
    g_pm_fifo_gain[g_pm_fifo_w % BSP_PM_FIFO_N] = (int16_t)faux_air_gain(g_tick_courant, lim);
    g_pm_fifo_tick[g_pm_fifo_w % BSP_PM_FIFO_N] = g_tick_courant;
    g_pm_fifo[g_pm_fifo_w++ % BSP_PM_FIFO_N] = m16;
    if (g_pm_fifo_w - g_pm_fifo_r > BSP_PM_FIFO_N) g_pm_fifo_r = g_pm_fifo_w - BSP_PM_FIFO_N;
}
static uint16_t bsp_apm_de_mav(double mav, int gain_db);
static int air_niveaux_on(void);
/* AGC of the realistic air (CALYPSO_AIR_AGC=1, default): the level of a cell burst (dBm)
 * does not change its I/Q amplitude -- a real receiver brings it back to full scale through
 * its AGC, and at amplitude 3000 the DSP does not decode the BCCH. The level only affects
 * the power MEASUREMENT: air_choisir() stores the amplitude factor by (fn, TN) and
 * calypso_bsp_rx_burst() applies it to the MAV of the measurement window
 * (bsp_pm_mav_noter). */
#define AIR_PM_RING 4096u
static struct { uint32_t fn; float k; } g_air_pm[AIR_PM_RING][8];
static double air_pm_k(uint32_t fn, unsigned tn)
{
    const typeof(g_air_pm[0][0]) *e = &g_air_pm[fn % AIR_PM_RING][tn & 7];
    return (e->fn == fn && e->k > 0) ? e->k : 1.0;
}
static double g_air_k_der = 1.0;   /* factor of the last burst delivered to the DSP (calypso_bsp_rx_burst) */
static int g_air_dpm = -1;          /* model D_PM for this burst (same scale as a_pm), -1 unknown */
static int faux_air_gain(uint32_t tick, int limite);
/* D_PM (a_serv_demod[1], 1/64 dB at baseband, layer1/prim_rx_nb.c:
 * agc_inp_dbm8_by_pm(D_PM >> 3)): the DSP computes it on full-scale I/Q; the rssi app
 * derives the level of the sync view (rx_level) from it. */
uint16_t calypso_bsp_air_dpm(uint16_t val)
{
    /* The DSP value is not used: on full-scale I/Q it saturates (rxlev 63, -47 dBm shown)
     * and a plain 20 log k offset is off by more than 10 dB. The a_pm model
     * (bsp_apm_de_mav, programmed gain of the window) gives the same scale as the
     * spectrum view. */
    if (!air_niveaux_on() || g_air_dpm < 0 || val == 0) return val;
    return (uint16_t)g_air_dpm;
}
static double air_env_dbm(const char *nom, double defaut);
/* [apm] probe (pont.c): the per-window MAVs of the current and previous frame */
int calypso_bsp_pm_mav_texte(char *buf, size_t n)
{
    int o = snprintf(buf, n, "tick %u:", g_pm_mav_tick);
    for (int k = 0; k < g_pm_mav_n && o < (int)n; k++) o += snprintf(buf + o, n - o, " %u", g_pm_mav[k]);
    if (o < (int)n) o += snprintf(buf + o, n - o, " | avant:");
    for (int k = 0; k < g_pm_mav_der_n && o < (int)n; k++) o += snprintf(buf + o, n - o, " %u", g_pm_mav_der[k]);
    if (o < (int)n) o += snprintf(buf + o, n - o, " | dernier=%u courant=%u", bsp_last_mav, g_tick_courant);
    return o;
}
/* did the current frame deliver a real burst (MAV >= 5000) in a measurement window? ([apm] probe) */
int calypso_bsp_pm_mav_fort(void)
{
    if (g_pm_mav_tick != g_tick_courant) return 0;
    for (int k = 0; k < g_pm_mav_n; k++) if (g_pm_mav[k] >= 5000) return 1;
    return 0;
}
/* the next null PM (pont.c, apm_jamais_nul): the MAV of the oldest unconsumed measurement window */
uint16_t calypso_bsp_rssi_apm_suivante(int *depile_mav)
{
    while (g_pm_fifo_r != g_pm_fifo_w && (uint32_t)(g_tick_courant - g_pm_fifo_tick[g_pm_fifo_r % BSP_PM_FIFO_N]) > 3)
        g_pm_fifo_r++;                       /* window never measured: not kept for the next one */
    if (g_pm_fifo_r != g_pm_fifo_w) {
        int g = g_pm_fifo_gain[g_pm_fifo_r % BSP_PM_FIFO_N];
        uint16_t m = g_pm_fifo[g_pm_fifo_r++ % BSP_PM_FIFO_N];
        if (depile_mav) *depile_mav = m;
        if (calypso_apm_trace_on())
            printf("  [apmS] tick=%u PM servie : mav=%u gain=%d (livree au tick %u), reste %u en file\n", g_tick_courant,
                   m, g, g_pm_fifo_tick[(g_pm_fifo_r - 1) % BSP_PM_FIFO_N], g_pm_fifo_w - g_pm_fifo_r);
        return bsp_apm_de_mav(m, g);
    }
    if (depile_mav) *depile_mav = -1;
    return calypso_bsp_rssi_apm();
}
uint16_t calypso_bsp_rssi_apm_k(int k)
{
    if (g_pm_mav_tick == g_tick_courant && k < g_pm_mav_n) return bsp_apm_de_mav(g_pm_mav[k], -999);
    if (g_pm_mav_tick != g_tick_courant || g_pm_mav_n == 0) {
        if (k < g_pm_mav_der_n) return bsp_apm_de_mav(g_pm_mav_der[k], -999);
    }
    return calypso_bsp_rssi_apm();
}

uint16_t calypso_bsp_rssi_apm(void)
{
    if (g_pm_der_ok) return bsp_apm_de_mav((double)g_pm_der_mav, g_pm_der_gain);
    return bsp_apm_de_mav((double)bsp_last_mav, -999);
}

static uint16_t bsp_apm_de_mav(double mav, int gain_db)
{
    static double mav_ref = 20929.0, rf_ref = -60.0;
    static int init;
    if (!init) {
        init = 1;
        const char *mr = calypso_getenv("CALYPSO_DECAN_PM_MAV_REF");
        if (mr && *mr) {
            mav_ref = atof(mr);
        }
        const char *rr = calypso_getenv("CALYPSO_DECAN_PM_RF_REF");
        if (rr && *rr) {
            rf_ref = atof(rr);
        } else if (air_niveaux_on()) {
            /* Realistic air: a single scale. mav_ref is the MAV of a burst modulated at 30000,
             * which the realistic air places at CALYPSO_AIR_REF_DBM (-40); the reference level
             * must be the same here, otherwise a cell played at -60 dBm comes out at a
             * different level in the model and in the rssi app. */
            rf_ref = air_env_dbm("CALYPSO_AIR_REF_DBM", -40.0);
            /* Measured MAV of a GMSK burst modulated at 30000 (mean of |I|,|Q|: 1330..1400 at
             * amplitude 3000); 20929 would leave the cell 4 dB under its setpoint. */
            if (!(mr && *mr)) mav_ref = 13700.0;
        }
        if (mav_ref < 1.0) {
            mav_ref = 1.0;
        }
    }
    if (mav < 1.0) {
        mav = 1.0;
    }
    double rf = 20.0 * log10(mav / mav_ref) + rf_ref;
    if (rf < -100.0) {
        rf = -100.0;   /* floor: track a weak signal without rejecting the cell */
    }
    if (rf > -30.0) {
        rf = -30.0;
    }
    int rfi = (int)(rf >= 0 ? rf + 0.5 : rf - 0.5);
    if (gain_db > -999) {
        /* same formula as calypso_trf6151_apm_for_rf, with the gain the L1 programmed for the window */
        int bb = rfi + gain_db, apm;
        if (bb < 0) bb = 0;
        apm = bb * 64;
        if (apm < 64) apm = 64;
        if (apm > 0xFFFF) apm = 0xFFFF;
        return (uint16_t)apm;
    }
    return calypso_trf6151_apm_for_rf(rfi);
}

/* Per-TN burst queue: FN-indexed ring so lookahead bursts from the BTS
 * (osmo-bts-trx schedules up to ~92 frames ahead) are preserved until the
 * QEMU virtual FN catches up to each burst's scheduled FN. On real hardware
 * BSP DMA is synchronous within the TDMA frame; in QEMU bursts arrive over
 * UDP from the bridge with their scheduled FN embedded in the TRXD header,
 * and delivery must happen at the exact virtual FN or the DSP correlates
 * samples against a frame boundary that does not match the modulator phase
 * (→ d_fb_det stays 0 indefinitely). */
#define BSP_NUM_TN     8                 /* one queue per timeslot */
/* [2026-09-20] 128 -> 8192. With a real BTS the bursts arrive at 217 frames/s
 * while the emulated DSP consumes ~100/s; at 128 slots the queue overflowed
 * every second and dropped the OLDEST bursts, so the stream the ROM saw had
 * holes: FCCH spacings of 4, 26, 18 frames instead of 10 and the SCH never 1
 * frame after the FCCH it predicted (measured: 88 SB attempts, 0 decoded).
 * 8192 per timeslot holds ~70 s of the deficit; the stream stays coherent,
 * only late, which is what acquisition needs (the SB sets the firmware time
 * in stream time). 8 x 8192 x 780 B = 51 MB of BSS. */
#define BSP_QUEUE_LEN  8192              /* lookahead depth per TN */
/* Match window: the real BSP captures samples around BDLENA, so an exact FN
 * match is a QEMU artefact. 64 frames covers the BTS scheduler lookahead
 * (measured delta 1..139, mean ~50; a window of 4 left 99 % of bursts stale
 * before the virtual FN caught up) and stays narrow enough not to swap an FCCH
 * for a non-FCCH in the 51-multiframe pattern (GSM 45.002: FCCH every 10
 * frames on the BCCH slot). */
#define BSP_FN_MATCH_WINDOW  64

/* === DARAM write-by-range instrumentation ===
 *
 * Buckets:
 *   low    : DARAM zone read by AR3 with stride +19 in the FB-det correlator
 *   target : the runtime daram_addr window
 *   wrap   : circular wrap zone AR2/AR7, BK=176, stride -19
 *   other  : anywhere else, including the daram_len=296 overflow
 *
 * Emitted as `[BSP] DARAM-WR-STATS ...` every BSP_DARAM_WR_LOG_EVERY writes.
 * low=target=wrap=0 means the BSP DMA was never armed; target>>0 with low=0
 * means the BSP writes but the env override was ignored. */
#define BSP_BUCKET_LOW_LO     0x0000
#define BSP_BUCKET_LOW_HI     0x03A3
#define BSP_BUCKET_TARGET_LO  0x3FB0
#define BSP_BUCKET_TARGET_HI  0x3FFF
#define BSP_BUCKET_WRAP_LO    0xFC5D
#define BSP_BUCKET_WRAP_HI    0xFFED
#define BSP_DARAM_WR_LOG_EVERY 1000


/* Size of the I/Q buffer handed to the DSP, in int16 (2 per sample).
 *
 * 148 is a GSM burst in BITS (3+57+1+26+1+57+3), i.e. exactly one burst at
 * 1 SPS with no search margin. What the DSP actually CONSUMES (Osmocom wiki
 * HardwareCalypsoDSP, task by task):
 *   RX NB : 150 I/Q samples, 10-bit TSC window from r68, correlation over
 *           16 bits (TSC[10..25])                  -> 300 int16
 *   SB    : 190 I/Q samples, 50-bit window from r39, correlation over the
 *           full 64 bits                            -> 380 int16
 * So the DSP reads PAST a 296-int16 deposit: 2 samples too far for an NB, 42
 * for an SB, landing on stale neighbouring DARAM right inside the SB
 * correlation window.
 *
 * Sized for the worst case (SB, 190) plus margin. The runtime default stays
 * 296, so behaviour is unchanged; this only makes CALYPSO_BSP_DARAM_LEN=380
 * reachable past the old `n > 296` guard. */
#define BSP_IQ_MAX_I16   384   /* 192 I/Q samples; SB asks for 190 */

typedef struct {
    int16_t  iq[BSP_IQ_MAX_I16];
    int      n;        /* number of int16 values */
    uint32_t fn;
    bool     valid;
} BspBurstSlot;

typedef struct {
    BspBurstSlot  slot[BSP_QUEUE_LEN];
} BspBurstQueue;

static struct {
    C54xState *dsp;
    uint16_t   daram_addr;
    uint16_t   daram_len;
    uint64_t   bursts_seen;
    uint64_t   bursts_written;
    uint64_t   bursts_dropped_no_window;
    uint64_t   bursts_dropped_queue_full;
    uint64_t   bursts_dropped_stale;
    uint8_t    inject_canary;     /* CALYPSO_BSP_INJECT_CANARY=1: overwrite
                                      samples with 0xCAFE to identify the
                                      target buffer through the read trace */
    uint8_t    bypass_bdlena;      /* CALYPSO_BSP_BYPASS_BDLENA=1: deliver
                                      every burst without waiting for the
                                      BDLENA window — debug-only env-gated
                                      hack to probe the target DARAM addr */
    int        trxd_fd;            /* UDP socket for TRXDv0 DL bursts */
    struct sockaddr_in trxd_peer;  /* BTS address (for UL replies) */
    bool       trxd_peer_valid;
    uint8_t    last_att;           /* last DL attenuation byte */

    /* FN-indexed queue per TN */
    BspBurstQueue  q[BSP_NUM_TN];

    /* DARAM write-by-range counters (see BSP_BUCKET_*). */
    uint64_t   wr_low;
    uint64_t   wr_target;
    uint64_t   wr_wrap;
    uint64_t   wr_other;
    uint64_t   wr_total;
    uint64_t   wr_last_logged;

    /* Drain timer: decouples BSP->DSP DMA delivery from tdma_tick, which can
     * be slow under icount=auto. Armed on QEMU_CLOCK_REALTIME; see
     * bsp_drain_cb for why that clock and not VIRTUAL. */
    QEMUTimer *drain_timer;
} bsp;

#define BSP_DRAIN_PERIOD_MS  5

/* === Deterministic replay ===
 * With CALYPSO_BSP_REPLAY_FILE set, the BSP loads a burst dump
 * (BSP_DUMP_RX_FILE format) and injects it on QEMU_CLOCK_VIRTUAL at a fixed
 * rate INSTEAD of listening on the UDP socket, making the source fully
 * deterministic. Capture with BSP_DUMP_RX_FILE, then replay the same file.
 *
 * Rate: one burst every 576 us of virtual time = one GSM TN slot
 * (8 slots x 217 frames/s = ~1736 bursts/s), close enough to the TDMA rhythm. */
typedef struct ReplayBurst {
    uint32_t fn;
    uint8_t  tn;
    uint16_t n;
    int16_t  iq[BSP_IQ_MAX_I16];
} ReplayBurst;

static ReplayBurst *replay_bursts = NULL;
static size_t       replay_count  = 0;
static size_t       replay_idx    = 0;
static QEMUTimer   *replay_timer  = NULL;
#define BSP_REPLAY_PERIOD_NS  (576ULL * 1000ULL)  /* 576us per TN slot */
/* Nanosecond period, to pair with timer_new_ns / qemu_clock_get_ns. */
#define BSP_DRAIN_PERIOD_NS  (BSP_DRAIN_PERIOD_MS * 1000000ULL)

/* Bump the bucket matching `addr`, then emit a stats line periodically.
 * Called on every BSP-side DARAM write (rx_burst and deliver_buffered). */
static inline void bsp_daram_wr_bucket(uint16_t addr)
{
    bsp.wr_total++;
    /* The target zone follows the runtime daram_addr, i.e. what the BSP really
     * writes. daram_addr == 0 is discovery mode: no target zone, so every
     * write counts as "other". */
    uint16_t tgt_lo = bsp.daram_addr;
    uint16_t tgt_hi = bsp.daram_addr ? (uint16_t)(bsp.daram_addr + bsp.daram_len - 1) : 0;
    if (addr <= BSP_BUCKET_LOW_HI) {
        bsp.wr_low++;
    } else if (tgt_lo && addr >= tgt_lo && addr <= tgt_hi) {
        bsp.wr_target++;
    } else if (addr >= BSP_BUCKET_WRAP_LO && addr <= BSP_BUCKET_WRAP_HI) {
        bsp.wr_wrap++;
    } else {
        bsp.wr_other++;
    }
    if (bsp.wr_total - bsp.wr_last_logged >= BSP_DARAM_WR_LOG_EVERY) {
        bsp.wr_last_logged = bsp.wr_total;
        BSP_LOG("DARAM-WR-STATS low=%llu target=%llu wrap=%llu other=%llu total=%llu",
                (unsigned long long)bsp.wr_low,
                (unsigned long long)bsp.wr_target,
                (unsigned long long)bsp.wr_wrap,
                (unsigned long long)bsp.wr_other,
                (unsigned long long)bsp.wr_total);
    }
}

/* Signed hyperframe distance (entry_fn - reference_fn) in (-H/2, H/2]. */
static int32_t bsp_fn_delta(uint32_t entry_fn, uint32_t ref_fn)
{
    int32_t d = (int32_t)entry_fn - (int32_t)ref_fn;
    if (d > (int32_t)(GSM_HYPERFRAME / 2))       d -= GSM_HYPERFRAME;
    else if (d <= -(int32_t)(GSM_HYPERFRAME / 2)) d += GSM_HYPERFRAME;
    return d;
}

/* Enqueue a burst into queue[tn]. If a slot already carries the same FN,
 * overwrite it (duplicate retransmission from BTS). If the queue is full,
 * drop the oldest entry (smallest fn_delta relative to enqueue). */
static void bsp_enqueue(uint8_t tn, uint32_t fn, const int16_t *iq, int n)
{
    if (tn >= BSP_NUM_TN) return;
    BspBurstQueue *qq = &bsp.q[tn];

    int free_idx = -1;
    int oldest_idx = 0;
    int32_t oldest_delta = INT32_MAX;

    for (int i = 0; i < BSP_QUEUE_LEN; i++) {
        BspBurstSlot *s = &qq->slot[i];
        if (s->valid && s->fn == fn) {
            memcpy(s->iq, iq, n * sizeof(int16_t));
            s->n = n;
            return;
        }
        if (!s->valid) {
            if (free_idx < 0) free_idx = i;
        } else {
            int32_t d = bsp_fn_delta(s->fn, fn);
            if (d < oldest_delta) { oldest_delta = d; oldest_idx = i; }
        }
    }

    int idx;
    if (free_idx >= 0) {
        idx = free_idx;
    } else {
        idx = oldest_idx;
        bsp.bursts_dropped_queue_full++;
        if (bsp.bursts_dropped_queue_full == 1 || (bsp.bursts_dropped_queue_full % 10000) == 0)
            BSP_LOG("FILE PLEINE tn=%u : %llu bursts jetes (le DSP consomme moins vite que le BTS n'emet)",
                    tn, (unsigned long long)bsp.bursts_dropped_queue_full);
    }
    BspBurstSlot *s = &qq->slot[idx];
    memcpy(s->iq, iq, n * sizeof(int16_t));
    s->n = n;
    s->fn = fn;
    s->valid = true;
}

/* Purge entries older than the match window and return the slot whose FN
 * is closest to current_fn (within ±BSP_FN_MATCH_WINDOW) for this TN, or
 * NULL if none. Future bursts beyond the window stay queued. */
static BspBurstSlot *bsp_take_for_fn(uint8_t tn, uint32_t current_fn)
{
    if (tn >= BSP_NUM_TN) return NULL;
    BspBurstQueue *qq = &bsp.q[tn];
    BspBurstSlot *match = NULL;
    int32_t best_abs = INT32_MAX;
    /* FN-PROBE: track the NEAREST burst, ignoring the window, to expose the
     * burst_fn vs dispatcher_fn offset and drift even when everything is stale. */
    uint32_t near_fn = 0; int32_t near_d = 0; int32_t near_ad = INT32_MAX; int n_valid = 0;

    for (int i = 0; i < BSP_QUEUE_LEN; i++) {
        BspBurstSlot *s = &qq->slot[i];
        if (!s->valid) continue;
        int32_t d = bsp_fn_delta(s->fn, current_fn);
        int32_t ad = d < 0 ? -d : d;
        n_valid++;
        if (ad < near_ad) { near_ad = ad; near_d = d; near_fn = s->fn; }
        if (d < -BSP_FN_MATCH_WINDOW) {
            s->valid = false;
            bsp.bursts_dropped_stale++;
        } else if (ad <= BSP_FN_MATCH_WINDOW && ad < best_abs) {
            match = s;
            best_abs = ad;
        }
    }
    /* FN-PROBE (CALYPSO_BSP_FN_PROBE): the FN carried by the nearest burst (set
     * in bsp_enqueue) side by side with the FN the dispatcher tests here
     * (current_fn = calypso_trx_get_fn). A CONSTANT delta is an offset; a
     * DRIFTING delta is a clock problem. Capped at 300 then 1 in 500. */
    {
        static int fp = -1;
        if (fp < 0) fp = calypso_gate("CALYPSO_BSP_FN_PROBE", 0);
        if (fp && n_valid > 0) {
            static unsigned fpn = 0;
            if (fpn < 300 || (fpn % 500) == 0)
                BSP_LOG("FN-PROBE tn=%u dispatcher_fn=%u burst_fn=%u delta=%d "
                        "n_valid=%d verdict=%s window=%d",
                        tn, current_fn, near_fn, near_d, n_valid,
                        match ? "MATCH" : (near_d > BSP_FN_MATCH_WINDOW ? "FUTURE" : "STALE"),
                        BSP_FN_MATCH_WINDOW);
            fpn++;
        }
    }
    /* Periodic stale ratio summary: a runaway ratio (e.g. 7000:1) is the
     * symptom of a stalled DSP — virtual fn isn't catching up to queued
     * burst FNs before the match window expires. Report every 5000 stales
     * so the spiral is visible without flooding the log. */
    {
        static uint64_t last_logged_stale;
        if (bsp.bursts_dropped_stale - last_logged_stale >= 5000) {
            last_logged_stale = bsp.bursts_dropped_stale;
            BSP_LOG("STALE ratio: stale=%llu written=%llu (cur_fn=%u)",
                    (unsigned long long)bsp.bursts_dropped_stale,
                    (unsigned long long)bsp.bursts_written,
                    current_fn);
        }
    }
    return match;
}

/* TPU-RX-WIRE (RANK2): take the NEAREST valid buffered burst for a TN, ignoring
 * the FN-match window. Used when the TPU RX window fired (a BDLENA pulse was
 * consumed) : the hardware RX window IS the timing signal, so we deliver the
 * closest burst we have rather than waiting for a virtual-FN match that never
 * lands in full mode (device_fn >> virtual cur_fn). Marks the slot consumed. */
static BspBurstSlot *bsp_take_nearest(uint8_t tn, uint32_t current_fn)
{
    if (tn >= BSP_NUM_TN) return NULL;
    BspBurstQueue *qq = &bsp.q[tn];
    BspBurstSlot *best = NULL;
    int32_t best_abs = INT32_MAX;
    for (int i = 0; i < BSP_QUEUE_LEN; i++) {
        BspBurstSlot *s = &qq->slot[i];
        if (!s->valid) continue;
        int32_t d = bsp_fn_delta(s->fn, current_fn);
        int32_t ad = d < 0 ? -d : d;
        if (ad < best_abs) { best = s; best_abs = ad; }
    }
    return best;
}

static uint16_t parse_uint_env(const char *name, uint16_t def)
{
    const char *v = calypso_getenv(name);
    if (!v || !*v) return def;
    /* Auto-detect hex even without a 0x prefix: any non-decimal hex digit
     * (a-f / A-F) forces base 16. strtoul with base 0 parses "2a00" as
     * decimal and yields 2. */
    int base = 0;
    for (const char *p = v; *p; p++) {
        if ((*p >= 'a' && *p <= 'f') || (*p >= 'A' && *p <= 'F')) {
            base = 16;
            break;
        }
    }
    return (uint16_t)strtoul(v, NULL, base);
}

uint16_t calypso_bsp_get_daram_addr(void) { return bsp.daram_addr; }
uint32_t calypso_bsp_get_last_fn(void) { return calypso_daram_last_fn; }
uint16_t calypso_bsp_get_daram_len(void)  { return bsp.daram_len; }

/* Reference probe state: the last burst rx_burst received, and where it put it. */
/* Reference probe: a short history of the bursts handed to the BSP. The DSP
 * reads DARAM whenever its own DMA has drained the RIF, which is not
 * necessarily the frame the burst arrived in, so comparing against the LAST
 * burst alone cannot tell "nothing arrived" from "the previous one is still
 * there". Keeping four lets the probe name the AGE of what the DSP is
 * looking at, which is the question the SB turns on. */
#define BSP_VERIF_HIST 4
static struct {
    int16_t  iq[BSP_IQ_MAX_I16];
    int      n;
    uint16_t addr;
    uint32_t fn;
    uint16_t page;   /* d_dsp_page as it stood when the burst was handed over */
} bsp_verif_h[BSP_VERIF_HIST];
static unsigned bsp_verif_pos;   /* next slot to write */
static int      bsp_verif_plein; /* how many slots hold a burst */

/* d_dsp_page of the burst the last compare() matched. Sampled per burst, so a
 * [verif] line can state the page EXACTLY instead of interpolating it from the
 * every-37th [bsp-page] trace — which is what made the first page/address
 * correlation unreadable. */
static uint16_t bsp_verif_last_page;
uint16_t calypso_bsp_verif_last_page(void) { return bsp_verif_last_page; }

/* Compare DARAM against each recorded burst and return the best match. *age is
 * 0 for the burst handed over on this frame, 1 for the previous one, and so on;
 * the address compared is the one recorded with that burst. Returns the number
 * of identical words, or -1 when nothing has been recorded yet. */
int calypso_bsp_verif_compare(uint32_t *fn, uint16_t *addr, int *n, int *age)
{
    if (!bsp_verif_plein || !bsp.dsp) return -1;
    int best = -1, best_age = -1;
    for (int a = 0; a < bsp_verif_plein; a++) {
        unsigned i = (bsp_verif_pos + BSP_VERIF_HIST - 1 - (unsigned)a) % BSP_VERIF_HIST;
        int ident = 0;
        for (int k = 0; k < bsp_verif_h[i].n; k++)
            if ((int16_t)bsp.dsp->data[(bsp_verif_h[i].addr + k) & 0x3fff]
                == bsp_verif_h[i].iq[k]) ident++;
        if (ident > best) { best = ident; best_age = a; }
    }
    unsigned b = (bsp_verif_pos + BSP_VERIF_HIST - 1 - (unsigned)best_age) % BSP_VERIF_HIST;
    bsp_verif_last_page = bsp_verif_h[b].page;
    if (fn)   *fn   = bsp_verif_h[b].fn;
    if (addr) *addr = bsp_verif_h[b].addr;
    if (n)    *n    = bsp_verif_h[b].n;
    if (age)  *age  = best_age;
    return best;
}
uint8_t  calypso_bsp_get_last_att(void)   { return bsp.last_att; }

/* ---- UDP TRXDv0 DL receive callback ---- */

/* [2026-09-21] FN-KEYED TS0 STORE (real chain, CALYPSO_BSP_STREAM=1).
 * The BTS runs in real time, the emulated DSP does not: delivering each UDP
 * burst the moment it arrives hands the ROM bursts of arbitrary frames, and
 * once the ARM has synchronised (its frame counter = the BTS's, learnt from
 * the SB) the four bursts of a BCCH block are not the four it asked for -
 * every block failed the Fire code with perfect bursts. So every TS0 burst
 * is kept by BTS frame number (ring of 2^18 frames, ~20 min at half speed):
 *  - before synchronisation (FB/SB search) the bursts are played in arrival
 *    order, one per DSP tick;
 *  - an SB delivered into an SB window (one-shot RIF DMA of 191 samples)
 *    fixes offset = SB fn - tick fn, exactly the relation of the synthetic
 *    cell (whose SB carries the tick fn);
 *  - afterwards tick T gets the burst of BTS frame T + offset, modulated
 *    (GMSK, sampling instant 0.5, amplitude 30000, gmsk_elargir on the normal
 *    bursts) and framed for the armed window (3 or 21 leading samples). */
#define BSP_TS0_RING (1u << 18)
#define BSP_FN_MAX   2715648u
static struct { uint32_t fn; uint8_t bits[148]; uint8_t valid, joue; } *g_ts0;
static uint32_t g_ts0_next; static int g_ts0_any;

/* Live settings (calypso_bsp.h). Tuning values such as CALYPSO_BSP_TCH_DECAL,
 * PONT_TCH_DEPOT_IDLE or CALYPSO_TI_SYNC_DL can be changed on a running c54x_exe: write
 * "NAME=value" into /dev/shm/calypso_reglages; the file is re-read at most every 50 calls
 * when its mtime changes. Falls back to the environment variable. */
int calypso_reglage(const char *nom, int defaut)
{
    static char texte[4096]; static struct timespec mtime; static unsigned n_appels;
    if (n_appels++ % 50 == 0) {
        struct stat st;
        if (stat("/dev/shm/calypso_reglages", &st) == 0) {
            if (st.st_mtim.tv_sec != mtime.tv_sec || st.st_mtim.tv_nsec != mtime.tv_nsec) {
                FILE *f = fopen("/dev/shm/calypso_reglages", "r");
                if (f) {
                    size_t n = fread(texte, 1, sizeof texte - 1, f); texte[n] = 0; fclose(f);
                    mtime = st.st_mtim;
                    printf("  [reglages] /dev/shm/calypso_reglages relu : %s\n", texte);
                    fflush(stdout);
                }
            }
        } else texte[0] = 0;
    }
    size_t l = strlen(nom);
    for (const char *p = texte; (p = strstr(p, nom)); p += l)
        if ((p == texte || p[-1] == '\n' || p[-1] == ' ') && p[l] == '=') return atoi(p + l + 1);
    const char *e = calypso_getenv(nom);
    return (e && *e) ? atoi(e) : defaut;
}
static int64_t g_ts0_offset = INT64_MIN;
/* Mobile FN: fn - tick of the last SCH delivered in a REAL SB window, i.e. what the TI L1
 * has just learnt; it then counts one frame per tick until the next SB. The BSP offset
 * (g_ts0_offset) is instead re-aligned by the BCCH reads and drifts (the DSP tick is slightly
 * faster than the BTS), so a RACH dated by it can land at fn-1 or fn+1 of what the mobile
 * expects and the IMM ASS is ignored. calypso_bsp_fn_mobile_du_tick() dates the RACH
 * (pont.c tx_tsp_fn). */
static int64_t g_off_mobile = INT64_MIN;
static int g_off_rac_vu;   /* the offset comes from an RA_C: delivered SBs no longer replace it */
int calypso_bsp_fn_mobile_du_tick(uint32_t tick, uint32_t *fn)
{
    if (g_off_mobile == INT64_MIN) return 0;
    int64_t w = ((int64_t)tick + g_off_mobile) % (int64_t)BSP_FN_MAX;
    if (w < 0) w += BSP_FN_MAX;
    *fn = (uint32_t)w;
    return 1;
}
/* ... and what the TI L1 WRITES in dedicated mode: a_a5fn (pont.c) gives its exact fn.
 * Dedicated mode (TCH above all) slips the tick by one frame; without this update the first
 * RACH after the call would be dated fn-1 by the offset of the SB from BEFORE the call and
 * its IMM ASS ignored. CALYPSO_BSP_OFF_A5FN=0 disables it. */
static uint32_t g_a5fn_tick; static int g_a5fn_vu;   /* last frame where a_a5fn gave the mobile fn */
void calypso_bsp_off_mobile_a5fn(uint32_t tick, uint32_t fn)
{
    /* By default a_a5fn does NOT set the offset: it oscillates by +-1 depending on which W
     * page is read at the tick, which would date the first RACH of the next access one
     * frame off. RA_C (calypso_bsp_off_mobile_rac) is authoritative; a_a5fn only keeps the
     * dedicated alignment alive. CALYPSO_BSP_OFF_A5FN=1: a_a5fn also sets the offset. */
    static int on = -1;
    if (on < 0) { const char *e = calypso_getenv("CALYPSO_BSP_OFF_A5FN"); on = (e && *e == '1'); }
    g_a5fn_tick = tick; g_a5fn_vu = 1;
    if (!on && g_off_mobile != INT64_MIN) return;
    int64_t off = (int64_t)fn - (int64_t)tick;
    if (off != g_off_mobile) {
        static unsigned n;
        if (n++ < 30)
            printf("  [off-mobile] a_a5fn fn=%u tick=%u : offset %lld -> %lld\n", fn, tick,
                   g_off_mobile == INT64_MIN ? 0LL : (long long)g_off_mobile, (long long)off);
        g_off_mobile = off;
    }
}
/* ... and the mobile's own truth: RA_C (TI L1 trace in rvinterf.log) gives the fn at which
 * IT sent its RACH. The trace only carries fn % 42432, so the multiple of 42432 of the
 * current offset is kept. The mobile offset drifts upwards over time while the BTS one (SB,
 * BCCH) drifts down, so estimates coming from the BTS would date the RACHs 1..3 frames early
 * and deliver the wrong frames in dedicated mode. */
/* Mobile FN through d_fn. The mobile fn slips against the tick during dedicated mode
 * (load) while the BSP offset stays, which breaks every BCCH/paging read (BS_I/NP_I 1). The
 * W page carries d_fn = fn_report | (fn%104) << 8: the frame the mobile believes it reads at
 * the next tick. (tick + 1 + offset) % 104 must equal it; a gap d (|d| <= 6) seen three
 * times in a row -> offset + d (and the mobile offset used to date the RACHs). Called by
 * montant.c on every page carrying a task. CALYPSO_BSP_DFN=0 disables, CALYPSO_BSP_DFN_K
 * shifts the reference. */
void calypso_bsp_dfn_observe(uint32_t tick, uint16_t dfn, uint16_t task_d)
{
    static int on = -1, k = 0, d_prec = 99, n_meme; static unsigned n_log;
    if (on < 0) { const char *e = calypso_getenv("CALYPSO_BSP_DFN"); on = !(e && *e == '0');
                  e = calypso_getenv("CALYPSO_BSP_DFN_K"); k = (e && *e) ? atoi(e) : 0; }
    if (!on || dfn == 0 || !(task_d & 0x7fff) || g_ts0_offset == INT64_MIN) return;
    unsigned f104 = (unsigned)(dfn >> 8);
    if (f104 >= 104) return;
    int64_t w = ((int64_t)tick + 1 + g_ts0_offset) % (int64_t)BSP_FN_MAX; if (w < 0) w += BSP_FN_MAX;
    int d = ((int)f104 + k - (int)(w % 104) + 2 * 104) % 104;
    if (d >= 52) d -= 104;
    if (n_log < 60 || (d && n_log % 50 == 0)) {
        n_log++;
        printf("  [dfn] tick=%u tache=%u d_fn%%104=%u livre%%104=%u ecart %+d (offset %lld)\n",
               tick, task_d & 0x7fff, f104, (unsigned)(w % 104), d, (long long)g_ts0_offset);
    } else n_log++;
    if (d == 0 || d < -6 || d > 6) { d_prec = 99; n_meme = 0; return; }
    n_meme = (d == d_prec) ? n_meme + 1 : 1; d_prec = d;
    if (n_meme < 3) return;
    g_ts0_offset = (g_ts0_offset + d + BSP_FN_MAX) % BSP_FN_MAX;
    if (g_off_mobile != INT64_MIN) g_off_mobile += d;
    printf("  [dfn] tick=%u : fn du mobile %+d trame(s) -> offset %lld\n", tick, d, (long long)g_ts0_offset);
    d_prec = 99; n_meme = 0;
}
void calypso_bsp_off_mobile_rac(uint32_t tick, uint32_t fn42432)
{
    int64_t ref = g_off_mobile != INT64_MIN ? g_off_mobile : (g_ts0_offset != INT64_MIN ? g_ts0_offset : 0);
    int64_t m = ((int64_t)fn42432 - (int64_t)(tick % 42432u)) % 42432; if (m < 0) m += 42432;
    int64_t d = (m - (ref % 42432 + 42432) % 42432) % 42432; if (d < 0) d += 42432; if (d > 21216) d -= 42432;
    int64_t off = ref + d;
    g_a5fn_tick = tick; g_a5fn_vu = 1; g_off_rac_vu = 1;
    if (off != g_off_mobile) {
        printf("  [off-mobile] RA_C fn%%42432=%u tick=%u : offset du mobile %lld -> %lld\n", fn42432, tick,
               g_off_mobile == INT64_MIN ? 0LL : (long long)g_off_mobile, (long long)off);
        g_off_mobile = off;
    }
    /* Optionally let the delivery offset follow RA_C (gap <= 6 only). Off by default:
     * RA_C - tick equals the delivery offset OR +1 depending on the access, so following it
     * can shift the delivery by one frame and break the NP_I reads.
     * CALYPSO_BSP_RAC_LIVRAISON=1 enables it. */
    static int rac_liv = -1;
    if (rac_liv < 0) { const char *e = calypso_getenv("CALYPSO_BSP_RAC_LIVRAISON"); rac_liv = (e && *e == '1'); }
    if (rac_liv && g_ts0_offset != INT64_MIN && off != g_ts0_offset) {
        int64_t e = off - g_ts0_offset;
        if (e >= -6 && e <= 6) {
            printf("  [off-mobile] offset de livraison %lld -> %lld (RA_C)\n", (long long)g_ts0_offset, (long long)off);
            g_ts0_offset = (off + BSP_FN_MAX) % BSP_FN_MAX;
        }
    }
}
/* TI firmware in dedicated mode (ALLC tasks seen by montant.c): see CALYPSO_BSP_DEDIE_DECAL */
static int g_dedie_ti;
extern unsigned long calypso_bsp_routeur_bilan(char *buf, size_t n);
static unsigned g_dedie_entrees;   /* entries into TI dedicated mode (pont.c: did a RACH succeed?) */
unsigned calypso_bsp_dedie_entrees(void) { return g_dedie_entrees; }
void calypso_bsp_set_dedie_ti(int actif)
{
    if (actif && !g_dedie_ti) g_dedie_entrees++;
    char b[160] = "";
    if (actif != g_dedie_ti) {
        if (!actif) calypso_bsp_routeur_bilan(b, sizeof b);
        printf("  [ts0] firmware TI %s le mode dedie%s\n", actif ? "entre dans" : "quitte", b);
    }
    g_dedie_ti = actif;
}
/* SB window at the end of the frame (CALYPSO_BSP_SB_FIN_TRAME=1, Magnetite / TI L1 TCS211).
 * The TI L1 opens its SB window SB_MARGIN (92 qbits) before the start of the burst: when
 * the SCH is aligned at the start of the frame, the window opens AT THE END OF THE PREVIOUS
 * FRAME (SB_I time_alignmt=4928), and the L1 infers "BTS frame N = local frame k+1"
 * (l1a_add_time_for_nb: 4928+80 -> 8, fn_offset-1). Delivering the burst of tick k at
 * tick k would arm every NB window one tick AFTER its burst. In this mode the offset is
 * set to fn - tick - 1 at the SCH delivered in an SB window, then VERIFIED on the BCCH read
 * itself (bsp_bcch_calage): the L1 arms its NB windows on the frames where it believes
 * the block is, which directly tells by how much the stream is shifted. The SCH must not
 * be delivered early: on resync the L1 arms TWO SB windows in a row and sets its clock
 * on the second. */
static int sb_fin_trame(void)
{
    static int on = -1;
    if (on < 0) { const char *e = calypso_getenv("CALYPSO_BSP_SB_FIN_TRAME"); on = (e && *e == '1'); }
    return on;
}
/* Fake air (CALYPSO_FAUX_AIR=<cell ARFCN>, Magnetite). Without it the BTS bursts are
 * delivered whatever frequency the phone is tuned to, and the TI L1 sees the cell
 * (BSIC 7) on 65, 81, 265... QEMU publishes every synthesizer programming
 * (calypso_rf_arfcn.h); here a window tuned elsewhere than the cell receives noise
 * (CALYPSO_FAUX_AIR_BRUIT, standard deviation, 200 by default) instead of the burst.
 * Unknown ARFCN (no shm, not yet programmed): burst delivered as is. */
#include "hw/arm/calypso/calypso_rf_arfcn.h"
#include <sys/mman.h>
#include <fcntl.h>
static int faux_air_cellule(void)
{
    static int a = -2;
    if (a == -2) { const char *e = calypso_getenv("CALYPSO_FAUX_AIR"); a = (e && *e) ? atoi(e) : -1;
                   if (a >= 0) printf("  [faux-air] cellule sur l'ARFCN %d : bruit sur les autres frequences\n", a); }
    return a;
}
/* ARFCN of the window of the tick: last programming done before `limite` qbits in frame tick. */
static int faux_air_arfcn(uint32_t tick, int limite)
{
    static const CalypsoRfArfcnShm *m;
    static uint32_t essai;
    if (!m) {
        if (essai++ % 216) return -1;              /* QEMU creates the file on its first write */
        int fd = open(CALYPSO_RF_ARFCN_SHM, O_RDONLY);
        if (fd < 0) return -1;
        void *p = mmap(NULL, sizeof *m, PROT_READ, MAP_SHARED, fd, 0);
        close(fd);
        if (p == MAP_FAILED) return -1;
        m = p;
    }
    if (m->magic != CALYPSO_RF_ARFCN_MAGIC) return -1;
    uint32_t w = __atomic_load_n(&m->widx, __ATOMIC_ACQUIRE);
    uint32_t n = w < CALYPSO_RF_ARFCN_N ? w : CALYPSO_RF_ARFCN_N - 4;   /* the oldest may be overwritten */
    for (uint32_t k = 1; k <= n; k++) {
        const CalypsoRfProg *e = &m->e[(w - k) % CALYPSO_RF_ARFCN_N];
        /* arfcn -1 = PWR write at the start of the frame (qbit 323/671, 0x003a: mode change, not
         * a tuning) decoded with a PLL of another band. The synthesizer is only tuned at the
         * END of the previous frame (PLL 4851, band 4861/4983): walk back to that tuning,
         * otherwise "unknown" would count as "cell" and the neighbour windows would receive
         * the cell burst. */
        if (e->arfcn < 0) continue;
        int32_t d = (int32_t)(e->fn - tick);
        if (d < 0 || (d == 0 && e->qbit <= limite)) return e->arfcn;
    }
    return -1;
}
/* RX gain programmed for the window of the tick (same lookup as faux_air_arfcn), -999 if unknown */
static int faux_air_gain(uint32_t tick, int limite)
{
    static const CalypsoRfArfcnShm *m;
    static uint32_t essai;
    if (!m) {
        if (essai++ % 216) return -999;
        int fd = open(CALYPSO_RF_ARFCN_SHM, O_RDONLY);
        if (fd < 0) return -999;
        void *p = mmap(NULL, sizeof *m, PROT_READ, MAP_SHARED, fd, 0);
        close(fd);
        if (p == MAP_FAILED) return -999;
        m = p;
    }
    if (m->magic != CALYPSO_RF_ARFCN_MAGIC) return -999;
    uint32_t w = __atomic_load_n(&m->widx, __ATOMIC_ACQUIRE);
    uint32_t n = w < CALYPSO_RF_ARFCN_N ? w : CALYPSO_RF_ARFCN_N - 4;
    int recent = -999;
    for (uint32_t k = 1; k <= n; k++) {
        const CalypsoRfProg *e = &m->e[(w - k) % CALYPSO_RF_ARFCN_N];
        /* Realistic air: the last RECEIVE tuning (not the transmit one) is the fallback. Outside
         * step-by-step mode (banc-rssi, tick 0 or ~130 frames behind QEMU) no tuning is "before"
         * the tick, and -999 would convert the PM with the RESET gain of c54x_exe
         * (calypso_trf6151_apm_for_rf). Same fallback as air_accord. */
        if (recent == -999 && !((uint16_t)e->reserve & (1u << 14))) recent = e->gain_db;
        int32_t d = (int32_t)(e->fn - tick);
        if (d < 0 || (d == 0 && e->qbit <= limite)) return e->gain_db;
    }
    return air_niveaux_on() ? recent : -999;
}
static void faux_air_bruit(int16_t *iq, int n_ech)
{
    static double sigma = -1; static uint32_t x = 0x12345678u;
    if (sigma < 0) { const char *e = calypso_getenv("CALYPSO_FAUX_AIR_BRUIT"); sigma = (e && *e) ? atof(e) : 200.0; }
    for (int k = 0; k < 2 * n_ech; k++) {
        double u = 0;
        for (int j = 0; j < 4; j++) { x = x * 1664525u + 1013904223u; u += (double)(x >> 8) / 16777216.0; }
        iq[k] = (int16_t)lrint((u - 2.0) * 1.732 * sigma);   /* sum of 4 uniforms ~ gaussian */
    }
}
/* Realistic air (CALYPSO_AIR_NIVEAUX=1, banc-rssi). Without it, every received burst is
 * played at amplitude 30000 whatever the frequency or direction the receiver is tuned to:
 * the rssi app sees the cell on any ARFCN with all eight bars (rxlev 63), including in the
 * UPLINK view where only the mobile is. Here:
 *   - QEMU publishes the mode and the uplink frequency (calypso_trf6151_rf_flags, field
 *     `reserve`);
 *   - the bridge also copies the uplink (byte 6 = 0xA5, pont/trx.py send_ul), stored
 *     separately (g_air_ul);
 *   - a window tuned to the downlink frequency of the cell receives the downlink at level
 *     CALYPSO_AIR_DL_DBM (-60) minus the BTS attenuation, a window tuned to its uplink
 *     frequency receives the mobile burst of that slot (uplink TN = position - 3) at level
 *     CALYPSO_AIR_UL_DBM (-75), everything else noise. Amplitude 30000 corresponds to
 *     CALYPSO_AIR_REF_DBM (-40): calibrate against the display.
 * Cell: CALYPSO_FAUX_AIR (otherwise every frequency is the cell). Without the QEMU flags:
 * nothing changes. */
static int air_niveaux_on(void)
{
    static int on = -1;
    if (on < 0) { const char *e = calypso_getenv("CALYPSO_AIR_NIVEAUX"); on = (e && *e == '1');
                  if (on) printf("  [air] niveaux realistes : DL/UL separes, amplitude selon le niveau\n"); }
    return on;
}
static double air_env_dbm(const char *nom, double defaut)
{
    const char *e = calypso_getenv(nom);
    return (e && *e) ? atof(e) : defaut;
}
/* tuning of the window: like faux_air_arfcn, but skipping the TRANSMIT tunings and keeping
 * the receptions on an uplink frequency. Returns 1 if known (*arfcn, *ul, *qbit), 0 otherwise. */
static int air_accord(uint32_t tick, int limite, int *arfcn, int *ul, int *qbit)
{
    static const CalypsoRfArfcnShm *m;
    static uint32_t essai;
    if (!m) {
        if (essai++ % 216) return 0;
        int fd = open(CALYPSO_RF_ARFCN_SHM, O_RDONLY);
        if (fd < 0) return 0;
        void *p = mmap(NULL, sizeof *m, PROT_READ, MAP_SHARED, fd, 0);
        close(fd);
        if (p == MAP_FAILED) return 0;
        m = p;
    }
    if (m->magic != CALYPSO_RF_ARFCN_MAGIC) return 0;
    uint32_t w = __atomic_load_n(&m->widx, __ATOMIC_ACQUIRE);
    uint32_t n = w < CALYPSO_RF_ARFCN_N ? w : CALYPSO_RF_ARFCN_N - 4;
    /* Fallback on the most recent tuning. Outside step-by-step mode (banc-rssi), the DSP runs
     * ~130 frames behind QEMU and the ring only keeps ~10 frames: no tuning is "before" the
     * tick. The synthesizer of an app like rssi stays on the same frequency for a long time,
     * so the last receive tuning is the right one. */
    int r_arfcn = -1, r_ul = 0, r_qbit = -1;
    for (uint32_t k = 1; k <= n; k++) {
        const CalypsoRfProg *e = &m->e[(w - k) % CALYPSO_RF_ARFCN_N];
        uint16_t f = (uint16_t)e->reserve;
        if (f & (1u << 14)) continue;                              /* transmit tuning */
        int a_ul = (f & (1u << 15)) && (f & (1u << 13));
        int a = a_ul ? (int)(f & 0x7ff) : e->arfcn;
        if (a < 0) continue;
        if (r_arfcn < 0) { r_arfcn = a; r_ul = a_ul; r_qbit = e->qbit; }
        int32_t d = (int32_t)(e->fn - tick);
        if (!(d < 0 || (d == 0 && e->qbit <= limite))) continue;
        *arfcn = a; *ul = a_ul; *qbit = e->qbit; return 1;
    }
    if (r_arfcn >= 0) { *arfcn = r_arfcn; *ul = r_ul; *qbit = r_qbit; return 1; }
    return 0;
}
/* the uplink copied by the bridge, per frame and per TN (BTS) */
#define AIR_UL_RING (1u << 12)
static struct { uint32_t fn; uint8_t bits[8][148]; uint8_t presents; } *g_air_ul;
static unsigned long g_air_ul_recus;
static struct { uint32_t fn; int valide; } g_air_ul_der[8];   /* last uplink fn received per TN */
static void air_ul_stocker(uint32_t fn, unsigned tn, const uint8_t *bits)
{
    if (!g_air_ul && !(g_air_ul = calloc(AIR_UL_RING, sizeof *g_air_ul))) return;
    unsigned i = fn % AIR_UL_RING;
    if (g_air_ul[i].fn != fn) { g_air_ul[i].fn = fn; g_air_ul[i].presents = 0; }
    memcpy(g_air_ul[i].bits[tn & 7], bits, 148);
    g_air_ul[i].presents |= (uint8_t)(1u << (tn & 7));
    g_air_ul_der[tn & 7].fn = fn; g_air_ul_der[tn & 7].valide = 1;
    g_air_ul_recus++;
}
/* The LAST uplink burst of the slot, if it is less than two multiframes (102 frames) away
 * from the played frame. The exact frame cannot be looked up: on the direct path the window
 * is served when the downlink arrives, which the BTS sends ahead of time, while the uplink
 * of the same fn does not exist yet. */
static const uint8_t *air_ul_bits(uint32_t fn, unsigned tn)
{
    if (!g_air_ul || !g_air_ul_der[tn & 7].valide) return NULL;
    uint32_t f = g_air_ul_der[tn & 7].fn;
    int32_t d = (int32_t)(fn - f);
    if (d < -102 || d > 102) return NULL;
    unsigned i = f % AIR_UL_RING;
    return (g_air_ul[i].fn == f && (g_air_ul[i].presents & (1u << (tn & 7)))) ? g_air_ul[i].bits[tn & 7] : NULL;
}
/* noise of the realistic air: floor CALYPSO_AIR_BRUIT_DBM (-110), on the same scale as the
 * bursts (30000 = CALYPSO_AIR_REF_DBM). The fake-air noise (standard deviation 200, ~-83 dBm
 * on this scale) would drown the mobile uplink (-75 dBm = 533). */
static void air_bruit(int16_t *iq, int n_ech)
{
    static double sigma = -1; static uint32_t x = 0x9e3779b9u;
    if (sigma < 0) sigma = 30000.0 * pow(10.0, (air_env_dbm("CALYPSO_AIR_BRUIT_DBM", -110.0)
                                               - air_env_dbm("CALYPSO_AIR_REF_DBM", -40.0)) / 20.0);
    for (int k = 0; k < 2 * n_ech; k++) {
        double u = 0;
        for (int j = 0; j < 4; j++) { x = x * 1664525u + 1013904223u; u += (double)(x >> 8) / 16777216.0; }
        iq[k] = (int16_t)lrint((u - 2.0) * 1.732 * sigma);
    }
}
/* scales samples modulated at 30000 (sb_moduler) to the amplitude chosen by the realistic air */
static void air_echelle(int16_t *iq, int n_ech, int amp)
{
    if (amp == 30000) return;
    double k = amp / 30000.0;
    for (int i = 0; i < 2 * n_ech; i++) iq[i] = (int16_t)lrint(iq[i] * k);
}
/* slot of a measurement window, from the qbit of the synthesizer tuning (~200 qbits before the window) */
static unsigned air_pos_fenetre(uint32_t tick)
{
    int a, ul, q;
    if (!air_accord(tick, 5000, &a, &ul, &q) || q < 0) return 0;
    return (unsigned)(((q + 300) / 625) & 7);
}
/* The burst to play in the window (slot `pos` of frame fn) and its amplitude (*amp, already
 * computed for the reference level); NULL = noise. Without CALYPSO_AIR_NIVEAUX, or with an
 * unknown tuning: dl as is. */
static const uint8_t *air_choisir(uint32_t tick, int limite, uint32_t fn, unsigned pos, const uint8_t *dl, int *amp)
{
    if (!air_niveaux_on()) return dl;
    int a, ul, q;
    if (!air_accord(tick, limite, &a, &ul, &q)) return dl;
    int cell = faux_air_cellule();
    static unsigned long n_dl, n_ul, n_bruit;
    const uint8_t *b = NULL;
    double dbm = 0;
    if (cell < 0 || a == cell) {
        if (!ul) { b = dl; dbm = air_env_dbm("CALYPSO_AIR_DL_DBM", -60.0) - calypso_bsp_get_last_att(); }
        else     { b = air_ul_bits(fn, (pos + 5) & 7); dbm = air_env_dbm("CALYPSO_AIR_UL_DBM", -75.0); }
    }
    static int agc = -1;
    if (agc < 0) { const char *e = calypso_getenv("CALYPSO_AIR_AGC"); agc = !(e && *e == '0'); }
    typeof(g_air_pm[0][0]) *pm = &g_air_pm[fn % AIR_PM_RING][pos & 7];
    pm->fn = fn; pm->k = 1.0f;
    if (b) {
        double r = *amp * pow(10.0, (dbm - air_env_dbm("CALYPSO_AIR_REF_DBM", -40.0)) / 20.0);
        if (r > 32000.0) r = 32000.0;
        if (r < 1.0) r = 1.0;
        if (agc) pm->k = (float)(r / *amp);    /* full-scale I/Q, the level goes to the measurement */
        else *amp = (int)r;
        if (ul) n_ul++; else n_dl++;
    } else n_bruit++;
    unsigned long t = n_dl + n_ul + n_bruit;
    if (t <= 30 || t % 5000 == 0)
        printf("  [air] tick=%u fn=%u pos=%u accord %s %d (cellule %d) -> %s amp=%d k_pm=%.3f [%lu DL, %lu UL, %lu bruit ; %lu UL recus]\n",
               tick, fn, pos, ul ? "UL" : "DL", a, cell, b ? (ul ? "burst montant" : "burst descendant") : "bruit",
               b ? *amp : 0, (double)pm->k, n_dl, n_ul, n_bruit, g_air_ul_recus);
    return b;
}
/* Background noise (CALYPSO_BSP_BRUIT_FOND=1, Magnetite). Slots without a burst are filled
 * with noise rather than zeros: the TI L1 takes its power measurements there, and 100 PMs
 * at 0 (l1_check_pm_error) make it believe the DSP is dead (l1_initialize_for_recovery,
 * DSP restarted every ~173 frames). A real receiver always sees noise. */
extern uint16_t g_remplissage_ts0[];
static const int16_t *bsp_remplissage(void)
{
    static int on = -1;
    static int16_t bruit[2 * 148];
    if (on < 0) { const char *e = calypso_getenv("CALYPSO_BSP_BRUIT_FOND"); on = (e && *e == '1'); }
    if (!on) return (const int16_t *)g_remplissage_ts0;
    faux_air_bruit(bruit, 148);
    return bruit;
}
static int g_bcch_verrou;        /* offset confirmed by the BCCH read: SCHs no longer re-align it */
static uint32_t g_bcch_vu_tick;  /* last tick where a BCCH NB window was seen armed */
/* TRX mode (CALYPSO_BSP_TRX=1, c54x_exe/run.sh OSMO_TRX=1): this instance is the radio of
 * the BTS (trx firmware), it never receives an SB; its uplink bursts come from the air
 * (pont/air.py) tagged with the GSM fn of the mobile. The fn <-> tick offset is then SET by
 * pont.c, which decodes the sch of the SB the firmware transmits (fn F at tick T):
 * calypso_bsp_trx_calage(). Uplink burst fn X is expected in the DSP frame where downlink
 * burst fn X+1 leaves (l1s_bts_cmd programs TX next_time.fn and RX "in the next frame",
 * l1s_bts_resp tags current_fn - 2): offset = F - T + delta, delta = -1 with the original
 * firmware, -2 with BTS_RX_LAG 3 (patched prim_bts.c: one frame of margin for the air)
 * (CALYPSO_BSP_TRX_DELTA). Once set, the mobile-mode re-alignments (delivered SB, second
 * chance) no longer touch the offset. */
static int g_trx_cale = 0;
/* Uplink prosthesis (TRX mode). The NB demodulation of the trx firmware extcode
 * (dsp_extcode.c) does not produce correct bits on this core (its result words d[5..33]
 * come out as 0000/8080/0007...), nor does its SCH encoder. Since the air carries the
 * exact bits, they are written directly into the result page the ARM will read
 * (l1s_bts_resp: d[0] toa, d[1] pm, d[3] energy > 0x1000, d[5+i/4] nibble (3 - i%4),
 * bit 3 = value; order of the 116 bits: burst[3..59], burst[88..144], burst[87],
 * burst[60]). The RACH (rx cmd AB) goes through the I/Q and is not touched. The
 * "bits already correct" counter shows whether the core emulates the extcode correctly
 * (CALYPSO_BSP_TRX_UL=0 to disable). */
static struct { uint32_t fn, tick; uint8_t bits[148]; int valide; } g_trx_ul;
#define BSP_AUTRES_RING (1u << 16)
struct autres_s { uint32_t fn; uint8_t bits[7][148]; uint8_t presents; };
static struct autres_s *g_autres;   /* filled by bsp_autres_stocker (mobile TS1..7), read by the TS1 prosthesis */
static unsigned long g_autres_stockes, g_autres_joues;
/* access burst (05.02 5.2.7): 8 extended tail bits then the 41-bit synchronisation sequence */
static int bsp_ts0_est_rach(const uint8_t *bits)
{
    static const char sync[] = "01001011011111111001100110101010001111000";
    for (int i = 0; i < 41; i++) if ((bits[8 + i] & 1) != (uint8_t)(sync[i] - '0')) return 0;
    return 1;
}
/* how many of the 116 bits of the page already match the exact burst (without writing):
 * ~85-116 in the page where the extcode just demodulated THIS burst, ~58 (chance) in the other. */
static int trx_ul_compter(const uint16_t *d, const uint8_t *bits)
{
    int j = 0;
    for (int i = 0; i < 116; i++) {
        int pos = i < 57 ? 3 + i : i < 114 ? 88 + (i - 57) : i == 114 ? 87 : 60;
        int sh = (3 - (i & 3)) << 2;
        if (((d[5 + (i >> 2)] >> sh) & 8) == ((bits[pos] & 1) ? 8 : 0)) j++;
    }
    return j;
}
static void trx_ul_ecrire(uint16_t *d, const uint8_t *bits, int *justes)
{
    /* d[] of l1s_bts_resp: toa, pm, -, energy, -, 116 bits as nibbles (bit 3); bit order:
     * burst[3..59], burst[88..144], burst[87], burst[60] */
    int j = 0;
    for (int i = 0; i < 116; i++) {
        int pos = i < 57 ? 3 + i : i < 114 ? 88 + (i - 57) : i == 114 ? 87 : 60;
        int bit = bits[pos] & 1;
        int sh = (3 - (i & 3)) << 2;
        if (((d[5 + (i >> 2)] >> sh) & 8) == (bit ? 8 : 0)) j++;
        d[5 + (i >> 2)] = (uint16_t)((d[5 + (i >> 2)] & ~(0xf << sh)) | ((bit ? 0xf : 0x0) << sh));
    }
    d[0] = 3;                                      /* toa = d[0] - 3 = 0 */
    if (d[3] <= 0x1000) { d[3] = 0x4000; d[1] = 0x1530; d[2] = 0; }
    if (justes) *justes = j;
}
int calypso_bsp_trx_ul_prothese(uint16_t *dspdata, uint32_t tick, int w_page)
{
    static int actif = -1; static unsigned n_ok, n_pages, n_justes, n_ts1;
    if (actif < 0) { const char *e = calypso_getenv("CALYPSO_BSP_TRX_UL"); actif = !(e && *e == '0'); }
    if (!actif || !g_trx_cale) return 0;
    (void)w_page;
    /* The page is selected by the parity of fn, not by d_dsp_page. The ARM W page (index
     * 0/1, toggles every frame) and the DSP page bit each start from 0 at their own reset:
     * their relation changes from one boot to the next, and a skipped DSP tick is enough
     * to invert it. The result page of frame fn is the one of index (fn + parity) & 1.
     * Default: write BOTH pages on every burst (the exact burst of fn is in the page the
     * ARM will read, whichever it is; the other page is rewritten at the next burst before
     * the ARM reads it). CALYPSO_BSP_TRX_PARITE=auto: the parity is VOTED, mainly by the
     * bits (the DSP page is the one whose demod resembles the exact burst, trx_ul_compter,
     * gap >= 20 bits), with the extcode energy detections (d3 > 0x1000 in a single page)
     * as a fallback; if, once calibrated, the written page stays random over 64 bursts,
     * the parity is flipped once (parite_flip). CALYPSO_BSP_TRX_PARITE=0|1: forced. */
    static int parite = -1; static unsigned votes[2], votes_nrj[2];
    static int parite_flip, mode_pages = -1; static unsigned n_post, j_post;
    if (mode_pages < 0) {
        const char *e = calypso_getenv("CALYPSO_BSP_TRX_PARITE");
        if (e && (*e == '0' || *e == '1')) { parite = *e - '0'; parite_flip = 1; mode_pages = 1;
            printf("  [ts0] prothese : parite de page FORCEE = %d (CALYPSO_BSP_TRX_PARITE)\n", parite); }
        else if (e && !strcmp(e, "auto")) { mode_pages = 0; printf("  [ts0] prothese : parite de page VOTEE (CALYPSO_BSP_TRX_PARITE=auto)\n"); }
        else { mode_pages = 2; parite_flip = 1;
            printf("  [ts0] prothese : burst exact ecrit dans LES DEUX pages (defaut depuis le 21/10 ; CALYPSO_BSP_TRX_PARITE=auto|0|1 pour une seule)\n"); }
    }
    int fait = 0;
    static unsigned n_pas_valide, n_diff, n_appels;
    n_appels++;
    if (!g_trx_ul.valide) n_pas_valide++;
    else if ((int32_t)(tick - g_trx_ul.tick) != 1) n_diff++;
    if (n_appels % 20000 == 0)
        printf("  [ts0] prothese TS0 : %u appels, %u sans burst, %u mauvais tick, parite %d (votes %u/%u)\n", n_appels, n_pas_valide, n_diff, parite, votes[0], votes[1]);
    /* TS0: the burst played by bsp_ts0_livrer at tick T = tick - 1 */
    if (g_trx_ul.valide && (int32_t)(tick - g_trx_ul.tick) == 1) {
        if (parite < 0) {
            int det = -1, j[2] = { -1, -1 };
            for (int pg = 0; pg < 2; pg++) {
                uint16_t *db = &dspdata[pg ? 0x2200 : 0x2000];
                if (db[16] != 2) continue;
                j[pg] = trx_ul_compter(&db[34 + 32], g_trx_ul.bits);
                if (db[34 + 32 + 3] > 0x1000) det = (det < 0) ? pg : -2;
            }
            if (j[0] >= 0 && j[1] >= 0 && (j[0] - j[1] >= 20 || j[1] - j[0] >= 20))
                votes[(unsigned)((j[1] > j[0]) ^ (g_trx_ul.fn & 1))]++;
            if (det >= 0) votes_nrj[(unsigned)(det ^ (g_trx_ul.fn & 1))]++;
            if (mode_pages != 0) { /* both pages: no decision, only the measurement is kept */ }
            else if (votes[0] + votes[1] >= 4 && votes[0] != votes[1] && 3 * (votes[0] > votes[1] ? votes[1] : votes[0]) <= votes[0] + votes[1]) {
                parite = votes[1] > votes[0];
                printf("  [ts0] prothese : parite de page calibree = %d (votes bits %u/%u, energie %u/%u) : la trame fn ecrit dans la page (fn + %d) & 1\n",
                       parite, votes[0], votes[1], votes_nrj[0], votes_nrj[1], parite);
            } else if (n_ok >= 40 && votes_nrj[0] + votes_nrj[1] >= 6 && votes_nrj[0] != votes_nrj[1]) {
                parite = votes_nrj[1] > votes_nrj[0];
                printf("  [ts0] prothese : parite de page calibree EN SECOURS par l'energie = %d (votes bits %u/%u, energie %u/%u) : les bits ne tranchent pas (demod au hasard dans les deux pages ?)\n",
                       parite, votes[0], votes[1], votes_nrj[0], votes_nrj[1]);
            }
            if (n_ok < 5 || (n_ok % 10 == 0 && n_ok < 100) || n_ok % 500 == 0)
                printf("  [ts0] prothese : bits justes page0=%d page1=%d (fn=%u, energie dans la page %d) votes bits %u/%u energie %u/%u\n",
                       j[0], j[1], g_trx_ul.fn, det, votes[0], votes[1], votes_nrj[0], votes_nrj[1]);
        }
        for (int pg = 0; pg < 2; pg++) {
            if (parite >= 0 && pg != (int)((g_trx_ul.fn + parite) & 1)) continue;
            uint16_t *db = &dspdata[pg ? 0x2200 : 0x2000];
            if (db[16] != 2) continue;                 /* rx[0].cmd: 2 = NB (AB window: left alone) */
            int justes = 0;
            trx_ul_ecrire(&db[34 + 32], g_trx_ul.bits, &justes);
            n_justes += (unsigned)justes; n_pages++; fait = 1;
            if (parite >= 0) { n_post++; j_post += (unsigned)justes; }
        }
        /* guard: written page still random (< 70/116 over 64 bursts) -> the parity was wrong */
        if (parite >= 0 && !parite_flip && n_post >= 64) {
            if (j_post < 70u * n_post) {
                parite ^= 1; parite_flip = 1;
                printf("  [ts0] prothese : la page ecrite restait au hasard (%.1f/116 sur %u bursts) -> parite basculee a %d\n", (double)j_post / n_post, n_post, parite);
            } else if (n_post == 64)
                printf("  [ts0] prothese : parite %d confirmee (%.1f/116 justes sur %u bursts)\n", parite, (double)j_post / n_post, n_post);
            if (n_post >= 64 && j_post >= 70u * n_post) parite_flip = 1;   /* confirmed: no more flipping */
        }
        if (fait) {
            n_ok++;
            if (n_ok <= 5 || n_ok % 500 == 0)
                printf("  [ts0] prothese montante : burst fn=%u (tick %u) ecrit a la phase A du tick %u ; bits deja justes avant ecriture : %.1f/116 en moyenne (%u bursts, parite %d)\n",
                       g_trx_ul.fn, g_trx_ul.tick, tick, n_pages ? (double)n_justes / n_pages : 0.0, n_ok, parite);
        }
        g_trx_ul.valide = 0;
    } else if (g_trx_ul.valide && (int32_t)(tick - g_trx_ul.tick) > 1) g_trx_ul.valide = 0;
    /* TS1 (the TCH): the mobile uplink burst stored for frame w = T + offset, written into
     * rx[2] / data[72..] of the page of w, which the patched firmware reports with
     * tn = rx_start + 1 (prim_bts.c). */
    if (g_autres && g_ts0_offset != INT64_MIN) {
        int64_t w = ((int64_t)(tick - 1) + g_ts0_offset) % (int64_t)BSP_FN_MAX; if (w < 0) w += BSP_FN_MAX;
        unsigned i = (unsigned)w % BSP_AUTRES_RING;
        static unsigned n_look, n_trouve;
        n_look++;
        if (g_autres[i].fn == (uint32_t)w && (g_autres[i].presents & 1)) {
            n_trouve++;
            for (int pg = 0; pg < 2; pg++) {
                if (parite >= 0 && pg != (int)(((uint32_t)w + parite) & 1)) continue;
                uint16_t *db = &dspdata[pg ? 0x2200 : 0x2000];
                uint16_t *d = &db[34 + 72];
                memset(d, 0, 40 * sizeof *d);
                trx_ul_ecrire(d, g_autres[i].bits[0], NULL);
                db[16 + 2 * 2] = 2;                    /* rx[2].cmd = NB: the marker read by l1s_bts_resp */
                fait = 1;
            }
            g_autres[i].presents &= (uint8_t)~1u;
            n_ts1++;
            if (n_ts1 <= 5 || n_ts1 % 500 == 0)
                printf("  [ts0] prothese montante TS1 : burst fn=%u ecrit (rx[2], data[72]) a la phase A du tick %u (%u bursts TS1, parite %d)\n", (uint32_t)w, tick, n_ts1, parite);
        }
        if (n_look % 20000 == 0)
            printf("  [ts0] prothese TS1 : %u recherches, %u trouvees, %lu bursts TS1..7 stockes au total\n", n_look, n_trouve, g_autres_stockes);
    }
    return fait;
}
int calypso_bsp_trx_calage(uint32_t fn_sb, uint32_t tick)
{
    static int actif = -1, delta = -3;   /* -3: BTS_RX_LAG 4 of the patched trx firmware (prim_bts.c); -1 for the original firmware */
    if (actif < 0) {
        const char *e = calypso_getenv("CALYPSO_BSP_TRX"); actif = (e && *e == '1') ? 1 : 0;
        e = calypso_getenv("CALYPSO_BSP_TRX_DELTA"); if (e && *e) delta = atoi(e);
        if (actif) BSP_LOG("mode TRX : offset fn/tick pose par le SB emis (delta %d), bursts montants joues a la trame du firmware", delta);
    }
    if (!actif) return 0;
    int64_t off = ((int64_t)fn_sb + delta - (int64_t)tick) % (int64_t)BSP_FN_MAX; if (off < 0) off += BSP_FN_MAX;
    if (g_trx_cale && off == g_ts0_offset) return 1;
    static unsigned n;
    if (n++ < 10 || n % 500 == 0)
        printf("  [ts0] mode TRX : offset fn-tick %s %lld (SB fn=%u au tick %u, delta %d)\n",
               g_trx_cale ? "recale a" : "pose a", (long long)off, fn_sb, tick, delta);
    g_ts0_offset = off; g_trx_cale = 1;
    return 1;
}
static uint32_t g_ts0_fn_max;   /* most recent BTS frame stored */
static unsigned long g_ts0_recales;      /* number of offset re-alignments (see bsp_ts0_service) */
static unsigned long g_ts0_silences;     /* erasures deposited (see bsp_ts0_service) */
static long long     g_ts0_recul_total;  /* sum of the step-backs, in frames */
static const uint8_t bsp_train_sb[64] = { 1,0,1,1,1,0,0,1,0,1,1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,1,1,1,1, 0,0,1,0,1,1,0,1,0,1,0,0,0,1,0,1,0,1,1,1,0,1,1,0,0,0,0,1,1,0,1,1, };
static int bsp_ts0_est_sb(const uint8_t *b) { for (int k = 0; k < 64; k++) if ((b[42 + k] & 1) != bsp_train_sb[k]) return 0; return 1; }
static int bsp_ts0_est_fcch(const uint8_t *b) { for (int k = 0; k < 148; k++) if (b[k] & 1) return 0; return 1; }
static int g_toa_bias = 0;   /* placement bias, in samples; see calypso_bsp_toa_feedback() */
static uint32_t g_tick_courant;        /* (forward definition: the current tick, set by bsp_ts0_service) */
static void bsp_ts0_stocker(uint32_t fn, const uint8_t *bits)
{
    if (!g_ts0) g_ts0 = calloc(BSP_TS0_RING, sizeof *g_ts0);
    if (!g_ts0) return;
    unsigned i = fn % BSP_TS0_RING;
    /* TRX mode: arrival margin of the uplink burst = (tick where it will be played) - (current tick);
     * <= 0: arrived too late, lost. Statistics over the first 2000, then every 2000. */
    if (g_trx_cale) {
        static long n, n_tard, somme; static int mini = 999;
        int64_t tj = ((int64_t)fn - g_ts0_offset) % (int64_t)BSP_FN_MAX; if (tj < 0) tj += BSP_FN_MAX;
        int64_t dm = tj - (int64_t)g_tick_courant; if (dm > BSP_FN_MAX / 2) dm -= BSP_FN_MAX; if (dm < -(int64_t)BSP_FN_MAX / 2) dm += BSP_FN_MAX;
        int m = (int)dm;
        n++; somme += m; if (m < mini) mini = m; if (m <= 0) n_tard++;
        if (n == 50 || n == 500 || n % 2000 == 0)
            printf("  [ts0] mode TRX : marge d'arrivee des bursts montants : min %d, moyenne %.1f trame(s), %ld/%ld en retard\n", mini, (double)somme / n, n_tard, n);
    }
    g_ts0[i].fn = fn; memcpy(g_ts0[i].bits, bits, 148); g_ts0[i].valid = 1; g_ts0[i].joue = 0;
    /* The most recent frame received from the BTS. On a miss it tells ON WHICH
     * SIDE the gap is: if the requested frame is beyond it, the DSP runs ahead
     * of the BTS (more lead is needed); if it is below, the burst did exist and
     * the cause is elsewhere. */
    if (!g_ts0_any || (int32_t)(fn - g_ts0_fn_max) > 0) { g_ts0_fn_max = fn; }
    bsp.bursts_seen++;   /* the drain loop of calypso_bsp_service() stops when this does not move */
    if (!g_ts0_any) { g_ts0_any = 1; g_ts0_next = fn; }
}
/* THE WINDOW'S TIMESLOT, AS TOLD BY THE FIRMWARE.
 *
 * tpu_window.c:89: tpu_enq_offset((5000 + l1s.tpu_offset + 625*tn) % 5000).
 * The TPU_OFFSET register, which QEMU relays in every TICK, therefore carries
 * the sync offset AND the position of the timeslot the RX window is armed on.
 *
 * l1s.tpu_offset is unknown but not needed: outside dedicated mode the ARM
 * listens to TS0, so the value observed then IS the reference. The distance to
 * the reference, divided by 625, gives the timeslot. The reference relearns
 * itself on every resync, since the mobile is then not in dedicated mode. */
static int g_bsp_tpu_offset = 0;
static int g_tpu_ref = -1;

/* Timeslot of the current window, 0 if unknown. */
static int bsp_fenetre_tn(void)
{
    if (g_tpu_ref < 0) {
        return 0;
    }
    int d = ((g_bsp_tpu_offset - g_tpu_ref) % 5000 + 5000) % 5000;
    return ((d + 312) / 625) % 8;   /* 625 qbits per timeslot, rounded */
}
/* For montant.c: the timeslot of the TPU's current RX window (-1 if unknown). The mobile uses
 * it to find the TS of its TCH ON ITS OWN when nobody announced the ASSIGNMENT COMMAND to it
 * (A5/1-ciphered on the SDCCH: neither the bridge nor the air can read it). */
int calypso_bsp_fenetre_tn(void) { return bsp_fenetre_tn(); }

/* THE DEDICATED TIMESLOT.
 *
 * The bridge sends the eight timeslots of every frame, the store above only
 * keeps one (TS0) and bsp_ts0_livrer() completes the frame. As long as the
 * mobile reads the BCCH and the CCCH this is exact: everything it needs is on
 * TS0. Once it enters dedicated mode it is not: the BSC allocates it e.g. an
 * SDCCH/8 on TS1, where the UA and the LOCATION UPDATING ACCEPT arrive.
 *
 * QEMU learns this timeslot from the firmware's L1CTL stream and announces it
 * through PONT_DCCH; the bursts of THAT timeslot are then kept, by BTS frame
 * number, and bsp_ts0_livrer() plays them INSTEAD of TS0. The frame stays one
 * burst per tick: the 1250-symbol pacing is not touched.
 *
 * One timeslot at a time, like the mobile: the store is allocated when the
 * channel is armed and restarts from scratch if the timeslot changes. */
#define BSP_DEDIE_RING (1u << 16)   /* ~5 min of BTS frames: the lock-step DSP drifts by several seconds */
static struct { uint32_t fn; uint8_t bits[148]; uint8_t valid; } *g_dedie;

/* THE FRAME DELIVERED TO THE DSP IS A REAL FRAME.
 *
 * The pacing (one burst per tick, indexed by BTS frame number) is not a
 * crutch: without it the ROM sees ~1526 symbols per frame instead of 1250 and
 * its TOA never settles (TOA at 2967, 3735, 4215, 48, 13536 and "BURST ID
 * 3!=2", "EMPTY"). Zero filler for the other timeslots WOULD be one: the mobile
 * would hear nothing of what the BTS emits outside TS0. All eight timeslots are
 * therefore kept, and the frame handed to the DSP carries what the BTS really
 * emitted. */

static void bsp_autres_stocker(uint32_t fn, unsigned tn, const uint8_t *bits)
{
    if (tn < 1 || tn > 7) {
        return;
    }
    if (!g_autres) {
        g_autres = calloc(BSP_AUTRES_RING, sizeof *g_autres);
        if (!g_autres) {
            return;
        }
    }
    unsigned i = fn % BSP_AUTRES_RING;
    if (g_autres[i].fn != fn) {
        g_autres[i].fn = fn;
        g_autres[i].presents = 0;
    }
    memcpy(g_autres[i].bits[tn - 1], bits, 148);
    g_autres[i].presents |= (uint8_t)(1u << (tn - 1));
    g_autres_stockes++;
}

static const uint8_t *bsp_autres_bits(uint32_t fn, unsigned tn)
{
    if (!g_autres || tn < 1 || tn > 7) {
        return NULL;
    }
    unsigned i = fn % BSP_AUTRES_RING;
    if (g_autres[i].fn != fn || !(g_autres[i].presents & (1u << (tn - 1)))) {
        return NULL;
    }
    return g_autres[i].bits[tn - 1];
}
static int g_dedie_tn = -1, g_dedie_ss;
/* Kind of the dedicated channel: 0 = SDCCH/4, 1 = SDCCH/8,
 * BSP_DEDIE_TCH = TCH -- on a TCH ALL the frames of the timeslot belong to
 * the connection (26-multiframe: 24 traffic + 1 SACCH + 1 idle), not only
 * the blocks of a sub-channel. */
#define BSP_DEDIE_TCH 2
/* PDCH (GPRS): like the TCH, all the frames of the timeslot belong to the
 * connection (blocks B0..B11 of the 52-multiframe, PTCCH, idle). */
#define BSP_DEDIE_PDCH 4
static int g_dedie_genre = 1;
static unsigned long g_dedie_stockes, g_dedie_joues, g_dedie_manques;

/* LAST TCH BURSTS DELIVERED TO THE DSP, for the downlink FACCH prosthesis of montant.c: under the
 * TI L1 (Magnetite) the ROM decodes the FACCH/F with its last two bursts missing ("01 8b 01 2b"
 * instead of the UA "01 73 01 2b"), while the delivered bursts carry the UA intact. */
#define TCH_ANNEAU 64u
static struct { uint32_t fn; uint8_t bits[148]; int ok; } g_tch_anneau[TCH_ANNEAU];
static uint32_t g_tch_dernier_fn; static int g_tch_dernier_ok;
int calypso_bsp_tch_dernier_fn(uint32_t *fn)
{
    if (!g_tch_dernier_ok || g_dedie_genre != BSP_DEDIE_TCH || g_dedie_tn <= 0) return 0;
    *fn = g_tch_dernier_fn;
    return 1;
}
int calypso_bsp_tch_burst(uint32_t fn, uint8_t bits[148])
{
    unsigned k = fn % TCH_ANNEAU;
    if (!g_tch_anneau[k].ok || g_tch_anneau[k].fn != fn) return 0;
    memcpy(bits, g_tch_anneau[k].bits, 148);
    return 1;
}
/* g_dedie_perdues: the dedicated frames that bsp_ts0_service() skips
 * ENTIRELY, for lack of a BTS burst for the requested frame. It leaves through
 * a `return` before bsp_ts0_livrer(), so neither `joues` nor `manques` see
 * them. Never read manques=0 as "nothing is lost" without reading perdues at
 * the same time. */
static unsigned long g_dedie_perdues;

/* Does this BTS frame belong to the mobile's dedicated channel?
 *
 * Only the frames of the channel are taken, those where its layer 1 arms its
 * window on the dedicated timeslot -- elsewhere it reads the BCCH and the
 * CCCH on TS0, and needs them. (The BTS emits on TS1 permanently, filler
 * bursts included: replacing TS0 on EVERY frame would leave the mobile with no
 * TS0 at all.)
 *
 * 45.002, SDCCH/8 + SACCH/8, 51-multiframe counted in pairs:
 *   downlink SDCCH, sub-channel i : frames 4i..4i+3 of every multiframe;
 *   downlink SACCH, i = 0..3      : frames 32+4i..35+4i, even multiframe;
 *                   i = 4..7      : same frames, odd multiframe. */
static bool bsp_dedie_trame(uint32_t fn)
{
    if (g_dedie_genre == BSP_DEDIE_TCH || g_dedie_genre == BSP_DEDIE_PDCH) {
        return true;    /* TCH, PDCH: the whole timeslot belongs to the connection */
    }
    /* TI FIRMWARE IN DEDICATED MODE ON AN SDCCH/8: ALL THE FRAMES OF THE TIMESLOT. The bench does not
     * know which sub-channel the mobile accepted: the BSC answers every RACH with a different SDCCH/8
     * (SS1, SS2...) and the bridge announces the last IMM ASS, while the mobile takes the one of ITS
     * reference. Wrong sub-channel -> SABM without UA, "lchan allocation failed ...
     * WAIT_RLL_RTP_ESTABLISH: Timeout". Once the firmware is in dedicated mode (g_dedie_ti: uplink
     * tasks posted, IMM ASS accepted) it no longer reads TS0: it gets the whole TS1 and picks its
     * sub-channel there. BEFORE (waiting for the IMM ASS), the SS alone: it needs TS0.
     * CALYPSO_BSP_SDCCH8_TOUT=0 falls back to the announced sub-channel. */
    if (g_dedie_genre == 1 && g_dedie_ti) {
        static int tout = -1;
        if (tout < 0) { const char *e = calypso_getenv("CALYPSO_BSP_SDCCH8_TOUT"); tout = !(e && *e == '0'); }
        if (tout) return true;
    }
    /* AND BEFORE THAT, NOTHING. Replacing TS0 with the SS frames as soon as the bridge announces an
     * IMM ASS (SS0, p51 0..3), while the mobile has accepted nothing yet, breaks the BCCH (2..5) and
     * overwrites the CCCH blocks 6..9 / 12..15 with SS1 / SS3: the IMM ASS of its own RACH is never
     * read, new RACH, SDCCH Timeout at the BSC. TI firmware (MS_MAGNETITE): TS0 intact until the entry
     * in dedicated mode (g_dedie_ti). CALYPSO_BSP_SDCCH_TS0_ATTENTE=0: SS frames replace TS0 at once. */
    if (g_dedie_genre == 1 && !g_dedie_ti) {
        static int attente = -1;
        if (attente < 0) { const char *e = calypso_getenv("CALYPSO_BSP_SDCCH_TS0_ATTENTE");
                           attente = e && *e ? (*e != '0') : (calypso_getenv("MS_MAGNETITE") != NULL); }
        if (attente) return false;
    }
    unsigned p51 = fn % 51u;
    unsigned ss = (unsigned)g_dedie_ss & 7u;
    if (p51 >= 4u * ss && p51 <= 4u * ss + 3u) {
        return true;
    }
    unsigned base = 32u + 4u * (ss & 3u);
    if (p51 >= base && p51 <= base + 3u) {
        return ((fn / 51u) % 2u) == (ss < 4u ? 0u : 1u);
    }
    return false;
}

/* State of the dedicated channel, readable without the DSP terminal:
 * /dev/shm/calypso_bsp_dedie, one line rewritten on every notable change.
 * It is the only way to know, from outside, whether the bursts of the
 * dedicated timeslot reach this point and whether they are played. */
static unsigned long g_dedie_replis;   /* see bsp_dedie_bits() */
static void bsp_dedie_etat(const char *quoi)
{
    static unsigned long dernier_joues, dernier_stockes, dernier_perdues;
    if (quoi == NULL && g_dedie_joues == dernier_joues &&
        g_dedie_stockes == dernier_stockes && g_dedie_perdues == dernier_perdues) {
        return;
    }
    dernier_joues = g_dedie_joues;
    dernier_stockes = g_dedie_stockes;
    dernier_perdues = g_dedie_perdues;
    FILE *f = fopen("/dev/shm/calypso_bsp_dedie", "w");
    if (!f) {
        return;
    }
    fprintf(f, "tn=%d ss=%d stockes=%lu joues=%lu manques=%lu replis=%lu perdues=%lu recales=%lu recul=%lld silences=%lu %s\n",
            g_dedie_tn, g_dedie_ss, g_dedie_stockes, g_dedie_joues,
            g_dedie_manques, g_dedie_replis, g_dedie_perdues,
            g_ts0_recales, g_ts0_recul_total, g_ts0_silences, quoi ? quoi : "");
    fclose(f);
}

void calypso_bsp_set_dedie(int tn, int genre, int ss)
{
    if (tn <= 0 || tn > 7 || genre == 0xFF) {
        if (g_dedie_tn > 0) {
            BSP_LOG("canal dedie libere (TS%d) : %lu bursts stockes, %lu joues",
                    g_dedie_tn, g_dedie_stockes, g_dedie_joues);
        }
        g_dedie_tn = -1;
        bsp_dedie_etat("libere");
        return;
    }
    if (g_dedie_tn != tn) {
        if (!g_dedie) {
            g_dedie = calloc(BSP_DEDIE_RING, sizeof *g_dedie);
        }
        if (g_dedie) {
            memset(g_dedie, 0, BSP_DEDIE_RING * sizeof *g_dedie);
        }
        g_dedie_stockes = g_dedie_joues = g_dedie_manques = g_dedie_replis = g_dedie_perdues = 0;
    }
    g_dedie_tn = tn;
    g_dedie_ss = ss;
    g_dedie_genre = genre;
    bsp_dedie_etat("arme");
    if (genre == BSP_DEDIE_PDCH) {
        BSP_LOG("canal dedie arme : PDCH TS%d (GPRS) - TOUTES ses trames remplacent TS0", tn);
        printf("  [ts0] canal dedie arme : PDCH TS%d (GPRS, toutes les trames)\n", tn);
        return;
    }
    if (genre == BSP_DEDIE_TCH) {
        BSP_LOG("canal dedie arme : TCH TS%d - TOUTES ses trames remplacent TS0", tn);
        printf("  [ts0] canal dedie arme : TCH TS%d (toutes les trames)\n", tn);
        return;
    }
    BSP_LOG("canal dedie arme : SDCCH/%d SS=%d TS%d - ses bursts remplacent TS0 sur les "
            "trames du canal (fn%%51 = %u-%u et SACCH %u-%u), TS0 ailleurs",
            genre ? 8 : 4, ss, tn, 4u * ((unsigned)ss & 7u), 4u * ((unsigned)ss & 7u) + 3u,
            32u + 4u * ((unsigned)ss & 3u), 32u + 4u * ((unsigned)ss & 3u) + 3u);
}

static void bsp_dedie_stocker(uint32_t fn, const uint8_t *bits)
{
    if (!g_dedie) {
        return;
    }
    unsigned i = fn % BSP_DEDIE_RING;
    g_dedie[i].fn = fn;
    memcpy(g_dedie[i].bits, bits, 148);
    g_dedie[i].valid = 1;
    g_dedie_stockes++;
}

/* THE DEDICATED STORE STARTS TOO LATE: FALL BACK ON g_autres.
 *
 * `g_dedie` is only allocated and filled when the channel is ARMED
 * (`calypso_bsp_set_dedie`), and the arming comes from QEMU's L1CTL tap, i.e.
 * from the FIRST dedicated block received -- not from the IMMEDIATE
 * ASSIGNMENT. The BSP plays BTS frame `tick + g_ts0_offset`, behind the one
 * arriving: the channel frames older than the arming would be played EMPTY
 * (one frame in four without burst, the signature of "Dropping frame with 110
 * bit errors" and of blocks rebuilt from three bursts out of four).
 *
 * `g_autres` ALREADY keeps the seven other timeslots of every frame,
 * unconditionally and from the first burst (it serves to complete the
 * continuous frame). `g_dedie` is a partial duplicate of it, kept because it
 * counts the channel bursts; when it does not have the frame, the one of
 * `g_autres` is taken instead of returning NULL.
 *
 * MONTANT_DEDIE_STRICT=1 disables the fallback, to measure the gap. */
static const uint8_t *bsp_dedie_bits(uint32_t fn)
{
    if (g_dedie_tn <= 0) {
        return NULL;
    }
    if (g_dedie) {
        unsigned i = fn % BSP_DEDIE_RING;
        if (g_dedie[i].valid && g_dedie[i].fn == fn) {
            return g_dedie[i].bits;
        }
    }
    static int strict = -1;
    if (strict < 0) {
        const char *e = calypso_getenv("MONTANT_DEDIE_STRICT");
        strict = (e && *e == '1');
    }
    if (strict) {
        return NULL;
    }
    const uint8_t *b = bsp_autres_bits(fn, (unsigned)g_dedie_tn);
    if (b) {
        g_dedie_replis++;
    }
    return b;
}

/* THE SCH OUTSIDE A ONE-SHOT WINDOW: SB block waiting for an armed DMA.
 *
 * The ROM's SB task does not work on the continuous stream of the FB search:
 * it programs a task page (ALGTH >= 302 words, i.e. >= 151 samples), double
 * page, and expects the burst 23 symbols from the start of its window
 * (tpu_window.c: L1_SB_MARGIN_Q = 23 * 4). What it receives depends on the
 * RIF residue (4 words per FB search frame, never empty) and on the instant it
 * arms its DMA relative to the deposit: if the DMA is not armed when
 * bsp_ts0_livrer() deposits the SCH, calypso_rif_rx_burst() drops the burst
 * ("n_muets") and the page only holds stale data.
 *
 * Behind CALYPSO_BSP_SB_FENETRE (1 by default, 0 = no retention):
 *   1. an SCH deposited while the DMA is not armed is not dropped: it is kept
 *      ONE tick and delivered, framed at 21 (TOA 23), in the one-shot SB window
 *      (>= 190 samples) the ROM arms right after; the ARM-tick offset is
 *      realigned on the tick of the actual delivery;
 *   2. it is NEVER delivered in another window (NB 151, continuous): an
 *      unclaimed block expires;
 *   3. the [sbwin] trace tells armed/one_shot/programmed page/RIF residue and
 *      the action, and pont.c adds TOA/PM/SNR to the [a_sch] traces: a well
 *      framed SB reads TOA=23 or 24.
 * The FB search (48-sample page, DMA armed continuously) and the one-shot
 * windows armed before the deposit do not change. */
static int16_t  g_sb_attente_iq[2 * 512];
/* THE LAST DEPOSITED SCH, whatever the DMA state at the deposit. At the
 * deposit of the SCH the DMA is most often ARMED for something else (64-sample
 * PM window, one_shot=1: the SCH is consumed there), then the SB window arms
 * and finds nothing. The last SCH (fn, tick, bits) is therefore ALWAYS kept,
 * with a note of whether it was delivered in an SB window; if it was not and
 * an SB window arms in this tick or the next, it is delivered there. On
 * silicon the SB window covers the instant of the burst whatever the DSP did
 * before: this is what it reconstructs. */
static struct { uint32_t fn; uint32_t tick; uint8_t bits[148]; int livre_sb; int valide; } g_dernier_sch;
static uint32_t g_sb_trace_jusqua;     /* tick up to which EVERY SCH is traced ([sbwin]) */
static unsigned long g_sb_relivres, g_sb_expires;
static uint32_t g_tick_courant;        /* current tick (bsp_ts0_service), to date the redelivery */
static int sb_fenetre_on(void)
{
    static int on = -1;
    if (on < 0) { const char *e = calypso_getenv("CALYPSO_BSP_SB_FENETRE"); on = (e && *e == '0') ? 0 : 1;
                  if (on) BSP_LOG("SB_FENETRE on : un SCH depose alors que la DMA n'est pas armee est garde un tick et livre dans la fenetre SB one-shot qui suit"); }
    return on;
}
/* Frames the SB burst in `iq` for the one-shot window as programmed at this
 * instant (191 samples, burst at 21: TOA 23); returns the number of samples
 * of the block, 0 if it is not an SB window. */
/* SCH MODULATION, TUNED IN REPLAY (c54x_exe --rejouer, REJEU_CONTINUER=1,
 * 8000 frames, deterministic). On the failures, mostly only bits 0-2 of the
 * SB word are wrong: the "razor's edge" profile of the NBs before
 * gmsk_elargir. Sweep (SCH decoded / SCH present, 8000 frames):
 *   widening a: 0 -> 34 %, 0.15 -> 50 %, 0.3 -> 60 %, 0.5-1.5 -> 63-66 %
 *   + sampling instant 0.35 (instead of 0.5)                     -> 76 %
 *   + gaussian noise sigma 3000 (10 % of the amplitude)           -> 82 %
 *   carrier phase, head margin: no effect; amplitude < 22000: no more FB.
 * Those three settings are the defaults here (CALYPSO_BSP_SB_SYM, _SB_DEC,
 * _SB_NOISE; 0 / 0.5 / 0 = none of them). The noise is seeded by the fn of
 * the frame: deterministic, replayable. */
/* TWO WAVEFORMS FOR THE SCH. In replay (8000 frames, 209 SCH): waveform A
 * (below) misses 20 % of the SCHs; waveform D (A + causal tail 0.8, 0.4 and
 * another noise seed) misses 17 %; both together only miss 7 % in common
 * (A+D = 15/209). g_sb_variante = 1 selects D, used by the second chance
 * (see bsp_ts0_service). */
static int g_sb_variante = 0;
static void sb_moduler(const uint8_t *bits, int16_t *dst, uint32_t fn)
{
    static double sym = -2, dec = -2, sigma = -2;
    if (sym == -2) {
        const char *e;
        e = calypso_getenv("CALYPSO_BSP_SB_SYM");   sym   = (e && *e) ? atof(e) : 1.0;
        e = calypso_getenv("CALYPSO_BSP_SB_DEC");   dec   = (e && *e) ? atof(e) : 0.35;
        e = calypso_getenv("CALYPSO_BSP_SB_NOISE"); sigma = (e && *e) ? atof(e) : 3000.0;
        if (dec < 0 || dec >= 1.0) dec = 0.5;
        BSP_LOG("SCH : elargissement %.2f, instant %.2f symbole, bruit sigma %.0f (mesure rejeu du 2026-09-30 : 82 %% de SCH decodes)", sym, dec, sigma);
    }
    gmsk_moduler(bits, 148, 30000, 0.0, dec, dst);
    if (sym != 0.0) gmsk_elargir(dst, 148, sym);
    if (g_sb_variante == 1) {                       /* forme D : traine causale 0.8, 0.4, normalisee */
        double xi[148], xq[148];
        for (int k = 0; k < 148; k++) { xi[k] = dst[2*k]; xq[k] = dst[2*k+1]; }
        for (int k = 0; k < 148; k++) {
            double yi = xi[k], yq = xq[k];
            if (k >= 1) { yi += 0.8 * xi[k-1]; yq += 0.8 * xq[k-1]; }
            if (k >= 2) { yi += 0.4 * xi[k-2]; yq += 0.4 * xq[k-2]; }
            dst[2*k] = (int16_t)lrint(yi / 2.2); dst[2*k+1] = (int16_t)lrint(yq / 2.2);
        }
    }
    if (sigma > 0) {
        uint32_t seed = fn * 2654435761u + 777u + (g_sb_variante ? 7919u : 0u);
        for (int k = 0; k < 296; k++) {
            seed = seed * 1103515245u + 12345u; double u1 = ((seed >> 8) & 0xffff) / 65536.0 + 1e-6;
            seed = seed * 1103515245u + 12345u; double u2 = ((seed >> 8) & 0xffff) / 65536.0;
            double g = sqrt(-2.0 * log(u1)) * cos(2.0 * M_PI * u2);
            double v = dst[k] + sigma * g;
            if (v > 32767) v = 32767;
            if (v < -32768) v = -32768;
            dst[k] = (int16_t)lrint(v);
        }
    }
}
static int sb_cadrer(const uint8_t *bits, int16_t *iq, int cap_ech, int *marge_out)
{
    if (!calypso_rhea_dma_one_shot()) return 0;
    int total = calypso_rhea_dma_get_len_words() / 2;           /* echantillons de la page one-shot */
    if (total < 190) return 0;                                  /* 151 = fenetre NB : pas pour un SCH */
    if (total > cap_ech) total = cap_ech;
    int marge = 21;
    if (marge > total - 148) marge = total - 148;
    memset(iq, 0, (size_t)(2 * total) * sizeof *iq);
    sb_moduler(bits, iq + 2 * marge, g_dernier_sch.fn);
    *marge_out = marge;
    return total;
}
/* The ROM arms a one-shot SB window of 191 samples (one_shot=1
 * page_prog=191, TOA=24 CRC_OK), often AFTER the deposit of the SCH in the
 * tick; calypso_rif_rx_burst() then drops the burst (empty window on page 0:
 * TOA=0 SNR=12) and page 1 receives the burst of the NEXT frame at TOA 24,
 * hence a wrong CRC. The kept block is redelivered ONLY in a one-shot SB
 * window (>= 190), at most one tick after the deposit -- never in the NB
 * windows of the BCCH reading (one_shot=0 page=151), where it would replace
 * the BCCH burst -- and the ARM-tick offset is realigned on the tick of the
 * actual delivery, as the native delivery of an SCH in an SB window does. An
 * unclaimed block expires. */
int calypso_bsp_sb_retenter(void)
{
    if (!g_dernier_sch.valide || g_dernier_sch.livre_sb) return 0;
    if ((int32_t)(g_tick_courant - g_dernier_sch.tick) > 1) {   /* trop vieux : la ROM ne l'attend plus */
        g_dernier_sch.valide = 0; g_sb_expires++;
        return 0;
    }
    if (!calypso_rhea_dma_rx_armed() || !calypso_rhea_dma_one_shot()) return 0;
    int marge = 0;
    int total = sb_cadrer(g_dernier_sch.bits, g_sb_attente_iq, 512, &marge);
    if (!total) return 0;                                       /* armee, mais pas une fenetre SB (PM 64, NB 151) : on attend */
    calypso_rif_flush();
    calypso_bsp_rx_burst(0, g_dernier_sch.fn, g_sb_attente_iq, 2 * total);
    bsp.bursts_written++;
    g_sb_relivres++;
    g_dernier_sch.livre_sb = 1;
    /* The SCH is delivered at this tick: it is the one that sets the ARM-tick
     * offset, exactly like the native delivery in an SB window (bsp_ts0_livrer). */
    int64_t off = (int64_t)g_dernier_sch.fn - (int64_t)g_tick_courant - sb_fin_trame();
    /* Not after a RA_C: the mobile does not realign its fn on those SBs (RA_C offset 752, SB 758 at
     * the same moment); the SDCCHs would be delivered six frames off (a_cd FIRE KO). */
    if (!g_off_rac_vu) g_off_mobile = (int64_t)g_dernier_sch.fn - (int64_t)g_tick_courant;   /* what the mobile just learnt */
    int recale = (off != g_ts0_offset);
    if (!g_trx_cale && !g_bcch_verrou) g_ts0_offset = off;
    if (g_sb_relivres <= 100 || g_sb_relivres % 100 == 0)
        printf("  [sbwin] SB fn=%u RELIVRE au tick %u (depose au tick %u, fenetre SB armee apres) : page=%d marge=%d%s [%lu relivres, %lu expires]\n",
               g_dernier_sch.fn, g_tick_courant, g_dernier_sch.tick, total, marge,
               recale ? ", offset ARM-tick recale" : "", g_sb_relivres, g_sb_expires);
    return 1;
}
/* WINDOW ROUTING IN DEDICATED MODE (TI firmware). The bench deposits ONE burst (148 samples, TS0)
 * per tick; in dedicated mode the ROM arms TWO one-shot windows per frame: the power measurement
 * (64) then the NB reception (151), 215 samples. On silicon the measurement falls on another
 * timeslot and consumes emptiness. Here: measurement window (< 150) <- filler (bsp_remplissage),
 * NB window (150..189) <- the real burst, kept in a ring if the NB is not armed at the deposit;
 * pont.c runs the DSP between two deliveries within the same tick (pump loop). Only in TI
 * dedicated mode (calypso_bsp_set_dedie_ti); CALYPSO_BSP_NB_RETENTER=0 disables. */
#define NB_GARDE_N 4
static struct { int valide; uint32_t fn, tick; uint8_t bits[148]; } g_nb_garde[NB_GARDE_N];
static unsigned g_nb_garde_w;
static unsigned long g_nb_relivres, g_nb_expires, g_pm_servies;
static int nb_retenter_on(void)
{
    /* IN IDLE MODE TOO (not only in TI dedicated mode): the TI L1 measures neighbours on the paging
     * and BCCH frames, so at the deposit a measurement window (64) is often the armed one; without
     * the retention the paging blocks come out in error (NP_I error_flag=1), the DSC counter runs
     * out, the cell is reselected every ~8 s and no paging is received (no MT SMS/call).
     * CALYPSO_BSP_NB_RETENTER=0 disables, =2: dedicated mode only. */
    /* DEFAULT ACCORDING TO THE FIRMWARE. The idle-mode retention serves the TI L1 (MS_MAGNETITE):
     * without it, every NP_I in error, no incoming call. The osmocom firmware does not need it and
     * it empties its FB search (FBSB result=255, see continue_fb in bsp_ts0_livrer): 2 (dedicated
     * only) by default without MS_MAGNETITE. CALYPSO_BSP_NB_RETENTER forces the value in both cases. */
    static int on = -1;
    if (on < 0) {
        const char *e = calypso_getenv("CALYPSO_BSP_NB_RETENTER");
        on = (e && *e) ? atoi(e) : (calypso_getenv("MS_MAGNETITE") ? 1 : 2);
        BSP_LOG("NB_RETENTER=%d (%s) : garde NB %s", on, (e && *e) ? "force" : "defaut du firmware",
                on == 1 ? "en veille et en dedie" : on == 2 ? "en dedie seulement" : "coupee");
    }
    return on == 1 || (on == 2 && g_dedie_ti);
}
static int pm_sur_cellule(void);   /* does the measurement target the cell frequency? (fake air) */
static void nb_garde_poser(uint32_t fn, uint32_t tick, const uint8_t *bits)
{
    unsigned k = g_nb_garde_w++ % NB_GARDE_N;
    if (g_nb_garde[k].valide) g_nb_expires++;
    g_nb_garde[k].fn = fn; g_nb_garde[k].tick = tick; g_nb_garde[k].valide = 1;
    memcpy(g_nb_garde[k].bits, bits, 148);
}
/* AGC ON THE NORMAL BURSTS. The TI L1 derives the cell's IL from a_serv_demod[D_PM], which the DSP
 * measures on the samples of the NB window (l1ctl_pagc: IL = -(pm - programmed agc ...)). A fixed
 * amplitude of 30000 whatever the programmed TRF6151 gain would freeze pm at 169, let the PAGC loop
 * run idle (gain 112/124/136, IL 59/19, s_rxlev 69/101 > 63) and hide the antenna icon
 * (rxlev*5/64 >= 5, iconsShowSignal). The amplitude follows the gain of the window: 30000 at the
 * reference gain CALYPSO_BSP_NB_GAIN_REF (dB, anchor of the level seen by the mobile: +1 dB ->
 * rxlev -1; 124: floor gain 92, IL 83, s_rxlev 63/69; 148: gain 112..124, IL 131, s_rxlev 45 = the
 * neighbour ARFCN 1 measured by a_pm). CALYPSO_BSP_NB_AGC=0 goes back to a fixed 30000. Only the
 * NB windows: the measurement windows go through the a_pm model, which already adds the gain
 * (bsp_apm_de_mav). */
static int nb_amp(uint32_t tick)
{
    static int on = -1; static double ref;
    static unsigned long n, n_ecrete; static int g_min = 999, g_max = -999;
    if (on < 0) { const char *e = calypso_getenv("CALYPSO_BSP_NB_AGC"); on = !(e && *e == '0');
                  e = calypso_getenv("CALYPSO_BSP_NB_GAIN_REF"); ref = (e && *e) ? atof(e) : 148.0; }
    if (!on) return 30000;
    int g = faux_air_gain(tick, 1250);
    if (g == -999) return 30000;
    double a = 30000.0 * pow(10.0, (g - ref) / 20.0);
    if (a > 32000.0) { a = 32000.0; n_ecrete++; }
    if (a < 30.0) a = 30.0;
    n++; if (g < g_min) g_min = g; if (g > g_max) g_max = g;
    if (n <= 10 || n % 2000 == 0)
        printf("  [nb-agc] tick=%u gain=%d ref=%.0f -> amplitude %d [%lu NB, gain %d..%d, %lu ecretees]\n",
               tick, g, ref, (int)a, n, g_min, g_max, n_ecrete);
    return (int)a;
}
static void nb_moduler_amp(const uint8_t *bits, int16_t *iq, int marge, int amp)
{
    static double sym = -2;
    if (sym == -2) { const char *e = calypso_getenv("CALYPSO_BSP_NB_SYM"); sym = (e && *e) ? atof(e) : 0.3; }
    gmsk_moduler(bits, 148, amp, 0.0, 0.5, iq + 2 * marge);
    gmsk_elargir(iq + 2 * marge, 148, sym);
}
static void nb_moduler(const uint8_t *bits, int16_t *iq, int marge)
{
    nb_moduler_amp(bits, iq, marge, 30000);
}
static int nb_marge_ti(void)
{
    static int nb_marge = -1;
    if (nb_marge < 0) { const char *e = calypso_getenv("CALYPSO_BSP_NB_MARGE"); nb_marge = (e && *e) ? atoi(e) : 3;
                        if (nb_marge < 0 || nb_marge > 3) nb_marge = 3; }
    return nb_marge;
}
/* the armed measurement window receives enough to complete (the ROM then arms the next one): the head of
 * the last kept burst if the measurement targets the cell (its power is the right one), filler otherwise */
static int pm_servir(int nwin)
{
    static int16_t iq[2 * 256];
    const int16_t *src = bsp_remplissage();
    int k_rec = -1;
    for (int k = 0; k < NB_GARDE_N; k++)
        if (g_nb_garde[k].valide && (k_rec < 0 || (int32_t)(g_nb_garde[k].fn - g_nb_garde[k_rec].fn) > 0)) k_rec = k;
    int sur_cell = pm_sur_cellule();
    if (k_rec >= 0 && sur_cell) {
        memset(iq, 0, sizeof iq);
        nb_moduler(g_nb_garde[k_rec].bits, iq, 0);
        src = iq;
    }
    /* PROBE: in paging idle mode (Magnetite, IBA_I), when the cell measurement (ARFCN 1) falls to the
     * noise floor, tells whether the measurement window receives filler for lack of a kept burst
     * (k_rec<0) or because the fake air sees it tuned elsewhere. First 40 then 1 in 500. */
    {
        static unsigned long n, n_sans_garde, n_hors;
        n++; if (k_rec < 0) n_sans_garde++; if (!sur_cell) n_hors++;
        if (n <= 40 || n % 500 == 0)
            printf("  [pm] tick=%u nwin=%d garde=%s (fn %u, %d ticks) faux-air=%d cellule=%d -> %s "
                   "[%lu servies, %lu sans garde, %lu hors cellule]\n",
                   g_tick_courant, nwin, k_rec >= 0 ? "oui" : "non",
                   k_rec >= 0 ? g_nb_garde[k_rec].fn : 0u,
                   k_rec >= 0 ? (int)(g_tick_courant - g_nb_garde[k_rec].tick) : -1,
                   faux_air_arfcn(g_tick_courant, 5000), faux_air_cellule(),
                   src == iq ? "burst" : "remplissage", n, n_sans_garde, n_hors);
    }
    calypso_rif_flush();
    calypso_bsp_rx_burst(0, g_depot_fn, src, 2 * (nwin > 148 ? 148 : nwin));
    g_pm_servies++;
    return 1;
}
/* DMA PROBE (CALYPSO_BSP_DMA_TRACE=1): every state change of the RX channel (armed, one-shot, length)
 * with the tick, during TI dedicated mode. The rhea-dma model traces its registers on stderr, but
 * c54x_exe (verbosite.c) swallows them. Shows what the ROM arms on the TCH (TS1). 3000 lines. */
static int pm_sur_cellule(void)
{
    int cell = faux_air_cellule();
    if (cell < 0) return 1;
    int a = faux_air_arfcn(g_tick_courant, pm_limite_qbit(pm_rang_fenetre()));   /* see pm_rang_fenetre() */
    return a < 0 || a == cell;
}
static void dma_trace(const char *ou)
{
    static int on = -1; static unsigned n; static int armee_p = -1, os_p = -1, len_p = -1;
    if (on < 0) { const char *e = calypso_getenv("CALYPSO_BSP_DMA_TRACE"); on = (e && *e == '1'); }
    if (!on || !g_dedie_ti || n >= 3000) return;
    int armee = calypso_rhea_dma_rx_armed(), os = calypso_rhea_dma_one_shot(), len = calypso_rhea_dma_get_len_words() / 2;
    if (armee == armee_p && os == os_p && len == len_p) return;
    armee_p = armee; os_p = os; len_p = len; n++;
    printf("  [dma] tick=%u %s : armee=%d one_shot=%d fenetre=%d rif=%d\n", g_tick_courant, ou, armee, os, len, calypso_rif_level());
}
void calypso_bsp_dma_trace(const char *ou) { dma_trace(ou); }
/* trace of the armings and deposits in TI dedicated mode (calypso_rhea_dma.c [dma-arm], rx_burst [dma-dep]) */
int calypso_bsp_dma_trace_on(void)
{
    static int on = -1; static unsigned n;
    if (on < 0) { const char *e = calypso_getenv("CALYPSO_BSP_DMA_TRACE"); on = (e && *e == '1'); }
    return on && g_dedie_ti && n++ < 20000;
}
unsigned long calypso_bsp_routeur_bilan(char *buf, size_t n)
{
    snprintf(buf, n, " (routeur : %lu NB relivres, %lu expires, %lu mesures servies)", g_nb_relivres, g_nb_expires, g_pm_servies);
    return g_nb_relivres;
}
int calypso_bsp_nb_retenter(void)
{
    dma_trace("pump");
    if (!nb_retenter_on()) return 0;
    /* NOT ON THE TCH. pont.c already deposits there after the IDLE, window armed (PONT_TCH_DEPOT_IDLE).
     * Two kept NBs redelivered in the same tick, with a DMA interrupt pending (IFR=4000) and bit 14
     * removed from the IMR (3000), send the ROM to pc=0 (SP-CORRUPT); the TI L1 then reinitialises
     * the DSP ~1400 frames later and the mobile loses the cell on every call.
     * CALYPSO_BSP_NB_RETENTER_TCH=1: redeliver on the TCH too. */
    {
        static int sur_tch = -1;
        if (sur_tch < 0) { const char *e = calypso_getenv("CALYPSO_BSP_NB_RETENTER_TCH"); sur_tch = (e && *e == '1'); }
        /* BUT THE MEASUREMENT WINDOW, YES. After the NB of the TCH, the ROM arms in the same frame a
         * 64-sample PM window (neighbours); if nobody serves it, it stays armed at the next tick and the
         * TS2 burst of that frame falls into it (64 samples, never delivered as NB): one NB in two lost,
         * FACCH UA and SACCH lost -> Assignment Failure. The measurement is served (no NB redelivered),
         * one per tick, never on a pending DMA interrupt. CALYPSO_BSP_PM_TCH=0 disables. */
        if (!sur_tch && g_dedie_tn > 0 && g_dedie_genre == BSP_DEDIE_TCH) {
            static int pm_tch = -1; static uint32_t t_pm = 0xffffffffu;
            if (pm_tch < 0) { const char *e = calypso_getenv("CALYPSO_BSP_PM_TCH"); pm_tch = !(e && *e == '0'); }
            if (!pm_tch || t_pm == g_tick_courant) return 0;
            if (!calypso_rhea_dma_rx_armed() || !calypso_rhea_dma_one_shot()) return 0;
            int nw = calypso_rhea_dma_get_len_words() / 2;
            if (nw <= 0 || nw >= 150) return 0;
            if (bsp.dsp && (bsp.dsp->ifr & (1u << 14))) return 0;
            t_pm = g_tick_courant;
            return pm_servir(nw);
        }
    }
    /* [nb-etat] the DMA state as seen here (redelivered vs expired NBs). */
    if (g_dedie_ti) {
        static unsigned n_et; static uint32_t t_et;
        int ng = 0; for (int k = 0; k < NB_GARDE_N; k++) ng += g_nb_garde[k].valide;
        if (ng && (n_et < 300 || g_tick_courant - t_et > 500)) {
            n_et++; t_et = g_tick_courant;
            printf("  [nb-etat] tick=%u armee=%d one_shot=%d nwin=%d garde=%d genre=%d tn=%d\n", g_tick_courant,
                   (int)calypso_rhea_dma_rx_armed(), (int)calypso_rhea_dma_one_shot(),
                   calypso_rhea_dma_get_len_words() / 2, ng, (int)g_dedie_genre, g_dedie_tn);
        }
    }
    if (!calypso_rhea_dma_rx_armed() || !calypso_rhea_dma_one_shot()) return 0;
    int nwin = calypso_rhea_dma_get_len_words() / 2;
    if (nwin <= 0) return 0;
    /* ON THE TCH: ONE DELIVERY PER TICK, NEVER ON A PENDING DMA INTERRUPT (two kept NBs redelivered
     * in the same tick with IFR bit 14 pending send the ROM to pc=0, jump outside the ROM from
     * 1:e9bd). Without redelivery, 70 % of the TCH NB windows stay empty (TOA=0, SNR=0): downlink
     * FACCH (UA) never decoded. */
    if (g_dedie_tn > 0 && g_dedie_genre == BSP_DEDIE_TCH) {
        static uint32_t t_livre = 0xffffffffu;
        if (t_livre == g_tick_courant) return 0;
        if (bsp.dsp && (bsp.dsp->ifr & (1u << 14))) return 0;
        if (nwin >= 150 && nwin < 190) t_livre = g_tick_courant;
    }
    if (nwin < 150) return pm_servir(nwin);
    if (nwin >= 190) return 0;                                  /* SB window: calypso_bsp_sb_retenter */
    /* the NB is armed: the oldest kept burst still fresh (2 ticks) */
    int k_min = -1;
    for (int k = 0; k < NB_GARDE_N; k++) {
        if (!g_nb_garde[k].valide) continue;
        if ((int32_t)(g_tick_courant - g_nb_garde[k].tick) > 2) { g_nb_garde[k].valide = 0; g_nb_expires++; continue; }
        if (k_min < 0 || (int32_t)(g_nb_garde[k].fn - g_nb_garde[k_min].fn) < 0) k_min = k;
    }
    if (k_min < 0) return 0;
    int marge = nb_marge_ti();
    static int16_t iq[2 * 256];
    int total = nwin > marge + 148 ? nwin : marge + 148;
    if (total > 256) total = 256;
    memset(iq, 0, sizeof iq);
    nb_moduler_amp(g_nb_garde[k_min].bits, iq, marge, nb_amp(g_tick_courant));
    calypso_rif_flush();
    calypso_bsp_rx_burst(0, g_nb_garde[k_min].fn, iq, 2 * total);
    bsp.bursts_written++;
    g_nb_garde[k_min].valide = 0;
    g_nb_relivres++;
    if (g_nb_relivres <= 40 || g_nb_relivres % 500 == 0)
        printf("  [nbwin] NB fn=%u p51=%u RELIVRE au tick %u (depose au tick %u) : fenetre=%d [%lu relivres, %lu expires, %lu mesures servies]\n",
               g_nb_garde[k_min].fn, g_nb_garde[k_min].fn % 51u, g_tick_courant, g_nb_garde[k_min].tick, nwin,
               g_nb_relivres, g_nb_expires, g_pm_servies);
    return 1;
}
/* pont.c: trace EVERY SCH for `ticks` ticks (after an FB detection, or when
 * the ARM posts an SB task), to see the exact deposit around the attempts. */
void calypso_bsp_sb_trace(unsigned ticks)
{
    uint32_t j = g_tick_courant + ticks;
    if ((int32_t)(j - g_sb_trace_jusqua) > 0) g_sb_trace_jusqua = j;
}
/* SB_FIN_TRAME: the BCCH reading (p51 2..5) tells where the L1 believes the block is. NB windows
 * armed on 3..6 = stream one frame ahead (offset - 1); on 1..4 = behind (offset + 1). Two blocks
 * in a row before correcting; a well aligned block locks the offset (re-read SCHs no longer
 * realign it), unlocked when no BCCH window is seen any more (the L1 has lost the cell). */
static void bsp_bcch_calage(uint32_t tick, unsigned p, bool nb)
{
    static uint8_t arme[7];
    static int tard, tot;
    if (p >= 1 && p <= 6) {
        arme[p] = nb;
        if (nb) g_bcch_vu_tick = tick;
    }
    if (p == 6) {
        bool bon = arme[2] && arme[3] && arme[4] && arme[5] && !arme[6];
        bool retard = !arme[2] && arme[3] && arme[4] && arme[5] && arme[6];
        bool avance = arme[1] && arme[2] && arme[3] && arme[4] && !arme[5];
        if (retard) { tard++; tot = 0; }
        else if (avance) { tot++; tard = 0; }
        else if (bon) {
            tard = tot = 0;
            if (!g_bcch_verrou) printf("  [bcch] tick=%u bloc BCCH dans ses fenetres (2..5) : offset %lld verrouille\n",
                                       tick, (long long)g_ts0_offset);
            g_bcch_verrou = 1;
        }
        if (tard >= 2 || tot >= 2) {
            int d = tard >= 2 ? -1 : +1;
            g_ts0_offset = (g_ts0_offset + d + BSP_FN_MAX) % BSP_FN_MAX;
            if (g_off_mobile != INT64_MIN) g_off_mobile += d;   /* [2026-10-22 14:20] le datage des RACH suit */
            printf("  [bcch] tick=%u fenetres NB sur p51 %s : offset %+d -> %lld\n",
                   tick, d < 0 ? "3..6 (flux en avance)" : "1..4 (flux en retard)", d, (long long)g_ts0_offset);
            tard = tot = 0;
        }
        memset(arme, 0, sizeof arme);
    }
    if (g_bcch_verrou && (uint32_t)(tick - g_bcch_vu_tick) > 160u) {
        g_bcch_verrou = 0;
        printf("  [bcch] tick=%u plus de lecture BCCH depuis %u trames : offset deverrouille\n", tick, tick - g_bcch_vu_tick);
    }
}
/* THE SAME ALIGNMENT ON THE CCCH BLOCKS (paging). In idle mode the L1 hardly re-reads the BCCH:
 * after a dedicated phase (LU, call) the offset could stay wrong by a few frames, every NP_I in
 * error, no page seen (MT SMS, incoming call) until the cell is lost. A series of NB windows must
 * start on a CCCH block start (6, 12, 16, 22, 26, 32, 36, 42, 46); shifted by d (|d| <= 3) twice
 * in a row -> offset - d. The BCCH (2..5) stays with bsp_bcch_calage(). Outside dedicated mode,
 * offset not locked by the BCCH. CALYPSO_BSP_CCCH_CALAGE=0 disables. */
static void bsp_ccch_calage(uint32_t tick, unsigned p, bool nb)
{
    static const unsigned debuts[] = { 6, 12, 16, 22, 26, 32, 36, 42, 46 };
    static int on = -1, prec_p = -1, debut = -1, d_prec = 99, d_n;
    if (on < 0) { const char *e = calypso_getenv("CALYPSO_BSP_CCCH_CALAGE"); on = !(e && *e == '0'); }
    if (!on) return;
    if (nb && debut >= 0 && (int)p == prec_p + 1) { prec_p = (int)p; return; }   /* la serie continue */
    if (debut >= 0 && prec_p - debut + 1 >= 4 && !g_bcch_verrou) {               /* une serie vient de finir */
        int d = 99;
        for (unsigned k = 0; k < sizeof debuts / sizeof debuts[0]; k++) {
            int e = debut - (int)debuts[k];
            if (e >= -3 && e <= 3 && (d == 99 || abs(e) < abs(d))) d = e;
        }
        if (d == 0) d_n = 0, d_prec = 0;
        else if (d != 99) {
            d_n = (d == d_prec) ? d_n + 1 : 1; d_prec = d;
            if (d_n >= 2) {
                g_ts0_offset = (g_ts0_offset - d + BSP_FN_MAX) % BSP_FN_MAX;
                if (g_off_mobile != INT64_MIN) g_off_mobile -= d;   /* le datage des RACH suit */
                printf("  [ccch] tick=%u fenetres NB sur p51 %d.. au lieu de %d.. : offset %+d -> %lld\n",
                       tick, debut, debut - d, -d, (long long)g_ts0_offset);
                d_n = 0; d_prec = 99;
            }
        }
    }
    debut = nb ? (int)p : -1;
    prec_p = nb ? (int)p : -1;
}
static void bsp_ts0_livrer(uint32_t tick_fn, unsigned i)
{
    static int16_t iq[2 * 512];
    const uint8_t *bits = g_ts0[i].bits;
    /* For the [ts0] trace: whether the delivered frame belongs to the
     * dedicated channel, and whether its burst does come from that timeslot.
     * Needed to compare the window geometry of an SDCCH block with that of a
     * BCCH block. */
    bool trame_dediee = false, burst_dedie = false;
    {   /* On the frames of the dedicated channel, its timeslot is what counts. */
        /* Two verdicts: the 45.002 table (channel frames) and what the firmware
         * says (window timeslot). MONTANT_TPU_TN=1 trusts the second; by default
         * the first is kept and the disagreements are LOGGED, because the TICK
         * value may be one frame behind the real programming of the window. */
        /* WITHOUT AN ARMED CHANNEL, NO FRAME IS "OURS". bsp_dedie_trame() reads
         * g_dedie_ss, which is 0 while nothing is armed: outside a connection
         * one frame in eight would otherwise be declared part of the channel,
         * bsp_dedie_bits() would return NULL on its g_dedie_tn <= 0 guard and
         * g_dedie_manques would mix camping time (where playing TS0 is right)
         * with the real holes of the channel. */
        bool a_nous = g_dedie_tn > 0 && bsp_dedie_trame(g_ts0[i].fn);
        {
            static int suit_tpu = -1;
            if (suit_tpu < 0) { const char *e = calypso_getenv("MONTANT_TPU_TN"); suit_tpu = (e && *e == '1'); }
            bool selon_tpu = (bsp_fenetre_tn() == g_dedie_tn);
            if (g_dedie_tn > 0 && selon_tpu != a_nous) {
                static unsigned long n;
                if (++n <= 40 || (n % 200) == 0) {
                    /* printf and not BSP_LOG: CALYPSO_DEBUG=BSP turns on all
                     * the BSP traces and makes the bench lose real time; this
                     * one is already capped (40, then 1 in 200) and serves to
                     * judge the SACCH of the SDCCH/8. */
                    printf("  [BSP] dedie : DESACCORD fn=%u p51=%u p102=%u : table=%d "
                            "tpu=%d (offset=%d ref=%d, tn_fenetre=%d) [%lu fois]\n",
                            g_ts0[i].fn, g_ts0[i].fn % 51u, g_ts0[i].fn % 102u,
                            (int)a_nous, (int)selon_tpu, g_bsp_tpu_offset,
                            g_tpu_ref, bsp_fenetre_tn(), n);
                }
            }
            if (suit_tpu) {
                a_nous = selon_tpu;
            }
        }
        trame_dediee = a_nous;
        const uint8_t *d = a_nous ? bsp_dedie_bits(g_ts0[i].fn) : NULL;
        /* SACCH/TF PROBE: on TCH/F the speech and the FACCH follow the
         * 26-multiframe, the SACCH the 104 (even TS: fn%26 == 12, TS2 block at
         * fn%104 = 38, 64, 90, 12). Logs what the BSP plays on those frames. */
        if (g_dedie_genre == BSP_DEDIE_TCH && a_nous && (g_ts0[i].fn % 26u) == 12u) {
            static unsigned long n_sacch;
            if (n_sacch++ < 24) {
                int uns = -1;
                if (d) { uns = 0; for (int k = 0; k < 148; k++) uns += (d[k] & 1); }
                printf("  [sacch_tf] tick=%u fn=%u fn%%104=%u TS%d burst=%s uns=%d one_shot=%d nwin=%d\n",
                       tick_fn, g_ts0[i].fn, g_ts0[i].fn % 104u, g_dedie_tn,
                       d ? "oui" : "NON", uns, (int)calypso_rhea_dma_one_shot(),
                       calypso_rhea_dma_one_shot() ? calypso_rhea_dma_get_len_words() / 2 : 0);
            }
            /* The bits themselves, to decode the SACCH outside the DSP (A5 +
             * gsm0503_xcch_decode): if it decodes there, the defect is in the
             * BSP/ROM path, otherwise in what the BSP receives.
             * 156-byte records: tick BE32, fn BE32, 148 bits 0/1.
             * 256 bursts at most (64 blocks), file reset at startup. */
            static FILE *f_sacch;
            static unsigned n_sacch_bits;
            if (!f_sacch && n_sacch_bits == 0)
                f_sacch = fopen("/dev/shm/calypso_sacch_tf.bin", "wb");
            if (f_sacch && d && n_sacch_bits < 256) {
                uint8_t h[8] = { tick_fn >> 24, tick_fn >> 16, tick_fn >> 8, tick_fn,
                                 g_ts0[i].fn >> 24, g_ts0[i].fn >> 16, g_ts0[i].fn >> 8, g_ts0[i].fn };
                fwrite(h, 1, 8, f_sacch);
                fwrite(d, 1, 148, f_sacch);
                fflush(f_sacch);
                if (++n_sacch_bits == 256) { fclose(f_sacch); f_sacch = NULL; }
            }
        }
        /* ALL THE TCH BURSTS, TO DECODE THEM OUTSIDE THE DSP. The SACCH/TF
         * alone (calypso_sacch_tf.bin) says nothing about the speech nor the
         * FACCH. Same format as calypso_sacch_tf.bin (tick BE32, fn BE32,
         * 148 bits 0/1), file reset on every TCH, 20000 bursts at most
         * (~92 s). tools/comparer_parole.py decodes them (A5 + FR/FACCH) and
         * compares them with /dev/shm/calypso_add_dl.bin. */
        if (g_dedie_genre == BSP_DEDIE_TCH && a_nous && d) {
            static FILE *f_tch;
            static unsigned n_tch;
            static uint32_t tick_prec;
            if (!f_tch || (uint32_t)(tick_fn - tick_prec) > 500u) {   /* nouvel appel */
                if (f_tch) fclose(f_tch);
                f_tch = fopen("/dev/shm/calypso_tch_dl.bin", "wb");
                n_tch = 0;
            }
            tick_prec = tick_fn;
            {   /* for the downlink FACCH prosthesis (montant.c) */
                unsigned k = g_ts0[i].fn % TCH_ANNEAU;
                g_tch_anneau[k].fn = g_ts0[i].fn; memcpy(g_tch_anneau[k].bits, d, 148); g_tch_anneau[k].ok = 1;
                g_tch_dernier_fn = g_ts0[i].fn; g_tch_dernier_ok = 1;
            }
            if (f_tch && n_tch < 20000) {
                uint8_t h[8] = { tick_fn >> 24, tick_fn >> 16, tick_fn >> 8, tick_fn,
                                 g_ts0[i].fn >> 24, g_ts0[i].fn >> 16, g_ts0[i].fn >> 8, g_ts0[i].fn };
                fwrite(h, 1, 8, f_tch);
                fwrite(d, 1, 148, f_tch);
                if ((++n_tch % 64) == 0) fflush(f_tch);
            }
        }
        if (d) {
            bits = d;
            burst_dedie = true;
            g_dedie_joues++;
            if (g_dedie_joues <= 40 || g_dedie_joues % 1000 == 0) {
                /* How many 1s in the 148 bits: a filler burst always has the
                 * same number, a real block varies. Enough to see at a glance
                 * whether the BTS really emits the SDCCH. */
                int uns = 0;
                for (int k = 0; k < 148; k++) uns += (d[k] & 1);
                BSP_LOG("dedie : TS%d joue (fn=%u p51=%u uns=%d, %lu fois)",
                        g_dedie_tn, g_ts0[i].fn, g_ts0[i].fn % 51u, uns, g_dedie_joues);
            }
        } else if (a_nous) {
            /* The frame belongs to the channel, but no burst of that timeslot
             * arrived: the block will be incomplete and fail the Fire code. */
            g_dedie_manques++;
            if (g_dedie_manques <= 40 || g_dedie_manques % 1000 == 0) {
                BSP_LOG("dedie : TS%d MANQUANT pour fn=%u p51=%u (%lu fois) - "
                        "le bloc partira incomplet",
                        g_dedie_tn, g_ts0[i].fn, g_ts0[i].fn % 51u, g_dedie_manques);
            }
        }
        if (a_nous) {
            bsp_dedie_etat(NULL);
        }
    }
    int sb = bsp_ts0_est_sb(bits), fcch = bsp_ts0_est_fcch(bits);
    memset(iq, 0, sizeof iq);
    const bool one_shot = calypso_rhea_dma_one_shot();
    const bool armee = calypso_rhea_dma_rx_armed();
    const int page_prog = calypso_rhea_dma_get_len_words() / 2;   /* ALGTH/2 words = samples per page, ONE_SHOT or not */
    const int rif_avant = calypso_rif_level();
    int nwin = one_shot ? page_prog : 0;
    /* CALYPSO_BSP_NB_MARGE: the TI L1 aims at an NB TOA of 3 (l1ctl_toa_update: ISH =
     * estimated_TOA - 16 qbits); with margin 3 the DSP measures 5, hence +8 qbits of tpu_offset
     * every period without end (the bench does not move the burst). Magnetite: 1. */
    static int nb_marge = -1;
    if (nb_marge < 0) { const char *e = calypso_getenv("CALYPSO_BSP_NB_MARGE"); nb_marge = (e && *e) ? atoi(e) : 3;
                        if (nb_marge < 0 || nb_marge > 3) nb_marge = 3; }
    int marge = nwin >= 190 ? 21 : nwin >= 150 ? nb_marge : 0;
    /* fake air: NB window (start of frame) -> synthesizer programmed before 1250 qbits;
     * SB window (end of frame), PM or continuous -> last programming of the frame. */
    bool hors_freq = false;
    if (faux_air_cellule() >= 0) {
        int a = faux_air_arfcn(tick_fn, (nwin >= 150 && nwin < 190) ? 1250 : 5000);
        hors_freq = (a >= 0 && a != faux_air_cellule());
        static unsigned n_hors, n_dans;
        if (hors_freq) n_hors++; else if (a >= 0) n_dans++;
        if (hors_freq && (n_hors <= 20 || n_hors % 5000 == 0))
            printf("  [faux-air] tick=%u fn=%u fenetre=%d accorde sur %d : bruit [%u hors, %u sur la cellule]\n",
                   tick_fn, g_ts0[i].fn, nwin, a, n_hors, n_dans);
        if (hors_freq) sb = fcch = 0;
    }
    /* realistic air: which burst (downlink of this timeslot, uplink of the mobile, or noise) and at which
     * level. A measurement window (< 150) targets the timeslot derived from the synth setting; NB/SB: TS0. */
    int amp_ts0 = (one_shot && nwin >= 150 && nwin < 190) ? nb_amp(tick_fn) : 30000;
    const uint8_t *bits_mod = bits;
    if (air_niveaux_on() && !hors_freq) {
        unsigned pos = (one_shot && nwin > 0 && nwin < 150) ? air_pos_fenetre(tick_fn) : 0;
        const uint8_t *dl = pos ? bsp_autres_bits(g_ts0[i].fn, pos) : bits;
        bits_mod = air_choisir(tick_fn, (nwin >= 150 && nwin < 190) ? 1250 : 5000, g_ts0[i].fn, pos, dl, &amp_ts0);
        if (!bits_mod) { hors_freq = true; bits_mod = bits; }
        if (bits_mod != bits || hors_freq) sb = fcch = 0;
    }
    /* An SCH is memorised here (g_dernier_sch); if the SB window only arms
     * after this deposit, calypso_bsp_sb_retenter() will deliver it there. */
    const char *sb_action = "cadree comme un burst normal";
    if (sb && sb_fenetre_on()) {
        g_dernier_sch.fn = g_ts0[i].fn; g_dernier_sch.tick = tick_fn;
        memcpy(g_dernier_sch.bits, bits, 148);
        g_dernier_sch.livre_sb = (one_shot && nwin >= 190);
        g_dernier_sch.valide = 1;
        if (!armee) sb_action = "DMA NON ARMEE (le RIF jette) : SCH memorise pour la fenetre SB";
        else if (!(one_shot && nwin >= 190)) sb_action = "autre fenetre (PM/NB/continue) : SCH memorise pour la fenetre SB";
    }
    /* TRX mode: the uplink NB burst played here is memorised; at phase A of the next frame,
     * calypso_bsp_trx_ul_prothese() writes its exact bits into the result page of the extcode
     * (see the function). */
    if (g_trx_cale && !sb && !fcch && !bsp_ts0_est_rach(bits)) {
        g_trx_ul.fn = g_ts0[i].fn; g_trx_ul.tick = tick_fn; memcpy(g_trx_ul.bits, bits, 148); g_trx_ul.valide = 1;
    }
    /* TI dedicated mode (routing, see calypso_bsp_nb_retenter): the NB is not armed at the deposit ->
     * the burst goes into the keep ring, and an armed measurement window receives filler instead */
    dma_trace("depot");
    bool devie = false;
    /* NEVER IN CONTINUOUS DMA (FB search). An NB of the FB search sent to the ring without delivering
     * anything (neither TS0 nor the seven filler timeslots) would make the ROM count only the FCCH/SCH
     * ticks (FB TOA ~1255 instead of ~8 frames) and the osmocom firmware would place its SB window 7
     * frames too early (FBSB result=255 in a loop). See the full frame below. */
    const bool continue_fb = armee && !one_shot;
    if (!sb && !fcch && !hors_freq && !continue_fb && nb_retenter_on() && !(one_shot && nwin >= 150 && nwin < 190)) {
        nb_garde_poser(g_ts0[i].fn, tick_fn, bits);
        if (one_shot && nwin > 0 && nwin < 150) pm_servir(nwin);
        devie = true;
    }
    if (hors_freq) { if (air_niveaux_on()) air_bruit(iq + 2 * marge, 148); else faux_air_bruit(iq + 2 * marge, 148); }
    else if (sb) {
        sb_moduler(bits, iq + 2 * marge, g_ts0[i].fn);   /* see sb_moduler() */
        if (air_niveaux_on()) air_echelle(iq + 2 * marge, 148, amp_ts0);   /* same level as the cell */
    }
    else    gmsk_moduler(bits_mod, 148, amp_ts0, 0.0, 0.5, iq + 2 * marge);
    if (!sb && !fcch && !hors_freq) {
        static double sym = -2;
        if (sym == -2) { const char *e = calypso_getenv("CALYPSO_BSP_NB_SYM"); sym = (e && *e) ? atof(e) : 0.3; }
        gmsk_elargir(iq + 2 * marge, 148, sym);
    }
    int total = marge > 0 ? (nwin > marge + 148 ? nwin : marge + 148) : 148;
    if (total > 256) total = 256;
    if (sb) {
        /* PROBE: does the SB land in a REAL SB window? The alignment of
         * g_ts0_offset and the 21-sample margin require nwin >= 190 (382-word
         * window). Otherwise the SB is framed like a normal burst (margin 3)
         * and the alignment does not happen through this path. Both cases are
         * counted. */
        static unsigned n_sb, n_sb_fenetre;
        n_sb++;
        if (one_shot && nwin >= 190) n_sb_fenetre++;
        if (one_shot && nwin >= 190) sb_action = "FENETRE SB (one-shot)";
        /* All the SCHs of the first 200, then 1 in 10: it must be visible on
         * every resync. */
        if (n_sb <= 200 || n_sb % 10 == 0 || (int32_t)(g_sb_trace_jusqua - tick_fn) > 0) {
            printf("  [sbwin] SB #%u fn=%u p51=%u tick=%u : armee=%d one_shot=%d page_prog=%d rif_avant=%d nwin=%d marge=%d -> %s (dans une vraie fenetre SB : %u/%u)\n",
                   n_sb, g_ts0[i].fn, g_ts0[i].fn % 51u, tick_fn, (int)armee, (int)one_shot, page_prog, rif_avant, nwin, marge,
                   sb_action, n_sb_fenetre, n_sb);
        }
    }
    if (sb && one_shot && nwin >= 190) {
        int64_t off = (int64_t)g_ts0[i].fn - (int64_t)tick_fn - sb_fin_trame();
        if (!g_off_rac_vu) g_off_mobile = (int64_t)g_ts0[i].fn - (int64_t)tick_fn;   /* [2026-10-22] voir sb_retenter */
        if (off != g_ts0_offset) printf("  [ts0] SB fn=%u livree au tick %u dans une fenetre SB : offset ARM-tick = %lld%s\n",
                                        g_ts0[i].fn, tick_fn, (long long)off, g_ts0_offset == INT64_MIN ? " (premier calage)" : " (recalage)");
        if (!g_trx_cale && !g_bcch_verrou) g_ts0_offset = off;
    }
    /* One line per delivered frame: the window alignment probe (nwin > 0 is
     * true for EVERY normal burst with the one-shot DMA, so it would print
     * continuously). Behind CALYPSO_BSP_TS0_DEBUG=1; by default, the first 20
     * deliveries then one in 5000, enough to see that the stream runs without
     * flooding the console. */
    { static int ts0_dbg = -1;
      if (ts0_dbg < 0) { const char *e = calypso_getenv("CALYPSO_BSP_TS0_DEBUG"); ts0_dbg = (e && *e && *e != '0'); }
      static unsigned nl, nd;
      /* The frames of the dedicated channel are rare (4 in 51, plus the SACCH)
       * and only last for a connection: ALL of them are traced, up to 300,
       * independently of CALYPSO_BSP_TS0_DEBUG. It is the only way to compare
       * their window and margin with those of a BCCH block. */
      bool trace = ts0_dbg ? (nl < 400 || nl % 5000 == 0 || fcch || sb || nwin > 0)
                           : (nl < 20 || nl % 5000 == 0);
      nl++;
      if (trame_dediee && nd < 300) { trace = true; nd++; }
      /* The first 400 normal BCCH frames (p51 2..5) tell which DMA window the ROM had
       * armed when the burst arrived (diagnoses an SB decoded with every BCCH block in
       * CRC KO, BS_I error_flag=1). */
      if (sb_fin_trame() && g_dedie_tn <= 0)
          bsp_bcch_calage(tick_fn, g_ts0[i].fn % 51u, nwin >= 150 && nwin < 190);
      if (sb_fin_trame() && g_dedie_tn <= 0 && !g_dedie_ti)
          bsp_ccch_calage(tick_fn, g_ts0[i].fn % 51u, nwin >= 150 && nwin < 190);
      { static unsigned nbcch;
        const unsigned p = g_ts0[i].fn % 51u;
        if (p >= 2 && p <= 5 && nbcch < 400) { trace = true; nbcch++; } }
      if (trace) printf("  [ts0] tick=%u fn=%u p51=%u %s fenetre=%d marge=%d rif_avant=%d%s\n",
                        tick_fn, g_ts0[i].fn, g_ts0[i].fn % 51u,
                        sb ? "SB" : fcch ? "FCCH" : "NB", nwin, marge, calypso_rif_level(),
                        trame_dediee ? (burst_dedie ? "  <- canal dedie" : "  <- canal dedie, BURST MANQUANT") : ""); }
    g_ts0[i].joue = 1;
    if (devie) return;                       /* garde / remplissage faits ci-dessus */
    calypso_bsp_rx_burst(0, g_ts0[i].fn, iq, 2 * total);
    bsp.bursts_written++;
    /* Continuous DMA (FB search): the ROM counts 1250 samples per frame and
     * the ARM turns its FB TOA into frames (ntdma) and bits with that
     * constant, so every tick must carry a WHOLE frame: TS0 (padded to 156
     * by calypso_bsp_rx_burst) then seven filler timeslots. With TS0 alone
     * (156 samples per tick) the SB window was armed eight ticks too late
     * and no SB ever decoded. One-shot windows take TS0 only. */
    if (!one_shot) {
        extern uint16_t g_remplissage_ts0[];   /* forward: defined below (filler, zeros by default) */
        static int16_t iq_autre[2 * 148];
        for (int tn = 1; tn < 8; tn++) {
            const uint8_t *b = bsp_autres_bits(g_ts0[i].fn, (unsigned)tn);
            int amp_autre = 30000;
            if (air_niveaux_on()) {   /* [2026-10-23] air realiste, voir air_choisir() */
                b = air_choisir(tick_fn, 5000, g_ts0[i].fn, (unsigned)tn, b, &amp_autre);
                if (!b) {
                    static int16_t bruit_autre[2 * 148];
                    air_bruit(bruit_autre, 148);
                    calypso_bsp_rx_burst((uint8_t)tn, g_ts0[i].fn, bruit_autre, 2 * 148);
                    continue;
                }
            }
            if (!b) {
                calypso_bsp_rx_burst((uint8_t)tn, g_ts0[i].fn, bsp_remplissage(), 2 * 148);
                continue;
            }
            memset(iq_autre, 0, sizeof iq_autre);
            gmsk_moduler(b, 148, amp_autre, 0.0, 0.5, iq_autre);
            {   /* same widening as the normal bursts of TS0 */
                static double sym2 = -2;
                if (sym2 == -2) { const char *e = calypso_getenv("CALYPSO_BSP_NB_SYM"); sym2 = (e && *e) ? atof(e) : 0.3; }
                gmsk_elargir(iq_autre, 148, sym2);
            }
            calypso_bsp_rx_burst((uint8_t)tn, g_ts0[i].fn, iq_autre, 2 * 148);
            g_autres_joues++;
        }
        if ((g_autres_joues % 5000) == 1) {
            BSP_LOG("trame complete : %lu bursts hors TS0 stockes, %lu joues "
                    "(le bourrage a zero ne sert plus que pour les manquants)",
                    g_autres_stockes, g_autres_joues);
        }
    }
}
/* THE BENCH CLOCK IS THE DSP'S, NOT THE WALL'S.
 *
 * bsp_ts0_service() plays BTS frame `tick + g_ts0_offset`. The ARM tick
 * advances at the pace of the emulated C54x (~6.7 ms per frame) while the BTS
 * runs in real time (4.615 ms). The DSP therefore consumes the ring 20 %
 * slower than it fills, and the lag is bounded by nothing. Catching up on the
 * DSP side is impossible (it already runs flat out), so the BTS must slow
 * down: the frame the DSP requests is published here, and pont.py
 * (pont/trx.py, class Clock) brakes its clock -- hence the BTS's IND CLOCK --
 * so as never to run ahead by more than a few frames.
 *
 * Format, 16 bytes: seq (non-zero, incremented on every write), cale (0 before
 * the first alignment on SB: the clock stays free), requested BTS frame, ARM
 * tick. CALYPSO_BSP_HORLOGE=0 disables the publication. */
#define SHM_HORLOGE "/dev/shm/calypso_horloge"
/* BTS frame of a tick, from the alignment of the TS0 stream (SB, then BCCH lock): the frame the
 * phone's L1 believes it lives in at this tick. pont.c (tx_tsp_fn) dates the uplink bursts with it
 * when the firmware does not set a_a5fn (TI L1: the RACH would otherwise leave as a "bare TICK" and
 * the air would not place it). 0 if not aligned yet. */
int calypso_bsp_fn_bts_du_tick(uint32_t tick, uint32_t *fn_bts)
{
    if (g_ts0_offset == INT64_MIN) return 0;
    int64_t w = ((int64_t)tick + g_ts0_offset) % (int64_t)BSP_FN_MAX;
    if (w < 0) w += BSP_FN_MAX;
    *fn_bts = (uint32_t)w;
    return 1;
}
static void bsp_horloge_publier(uint32_t fn_bts, uint32_t tick_fn, int cale)
{
    static int actif = -1, fd = -1;
    static uint32_t seq;
    if (actif < 0) {
        const char *e = calypso_getenv("CALYPSO_BSP_HORLOGE");
        actif = (e && *e == '0') ? 0 : 1;
    }
    if (!actif) {
        return;
    }
    if (fd < 0) {
        fd = open(SHM_HORLOGE, O_CREAT | O_WRONLY, 0644);
        if (fd < 0) {
            actif = 0;
            return;
        }
    }
    if (++seq == 0) {
        seq = 1;            /* 0 veut dire « rien de publie » cote lecteur */
    }
    uint32_t rec[4] = { seq, (uint32_t)cale, fn_bts, tick_fn };
    if (pwrite(fd, rec, sizeof rec, 0) != (ssize_t)sizeof rec) {
        /* nothing to do: the free clock takes over on the bridge side */
    }
}

/* THE SECOND CHANCE. The firmware posts two SB tasks on two consecutive
 * frames; the first falls on the FCCH frame (null burst), empty window, chance
 * lost; the second on the SCH. The BSP already has the SCH of the next frame
 * in its ring (the BTS is ahead). It is therefore delivered in the window of
 * attempt 1, in waveform D, then the ROM's result is read in the API RAM at
 * the next tick (a_sch R page: B_BLUD without B_SCH_CRC, T2 <= 25, T3' <= 4):
 *   - decoded: the firmware set its clock on fn=SCH at that tick; the ARM-tick
 *     offset is realigned by one frame (fn_sch - tick), the frame of the next
 *     tick is fn_sch+1, the stream stays contiguous (the SCH itself is not
 *     replayed: no need);
 *   - missed: nothing changes, the native SCH goes out in attempt 2
 *     (waveform A).
 * Replay: A alone misses 20 %, A+D in common 7 %. CALYPSO_BSP_SB_DOUBLE=0
 * disables. */
static struct { int actif; uint32_t fn_sch; uint32_t tick; uint16_t a0[2]; unsigned n, ok, ko; } g_sc;
static int sb_double_on(void)
{
    static int on = -1;
    /* DISABLED BY DEFAULT. Every decode in attempt 1 advances by ONE frame the
     * index the DSP requests from the BTS; the bridge's DL margin cannot
     * rebuild itself (the BTS does not go faster than real time), and long
     * calls then end in "LOS during dedicated mode" with a downlink TCH that
     * degrades progressively (a_dd err, FIRE). CALYPSO_BSP_SB_DOUBLE=1 enables. */
    if (on < 0) { const char *e = calypso_getenv("CALYPSO_BSP_SB_DOUBLE"); on = (e && *e == '1') ? 1 : 0;
                  if (on) BSP_LOG("SB_DOUBLE on : le SCH suivant est aussi livre (forme D) dans la fenetre de la tentative 1"); }
    return on;
}
/* a_sch[0..4] of R page pg: words (0x50|0x78 + 0x1E)/2 of the API RAM (calypso_api.h) */
static const uint16_t *sc_a_sch(int pg)
{
    if (!bsp.dsp || !bsp.dsp->api_ram) return NULL;
    return &bsp.dsp->api_ram[((pg ? 0x78u : 0x50u) + 0x1Eu) / 2];
}
static int sc_rom_a_decode(void)
{
    for (int pg = 0; pg < 2; pg++) {
        const uint16_t *a = sc_a_sch(pg);
        if (!a || a[0] == g_sc.a0[pg]) continue;
        if ((a[0] & 0x8100) != 0x8000) continue;               /* B_BLUD sans B_SCH_CRC */
        uint32_t sb = (uint32_t)a[3] | ((uint32_t)a[4] << 16);
        unsigned t2 = (sb >> 18) & 0x1f, t3p = ((sb >> 24) & 1) | ((sb >> 15) & 6);
        if (t2 <= 25 && t3p <= 4) return 1;
    }
    return 0;
}
static void bsp_ts0_service(uint32_t tick_fn)
{
    /* phone 2 of the TRX (TS1): it emits no SB, the fn/tick offset is given
     * (CALYPSO_BSP_TRX_OFFSET = firmware fn - tick, -1 measured on phone 1) + delta */
    static int fixe = -1;
    if (fixe < 0) {
        const char *e = calypso_getenv("CALYPSO_BSP_TRX_OFFSET");
        fixe = (e && *e) ? 1 : 0;
        if (fixe) {
            const char *dd = calypso_getenv("CALYPSO_BSP_TRX_DELTA"); int delta = (dd && *dd) ? atoi(dd) : -3;
            int64_t off = ((int64_t)atoi(e) + delta) % (int64_t)BSP_FN_MAX; if (off < 0) off += BSP_FN_MAX;
            g_ts0_offset = off; g_trx_cale = 1;
            printf("  [ts0] mode TRX : offset fn-tick FIXE %lld (CALYPSO_BSP_TRX_OFFSET=%s, delta %d)\n", (long long)off, e, delta);
        }
    }
    if (!g_ts0 || !g_ts0_any) return;
    g_tick_courant = tick_fn;
    /* second chance: at the next tick, the ROM's verdict decides the clock */
    if (g_sc.actif) {
        if (tick_fn == g_sc.tick + 1) {
            if (sc_rom_a_decode()) {
                int64_t off = (int64_t)g_sc.fn_sch - (int64_t)g_sc.tick - sb_fin_trame();
                if (off < 0) off += BSP_FN_MAX;
                g_sc.ok++; if (!g_trx_cale && !g_bcch_verrou) g_ts0_offset = off;
                if (g_sc.ok <= 300 || g_sc.ok % 50 == 0)
                    printf("  [sbwin] SECONDE CHANCE : SB fn=%u DECODE a la tentative 1 (tick %u), offset ARM-tick recale [%u/%u]\n",
                           g_sc.fn_sch, g_sc.tick, g_sc.ok, g_sc.n);
            } else {
                g_sc.ko++;
                if (g_sc.ko <= 300 || g_sc.ko % 50 == 0)
                    printf("  [sbwin] SECONDE CHANCE : SB fn=%u rate a la tentative 1, SCH natif en tentative 2 [%u/%u]\n",
                           g_sc.fn_sch, g_sc.ok, g_sc.n);
            }
        }
        g_sc.actif = 0;
    }
    /* An SCH kept at the previous tick is delivered in the SB window that has
     * just armed; it realigns the ARM-tick offset, so the frame this tick would
     * have played will be played at the next tick: the stream stays contiguous. */
    if (calypso_bsp_sb_retenter()) return;
    static unsigned manques, manques_log;
    if (g_ts0_offset != INT64_MIN) {
        int64_t w = ((int64_t)tick_fn + g_ts0_offset) % (int64_t)BSP_FN_MAX; if (w < 0) w += BSP_FN_MAX;
        /* CATCHING UP WITH THE LIVE STREAM (CALYPSO_BSP_RATTRAPE=1, Magnetite). At boot the instance
         * falls behind the BTS (DSP restarts by the firmware, ARM lock waiting up to 5 s per frame)
         * and never catches up on its own: uplink RACHs dated in the past are never played. Beyond
         * the threshold of lateness relative to the last received frame, the offset jumps to the live
         * stream (margin, see below) and the BCCH lock drops: the L1 sees a discontinuity and
         * resynchronises on the live stream. */
        {
            /* margin 2 and threshold 30: the uplink only has ~2 frames of play at the TRX
             * (BTS_RX_LAG); with 20 to 200 frames of lateness the RACHs arrive after their frame
             * and are never played. */
            static int rattrape = -1, marge_r = 2, seuil_r = 30;
            if (rattrape < 0) { const char *e = calypso_getenv("CALYPSO_BSP_RATTRAPE"); rattrape = (e && *e == '1');
                                e = calypso_getenv("CALYPSO_BSP_RATTRAPE_MARGE"); if (e && *e) marge_r = atoi(e);
                                e = calypso_getenv("CALYPSO_BSP_RATTRAPE_SEUIL"); if (e && *e) seuil_r = atoi(e);
                                if (seuil_r <= marge_r) seuil_r = marge_r + 1; }
            int32_t retard = (int32_t)(g_ts0_fn_max - (uint32_t)w);
            if (rattrape && g_ts0_any && !g_trx_cale && retard > seuil_r) {
                int64_t saut = retard - marge_r;
                g_ts0_offset = (g_ts0_offset + saut) % (int64_t)BSP_FN_MAX;
                g_bcch_verrou = 0;
                w = ((int64_t)tick_fn + g_ts0_offset) % (int64_t)BSP_FN_MAX; if (w < 0) w += BSP_FN_MAX;
                printf("  [ts0] RATTRAPAGE : %d trames de retard sur le BTS (derniere recue fn=%u) -> offset %+lld, livre fn=%lld\n",
                       retard, g_ts0_fn_max, (long long)saut, (long long)w);
            }
        }
        /* SHIFT IN DEDICATED MODE (CALYPSO_BSP_DEDIE_DECAL=<n>, Magnetite). The offset is locked on
         * the BCCH reading (which can differ by 1-2 frames from the SBs); the shift lets the TI L1
         * decode the SACCH (SA_I) and the SDCCH (UA) in dedicated mode. Applied only during the
         * dedicated tasks signalled by montant.c (calypso_bsp_set_dedie_ti). */
        {
            static int decal = -999;
            if (decal == -999) { const char *e = calypso_getenv("CALYPSO_BSP_DEDIE_DECAL"); decal = (e && *e) ? atoi(e) : 0;
                                 if (decal) printf("  [ts0] decalage en dedie TI : %+d trame(s)\n", decal); }
            if (decal && g_dedie_ti) { w = (w + decal) % (int64_t)BSP_FN_MAX; if (w < 0) w += BSP_FN_MAX; }
        }
        /* TCH ONLY: CALYPSO_BSP_TCH_DECAL=<n> frames on the BCCH alignment. On SDCCH the DSP's A5 falls
         * on the delivered burst (+1 structural, [a5] "gap +1"); on TCH "gap +2": one frame too many,
         * downlink FACCH (UA) in Fire KO, RR Assignment Failure. */
        {
            int tch_decal = calypso_reglage("CALYPSO_BSP_TCH_DECAL", 0);   /* [2026-10-22] a chaud */
            if (tch_decal && g_dedie_tn > 0 && g_dedie_genre == BSP_DEDIE_TCH) {
                w = (w + tch_decal) % (int64_t)BSP_FN_MAX; if (w < 0) w += BSP_FN_MAX;
            }
        }
        /* IN TI DEDICATED MODE, THE MOBILE'S FRAME. The TI L1 writes a_a5fn = the fn it processes
         * (pont.c -> g_off_mobile); the BSP offset, aligned on the BCCH, can be 1 to 2 frames behind it
         * on TCH (shifted FACCH/TCH deinterleaving, A5 at the wrong fn -> every a_fd in Fire KO,
         * ciphered or not, and Assignment Failure). While a_a5fn has spoken less than 200 frames ago,
         * tick + g_off_mobile is played. CALYPSO_BSP_DEDIE_A5FN=0 falls back to the BCCH offset. */
        {
            static int a5fn_on = -1;
            /* OFF BY DEFAULT: the BCCH alignment already gives the mobile's frame in dedicated mode
             * (SDCCH 100 %, TCH d_fn aligned); this alignment can jump by +7 frames (noisy RA_C offset)
             * and leave idle mode misaligned after the dedicated phase (every NP_I in error, incoming
             * call never paged). CALYPSO_BSP_DEDIE_A5FN=1 enables. */
            if (a5fn_on < 0) { const char *e = calypso_getenv("CALYPSO_BSP_DEDIE_A5FN"); a5fn_on = (e && *e == '1'); }
            if (a5fn_on && g_dedie_ti && g_a5fn_vu && g_off_mobile != INT64_MIN &&
                (uint32_t)(tick_fn - g_a5fn_tick) < 3000) {
                /* RA_C reports the frame FOLLOWING the transmission: in dedicated mode the mobile opens
                 * its SDCCH/8 SS3 windows at (tick + RA_C offset) % 51 = 13..16 / 45..48 instead of
                 * 12..15 / 44..47 -> one frame less for reception (CALYPSO_BSP_DEDIE_DL_DECAL, default -1). */
                static int dl_decal = 99;
                if (dl_decal == 99) { const char *e = calypso_getenv("CALYPSO_BSP_DEDIE_DL_DECAL"); dl_decal = (e && *e) ? atoi(e) : -1; }
                /* the TCH has its own setting (CALYPSO_BSP_TCH_DL_DECAL, default = the SDCCH one):
                 * TCH [a5]: deciphered fn = last deposit + 2 with -1 */
                static int tch_decal = 99;
                if (tch_decal == 99) { const char *e = calypso_getenv("CALYPSO_BSP_TCH_DL_DECAL"); tch_decal = (e && *e) ? atoi(e) : dl_decal; }
                int dec = g_dedie_genre == BSP_DEDIE_TCH ? tch_decal : dl_decal;
                int64_t wm = ((int64_t)tick_fn + g_off_mobile + (g_off_rac_vu ? dec : 0)) % (int64_t)BSP_FN_MAX; if (wm < 0) wm += BSP_FN_MAX;
                static unsigned n_dm;
                if (wm != w && (n_dm++ < 20 || n_dm % 2000 == 0))
                    printf("  [dedie-a5fn] tick=%u trame BCCH %lld -> trame du mobile %lld (%+lld) [%u]\n", tick_fn,
                           (long long)w, (long long)wm, (long long)(wm - w), n_dm);
                w = wm;
            }
        }
        unsigned i = (unsigned)w % BSP_TS0_RING;
        bsp_horloge_publier((uint32_t)w, tick_fn, 1);
        /* SB_FIN_TRAME, offset LOCKED by the BCCH reading: the TI L1 opens its SB window at the
         * end of the frame preceding the SB; with the stream aligned on the NBs, the SCH would
         * arrive at the next tick, window closed (every camping SB re-read missed: SB_I flag 0,
         * NET_I 0 -> cell abandoned). An SB window armed during frame w therefore receives SCH
         * w+1, without realignment (lock). Outside the lock (resync), reading ahead would skew
         * the alignment. */
        if (sb_fin_trame() && g_bcch_verrou && g_dedie_tn <= 0 && calypso_rhea_dma_rx_armed() &&
            calypso_rhea_dma_one_shot() && calypso_rhea_dma_get_len_words() / 2 >= 190) {
            uint32_t fn1 = (uint32_t)((w + 1) % BSP_FN_MAX);
            unsigned j = fn1 % BSP_TS0_RING;
            if (g_ts0[j].valid && g_ts0[j].fn == fn1 && bsp_ts0_est_sb(g_ts0[j].bits)) {
                static unsigned n_avance;
                if (++n_avance <= 20 || n_avance % 500 == 0)
                    printf("  [sbwin] SB_FIN_TRAME (verrou) : fenetre SB au tick %u (trame %u) -> SCH fn=%u [%u]\n",
                           tick_fn, (uint32_t)w, fn1, n_avance);
                bsp_ts0_livrer(tick_fn, j);
                g_dernier_sch.valide = 0;
                g_ts0[i].joue = 1;
                return;
            }
        }
        if (g_ts0[i].valid && g_ts0[i].fn == (uint32_t)w && sb_double_on() && g_dedie_tn <= 0 &&
            bsp_ts0_est_fcch(g_ts0[i].bits) && calypso_rhea_dma_rx_armed() && calypso_rhea_dma_one_shot() &&
            calypso_rhea_dma_get_len_words() / 2 >= 190) {
            unsigned j = (unsigned)((w + 1) % BSP_FN_MAX) % BSP_TS0_RING;
            if (g_ts0[j].valid && g_ts0[j].fn == (uint32_t)((w + 1) % BSP_FN_MAX) && bsp_ts0_est_sb(g_ts0[j].bits)) {
                for (int pg = 0; pg < 2; pg++) { const uint16_t *a = sc_a_sch(pg); g_sc.a0[pg] = a ? a[0] : 0; }
                /* bsp_ts0_livrer() realigns the offset as soon as an SCH goes out in
                 * an SB window; here it must NOT: if the ROM misses, attempt 2 must see
                 * the native SCH at the next tick (otherwise the next frame would go out
                 * in its place and D would replace A instead of adding to it). The
                 * realignment only happens at the next tick, on CRC OK read in the API
                 * RAM. */
                int64_t off_avant = g_ts0_offset;
                g_sb_variante = 1;
                bsp_ts0_livrer(tick_fn, j);           /* the next SCH, waveform D, in the window of attempt 1 */
                g_sb_variante = 0;
                if (!g_trx_cale) g_ts0_offset = off_avant;
                g_ts0[i].joue = 1;                    /* the FCCH frame is not played: the window was for the SB */
                g_sc.actif = 1; g_sc.fn_sch = g_ts0[j].fn; g_sc.tick = tick_fn; g_sc.n++;
                if (g_sc.n <= 300 || g_sc.n % 50 == 0)
                    printf("  [sbwin] SECONDE CHANCE : trame FCCH fn=%u au tick %u, fenetre SB armee -> SCH fn=%u livre (forme D)\n",
                           (uint32_t)w, tick_fn, g_ts0[j].fn);
                return;
            }
        }
        if (g_ts0[i].valid && g_ts0[i].fn == (uint32_t)w) { bsp_ts0_livrer(tick_fn, i); return; }
        /* THE DSP RUNS AHEAD OF THE BTS: STEP BACK (optional).
         *
         * osmo-bts-trx beats its frames on a timerfd hard-set to 4615 us and
         * cannot follow a slower clock -- its drift filter is an empty TODO
         * (scheduler_trx.c:571). When the DSP runs slower than real time the
         * BTS believes it is "faster than the TRX", compensates, then
         * resynchronises -- and produces NOTHING meanwhile. The DSP keeps
         * ticking and requests frames that do not exist yet.
         *
         * This path steps the offset back to restart from the most recent
         * received frame: the DSP falls behind wall time but its stream stays
         * CONTIGUOUS (STREAM mode: only the ORDER matters, the SCH carries the
         * real FN for synchronisation). Holes become latency.
         *
         * DISABLED BY DEFAULT: replacing a missing frame with the most recent
         * available one does not deliver a hole but THE WRONG BURST AT THE
         * RIGHT PLACE. For the deinterleaver a wrong but plausible input is
         * worse than an absence: it can no longer treat it as an erasure
         * (A/B: twice as many frames dropped by the mobile). CALYPSO_BSP_RECALE=1
         * enables it. This path exits before `manques++`, so it blinds the
         * "manques" counter. */
        if ((int32_t)((uint32_t)w - g_ts0_fn_max) > 0) {
            static int recale = -1;
            if (recale < 0) { const char *e = calypso_getenv("CALYPSO_BSP_RECALE");
                              recale = (e && *e == '1') ? 1 : 0;   /* OFF by default: see above */
                              if (recale) BSP_LOG("recalage actif : le flux TS0 reste contigu quand le BTS prend du retard"); }
            if (recale) {
                unsigned j = g_ts0_fn_max % BSP_TS0_RING;
                if (g_ts0[j].valid && g_ts0[j].fn == g_ts0_fn_max) {
                    int64_t recul = (int64_t)(uint32_t)w - (int64_t)g_ts0_fn_max;
                    g_ts0_offset -= recul;
                    if (g_ts0_offset < 0) g_ts0_offset += BSP_FN_MAX;
                    g_ts0_recales++;
                    g_ts0_recul_total += recul;
                    if (g_ts0_recales <= 20 || g_ts0_recales % 200 == 0) {
                        printf("  [ts0] recalage %lu : le BTS avait %lld trames de retard (fn=%u -> %u), flux garde contigu\n",
                               g_ts0_recales, (long long)recul, (uint32_t)w, g_ts0_fn_max);
                    }
                    bsp_ts0_livrer(tick_fn, j);
                    return;
                }
            }
        }
        manques++;
        if (g_dedie_tn > 0 && bsp_dedie_trame((uint32_t)w)) {
            /* Dedicated channel frame skipped entirely: out of reach of
             * g_dedie_manques, which is only touched in bsp_ts0_livrer(). */
            g_dedie_perdues++;
            if (g_dedie_perdues <= 40 || g_dedie_perdues % 200 == 0) {
                BSP_LOG("canal dedie (TS%d) : trame fn=%u perdue, aucun burst du BTS (perdues=%lu, joues=%lu)",
                        g_dedie_tn, (uint32_t)w, g_dedie_perdues, g_dedie_joues);
            }
            bsp_dedie_etat(NULL);
        }
        if (manques_log++ < 10 || manques % 200 == 0) {
            int32_t devant = (int32_t)((uint32_t)w - g_ts0_fn_max);
            printf("  [ts0] tick=%u : pas de burst BTS pour fn=%u (manques=%u/%u) ; derniere trame recue fn=%u, soit %+d : %s\n",
                   tick_fn, (uint32_t)w, manques, tick_fn, g_ts0_fn_max, devant,
                   devant > 0 ? "le DSP COURT DEVANT le BTS (avance insuffisante)"
                              : "le burst a existe puis a disparu (autre cause)");
        }
        /* A MISSING FRAME MUST BE AN ERASURE, NOT THE PREVIOUS FRAME REPLAYED.
         *
         * Returning here without depositing anything does not "hole" the
         * stream: the API page KEEPS the burst of the previous tick, and the
         * DSP decoding task runs on it anyway. Verified chain:
         *   - calypso_bsp_rx_burst() is the only writer of the burst DARAM, and
         *     is only called from bsp_ts0_livrer() -- which this path skips;
         *   - calypso_rif_drain() returns 0 on an empty FIFO (calypso_rif.c);
         *   - the transfer then exits through `if (got <= 0) break`
         *     (calypso_rhea_dma.c) WITHOUT writing anything into the page;
         *   - on the DRR path it is worse, the last word is repeated: "On an
         *     empty FIFO, DRR keeps its last value [...] returning 0 would
         *     fabricate a sample" (calypso_rif.c).
         * A decoder re-reading the same page twice yields blocks with an
         * IDENTICAL error count; null samples instead give soft bits close to
         * zero, the only input the deinterleaver can treat as uncertainty
         * rather than data.
         *
         * Touches no delivered frame: this block ONLY runs on the ticks where
         * the DSP would otherwise re-read stale samples. It does not lower
         * `perdues`, which serves as a no-effect witness.
         * CALYPSO_BSP_SILENCE=0 disables the erasure. */
        {
            static int silence = -1;
            if (silence < 0) {
                const char *e = calypso_getenv("CALYPSO_BSP_SILENCE");
                silence = (e && *e == '0') ? 0 : 1;
                if (silence) BSP_LOG("effacement actif : une trame sans burst depose des echantillons nuls");
            }
            if (silence) {
                static int16_t vide[2 * 256];
                const bool one_shot = calypso_rhea_dma_one_shot();
                int nwin  = one_shot ? calypso_rhea_dma_get_len_words() / 2 : 0;
                int marge = nwin >= 190 ? 21 : nwin >= 150 ? 3 : 0;
                int total = marge > 0 ? (nwin > marge + 148 ? nwin : marge + 148) : 148;
                if (total > 256) total = 256;
                memset(vide, 0, (size_t)(2 * total) * sizeof *vide);
                if (bsp_remplissage() != (const int16_t *)g_remplissage_ts0)
                    faux_air_bruit(vide, total);              /* background noise, not zeros (PM = 0) */
                calypso_bsp_rx_burst(0, (uint32_t)w, vide, 2 * total);
                bsp.bursts_written++;
                /* In continuous mode the frame must be 1250 samples, otherwise
                 * the ROM's symbol counter drifts: same fillers as
                 * bsp_ts0_livrer(). */
                if (!one_shot) {
                    for (int tn = 1; tn < 8; tn++) {
                        calypso_bsp_rx_burst((uint8_t)tn, (uint32_t)w, bsp_remplissage(), 2 * 148);
                    }
                }
                g_ts0_silences++;
                if (g_ts0_silences <= 20 || g_ts0_silences % 500 == 0) {
                    printf("  [ts0] silence %lu : fn=%u sans burst, effacement depose "
                           "(au lieu de relire la page du tick precedent)\n",
                           g_ts0_silences, (uint32_t)w);
                }
            }
        }
        return;
    }
    bsp_horloge_publier(0, tick_fn, 0);
    for (unsigned k = 0; k < BSP_TS0_RING; k++) {
        unsigned i = (g_ts0_next + k) % BSP_TS0_RING;
        if (g_ts0[i].valid && !g_ts0[i].joue) { bsp_ts0_livrer(tick_fn, i); g_ts0_next = g_ts0[i].fn + 1; return; }
    }
}
static void bsp_trxd_readable(void *opaque)
{
    /* The bridge sends 8 header bytes + 4*148 I/Q = 600 bytes, and a burst at
     * 4 SPS reaches 592 I/Q. Smaller buffers truncated silently: the BSP then
     * saw unconverted soft bits, the IQ_PASSTHROUGH branch was never taken,
     * the hard cos_tab fallback ran and AFC rotation was ineffective. */
    uint8_t buf[4096];
    struct sockaddr_in addr;
    socklen_t alen = sizeof(addr);

    ssize_t n = recvfrom(bsp.trxd_fd, buf, sizeof(buf), MSG_DONTWAIT,
                         (struct sockaddr *)&addr, &alen);
    if (n < 8) return;

    /* FEED-FP, leg 1/2 — INPUT. Read-only capped probe, CALYPSO_BSP_FINGERPRINT
     * (default 0).
     *
     * corr_iq.py measured 400/400 IDENTICAL FCCH bursts (rms=32533, coh=0.998,
     * both constant) at the DSP input, while the calypso-ipc-device source
     * served 10111 FCCH out of 103244 bursts = 9.8 %, exactly the GSM rate:
     * the freeze is INSIDE QEMU. This leg fingerprints what ARRIVES over UDP;
     * leg 2/2 (c54x_bsp_load) fingerprints what LEAVES towards the RIF. Varied
     * fingerprints here plus constant ones there bracket the freeze.
     *
     * ⚠️ Hash the WHOLE burst, not 8 words: two different bursts can share a
     * prefix, and a prefix comparison reads as a false freeze. */
    {
        static int fp_on = -1;
        if (fp_on < 0) fp_on = calypso_gate("CALYPSO_BSP_FINGERPRINT", 0);
        if (fp_on && n > 8) {
            uint32_t h = 2166136261u;      /* FNV-1a 32 bits */
            unsigned nz = 0;
            for (ssize_t i = 8; i < n; i++) {
                h = (h ^ buf[i]) * 16777619u;
                if (buf[i]) nz++;
            }
            static uint32_t prev_h;
            static unsigned long long n_tot, n_same;
            n_tot++;
            if (n_tot > 1 && h == prev_h) n_same++;
            prev_h = h;
            if (n_tot <= 20 || (n_tot % 500) == 0)
                fprintf(stderr, "[bsp] FEED-FP IN  #%llu fp=%08x nz=%u/%lld "
                        "identiques=%llu/%llu\n",
                        n_tot, h, nz, (long long)(n - 8), n_same, n_tot);
        }
    }

    /* Publish the DL I/Q: feeds the FB-STREAM ring (native correlator input)
     * and the magnitude measurement a_pm derives from. buf[8..] holds
     * interleaved int16 I/Q (cs16, passthrough mode). */
    if (n > 8) {
        bsp_iq_publish((const int16_t *)(buf + 8), (int)((n - 8) / 2));
    }

    /* Out-of-band I/Q tee (observability): copy of the raw burst to
     * CALYPSO_IQ_TEE_HOST:CALYPSO_IQ_TEE_PORT, read by the osmo-operator live
     * FFT. Opt-in: nothing is sent unless one of the two variables is set. */
    {
        const char *tee_p = calypso_getenv("CALYPSO_IQ_TEE_PORT");
        const char *tee_h = calypso_getenv("CALYPSO_IQ_TEE_HOST");
        if ((tee_p && *tee_p) || (tee_h && *tee_h)) {
            static int tee_fd = -1;
            static struct sockaddr_in tee_dst;
            if (tee_fd == -1) {
                tee_fd = socket(AF_INET, SOCK_DGRAM, 0);
                int port = (tee_p && *tee_p) ? atoi(tee_p) : 6703;
                memset(&tee_dst, 0, sizeof(tee_dst));
                tee_dst.sin_family = AF_INET;
                tee_dst.sin_port = htons(port);
                tee_dst.sin_addr.s_addr = (tee_h && *tee_h) ? inet_addr(tee_h)
                                                            : htonl(INADDR_LOOPBACK);
                BSP_LOG("IQ-TEE -> %s:%d (FFT live)",
                        (tee_h && *tee_h) ? tee_h : "127.0.0.1", port);
            }
            if (tee_fd >= 0) {
                sendto(tee_fd, buf, n, MSG_DONTWAIT,
                       (struct sockaddr *)&tee_dst, sizeof(tee_dst));
            }
        }
    }

    /* Diag: log the first 10 receive sizes to check that the bridge really
     * sends 600 bytes (I/Q mode) and that the BSP buffer does not truncate. */
    {
        static int rxsz_log = 0;
        if (rxsz_log++ < 10) {
            BSP_LOG("RXSZ #%d recv=%zd from %s:%d", rxsz_log,
                    n, inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
        }
    }

    /* Refine UL peer to actual DL sender (init-time default is bridge
     * 127.0.0.1:5702 — DL source confirms it or replaces it). */
    if (addr.sin_addr.s_addr != bsp.trxd_peer.sin_addr.s_addr ||
        addr.sin_port != bsp.trxd_peer.sin_port) {
        bsp.trxd_peer = addr;
        BSP_LOG("TRXD peer learned: %s:%d",
                inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
    }

    /* TRXDv0 DL: tn(1) fn(4) rssi(1) toa(2) bits(148) = 156 bytes. The RX
     * header is 8 bytes, like TX. Even when the BTS emits 154-byte packets,
     * the n-8 skip plus the 148-bit clamp keeps the DSP demod aligned; n-6
     * broke mobile L1 sync, leaving the mobile in the cell-selection loop
     * without ever reaching LU. */
    uint8_t  tn  = buf[0] & 0x07;
    uint32_t fn  = ((uint32_t)buf[1]<<24)|((uint32_t)buf[2]<<16)|
                   ((uint32_t)buf[3]<<8)|buf[4];
    /* uplink copied by the bridge (byte 6 = 0xA5): stored apart for the realistic air, never played
     * as a downlink burst (it would break the sync), whether CALYPSO_AIR_NIVEAUX is set or not. */
    if (n >= 8 + 148 && buf[6] == 0xA5) {
        air_ul_stocker(fn, tn, buf + 8);
        bsp.bursts_seen++;   /* the drain loop of calypso_bsp_service() stops when this no longer moves */
        return;
    }
    bsp.last_att = (n > 5) ? buf[5] : 0;

    int nbits = (int)n - 8;  /* TRXDv0 header is 8 bytes (TS+FN+RSSI+ToA) */
    if (nbits > 148) nbits = 148;
    if (nbits <= 0) return;

    const uint8_t *bits = buf + 8;
    {   /* FN-keyed store of the TS0 bursts (see bsp_ts0_stocker); delivery
         * happens per DSP tick in calypso_bsp_service(). */
        static int stream = -1;
        if (stream < 0) { const char *e = calypso_getenv("CALYPSO_BSP_STREAM"); stream = (e && *e == '1') ? 1 : 0; }
        if (stream) {
            if (tn == 0 && nbits == 148) { bsp_ts0_stocker(fn, bits); return; }
            if (tn == g_dedie_tn && nbits == 148) {
                /* The dedicated channel: kept, without changing the pacing -
                 * bsp_ts0_livrer() will play it instead of TS0 when the window
                 * is one-shot. */
                bsp_dedie_stocker(fn, bits);
            }
            if (tn >= 1 && tn <= 7 && nbits == 148) {
                /* The other timeslots of the SAME frame: they take the place of
                 * the zero filler in the continuous frame. */
                bsp_autres_stocker(fn, tn, bits);
                bsp.bursts_seen++;
                return;
            }
            /* [2026-09-21] TS1..TS7 STOP HERE. The frame handed to the ROM is
             * assembled by bsp_ts0_livrer(), which already appends its own
             * seven filler timeslots after the stored TS0 burst - exactly 1250
             * symbols per tick. Letting the bridge's other timeslots carry on
             * to calypso_bsp_rx_burst() at the end of this function fed the RIF
             * a SECOND, unpaced stream on top of that one: measured 157
             * non-TS0 bursts/s against ~89 DSP ticks/s, i.e. ~276 extra
             * samples per ARM frame. The ROM, which counts its frame sample by
             * sample, then saw ~1526 symbols per frame instead of 1250: its FB
             * ToA never settled on 23 + n*1250 (observed residues 280/652/810/
             * 1152 modulo 1250), the RIF transit stage saturated (rif_avant=
             * 8196, oldest samples destroyed) and the narrow FB1 search never
             * found the tone again - FB0 hit, FB1 never, FBSB result=255.
             * bursts_seen must still move: the drain loop of
             * calypso_bsp_service() stops as soon as it does not. */
            bsp.bursts_seen++;
            { static unsigned long n_hors; if (n_hors++ == 0 || n_hors % 20000 == 0)
                  BSP_LOG("STREAM : %lu bursts hors TS0 ignores (la trame est assemblee par bsp_ts0_livrer)", n_hors); }
            return;
        }
    }

    /* Log burst type: check if all-zero (FB) or mixed (NB/SB) */
    {
        int zeros = 0, ones = 0;
        for (int i = 0; i < nbits; i++) {
            if (bits[i] == 0) zeros++;
            else ones++;
        }
        static int burst_log = 0;
        if (burst_log < 20 || (burst_log % 10000) == 0) {
            BSP_LOG("BURST fn=%u tn=%u zeros=%d ones=%d %s",
                    fn, tn, zeros, ones,
                    zeros == nbits ? "*** FB ***" :
                    ones > 100 ? "DUMMY/NB" : "SB/OTHER");
        }
        burst_log++;
    }

    /* FN-alignment instrumentation: measure burst arrival FN vs QEMU
     * virtual FN. A persistent negative delta means BTS is lagging
     * (bursts arrive for FNs that have already passed); a positive
     * delta is normal lookahead. */
    {
        uint32_t cur_fn = calypso_trx_get_fn();
        int32_t  delta  = bsp_fn_delta(fn, cur_fn);

        static int rx_log = 0;
        if (rx_log < 100 || (rx_log % 1000) == 0) {
            BSP_LOG("RX tn=%u fn=%u cur_fn=%u delta=%d",
                    tn, fn, cur_fn, delta);
        }
        rx_log++;

        /* Rolling summary over last 500 samples: min/max/mean */
        static int32_t  hist[500];
        static unsigned hist_pos = 0;
        static unsigned hist_seen = 0;
        hist[hist_pos] = delta;
        hist_pos = (hist_pos + 1) % 500;
        hist_seen++;
        if ((hist_seen % 500) == 0) {
            unsigned nh = hist_seen < 500 ? hist_seen : 500;
            int32_t mn = INT32_MAX, mx = INT32_MIN;
            int64_t sum = 0;
            for (unsigned i = 0; i < nh; i++) {
                int32_t d = hist[i];
                if (d < mn) mn = d;
                if (d > mx) mx = d;
                sum += d;
            }
            BSP_LOG("RX delta stats (last %u): min=%d max=%d mean=%lld",
                    nh, mn, mx, (long long)(sum / (int64_t)nh));
        }
    }

    /* GMSK modulation: convert TRXDv0 hard bits to I/Q samples.
     * GMSK with h=0.5: each bit adds ±π/2 to the phase.
     * NRZ encoding: bit 0 → phase += π/2, bit 1 → phase -= π/2.
     *
     * The Calypso IOTA chip delivers complex I/Q pairs to the BSP.
     * Phase increments are exactly ±π/2, so I=cos(φ) and Q=sin(φ)
     * cycle through {±1, 0}. We produce interleaved I,Q pairs.
     *
     * For FB (all-zero bits): phase advances π/2 per bit → pure tone.
     * I/Q sequence: (1,0),(0,1),(-1,0),(0,-1),(1,0),...
     *
     * IQ PASSTHROUGH: when the UDP payload is >= 296 bytes and
     * CALYPSO_BSP_IQ_PASSTHROUGH is on, buf[8..] is read as LE int16 I/Q pairs
     * (the format calypso-ipc-device sends: scipy GMSK at BT=0.3, more
     * realistic than our hard +/-pi/2 modulation). Otherwise the internal
     * modulator runs (148 hard bits -> 296 int16). */
    int16_t iq[BSP_IQ_MAX_I16];  /* see BSP_IQ_MAX_I16 */
    int iq_count = 0;

    static int iq_pt_mode = -1;
    if (iq_pt_mode < 0) {
        const char *e = calypso_getenv("CALYPSO_BSP_IQ_PASSTHROUGH");
        /* Passthrough is the DEFAULT: cos/sin synthesis yields an incoherent
         * tone. Opt out explicitly with CALYPSO_BSP_IQ_PASSTHROUGH=0. */
        iq_pt_mode = (e && *e == '0') ? 0 : 1;
        BSP_LOG("IQ_PASSTHROUGH=%d (defaut ON ; synthese=opt-out =0)", iq_pt_mode);
    }
    int iq_bytes = (int)n - 8;  /* payload bytes after 8-byte hdr */
    /* The bridge sends 2 int16 per bit (I,Q interleaved) = 4 bytes/bit, so
     * 146-148 bits is 584-592 payload bytes. Auto-detect: >= 4*146 bytes means
     * I/Q mode, otherwise soft bits at 1 byte per bit. */
    int iq_min_bits = 146;
    if (iq_pt_mode && iq_bytes >= 4 * iq_min_bits) {
        /* The device sends 592 I/Q at 4 SPS (OSR=4) while the DSP correlator
         * wants 148 samples at 1 SPS (FCCH = +pi/2 per sample). Decimate by
         * CALYPSO_BSP_IQ_DECIM (default 4): the +0.393 rad/sample tone at 4 SPS
         * becomes +0.393*decim = +pi/2. decim=1 keeps the first 148 samples at
         * 4 SPS = 37 symbols, which never correlates. */
        static int decim = -1;
        if (decim < 0) {
            const char *d = calypso_getenv("CALYPSO_BSP_IQ_DECIM");
            decim = (d && *d) ? atoi(d) : 4;
            if (decim < 1) decim = 1;
            BSP_LOG("IQ_DECIM=%d (STEP3 decimation ->1SPS)", decim);
        }
        const int16_t *isrc = (const int16_t *)(buf + 8);
        int total_cplx = iq_bytes / 4;   /* up to 592 (buf[4096]) */
        iq_count = 0;
        for (int k = 0; k * decim < total_cplx && iq_count <= BSP_IQ_MAX_I16 - 2; k++) {
            iq[iq_count++] = isrc[2 * (k * decim)];      /* I */
            iq[iq_count++] = isrc[2 * (k * decim) + 1];  /* Q */
        }
        nbits = iq_count / 2;

        /* WINDOW WIDENING (passthrough branch). Same reason as the synthesis
         * branch: the DSP consumes 150 samples for an NB and 190 for an SB,
         * and we delivered 148. The UDP payload is 592 complex at 4 SPS =
         * exactly 148 symbols, so the missing 42 symbol periods are not in it
         * either.
         *
         * Rather than arbitrary guard bits, EXTRAPOLATE the phase rotation
         * measured on the last two real samples and continue it. For an FCCH
         * (pure tone) this is exact; for a normal burst it is a phase-coherent
         * extrapolation, which avoids punching a hole right inside the SB
         * correlation window (50 bits from r39).
         *
         * ⚠️ THESE SAMPLES ARE SYNTHETIC. A detection that appears ONLY
         *    thanks to them is suspect and must be reported as such.
         * Default 148 leaves behaviour unchanged. */
        {
            static int win = -1;
            if (win < 0) {
                const char *e = calypso_getenv("CALYPSO_BSP_RX_WINDOW");
                win = (e && *e) ? atoi(e) : 148;
                if (win > BSP_IQ_MAX_I16 / 2) win = BSP_IQ_MAX_I16 / 2;
                if (win != 148)
                    BSP_LOG("RX_WINDOW=%d echantillons (passthrough) — "
                            "prolongement par extrapolation de phase, SYNTHETIQUE",
                            win);
            }
            if (win > nbits && iq_count >= 4) {
                double i1 = iq[iq_count - 2], q1 = iq[iq_count - 1];
                double i0 = iq[iq_count - 4], q0 = iq[iq_count - 3];
                double n0 = i0 * i0 + q0 * q0;
                double pr = 1.0, pi_ = 0.0;      /* rotation per sample */
                if (n0 > 0.0) {
                    pr  = (i1 * i0 + q1 * q0) / n0;
                    pi_ = (q1 * i0 - i1 * q0) / n0;
                }
                double cr = i1, ci = q1;
                while (nbits < win && iq_count + 1 < BSP_IQ_MAX_I16) {
                    double nr = cr * pr - ci * pi_;
                    double ni = cr * pi_ + ci * pr;
                    cr = nr; ci = ni;
                    if (cr >  32767.0) cr =  32767.0;
                    if (cr < -32768.0) cr = -32768.0;
                    if (ci >  32767.0) ci =  32767.0;
                    if (ci < -32768.0) ci = -32768.0;
                    iq[iq_count++] = (int16_t)cr;
                    iq[iq_count++] = (int16_t)ci;
                    nbits++;
                }
            }
        }

        /* AFC rotation (TWL3025 VCXO offset) is NOT applied here: it runs at
         * delivery time, where the DAC value is current instead of stale by
         * the lookahead depth. The AFC loop closes as firmware delta -> DSP
         * TSP -> TWL3025 DAC -> rotated samples -> DSP correlator. */
        /* calypso_twl3025_apply_phase(iq, copy_count / 2, fn, tn); */
        static int pt_log = 0;
        if (pt_log < 10 || (pt_log % 5000) == 0) {
            BSP_LOG("IQ passthrough #%d fn=%u tn=%u bytes=%d nbits=%d "
                    "iq[0..3]=%d,%d,%d,%d",
                    pt_log, fn, tn, iq_bytes, nbits,
                    iq[0], iq[1], iq[2], iq[3]);
        }
        pt_log++;
    } else {
        /* Q15 full-scale amplitude: real BSP/IOTA delivers near-full-range Q15
         * samples. ±0x7FFE keeps one bit of headroom below INT16_MIN. */
        /* [2026-09-20] GMSK (BT = 0.3, one sample per symbol at the symbol
         * centre), the same modulator as the synthetic cell of c54x_exe. The
         * hard +-90-degree MSK used before (I/Q on the phase points, full scale)
         * let the ROM find the FCCH tone but not decode the SCH: replayed on the
         * c54x_exe bench with that modulation, 1 CRC OK on 81 SB attempts
         * against 27 on 76 with GMSK, and the FB frequency estimate wandered to
         * +1050 Hz. Amplitude 30000 as the synthetic cell. */
        /* the SCH goes through sb_moduler() (instant, widening, noise tuned in
         * replay); FCCH as is; NB widened (CALYPSO_BSP_NB_SYM). */
        int est_sb = 0;
        if (nbits == 148) {
            static const uint8_t train_sb[64] = { 1,0,1,1,1,0,0,1,0,1,1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,1,1,1,1, 0,0,1,0,1,1,0,1,0,1,0,0,0,1,0,1,0,1,1,1,0,1,1,0,0,0,0,1,1,0,1,1, };
            est_sb = 1;
            for (int k = 0; k < 64 && est_sb; k++) if ((bits[42 + k] & 1) != train_sb[k]) est_sb = 0;
        }
        /* realistic air also on this direct path (without CALYPSO_BSP_STREAM: banc-rssi) */
        int amp_dir = 30000, bruit_dir = 0;
        if (air_niveaux_on() && nbits == 148) {
            const uint8_t *b2 = air_choisir(g_tick_courant, 5000, fn, tn, bits, &amp_dir);
            if (!b2) bruit_dir = 1;
            else if (b2 != bits) { bits = b2; est_sb = 0; }
        }
        if (bruit_dir) air_bruit(iq + iq_count, nbits);
        else if (est_sb) {
            /* the SCH at the same level as the rest of the cell: at 30000 among bursts at 3000, it
             * would saturate after the gain setting on the FB (FB1 -71 dBm then "Sync error") */
            sb_moduler(bits, iq + iq_count, fn);
            air_echelle(iq + iq_count, nbits, amp_dir);
        }
        else {
            gmsk_moduler(bits, nbits, amp_dir, 0.0, 0.5, iq + iq_count);
            if (nbits == 148) {
                static double sym = -2;
                if (sym == -2) { const char *e = calypso_getenv("CALYPSO_BSP_NB_SYM"); sym = (e && *e) ? atof(e) : 0.3; }
                int zeros = 1;
                for (int k = 0; k < 148 && zeros; k++) if (bits[k]) zeros = 0;
                if (!zeros) gmsk_elargir(iq + iq_count, nbits, sym);
            }
        }
        iq_count += 2 * nbits;

        /* WINDOW WIDENING — CALYPSO_BSP_RX_WINDOW.
         *
         * We delivered exactly `nbits` samples, i.e. 148 = a GSM burst in BITS
         * (3+57+1+26+1+57+3), with no margin. What the DSP consumes (Osmocom
         * wiki HardwareCalypsoDSP, task by task):
         *   RX NB : 150 samples, 10-bit TSC window from r68, correlation over
         *           16 bits (TSC[10..25])
         *   SB    : 190 samples, 50-bit window from r39, correlation over the
         *           full 64 bits
         *
         * The capture does start AT THE BEGINNING of the burst: in an SB the
         * 64-bit training sequence occupies bits 42..105 (3 tail + 39 data +
         * 64 TSC) and the DSP searches 50 bits from r39 -> 39..89, which
         * contains 42. The missing samples are therefore AFTER the burst: the
         * guard period (8.25 bits) then the start of the neighbouring slot.
         * Hardware agrees: the TRF6151 analog window measures 914.6 us against
         * 577 us of useful burst, about 1.58x.
         *
         * ⚠️ ASSUMPTION, and the limit of this fix: we do NOT have the
         *    neighbouring slot signal — the source supplies one burst only. So
         *    we extend the modulator with GUARD bits (bit=1, like the firmware
         *    TX padding), which gives a defined, phase-continuous fill instead
         *    of zeros that would punch a hole right in the middle of the SB
         *    correlation window.
         *    => A detection that appears ONLY thanks to those samples is
         *    suspect: they are synthetic. Report it as such.
         *
         * Default 148 leaves behaviour unchanged. Use 190 to test SB.
         */
        {
            static int win = -1;
            if (win < 0) {
                const char *e = calypso_getenv("CALYPSO_BSP_RX_WINDOW");
                win = (e && *e) ? atoi(e) : 148;
                if (win < nbits) win = nbits;               /* never truncate */
                if (win > BSP_IQ_MAX_I16 / 2) win = BSP_IQ_MAX_I16 / 2;
                if (win != 148)
                    fprintf(stderr, "[bsp] RX_WINDOW = %d echantillons "
                            "(burst=%d + %d de garde SYNTHETIQUE) — "
                            "NB en demande 150, SB 190\n",
                            win, nbits, win - nbits);
            }
            for (int g = nbits; g < win && iq_count + 1 < BSP_IQ_MAX_I16; g++) {
                iq[iq_count++] = 0;                 /* guard: silence */
                iq[iq_count++] = 0;
            }
        }
    }

    /* Hand the burst straight to the DSP: the samples land in DARAM now, as
     * the RIF delivers them on silicon. There is no FN rendez-vous to keep —
     * the burst carries its own frame number for the SCH, and the correlator
     * only ever sees the window it is given. */
    /* [2026-09-21] Frame the TS0 burst like the synthetic cell when the ARM
     * has armed a task window (one-shot RIF DMA of 151 samples for a normal
     * burst, 191 for the SB; tpu_window.c): 3 (NB) or 21 (SB) silent samples
     * ahead, the window length in all. Measured on the real chain before: the
     * bare 148-sample burst put the ROM's NB TOA at 2 instead of 5, the
     * equaliser's pre-cursor tap fell outside its 3-tap window and every BCCH
     * block failed the Fire code; the SB read TOA 3 instead of 23. */
    if (tn == 0 && calypso_rhea_dma_one_shot()) {
        int nwin = calypso_rhea_dma_get_len_words() / 2;
        /* THE BIAS OF THE TOA LOOP IS APPLIED HERE.
         * calypso_bsp_toa_feedback() (below) measures the gap between the TOA
         * seen by the ROM and the target 23, and integrates it into
         * g_toa_bias. ONLY the SB window (nwin >= 190) is corrected. The
         * normal burst keeps its margin of 3, which gives it the expected and
         * measured TOA 5, and must not be touched. */
        int marge = nwin >= 190 ? 21 : nwin >= 150 ? 3 : 0;
        { static unsigned n; if (n++ < 40 || (n % 500) == 0)
            printf("  [cadre] nwin=%d marge=%d biais=%d iq=%d %s\n",
                   nwin, marge, g_toa_bias, iq_count, nwin >= 190 ? "(fenetre SB)" : "(NB)"); }
        if (nwin >= 190) {
            marge += g_toa_bias;
            if (marge < 0)  marge = 0;
            if (marge > 96) marge = 96;   /* la fenetre SB fait ~191 echantillons */
        }
        if (marge > 0 && iq_count >= 2 * 148) {
            static int16_t cadre[2 * 256];
            int total = nwin > marge + 148 ? nwin : marge + 148;
            if (total > 256) total = 256;
            int n = iq_count < 2 * 148 ? iq_count : 2 * 148;   /* the burst alone, guard dropped */
            memset(cadre, 0, sizeof cadre);
            memcpy(cadre + 2 * marge, iq, (size_t)n * sizeof(int16_t));
            { static unsigned nl; if (nl++ < 24 || nl % 5000 == 0) printf("  [livre] fn=%u p51=%u fenetre=%d marge=%d\n", fn, fn % 51u, nwin, marge); }
            calypso_bsp_rx_burst(tn, fn, cadre, 2 * total);
            return;
        }
    }
    calypso_bsp_rx_burst(tn, fn, iq, iq_count);

    /* Delivery is handled exclusively by calypso_bsp_deliver_buffered()
     * called from the TDMA tick. No immediate delivery — it would
     * double-consume BDLENA pulses and race with the buffered path. */
}

/* ---- Init ---- */

/* REALTIME drain callback: pulls the BSP UDP queue into DSP DMA at a 5 ms
 * wall-clock rate (200/s). Monotonic anti-drift rearm on `last_target +
 * period`, so dispatcher jitter does not accumulate.
 *
 * On QEMU_CLOCK_VIRTUAL under heavy DSP load the drain ran slower than wall
 * time, the BSP queue overflowed and 95 % of bursts were dropped. Since
 * tdma_tick is REALTIME-monotonic with a clk_master pthread, virtual and wall
 * are aligned and REALTIME matches the ARM frame_irq/tdma rate. */

/* Track the firmware tpu_offset (relayed by QEMU in the TICK). When the
 * firmware shifts its RX window (synchronize_tdma) the burst must follow so
 * the measured ToA converges to 23, i.e. native acquisition without a canned
 * ToA. Units: 4 qbits = 1 bit = 1 sample at 1 SPS. */
static int  g_bsp_tpu_ref = 0x7fffffff;   /* premier offset observe = origine */
void calypso_bsp_set_tpu_offset(int qbits)
{
    g_bsp_tpu_offset = qbits;
    if (g_dedie_tn <= 0) {
        g_tpu_ref = qbits;          /* no dedicated channel: the ARM is on TS0 */
    }
}

/* Native ToA lock (CALYPSO_BSP_TOA_LOCK=1): slow closed loop driving the burst
 * placement bias so the ToA the DSP measures reaches 23 ("on time") WITHOUT
 * canning the output. The firmware then sees the alignment and stops
 * correcting. One-sample-per-frame integrator, for stability. */
/* defined above, before the framing that applies it */
void calypso_bsp_toa_feedback(int toa)
{
    static int en = -1;
    if (en < 0) {
        const char *e = calypso_getenv("CALYPSO_BSP_TOA_LOCK"); en = (e && *e=='1') ? 1 : 0;
        if (en) BSP_LOG("TOA_LOCK on : verrouillage natif du TOA sur 23 (biais placement)");
    }
    { static unsigned n; if (n++ < 40 || (n % 500) == 0)
        printf("  [toaloop] appel n=%u en=%d toa=%d biais=%d\n", n, en, toa, g_toa_bias); }
    if (!en || toa <= 0) return;
    int within = toa % 156;              /* intra-frame position (ntdma removed) */
    int err = within - 23;               /* target: 23 */
    if (err > 80) err -= 156;            /* take the shorter path across the wrap */
    if (err < -80) err += 156;
    if (err == 0) return;
    g_toa_bias -= (err > 0) ? 1 : -1;    /* slow integrator: burst earlier when ToA is too large */
}


/* Explicit drain for the non-QEMU host (c54x_exe --arm): the bsp_trxd_readable
 * iohandler and the bsp_drain_cb timer only exist inside the QEMU event loop.
 * Standalone, nobody empties UDP socket 6702 and bridge/BTS bursts pile up in
 * Recv-Q without ever reaching the DSP. Called once per frame from pont.c.
 * Returns the number of bursts read. */
/* [2026-09-20] STREAM frame assembler. One TDMA frame of the BTS stream is
 * handed to the DSP per tick, in timeslot order, ALWAYS 1250 symbols long:
 *   - continuous DMA (FB search): TS0..TS7 of that frame, each padded to
 *     156 (157 on TS3/TS7) symbols by calypso_bsp_rx_burst(); a timeslot the
 *     BTS did not send (idle TS carry nothing over TRXD) gets the dummy burst
 *     instead. The old path loaded the raw 148-symbol bursts that had arrived,
 *     so a frame was 1184 symbols at best and shorter with idle timeslots: the
 *     ROM's symbol counter drifted (FB TOA intra-frame offsets of 264..632
 *     instead of a constant), the FB1 narrow window and the SB frame missed.
 *   - one-shot DMA (the SB window): TS0 alone, framed like the synthetic cell
 *     (21 silent samples before and after), which is what puts the ROM's
 *     SB TOA near 23.
 * Slots older than the delivered frame are purged (their TS0 was lost). */
uint16_t g_remplissage_ts0[2 * 157];
#define g_remplissage g_remplissage_ts0
static bool bsp_source_toutes_ts;   /* the source delivers TS1..7 itself: no automatic fillers */
static BspBurstSlot *bsp_slot_exact(uint8_t tn, uint32_t fn)
{
    BspBurstQueue *qq = &bsp.q[tn];
    for (int i = 0; i < BSP_QUEUE_LEN; i++)
        if (qq->slot[i].valid && qq->slot[i].fn == fn) return &qq->slot[i];
    return NULL;
}
static void bsp_livrer_trame(uint32_t fn)
{
    static int16_t iq[2 * 256];
    const bool one_shot = calypso_rhea_dma_one_shot();
    bsp_source_toutes_ts = true;
    for (int tn = 0; tn < BSP_NUM_TN; tn++) {
        BspBurstSlot *sl = bsp_slot_exact((uint8_t)tn, fn);
        if (one_shot) {
            if (tn == 0 && sl) {
                /* The window geometry of tpu_window.c: the burst sits 23 symbols
                 * into the SB window (191 samples taken by the DSP, ALGTH 764)
                 * and 3 symbols into the NB window (151 samples, ALGTH 604);
                 * the 64-sample PM window takes the head of the burst. A 21
                 * margin in a 151-sample window cut the last 18 symbols of
                 * every normal burst. */
                int nwin = calypso_rhea_dma_get_len_words() / 2;
                int marge = nwin >= 190 ? 21 : nwin >= 150 ? 3 : 0;
                int total = nwin > marge + 148 ? nwin : marge + 148;
                if (total > 256) total = 256;
                int n = sl->n < 296 ? sl->n : 296;
                memset(iq, 0, sizeof iq);
                memcpy(iq + 2 * marge, sl->iq, (size_t)n * sizeof(int16_t));
                calypso_bsp_rx_burst(0, fn, iq, 2 * total);
                bsp.bursts_written++;
                { static unsigned nl; if (nl++ < 40 || nl % 2000 == 0) printf("  [livre] fn=%u p51=%u one_shot=1 nwin=%d marge=%d n=%d total=%d\n", fn, fn % 51u, nwin, marge, n / 2, total); }
            }
        } else if (sl) {
            int n = sl->n < 296 ? sl->n : 296;
            /* [2026-09-21] TS0 in STREAM mode: frame it like the one-shot
             * branch whenever the RIF DMA length is a task window (151 = NB,
             * 191 = SB), even if the one-shot flag reads false at delivery
             * time. Measured on the real chain: the NB bursts reached the ROM
             * at TOA 2 (no margin) instead of 5 as on the synthetic cell, the
             * equaliser's pre-cursor tap fell outside its window and every
             * block failed the Fire code. */
            int nwin = tn == 0 ? calypso_rhea_dma_get_len_words() / 2 : 0;
            int marge = nwin >= 190 ? 21 : nwin >= 150 ? 3 : 0;
            if (tn == 0 && marge > 0) {
                int total = nwin > marge + 148 ? nwin : marge + 148;
                if (total > 256) total = 256;
                memset(iq, 0, sizeof iq);
                memcpy(iq + 2 * marge, sl->iq, (size_t)n * sizeof(int16_t));
                calypso_bsp_rx_burst(0, fn, iq, 2 * total);
            } else
                calypso_bsp_rx_burst((uint8_t)tn, fn, sl->iq, n);
            bsp.bursts_written++;
            { static unsigned nl; if (tn == 0 && (nl++ < 40 || nl % 2000 == 0)) printf("  [livre] fn=%u p51=%u one_shot=0 nwin=%d marge=%d n=%d\n", fn, fn % 51u, nwin, marge, n / 2); }
        } else {
            calypso_bsp_rx_burst((uint8_t)tn, fn, (const int16_t *)g_remplissage, 2 * 148);
        }
        if (sl) sl->valid = false;
    }
    /* purge what is older than this frame */
    for (int tn = 0; tn < BSP_NUM_TN; tn++)
        for (int i = 0; i < BSP_QUEUE_LEN; i++) {
            BspBurstSlot *s2 = &bsp.q[tn].slot[i];
            if (s2->valid && bsp_fn_delta(s2->fn, fn) < 0) { s2->valid = false; bsp.bursts_dropped_stale++; }
        }
    { static unsigned nl; if (nl++ < 3) BSP_LOG("STREAM trame fn=%u livree (%s)", fn, one_shot ? "fenetre SB, TS0 + marges" : "8 TS, 1250 symboles"); }
}

/* WAIT FOR THE LATE FRAME RATHER THAN HOLE IT.
 *
 * osmo-bts-trx beats its frames on its own timer and realigns on every
 * IND CLOCK of the bridge (every 51 frames): it stalls, then catches up by a
 * block. The frames of a given multiframe phase can thus arrive AFTER the DSP
 * requests them, and bsp_ts0_service() would replace them by an erasure (the
 * mobile's downlink block, fn%51 = 16..19, lost one time in two; the UA never
 * gets through).
 *
 * The BTS is slaved to the DSP (pont/dsp/clock.py): waiting a few
 * milliseconds for it to deliver creates no drift, it aligns the DSP on it.
 * The wait only happens if the requested frame is AHEAD of the last received
 * one (the BTS is late, the frame is about to arrive) and by less than 200
 * frames; otherwise (burst gone, or alignment lost) no wait.
 * CALYPSO_BSP_ATTENTE_MS: 0 (default, QEMU unchanged) = no wait;
 * c54x_exe/run.sh sets 40 in MODE=dsp. */
static void bsp_attendre_trame(uint32_t tick_fn)
{
    static int max_ms = -1;
    static unsigned long attentes, servies, echues;
    static double total_ms;
    if (max_ms < 0) {
        const char *e = calypso_getenv("CALYPSO_BSP_ATTENTE_MS");
        max_ms = e ? atoi(e) : 0;
        if (max_ms > 0) {
            printf("  [ts0] attente des trames en retard de la BTS : jusqu'a %d ms\n", max_ms);
        }
    }
    /* TRX mode (g_trx_cale): the uplink bursts are RARE (RACH, then 4 bursts per SDCCH block);
     * waiting 40 ms on every frame without burst would choke the lock-step DSP/ARM pace from the
     * first received burst (the trx firmware would program nothing more: 121 insn/frame, clock lost).
     * A frame without burst is a silence. */
    if (max_ms <= 0 || !g_ts0 || !g_ts0_any || g_ts0_offset == INT64_MIN || bsp.trxd_fd < 0 || g_trx_cale) {
        return;
    }
    int64_t w = ((int64_t)tick_fn + g_ts0_offset) % (int64_t)BSP_FN_MAX;
    if (w < 0) w += BSP_FN_MAX;
    unsigned i = (unsigned)w % BSP_TS0_RING;
    if (g_ts0[i].valid && g_ts0[i].fn == (uint32_t)w) {
        return;
    }
    int32_t devant = (int32_t)((uint32_t)w - g_ts0_fn_max);
    if (devant <= 0 || devant > 200) {
        return;
    }
    attentes++;
    int64_t t0 = qemu_clock_get_ns(QEMU_CLOCK_REALTIME);
    int64_t fin = t0 + (int64_t)max_ms * 1000000;
    bool servie = false;
    for (;;) {
        int64_t reste = fin - qemu_clock_get_ns(QEMU_CLOCK_REALTIME);
        if (reste <= 0) {
            break;
        }
        struct pollfd p = { .fd = bsp.trxd_fd, .events = POLLIN };
        if (poll(&p, 1, (int)((reste + 999999) / 1000000)) <= 0) {
            break;
        }
        for (int k = 0; k < 256; k++) {
            unsigned long long avant = bsp.bursts_seen;
            bsp_trxd_readable(NULL);
            if (bsp.bursts_seen == avant) break;
        }
        if (g_ts0[i].valid && g_ts0[i].fn == (uint32_t)w) {
            servie = true;
            break;
        }
    }
    double ms = (qemu_clock_get_ns(QEMU_CLOCK_REALTIME) - t0) / 1e6;
    total_ms += ms;
    if (servie) servies++; else echues++;
    if (attentes <= 10 || attentes % 1000 == 0) {
        printf("  [ts0] attente %lu : fn=%u (%+d devant la BTS) %s en %.1f ms "
               "(servies=%lu echues=%lu, %.0f ms au total)\n",
               attentes, (uint32_t)w, devant, servie ? "arrivee" : "ECHUE", ms,
               servies, echues, total_ms);
    }
}

int calypso_bsp_service(uint32_t current_fn)
{
    int n = 0;
    /* 1) empty the UDP socket into the internal queue (bsp_trxd_readable) */
    while (bsp.trxd_fd >= 0 && n < 256) {
        unsigned long long before = bsp.bursts_seen;
        bsp_trxd_readable(NULL);
        if (bsp.bursts_seen == before) break;   /* recvfrom < 8: nothing left */
        n++;
    }
    /* STREAM (CALYPSO_BSP_STREAM=1): deliver ONE TS0 burst per frame, the
     * oldest in FN order, instead of everything. The DSP then sees a stream of
     * CONSECUTIVE frames as on silicon: the firmware FB search finds the FCCH
     * at a CONSTANT position, the ToA stabilises and acquisition locks without
     * stalling the output. Decouples the absolute FN (BTS) from the tick FN
     * (QEMU): only the ORDER matters, and the SCH carries the real FN for
     * synchronisation. */
    static int stream = -1;
    if (stream < 0) { const char *e = calypso_getenv("CALYPSO_BSP_STREAM"); stream = (e && *e=='1') ? 1 : 0;
                      if (stream) BSP_LOG("STREAM on : 1 burst TS0/trame en ordre FN (cohérence horloge)"); }
    if (stream) {
        /* [2026-09-21] FN-keyed TS0 store (bsp_ts0_service): arrival order
         * until an SB fixes the ARM/tick offset, then the burst of the ARM's
         * frame at every tick. */
        bsp_attendre_trame(current_fn);
        bsp_ts0_service(current_fn);
        return n;
    }
    /* 2) otherwise: deliver this frame's bursts (DARAM + interrupt). */
    calypso_bsp_deliver_buffered(current_fn);
    return n;
}

static void bsp_drain_cb(void *opaque)
{
    static int64_t last_target = 0;
    /* Drain the DL UDP socket HERE, off the reliable REALTIME timer. Under
     * icount=auto the DSP (c54x_run) monopolises the mainloop thread, so the
     * bsp_trxd_readable iohandler is never served, device packets pile up
     * unread (Recv-Q grows) and delivery stops (BSP-DELIVER=0, D_BURST_D
     * empty, snr=0). 64 iterations is margin (~1-2 bursts per 5 ms). */
    {
        /* Direct PEEK on bsp.trxd_fd: is the data on THIS fd?
         * (errno=EAGAIN means nothing here; >0 means data is present.) */
        uint8_t tb[16]; struct sockaddr_in sa; socklen_t sl = sizeof(sa);
        errno = 0;
        ssize_t pk = (bsp.trxd_fd >= 0)
            ? recvfrom(bsp.trxd_fd, tb, sizeof(tb), MSG_DONTWAIT | MSG_PEEK,
                       (struct sockaddr *)&sa, &sl)
            : -99;
        int e = errno;
        for (int i = 0; i < 64 && bsp.trxd_fd >= 0; i++)
            bsp_trxd_readable(NULL);
        static uint64_t dc = 0;
        if (dc < 30 || (dc % 2000) == 0)
            /* Report `bursts_written` (incremented in deliver_buffered when a
             * burst really lands in DARAM), not `bursts_seen`: bursts_seen only
             * moves in calypso_bsp_rx_burst, which the inline-write delivery
             * path bypasses, so it reads 0 forever. */
            fprintf(stderr, "[BSP] DRAIN-CB #%llu fd=%d PEEK=%zd errno=%d "
                    "delivered=%llu enq_drops(stale=%llu,full=%llu) seen_DEAD=%llu\n",
                    (unsigned long long)dc, bsp.trxd_fd, pk, e,
                    (unsigned long long)bsp.bursts_written,
                    (unsigned long long)bsp.bursts_dropped_stale,
                    (unsigned long long)bsp.bursts_dropped_queue_full,
                    (unsigned long long)bsp.bursts_seen);
        dc++;
    }
    if (bsp.dsp) {
        uint32_t cur_fn = calypso_trx_get_fn();
        calypso_bsp_deliver_buffered(cur_fn);
    }
    int64_t now = qemu_clock_get_ns(QEMU_CLOCK_REALTIME);
    if (last_target == 0) last_target = now;
    int64_t target = last_target + BSP_DRAIN_PERIOD_NS;
    while (target <= now) {
        target += BSP_DRAIN_PERIOD_NS;
    }
    last_target = target;
    timer_mod(bsp.drain_timer, target);
}

/* Replay callback: enqueue one burst per virtual TN slot. */
static void bsp_replay_cb(void *opaque)
{
    if (replay_idx < replay_count) {
        ReplayBurst *r = &replay_bursts[replay_idx++];
        bsp_enqueue(r->tn, r->fn, r->iq, r->n);
        if (replay_idx <= 5 || (replay_idx % 1000) == 0) {
            BSP_LOG("REPLAY inject #%zu fn=%u tn=%u n=%u",
                    replay_idx, r->fn, (unsigned)r->tn, (unsigned)r->n);
        }
    } else if (replay_idx == replay_count && replay_count > 0) {
        BSP_LOG("REPLAY exhausted after %zu bursts (idle from now)",
                replay_count);
        replay_idx++;  /* prevent log spam */
    }
    timer_mod(replay_timer,
              qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + BSP_REPLAY_PERIOD_NS);
}

/* Load all bursts from a BSP_DUMP_RX_FILE-format dump into memory.
 * Returns number loaded, 0 on failure. */

/* RX delivery: end-of-transfer interrupt of the RIF-RX DMA channel.
 *
 * CAL000 §3.7.1 allows only two vectors for the RIF, one per exchange mode, and
 * both are unmasked in the measured IMR (0x50ef):
 *   vec16 / bit 0  = INT0n  "RIF receive"  — XIO mode, one word at a time
 *   vec30 / bit 14 = INT10n "DMA interrupt" — buffered mode, dedicated RIF-RX
 *                    channel (CAL000 §6: "The RIF-RX and RIF-TX have a
 *                    dedicated channel each"; "an end-DMA request is sent")
 * The BSP deposits a BUFFER in DARAM, so this is buffered mode: vec30, whose
 * trampoline is at 0x0158. CAL000 §5.1 has the vector map.
 *
 * Two earlier choices were wrong. vec21 (XINT = SPI transmit) and vec19 (TINT =
 * DSP timer) are RETE stubs in the PDROM table, unrelated to the RIF; a
 * native_twl run announced 24644 bursts on vec21 and 24857 on vec19, so the
 * correlator was never told a burst had arrived. vec28/bit12 is the TPU FRAME
 * interrupt, which announces "new frame", not "a burst arrived": the DSP arms
 * its RX window then waits for the end-of-reception interrupt.
 *
 * Measured on the deterministic replay (c54x_exe --rejouer, synthetic cell),
 * FB correlator (0x770a) executions:
 *   vec28/12 : 0      vec16/0 : 0      vec30/14 : 110 */
static void calypso_bsp_deliver(C54xState *dsp, int vec, int bit)
{
    (void)vec; (void)bit;
    c54x_interrupt_ex(dsp, C54X_IT_DMA_VEC, C54X_IT_DMA_BIT);
}

static size_t bsp_replay_load(const char *path)
{
    FILE *f = fopen(path, "rb");
    if (!f) {
        BSP_LOG("REPLAY open '%s' failed: %s", path, strerror(errno));
        return 0;
    }
    size_t loaded = 0;
    size_t cap = 256;
    replay_bursts = calloc(cap, sizeof(ReplayBurst));
    if (!replay_bursts) { fclose(f); return 0; }
    while (1) {
        uint8_t hdr[12];
        if (fread(hdr, 1, 12, f) != 12) break;
        if (memcmp(hdr, "IQ16", 4) != 0) {
            BSP_LOG("REPLAY bad magic at burst %zu, stop", loaded);
            break;
        }
        uint32_t fn = (uint32_t)hdr[4]
                    | ((uint32_t)hdr[5] << 8)
                    | ((uint32_t)hdr[6] << 16)
                    | ((uint32_t)hdr[7] << 24);
        uint8_t  tn = hdr[8];
        uint16_t n  = (uint16_t)hdr[9] | ((uint16_t)hdr[10] << 8);
        if (n == 0 || n > BSP_IQ_MAX_I16) {
            BSP_LOG("REPLAY out-of-range n=%u at burst %zu, stop", n, loaded);
            break;
        }
        if (loaded >= cap) {
            cap *= 2;
            ReplayBurst *grown = realloc(replay_bursts,
                                         cap * sizeof(ReplayBurst));
            if (!grown) {
                BSP_LOG("REPLAY OOM at %zu bursts, stop", loaded);
                break;
            }
            replay_bursts = grown;
        }
        ReplayBurst *r = &replay_bursts[loaded];
        r->fn = fn;
        r->tn = tn;
        r->n  = n;
        if (fread(r->iq, sizeof(int16_t), n, f) != (size_t)n) {
            BSP_LOG("REPLAY truncated at burst %zu, stop", loaded);
            break;
        }
        loaded++;
    }
    fclose(f);
    return loaded;
}

void calypso_bsp_init(C54xState *dsp)
{
    bsp.dsp = dsp;
    calypso_manifest_once();   /* dump active overrides (CALYPSO_INVARIANTS gate, default off) */
    /* DARAM target the BSP DMAs DL samples into. Default 0x2a00, identified
     * by canary injection:
     *   1. static PROM0 scan: 0x2a00 is the top STM #imm,ARx init (50 sites,
     *      AR1..AR6; companion BK=0x015e=350 = GSM burst size)
     *   2. runtime canary (CALYPSO_BSP_INJECT_CANARY=1): the DSP READS 0xCAFE
     *      at 0x2a00..0x2a13 from PC=0x93a5, the real consumer routine, with
     *      AR3 post-incrementing. End-to-end proof.
     * Override via env if needed. */
    bsp.daram_addr     = parse_uint_env("CALYPSO_BSP_DARAM_ADDR", 0x2a00);
    bsp.daram_len      = parse_uint_env("CALYPSO_BSP_DARAM_LEN",  296);
    bsp.bursts_seen = 0;
    bsp.bursts_written = 0;
    bsp.bursts_dropped_no_window = 0;
    /* HACK — CALYPSO_BSP_BYPASS_BDLENA=1 bypasses the IOTA BDLENA window. On
     * silicon the BSP only delivers samples to the DSP during the BDLENA window
     * IOTA asserts; in emulation this flag delivers EVERY burst, which is how
     * the DARAM address the correlator really reads gets probed. Default OFF.
     * Removal criterion: DARAM target identified and a_pm / a_sync_demod
     * published nonzero by the DSP. */
    bsp.bypass_bdlena = (uint8_t)parse_uint_env("CALYPSO_BSP_BYPASS_BDLENA", 0);
    if (bsp.bypass_bdlena) {
        BSP_LOG("HACK: CALYPSO_BSP_BYPASS_BDLENA=1 — IOTA BDLENA gate DISABLED");
    }
    /* Canary injection (debug): with CALYPSO_BSP_INJECT_CANARY=1 the BSP
     * overwrites every sample with the 0xCAFE marker before the DARAM write.
     * Combined with the data_read_locked canary watch in calypso_c54x.c, this
     * pinpoints WHERE the DSP reads the BSP buffer at runtime. Disable in
     * normal runs. */
    bsp.inject_canary = (uint8_t)parse_uint_env("CALYPSO_BSP_INJECT_CANARY", 0);
    if (bsp.inject_canary) {
        BSP_LOG("HACK: CALYPSO_BSP_INJECT_CANARY=1 — samples overwritten with 0xCAFE for buffer discovery");
    }
    bsp.bursts_dropped_queue_full = 0;
    bsp.bursts_dropped_stale = 0;
    memset(bsp.q, 0, sizeof(bsp.q));
    bsp.trxd_fd = -1;
    /* Pre-set UL peer to bridge default (TRXDv0 listener on 127.0.0.1:5702).
     * Eliminates the race where ARM/DSP fires the first UL burst before any
     * DL has arrived to learn the peer addr. The peer is refined to the
     * actual sender on first DL receive (bsp_trxd_readable). */
    memset(&bsp.trxd_peer, 0, sizeof(bsp.trxd_peer));
    bsp.trxd_peer.sin_family = AF_INET;
    bsp.trxd_peer.sin_port   = htons(5702);
    bsp.trxd_peer.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    bsp.trxd_peer_valid = true;

    /* Deterministic-replay short-circuit. If CALYPSO_BSP_REPLAY_FILE is
     * set, load it now and skip the UDP listener. Replay timer takes over
     * the supply role. */
    const char *replay_path = calypso_getenv("CALYPSO_BSP_REPLAY_FILE");
    if (replay_path && *replay_path) {
        replay_count = bsp_replay_load(replay_path);
        BSP_LOG("REPLAY mode: loaded %zu bursts from %s (UDP socket bypassed)",
                replay_count, replay_path);
        if (replay_count > 0) {
            replay_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, bsp_replay_cb,
                                        NULL);
            timer_mod(replay_timer,
                      qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL)
                      + BSP_REPLAY_PERIOD_NS);
        }
        goto skip_udp_listener;
    }

    /* Bind UDP socket for TRXDv0 DL bursts from bridge/BTS.
     *
     * Default bind = 0.0.0.0 (was 127.0.0.1 hard-coded) so external
     * sources can inject bursts — bridge in the same netns still works,
     * and the host or other containers can reach BSP via the container
     * IP or via Docker port mapping (-p 6702:6702/udp).
     *
     * Override via env :
     *   CALYPSO_BSP_BIND_ADDR=<ip>  bind explicit IPv4 (e.g. 127.0.0.1,
     *                                172.20.0.11). Default: 0.0.0.0
     *   CALYPSO_BSP_BIND_LOOPBACK=1 legacy alias = 127.0.0.1 */
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd >= 0) {
        int one = 1;
        setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));

        const char *bind_addr_env = calypso_getenv("CALYPSO_BSP_BIND_ADDR");
        const char *bind_lo_env   = calypso_getenv("CALYPSO_BSP_BIND_LOOPBACK");
        const char *bind_addr     = NULL;
        if (bind_addr_env && *bind_addr_env)
            bind_addr = bind_addr_env;
        else if (bind_lo_env && *bind_lo_env == '1')
            bind_addr = "127.0.0.1";
        else
            bind_addr = "0.0.0.0";

        /* Port override: lets a Python proxy (iq_proxy.py) sit between the
         * source and QEMU. The source keeps sending to 6702 while QEMU listens
         * on CALYPSO_BSP_PORT and the proxy applies e.g. a Doppler shift. */
        const char *port_env = calypso_getenv("CALYPSO_BSP_PORT");
        int bsp_port = BSP_TRXD_PORT;
        if (port_env && *port_env) {
            int p = atoi(port_env);
            if (p > 0 && p < 65536) bsp_port = p;
        }
        struct sockaddr_in sa = {
            .sin_family = AF_INET,
            .sin_port = htons(bsp_port),
        };
        if (inet_aton(bind_addr, &sa.sin_addr) == 0) {
            BSP_LOG("CALYPSO_BSP_BIND_ADDR=%s invalid, falling back to 0.0.0.0",
                    bind_addr);
            sa.sin_addr.s_addr = htonl(INADDR_ANY);
            bind_addr = "0.0.0.0";
        }
        if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) == 0) {
            fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
            qemu_set_fd_handler(fd, bsp_trxd_readable, NULL, NULL);
            bsp.trxd_fd = fd;
            BSP_LOG("TRXD UDP listening on %s:%d", bind_addr, bsp_port);
        } else {
            BSP_LOG("TRXD bind %s:%d failed: %s",
                    bind_addr, bsp_port, strerror(errno));
            close(fd);
        }
    }

skip_udp_listener:
    /* Pre-init env-gated state so the first RACH burst does not pay for
     * getenv/strtoul mid-run. */
    (void)d_rach_word_offset();
    (void)rach_force_bsic();

    /* Arm the REALTIME drain timer: wall-paced 5 ms, monotonic anti-drift in
     * bsp_drain_cb, on the same CLOCK_MONOTONIC as the clk_master pthread
     * (calypso_trx.c). On VIRTUAL the drain lagged under DSP load and 95 % of
     * bursts were dropped. */
    bsp.drain_timer = timer_new_ns(QEMU_CLOCK_REALTIME, bsp_drain_cb, NULL);
    timer_mod(bsp.drain_timer,
              qemu_clock_get_ns(QEMU_CLOCK_REALTIME) + BSP_DRAIN_PERIOD_NS);
    BSP_LOG("BSP drain timer armed: %dms REALTIME wall-paced, monotonic",
            BSP_DRAIN_PERIOD_MS);

    BSP_LOG("init dsp=%p daram_addr=0x%04x len=%u%s%s",
            (void *)dsp, bsp.daram_addr, bsp.daram_len,
            bsp.daram_addr ? "" : "  (DISCOVERY mode — no DMA)",
            "");
}

/* ---- DL burst → DSP DARAM ---- */

/* TS1..TS7 filler (see calypso_bsp_set_remplissage): a dummy burst padded to a
 * 157-symbol timeslot with guard silence; zeros until the bench registers one. */
/* g_remplissage and bsp_source_toutes_ts: defined before bsp_livrer_trame() */
void calypso_bsp_set_remplissage(const int16_t *iq, int n_int16)
{
    memset(g_remplissage, 0, sizeof g_remplissage);
    if (!iq || n_int16 <= 0) return;
    if (n_int16 > 2 * 157) n_int16 = 2 * 157;
    for (int i = 0; i < n_int16; i++) g_remplissage[i] = (uint16_t)iq[i];
}

/* TCH RECORDER, to replay off the bench EXACTLY what the DSP received
 * (tools/rejeu_banc.c of c54x_exe). Every I/Q delivery during a TCH: 'B',
 * tick BE32, tn, fn BE32, one_shot, nwin BE16, n_int16 BE16, then n_int16
 * native int16 samples. The ARM writes into the API RAM are recorded next to
 * it by pont.c ('A'). 60000 deliveries at most, from the arming of the
 * dedicated channel (SDCCH included); CALYPSO_REJEU_ENREG=0 disables. */
uint32_t calypso_trx_get_fn(void);
static FILE *g_enreg_f;
static unsigned long g_enreg_n;
FILE *calypso_bsp_enreg_fichier(void)
{
    static int on = -1;
    /* CALYPSO_REJEU_ENREG=tch: record only the TCH (otherwise the cap can be
     * reached by the SDCCHs at the start of the run, before the call);
     * CALYPSO_REJEU_ENREG_MAX=<n>: delivery cap (60000). */
    static int tch_seul = 0; static unsigned long plafond = 60000;
    if (on < 0) { const char *e = calypso_getenv("CALYPSO_REJEU_ENREG"); on = !(e && *e == '0');
                  tch_seul = (e && !strcmp(e, "tch")) ? 1 : 0;
                  const char *m = calypso_getenv("CALYPSO_REJEU_ENREG_MAX"); if (m && *m) plafond = strtoul(m, NULL, 0); }
    if (tch_seul && g_dedie_genre != BSP_DEDIE_TCH) return NULL;
    /* any dedicated channel (SDCCH included): what corrupts the DSP state may precede the TCH */
    if (!on || g_dedie_tn <= 0 || g_enreg_n >= plafond)
        return NULL;
    if (!g_enreg_f)
        g_enreg_f = fopen("/dev/shm/calypso_rejeu_tch.bin", "wb");
    return g_enreg_f;
}
static void bsp_enreg_burst(uint8_t tn, uint32_t fn, const int16_t *iq, int n)
{
    FILE *f = calypso_bsp_enreg_fichier();
    if (!f) return;
    uint32_t t = calypso_trx_get_fn();
    int nwin = calypso_rhea_dma_one_shot() ? calypso_rhea_dma_get_len_words() : 0;
    uint8_t h[15] = { 'B', t >> 24, t >> 16, t >> 8, t, tn, fn >> 24, fn >> 16, fn >> 8, fn,
                      (uint8_t)calypso_rhea_dma_one_shot(), (uint8_t)(nwin >> 8), (uint8_t)nwin,
                      (uint8_t)(n >> 8), (uint8_t)n };
    fwrite(h, 1, sizeof h, f);
    fwrite(iq, sizeof(int16_t), (size_t)n, f);
    fflush(f);
    g_enreg_n++;
}

void calypso_bsp_rx_burst(uint8_t tn, uint32_t fn,
                          const int16_t *iq, int n_int16)
{
    bsp.bursts_seen++;
    bsp_enreg_burst(tn, fn, iq, n_int16);
    /* [2026-09-19] Publish the frame of the burst being handed over. The global
     * existed (c54x_internal.h) but nothing ever wrote it, so every consumer
     * read 0 — including the rhea-dma transfer trace, which could not be lined
     * up with the firmware's "=>FB @ FNR X" for want of a frame number. */
    calypso_daram_last_fn = (unsigned)fn;
    g_depot_fn = (unsigned)fn; g_depot_seq++;
    /* Liveness marker for the rx_burst path (CALYPSO_DEBUG=BSP-RXBURST). */
    {
        static unsigned rxb_n;
        if (calypso_debug_enabled("BSP-RXBURST") &&
            (rxb_n <= 20 || rxb_n % 2000 == 0))
            fprintf(stderr, "[BSP] BSP-RXBURST #%u fn=%u tn=%u n=%d\n",
                    rxb_n, (unsigned)fn, (unsigned)tn, n_int16);
        rxb_n++;
    }

    if (!bsp.dsp) {
        if (bsp.bursts_seen <= 3)
            BSP_LOG("rx_burst: no DSP attached, dropping fn=%u tn=%u", fn, tn);
        return;
    }
    if (n_int16 <= 0 || iq == NULL) return;
    g_air_k_der = air_niveaux_on() ? air_pm_k(fn, tn) : 1.0;
    if (air_niveaux_on()) {
        uint64_t acc = 0;
        for (int i = 0; i < n_int16; i++) { int v = iq[i]; acc += (v < 0) ? (uint32_t)(-v) : (uint32_t)v; }
        g_air_dpm = bsp_apm_de_mav((double)acc / n_int16 * g_air_k_der, faux_air_gain(g_tick_courant, 5000));
    }
    if (calypso_bsp_dma_trace_on())
        printf("  [dma-dep] tick=%u insn=%u fn=%u tn=%u n=%d armee=%d one_shot=%d fenetre=%d\n", g_tick_courant,
               bsp.dsp->insn_count, (unsigned)fn, (unsigned)tn, n_int16 / 2, (int)calypso_rhea_dma_rx_armed(),
               (int)calypso_rhea_dma_one_shot(), calypso_rhea_dma_get_len_words() / 2);
    if (calypso_rhea_dma_one_shot() && calypso_rhea_dma_get_len_words() / 2 > 0 &&
        calypso_rhea_dma_get_len_words() / 2 < 150)
        bsp_pm_mav_noter(iq, n_int16, g_air_k_der);   /* measurement window: see calypso_bsp_rssi_apm_k() */

    /* [2026-09-20] FULL TIMESLOT. A bare 148-symbol burst (296 words) is padded
     * with the 8.25 guard symbols of its timeslot (156 symbols, 157 on
     * timeslots 3 and 7 so a frame is exactly 1250) whenever DMA2 runs in
     * continuous mode, i.e. during the FB search. There the RIF is a continuous
     * stream over the WHOLE frame (the firmware divides the ROM's ToA by
     * BITS_PER_TDMA = 1250, tpu.h) and the ROM counts what it consumes. The
     * one-shot SB window keeps the block it is given (190 samples with the
     * margins). CALYPSO_BSP_TRAME_PLEINE=0 disables. */
    static int pleine = -1;
    if (pleine < 0) { const char *e = calypso_getenv("CALYPSO_BSP_TRAME_PLEINE"); pleine = (e && *e == '0') ? 0 : 1; }
    const bool continu = !calypso_rhea_dma_one_shot();
    {
        static int16_t plein[2 * 157];
        if (pleine && n_int16 <= 296 && continu) {
            int cible = 2 * (156 + ((tn == 3 || tn == 7) ? 1 : 0));
            memcpy(plein, iq, (size_t)n_int16 * sizeof(int16_t));
            memset(plein + n_int16, 0, (size_t)(cible - n_int16) * sizeof(int16_t));
            iq = plein; n_int16 = cible;
        }
    }

    /* FOLLOW THE DMA AAD (CALYPSO_BSP_AAD_FOLLOW, default 1). bsp.daram_addr is
     * an env constant (0x2a00) that was never programmed from the address the
     * ROM hands its DMA, so the burst landed somewhere other than where the
     * running task reads it (0x0cce for both FB and SB). We now deposit at the
     * address actually programmed, whenever it is known. */
    {
        static int follow = -1;
        if (follow < 0) { const char *e = calypso_getenv("CALYPSO_BSP_AAD_FOLLOW"); follow = (e && *e=='0') ? 0 : 1; }
        if (follow) {
            uint16_t aad = calypso_rhea_dma_get_daram();
            if (aad && aad != bsp.daram_addr) {
                static uint16_t prev; static unsigned nl;
                if (aad != prev && nl < 8) { BSP_LOG("AAD_FOLLOW : depot du burst en 0x%04x (etait 0x%04x)", aad, bsp.daram_addr); prev = aad; nl++; }
                bsp.daram_addr = aad;
            }
            /* AND THE LENGTH. bsp.daram_len was hardcoded to 296, which
             * TRUNCATED the SB window: that window is 380 words (190 complex),
             * so the second 39-bit data block of the SCH was never transferred.
             * Measured: the DSP buffer matched the delivered samples over
             * exactly 296 words, then diverged. We now follow the page length
             * the DSP programs itself (ALGTH). CALYPSO_BSP_LEN_FOLLOW=0
             * restores the fixed cap. */
            static int lfollow = -1;
            if (lfollow < 0) { const char *e = calypso_getenv("CALYPSO_BSP_LEN_FOLLOW"); lfollow = (e && *e=='0') ? 0 : 1; }
            if (lfollow) {
                uint16_t lw = calypso_rhea_dma_get_len_words();
                if (lw && lw != bsp.daram_len && lw <= 2048) {
                    static uint16_t prevl; static unsigned nll;
                    if (lw != prevl && nll < 8) {
                        BSP_LOG("LEN_FOLLOW : fenetre de %u mots (etait %u) — "
                                "296 tronquait la SB qui en demande 380", lw, bsp.daram_len);
                        prevl = lw; nll++;
                    }
                    bsp.daram_len = lw;
                }
            }
        }
    }
    /* [2026-09-19] FOLLOW THE PAGE (CALYPSO_BSP_PAGE_FOLLOW, default 0 — an
     * experiment, not a behaviour).
     *
     * Measured with CALYPSO_BSP_VERIF over 4000 bursts: the address the burst
     * is deposited at is statistically INDEPENDENT of the page the DSP task is
     * about to read. Crossing each comparison with d_dsp_page gave
     *
     *     w_page=0 -> 0x0cce 2340x, 0x0e4e 272x, 0x2a00 222x
     *     w_page=1 -> 0x0cce 1024x, 0x0e4e 142x
     *
     * the same ~89% share to 0x0cce either way, while the DSP does alternate
     * its pages properly (d_dsp_page cycles 0x0002/0x0003, never 0x0001). The
     * burst was found intact in DARAM in 0.4% of cases, and not at all in 46%.
     * On silicon the API buffer ping-pongs with that bit; here it does not, so
     * the task reads a page the samples never reached.
     *
     * This deposits at base + w_page*stride while the GSM task is active. The
     * base is the LOWEST address the AAD has shown (0x0cce on this bench, the
     * page-0 buffer) and the stride defaults to the observed 0x180 = 384 words
     * separating the two buffers. CALYPSO_BSP_PAGE_STRIDE overrides it and
     * accepts a NEGATIVE value, which tests the inverted page mapping. */
    {
        static int pfollow = -1;
        static long stride;
        static uint16_t base0;
        if (pfollow < 0) {
            const char *e = calypso_getenv("CALYPSO_BSP_PAGE_FOLLOW");
            pfollow = (e && *e && *e != '0') ? 1 : 0;
            const char *st = calypso_getenv("CALYPSO_BSP_PAGE_STRIDE");
            stride = (st && *st) ? strtol(st, NULL, 0) : 0x180;
        }
        if (pfollow && bsp.dsp && bsp.dsp->api_ram && bsp.daram_addr) {
            uint16_t pg = bsp.dsp->api_ram[0x08D4 - 0x0800];
            if (!base0 || bsp.daram_addr < base0) base0 = bsp.daram_addr;
            if (pg & 2) {                       /* B_GSM_TASK: a task is armed */
                uint16_t cible = (uint16_t)(base0 + ((pg & 1) ? stride : 0));
                if (cible != bsp.daram_addr) {
                    static unsigned nl;
                    if (nl < 8) {
                        BSP_LOG("PAGE_FOLLOW : w_page=%d -> depot en 0x%04x "
                                "(AAD disait 0x%04x, base 0x%04x, pas %+ld)",
                                (int)(pg & 1), cible, bsp.daram_addr, base0, stride);
                        nl++;
                    }
                    bsp.daram_addr = cible;
                }
            }
        }
    }

    if (bsp.daram_addr == 0) {
        if (bsp.bursts_seen <= 5) {
            BSP_LOG("rx_burst fn=%u tn=%u n=%d (target unset)",
                    fn, tn, n_int16);
        }
        return;
    }

    /* The BDLENA gate is gone: the burst is delivered unconditionally. NO
     * d_dsp_page write happens here — the firmware drives the page flip through
     * dsp_end_scenario (MMIO write at 0x01A8). We only signal sample arrival to
     * the DSP; d_dsp_page is observed read-only below.
     *
     * FCCH probe (CALYPSO_IQDUMP_FCCH=1): coherence and dphi of the DECIMATED
     * burst written to DARAM. A real decimated FCCH gives dphi ~ +1.571 (pi/2)
     * and coh ~ 1, which checks the content layer of the feed. */
    if (calypso_getenv("CALYPSO_IQDUMP_FCCH")) {
        int ns = n_int16 / 2;
        double accr = 0, acci = 0, den = 0;
        for (int k = 1; k < ns; k++) {
            double i0 = iq[2*(k-1)], q0 = iq[2*(k-1)+1];
            double i1 = iq[2*k],     q1 = iq[2*k+1];
            accr += i1*i0 + q1*q0;
            acci += q1*i0 - i1*q0;
            den  += sqrt((i0*i0+q0*q0)*(i1*i1+q1*q1));
        }
        double mag = sqrt(accr*accr + acci*acci);
        double coh = den > 0 ? mag/den : 0;
        double dphi = atan2(acci, accr);
        if (coh > 0.85) {
            static int fcp = 0;
            if (fcp++ < 30)
                fprintf(stderr, "[BSP] FCCH-PROBE fn=%u ns=%d coh=%.3f dphi=%.3f (vise +1.571 @1SPS)\n",
                        (unsigned)fn, ns, coh, dphi);
        }
    }

    if (bsp.dsp && bsp.dsp->api_ram) {
        static uint32_t obs_n = 0;
        /* 0x08E2 is d_dsp_state; d_dsp_page is 0x08D4 (calypso_fbsb.h). */
        uint16_t cur = bsp.dsp->api_ram[0x08D4 - 0x0800];
        obs_n++;
        if (calypso_debug_enabled("PUMP") &&
            (obs_n <= 20 || obs_n % 37 == 0)) {
            fprintf(stderr, "[bsp-page] #%u rx_burst fn=%u tn=%u "
                    "d_dsp_page=0x%04x (B_GSM_TASK=%d w_page=%d)\n",
                    obs_n, fn, tn, cur, !!(cur & 2), !!(cur & 1));
            fflush(stderr);
        }
    }
    /* This wire announces an RX BURST: end of RIF-RX DMA transfer, hence
     * vec30/bit14 (INT10n). The IFR test skips the assert while the DSP has not
     * served the previous one, so interrupts do not stack when the DSP is
     * slower than the BSP delivery rate. */
    if (bsp.dsp && bsp.dsp->running &&
        !(bsp.dsp->ifr & (1 << C54X_IT_DMA_BIT))) {
        calypso_bsp_deliver(bsp.dsp, C54X_IT_DMA_VEC, C54X_IT_DMA_BIT);
        if (bsp.dsp->idle) bsp.dsp->idle = false;
    }

    /* @BEQUILLE — BSP_DIRECT_BRINT0  (CALYPSO_BSP_DIRECT_BRINT0, EXISTS, default OFF ; calypso_wire.env:=1)
     *   masque  : on silicon, BSP DMA completion (the BDLENA window) raises BRINT0
     *             on vec21/bit5 — the "buffer received" interrupt that wakes the
     *             FB-det correlator handler (ISR PROM1[0xFFD4] -> CALL 0xf310).
     *             The direct-feed path writes DARAM but never raises it, because
     *             the TPU->TSP->IOTA->BSP chain that produces the pulse is not
     *             wired; without it the correlator at 0x8d00 is NEVER dispatched
     *             (0 hits measured). We raise it here, with the same IFR bit5
     *             anti-stacking test as deliver_buffered.
     *   retirer : once calypso_iota_take_bdl_pulse() is fed by the TPU RX window
     *             and consumed on the live path (see TPU_RX_WIRE).
     */
    { static int _db = -1; if (_db < 0) _db = calypso_gate("CALYPSO_BSP_DIRECT_BRINT0", 0);
      /* MISSION-GATE: raise BRINT0 only while the DSP is actually on the FB/SB
       * mission (d_task_md), so the correlator wakeup lands at the right moment,
       * like the real "buffer received" on silicon.
       * FB=5 SB=6 TCH_FB=8 TCH_SB=9 (osmocom l1_environment.h). */
      uint16_t _md = c54x_task_md(bsp.dsp);
      int _fbsb = (_md == 5 || _md == 6 || _md == 8 || _md == 9);
      if (_db && _fbsb && bsp.dsp && bsp.dsp->running && !(bsp.dsp->ifr & (1 << 5))) {
        calypso_bsp_deliver(bsp.dsp, 21, 5);
        if (bsp.dsp->idle) bsp.dsp->idle = false;
      } }

    /* RX-FBFLAGS on the LIVE path (rx_burst). The dispatcher at
     * 0x8d00 -> 0xa076 is PURE POLLING (BITF/BC on RAM), so no interrupt or INTM
     * change is needed; it only needs the handshake bits.
     *
     * @BEQUILLE — RX_FBFLAGS (live rx_burst path)  (CALYPSO_RX_FBFLAGS, EXISTS, default OFF)
     *   masque  : the BRINT0 ISR (PROM1[0xFFD4] -> CALL 0xf310) is never taken
     *             (INTM=1), so the FB-det handshake bits it should set stay clear:
     *             data[0x3fad] bit15 (the kernel master gate, CC 0xa0a0 @0x8754 ->
     *             kernel 0xa076), 0x3faa bit2+bit8, 0x3fab bit8, 0x3fae bit8. We
     *             set them from burst delivery instead, gated on the FB/SB mission.
     *   retirer : once BRINT0 is really served and its ISR writes these bits; the
     *             twin block in deliver_buffered is dead code and omits 0x3fad,
     *             so remove that one first.
     *   NB      : encloses POKE_TASK_MD and POKE_DISPATCH below.
     */
    { static int _fbf = -1; if (_fbf < 0) _fbf = calypso_gate("CALYPSO_RX_FBFLAGS", 0);
      uint16_t _mdf = c54x_task_md(bsp.dsp);
      int _fbsbf = (_mdf == 5 || _mdf == 6 || _mdf == 8 || _mdf == 9);
      if (_fbf && _fbsbf && bsp.dsp) {
          { static unsigned _n; if (_n++ == 0) fprintf(stderr, "[bequille] RX_FBFLAGS ACTIF\n"); }
          bsp.dsp->data[0x3fad] |= 0x8000;   /* master kernel gate  @0x8754 */
          calypso_rxfb_fired = 1;   /* arms the 0x8753 probe on the c54x side */
          bsp.dsp->data[0x3faa] |= 0x0104;   /* bit2+bit8           @0x886b/85/98 */
          bsp.dsp->data[0x3fab] |= 0x0100;   /* bit8 (FBEN)         @0x888d */
          bsp.dsp->data[0x3fae] |= 0x0100;   /* bit8                @0x90c8/ed/28 */
          /* @BEQUILLE — POKE_TASK_MD (+ POKE_DISPATCH below)  (CALYPSO_POKE_TASK_MD,
           *              atoi>0, but DEFAULT 1 WHEN THE VARIABLE IS ABSENT)
           *   masque  : the task descriptor (d_task_md, API-RAM pages 0x0804/0x0818)
           *             is never published to the DSP, so the CALA enters the
           *             correlator with task_md=0, i.e. NO mission, and spins on
           *             nothing. We write the FB/SB mission by hand into the two
           *             API-RAM pages the DSP dispatcher reads; d_dsp_page already
           *             has bit1 (task-ready) set, only task_md was missing.
           *             POKE_DISPATCH goes further and replicates dsp_end_scenario.
           *   retirer : once the write-page DMA reaches the DSP (task_md read from
           *             0x0586+DB_W_D_TASK_MD); both pokes then become no-ops.
           *   NB      : default ON, but nested inside the RX_FBFLAGS block, so it is
           *             never reached without CALYPSO_RX_FBFLAGS.
           */
          { static int _pt = -1; if (_pt < 0) { const char *_pe = calypso_getenv("CALYPSO_POKE_TASK_MD"); _pt = _pe ? (atoi(_pe) > 0) : 1; }  /* default ON, =0 to disable */
            if (_pt) { static unsigned _n;
                       if (_n++ == 0) fprintf(stderr, "[bequille] POKE_TASK_MD ACTIF (ecrit d_task_md=%u pages 0/1)\n", _mdf);
                       bsp.dsp->data[0x0804] = _mdf;   /* task_md page0 = mission (5=FB 6=SB) */
                       bsp.dsp->data[0x0818] = _mdf; } /* task_md page1 */ }
          /* POKE_DISPATCH replicates osmocom dsp_end_scenario (dsp.c:480):
           * d_task_md on the current WRITE page plus d_dsp_page =
           * B_GSM_TASK(0x0002)|w_page, ALTERNATING 2<->3 (native stays at 2, so
           * w_page never flips). */
          { static int _pd = -1; static uint16_t _wp = 0;
            if (_pd < 0) { const char *_de = calypso_getenv("CALYPSO_POKE_DISPATCH"); _pd = _de ? (atoi(_de) > 0) : 0; }
            if (_pd) {
                bsp.dsp->data[_wp ? 0x0818 : 0x0804] = _mdf;      /* d_task_md on the write page */
                /* d_dsp_page is 0x08D4, not 0x08E2 (that is d_dsp_state), and the
                 * ROM reads the API RAM, not data[]: calypso_c54x.c serves the
                 * 0x0800+ range from api_ram. */
                if (bsp.dsp->api_ram)
                    bsp.dsp->api_ram[0x08D4 - 0x0800] = (uint16_t)(0x0002 | _wp);
                else
                    bsp.dsp->data[0x08D4] = (uint16_t)(0x0002 | _wp);
                _wp ^= 1;                                         /* flip w_page (2<->3) */
            } }
          static unsigned _fbfn = 0;
          if (_fbfn++ < 8)
              fprintf(stderr, "[c54x] RX-FBFLAGS(live): 3fad|=0x8000 3faa|=0x104 "
                      "3fab|=0x100 3fae|=0x100 (task_md=%u fn=%u)\n",
                      _mdf, (unsigned)fn);
      } }

    /* @BEQUILLE — BSP_DISPATCH_FB (+ _TGT, _NOIMR, _ONESHOT)  (CALYPSO_BSP_DISPATCH_FB,
     *              EXISTS, default OFF ; calypso_wire.env:=1)
     *   masque  : the native LUT at 0x8341 that installs the FB-det handler 0x8d00
     *             into the dispatch slots is never reached (0x7234 derails into the
     *             0x013b overlay through a garbage d_dsp_page), so the computed
     *             BACC/CALA always resolves to the stub 0xab38. We write slots
     *             0x43c0/0x4387/0x43d8 by hand — per burst on the FB/SB mission,
     *             not a static pin — and open IMR bit9 in the scheduler's place, so
     *             the next BACC(0xb40f)/CALA(0xb01e) lands in the correlator.
     *   retirer : once 0x7234 reaches 0x8341 and populates these slots by itself;
     *             the IMR unmask is separable (CALYPSO_BSP_DISPATCH_NOIMR=1).
     */
    { static int _di = -1; static uint16_t _tgt = 0; static int _os = -1; static int _done = 0;
      if (_di < 0) { _di = calypso_gate("CALYPSO_BSP_DISPATCH_FB", 0);
        const char *_t = calypso_getenv("CALYPSO_BSP_DISPATCH_FB_TGT");
        _tgt = (_t && *_t) ? (uint16_t)strtoul(_t, NULL, 0) : 0x8d00;
        _os = calypso_gate("CALYPSO_BSP_DISPATCH_ONESHOT", 0); } /* default target 0x8d00.
        * ONESHOT installs and raises BRINT0 only ONCE instead of re-dispatching
        * every frame; re-dispatching congests the correlator, which re-enters in
        * a loop without finishing. One pulse lets it run a full pass. */
      uint16_t _md2 = c54x_task_md(bsp.dsp);
      int _fb2 = (_md2 == 5 || _md2 == 6 || _md2 == 8 || _md2 == 9);
      if (_di && _fb2 && bsp.dsp && bsp.dsp->running && !(_os && _done)) {
        _done = 1;
        bsp.dsp->data[0x43c0] = _tgt;   /* terminal BACC slot 0xb40f (was go-live 0xa4c7) */
        bsp.dsp->data[0x4387] = _tgt;   /* idle/CALA slot 0xb01e (was stub 0xab38) */
        bsp.dsp->data[0x43d8] = _tgt;   /* reseed slot (was stub 0xab38) */
        { /* NOIMR: unmasking the IMR is separable from installing the handler.
           * The unmask preempts the FB routine 6 instructions after entry
           * (interrupt to vec21/0x00d4), so CALYPSO_BSP_DISPATCH_NOIMR=1
           * installs the handler WITHOUT touching the IMR. */
          static int _noimr = -1;
          if (_noimr < 0) _noimr = calypso_gate("CALYPSO_BSP_DISPATCH_NOIMR", 0);
          if (!_noimr) bsp.dsp->imr |= 0x0200;   /* bit9: route the frame scheduler */
        }
        static unsigned _dl = 0;
        if (_dl++ < 8)
            fprintf(stderr, "[c54x] BSP-DISPATCH-FB : install 0x%04x (LUT setup FB) "
                    "-> slots 0x43c0/0x4387/0x43d8 + IMR|=bit9 (task_md=%u fn=%u) insn=%u\n",
                    _tgt, _md2, (unsigned)fn, bsp.dsp->insn_count);
      } }

    /* [2026-09-19] Deposit the WHOLE burst. The DARAM write used to be clamped to
     * bsp.daram_len, a length read back from the DMA page register, which is 96 on
     * most frames: a 296-word burst then lost two thirds of its samples and what
     * landed in memory matched the burst at no offset at all. Measured before the
     * fix: 0/296 words identical to the injected burst right after rx_burst, on
     * every SCH frame. The only real bound is the buffer itself. The serial-port
     * path above was already fixed this way; the DARAM write was not. */
    int n = n_int16 < BSP_IQ_MAX_I16 ? n_int16 : BSP_IQ_MAX_I16;

    /* Load samples into the BSP serial port buffer (PORTR PA=0x0034). The DSP
     * reads one sample per PORTR instruction from this buffer. Deliver the
     * WHOLE burst (iq[] = interleaved I/Q, 2*nbits int16): truncating to 148
     * int16 gave the correlator only 74 complex symbols = HALF the burst, so
     * the FCCH tone could never correlate and FBSB_CONF was never emitted.
     * Bound on n_int16, the real burst size, not on n, which is clamped to
     * daram_len for the DARAM write. */
    {
        /* Same cap as the DARAM buffer (BSP_IQ_MAX_I16). This path feeds
         * c54x_bsp_load, the BSP port, which measures `BSP LOAD=0` in every
         * run and so is not the active path — but the two caps must not
         * diverge. */
        uint16_t samples[BSP_IQ_MAX_I16];
        int ns = n_int16 > BSP_IQ_MAX_I16 ? BSP_IQ_MAX_I16 : n_int16;
        for (int i = 0; i < ns; i++)
            samples[i] = (uint16_t)iq[i];
        c54x_bsp_load(bsp.dsp, samples, ns);
    }
    /* [2026-09-20] THE OTHER SEVEN TIMESLOTS. The injectors only carry TS0
     * (the cell's FCCH/SCH/BCCH bursts); on silicon the FB search receives the
     * whole frame, 1250 symbols, and its ToA counts them. When a TS0 burst
     * arrives in continuous mode, the idle remainder of the frame follows it
     * here (no signal on TS1..TS7), so the stream keeps the frame's length.
     * A caller that delivers all eight timeslots itself passes tn != 0 for the
     * others and is left alone. */
    /* [2026-09-20] A source that delivers the other timeslots itself (pont.py
     * --dsp-port forwards all eight TRXD bursts of every frame) must not get
     * the seven fillers on top: the stream then carried 15 timeslots per
     * frame, the ROM's frame counting and TOA were off by 2x and the DSP did
     * twice the DMA/ISR work per frame. Remembered from the first tn != 0
     * burst seen. */
    if (tn != 0) bsp_source_toutes_ts = true;
    if (pleine && continu && tn == 0 && n_int16 <= 2 * 157 && !bsp_source_toutes_ts) {
        for (int ts = 1; ts < 8; ts++)
            c54x_bsp_load(bsp.dsp, g_remplissage, 2 * (156 + ((ts == 3 || ts == 7) ? 1 : 0)));
    }

    /* [2026-09-19] REFERENCE PROBE. Keeps a copy of the burst rx_burst was handed,
     * together with the destination address, length and frame. Everything it needs
     * is in scope here, so no caller has to guess which burst went where. The probe
     * The comparison itself runs LATER, from the frame loop, once the DSP has
     * executed and its DMA has drained the RIF: nothing is in DARAM yet at the
     * moment the burst is handed over. Read it with calypso_bsp_verif_compare().
     * Gate CALYPSO_BSP_VERIF. */
    if (calypso_getenv("CALYPSO_BSP_VERIF")) {
        unsigned i = bsp_verif_pos;
        int nv = n_int16 < BSP_IQ_MAX_I16 ? n_int16 : BSP_IQ_MAX_I16;
        memcpy(bsp_verif_h[i].iq, iq, (size_t)nv * sizeof(int16_t));
        bsp_verif_h[i].n = nv;
        bsp_verif_h[i].addr = bsp.daram_addr;
        bsp_verif_h[i].fn = fn;
        bsp_verif_h[i].page = (bsp.dsp && bsp.dsp->api_ram)
                            ? bsp.dsp->api_ram[0x08D4 - 0x0800] : 0xffffu;
        bsp_verif_pos = (i + 1) % BSP_VERIF_HIST;
        if (bsp_verif_plein < BSP_VERIF_HIST) bsp_verif_plein++;
    }

    /* [2026-09-19] The burst is NOT written into DARAM from here any more.
     * c54x_bsp_load above hands it to the RIF receive FIFO, and the DSP's own
     * DMA2 drains the FIFO into the API page it has programmed
     * (calypso_rhea_dma_rx_request). That is the silicon path and it works:
     * measured 17500 transfers of 296 words in 4 chained pages to the address
     * the firmware itself armed.
     *
     * Writing DARAM here as well made rx_burst a SECOND writer of the same
     * buffer, and the later writer wins: the DMA deposited the SCH burst at the
     * address the SB task had armed, then the next burst to arrive — a frame
     * that is not the SCH — overwrote it before the DSP read it. Hence an SB
     * that is attempted and comes back empty. One buffer, one writer. */
    bsp.bursts_written++;

    /* I/Q dumps for offline analysis (e.g. locating the FCCH peak at
     * +67.7 kHz = 1625/24). Captures COHERENT bursts (= FCCH) rather than the
     * first 24, which are non-FCCH startup traffic; coh uses the same math as
     * FCCH-PROBE. Outputs: /tmp/iq_rx_*.bin (CALYPSO_IQDUMP, raw int16) and
     * BSP_DUMP_RX_FILE (IQ16: 12-byte header [magic|fn LE|tn|n_int16 LE|pad]
     * then int16). */
    if (calypso_getenv("CALYPSO_IQDUMP") || calypso_getenv("BSP_DUMP_RX_FILE")) {
        int nsx = n / 2;
        double ar = 0, ai = 0, dn = 0;
        for (int k = 1; k < nsx; k++) {
            double i0 = iq[2*(k-1)], q0 = iq[2*(k-1)+1];
            double i1 = iq[2*k],     q1 = iq[2*k+1];
            ar += i1*i0 + q1*q0; ai += q1*i0 - i1*q0;
            dn += sqrt((i0*i0+q0*q0)*(i1*i1+q1*q1));
        }
        double bcoh = dn > 0 ? sqrt(ar*ar+ai*ai)/dn : 0;
        if (bcoh > 0.85) {   /* coherent burst = FCCH */
            if (calypso_getenv("CALYPSO_IQDUMP")) {
                static unsigned rx_dump_n;
                if (rx_dump_n < 24) {
                    char path[80];
                    snprintf(path, sizeof(path), "/tmp/iq_rx_%03u.bin", rx_dump_n);
                    FILE *f = fopen(path, "wb");
                    if (f) { fwrite(iq, sizeof(int16_t), n, f); fclose(f); }
                    rx_dump_n++;
                }
            }
            const char *bp = calypso_getenv("BSP_DUMP_RX_FILE");
            if (bp && *bp) {
                static FILE *bf; static int binit;
                if (!binit) { bf = fopen(bp, "wb"); binit = 1; }
                if (bf) {
                    uint8_t hdr[12] = { 0x49,0x51,0x31,0x36,
                        (uint8_t)fn, (uint8_t)(fn>>8), (uint8_t)(fn>>16), (uint8_t)(fn>>24),
                        (uint8_t)tn, (uint8_t)n, (uint8_t)(n>>8), 0 };
                    fwrite(hdr, 1, 12, bf);
                    fwrite(iq, sizeof(int16_t), n, bf);
                    fflush(bf);
                }
            }
        }
    }

    /* Log DARAM content after write for FB bursts (inside lock so values
     * read are consistent with what we just wrote). */
    if (bsp.bursts_written <= 3) {
        BSP_LOG("DARAM after write [0x%04x]: %d %d %d %d %d %d %d %d",
                bsp.daram_addr,
                n>0?(int16_t)bsp.dsp->data[bsp.daram_addr]:0,
                n>⟨1⟩?(int16_t)bsp.dsp->data[bsp.daram_addr+⟨2⟩]:0,  ×6
    ⟨⟩ = (1,1) (2,2) (3,3) (4,4) (5,5) (6,6)
                n>7?(int16_t)bsp.dsp->data[bsp.daram_addr+7]:0);
    }
    calypso_pcb_daram_lock_release();
    if (bsp.bursts_written <= 5 || (bsp.bursts_written % 1000) == 0) {
        BSP_LOG("DMA fn=%u tn=%u n=%d → DARAM[0x%04x..0x%04x] total=%llu "
                "iq[0..3]=%d,%d,%d,%d",
                fn, tn, n, bsp.daram_addr,
                (unsigned)(bsp.daram_addr + n - 1),
                (unsigned long long)bsp.bursts_written,
                n>0 ? iq[0] : 0, n>1 ? iq[1] : 0,
                n>2 ? iq[2] : 0, n>3 ? iq[3] : 0);
    }

    /* Fire BRINT0. On silicon the firmware opens the RX window through a TPU
     * scenario -> TSP write -> IOTA BDLENA, and BRINT0 fires once per window;
     * here the IFR bit rate-limits it instead. */
    if (bsp.dsp && !(bsp.dsp->ifr & (1 << 5))) {
        calypso_bsp_deliver(bsp.dsp, 21, 5);
        if (bsp.dsp->idle) bsp.dsp->idle = false;
    }
}

/* ---- Deliver buffered burst when BDLENA fires ---- */
/* Called from calypso_tdma_tick (calypso_trx.c) each frame.
 * For each TN: purge stale entries, then if a queued burst matches the
 * current QEMU virtual FN and a BDLENA pulse is pending, deliver it. */
void calypso_bsp_deliver_buffered(uint32_t current_fn)
{
    if (!bsp.dsp || bsp.daram_addr == 0) return;

    for (int tn = 0; tn < BSP_NUM_TN; tn++) {
        /* Drain ALL matchable bursts per call. One burst per call dropped the
         * effective drain rate below the IPC arrival rate under BQL contention:
         * the queue filled and bursts more than 64 FN behind cur_fn were marked
         * stale (87 % drop measured). BSP_FN_MATCH_WINDOW in bsp_take_for_fn
         * bounds the catch-up, so there is no runaway. */
        int rxwin = 0;
        {
            /* @BEQUILLE — TPU_RX_WIRE (BDLENA pulse consumption)  (CALYPSO_TPU_RX_WIRE,
             *              EXISTS, default OFF)
             *   masque  : the RX-window -> BSP transfer is not wired.
             *             calypso_iota_take_bdl_pulse() has exactly one caller, this
             *             one, so DARAM 0x2a00 stayed empty and the FB correlator ran
             *             on garbage. On a pulse for this TN the wire (a) consumes the
             *             pulse, (b) sets the FB task in scheduler word d[0x3f92] bit11
             *             in place of the native ORM at 0xa539, which never runs because
             *             d[0x5a00]==0x88, and (c) delivers the NEAREST buffered burst,
             *             bypassing the FN-match window that never lands in full mode.
             *   retirer : once d[0x3f92] is set by the native ORM and the FN match
             *             succeeds without the bypass.
             */
            static int en = -1;
            if (en < 0) en = calypso_gate("CALYPSO_TPU_RX_WIRE", 0);
            if (en && calypso_iota_take_bdl_pulse((uint8_t)tn)) {
                rxwin = 1;
                if (bsp.dsp) bsp.dsp->data[0x3f92] |= 0x0800;
                static unsigned rxw_log;
                if (rxw_log++ < 12)
                    BSP_LOG("TPU-RX-WIRE tn=%u fn=%u : BDLENA pulse consumed -> "
                            "d[0x3f92]|=0x0800 (FB task queued) + deliver nearest",
                            tn, current_fn);
            }
        }
        BspBurstSlot *sl;
        while ((sl = rxwin ? bsp_take_nearest((uint8_t)tn, current_fn)
                           : bsp_take_for_fn(tn, current_fn)) != NULL) {

        /* No d_dsp_page write here; the probe below is read-only (see
         * calypso_bsp_rx_burst). */
        if (bsp.dsp && bsp.dsp->api_ram) {
            static uint32_t obs_n = 0;
            /* 0x08E2 is d_dsp_state; d_dsp_page is 0x08D4 (calypso_fbsb.h). */
        uint16_t cur = bsp.dsp->api_ram[0x08D4 - 0x0800];
            obs_n++;
            if (calypso_debug_enabled("PUMP") &&
                (obs_n <= 20 || obs_n % 37 == 0)) {
                fprintf(stderr, "[bsp-page] #%u drain fn=%u tn=%u "
                        "d_dsp_page=0x%04x (B_GSM_TASK=%d w_page=%d)\n",
                        obs_n, current_fn, tn, cur,
                        !!(cur & 2), !!(cur & 1));
                fflush(stderr);
            }
        }
        /* Anti-stacking gate: skip while the IFR bit is still set, i.e. the DSP
         * has not served the previous interrupt. */
        if (bsp.dsp && bsp.dsp->running &&
            !(bsp.dsp->ifr & (1 << C54X_IT_DMA_BIT))) {
            calypso_bsp_deliver(bsp.dsp, C54X_IT_DMA_VEC, C54X_IT_DMA_BIT);
            if (bsp.dsp->idle) bsp.dsp->idle = false;
        }

        /* Same rule as rx_burst: bound on the burst size and the buffer, never on
         * the DMA-derived page length. */
        int n = sl->n < BSP_IQ_MAX_I16 ? sl->n : BSP_IQ_MAX_I16;

        /* === SB-INPUT discriminator (phase based) ===
         * GMSK has a constant envelope, so magnitude(I,Q) is constant for FCCH
         * AND SCH. The only separating discriminant is the phase trajectory:
         *   FCCH   = pure tone -> constant dphase -> cross[k] = I[k]*Q[k-1] -
         *            Q[k]*I[k-1] keeps the same sign for every k
         *   SCH/NB = GMSK data -> dphase varies +/-90 deg per sample -> cross
         *            alternates
         * Count cross products sharing cross[0]'s sign over 10 pairs:
         *   >= 9 same sign -> TONAL_FB, <= 8 -> MODULATED.
         * nmax is kept as well, to detect SILENT. Capped at 600. */
        {
            static unsigned db_log;
            const unsigned LIMIT = 600;
            if (db_log < LIMIT) {
                const int N = 22 < n / 2 ? 22 : n / 2;  /* N pairs ⇒ 2N samples */
                int nmax = 0;
                for (int i = 0; i < 2 * N && i < n; i++) {
                    int s = (int)sl->iq[i];
                    if (s < 0) s = -s;
                    if (s > nmax) nmax = s;
                }
                int same_sign = 0;
                int cross0 = 0;
                int cross_logged[8] = {0};
                int cross_log_cnt = 0;
                int n_cross = 0;
                for (int k = 1; k < N && n_cross < 11; k++) {
                    int I  = (int)sl->iq[2*k];
                    int Q  = (int)sl->iq[2*k + 1];
                    int Ip = (int)sl->iq[2*(k-1)];
                    int Qp = (int)sl->iq[2*(k-1) + 1];
                    /* Wide type to avoid overflow: I*Q reaches 1G, the difference 2G. */
                    long cross_l = (long)I * (long)Qp - (long)Q * (long)Ip;
                    int cross = cross_l > 0 ? 1 : (cross_l < 0 ? -1 : 0);
                    if (n_cross == 0) cross0 = cross;
                    else if (cross != 0 && cross == cross0) same_sign++;
                    if (cross_log_cnt < 8) cross_logged[cross_log_cnt++] = cross;
                    n_cross++;
                }
                const char *cat;
                if (nmax < 64) cat = "SILENT";
                else if (same_sign >= 8) cat = "TONAL_FB";
                else cat = "MODULATED";
                BSP_LOG("BURST-IN fn=%u tn=%u %s nmax=%d cross0=%d same=%d/10 "
                        "signs=%d,%d,%d,%d,%d,%d,%d,%d",
                        (unsigned)sl->fn, (unsigned)tn, cat,
                        nmax, cross0, same_sign,
                        cross_logged[0], cross_logged[1], cross_logged[2],
                        cross_logged[3], cross_logged[4], cross_logged[5],
                        cross_logged[6], cross_logged[7]);
                db_log++;
                if (db_log == LIMIT)
                    BSP_LOG("BURST-IN log capped at %u", LIMIT);
            }
        }

        /* Liveness marker for the buffered delivery path
         * (CALYPSO_DEBUG=BSP-DELIVER). */
        {
            static unsigned dlv_n;
            if (calypso_debug_enabled("BSP-DELIVER") &&
                (dlv_n <= 20 || dlv_n % 2000 == 0))
                fprintf(stderr, "[BSP] BSP-DELIVER #%u fn=%u tn=%u n=%d (apply AFC)\n",
                        dlv_n, (unsigned)sl->fn, (unsigned)tn, n);
            dlv_n++;
        }
        /* apply_phase lives in c54x_bsp_load, where ALL feeds converge towards
         * the RIF. Applying it here as well would rotate twice, since this path
         * also goes through c54x_bsp_load. */

        uint16_t samples[296];
        for (int i = 0; i < n && i < 296; i++)
            samples[i] = (uint16_t)sl->iq[i];
        c54x_bsp_load(bsp.dsp, samples, n > 296 ? 296 : n);

        /* woff is LOCAL: a static offset rolled across bursts. */
        unsigned woff = 0;
        calypso_pcb_daram_lock_acquire();
        for (int i = 0; i < n; i++) {
            uint16_t a = (uint16_t)(bsp.daram_addr + woff);
            /* CALYPSO_BSP_INJECT_CANARY: overwrite with the 0xCAFE marker to
             * identify the real target buffer on the DSP side, through the
             * canary-read hook in c54x. */
            uint16_t v = bsp.inject_canary ? 0xCAFE : (uint16_t)sl->iq[i];
            bsp.dsp->data[a] = v;
            bsp_daram_wr_bucket(a);
            woff++;
            if (woff >= bsp.daram_len) woff = 0;
        }
        calypso_pcb_daram_lock_release();
        bsp.bursts_written++;

        /* I/Q dump for the deliver_buffered path: the post-AFC samples handed
         * to the correlator. Gated by CALYPSO_IQDUMP, independent counter,
         * iq_dlv prefix. */
        if (calypso_getenv("CALYPSO_IQDUMP")) {
            static unsigned dlv_dump_n;
            if (dlv_dump_n < 24) {
                char path[80];
                snprintf(path, sizeof(path), "/tmp/iq_dlv_%03u.bin", dlv_dump_n);
                FILE *f = fopen(path, "wb");
                if (f) {
                    for (int i = 0; i < n; i++) {
                        int16_t s = (int16_t)sl->iq[i];
                        fwrite(&s, sizeof(int16_t), 1, f);
                    }
                    fclose(f);
                    BSP_LOG("IQDUMP dlv #%u fn=%u tn=%u → %s (%d int16)",
                            dlv_dump_n, (unsigned)sl->fn, (unsigned)tn, path, n);
                }
                dlv_dump_n++;
            }
        }
        sl->valid = false;  /* consumed */

        /* === BRINT0 assert ===
         * Fire the BRINT0 IRQ (vec 21, IMR bit 5) after the DARAM write. On
         * silicon, BSP DMA completion raises it, waking the DSP into the ISR at
         * PROM1[0xFFD4] -> CALL 0xf310. Without it the DSP never learns a burst
         * is available and stays in its dispatcher loop polling data[0x3fab]
         * forever (59M reads observed). The chain:
         *   1. the 0xCAFE canary proves the end-to-end BSP->DSP read at 0x2a00
         *      (PC=0x93a5)
         *   2. the DSP polls data[0x3fab] bits through the dispatcher table at
         *      data[0x16b3]
         *   3. those bits are ORed by the ISR that BRINT0 triggers
         * The IFR test skips the assert while the previous BRINT0 is unserved,
         * so the IOTA pending queue cannot overflow when the DSP handles ISRs
         * more slowly than the 217 Hz burst rate. */
        if (bsp.dsp && !(bsp.dsp->ifr & (1 << 5))) {
            calypso_bsp_deliver(bsp.dsp, 21, 5);
        }

        /* RX-FBFLAGS on the buffered path. On silicon the BRINT0 ISR (0xf310)
         * ORs the FB-det handshake bits the correlator handler polls:
         *   data[0x3faa] bit2 (0x0004) + bit8 (0x0100)   @0x886b/0x8885/0x8898
         *   data[0x3fab] bit8 (0x0100)                   @0x888d
         *   data[0x3fae] bit8 (0x0100)                   @0x90c8/0x90ed/0x9128
         * The emulated ISR does not set them, so the handler loops over
         * 0x90b0-0x9130 without ever reaching the kernel. We set them at burst
         * delivery to DARAM 0x2a00, i.e. "burst ready".
         */
        {
            /* @BEQUILLE — RX_FBFLAGS (buffered path)  (CALYPSO_RX_FBFLAGS, EXISTS, default OFF)
             *   masque  : the same FB-det handshake bits the BRINT0 ISR should set, but
             *             WITHOUT data[0x3fad] bit15, so it cannot unblock the kernel.
             *   retirer : FIRST — this block is dead code: rx_burst delivers
             *             directly, so the buffered queue stays empty.
             */
            static int _fbf = -1;
            if (_fbf < 0) _fbf = calypso_gate("CALYPSO_RX_FBFLAGS", 0);
            if (_fbf && bsp.dsp) {
                bsp.dsp->data[0x3faa] |= 0x0104;   /* bit2 + bit8 */
                bsp.dsp->data[0x3fab] |= 0x0100;   /* bit8 (FBEN target) */
                bsp.dsp->data[0x3fae] |= 0x0100;   /* bit8 (confirmed gate) */
                static unsigned _fbfn = 0;
                if (_fbfn++ < 8)
                    BSP_LOG("RX-FBFLAGS: pose 0x3faa|=0x104 0x3fab|=0x100 0x3fae|=0x100 "
                            "(handshake FB-det depuis livraison burst)");
            }
        }

        /* RX I/Q tap: when BSP_DUMP_RX_FILE is set, append the raw burst
         * (n int16_t LE, interleaved I/Q) to the file. 12-byte header per burst:
         *   magic 'IQ16' (4B) | fn (4B LE) | tn (1B) | n_int16 (2B LE) | pad (1B)
         * Readable with fcch_ref.py <dump> --fmt int16 --burst N. */
        {
            static FILE *rx_dump_f = NULL;
            static int   rx_dump_init = 0;
            if (!rx_dump_init) {
                rx_dump_init = 1;
                const char *p = calypso_getenv("BSP_DUMP_RX_FILE");
                if (p && *p) {
                    rx_dump_f = fopen(p, "ab");
                    BSP_LOG("BSP_DUMP_RX_FILE='%s' fopen=%s",
                            p, rx_dump_f ? "ok" : strerror(errno));
                } else {
                    BSP_LOG("BSP_DUMP_RX_FILE not set (p=%p p[0]=%c)",
                            (void *)p, p ? p[0] : '?');
                }
            }
            if (rx_dump_f) {
                uint8_t hdr[12] = {
                    'I','Q','1','6',
                    (uint8_t)(sl->fn      ), (uint8_t)(sl->fn >>  8),
                    (uint8_t)(sl->fn >> 16), (uint8_t)(sl->fn >> 24),
                    tn,
                    (uint8_t)(n      ), (uint8_t)(n >> 8),
                    0
                };
                fwrite(hdr, 1, 12, rx_dump_f);
                fwrite(sl->iq, sizeof(int16_t), n, rx_dump_f);
                fflush(rx_dump_f);
            }
        }

        if (bsp.bursts_written <= 10 || (bsp.bursts_written % 1000) == 0) {
            BSP_LOG("DMA tn=%u fn=%u n=%d total=%llu stale=%llu qfull=%llu",
                    tn, sl->fn, n,
                    (unsigned long long)bsp.bursts_written,
                    (unsigned long long)bsp.bursts_dropped_stale,
                    (unsigned long long)bsp.bursts_dropped_queue_full);

            /* Dump first 8 words written so we can verify the I/Q
             * constellation actually landed in the DSP data memory at
             * daram_addr — independent of any ARM-side mapping. */
            calypso_pcb_daram_lock_acquire();
            BSP_LOG("DMA @0x%04x: %04x %04x %04x %04x %04x %04x %04x %04x",
                    bsp.daram_addr,
                    bsp.dsp->data[bsp.daram_addr + ⟨1⟩],  ×7
    ⟨⟩ = (0) (1) (2) (3) (4) (5) (6)
                    bsp.dsp->data[bsp.daram_addr + 7]);
            calypso_pcb_daram_lock_release();
        }

        /* Fire BRINT0 */
        if (bsp.dsp && !(bsp.dsp->ifr & (1 << 5))) {
            calypso_bsp_deliver(bsp.dsp, 21, 5);
            if (bsp.dsp->idle) bsp.dsp->idle = false;
        }
        }  /* end while drain */
    }
}

/* ---- UL burst → UDP to BTS ---- */

void calypso_bsp_send_ul(uint8_t tn, uint32_t fn, const uint8_t bits[148])
{
    if (bsp.trxd_fd < 0 || !bsp.trxd_peer_valid) return;

    /* TRXDv0 UL (TRX → BTS): tn(1) fn(4) rssi(1) toa(2) bits(148) = 156 bytes.
     *
     * The osmo-bts-trx TRXD protocol is *asymmetric* :
     *   - DL (BTS → TRX) : 6-byte header, 154 bytes total. No ToA.
     *   - UL (TRX → BTS) : 8-byte header WITH ToA, 156 bytes total. The
     *     ToA is needed by BTS RACH/SACCH timing-advance estimation.
     *
     * Sending 154-byte UL caused osmo-bts-trx::trx_if.c:821 to log
     *   "Rx TRXD PDU with odd burst length 146"
     * (BTS subtracts its 8-byte header from msg len, expects 148 body).
     * Always send 156 bytes for UL. */
    uint8_t pkt[8 + 148];
    pkt[0] = tn & 0x07;
    pkt[1] = (fn >> 24) & 0xff;
    pkt[2] = (fn >> 16) & 0xff;
    pkt[3] = (fn >>  8) & 0xff;
    pkt[4] =  fn        & 0xff;
    pkt[5] = 60;            /* RSSI → -60 dBm at the BTS */
    pkt[6] = 0; pkt[7] = 0; /* ToA256 = 0 (centered, no timing advance request) */
    for (int i = 0; i < 148; i++)
        pkt[8 + i] = bits[i] ? 127 : (uint8_t)(-127);

    /* Hex dump of every UL burst as it is sent, symmetric with the
     * calypso-ipc-device UL print, so L1 -> bridge -> BTS can be correlated at
     * the byte level when chasing TRXD framing or RACH parity issues. Capped at
     * 200 to keep the log finite. */
    {
        static unsigned ul_log_count = 0;
        if (ul_log_count++ < 200 || (ul_log_count % 1000) == 0) {
            BSP_LOG("UL #%u TN=%u fn=%u rssi=-60 toa=0 len=%zu "
                    "hdr=%02x%02x%02x%02x%02x%02x%02x%02x "
                    "bits[0:16]=[%+d %+d %+d %+d %+d %+d %+d %+d "
                    "%+d %+d %+d %+d %+d %+d %+d %+d]",
                    ul_log_count, tn, fn, sizeof(pkt),
                    pkt[0], pkt[1], pkt[2], pkt[3],
                    pkt[4], pkt[5], pkt[6], pkt[7],
                    (int8_t)pkt[8], (int8_t)pkt[9], (int8_t)pkt[10],
                    (int8_t)pkt[11], (int8_t)pkt[12], (int8_t)pkt[13],
                    (int8_t)pkt[14], (int8_t)pkt[15],
                    (int8_t)pkt[16], (int8_t)pkt[17], (int8_t)pkt[18],
                    (int8_t)pkt[19], (int8_t)pkt[20], (int8_t)pkt[21],
                    (int8_t)pkt[22], (int8_t)pkt[23]);
        }
    }

    sendto(bsp.trxd_fd, pkt, sizeof(pkt), 0,
           (struct sockaddr *)&bsp.trxd_peer, sizeof(bsp.trxd_peer));
}

bool calypso_bsp_tx_burst(uint8_t tn, uint32_t fn, uint8_t bits[148])
{
    if (!bsp.dsp || !bits) return false;

    /* On real Calypso, the DSP encodes the UL burst (channel coding +
     * interleaving + burst formation) and writes the 148 hard bits to a
     * DARAM buffer that the BSP TX DMA reads. The exact destination is
     * configured per task by TPU scenarios. We currently read from a
     * candidate location; if it's all-zero, the DSP encoder did not run
     * for this frame (timing miss or wrong addr) and we drop the burst. */
    bool any = false;
    calypso_pcb_daram_lock_acquire();
    for (int i = 0; i < 148; i++) {
        uint16_t w = bsp.dsp->data[0x0900 + i];
        bits[i] = (uint8_t)(w & 1);
        if (bits[i]) any = true;
    }
    calypso_pcb_daram_lock_release();

    return any;
}

/* ---- RACH access burst encoding ---- */
#include <osmocom/coding/gsm0503_coding.h>

/* d_rach lives in NDB at a struct offset that depends on the DSP version.
 * The firmware writes (uic|bsic)<<2 | (ra<<8) to ndb->d_rach right before
 * setting db_w->d_task_ra.
 *
 * Default 0x023A — confirmed empirically 2026-05-07 via D_RACH-FINDER ring
 * trace : ARM-side write at API byte 0x0474 (= DSP word 0x0A3A = word 0x23A
 * from API base) carries values 0x0300, 0x0f00, ... matching mobile L3
 * `RANDOM ACCESS ra 0xRR` log lines exactly.
 *
 * Read from the environment once and cached, so nothing is parsed mid-run. */
#define D_RACH_DEFAULT_OFFSET 0x023A
static uint32_t d_rach_word_offset(void)
{
    static uint32_t cached = 0;
    static bool     done = false;
    if (done) return cached;
    const char *e = calypso_getenv("CALYPSO_NDB_D_RACH_OFFSET");
    if (e && *e) {
        cached = (uint32_t)strtoul(e, NULL, 0);
        BSP_LOG("d_rach offset: 0x%04x (env=%s)", cached, e);
    } else {
        cached = D_RACH_DEFAULT_OFFSET;
        BSP_LOG("d_rach offset: 0x%04x (default macro — pinned 2026-05-07)", cached);
    }
    done = true;
    return cached;
}

/* CALYPSO_RACH_FORCE_BSIC=N forces the BSIC used by the RACH encoder to a
 * fixed value, overriding whatever is read from d_rach. Useful when the
 * d_rach offset is uncertain : if the BTS responds with IMM_ASS_CMD as
 * soon as we encode with the BSC's `base_station_id_code`, the chain is
 * proven and we know the only remaining bug is the d_rach offset.
 *
 * Returns -1 if unset, otherwise the forced BSIC value (0..63).
 *
 * @BEQUILLE — RACH_FORCE_BSIC  (CALYPSO_RACH_FORCE_BSIC, VALUE, default unset = inert)
 *   masque  : the uncertainty on the NDB offset of d_rach. Instead of reading the
 *             BSIC the firmware wrote, we impose the BSC's own, to prove the RACH
 *             encoding chain independently of that offset.
 *   retirer : once CALYPSO_NDB_D_RACH_OFFSET is confirmed, i.e. IMM_ASS_CMD is
 *             received with the BSIC read from d_rach and no forcing.
 */
static int rach_force_bsic(void)
{
    static int cached = -2;
    if (cached != -2) return cached;
    const char *e = calypso_getenv("CALYPSO_RACH_FORCE_BSIC");
    /* Same empty-string-as-unset handling as d_rach_word_offset(). */
    if (!e || !*e) {
        cached = -1;
        BSP_LOG("CALYPSO_RACH_FORCE_BSIC unset → BSIC read from d_rach");
        return cached;
    }
    long v = strtol(e, NULL, 0);
    if (v < 0 || v > 63) {
        BSP_LOG("CALYPSO_RACH_FORCE_BSIC=%s out of range [0..63] — ignored", e);
        cached = -1;
        return cached;
    }
    cached = (int)v;
    BSP_LOG("CALYPSO_RACH_FORCE_BSIC=%d (forcing all RACH bursts with this BSIC)", cached);
    return cached;
}

bool calypso_bsp_tx_rach_burst(uint32_t fn, uint8_t bits[148])
{
    if (!bsp.dsp || !bits) return false;

    /* Read d_rach from NDB. dsp->data[] is the DSP-side word view; the
     * API RAM at DSP word 0x0800.. is shared with the ARM-visible page
     * at 0xFFD00000. We address via dsp->data[0x0800 + offset]. */
    uint32_t off = d_rach_word_offset();
    uint16_t d_rach = calypso_dsp_daram_read(bsp.dsp, 0x0800 + off);
    if (d_rach == 0) {
        /* Pre-LU : firmware hasn't written d_rach yet. Normal during cell
         * selection / SI decode phase. Don't alarm — just skip silently
         * (cap log to first 5 to keep it visible if there's a real issue). */
        static unsigned zero_log = 0;
        if (zero_log++ < 5) {
            BSP_LOG("RACH: d_rach@0x%04x is zero — skipping #%u "
                    "(normal pre-LU, mobile not yet in RR_EST_REQ)",
                    off, zero_log);
        }
        return false;
    }

    /* prim_rach.c:73 packs as:
     *   d_rach[7:0]  = uic<<2 (or bsic<<2)
     *   d_rach[15:8] = ra (8-bit RACH info) */
    uint8_t uic_or_bsic = (uint8_t)((d_rach & 0xFF) >> 2);
    uint8_t ra          = (uint8_t)((d_rach >> 8) & 0xFF);

    /* Optional BSIC override (probes whether wrong BSIC is the only blocker). */
    int forced = rach_force_bsic();
    if (forced >= 0) {
        uic_or_bsic = (uint8_t)forced;
    }

    /* gsm0503_rach_ext_encode writes 148 unpacked bits (ubit_t=uint8_t 0/1)
     * into burst[]. is_11bit=false → use 8-bit RACH (legacy GSM). */
    int rc = gsm0503_rach_ext_encode(bits, ra, uic_or_bsic, false);
    if (rc < 0) {
        BSP_LOG("RACH encode failed rc=%d ra=0x%02x bsic=0x%02x", rc, ra, uic_or_bsic);
        return false;
    }

    static int rach_log = 0;
    if (++rach_log <= 20) {
        BSP_LOG("RACH encode #%d fn=%u ra=0x%02x bsic=0x%02x d_rach=0x%04x",
                rach_log, fn, ra, uic_or_bsic, d_rach);
    }
    return true;
}

/* Emit an UL RACH access burst from an EXPLICIT ra/bsic pair, bypassing the
 * d_rach DARAM read. Called from the write-d_rach hook in calypso_trx.c for the
 * case where the DSP d_task_ra task is swallowed and calypso_bsp_tx_rach_burst
 * never fires. One call is one real RACH attempt. */
bool calypso_bsp_send_rach_ra(uint8_t ra, uint8_t bsic, uint32_t fn, uint8_t tn)
{
    int forced = rach_force_bsic();
    if (forced >= 0) bsic = (uint8_t)forced;
    uint8_t bits[148] = {0};
    int rc = gsm0503_rach_ext_encode(bits, ra, bsic, false);
    if (rc < 0) {
        BSP_LOG("RACH-RA encode fail rc=%d ra=0x%02x bsic=0x%02x", rc, ra, bsic);
        return false;
    }
    static int lg = 0;
    if (++lg <= 20)
        BSP_LOG("RACH-RA encode #%d fn=%u ra=0x%02x bsic=0x%02x (hook d_rach)", lg, fn, ra, bsic);
    calypso_bsp_send_ul(tn, fn, bits);   /* -> 127.0.0.1:5702 -> bridge g_bsp_fd */
    return true;
}

4.52 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_bsp.h

6992 octets, 137 lignes → 137 lignes

/*
 * Calypso BSP/RIF DMA - public interface.
 *
 * Faithful path for downlink I/Q samples between sercomm_gate (the QEMU
 * surrogate of the IOTA RF frontend wired through calypso-ipc-device) and the
 * Calypso DSP DARAM. No NDB result hacking: the DSP code itself is expected to
 * find FB/SB and post the results in the NDB.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#ifndef HW_ARM_CALYPSO_BSP_H
#define HW_ARM_CALYPSO_BSP_H

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>

struct C54xState;

/*
 * Initialise the BSP DMA module. Call once after the C54x has been created.
 *
 * Two env vars control the DMA destination:
 *   CALYPSO_BSP_DARAM_ADDR - word address inside DSP data space (hex/dec)
 *   CALYPSO_BSP_DARAM_LEN  - max number of int16 words to copy per burst
 *
 * With CALYPSO_BSP_DARAM_ADDR unset or zero the BSP runs in DISCOVERY mode: it
 * logs every received burst but writes nothing into DARAM, so the FBDET
 * data-read tracer in c54x_mem.c can reveal the real buffer location before
 * the address is locked down.
 */
void calypso_bsp_init(struct C54xState *dsp);

void calypso_bsp_set_tpu_offset(int qbits);
/* Dedicated channel announced by QEMU (PONT_DCCH): kind 0 = SDCCH/4, 1 = SDCCH/8,
 * 0xFF (or tn <= 0) = released. The bursts of that timeslot replace those of
 * TS0 in the window delivered to the DSP. */
void calypso_bsp_set_dedie(int tn, int genre, int ss);
void calypso_bsp_toa_feedback(int toa);
int calypso_bsp_trx_calage(uint32_t fn_sb, uint32_t tick);
int calypso_bsp_trx_ul_prothese(uint16_t *dspdata, uint32_t tick, int w_page);   /* TRX mode: exact bits of the uplink burst in the result page (pont.c, phase A) */   /* TRX mode: fn/tick offset since the SB sent (pont.c) */
int  calypso_bsp_service(uint32_t current_fn);
int  calypso_bsp_sb_retenter(void);
int  calypso_bsp_nb_retenter(void);
void calypso_bsp_off_mobile_a5fn(uint32_t tick, uint32_t fn);
unsigned calypso_bsp_dedie_entrees(void);
void calypso_bsp_dfn_observe(uint32_t tick, uint16_t dfn, uint16_t task_d);   /* mobile fn (d_fn) */
/* Hot-tunable setting: NAME=value in /dev/shm/calypso_reglages (re-read when it
 * changes), else the environment variable NAME, else the default. */
int calypso_reglage(const char *nom, int defaut);
int calypso_bsp_tch_dernier_fn(uint32_t *fn);              /* last TCH burst delivered (0 outside TCH) */
int calypso_bsp_tch_burst(uint32_t fn, uint8_t bits[148]); /* its bits, 64 frames of history */
void calypso_bsp_off_mobile_rac(uint32_t tick, uint32_t fn42432);   /* RA_C from the L1 trace */   /* count of TI dedicated-mode entries */   /* exact fn of the dedicated channel */
int  calypso_bsp_fn_mobile_du_tick(uint32_t tick, uint32_t *fn);   /* fn learnt at the last SB */   /* NB kept in TI dedicated mode */
void calypso_bsp_sb_trace(unsigned ticks);   /* trace every SCH for n ticks ([sbwin]) */     /* redeliver an SCH held back for lack of armed DMA; 1 if delivered (pont.c, pump loop) */  /* drains UDP 6702 and delivers the current frame (standalone host) */

/*
 * Receive a downlink burst.
 *
 *   tn       - timeslot number (0..7)
 *   fn       - TDMA frame number
 *   iq       - interleaved int16 I,Q,I,Q,... in DSP-native (host) endianness
 *   n_int16  - number of int16 elements in iq[]  (= 2 * n_complex_samples)
 */
FILE *calypso_bsp_enreg_fichier(void);   /* TCH recorder, see calypso_bsp.c */
void calypso_bsp_rx_burst(uint8_t tn, uint32_t fn,
                          const int16_t *iq, int n_int16);

/* [2026-09-20] Filler for timeslots 1..7 during the FB search (continuous
 * DMA): the ROM receives the whole 1250-symbol frame, and on a real C0 carrier
 * the other timeslots carry dummy bursts, not silence. The bench registers one
 * GMSK dummy burst (148 samples, 296 int16); the default filler is zeros. */
void calypso_bsp_set_remplissage(const int16_t *iq, int n_int16);

/*
 * Transmit an uplink burst - symmetric to rx_burst.
 *
 * Reads 148 hard bits from the DSP UL buffer (where the L1 firmware
 * deposits encoded TX data) and fills bits[148]. Returns true if
 * the burst is valid (any non-zero), false otherwise.
 */
bool calypso_bsp_tx_burst(uint8_t tn, uint32_t fn, uint8_t bits[148]);

/* Build a RACH access burst (148 bits) by reading d_rach from NDB and
 * channel-encoding it via libosmocoding (gsm0503_rach_ext_encode).
 * Returns true if a valid RACH was produced, false if d_rach is zero or
 * the encoder failed. Called by calypso_trx.c when ARM L1 commits a
 * d_task_ra (RACH access). */
bool calypso_bsp_tx_rach_burst(uint32_t fn, uint8_t bits[148]);
bool calypso_bsp_send_rach_ra(uint8_t ra, uint8_t bsic, uint32_t fn, uint8_t tn);  /* uplink RACH built from d_rach */

uint16_t calypso_bsp_get_daram_addr(void);
uint32_t calypso_bsp_get_last_fn(void);
int calypso_bsp_fn_bts_du_tick(uint32_t tick, uint32_t *fn_bts);
uint16_t calypso_bsp_get_daram_len(void);

/* Reference probe: compares DARAM against the last bursts the BSP was handed;
 * *age names which one matched best (0 = this frame). -1 if none recorded. */
int calypso_bsp_verif_compare(uint32_t *fn, uint16_t *addr, int *n, int *age);
uint16_t calypso_bsp_verif_last_page(void);
uint8_t  calypso_bsp_get_last_att(void);

/* Send UL burst via UDP to BTS */
void calypso_bsp_send_ul(uint8_t tn, uint32_t fn, const uint8_t bits[148]);

/* Deliver buffered DL bursts when BDLENA windows are available.
 * Called each TDMA frame from calypso_tdma_tick().
 * current_fn is the QEMU virtual FN: only bursts tagged with that FN
 * (per TN) are delivered; stale bursts (fn < current_fn) are dropped,
 * future bursts (fn > current_fn) are kept for later frames. */
void calypso_bsp_deliver_buffered(uint32_t current_fn);

/* ---- DSP I/Q chain (moved out of the shunt on 2026-09-03) --------------- */

/* FB-STREAM: pops the next (I,Q) pair from the ring of decimated FCCH samples
 * fed by the DL bursts. Consumed by the data[0x9213]/[0x9215] read intercept,
 * which is the INPUT of the native correlator (gate CALYPSO_FB_STREAM).
 * Returns false when the ring is empty. */
bool calypso_bsp_fb_stream_next(uint16_t *outI, uint16_t *outQ);

/* RSSI integrator model: a_pm derives from the mean magnitude (MAV) actually
 * measured on the DL, calibrated by the trf6151 model as
 * a_pm = calib_RF(20*log10(MAV/MAV_REF) + RF_REF). It stands in for the ABB
 * power register the DSP reads, which the emulated ADC does not provide. */
uint16_t calypso_bsp_rssi_apm(void);
/* the k-th measurement of the frame (measurement windows in order), see calypso_bsp.c */
uint16_t calypso_bsp_rssi_apm_k(int k);
int calypso_bsp_pm_mav_texte(char *buf, size_t n);
int calypso_bsp_pm_mav_fort(void);
uint16_t calypso_bsp_rssi_apm_suivante(int *depile_mav);
/* realistic air + AGC: the D_PM written by the DSP (a_serv_demod[1]) brought back to the
 * level of the last burst */
uint16_t calypso_bsp_air_dpm(uint16_t val);
int calypso_apm_trace_on(void);

int calypso_bsp_fenetre_tn(void);
#endif /* HW_ARM_CALYPSO_BSP_H */

4.53 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_c54x.c

434985 octets, 7608 lignes → 7563 lignes (17 groupes compactés)

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * calypso_c54x.c - TMS320C54x core: init, reset, the c54x_run loop, public API.
 *
 * [2026-09-18] Split of a single 21475-line file, by role:
 *   calypso_c54x.c  init / reset / c54x_run loop / public API
 *   c54x_exec.c     c54x_exec_one and the instruction families
 *   c54x_decode.c   operand resolution (Smem/Lmem/Xmem), conditions
 *   c54x_mem.c      data and program memory, overlay, locks
 *   c54x_irq.c      IFR/IMR, frame interrupt, interrupts
 *   c54x_probes.c   probes and traces (diagnostics only)
 */
#include "c54x_internal.h"
#include "hw/arm/calypso/calypso_debug.h"

/* Provided by QEMU (calypso_trx.c, calypso_inth.c) or by the c54x_exe shims
 * (src/main.c). */
extern uint32_t calypso_trx_get_fn(void);
extern void calypso_inth_arm_ack(void);

int c54x_rapide = 0;

/* Switch of the pure probes, see c54x_internal.h. Resolved once; a caller
 * (main.c of c54x_exe, -vvvv) may have set it beforehand. */
int c54x_sondes = -1;
void c54x_sondes_resoudre(void)
{
    if (c54x_sondes >= 0) return;
    const char *d = calypso_getenv("CALYPSO_DEBUG");
    c54x_sondes = calypso_gate("CALYPSO_SONDES", (d && *d) ? 1 : 0);
}

/* ANNEAU-EXEC + dump on SP-CORRUPT (e.g. "SP-CORRUPT pc=0xeac9 op=0xe58b sp
 * 0x5ac0 -> 0x0004", PDROM 0xeac6 STM #0x2ba3,AR2 ; 0xeac8 RPT #16 ; 0xeac9
 * MVDD *AR2+,*AR5+). ALWAYS active, outside the probe switch: the ring costs
 * about ten writes per instruction, the dump only costs on SP-CORRUPT (first 3,
 * excluding the reset artefact at insn=0). One entry per PASS through the loop,
 * hence one per RPT iteration (the RPT redoes the whole loop head without
 * advancing the PC). State BEFORE the instruction, plus SP AFTER and "interrupt
 * taken". The lines contain "SP-CORRUPT": level 0 of verbosite.c, visible
 * without -v. Only writes the ring and stderr: changes nothing in the DSP. */
unsigned g_c54x_it_prises = 0;
typedef struct {
    uint32_t insn;
    uint16_t pc, op, sp, sp_apres, ar2, ar5, rpt_count, st0, ar3, ar4, brc;   /* st0: DP along the path */
    uint8_t  xpc, rpt, intm, it;
    uint16_t it_n;
} C54xAnneauExec;
#define C54X_ANNEAU_N 512   /* deep enough to trace back a whole routine ([imr0] probe) */
static C54xAnneauExec g_anneau_exec[C54X_ANNEAU_N];
static unsigned g_anneau_exec_idx = 0;

/* [imr0] probe (c54x_mem.c): the execution path leading to the IMR write, on stdout, one line
 * per distinct instruction (RPT/loop iterations folded). */
void c54x_anneau_vider_stdout(const char *etiquette, unsigned n_max)
{
    unsigned n = g_anneau_exec_idx < C54X_ANNEAU_N ? g_anneau_exec_idx : C54X_ANNEAU_N;
    if (n > n_max) n = n_max;
    uint16_t pc_prec = 0xffff; unsigned rep = 0;
    for (unsigned k = n; k >= 1; k--) {
        const C54xAnneauExec *e = &g_anneau_exec[(g_anneau_exec_idx - k) % C54X_ANNEAU_N];
        if (e->pc == pc_prec) { rep++; continue; }
        if (rep) printf("  [%s]        (x%u)\n", etiquette, rep + 1);
        rep = 0; pc_prec = e->pc;
        printf("  [%s] -%03u insn=%u xpc=%u pc=0x%04x op=0x%04x sp=%04x ar2=%04x ar3=%04x ar4=%04x ar5=%04x brc=%04x dp=%03x intm=%u it=%u\n",
               etiquette, k, e->insn, e->xpc, e->pc, e->op, e->sp, e->ar2, e->ar3, e->ar4, e->ar5, e->brc,
               e->st0 & 0x1ff, e->intm, e->it);
    }
    if (rep) printf("  [%s]        (x%u)\n", etiquette, rep + 1);
}

static void __attribute__((noinline, cold))
sp_corrupt_vidage(C54xState *s, uint16_t exec_pc, uint16_t sp_avant, int it_courante)
{
    fprintf(stderr, "[c54x] SP-CORRUPT-DUMP insn=%u exec_pc=0x%04x SP 0x%04x -> 0x%04x | "
            "IT prise dans l'instruction courante : %s | IT prises au total=%u, "
            "derniere vec=%d a insn=%llu (fg_pc=0x%04x)\n",
            s->insn_count, exec_pc, sp_avant, s->sp, it_courante ? "OUI" : "non",
            g_c54x_it_prises, g_last_intr_vec,
            (unsigned long long)g_last_intr_insn, g_last_intr_fg_pc);
    fprintf(stderr, "[c54x] SP-CORRUPT-DUMP AR0=%04x AR1=%04x AR2=%04x AR3=%04x AR4=%04x "
            "AR5=%04x AR6=%04x AR7=%04x BK=%04x SP=%04x ST0=%04x ST1=%04x INTM=%d "
            "IMR=%04x IFR=%04x XPC=%u PMST=%04x BRC=%u RSA=%04x REA=%04x rptb=%d "
            "rpt=%d/%u delay=%u\n",
            s->ar[0], s->ar[1], s->ar[2], s->ar[3], s->ar[4], s->ar[5], s->ar[6], s->ar[7],
            s->bk, s->sp, s->st0, s->st1, !!(s->st1 & ST1_INTM), s->imr, s->ifr,
            (unsigned)s->xpc, s->pmst, (unsigned)s->brc, s->rsa, s->rea,
            (int)s->rptb_active, (int)s->rpt_active, (unsigned)s->rpt_count,
            (unsigned)s->delay_slots);
    unsigned n = g_anneau_exec_idx < 64 ? g_anneau_exec_idx : 64;   /* the SP-CORRUPT dump keeps its 64 */
    for (unsigned k = n; k >= 1; k--) {
        const C54xAnneauExec *e = &g_anneau_exec[(g_anneau_exec_idx - k) % C54X_ANNEAU_N];
        fprintf(stderr, "[c54x] SP-CORRUPT-ANNEAU[-%02u] insn=%u pc=0x%04x op=0x%04x "
                "xpc=%u sp=%04x->%04x ar2=%04x ar5=%04x rpt=%u/%u intm=%u it=%u(n=%u)%s\n",
                k, e->insn, e->pc, e->op, e->xpc, e->sp, e->sp_apres, e->ar2, e->ar5,
                e->rpt, e->rpt_count, e->intm, e->it, e->it_n,
                (e->sp_apres != e->sp) ? "  <== SP change ici" : "");
    }
}

/* Fast-forward state kept outside the function: the resolution (getenv,
 * arming message) is in dsp_idle_ff_init(), called once, and
 * dsp_idle_fast_forward() is short enough to be inlined in the loop. */
static int     ff_enabled = -1;
static int     ff_n_ranges = 0;
static uint16_t ff_lo[DSP_IDLE_FF_MAX_RANGES];
static uint16_t ff_hi[DSP_IDLE_FF_MAX_RANGES];
static uint64_t ff_hits = 0;

static void __attribute__((noinline, cold)) dsp_idle_ff_init(void)
{
    {
        const char *e = calypso_getenv("CALYPSO_DSP_IDLE_FF");
        ff_enabled = (!e || *e != '0') ? 1 : 0;
        /* Defaults: two empirically observed dispatcher loops in the
         * stock layer1.highram.elf firmware:
         *   1) 0xe9ac..0xe9b7 — PROM1 mirror, init/SP-aware path
         *   2) 0xcc62..0xcc6f — PROM0 page 0, runtime mailbox poll loop
         * Override via CALYPSO_DSP_IDLE_RANGE="lo1:hi1,lo2:hi2,..."
         * (max 4 ranges). Each range is hex. Empty = use defaults. */
        const char *r = calypso_getenv("CALYPSO_DSP_IDLE_RANGE");
        if (r && *r) {
            const char *p = r;
            while (*p && ff_n_ranges < DSP_IDLE_FF_MAX_RANGES) {
                unsigned lo, hi;
                if (sscanf(p, "%x:%x", &lo, &hi) == 2 && lo <= hi &&
                    lo <= 0xFFFF && hi <= 0xFFFF) {
                    ff_lo[ff_n_ranges] = (uint16_t)lo;
                    ff_hi[ff_n_ranges] = (uint16_t)hi;
                    ff_n_ranges++;
                }
                while (*p && *p != ',') p++;
                if (*p == ',') p++;
            }
        }
        if (ff_n_ranges == 0) {
            ff_lo[0] = 0xe9ac; ff_hi[0] = 0xe9b7;
            ff_lo[1] = 0xcc62; ff_hi[1] = 0xcc6f;
            ff_n_ranges = 2;
        }
        char buf[160] = ""; int blen = 0;
        for (int i = 0; i < ff_n_ranges; i++) {
            blen += snprintf(buf + blen, sizeof(buf) - blen,
                             "%s0x%04x..0x%04x",
                             i ? "," : "", ff_lo[i], ff_hi[i]);
        }
        C54_LOG("DSP IDLE FF: %s, ranges=[%s]",
                ff_enabled ? "enabled" : "disabled", buf);
    }
}

static inline bool dsp_idle_fast_forward(C54xState *s, int *consumed_out)
{
    if (__builtin_expect(ff_enabled < 0, 0)) dsp_idle_ff_init();
    if (!ff_enabled) return false;
    bool in_range = false;
    for (int i = 0; i < ff_n_ranges; i++) {
        if (s->pc >= ff_lo[i] && s->pc <= ff_hi[i]) {
            in_range = true;
            break;
        }
    }
    if (!in_range) return false;

    /* Task slots in both write pages - DSP word addresses:
     *   page 0 : 0x0800 (d_task_d), 0x0802 (d_task_u),
     *            0x0804 (d_task_md), 0x0807 (d_task_ra)
     *   page 1 : 0x0814, 0x0816, 0x0818, 0x081B (offsets +0x14)
     */
    if (s->data[0x0800] | s->data[0x0802] | s->data[0x0804] | s->data[0x0807] |
        s->data[0x0814] | s->data[0x0816] | s->data[0x0818] | s->data[0x081B]) {
        return false;  /* something pending → exec normally */
    }
    /* Pending IRQ would also break us out of the dispatcher next iter. */
    if (!(s->st1 & ST1_INTM) && (s->ifr & s->imr)) {
        return false;
    }

    /* Fast-forward this dispatcher iteration.
     *
     * Cycle budget: a real C54x at 65 MHz gives 1 cycle = 15 ns. The
     * dispatcher body is ~8 instructions per pass (matches the 8 hot PCs
     * observed), so one pass = 8 cycles = 120 ns of DSP time. The caller caps
     * the fast-forward run length per c54x_run invocation so the skips never
     * overshoot the n_insns budget.
     *
     * Wall-clock alignment (CLK IND cadence) belongs to the TDMA timer in
     * calypso_trx.c, not to this function. */
    *consumed_out = 8;
    ff_hits++;
    if ((ff_hits & 0xFFFFFFu) == 0) {
        C54_LOG("DSP IDLE FF: %llu skips so far (PC=0x%04x SP=0x%04x)",
                (unsigned long long)ff_hits, s->pc, s->sp);
    }
    return true;
}

/* SP observability state for the CALYPSO_TRAP_OOR probe. Pure observation,
 * no PC whitelist: the SP clobber lives in legitimate code.
 *   - g_sp_trail[256] : SP changes with |delta| > 32 (scheduler reloads, large
 *     allocations); push/pop +-1 noise is skipped.
 *   - sp_low watermark : every new low, coalesced per PC on powers of ten.
 *     Catches both absolute reloads and push-drain runaway.
 *   - Each event records A_low, the candidate STL A,Smem source.
 *   - Halt at a fixed checkpoint (CALYPSO_TRAP_CHECKPOINT, default 4200000). */

struct c54x_g_sp_trail_s g_sp_trail[256];
unsigned g_sp_trail_idx = 0;

/* sp_low watermark, coalesced per PC */
uint16_t g_sp_low = 0xFFFF;
uint16_t g_sp_low_pc = 0xFFFF;
unsigned g_sp_low_hits_at_pc = 0;
unsigned g_sp_low_distinct_pcs = 0;

/* Per-PC SP-decrement histogram.
 *
 * Gated on the SP VALUE, not on insn_count. insn_count is inflated by the
 * idle fast-forward (which credits cycles without executing opcodes) and
 * jittered by external I/O (bridge/osmocon/BTS over UDP+PTY move the guest
 * instant at which a burst arrives), so an insn window is not reproducible
 * run to run.
 *
 * The histogram arms when SP falls below the plateau (default < 0x2000) and
 * stays armed until SP < 0x0100 (near underflow); inside that window every
 * SP decrement is counted per PC. The window auto-aligns on the descent
 * whatever the fast-forward and the external jitter do, and it excludes the
 * balanced PSHM/POP churn of the plateau, so a real leaker dominates
 * mechanically.
 *
 * Env overrides:
 *   CALYPSO_SP_HIST_ARM   (default 0x2000) - arm threshold
 *   CALYPSO_SP_HIST_DUMP  (default 0x0100) - dump threshold
 *
 * Covers every SP-write path (direct s->sp--, MMR_SP through the data_write
 * callback, IRQ push): they all go through the s->sp variable. */
SpDecEntry g_sp_dec_hist[SP_HIST_MAX];
unsigned   g_sp_dec_used = 0;
unsigned   g_sp_dec_total_events = 0;
uint16_t   g_sp_dec_arm_threshold = 0;
uint16_t   g_sp_dec_dump_threshold = 0;
int        g_sp_dec_enabled = -1;
int        g_sp_dec_armed = 0;
int        g_sp_dec_dumped = 0;
unsigned   g_sp_dec_arm_insn = 0;   /* insn at which we armed */;
uint16_t   g_sp_dec_arm_sp = 0;     /* SP value at arm */;

/* Raw SP ring buffer: per-iteration (insn, PC, SP, op) recorded at the top of
 * the run loop, unfiltered and unclassified. Two trigger modes.
 *
 * "bootstub" fires on the edge prev_pc outside [0x00,0x7F] -> s->pc inside it,
 * which captures the offending RET together with its SP and the popped word
 * mem[topgate_last_sp]. That discriminates two unrelated bugs:
 *   - valid SP (~0x3fbb) with mem[SP] == 0 : the return slot was overwritten
 *     by a stray write; fd28-fd2a is not involved.
 *   - SP outside the stack (~0x2bc0) : the 0xfd2a A=AR4 family, fixed in
 *     fd28-fd2a.
 * "floor" fires on the SP floor crossing, 600k insns later, already inside the
 * boot-stub spiral where pops wrap SP forward - too late to name the culprit.
 *
 * Env gates:
 *   CALYPSO_SP_RING=1          enable (default OFF, zero cost otherwise)
 *   CALYPSO_SP_RING_MAX=N      dumps per run (default 4)
 *   CALYPSO_SP_RING_TRIG=mode  floor|bootstub|both (default bootstub)
 *   CALYPSO_SP_RING_INSN_MIN=N skip the first N insns (default 1000000: the
 *                              firmware makes legitimate CALLs into the boot
 *                              stub 0x0000/0x0001 during init, and the first
 *                              one would burn the one-shot on a false
 *                              positive) */
SpRingEntry g_sp_ring[SP_RING_SZ];
unsigned    g_sp_ring_head = 0;
uint64_t    g_sp_ring_total = 0;
int         g_sp_ring_enabled = -1;
unsigned    g_sp_ring_dump_count = 0;
unsigned    g_sp_ring_dump_max = 0;
/* Trigger mode: 1 = floor-cross, 2 = bootstub-entry, 3 = both */
int         g_sp_ring_trig_mode = 0;
unsigned    g_sp_ring_insn_min  = 0;  /* skip first N insns (boot phase) */;

static void sp_ring_record(unsigned insn, uint16_t pc, uint16_t sp, uint16_t op)
{
    if (g_sp_ring_enabled <= 0) return;
    SpRingEntry *e = &g_sp_ring[g_sp_ring_head];
    e->insn = insn; e->pc = pc; e->sp = sp; e->op = op;
    g_sp_ring_head = (g_sp_ring_head + 1) & (SP_RING_SZ - 1);
    g_sp_ring_total++;
}

static void sp_ring_dump(const char *trig, unsigned insn_now, uint16_t sp_now)
{
    if (g_sp_ring_enabled <= 0) return;
    if (g_sp_ring_dump_max && g_sp_ring_dump_count >= g_sp_ring_dump_max) return;
    g_sp_ring_dump_count++;
    fprintf(stderr,
        "[c54x] SP-RING DUMP[%s] @insn=%u sp_now=0x%04x total_recorded=%llu "
        "dump#%u\n",
        trig, insn_now, sp_now,
        (unsigned long long)g_sp_ring_total, g_sp_ring_dump_count);
    unsigned n = (g_sp_ring_total < SP_RING_SZ)
                 ? (unsigned)g_sp_ring_total : SP_RING_SZ;
    unsigned start = (g_sp_ring_total < SP_RING_SZ) ? 0 : g_sp_ring_head;
    for (unsigned k = 0; k < n; k++) {
        unsigned idx = (start + k) & (SP_RING_SZ - 1);
        SpRingEntry *e = &g_sp_ring[idx];
        fprintf(stderr,
            "[c54x] SP-RING[%u] insn=%u PC=0x%04x SP=0x%04x op=0x%04x\n",
            k, e->insn, e->pc, e->sp, e->op);
    }
}

static void sp_ring_init_lazy(void)
{
    if (g_sp_ring_enabled >= 0) return;
    const char *e = cdbg_env("SP-RING");
    g_sp_ring_enabled = (e && *e == '1') ? 1 : 0;
    const char *m = calypso_getenv("CALYPSO_SP_RING_MAX");
    g_sp_ring_dump_max = (m && *m) ? (unsigned)strtoul(m, NULL, 0) : 4u;
    /* Default trigger is bootstub; floor-cross fires inside the spiral, too
     * late to name the offending RET. */
    const char *t = calypso_getenv("CALYPSO_SP_RING_TRIG");
    if (!t || !*t || !strcmp(t, "bootstub")) g_sp_ring_trig_mode = 2;
    else if (!strcmp(t, "floor"))            g_sp_ring_trig_mode = 1;
    else if (!strcmp(t, "both"))             g_sp_ring_trig_mode = 3;
    else                                     g_sp_ring_trig_mode = 2;
    const char *im = calypso_getenv("CALYPSO_SP_RING_INSN_MIN");
    g_sp_ring_insn_min = (im && *im) ? (unsigned)strtoul(im, NULL, 0) : 1000000u;
    if (g_sp_ring_enabled) {
        fprintf(stderr,
            "[c54x] SP-RING enabled, sz=%u, max_dumps=%u, trig=%s, "
            "insn_min=%u\n",
            SP_RING_SZ, g_sp_ring_dump_max,
            g_sp_ring_trig_mode == 1 ? "floor" :
            g_sp_ring_trig_mode == 2 ? "bootstub" :
            g_sp_ring_trig_mode == 3 ? "both" : "?",
            g_sp_ring_insn_min);
    }
}

/* Detect the edge where PC enters the boot-stub area [0x0000, 0x007F].
 * topgate_last_pc = PC of the insn just executed (the RET that branched)
 * cur_pc          = destination = popped return address
 * topgate_last_sp = SP before the RET pop
 * cur_sp          = SP after it (= topgate_last_sp + 1 for a 1-word return)
 * Dumps the verbose state plus the ring, to identify the corrupting RET.
 * Capped at one detailed dump per run: the first one holds the caller, later
 * fires are re-entries of the same spiral. */
int g_bootstub_dumped = 0;
static void sp_ring_check_bootstub_entry(C54xState *s,
                                         uint16_t prev_pc, uint16_t prev_op,
                                         uint16_t prev_sp, uint16_t cur_pc,
                                         uint16_t cur_sp, unsigned insn)
{
    if (g_sp_ring_enabled <= 0) return;
    if (!(g_sp_ring_trig_mode & 2)) return;
    if (g_bootstub_dumped) return;
    /* Skip the boot phase: the firmware makes legitimate CALLs into the boot
     * stub 0x0000-0x0001 during init (LDMM SP,B is documented). Without this
     * gate the first trigger fires at insn=145 and burns the one-shot. */
    if (insn < g_sp_ring_insn_min) return;
    int was_inside = (prev_pc <= 0x007F);
    int now_inside = (cur_pc  <= 0x007F);
    if (was_inside || !now_inside) return;

    g_bootstub_dumped = 1;
    uint16_t popped = s->data[prev_sp & 0xFFFF];
    uint16_t neighborhood[8];
    for (int k = 0; k < 8; k++) {
        neighborhood[k] = s->data[(uint16_t)(prev_sp + k) & 0xFFFF];
    }
    fprintf(stderr,
        "[c54x] BOOTSTUB-ENTRY caught @insn=%u\n"
        "[c54x]   prev_pc=0x%04x prev_op=0x%04x  (the RET site = corrupter)\n"
        "[c54x]   cur_pc=0x%04x  (destination, in bootstub)\n"
        "[c54x]   prev_sp=0x%04x  cur_sp=0x%04x  (delta=%+d)\n"
        "[c54x]   mem[prev_sp]=0x%04x  (= popped return addr, must equal cur_pc)\n"
        "[c54x]   AR0..7: %04x %04x %04x %04x %04x %04x %04x %04x  ARP=%d DP=%d\n"
        "[c54x]   ST0=0x%04x ST1=0x%04x INTM=%d XPC=%d\n"
        "[c54x]   stack neighborhood mem[prev_sp..+7]: %04x %04x %04x %04x %04x %04x %04x %04x\n",
        insn, prev_pc, prev_op, cur_pc,
        prev_sp, cur_sp, (int)cur_sp - (int)prev_sp,
        popped,
        s->ar[0], s->ar[1], s->ar[2], s->ar[3],
        s->ar[4], s->ar[5], s->ar[6], s->ar[7],
        arp(s), dp(s),
        s->st0, s->st1, !!(s->st1 & ST1_INTM), s->xpc,
        neighborhood[0], neighborhood[1], neighborhood[2], neighborhood[3],
        neighborhood[4], neighborhood[5], neighborhood[6], neighborhood[7]);

    /* Discriminator:
     *   valid SP (~0x3fbb observed) with popped == 0 -> the return slot was
     *     overwritten by a stray write; 0xfd2a is innocent.
     *   SP in a non-stack buffer (~0x2bc0 or similar) -> the 0xfd2a A=AR4
     *     family; fd28-fd2a is the fix. */
    int sp_in_valid_stack = (prev_sp >= 0x3000 && prev_sp <= 0x5FFF);
    int sp_in_buffer_area = (prev_sp >= 0x2000 && prev_sp <= 0x2FFF);
    if (calypso_debug_enabled("BOOTSTUB-ENTRY")) fprintf(stderr,
        "[c54x] BOOTSTUB-ENTRY VERDICT: sp_in_valid_stack=%d "
        "sp_in_buffer_area=%d popped_is_zero=%d\n"
        "[c54x]   → %s\n",
        sp_in_valid_stack, sp_in_buffer_area, (popped == 0),
        sp_in_valid_stack && popped == 0
            ? "RETURN SLOT OVERWRITTEN (sauvage write near SP), audit ailleurs"
            : sp_in_buffer_area
            ? "SP IN NON-STACK BUFFER (likely 0xfd2a family), audit fd28-fd2a"
            : "INCONCLUSIVE — inspect ring + state above");

    sp_ring_dump("bootstub-entry", insn, cur_sp);
}

static void sp_hist_dump(const char *trig, unsigned insn_now, uint16_t sp_now)
{
    if (calypso_debug_enabled("SP-HIST")) fprintf(stderr,
        "[c54x] SP-HIST DUMP[%s] arm@(insn=%u,sp=0x%04x) now@(insn=%u,sp=0x%04x) "
        "events=%u distinct_pcs=%u\n",
        trig, g_sp_dec_arm_insn, g_sp_dec_arm_sp, insn_now, sp_now,
        g_sp_dec_total_events, g_sp_dec_used);

    /* Top-K by dec_count (trickle leak). */
    if (calypso_debug_enabled("SP-HIST")) fprintf(stderr, "[c54x] SP-HIST TOP BY COUNT (corrupteur trickle):\n");
    for (unsigned k = 0; k < 20 && k < g_sp_dec_used; k++) {
        unsigned best = k;
        for (unsigned i = k + 1; i < g_sp_dec_used; i++) {
            if (g_sp_dec_hist[i].dec_count > g_sp_dec_hist[best].dec_count)
                best = i;
        }
        if (best != k) {
            SpDecEntry tmp = g_sp_dec_hist[k];
            g_sp_dec_hist[k] = g_sp_dec_hist[best];
            g_sp_dec_hist[best] = tmp;
        }
        if (calypso_debug_enabled("SP-HIST")) fprintf(stderr,
            "[c54x] SP-HIST #%u pc=0x%04x op_last=0x%04x dec_count=%u "
            "delta_sum=%d\n",
            k + 1, g_sp_dec_hist[k].pc, g_sp_dec_hist[k].op_last,
            g_sp_dec_hist[k].dec_count, g_sp_dec_hist[k].delta_sum);
    }

    /* Top-K by |delta_sum| (single-event corrupter: one huge event). */
    if (calypso_debug_enabled("SP-HIST")) fprintf(stderr, "[c54x] SP-HIST TOP BY |delta_sum| (corrupteur single-jump):\n");
    for (unsigned k = 0; k < 10 && k < g_sp_dec_used; k++) {
        unsigned best = k;
        int32_t best_abs = g_sp_dec_hist[k].delta_sum < 0
                           ? -g_sp_dec_hist[k].delta_sum
                           :  g_sp_dec_hist[k].delta_sum;
        for (unsigned i = k + 1; i < g_sp_dec_used; i++) {
            int32_t a = g_sp_dec_hist[i].delta_sum < 0
                        ? -g_sp_dec_hist[i].delta_sum
                        :  g_sp_dec_hist[i].delta_sum;
            if (a > best_abs) { best = i; best_abs = a; }
        }
        if (best != k) {
            SpDecEntry tmp = g_sp_dec_hist[k];
            g_sp_dec_hist[k] = g_sp_dec_hist[best];
            g_sp_dec_hist[best] = tmp;
        }
        if (calypso_debug_enabled("SP-HIST")) fprintf(stderr,
            "[c54x] SP-HIST D#%u pc=0x%04x op_last=0x%04x dec_count=%u "
            "delta_sum=%d\n",
            k + 1, g_sp_dec_hist[k].pc, g_sp_dec_hist[k].op_last,
            g_sp_dec_hist[k].dec_count, g_sp_dec_hist[k].delta_sum);
    }
    g_sp_dec_dumped = 1;
}

static void sp_hist_account(uint16_t exec_pc, uint16_t exec_op,
                            uint16_t sp_before, uint16_t sp_now,
                            unsigned insn)
{
    if (g_sp_dec_enabled < 0) {
        const char *e_arm = calypso_getenv("CALYPSO_SP_HIST_ARM");
        const char *e_dump = calypso_getenv("CALYPSO_SP_HIST_DUMP");
        unsigned arm  = (e_arm  && *e_arm)  ? (unsigned)strtoul(e_arm,  NULL, 0) : 0x2000u;
        unsigned dump = (e_dump && *e_dump) ? (unsigned)strtoul(e_dump, NULL, 0) : 0x0100u;
        if (arm > 0xFFFF) arm = 0xFFFF;
        if (dump > 0xFFFF) dump = 0xFFFF;
        if (dump >= arm) dump = (arm > 0x100) ? (arm - 0x100) : 0;
        g_sp_dec_arm_threshold  = (uint16_t)arm;
        g_sp_dec_dump_threshold = (uint16_t)dump;
        g_sp_dec_enabled = calypso_debug_enabled("SP-HIST") ? 1 : 0;
        fprintf(stderr,
            "[c54x] SP-HIST gating SP-value : ARM<0x%04x DUMP<0x%04x\n",
            g_sp_dec_arm_threshold, g_sp_dec_dump_threshold);
    }
    if (!g_sp_dec_enabled) return;
    /* No one-shot freeze here: returning early once dumped would drop every
     * event after the first dump. Repeat dumps while SP stays under the
     * threshold are bounded by sp_ring_dump_max and by the top-of-loop edge
     * detection. */

    /* No (int16_t) cast on the delta: the signed wrap misclassified high->low
     * falls as pops. Plain int32 subtraction instead:
     *   0x9006 -> 0x0000 : delta = -36870 (descent captured)
     *   0xC000 -> 0x0000 : delta = -49152 (descent captured)
     * This does break the underflow wrap (0x2bc0 -> 0xfff8 = +52280, seen as
     * a pop), but the ring buffer, not this histogram, decides the kill; the
     * histogram only tracks trickle drift. */
    if (!g_sp_dec_armed) {
        int32_t first_check = (int32_t)sp_now - (int32_t)sp_before;
        if (first_check < 0) {
            g_sp_dec_armed = 1;
            g_sp_dec_arm_insn = insn;
            g_sp_dec_arm_sp = sp_now;
            fprintf(stderr,
                "[c54x] SP-HIST ARMED @insn=%u SP=0x%04x (sp_before=0x%04x delta=%d) "
                "pc=0x%04x op=0x%04x\n",
                insn, sp_now, sp_before, first_check, exec_pc, exec_op);
        } else {
            return;  /* no negative delta yet — wait */
        }
    }

    /* Record the event BEFORE the dump check, otherwise a single-event jump
     * that crosses the dump threshold in one instruction is lost. */
    int32_t delta = (int32_t)sp_now - (int32_t)sp_before;
    if (delta < 0) {
        g_sp_dec_total_events++;
        unsigned i;
        for (i = 0; i < g_sp_dec_used; i++) {
            if (g_sp_dec_hist[i].pc == exec_pc) break;
        }
        if (i == g_sp_dec_used) {
            if (g_sp_dec_used >= SP_HIST_MAX) {
                static int sat_log = 0;
                if (!sat_log) {
                    fprintf(stderr,
                        "[c54x] SP-HIST saturated (>%u distinct PCs) — "
                        "broaden if needed\n", SP_HIST_MAX);
                    sat_log = 1;
                }
            } else {
                g_sp_dec_hist[i].pc = exec_pc;
                g_sp_dec_hist[i].op_last = exec_op;
                g_sp_dec_hist[i].dec_count = 0;
                g_sp_dec_hist[i].delta_sum = 0;
                g_sp_dec_used++;
            }
        }
        if (i < g_sp_dec_used) {
            g_sp_dec_hist[i].op_last = exec_op;
            g_sp_dec_hist[i].dec_count++;
            g_sp_dec_hist[i].delta_sum += delta;
        }
        /* Log the first 10 events verbatim: for a single-event jump the
         * corrupter is among them (often the only entry in the histogram). */
        if (g_sp_dec_total_events <= 10) {
            if (calypso_debug_enabled("SP-HIST")) fprintf(stderr,
                "[c54x] SP-HIST EVENT #%u pc=0x%04x op=0x%04x "
                "sp_before=0x%04x sp_now=0x%04x delta=%d insn=%u\n",
                g_sp_dec_total_events, exec_pc, exec_op,
                sp_before, sp_now, delta, insn);
        }
    }

    /* Dump AFTER the accounting. Edge-triggered only: fire when SP crosses
     * below the floor (sp_before >= threshold && sp_now < threshold). Gated by
     * bit 0 of g_sp_ring_trig_mode; bootstub is the default because
     * floor-cross fires inside the spiral, too late. */
    if ((g_sp_ring_trig_mode & 1) &&
        sp_before >= g_sp_dec_dump_threshold &&
        sp_now    <  g_sp_dec_dump_threshold) {
        sp_ring_dump("sp-floor-cross", insn, sp_now);
        sp_hist_dump("sp-floor-cross", insn, sp_now);
    }
}

static void dsp_trap_dump(C54xState *s, uint16_t exec_pc, uint16_t exec_op,
                          uint16_t sp_before, const char *trig)
{
    if (calypso_debug_enabled("TRAP")) fprintf(stderr,
        "[c54x] TRAP[%s] insn=%u exec_pc=0x%04x exec_op=0x%04x "
        "next_pc=0x%04x sp_before=0x%04x sp_now=0x%04x INTM=%d\n",
        trig, s->insn_count, exec_pc, exec_op, s->pc,
        sp_before, s->sp, !!(s->st1 & ST1_INTM));
    if (calypso_debug_enabled("TRAP")) fprintf(stderr, "[c54x] TRAP pc_ring[-16..-1]:");
    for (int i = 16; i >= 1; i--)
        fprintf(stderr, " %04x", pc_ring[(pc_ring_idx - i) & 255]);
    fprintf(stderr, "\n[c54x] TRAP sp_low=0x%04x at last_pc=0x%04x hits_at_pc=%u distinct_pcs=%u\n",
            g_sp_low, g_sp_low_pc, g_sp_low_hits_at_pc, g_sp_low_distinct_pcs);
    /* SP-HIST dump (SP-windowed, independent of insn_count). */
    if (g_sp_dec_used > 0 && !g_sp_dec_dumped)
        sp_hist_dump("trap", s->insn_count, s->sp);
    fprintf(stderr, "[c54x] TRAP sp_trail[-256..-1] (|Δ|>32 only; insn old->new @pc op A_low):\n");
    for (int i = 256; i >= 1; i--) {
        unsigned k = (g_sp_trail_idx - i) & 255;
        if (g_sp_trail[k].insn == 0 && g_sp_trail[k].exec_pc == 0) continue;
        fprintf(stderr, "  %u  %04x->%04x  pc=%04x op=%04x A_low=%04x\n",
                g_sp_trail[k].insn, g_sp_trail[k].old_sp,
                g_sp_trail[k].new_sp, g_sp_trail[k].exec_pc,
                g_sp_trail[k].exec_op, g_sp_trail[k].a_low);
    }
    fprintf(stderr,
        "[c54x] TRAP regs A=%010llx B=%010llx T=%04x  "
        "AR0..7: %04x %04x %04x %04x %04x %04x %04x %04x  "
        "BK=%04x ARP=%d DP=%d  ST0=%04x ST1=%04x PMST=%04x  "
        "RSA=%04x REA=%04x BRC=%d  IFR=%04x IMR=%04x XPC=%d\n",
        (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
        (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
        s->t,
        s->ar[0], s->ar[1], s->ar[2], s->ar[3],
        s->ar[4], s->ar[5], s->ar[6], s->ar[7],
        s->bk, (s->st0 >> 13) & 7, s->st0 & 0x1FF,
        s->st0, s->st1, s->pmst,
        s->rsa, s->rea, s->brc, s->ifr, s->imr, s->xpc);
    fprintf(stderr,
        "[c54x] TRAP prog[exec_pc..+3]=%04x %04x %04x %04x  "
        "prog[next_pc..+3]=%04x %04x %04x %04x\n",
        s->prog[exec_pc], s->prog[(uint16_t)(exec_pc+1)],
        s->prog[(uint16_t)(exec_pc+2)], s->prog[(uint16_t)(exec_pc+3)],
        s->prog[s->pc], s->prog[(uint16_t)(s->pc+1)],
        s->prog[(uint16_t)(s->pc+2)], s->prog[(uint16_t)(s->pc+3)]);
}

int c54x_run(C54xState *s, int n_insns)
{
    int executed = 0;

    /* Run counter, used by the BOOT trace further down. */
    static int run_num = 0;
    run_num++;

    /* Head probes (one pass per call, but the bench and the replay call
     * c54x_run in slices of 1 to 64 instructions): all pure, behind the
     * switch. */
    if (__builtin_expect(c54x_sondes < 0, 0)) c54x_sondes_resoudre();
    if (C54X_SONDES) {
    /* SP history ring buffer (64 entries × insn/PC/SP). Sampled every
     * 1M insns at top of run-loop. Dumped on STATE-DUMP. Reveals whether
     * SP descends monotonically (cumulative leak — each ISR entry leaks
     * one stack frame) or oscillates around a value (one big initial
     * drop then steady-state). Different fixes. */
    static struct { unsigned insn; uint16_t pc; uint16_t sp; } sp_ring[64];
    static unsigned sp_ring_idx = 0;
    static unsigned next_sp_sample = 1000000u;
    if (s->insn_count >= next_sp_sample) {
        next_sp_sample += 1000000u;
        sp_ring[sp_ring_idx & 63].insn = s->insn_count;
        sp_ring[sp_ring_idx & 63].pc   = s->pc;
        sp_ring[sp_ring_idx & 63].sp   = s->sp;
        sp_ring_idx++;
    }

    /* XPC tracking probe. Tells whether the CCCH demod path ever reaches
     * PROM1 (XPC=1) through the B 0x9ab1 at 0x19aac, and where it lands:
     *   - insn count per XPC (0..3)
     *   - last PC visited per XPC
     *   - first-visit insn per XPC
     *   - ring of the last 16 PCs visited under XPC=1
     */
    {
        static uint64_t xpc_insn_count[4] = {0};
        static uint16_t xpc_last_pc[4]    = {0};
        static uint64_t xpc_first_insn[4] = {0,0,0,0};
        static uint16_t xpc1_pc_ring[16];
        static unsigned xpc1_pc_ring_idx = 0;
        static unsigned xpc1_pc_ring_count = 0;
        static unsigned next_xpc_dump = 100000000u;  /* 100M */
        uint8_t cur_xpc = s->xpc & 0x3;
        xpc_insn_count[cur_xpc]++;
        xpc_last_pc[cur_xpc] = s->pc;
        if (xpc_first_insn[cur_xpc] == 0)
            xpc_first_insn[cur_xpc] = s->insn_count;
        if (cur_xpc == 1) {
            xpc1_pc_ring[xpc1_pc_ring_idx & 15] = s->pc;
            xpc1_pc_ring_idx++;
            xpc1_pc_ring_count++;
        }
        if (s->insn_count >= next_xpc_dump) {
            next_xpc_dump += 100000000u;
            if (calypso_debug_enabled("XPC-STATS")) fprintf(stderr,
                    "[c54x] XPC-STATS insn=%u counts: 0=%llu 1=%llu 2=%llu 3=%llu | "
                    "first_insn: 0=%llu 1=%llu 2=%llu 3=%llu | last_pc: 0=0x%04x 1=0x%04x 2=0x%04x 3=0x%04x\n",
                    s->insn_count,
                    (unsigned long long)xpc_insn_count[⟨1⟩],  ×4
    ⟨⟩ = (0) (1) (2) (3)
                    (unsigned long long)xpc_first_insn[⟨1⟩],  ×4
    ⟨⟩ = (0) (1) (2) (3)
                    xpc_last_pc[0], xpc_last_pc[1], xpc_last_pc[2], xpc_last_pc[3]);
            if (xpc1_pc_ring_count > 0) {
                /* Last 16 PCs visited under XPC=1 */
                if (calypso_debug_enabled("XPC1-PC-RING")) fprintf(stderr,
                        "[c54x] XPC1-PC-RING count=%u last16: "
                        "%04x %04x %04x %04x %04x %04x %04x %04x "
                        "%04x %04x %04x %04x %04x %04x %04x %04x\n",
                        xpc1_pc_ring_count,
                        xpc1_pc_ring[(xpc1_pc_ring_idx-⟨1⟩)&15],  ×15
    ⟨⟩ = (16) (15) (14) (13) (12) (11) (10) (9) (8) (7) (6) (5) (4) (3) (2)
                        xpc1_pc_ring[(xpc1_pc_ring_idx-1)&15]);
            }
        }
    }

    /* DISPATCH-CALLER probe. The three callers of 0x9aaf found by a PROM
     * scan:
     *   PC=0x8815 : f074 9aaf  (B 0x9aaf from the table at 0x8810)
     *   PC=0x9296 : f274 9aaf  (BD 0x9aaf)
     *   PC=0x9418 : f274 9aaf  (BD 0x9aaf)
     * Logs A, AR0..2 and data[0x0828/9] on each hit. */
    if (s->pc == 0x8815 || s->pc == 0x9296 || s->pc == 0x9418) {
        static unsigned hit_counts[3] = {0, 0, 0};
        int idx = (s->pc == 0x8815) ? 0 : (s->pc == 0x9296) ? 1 : 2;
        hit_counts[idx]++;
        if (hit_counts[idx] <= 20 || hit_counts[idx] % 100 == 0) {
            fprintf(stderr,
                    "[c54x] DISPATCH-CALLER hit=%u pc=0x%04x "
                    "A=0x%010llx AR0=0x%04x AR1=0x%04x AR2=0x%04x "
                    "data[0x0828]=0x%04x data[0x0829]=0x%04x "
                    "data[0x083c]=0x%04x data[0x083d]=0x%04x insn=%u\n",
                    hit_counts[idx], s->pc,
                    (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                    s->ar[0], s->ar[1], s->ar[2],
                    s->data[0x0828], s->data[0x0829],
                    s->data[0x083c], s->data[0x083d],
                    s->insn_count);
        }
    }

    /* AR7-INIT-CHAIN / MVMD-AR7-BRC / RPTB-ARMED probe: value of AR7 at the
     * MVMD AR7,BRC in PC=0x8208, the last 16 writes that produced it, and the
     * BRC state after the RPTBD setup. */
    {
        static uint16_t prev_ar7 = 0xFFFF;
        static struct {
            uint16_t pc;
            uint16_t old_val;
            uint16_t new_val;
            uint64_t insn;
            uint8_t  xpc;
        } ar7_history[16] = {{0}};
        static unsigned ar7_hist_idx = 0;

        if (s->ar[7] != prev_ar7) {
            ar7_history[ar7_hist_idx & 15].pc = s->pc;
            ar7_history[ar7_hist_idx & 15].old_val = prev_ar7;
            ar7_history[ar7_hist_idx & 15].new_val = s->ar[7];
            ar7_history[ar7_hist_idx & 15].insn = s->insn_count;
            ar7_history[ar7_hist_idx & 15].xpc = s->xpc & 0x3;
            ar7_hist_idx++;
            prev_ar7 = s->ar[7];
        }

        /* (b) Full snapshot on each hit of PC=0x8208 (MVMD AR7, BRC) */
        if (s->pc == 0x8208) {
            static unsigned mvmd_hits = 0;
            mvmd_hits++;
            if (mvmd_hits <= 20 || (mvmd_hits % 100) == 0) {
                if (calypso_debug_enabled("MVMD-AR7-BRC")) fprintf(stderr,
                        "[c54x] MVMD-AR7-BRC #%u AR7=0x%04x BRC_before=0x%04x "
                        "AR0=0x%04x AR1=0x%04x AR2=0x%04x AR6=0x%04x DP=%d insn=%u\n",
                        mvmd_hits, s->ar[7], s->brc,
                        s->ar[0], s->ar[1], s->ar[2], s->ar[6], dp(s),
                        s->insn_count);
                int n_hist = ar7_hist_idx < 16 ? ar7_hist_idx : 16;
                for (int i = 0; i < n_hist; i++) {
                    int slot = (ar7_hist_idx - n_hist + i) & 15;
                    if (calypso_debug_enabled("AR7-HIST")) fprintf(stderr,
                            "[c54x]   AR7-HIST[%d] pc=XPC%u:0x%04x 0x%04x->0x%04x insn=%llu\n",
                            i, ar7_history[slot].xpc, ar7_history[slot].pc,
                            ar7_history[slot].old_val, ar7_history[slot].new_val,
                            (unsigned long long)ar7_history[slot].insn);
                }
            }
        }

        /* (c) RPTB state after setup (PC=0x820c = delay slot after RPTBD) */
        if (s->pc == 0x820c) {
            static unsigned rptb_hits = 0;
            rptb_hits++;
            if (rptb_hits <= 20 || (rptb_hits % 100) == 0) {
                if (calypso_debug_enabled("RPTB-ARMED")) fprintf(stderr,
                        "[c54x] RPTB-ARMED #%u BRC=0x%04x RSA=0x%04x REA=0x%04x "
                        "ST1=0x%04x (INTM=%d) insn=%u\n",
                        rptb_hits, s->brc, s->rsa, s->rea, s->st1,
                        (s->st1 >> 11) & 1, s->insn_count);
            }
        }
    }

    /* INT3-BLOCKED probe. Samples 1/1000 of the context (PC/ST1/BRC/XPC)
     * while INT3 is pending and INTM=1. Discriminates (a) an opcode that sets
     * INTM without clearing it (POPM variant), (b) a long non-interruptible
     * RPTB (BRC > 0 throughout), (c) a raw STM ST1 / MVDM ST1. */
    {
        static uint64_t blocked_count = 0;
        static uint16_t sample_pcs[32] = {0};
        static uint16_t sample_st1s[32] = {0};
        static uint16_t sample_brcs[32] = {0};
        static uint8_t  sample_xpcs[32] = {0};
        static unsigned sample_idx = 0;
        static unsigned next_blocked_dump = 100000000u;

        bool int3_pending_now = (s->ifr & 0x08) != 0;
        bool intm_set_now = ((s->st1 >> 11) & 1) != 0;

        if (int3_pending_now && intm_set_now) {
            blocked_count++;
            if ((blocked_count % 1000) == 0) {
                sample_pcs[sample_idx & 31] = s->pc;
                sample_st1s[sample_idx & 31] = s->st1;
                sample_brcs[sample_idx & 31] = s->brc;
                sample_xpcs[sample_idx & 31] = s->xpc & 0x3;
                sample_idx++;
            }
        }

        if (s->insn_count >= next_blocked_dump) {
            next_blocked_dump += 100000000u;
            if (calypso_debug_enabled("INT3-BLOCKED")) fprintf(stderr,
                    "[c54x] INT3-BLOCKED insn=%u blocked_total=%llu blocked_samples=%u\n",
                    s->insn_count,
                    (unsigned long long)blocked_count,
                    sample_idx);
            int n = sample_idx < 32 ? sample_idx : 32;
            for (int i = 0; i < n; i++) {
                int slot = (sample_idx - n + i) & 31;
                if (calypso_debug_enabled("INT3-BLOCKED-SAMPLE")) fprintf(stderr,
                        "[c54x] INT3-BLOCKED-SAMPLE pc=XPC%u:0x%04x st1=0x%04x brc=0x%04x\n",
                        sample_xpcs[slot], sample_pcs[slot],
                        sample_st1s[slot], sample_brcs[slot]);
            }
        }
    }

    /* IRQ-FRAME-HEALTH probe. INT3 is the frame interrupt (IMR bit 3,
     * vector 19, address 0xFFCC); this counts fire/serviced/missed and the
     * service latency. Discriminates a mis-vectored ISR (serviced < fire), a
     * dead TPU/TSP source (fire == 0) and compute that is too slow
     * (missed > 0). */
    {
        static uint64_t int3_fire_count = 0;
        static uint64_t int3_serviced_count = 0;
        static uint64_t int3_missed_count = 0;
        static uint64_t last_int3_fire_insn = 0;
        static uint64_t last_int3_service_insn = 0;
        static uint64_t total_service_latency_insn = 0;
        static bool int3_pending_prev = false;
        static unsigned next_irq_dump = 200000000u;

        bool int3_now_pending = (s->ifr & 0x08) != 0;
        bool int3_just_fired = int3_now_pending && !int3_pending_prev;
        /* ISR enter approximation : INT3 cleared from IFR while INTM=0 */
        bool int3_just_serviced = !int3_now_pending && int3_pending_prev &&
                                  ((s->st1 >> 11) & 1) == 0;

        if (int3_just_fired) {
            int3_fire_count++;
            if (int3_pending_prev) {
                int3_missed_count++;
            }
            last_int3_fire_insn = s->insn_count;
        }
        if (int3_just_serviced) {
            int3_serviced_count++;
            if (last_int3_fire_insn > last_int3_service_insn) {
                total_service_latency_insn += (s->insn_count - last_int3_fire_insn);
            }
            last_int3_service_insn = s->insn_count;
        }
        int3_pending_prev = int3_now_pending;

        if (s->insn_count >= next_irq_dump) {
            next_irq_dump += 200000000u;
            uint64_t avg_latency = int3_serviced_count > 0
                ? total_service_latency_insn / int3_serviced_count : 0;
            double service_ratio = int3_fire_count > 0
                ? (double)int3_serviced_count / int3_fire_count : 0.0;
            if (calypso_debug_enabled("IRQ-FRAME-HEALTH")) fprintf(stderr,
                    "[c54x] IRQ-FRAME-HEALTH insn=%u int3_fire=%llu int3_serviced=%llu "
                    "int3_missed=%llu avg_latency_insn=%llu service_ratio=%.2f\n",
                    s->insn_count,
                    (unsigned long long)int3_fire_count,
                    (unsigned long long)int3_serviced_count,
                    (unsigned long long)int3_missed_count,
                    (unsigned long long)avg_latency,
                    service_ratio);
        }
    }

    /* EXIT-COMPUTE / IRQ-DURING-COMPUTE probe. The DSP spins under XPC=2 in
     * the hot zone 0xdf80..0xdfc0 (CCCH demod MAC loop). exits_count together
     * with irq_pending_in_compute discriminate three cases: compute never
     * exits, the IRQ never fires (missing TPU/TSP source), or the IRQ fires
     * but is never serviced (INTM stuck or mis-vectored ISR). */
    {
        static uint16_t last_pc_sample = 0;
        static uint8_t  last_xpc_sample = 0;
        static unsigned exits_count = 0;
        static unsigned irqs_pending_during_compute = 0;
        static unsigned int3_pending_during_compute = 0;
        static uint64_t insns_in_compute = 0;
        static unsigned next_compute_dump = 200000000u;

        bool in_compute_now = ((s->xpc & 0x3) == 2 &&
                               s->pc >= 0xdf80 && s->pc <= 0xdfc0);
        bool was_in_compute = (last_xpc_sample == 2 &&
                               last_pc_sample >= 0xdf80 && last_pc_sample <= 0xdfc0);

        if (was_in_compute && !in_compute_now) {
            exits_count++;
            if (exits_count <= 30 || exits_count % 200 == 0) {
                fprintf(stderr,
                        "[c54x] EXIT-COMPUTE #%u from=XPC%u:0x%04x to=XPC%u:0x%04x "
                        "A=0x%010llx IFR=0x%04x INTM=%d insn=%u\n",
                        exits_count,
                        last_xpc_sample, last_pc_sample,
                        s->xpc & 0x3, s->pc,
                        (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                        s->ifr, (s->st1 >> 11) & 1, s->insn_count);
            }
        }

        if (in_compute_now) {
            insns_in_compute++;
            if (s->ifr != 0) {
                irqs_pending_during_compute++;
                if (s->ifr & 0x08) int3_pending_during_compute++;
            }
        }

        if (s->insn_count >= next_compute_dump) {
            next_compute_dump += 200000000u;
            fprintf(stderr,
                    "[c54x] COMPUTE-STATS insn=%u in_compute=%llu exits=%u "
                    "irq_pending_in_compute=%u int3_pending_in_compute=%u\n",
                    s->insn_count,
                    (unsigned long long)insns_in_compute,
                    exits_count,
                    irqs_pending_during_compute,
                    int3_pending_during_compute);
        }

        last_pc_sample = s->pc;
        last_xpc_sample = s->xpc & 0x3;
    }

    /* DISPATCH-ENTRY probe. The dispatcher caller branches to
     * 0x8810 + task_id*3, where each entry is { 0xf4e4 (FRET or padding),
     * 0xf074 (B opcode), <target> }. Probes the PCs that start an entry
     * (0x8810 + N*3), so task_id = (PC - 0x8810) / 3, and reads the target at
     * PC+2. */
    if (s->pc >= 0x8810 && s->pc < 0x8900 && ((s->pc - 0x8810) % 3) == 0) {
        static unsigned entry_hits = 0;
        entry_hits++;
        if (entry_hits <= 50 || entry_hits % 200 == 0) {
            uint16_t entry_idx = (s->pc - 0x8810) / 3;
            uint16_t header = s->prog[s->pc];     /* normally 0xf4e4 */
            uint16_t branch = s->prog[s->pc + 1]; /* normally 0xf074 */
            uint16_t target = s->prog[s->pc + 2];
            if (calypso_debug_enabled("DISPATCH-ENTRY")) fprintf(stderr,
                    "[c54x] DISPATCH-ENTRY #%u pc=0x%04x entry_idx=%u "
                    "header=0x%04x branch=0x%04x target=0x%04x "
                    "A=0x%010llx insn=%u\n",
                    entry_hits, s->pc, entry_idx,
                    header, branch, target,
                    (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                    s->insn_count);
        }
    }

    /* Periodic DSP state dump (every 500M insns, starting at 500M).
     * Captures: state regs, hot-zone disasm (0xa2c0..0xa2d0 + 0xb8e0..0xb910),
     * vector table at current PMST IPTR base, hot-PC opcodes, SP history. */
    {
        static unsigned next_dump = 500000000u;
        if (s->insn_count >= next_dump) {
            next_dump += 500000000u;
            uint16_t iptr = (s->pmst >> PMST_IPTR_SHIFT) & 0x1FF;
            uint16_t vbase = iptr * 0x80;
            C54_LOG("STATE-DUMP insn=%u PC=0x%04x ST0=0x%04x ST1=0x%04x INTM=%d IMR=0x%04x IFR=0x%04x XPC=%d PMST=0x%04x SP=0x%04x AR1=0x%04x AR2=0x%04x BRC=%d",
                    s->insn_count, s->pc, s->st0, s->st1,
                    !!(s->st1 & ST1_INTM),
                    s->imr, s->ifr, s->xpc, s->pmst, s->sp,
                    s->ar[1], s->ar[2], s->brc);
            C54_LOG("STATE-DUMP prog[0xa2c0..0xa2d0]: %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x",
                    s->prog[0xa2c0], s->prog[0xa2c1], s->prog[0xa2c2], s->prog[0xa2c3],
                    s->prog[0xa2c4], s->prog[0xa2c5], s->prog[0xa2c6], s->prog[0xa2c7],
                    s->prog[0xa2c8], s->prog[0xa2c9], s->prog[0xa2ca], s->prog[0xa2cb],
                    s->prog[0xa2cc], s->prog[0xa2cd], s->prog[0xa2ce], s->prog[0xa2cf],
                    s->prog[0xa2d0]);
            /* Hot zone b8e9..b906 (vec1 handler). */
            C54_LOG("STATE-DUMP prog[0xb8e0..0xb910]: %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x",
                    s->prog[0xb8e0], s->prog[0xb8e1], s->prog[0xb8e2], s->prog[0xb8e3],
                    s->prog[0xb8e4], s->prog[0xb8e5], s->prog[0xb8e6], s->prog[0xb8e7],
                    s->prog[0xb8e8], s->prog[0xb8e9], s->prog[0xb8ea], s->prog[0xb8eb],
                    s->prog[0xb8ec], s->prog[0xb8ed], s->prog[0xb8ee], s->prog[0xb8ef],
                    s->prog[0xb8f0], s->prog[0xb8f1], s->prog[0xb8f2], s->prog[0xb8f3],
                    s->prog[0xb8f4], s->prog[0xb8f5], s->prog[0xb8f6], s->prog[0xb8f7],
                    s->prog[0xb8f8], s->prog[0xb8f9], s->prog[0xb8fa], s->prog[0xb8fb],
                    s->prog[0xb8fc], s->prog[0xb8fd], s->prog[0xb8fe], s->prog[0xb8ff],
                    s->prog[0xb900]);
            C54_LOG("STATE-DUMP prog[0xb900..0xb920]: %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x",
                    s->prog[0xb900], s->prog[0xb901], s->prog[0xb902], s->prog[0xb903],
                    s->prog[0xb904], s->prog[0xb905], s->prog[0xb906], s->prog[0xb907],
                    s->prog[0xb908], s->prog[0xb909], s->prog[0xb90a], s->prog[0xb90b],
                    s->prog[0xb90c], s->prog[0xb90d], s->prog[0xb90e], s->prog[0xb90f],
                    s->prog[0xb910], s->prog[0xb911], s->prog[0xb912], s->prog[0xb913],
                    s->prog[0xb914], s->prog[0xb915], s->prog[0xb916], s->prog[0xb917],
                    s->prog[0xb918], s->prog[0xb919], s->prog[0xb91a], s->prog[0xb91b],
                    s->prog[0xb91c], s->prog[0xb91d], s->prog[0xb91e], s->prog[0xb91f],
                    s->prog[0xb920]);
            C54_LOG("STATE-DUMP vbase=0x%04x prog[vbase..vbase+0x18]: %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x",
                    vbase,
                    s->prog[vbase+0x00], s->prog[vbase+0x01], s->prog[vbase+0x02], s->prog[vbase+0x03],
                    s->prog[vbase+0x04], s->prog[vbase+0x05], s->prog[vbase+0x06], s->prog[vbase+0x07],
                    s->prog[vbase+0x08], s->prog[vbase+0x09], s->prog[vbase+0x0a], s->prog[vbase+0x0b],
                    s->prog[vbase+0x0c], s->prog[vbase+0x0d], s->prog[vbase+0x0e], s->prog[vbase+0x0f],
                    s->prog[vbase+0x10], s->prog[vbase+0x11], s->prog[vbase+0x12], s->prog[vbase+0x13],
                    s->prog[vbase+0x14], s->prog[vbase+0x15], s->prog[vbase+0x16], s->prog[vbase+0x17],
                    s->prog[vbase+0x18]);
            /* Hot-PC opcode dump for known correlator/handler sites */
            C54_LOG("STATE-DUMP HOT-OPS: 0x8d33=%04x 0x8eb9=%04x 0x8f51=%04x 0xa2c7=%04x 0xa2c8=%04x 0xb8e9=%04x 0xb8eb=%04x 0xb8f4=%04x 0xb8f5=%04x 0xb906=%04x",
                    s->prog[0x8d33], s->prog[0x8eb9], s->prog[0x8f51],
                    s->prog[0xa2c7], s->prog[0xa2c8],
                    s->prog[0xb8e9], s->prog[0xb8eb], s->prog[0xb8f4],
                    s->prog[0xb8f5], s->prog[0xb906]);
            /* DARAM 0x066F..0x0682 wait-loop disassembly: tells a B-self
             * (f073 066f) from IDLE n (f7e1/f7e2/f7e3) or a poll-and-branch.
             * An IDLE here would put the bug in the emulator's IDLE
             * handler. */
            C54_LOG("STATE-DUMP prog[0x0660..0x0690]: %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x",
                    s->prog[0x0660], s->prog[0x0661], s->prog[0x0662], s->prog[0x0663],
                    s->prog[0x0664], s->prog[0x0665], s->prog[0x0666], s->prog[0x0667],
                    s->prog[0x0668], s->prog[0x0669], s->prog[0x066a], s->prog[0x066b],
                    s->prog[0x066c], s->prog[0x066d], s->prog[0x066e], s->prog[0x066f],
                    s->prog[0x0670], s->prog[0x0671], s->prog[0x0672], s->prog[0x0673],
                    s->prog[0x0674], s->prog[0x0675], s->prog[0x0676], s->prog[0x0677],
                    s->prog[0x0678], s->prog[0x0679], s->prog[0x067a], s->prog[0x067b],
                    s->prog[0x067c], s->prog[0x067d], s->prog[0x067e], s->prog[0x067f],
                    s->prog[0x0680]);
            /* Same range but data[] view in case OVLY=1 routes fetches
             * to data array (different memory than prog). */
            C54_LOG("STATE-DUMP data[0x0660..0x0680]: %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x",
                    s->data[0x0660], s->data[0x0661], s->data[0x0662], s->data[0x0663],
                    s->data[0x0664], s->data[0x0665], s->data[0x0666], s->data[0x0667],
                    s->data[0x0668], s->data[0x0669], s->data[0x066a], s->data[0x066b],
                    s->data[0x066c], s->data[0x066d], s->data[0x066e], s->data[0x066f],
                    s->data[0x0670], s->data[0x0671], s->data[0x0672], s->data[0x0673],
                    s->data[0x0674], s->data[0x0675], s->data[0x0676], s->data[0x0677],
                    s->data[0x0678], s->data[0x0679], s->data[0x067a], s->data[0x067b],
                    s->data[0x067c], s->data[0x067d], s->data[0x067e], s->data[0x067f],
                    s->data[0x0680]);
            /* IRQ entry handler at PC=0x1854 (last 0→1 transition) */
            C54_LOG("STATE-DUMP prog[0x1850..0x1860]: %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x",
                    s->prog[0x1850], s->prog[0x1851], s->prog[0x1852], s->prog[0x1853],
                    s->prog[0x1854], s->prog[0x1855], s->prog[0x1856], s->prog[0x1857],
                    s->prog[0x1858], s->prog[0x1859], s->prog[0x185a], s->prog[0x185b],
                    s->prog[0x185c], s->prog[0x185d], s->prog[0x185e], s->prog[0x185f],
                    s->prog[0x1860]);
            /* SP history ring (last 32 sampled at 1M-insn intervals) */
            {
                char buf[2048]; int o = 0;
                int start = (sp_ring_idx >= 32) ? (sp_ring_idx - 32) : 0;
                for (unsigned i = start; i < sp_ring_idx; i++) {
                    int idx = i & 63;
                    o += snprintf(buf+o, sizeof(buf)-o,
                                  "[%u:PC=%04x SP=%04x] ",
                                  sp_ring[idx].insn,
                                  sp_ring[idx].pc,
                                  sp_ring[idx].sp);
                    if (o > (int)sizeof(buf) - 64) break;
                }
                C54_LOG("STATE-DUMP SP-RING (last %d): %s",
                        (int)(sp_ring_idx >= 32 ? 32 : sp_ring_idx), buf);
            }
        }
    }
    }   /* C54X_SONDES : sondes de tete */

    while (executed < n_insns && s->running && !s->idle) {
        unsigned it_avant = g_c54x_it_prises;   /* ANNEAU-EXEC */
        /* Cold-reset redirect to the firmware entry point, off by default.
         * The PROM dump does not contain the Calypso silicon mask-ROM, which
         * on real hardware runs at reset, sets SP=0x5AC8 plus the MMRs and
         * jumps to the firmware entry PROM0[0x7120] (prog[0x7120]=0x7718
         * STM #lk,SP, prog[0x7121]=0x5ac8). The SP==0x1100 gate restricts this
         * to the cold reset (the silicon reset value): once 0x7120 has set
         * SP=0x5AC8 the condition no longer holds, so the later sequential
         * firmware walks through 0xff80 are not hijacked (see SOFT-RESET-TRIG
         * below, insn > 100k).
         *
         * @BEQUILLE - REDIR_LEGACY  (CALYPSO_REDIR_LEGACY, EXISTS, default OFF)
         *   masks   : the real reset vector 0xff80 -> 0xb410, diverted to stand in
         *             for the TI mask-ROM missing from the dump.
         *   remove  : as soon as the real reset handler 0xb410 sets SP=0x5AC8 itself
         *             (STM #0x5AC8,SP decoded correctly) and the boot runs through
         *             without over-pop - that is the bug to trace, not to bypass.
         *   note    : owns INITTAB and REDIR7000; mutually exclusive with
         *             MASKROM_INIT.
         */
        static int redir_legacy = -1;
        if (redir_legacy < 0) redir_legacy = calypso_gate("CALYPSO_REDIR_LEGACY", 0);
        if (redir_legacy && s->pc == 0xFF80 && s->sp == 0x1100) {
            static int redirect_log;
            /* @BEQUILLE - REDIR7000  (CALYPSO_REDIR7000, EXISTS, default OFF)
             *   masks   : the init of the BACC-A tables (d[0x4c5b]/d[0x3fe1]) that entry
             *             point 0x7120 assumes is already done; redirects the reset to
             *             0x7000, which runs the full init (tables plus A), so BACC A
             *             reaches the real firmware entry instead of A=0 -> boot stub.
             *   remove  : same condition as INITTAB (table populated by the firmware path).
             *   note    : nested inside REDIR_LEGACY, and overridden by INITTAB (else if).
             */
            static int redir7000 = -1;
            if (redir7000 < 0) redir7000 = calypso_gate("CALYPSO_REDIR7000", 0);
            if (redirect_log < 3) {
                C54_LOG("SILICON-BOOT-REDIRECT PC=0xFF80 SP=0x1100 → 0x%04x%s",
                        redir7000 ? 0x7000 : 0x7120,
                        redir7000 ? " (REDIR7000: SP=0x5AC8 + init complète A-tables)" : "");
                redirect_log++;
            }
            /* @BEQUILLE - INITTAB  (CALYPSO_INITTAB, EXISTS, default OFF)
             *   masks   : the absence of the TI mask-ROM that populates the task handler
             *             table 0x4c24-0x4c5d at reset; without it 0x7120 does
             *             BACC d[0x4c5b] = null. Sets SP, pushes 0x7120 as return address
             *             and jumps to the table init 0xc704, which fills
             *             data[0x4c24-0x4c5d] and returns into the normal boot.
             *   remove  : as soon as the table is populated by a firmware path (0xc704
             *             reached natively after the 0x8869 clear) - INSTALL-TRACE probe
             *             shows d[4c5c] != 0.
             *   note    : never evaluated unless CALYPSO_REDIR_LEGACY is set.
             */
            static int inittab = -1;
            if (inittab < 0) inittab = calypso_gate("CALYPSO_INITTAB", 0);
            if (inittab) {
                s->sp = 0x5AC8;
                s->sp--; s->data[s->sp] = 0x7120;   /* return = normal boot */
                s->pc = 0xc704;                       /* run table init -> RET 0x7120 */
            } else if (redir7000) { s->sp = 0x5AC8; s->pc = 0x7000; }
            else s->pc = 0x7120;
        }
        /* Mask-ROM table init, mutually exclusive with redir_legacy (which
         * already handles 0xff80). */
        if (!redir_legacy && s->pc == 0xFF80 && s->sp == 0x1100) {
            /* @BEQUILLE - MASKROM_INIT  (CALYPSO_MASKROM_INIT, EXISTS, default OFF)
             *   masks   : same as INITTAB - the missing TI mask-ROM that sets SP=0x5AC8
             *             and populates the task handler table (0x4c04-0x4c5d) at cold
             *             reset. Without it the table is zero after the RPTB clear at
             *             0x8869, entry 0x7120 does BACC d[0x4c5b] = null, and
             *             0x7025/0xd247/0xc8e9/corr never run. Pushes 0xb410 as return
             *             address and jumps to 0xc704 (absolute addressing, so it works
             *             with AR/DP still uninitialised).
             *   remove  : same condition as INITTAB (table populated by a firmware path).
             */
            static int mrti = -1;
            if (mrti < 0) mrti = calypso_gate("CALYPSO_MASKROM_INIT", 0);   /* opt-in: forcing the boot op derails from a cold state; kept for A/B */
            if (mrti) {
                static int mrti_log = 0;
                if (mrti_log < 2) { mrti_log++;
                    fprintf(stderr, "[c54x] MASK-ROM-INIT: cold-reset SP=0x5AC8, run table-init 0xc704 (RET 0xb410) insn=%u\n", s->insn_count); }
                s->sp = 0x5AC8;
                s->sp--; s->data[s->sp] = 0x7120;   /* return = firmware entry (BACC d[0x4c5b] populated) */
                s->pc = 0xc704;                       /* fill handler table -> RET 0x7120 -> operational */
            }
        }
        if (s->pc == 0xb3e4) {
            /* @BEQUILLE - D247  (CALYPSO_D247, EXISTS, default OFF)
             *   masks   : the absence of the TI mask-ROM bootstrap that, on silicon,
             *             calls the operational subsystem 0xd247 (install handler table
             *             0xc704, TDMA slots 0xc867, vectors). In QEMU 0xd247 has one
             *             native caller, PROM0 0x7102, in a block never reached on a cold
             *             boot; without it d[0x4c5c]=0 and d[0x3f6b]=0xd294 (RET no-op),
             *             so FB acquisition is a no-op and d[3f70] never reaches 2. Pushes
             *             the return address and diverts the PC, once, at the boot-init
             *             terminal 0xb3e4 (SP=0x5AC8, cells seeded; RET @0xd25f comes
             *             back to 0xb3e4).
             *   remove  : as soon as the native calling block 0x70ce-0x7106 is reached
             *             (D247-TRACE shows site 0x7102 non-zero), or as soon as the table
             *             d[4c5c] is populated by the firmware path.
             */
            static int _d247 = -1;
            if (_d247 < 0) _d247 = calypso_gate("CALYPSO_D247", 0);   /* opt-in OFF: the bootstrap does reach 0xc6a5 (coefficient init) but loops on an empty source. Kept for A/B */
            static int _d247_done = 0;
            if (_d247 && !_d247_done) {
                _d247_done = 1;
                fprintf(stderr, "[c54x] BOOTSTRAP-D247 @0xb3e4 : run 0xd247 (install table+slots+vec) insn=%u SP=0x%04x\n", s->insn_count, s->sp);
                s->sp--; s->data[s->sp] = 0xb3e4;   /* return = boot-init terminal */
                s->pc = 0xd247;
            }
        }
        if (s->pc == 0x886a && s->data[0x4c5c] == 0) {
            /* @BEQUILLE - REPOPULATE  (CALYPSO_REPOPULATE, EXISTS, default OFF)
             *   masks   : the RPTB memset 0x8866-0x886a wipes the handler table after it
             *             was populated (insn ~19793, well after insn 92) and the firmware
             *             never calls 0xc704 again; the real firmware order is
             *             clear -> populate. At the clear's RET (0x886a), when the table is
             *             empty, the PC is diverted to 0xc704, whose own RET @0xc826 pops
             *             the same return address - so it self-heals on every clear.
             *   remove  : as soon as 0xc704 is reached AFTER the clear by the native flow
             *             (D247-TRACE: d[4c41]/d[4c46] non-zero at end of boot).
             */
            static int mrti2 = -1;
            if (mrti2 < 0) mrti2 = calypso_gate("CALYPSO_REPOPULATE", 0);   /* opt-in OFF: populating 0x4c5c does NOT unblock FB acquisition (measured: fb0_att stays 0). Kept for A/B */
            if (mrti2) {
                static int rlg = 0;
                if (rlg < 3) { rlg++;
                    fprintf(stderr, "[c54x] TABLE-REPOPULATE @0x886a (clear a wipe la table) -> run 0xc704 insn=%u\n", s->insn_count); }
                s->pc = 0xc704;
            }
        }
        /* Pure probes D247-TRACE .. PROM3-VISIT, behind the switch (none of
         * them writes the DSP state). */
        if (C54X_SONDES) {
        /* D247-TRACE (read-only). 0xd247 has exactly one native caller, PROM0
         * 0x7102, inside the operational block 0x70ce-0x7106. These probes
         * force nothing:
         *   (a) is 0x7102 reached natively, i.e. does the calling block run?
         *   (b) does 0xd247 fire, and what is the table state before and after
         *       its RET (@0xd25f)?
         *   (c) does the clear at 0x87ff run (its callers are in PROM1 via
         *       FCALL, not PROM0), and before or after 0xd247 - does it wipe
         *       the work of 0xc704?
         * d[4c41] and d[4c46] are two table slots read by the dispatcher
         * 0xc8e9 (CALA), so they report install success directly. On by
         * default, 20 lines per site. CALYPSO_D247_TRACE_OFF=1 disables; the
         * gate tests the VALUE with atoi, not mere presence. */
        {
            static int _d247t = -1;
            if (_d247t < 0) { const char *_e = calypso_getenv("CALYPSO_D247_TRACE_OFF"); _d247t = (_e && atoi(_e)) ? 0 : 1; }
            if (_d247t) {
                static unsigned _n7102=0, _nd247=0, _nd25f=0, _n87ff=0;
                if (s->pc == 0x7102 && _n7102++ < 20)
                    fprintf(stderr, "[c54x] D247-TRACE #%u @0x7102 (caller reel de 0xd247) "
                            "d[3f70]=0x%04x SP=0x%04x insn=%u\n",
                            _n7102, s->data[0x3f70], s->sp, s->insn_count);
                if (s->pc == 0xd247 && _nd247++ < 20)
                    fprintf(stderr, "[c54x] D247-TRACE #%u ENTRY 0xd247 d[3f70]=0x%04x SP=0x%04x "
                            "d[4c41]=0x%04x d[4c46]=0x%04x d[4c5c]=0x%04x (avant install) insn=%u\n",
                            _nd247, s->data[0x3f70], s->sp, s->data[0x4c41], s->data[0x4c46],
                            s->data[0x4c5c], s->insn_count);
                if (s->pc == 0xd25f && _nd25f++ < 20)
                    fprintf(stderr, "[c54x] D247-TRACE #%u RET 0xd25f d[3f70]=0x%04x "
                            "d[4c41]=0x%04x d[4c46]=0x%04x d[4c5c]=0x%04x (apres install) insn=%u\n",
                            _nd25f, s->data[0x3f70], s->data[0x4c41], s->data[0x4c46],
                            s->data[0x4c5c], s->insn_count);
                if (s->pc == 0x87ff && _n87ff++ < 20)
                    fprintf(stderr, "[c54x] D247-TRACE #%u CLEAR-ENTRY 0x87ff d[4c5c]=0x%04x "
                            "(table AVANT clear) SP=0x%04x insn=%u\n",
                            _n87ff, s->data[0x4c5c], s->sp, s->insn_count);
            }
        }
        /* CYCLE-TRACE (read-only). Cycle 1 (bit 4 armed, CALYPSO_SEED_52FD)
         * completes cleanly through a51c -> a526 -> a529 -> a534 -> a537 ->
         * a53c -> a53f -> a541 -> a544 -> a549 -> a582 -> b522 -> 011e; from
         * cycle 2 on the flow falls into a 0x71d7 <-> 0x71db loop (146
         * iterations observed) instead of repeating that path. This traces
         * EVERY pass, not just the first, to show where the two cycles
         * diverge, and logs entry into the wrapper 0x71d3 (before the loop)
         * with d[3f92], d[5a00], d[435b] (IMR shadow) and the real IMR.
         * 80 lines per site. */
        {
            static int _cyc = -1;
            if (_cyc < 0) { const char *_e = calypso_getenv("CALYPSO_D247_TRACE_OFF"); _cyc = (_e && atoi(_e)) ? 0 : 1; }
            if (_cyc) {
                static unsigned n51c=0,n537=0,n53c=0,n53f=0,n544=0,n549=0,n71d3=0;
                unsigned _cap = 20000;
                if (s->pc==0xa51c && n51c++<80)
                    fprintf(stderr, "[c54x] CYCLE-TRACE #%u ENTRY-a51c d[3f92]=0x%04x d[5a00]=0x%04x "
                            "d[435b]=0x%04x IMR=0x%04x insn=%u\n", n51c, s->data[0x3f92], s->data[0x5a00],
                            s->data[0x435b], s->imr, s->insn_count);
                if (s->pc==0xa537 && n537++<80)
                    fprintf(stderr, "[c54x] CYCLE-TRACE #%u a537(CMPM d5a00,0x88) TC=%d d[5a00]=0x%04x insn=%u\n",
                            n537, !!(s->st0 & ST0_TC), s->data[0x5a00], s->insn_count);
                if (s->pc==0xa53c && n53c++<_cap) {
                    /* The effective address is computed the way the BITF does,
                     * and printed. Hard-coding data[0x0810] is wrong: the BITF
                     * reads *AR1(0x0010), and AR1 alternates between 0x0800 and
                     * 0x0814, so the cell actually tested is 0x0810 or 0x0824. */
                    uint16_t _cs = (uint16_t)(s->ar[1] + 0x0010);
                    fprintf(stderr, "[c54x] CYCLE-TRACE #%u a53c(BITF AR1+10,0x8000) AR1=0x%04x d[3f92]=0x%04x "
                            "data[0x%04x]=0x%04x(B_TASK_ABORT=%d) fn=%u insn=%u\n",
                            n53c, s->ar[1], s->data[0x3f92], _cs, s->data[_cs],
                            !!(s->data[_cs] & 0x8000), s->data[0x0585], s->insn_count);
                }
                if (s->pc==0xa53f && n53f++<_cap)
                    fprintf(stderr, "[c54x] CYCLE-TRACE #%u a53f(BC a575 if NTC) TC=%d insn=%u\n",
                            n53f, !!(s->st0 & ST0_TC), s->insn_count);
                if (s->pc==0xa544 && n544++<80)
                    fprintf(stderr, "[c54x] CYCLE-TRACE #%u a544(BC a549 if NTC, bit0 d09bc) TC=%d d[09bc]=0x%04x insn=%u\n",
                            n544, !!(s->st0 & ST0_TC), s->data[0x09bc], s->insn_count);
                if (s->pc==0xa549 && n549++<80)
                    fprintf(stderr, "[c54x] CYCLE-TRACE #%u CONVERGE-a549 insn=%u\n", n549, s->insn_count);
                if (s->pc==0x71d3 && n71d3++<80)
                    fprintf(stderr, "[c54x] CYCLE-TRACE #%u ENTRY-71d3(wrapper) d[3f92]=0x%04x d[5a00]=0x%04x "
                            "d[435b]=0x%04x IMR=0x%04x SP=0x%04x insn=%u\n",
                            n71d3, s->data[0x3f92], s->data[0x5a00], s->data[0x435b], s->imr, s->sp, s->insn_count);
            }
        }
        /* CLUSTERB-8D21 (read-only). CALLD target inside the correlator range
         * (0x8d00-0x9000), called only by the task-type 4/6 handlers
         * (Cluster B). Static disassembly shows two nested RPTB/RPTBD, T=0x18
         * (24, tap-count shaped) and circular indirect MAR addressing - a
         * genuine signal-processing signature, unlike the audio cluster
         * c1fa/c27b and the bitmask helpers 8f7f/8f9d. 30 lines. */
        {
            static int _c8d21 = -1;
            if (_c8d21 < 0) { const char *_e = calypso_getenv("CALYPSO_D247_TRACE_OFF"); _c8d21 = (_e && atoi(_e)) ? 0 : 1; }
            if (_c8d21 && s->pc == 0x8d21) {
                static unsigned _n8d21 = 0;
                if (_n8d21++ < 30)
                    fprintf(stderr, "[c54x] CLUSTERB-8D21 #%u AR2=0x%04x AR3=0x%04x AR5=0x%04x "
                            "BK=0x%04x A=0x%06llx B=0x%06llx insn=%u\n",
                            _n8d21, s->ar[2], s->ar[3], s->ar[5], s->bk,
                            (unsigned long long)(s->a & 0xFFFFFFULL),
                            (unsigned long long)(s->b & 0xFFFFFFULL), s->insn_count);
            }
        }
        /* BITF-000B-HIT (read-only). The two BITF on data[0x000b] in the
         * background dispatcher (0xdeb6: BITF *(0x000b),0x4000, bit 14;
         * 0xdec2: BITF *(0x000b),0x2000, bit 13). Tests whether the go-live
         * cycle that loops without ever reaching the correlator is waiting on
         * a counter or flag in 0x000b that only real TPU sequencer timing
         * would advance (the 11 tpu_enq_at(0) of l1s_rx_win_ctrl are not
         * modelled, see calypso_tpu.c). Logs data[0x000b] BEFORE execution,
         * i.e. what the BITF is about to test, at both PCs. Complements
         * WATCH-000B-WR, which says whether the cell is written at all.
         * 40 lines each. */
        {
            static unsigned _nb6 = 0, _nc2 = 0;
            if (s->pc == 0xdeb6 && _nb6++ < 40)
                fprintf(stderr, "[c54x] BITF-000B-HIT #%u PC=0xdeb6 mask=0x4000 "
                        "data[0x000b]=0x%04x TC-will-be=%d insn=%u\n",
                        _nb6, s->data[0x000b], (s->data[0x000b] & 0x4000) != 0, s->insn_count);
            if (s->pc == 0xdec2 && _nc2++ < 40)
                fprintf(stderr, "[c54x] BITF-000B-HIT #%u PC=0xdec2 mask=0x2000 "
                        "data[0x000b]=0x%04x TC-will-be=%d insn=%u\n",
                        _nc2, s->data[0x000b], (s->data[0x000b] & 0x2000) != 0, s->insn_count);
        }
        /* CLUSTERB-SITES (read-only). The three task-type dispatch sites:
         * 0x8ac4 = task 3 / site 1, 0x8b01 = task 4 / site 2 (the one that
         * fired once), 0x8b8c = task 6 / site 3 (most likely SB_DSP_TASK=6).
         * Logs the current task type d[0x4357] and AR3, expected to be 0x2bc0
         * (the I/Q pointer) for sites 2 and 3. 30 lines per site. */
        {
            static int _cbs = -1;
            if (_cbs < 0) { const char *_e = calypso_getenv("CALYPSO_D247_TRACE_OFF"); _cbs = (_e && atoi(_e)) ? 0 : 1; }
            if (_cbs) {
                static unsigned _ncbs[3] = {0};
                uint16_t sites[3] = {0x8ac4, 0x8b01, 0x8b8c};
                for (int _i = 0; _i < 3; _i++) {
                    if (s->pc == sites[_i] && _ncbs[_i]++ < 30) {
                        fprintf(stderr, "[c54x] CLUSTERB-SITE#%d #%u @0x%04x task_type(d[0x4357])=0x%04x "
                                "AR3=0x%04x insn=%u\n",
                                _i+1, _ncbs[_i], sites[_i], s->data[0x4357], s->ar[3], s->insn_count);
                    }
                }
            }
        }
        /* TASKTYPE-SRC (read-only). 0xa6e9 is STL A,*(0x4357), the source of
         * the internal task-type code that drives the whole Cluster B
         * dispatch. Logs A, to identify the upstream event behind each value.
         * 40 lines. */
        {
            static int _tts = -1;
            if (_tts < 0) { const char *_e = calypso_getenv("CALYPSO_D247_TRACE_OFF"); _tts = (_e && atoi(_e)) ? 0 : 1; }
            if (_tts && s->pc == 0xa6e9) {
                static unsigned _ntts = 0;
                if (_ntts++ < 40)
                    fprintf(stderr, "[c54x] TASKTYPE-SRC #%u A=0x%06llx (-> d[0x4357]) insn=%u\n",
                            _ntts, (unsigned long long)(s->a & 0xFFFFFFULL), s->insn_count);
            }
        }
        /* A546-HIT (read-only). The only known native BACC to the 0xd247
         * bootstrap goes through 0xa546 (LD d[0x3fe0],A ; BACC A), itself
         * gated by BITF d[0x09bc],1 at 0xa544 (see WATCH-09BC-WR). Confirms
         * whether that path fires. */
        {
            static int _a546on = -1;
            if (_a546on < 0) { const char *_e = calypso_getenv("CALYPSO_D247_TRACE_OFF"); _a546on = (_e && atoi(_e)) ? 0 : 1; }
            if (_a546on && s->pc == 0xa546) {
                static unsigned _na546 = 0;
                if (_na546++ < 20)
                    fprintf(stderr, "[c54x] A546-HIT #%u : BACC natif via d[0x3fe0]=0x%04x va tirer "
                            "d[0x09bc]=0x%04x insn=%u\n",
                            _na546, s->data[0x3fe0], s->data[0x09bc], s->insn_count);
            }
        }
        /* C1FA-ENTRY (read-only). 0xc1fa is the only CALA target of the
         * dispatch at 0xa57c (LD d[0x3fd4],A ; CALA A) that had never been
         * traced; CALA-TRACE shows the constant 0xc1fa on every hit. 20 lines,
         * plus a dump of prog[0xc1fa..+0x60] on the first hit so it can be
         * disassembled offline. */
        {
            static int _c1fa = -1;
            if (_c1fa < 0) { const char *_e = calypso_getenv("CALYPSO_D247_TRACE_OFF"); _c1fa = (_e && atoi(_e)) ? 0 : 1; }
            if (_c1fa && s->pc == 0xc1fa) {
                static unsigned _nc1fa = 0;
                if (_nc1fa++ < 20)
                    fprintf(stderr, "[c54x] C1FA-ENTRY #%u A=0x%06llx SP=0x%04x "
                            "AR0..7=%04x %04x %04x %04x %04x %04x %04x %04x insn=%u\n",
                            _nc1fa, (unsigned long long)(s->a & 0xFFFFFFULL), s->sp,
                            s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                            s->ar[4], s->ar[5], s->ar[6], s->ar[7], s->insn_count);
                if (_nc1fa == 1) {
                    fprintf(stderr, "[c54x] C1FA-PROG-DUMP prog[0xc1fa..0xc25a]:\n");
                    for (uint16_t a = 0xc1fa; a <= 0xc25a; a++)
                        fprintf(stderr, "[c54x]   prog[0x%04x]=0x%04x\n", a, prog_fetch(s, a));
                }
            }
        }
        /* CLUSTER-B-PROBE (read-only). An xref scan found a PROM0 path that is
         * not polluted by the GPRS bootstrap (Cluster A / 0x87ff) and leads to
         * 0x8f7f/0x8f9d, inside the correlator range, through a per-item
         * dispatcher at 0x86d4-0x871c. Checks whether that path is ever
         * reached natively. 20 lines per site. */
        {
            static int _clb = -1;
            if (_clb < 0) { const char *_e = calypso_getenv("CALYPSO_D247_TRACE_OFF"); _clb = (_e && atoi(_e)) ? 0 : 1; }
            if (_clb) {
                static unsigned _nclb[8] = {0};
                uint16_t clb_pcs[8] = {0x86cc, 0x86d4, 0x8ac4, 0x8ad2, 0x8b01, 0x8b09, 0x8b8c, 0x8b94};
                for (int _i = 0; _i < 8; _i++) {
                    if (s->pc == clb_pcs[_i] && _nclb[_i]++ < 20) {
                        fprintf(stderr, "[c54x] CLUSTER-B-PROBE #%u @0x%04x A=0x%06llx SP=0x%04x "
                                "AR0..5=%04x %04x %04x %04x %04x %04x insn=%u\n",
                                _nclb[_i], clb_pcs[_i], (unsigned long long)(s->a & 0xFFFFFFULL), s->sp,
                                s->ar[0], s->ar[1], s->ar[2], s->ar[3], s->ar[4], s->ar[5], s->insn_count);
                    }
                }
            }
        }
        /* SOFT-RESET-TRIGGER probe. The SP-CATASTROPHE trace shows the boot
         * init at PC=0x7120 firing again at insn=190M, i.e. an internal
         * firmware soft reset. Logs every arrival at PC=0xFF80 or PC=0x7120
         * after insn > 100k (the initial silicon reset is long past), with the
         * pc_ring[-16..-1] trail and SP/AR/IMR/IFR/INTM, which names the
         * instruction that branched here. */
        if ((s->pc == 0xFF80 || s->pc == 0x7120) && s->insn_count > 100000) {
            /* Deeper trail, gated by CALYPSO_DEBUG=SOFT_RESET_TRAIL:
             * pc[-64..-1] walks back ~64 instructions before the soft reset to
             * identify the caller chain. */
            if (calypso_debug_enabled("SOFT_RESET_TRAIL")) {
                static unsigned deep_log;
                if (deep_log < 5) {
                    fprintf(stderr,
                        "[c54x] SOFT-RESET DEEP-TRAIL #%u (last 64 PCs):\n",
                        deep_log);
                    for (int row = 0; row < 8; row++) {
                        fprintf(stderr, "[c54x] SR-DEEP[%2d-%2d] :",
                                -64 + row*8, -57 + row*8);
                        for (int col = 0; col < 8; col++) {
                            int idx = -64 + row*8 + col;
                            fprintf(stderr, " %04x",
                                pc_ring[(pc_ring_idx + idx) & 255]);
                        }
                        fprintf(stderr, "\n");
                    }
                    deep_log++;
                }
            }
            static unsigned srt_log;
            if (srt_log < 30) {
                C54_LOG("SOFT-RESET-TRIG #%u PC=0x%04x insn=%u SP=0x%04x "
                        "IMR=0x%04x IFR=0x%04x INTM=%d B=0x%010llx "
                        "AR0=%04x AR1=%04x AR2=%04x AR3=%04x "
                        "AR4=%04x AR5=%04x AR6=%04x AR7=%04x",
                        srt_log, s->pc, s->insn_count, s->sp,
                        s->imr, s->ifr, !!(s->st1 & ST1_INTM),
                        (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                        s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                        s->ar[4], s->ar[5], s->ar[6], s->ar[7]);
                C54_LOG("SOFT-RESET-TRIG #%u trail pc[-16..-1] = "
                        "%04x %04x %04x %04x %04x %04x %04x %04x "
                        "%04x %04x %04x %04x %04x %04x %04x %04x",
                        srt_log,
                        pc_ring[(pc_ring_idx-⟨1⟩)&255], pc_ring[(pc_ring_idx-⟨2⟩)&255],  ×4
    ⟨⟩ = (17,16) (15,14) (13,12) (11,10)
                        pc_ring[(pc_ring_idx-⟨1⟩)&255],  pc_ring[(pc_ring_idx-⟨2⟩)&255],  ×3
    ⟨⟩ = (9,8) (7,6) (5,4)
                        pc_ring[(pc_ring_idx-3)&255],  pc_ring[(pc_ring_idx-2)&255]);
                srt_log++;
            }
        }
        if (!c54x_rapide) {   /* PROM3-VISIT probe */
        /* PROM3-VISIT probe. Counts DSP visits to the candidate SB-decode
         * entries 0x8167, 0x81ff and 0x82b8 (PROM3 SB dispatch candidates).
         * Logs the first visit only (insn_count plus caller from the ring),
         * then counts silently. Task 6 firing 30 times with no visit points at
         * the dispatch; visits with sb_att still 0 point deeper into the
         * demodulator. */
        {
            static uint64_t v8167, v81ff, v82b8;
            static uint32_t v8167_first_insn, v81ff_first_insn, v82b8_first_insn;
            uint16_t pc = s->pc;
            if (pc == 0x8167) {
                if (v8167 == 0) {
                    v8167_first_insn = s->insn_count;
                    C54_LOG("PROM3-VISIT 0x8167 FIRST-HIT insn=%u SP=%04x "
                            "AR2=%04x AR3=%04x AR4=%04x AR5=%04x",
                            v8167_first_insn, s->sp,
                            s->ar[2], s->ar[3], s->ar[4], s->ar[5]);
                }
                v8167++;
                if ((v8167 % 100000) == 0)
                    C54_LOG("PROM3-VISIT 0x8167 count=%llu insn=%u",
                            (unsigned long long)v8167, s->insn_count);
            }
            if (pc == 0x81ff) {
                if (v81ff == 0) {
                    v81ff_first_insn = s->insn_count;
                    C54_LOG("PROM3-VISIT 0x81ff FIRST-HIT insn=%u SP=%04x "
                            "AR2=%04x AR3=%04x AR4=%04x AR5=%04x",
                            v81ff_first_insn, s->sp,
                            s->ar[2], s->ar[3], s->ar[4], s->ar[5]);
                }
                v81ff++;
                if ((v81ff % 100000) == 0)
                    C54_LOG("PROM3-VISIT 0x81ff count=%llu insn=%u",
                            (unsigned long long)v81ff, s->insn_count);
            }
            if (pc == 0x82b8) {
                if (v82b8 == 0) {
                    v82b8_first_insn = s->insn_count;
                    C54_LOG("PROM3-VISIT 0x82b8 FIRST-HIT insn=%u SP=%04x "
                            "AR2=%04x AR3=%04x AR4=%04x AR5=%04x",
                            v82b8_first_insn, s->sp,
                            s->ar[2], s->ar[3], s->ar[4], s->ar[5]);
                }
                v82b8++;
                if ((v82b8 % 100000) == 0)
                    C54_LOG("PROM3-VISIT 0x82b8 count=%llu insn=%u",
                            (unsigned long long)v82b8, s->insn_count);
            }
        }
        }   /* !c54x_rapide */
        }   /* C54X_SONDES : D247-TRACE .. PROM3-VISIT */
        /* Top-of-loop SP chokepoint. The end-of-loop SP hook is bypassed by
         * every instruction that exits early (goto unimpl, return, continue, a
         * handler that leaves the dispatch chain): the SP write happens but is
         * never accounted. Measured: 61 events captured against an expected
         * descent of 11k+ words.
         *
         * Reading s->sp here, at a mandatory pass-through point, and comparing
         * it with the previous iteration's value is bypass-proof by
         * construction - it watches the VALUE at a chokepoint, not the write
         * sites. The statics persist across c54x_run calls. */
        if (!c54x_rapide) {   /* per-instruction SP/A/AR6/MVPD/correlator probes */
        {
            static uint16_t topgate_last_sp = 0;
            static uint16_t topgate_last_pc = 0;
            static uint16_t topgate_last_op = 0;
            static int      topgate_valid   = 0;

            if (topgate_valid && s->sp != topgate_last_sp) {
                /* Account the PREVIOUS instruction that changed SP, whatever
                 * exit path it took (early exit, return, ...). */
                sp_hist_account(topgate_last_pc, topgate_last_op,
                                topgate_last_sp, s->sp, s->insn_count);
            }

            /* Bootstub-entry trigger: detects the edge prev_pc outside the
             * boot stub -> cur_pc inside it, i.e. the corrupted RET that
             * jumped to 0x00XX. Dumps the verbose state plus the ring, which
             * holds ~4096 iterations of run-up. */
            if (topgate_valid) {
                sp_ring_check_bootstub_entry(s,
                    topgate_last_pc, topgate_last_op, topgate_last_sp,
                    s->pc, s->sp, s->insn_count);
            }

            /* A provenance tracer: tracks A's last writer and dumps at the
             * trigger PC. Settles the NMI-versus-A-divergence fork without an
             * invasive implementation. */
            a_track_init_lazy();
            if (topgate_valid) {
                a_track_iter(s, topgate_last_pc, topgate_last_op);
            }

            /* AR6 windowed snapshot: tells AR6=0 (base divergence) from
             * AR6=0x16 (self-alias feedback) at the trigger PC. Env
             * CALYPSO_AR6_AT_PC=0x821a plus the window size. */
            ar6_at_init_lazy();
            if (topgate_valid) {
                ar6_at_iter(s, topgate_last_pc, topgate_last_op);
            }

            topgate_last_sp = s->sp;
            topgate_last_pc = s->pc;
            topgate_last_op = prog_fetch(s, s->pc);
            topgate_valid   = 1;

            sp_ring_init_lazy();
            sp_ring_record(s->insn_count, s->pc, s->sp, topgate_last_op);

            /* MVPD overlay occupancy: lazy init, then dump once the boot
             * phase has ended. */
            mvpd_trace_init_lazy();
            mvpd_trace_dump_if_due(s->insn_count);

            /* Correlator entry trace: detects the edge prev_pc outside
             * [CORR_PC_LO..CORR_PC_HI) -> cur_pc inside it, and logs the full
             * state on entry (AR3/4/5 are the likely buffer pointers). Dumps
             * the accumulated reads every 20 entries, to see whether the
             * pattern stabilises or varies between runs. */
            corr_trace_init_lazy();
            if (g_corr_trace_enabled > 0 && topgate_valid) {
                /* Use CORR_PC_HI (0x9000), not 0x8f80: the shorter range
                 * silently missed the Cluster B targets 0x8f9d and 0x8fb8. */
                int prev_in = (topgate_last_pc >= CORR_PC_LO && topgate_last_pc < CORR_PC_HI);
                int cur_in  = (s->pc >= CORR_PC_LO && s->pc < CORR_PC_HI);
                if (!prev_in && cur_in) {
                    g_corr_entry_count++;
                    if (g_corr_entry_count <= g_corr_entry_log_cap) {
                        fprintf(stderr,
                            "[c54x] CORR-ENTRY #%u @insn=%u prev_pc=0x%04x → cur_pc=0x%04x\n"
                            "[c54x]   AR0..7: %04x %04x %04x %04x %04x %04x %04x %04x  "
                            "ARP=%d DP=%d BK=0x%04x\n"
                            "[c54x]   SP=0x%04x ST0=0x%04x ST1=0x%04x INTM=%d XPC=%d\n",
                            g_corr_entry_count, s->insn_count,
                            topgate_last_pc, s->pc,
                            s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                            s->ar[4], s->ar[5], s->ar[6], s->ar[7],
                            arp(s), dp(s), s->bk,
                            s->sp, s->st0, s->st1, !!(s->st1 & ST1_INTM), s->xpc);
                    }
                    /* Dump every 20 entries, to see whether the addresses
                     * read stabilise (correlator repeating) or vary. */
                    if ((g_corr_entry_count % 20) == 0) {
                        char tag[32];
                        snprintf(tag, sizeof(tag), "every20-entry%u", g_corr_entry_count);
                        corr_read_dump(tag);
                    }
                }
            }
        }
        }   /* !c54x_rapide */

        /* DSP idle fast-forward — see dsp_idle_fast_forward() comment.
         * Skips MAC simulation when DSP is in its empty-task-slot
         * polling loop, returning host CPU to the rest of QEMU. */
        {
            int ff_cyc;
            if (dsp_idle_fast_forward(s, &ff_cyc)) {
                s->cycles    += ff_cyc;
                s->insn_count += ff_cyc;
                executed     += ff_cyc;
                continue;
            }
        }

        /* Replay any interrupt that fired while INTM=1.
         * c54x_interrupt_ex sets IFR but does nothing else when INTM=1;
         * the real C54x re-evaluates pending interrupts every cycle, so
         * as soon as INTM clears (via RETE or RSBX INTM) a pending
         * BRINT0/TINT0/... must dispatch. Without this, a BRINT0 that
         * arrived inside another ISR is lost and the FB correlator never
         * receives its I/Q samples (d_fb_det stays 0). */
        if (!(s->st1 & ST1_INTM)) {
            uint16_t pending = s->ifr & s->imr;
            if (pending) {
                int imr_bit = __builtin_ctz(pending);
                int vec = imr_bit + 16;
                c54x_ifr_clear(s, (uint16_t)(1 << imr_bit), "vector-ex");
                s->sp--;
                data_write(s, s->sp, s->pc);
                /* A C54x interrupt is a far transition: save XPC
                 * unconditionally and force page 0 for the vector fetch.
                 * Fetching the vector through the live XPC reads the wrong
                 * page. */
                s->sp--;
                data_write(s, s->sp, s->xpc);
                s->st1 |= ST1_INTM;
                /* Set g_last_intr_* here too, otherwise the HIGHVEC and DISP
                 * probes miss replayed interrupts. fg_pc is captured BEFORE
                 * s->pc is overwritten by the vector. */
                g_last_intr_insn = s->insn_count; g_last_intr_vec = vec; { extern uint16_t g_piege_pile_sp; extern int g_piege_pile_on; if (!g_piege_pile_on) { g_piege_pile_sp = (uint16_t)(s->sp + 2); g_piege_pile_on = 1; } }
                g_last_intr_fg_pc = (uint16_t)s->pc; g_last_intr_fg_dp = dp(s);
                g_c54x_it_prises++;   /* [2026-09-23] diagnostic ANNEAU-EXEC */
                s->xpc = 0;
                uint16_t iptr = (s->pmst >> PMST_IPTR_SHIFT) & 0x1FF;
                s->pc = (iptr * 0x80) + vec * 4;
                static int pending_log = 0;
                if (pending_log < 20) {
                    C54_LOG("PENDING IRQ replay vec=%d bit=%d PC->0x%04x SP=0x%04x insn=%u",
                            vec, imr_bit, s->pc, s->sp, s->insn_count);
                    pending_log++;
                }
            }
        }

        /* Record PC in ring buffer */
        pc_ring[pc_ring_idx & 255] = s->pc;
        pc_ring_idx++;

        /* [2026-09-23] Sondes pures HIT-b906 .. HOT-OPS (INTM-TRANS, SP-WATCH,
         * SP-DRAIN, CALLSITE, XPC-WR, AR2-WR, histogrammes de PC, ...). */
        if (C54X_SONDES) {
        /* Push counter at PC=0xb906. Logs at powers of ten to track the
         * cadence; SP is captured at the hit. */
        {
            static unsigned hit_b906 = 0;
            if (s->pc == 0xb906) {
                hit_b906++;
                if (hit_b906 == 1 || hit_b906 == 10 || hit_b906 == 100 ||
                    hit_b906 == 1000 || hit_b906 == 10000 ||
                    hit_b906 == 100000 || hit_b906 == 1000000) {
                    C54_LOG("HIT-b906 #%u op=0x%04x SP=0x%04x XPC=%d insn=%u",
                            hit_b906, s->prog[0xb906], s->sp, s->xpc,
                            s->insn_count);
                }
            }
        }

        /* INTM transition tracer: every change of ST1 bit 11 with
         * surrounding state. Identifies which IRQ entered the trap and
         * whether RETE / RSBX paths ever execute again. On each 0->1
         * (IRQ entry), also dump prog[PC..PC+8] and the 4 most-recently
         * pushed stack words (data[SP..SP+3]) so we can see what handler
         * we're entering and why it never RETEs.
         *
         * NOTE: this block runs BEFORE c54x_exec_one of the current
         * iteration. So when a transition is observed, the cause was
         * either (a) the previous iteration's exec_one (RETE, RSBX INTM
         * etc. — INTM 1→0), or (b) the pending-IRQ replay block above
         * (INTM 0→1, PC moved to vector). For (a), s->pc has already
         * advanced past the cause — log the previous iteration's
         * exec_pc/exec_op (captured at end of loop into last_exec_*) so
         * the cause is unambiguous. For (b), s->pc IS the vector entry
         * and is informative as-is. */
        {
            static int intm_log = 0;
            static uint16_t prev_intm = 0xFFFF;
            uint16_t cur_intm = !!(s->st1 & ST1_INTM);
            /* Dead site: TINT0 used to be forced on every INTM 1->0
             * transition here. It broke BRINT0, because vec 20 (bit 4) beats
             * vec 21 (bit 5) in priority and starved the I/Q delivery. TINT0
             * now comes from the faithful TIMER0 tick further down, which
             * honours the IMR. The statics are read once and discarded. */
            {
                static int _t0i = -1;
                if (_t0i < 0) _t0i = calypso_gate("CALYPSO_TINT0_MASTER", 0);
                static unsigned _t0period = 0;
                if (_t0period == 0) { const char *_p = calypso_getenv("CALYPSO_TINT0_PERIOD"); _t0period = _p ? (unsigned)atoi(_p) : 1500; if (_t0period < 1) _t0period = 1500; }
                static unsigned _t0last = 0;
                (void)_t0i; (void)_t0last; (void)_t0period;
            }
            if (prev_intm != 0xFFFF && cur_intm != prev_intm && intm_log < 200) {
                C54_LOG("INTM-TRANS %u->%u current PC=0x%04x op=0x%04x | "
                        "cause prev_exec PC=0x%04x op=0x%04x | "
                        "XPC=%d IFR=0x%04x SP=0x%04x insn=%u",
                        (unsigned)prev_intm, (unsigned)cur_intm,
                        s->pc, s->prog[s->pc],
                        s->last_exec_pc, s->last_exec_op,
                        s->xpc, s->ifr, s->sp,
                        s->insn_count);
                if (cur_intm == 1) {
                    C54_LOG("  HANDLER prog[PC..PC+8]: %04x %04x %04x %04x %04x %04x %04x %04x %04x",
                            s->prog[s->pc],
                            s->prog[(uint16_t)(s->pc + ⟨1⟩)],  ×7
    ⟨⟩ = (1) (2) (3) (4) (5) (6) (7)
                            s->prog[(uint16_t)(s->pc + 8)]);
                    C54_LOG("  STACK data[SP..SP+3]: %04x %04x %04x %04x",
                            s->data[s->sp],
                            s->data[(uint16_t)(s->sp + 1)],
                            s->data[(uint16_t)(s->sp + 2)],
                            s->data[(uint16_t)(s->sp + 3)]);
                }
                intm_log++;
            }
            /* INT3-CYCLE-TRACE: fire end-good on ANY INTM 1->0 transition,
             * not just RETE - the firmware uses a POPM ST1 + RCD pattern. The
             * callee is a no-op when the probe is disabled or no cycle is
             * active, so the unconditional call is safe. */
            if (prev_intm == 1 && cur_intm == 0) {
                int3_cycle_end_good(s, s->pc);
            }
            prev_intm = cur_intm;
        }

        /* SP-WATCH: log every transition where SP enters / leaves the
         * API mailbox region [0x0800..0x08FF]. This pinpoints the exact
         * instruction that corrupts the stack pointer so we don't have
         * to keep recoding to investigate. */
        {
            static uint16_t prev_sp = 0xFFFF;
            bool was_in = (prev_sp >= 0x0800 && prev_sp < 0x0900);
            bool is_in  = (s->sp  >= 0x0800 && s->sp  < 0x0900);
            if (was_in != is_in) {
                if (calypso_debug_enabled("SP-WATCH")) fprintf(stderr,
                        "[c54x] SP-WATCH %s SP=0x%04x (prev=0x%04x) "
                        "PC=0x%04x op=0x%04x insn=%u\n",
                        is_in ? "ENTER api" : "LEAVE api",
                        s->sp, prev_sp, s->pc, s->prog[s->pc], s->insn_count);
            }
            prev_sp = s->sp;
        }

        /* SP-DRAIN probe (CALYPSO_DEBUG=SP-DRAIN): attributes each net SP
         * decrement to the instruction that just executed (last_exec_pc/op,
         * captured at the end of the previous iteration). These blocks run
         * BEFORE exec_one of the current iteration, so s->sp reflects the
         * previous instruction. Isolates the unpaired instruction that drains
         * SP in the boot trampoline 0x0000 <-> 0xffcd. 8-slot histogram plus
         * the first 120 events. Silent by default. */
        if (calypso_debug_enabled("SP-DRAIN")) {
            static uint16_t sd_prev_sp = 0xFFFF;
            static unsigned  sd_log = 0;
            static uint32_t  sd_cnt[8];
            static uint16_t  sd_pc[8];
            static uint32_t  sd_events;
            if (sd_prev_sp != 0xFFFF) {
                int delta = (int)(uint16_t)(sd_prev_sp - s->sp); /* >0 = push */
                if (delta > 0 && delta < 0x100) {
                    uint16_t cpc = s->last_exec_pc;
                    int slot = -1, freeslot = -1;
                    for (int i = 0; i < 8; i++) {
                        if (sd_cnt[i] && sd_pc[i] == cpc) { slot = i; break; }
                        if (!sd_cnt[i] && freeslot < 0) freeslot = i;
                    }
                    if (slot < 0 && freeslot >= 0) { slot = freeslot; sd_pc[slot] = cpc; }
                    if (slot >= 0) sd_cnt[slot] += (uint32_t)delta;
                    if (sd_log < 120) {
                        sd_log++;
                        C54_DBG("SP-DRAIN",
                                "push -%d SP=0x%04x<-0x%04x by PC=0x%04x op=0x%04x insn=%u",
                                delta, s->sp, sd_prev_sp, cpc,
                                s->last_exec_op, s->insn_count);
                    }
                    if ((++sd_events % 1000) == 0) {
                        C54_DBG("SP-DRAIN",
                                "TOP pushers: %04x:%u %04x:%u %04x:%u %04x:%u "
                                "%04x:%u %04x:%u %04x:%u %04x:%u (events=%u SP=0x%04x)",
                                sd_pc[⟨1⟩], sd_cnt[⟨2⟩], sd_pc[⟨3⟩], sd_cnt[⟨4⟩],  ×4
    ⟨⟩ = (0,0,1,1) (2,2,3,3) (4,4,5,5) (6,6,7,7)
                                sd_events, s->sp);
                    }
                }
            }
            sd_prev_sp = s->sp;
        }

        /* CALLSITE probe (CALYPSO_DEBUG=CALLSITE): at the RCD epilogue
         * 0x7707, dumps the return address RCD is about to pop, the call-site
         * opcode (FCALL F9xx versus CALL F074) and the pre-RETD pc-ring, which
         * tells a park from a crash. */
        if (s->pc == 0x7707 && calypso_debug_enabled("CALLSITE")) {
            static int n7707 = 0;
            if (n7707 < 8) {
                n7707++;
                uint16_t ret = data_read(s, s->sp);
                C54_DBG("CALLSITE",
                    "RCD@7707 #%d SP=0x%04x ret=0x%04x caller[ret-2..ret-1]=0x%04x 0x%04x XPC=%d insn=%u",
                    n7707, s->sp, ret, prog_read(s, (uint16_t)(ret-2)),
                    prog_read(s, (uint16_t)(ret-1)), s->xpc, s->insn_count);
                char buf[300]; int o=0;
                for (int i=20;i>=1;i--)
                    o+=snprintf(buf+o,sizeof(buf)-o,"%04x ", pc_ring[(pc_ring_idx-i)&255]);
                C54_DBG("CALLSITE", "  pre-RETD pcring(20): %s", buf);
            }
        }

        /* XPC-WR tracer (CALYPSO_DEBUG=XPC-WR): every XPC transition with the
         * instruction that caused it, i.e. the origin of the XPC=3 garbage. */
        if (calypso_debug_enabled("XPC-WR")) {
            static uint8_t xprev = 0xFF;
            if (xprev != 0xFF && (uint8_t)s->xpc != xprev) {
                C54_DBG("XPC-WR",
                    "XPC %u->%u cause prev_exec PC=0x%04x op=0x%04x SP=0x%04x insn=%u",
                    xprev, (unsigned)(s->xpc & 0xFF), s->last_exec_pc,
                    s->last_exec_op, s->sp, s->insn_count);
            }
            xprev = (uint8_t)s->xpc;
        }

        /* AR2-WR tracer (CALYPSO_DEBUG=AR2-WR): tells a reset from a runaway.
         * delta == -1 is the normal post-decrement (logged every 200);
         * delta != -1 is a reset, jump or load, which is the discriminator
         * between "a reset exists" and "there is never a reset". Reports BK
         * and the reset target. */
        if (calypso_debug_enabled("AR2-WR")) {
            static int      ar2_first = 1;
            static uint16_t ar2_prev = 0;
            static uint32_t ar2_dec  = 0;
            uint16_t cur = s->ar[2];
            if (!ar2_first && cur != ar2_prev) {
                int delta = (int)(int16_t)(cur - ar2_prev);
                if (delta == -1) {
                    if ((++ar2_dec % 200) == 0)
                        C54_DBG("AR2-WR", "AR2 dec #%u ->0x%04x (linear -1) PC=0x%04x insn=%u",
                                ar2_dec, cur, s->last_exec_pc, s->insn_count);
                } else {
                    C54_DBG("AR2-WR",
                        "AR2 %s 0x%04x->0x%04x (delta=%+d) cause PC=0x%04x op=0x%04x BK=0x%04x insn=%u",
                        delta > 0 ? "RESET/UP" : "JUMP-DN", ar2_prev, cur, delta,
                        s->last_exec_pc, s->last_exec_op, s->bk, s->insn_count);
                }
            }
            ar2_first = 0; ar2_prev = cur;
        }

        /* Dump entry into the 0xe260 loop (first 5 hits). */
        if (s->pc == 0xe260 || s->pc == 0xe261) {
            static int e260_log = 0;
            if (e260_log < 5) {
                e260_log++;
                C54_LOG("E260-ENTRY #%d PC=0x%04x AR2=%04x AR5=%04x BRC=%d RSA=%04x REA=%04x rptb=%d IMR=%04x SP=%04x insn=%u",
                        e260_log, s->pc, s->ar[2], s->ar[5], s->brc, s->rsa, s->rea, s->rptb_active, s->imr, s->sp, s->insn_count);
                int idx = pc_ring_idx;
                char buf[1024]; int o = 0;
                for (int i = 50; i >= 1; i--) {
                    o += snprintf(buf+o, sizeof(buf)-o, "%04x ", pc_ring[(idx-i)&255]);
                }
                C54_LOG("E260-PCRING (last 50): %s", buf);
                /* Runtime opcodes 0xe255..0xe28f */
                char ob[1024]; int oo = 0;
                for (uint16_t a = 0xe255; a <= 0xe28f; a++) {
                    oo += snprintf(ob+oo, sizeof(ob)-oo, "%04x ", s->prog[a]);
                }
                C54_LOG("E260-PROG[e255..e28f]: %s", ob);
            }
        }

        /* CALA loop tracer: A and SP at PC=0xd24e and 0xd250 (first 40). */
        if (s->pc == 0xd24e || s->pc == 0xd250) {
            static int cala_log = 0;
            if (cala_log++ < 40) {
                C54_LOG("CALA-TRACE PC=0x%04x A=%08x SP=0x%04x BRC=%d AR2=%04x AR3=%04x AR4=%04x AR5=%04x insn=%u",
                        s->pc, (uint32_t)(s->a & 0xFFFFFFFF), s->sp, s->brc,
                        s->ar[2], s->ar[3], s->ar[4], s->ar[5], s->insn_count);
            }
        }

        /* PC histogram: visits per PC, top 20 dumped every 2M insns. */
        {
            static uint32_t pc_hist[0x10000];
            static uint64_t hist_last_dump = 0;
            pc_hist[s->pc]++;
            if (s->insn_count - hist_last_dump >= 2000000) {
                hist_last_dump = s->insn_count;
                /* find the top 20 */
                uint32_t top_cnt[20] = {0};
                uint16_t top_pc[20] = {0};
                for (int i = 0; i < 0x10000; i++) {
                    uint32_t c = pc_hist[i];
                    if (c == 0) continue;
                    for (int j = 0; j < 20; j++) {
                        if (c > top_cnt[j]) {
                            for (int k = 19; k > j; k--) {
                                top_cnt[k] = top_cnt[k-1];
                                top_pc[k] = top_pc[k-1];
                            }
                            top_cnt[j] = c;
                            top_pc[j] = (uint16_t)i;
                            break;
                        }
                    }
                }
                C54_LOG("PC HIST insn=%u top: %04x:%u %04x:%u %04x:%u %04x:%u %04x:%u %04x:%u %04x:%u %04x:%u %04x:%u %04x:%u",
                        s->insn_count,
                        top_pc[⟨1⟩], top_cnt[⟨2⟩], top_pc[⟨3⟩], top_cnt[⟨4⟩], top_pc[⟨5⟩], top_cnt[⟨6⟩],  ×3
    ⟨⟩ = (0,0,1,1,2,2) (3,3,4,4,5,5) (6,6,7,7,8,8)
                        top_pc[9], top_cnt[9]);
                C54_LOG("PC HIST cont:        %04x:%u %04x:%u %04x:%u %04x:%u %04x:%u %04x:%u %04x:%u %04x:%u %04x:%u %04x:%u",
                        top_pc[⟨1⟩], top_cnt[⟨2⟩], top_pc[⟨3⟩], top_cnt[⟨4⟩], top_pc[⟨5⟩], top_cnt[⟨6⟩],  ×3
    ⟨⟩ = (10,10,11,11,12,12) (13,13,14,14,15,15) (16,16,17,17,18,18)
                        top_pc[19], top_cnt[19]);
                memset(pc_hist, 0, sizeof(pc_hist));
            }
        }

        /* Rolling PC sampler: a histogram over a 100k-insn window, reset each
         * window, so the top-5 always answers "what is the DSP doing right
         * now".
         *
         * The cumulative-since-boot PC histogram is useless for that: it shows
         * 0xa218..0xa222 dominant because the init loop at 0xa222 (BANZD on
         * AR5, 60k iterations, ~984k insns = 0.06% of a 1.7B run) ran once
         * early and stays at the top forever. */
        {
            static uint32_t pc_recent[0x10000];
            static uint32_t recent_last_dump = 0;
            pc_recent[s->pc]++;
            if (s->insn_count - recent_last_dump >= 100000) {
                recent_last_dump = s->insn_count;
                uint32_t top_cnt[5] = {0};
                uint16_t top_pc[5]  = {0};
                for (int i = 0; i < 0x10000; i++) {
                    uint32_t c = pc_recent[i];
                    if (c <= top_cnt[4]) continue;
                    top_cnt[4] = c; top_pc[4] = (uint16_t)i;
                    for (int j = 4; j > 0 && top_cnt[j] > top_cnt[j-1]; j--) {
                        uint32_t tc = top_cnt[j]; top_cnt[j] = top_cnt[j-1]; top_cnt[j-1] = tc;
                        uint16_t tp = top_pc[j]; top_pc[j] = top_pc[j-1]; top_pc[j-1] = tp;
                    }
                }
                C54_LOG("PC RECENT (last 100k) top: %04x:%u %04x:%u %04x:%u %04x:%u %04x:%u",
                        top_pc[0], top_cnt[0], top_pc[1], top_cnt[1],
                        top_pc[2], top_cnt[2], top_pc[3], top_cnt[3],
                        top_pc[4], top_cnt[4]);
                memset(pc_recent, 0, sizeof(pc_recent));
            }
        }

        /* ENTER-RPTB-A218 probe. Measured over the first 20 events: BRC is 0
         * and AR1 is 0 on every visit, AR2 increments by 2, and visits are 16
         * insns apart. Logs the first 200 events, then samples every 100k
         * visits so the late run is covered too (a cap of 20 saturated at
         * insn=48M on a 2.4B-insn run). The !s->rpt_active guard avoids
         * spurious mid-RPTB hits. */
        if (s->pc == 0xa218 && !s->rpt_active) {
            static unsigned a218_total = 0;
            static int a218_log = 0;
            a218_total++;
            bool log_now = (a218_log < 200) ||
                           (a218_total % 100000 == 0);
            if (log_now) {
                C54_LOG("ENTER-RPTB-A218 #%d total=%u BRC=%u (0x%04x) "
                        "AR0=0x%04x AR1=0x%04x AR2=0x%04x AR3=0x%04x "
                        "AR4=0x%04x AR5=0x%04x A=%010llx T=0x%04x "
                        "ST0=0x%04x insn=%u",
                        a218_log + 1, a218_total, s->brc, s->brc,
                        s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                        s->ar[4], s->ar[5],
                        (unsigned long long)(s->a & 0xFFFFFFFFFFLL),
                        s->t, s->st0, s->insn_count);
                a218_log++;
            }
        }
        /* BANZD-A222 probe. 0xa222 op=0x6e81 with operand 0x8208 is
         * `BANZD pmad, *Sind`. Captures every AR, so the one the *Sind operand
         * actually decodes to is observed rather than guessed: AR1 is 0 on
         * every visit, so the branch test uses a different AR (AR5). First 200
         * events, then every 100k. */
        if (s->pc == 0xa222 && !s->rpt_active) {
            static unsigned a222_total = 0;
            static int a222_log = 0;
            a222_total++;
            bool log_now = (a222_log < 200) ||
                           (a222_total % 100000 == 0);
            if (log_now) {
                C54_LOG("BANZD-A222 #%d total=%u op=0x%04x op2=0x%04x "
                        "AR0=0x%04x AR1=0x%04x AR2=0x%04x AR3=0x%04x "
                        "AR4=0x%04x AR5=0x%04x AR6=0x%04x AR7=0x%04x "
                        "BRC=%u insn=%u",
                        a222_log + 1, a222_total,
                        s->prog[s->pc], s->prog[(uint16_t)(s->pc + 1)],
                        s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                        s->ar[4], s->ar[5], s->ar[6], s->ar[7],
                        s->brc, s->insn_count);
                a222_log++;
            }
        }
        /* Companion probe at 0xa215 (BRC setup) and 0xa217 (outer entry).
         * 0xa215 op=0x4492 with 0xa216 operand 0x0092 is `ADD/SUB Smem,16,dst`
         * per tic54x-opc.c (2 words, mask FE00, base 0x4400). Logs A before
         * and after plus the Smem read, to trace the value landing in dst,
         * which may eventually feed BRC. 30 events. */
        if (s->pc == 0xa215 || s->pc == 0xa217) {
            static int brc_setup_215 = 0;
            static int brc_setup_217 = 0;
            int *cnt = (s->pc == 0xa215) ? &brc_setup_215 : &brc_setup_217;
            if (*cnt < 30) {
                C54_LOG("ENTER-A%04x #%d AR0=%04x AR1=%04x AR2=%04x "
                        "A=%010llx B=%010llx T=%04x BRC=%u DP=0x%03x insn=%u",
                        s->pc, *cnt + 1,
                        s->ar[0], s->ar[1], s->ar[2],
                        (unsigned long long)(s->a & 0xFFFFFFFFFFLL),
                        (unsigned long long)(s->b & 0xFFFFFFFFFFLL),
                        s->t, s->brc, (s->st0 & 0x1FF), s->insn_count);
                (*cnt)++;
            }
        }

        /* XC-COND probe at PC=0xa0e0 / 0xa0e4. The routine 0xa0e0..0xa0e9 ends
         * at PC=0xa0e7 op=0xc8be with AR4 consistently 0x18 (= MMR_SP) before
         * the instruction, so the ST||LD writes to SP.
         *
         * The static dump shows two `XC 1, cond` before 0xc8be:
         *   0xa0e0 = 0xfd30  ; XC 1, cond=0x30 (TC)
         *   0xa0e4 = 0xfd43  ; XC 1, cond=0x43 (ALT, A<0)
         * When such a condition is false, the conditional STM #lk,AR4 (likely
         * at 0xa0e5) is skipped and AR4 keeps the stale 0x18 from an earlier
         * path. Logs the condition byte, the TC/A/B flags, AR4 and the next
         * opcode (the one skipped or executed) on every visit, 100 per PC. */
        if (s->pc == 0xa0e0 || s->pc == 0xa0e4) {
            static unsigned xc_log_e0;
            static unsigned xc_log_e4;
            unsigned *cnt = (s->pc == 0xa0e0) ? &xc_log_e0 : &xc_log_e4;
            if (*cnt < 100) {
                uint16_t op_xc = s->prog[s->pc];
                uint8_t  cond_byte = op_xc & 0xFF;
                uint16_t next_op   = s->prog[(uint16_t)(s->pc + 1)];
                /* Mirrors the condition decode of the XC handler in
                 * c54x_exec_one, common subset only. */
                bool cond = false;
                if      (cond_byte == 0x00) cond = true;
                else if (cond_byte == 0x0C) cond = (s->st0 & ST0_C) != 0;
                else if (cond_byte == 0x08) cond = !(s->st0 & ST0_C);
                else if (cond_byte == 0x30) cond = (s->st0 & ST0_TC) != 0;
                else if (cond_byte == 0x20) cond = !(s->st0 & ST0_TC);
                else if (cond_byte == 0x45) cond = (sext40(s->a) == 0);
                else if (cond_byte == 0x44) cond = (sext40(s->a) != 0);
                else if (cond_byte == 0x46) cond = (sext40(s->a) > 0);
                else if (cond_byte == 0x42) cond = (sext40(s->a) >= 0);
                else if (cond_byte == 0x43) cond = (sext40(s->a) < 0);
                else if (cond_byte == 0x47) cond = (sext40(s->a) <= 0);
                else if (cond_byte == 0x4D) cond = (sext40(s->b) == 0);
                else if (cond_byte == 0x4C) cond = (sext40(s->b) != 0);
                else if (cond_byte == 0x4E) cond = (sext40(s->b) > 0);
                else if (cond_byte == 0x4A) cond = (sext40(s->b) >= 0);
                else if (cond_byte == 0x4B) cond = (sext40(s->b) < 0);
                else if (cond_byte == 0x4F) cond = (sext40(s->b) <= 0);
                if (calypso_debug_enabled("XC-COND")) fprintf(stderr,
                        "[c54x] XC-COND #%u PC=0x%04x op=0x%04x cond=0x%02x "
                        "→ %s | TC=%d C=%d A=%010llx (sgn:%c) "
                        "B=%010llx (sgn:%c) AR4=0x%04x next_op=0x%04x insn=%u\n",
                        *cnt + 1, s->pc, op_xc, cond_byte,
                        cond ? "TAKEN " : "SKIPPED",
                        !!(s->st0 & ST0_TC),
                        !!(s->st0 & ST0_C),
                        (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                        sext40(s->a) < 0 ? '-' : (sext40(s->a) == 0 ? '0' : '+'),
                        (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                        sext40(s->b) < 0 ? '-' : (sext40(s->b) == 0 ? '0' : '+'),
                        s->ar[4], next_op, s->insn_count);
                (*cnt)++;
            }
        }

        /* MAC-8d33 trace: the FB-det inner correlator.
         * The DSP loops indefinitely in 0x8d2d..0x8d36. Static dump:
         *   8d2d 0x771a 0x0004      ; (2-word) — likely setup
         *   8d2f 0xf072 0x8d33      ; RPTB pmad, end=0x8d33 (per tic54x)
         *   8d31 0xf461             ; F46x = SFTA src,shift,dst (1-word)
         *   8d32 0xf591             ; F591 = ROL B (per our decoder)
         *   8d33 0xf3e2             ; F3E0-F3FF = SFTL src,SHIFT,DST  (writes a_sync_SNR)
         *   8d34 0x6e89 0x8d2d      ; BANZD pmad=0x8d2d, *AR - outer back-branch
         *   8d36 0xf3e1             ; SFTL B,1,B (exit path)
         * PC histogram counts (105k outer / 526k inner = 5x) confirm the RPTB
         * body (0x8d32, 0x8d33, 0x8d34) runs 5 times per outer iteration.
         *
         * Captures A before, T and AR2..AR5 at each PC in this zone, rate
         * limited to: the first 50 (init and early convergence), every 5000th
         * (steady state), and any |A_after - last_logged_A| > 0x100000, a
         * significant accumulator shift worth dumping. */
        if (s->pc >= 0x8d2c && s->pc <= 0x8d3a) {
            static uint64_t mac8d_count;
            static int64_t  last_logged_a;
            int64_t a_now = sext40(s->a);
            int64_t da = a_now - last_logged_a;
            if (da < 0) da = -da;
            mac8d_count++;
            bool log_now = (mac8d_count <= 50) ||
                           (mac8d_count % 5000) == 0 ||
                           da > 0x100000LL;
            if (log_now) {
                C54_LOG("MAC-8d33 #%llu PC=0x%04x op=0x%04x A_pre=%010llx B=%010llx "
                        "T=0x%04x ARs: %04x %04x %04x %04x %04x %04x BRC=%d insn=%u",
                        (unsigned long long)mac8d_count,
                        s->pc, s->prog[s->pc],
                        (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                        (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                        s->t,
                        s->ar[2], s->ar[3], s->ar[4], s->ar[5], s->ar[6], s->ar[7],
                        s->brc, s->insn_count);
                last_logged_a = a_now;
            }
        }
        /* Outer-entry tracer at PC=0x8d2d: always logs A on entry, 200 events.
         * A non-zero on outer entry means the accumulator was not reset
         * between FB-det attempts - the 21 consecutive 0x2fb0 SNR values
         * observed are consistent with an accumulator stuck across
         * attempts. */
        if (s->pc == 0x8d2d) {
            static uint64_t enter_8d2d;
            enter_8d2d++;
            if (enter_8d2d <= 200) {
                C54_LOG("ENTER-8d2d #%llu A_pre=%010llx B_pre=%010llx T=0x%04x "
                        "ARs: %04x %04x %04x %04x %04x %04x %04x %04x SP=0x%04x BRC=%d insn=%u",
                        (unsigned long long)enter_8d2d,
                        (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                        (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                        s->t,
                        s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                        s->ar[4], s->ar[5], s->ar[6], s->ar[7],
                        s->sp, s->brc, s->insn_count);
            }
        }

        /* HOT-OPS probe for 0xe9ac..0xe9b7 plus 0xe981..0xe983. The DSP locks
         * in a deterministic 7-instruction loop at 0xe9ac..0xe9b7 (PROM1
         * mirror), with an outer 3-PC loop at 0xe981..0xe983 reloading a BRC
         * counter - the shape of `RPTB end_addr` plus an outer reset. One-shot
         * dump of the real opcodes on first entry into the body range, with
         * the words before it (the RPTB instruction itself) and the outer
         * loop. */
        {
            static bool e9ac_dumped = false;
            if (!e9ac_dumped && s->pc >= 0xe9ac && s->pc <= 0xe9b7) {
                e9ac_dumped = true;
                fprintf(stderr,
                        "[c54x] HOT-OPS-DUMP triggered at PC=0x%04x insn=%u\n",
                        s->pc, s->insn_count);
                fprintf(stderr,
                        "[c54x] HOT-OPS prog[0xe9a0..0xe9bf]:");
                for (uint16_t a = 0xe9a0; a <= 0xe9bf; a++)
                    fprintf(stderr, " %04x", s->prog[a]);
                fprintf(stderr, "\n");
                fprintf(stderr,
                        "[c54x] HOT-OPS prog[0xe97c..0xe98f] (outer):");
                for (uint16_t a = 0xe97c; a <= 0xe98f; a++)
                    fprintf(stderr, " %04x", s->prog[a]);
                fprintf(stderr, "\n");
                fprintf(stderr,
                        "[c54x] HOT-OPS state: BRC=%d RSA=0x%04x REA=0x%04x "
                        "rptb_active=%d ST1=0x%04x AR0..7: %04x %04x %04x %04x "
                        "%04x %04x %04x %04x\n",
                        s->brc, s->rsa, s->rea, s->rptb_active, s->st1,
                        s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                        s->ar[4], s->ar[5], s->ar[6], s->ar[7]);
            }
        }
        }   /* C54X_SONDES : HIT-b906 .. HOT-OPS */

        /* Track SP changes inside RPTB loops */
        uint16_t sp_before = s->sp;
        /* Snapshot for the transfer ring, the A-write ring and the NOP-region
         * guard. pre_op only serves the !c54x_rapide block at the end of the
         * loop: no prog_fetch when that block is off. Under C54X_SONDES it is
         * kept, so that the OVLY-SCRATCH arming message (first call of
         * c54x_ovly_bas) comes out at the same place. */
        uint16_t pre_pc  = s->pc;
        uint8_t  pre_xpc = s->xpc & 0x3;
        uint16_t pre_op  = (c54x_rapide && !C54X_SONDES) ? 0 : prog_fetch(s, s->pc);
        int64_t  pre_a   = s->a;

        /* [2026-09-23] Sondes pures EB04 .. BOOT trace, derriere l'interrupteur. */
        if (C54X_SONDES) {
        /* EB04 loop: dump the first 20 iterations. */
        if (s->pc == 0xEB04) {
            static int eb04_log = 0;
            if (eb04_log < 20) {
                C54_LOG("EB04 op=%04x A=0x%010llx B=0x%010llx T=%04x "
                        "INTM=%d IMR=%04x IFR=%04x rptb=%d RSA=%04x REA=%04x BRC=%d "
                        "AR0=%04x AR1=%04x AR2=%04x AR3=%04x AR4=%04x AR5=%04x AR6=%04x AR7=%04x",
                        prog_fetch(s, s->pc),
                        (unsigned long long)(s->a & 0xFFFFFFFFFFLL),
                        (unsigned long long)(s->b & 0xFFFFFFFFFFLL),
                        s->t,
                        !!(s->st1 & ST1_INTM), s->imr, s->ifr,
                        s->rptb_active, s->rsa, s->rea, s->brc,
                        s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                        s->ar[4], s->ar[5], s->ar[6], s->ar[7]);
                eb04_log++;
            }
        }

        /* Dump DSP state when stuck — triggers once after 500M instructions
         * if DSP hasn't reached IDLE yet */
        {
            static int dumped = 0;
            if (s->insn_count > 500000000 && !dumped && !s->idle) {
                dumped = 1;
                C54_LOG("DSP NO-IDLE dump at insn=%u PC=0x%04x:", s->insn_count, s->pc);
                C54_LOG("  ST0=0x%04x ST1=0x%04x PMST=0x%04x SP=0x%04x INTM=%d",
                        s->st0, s->st1, s->pmst, s->sp, !!(s->st1 & ST1_INTM));
                C54_LOG("  IMR=0x%04x IFR=0x%04x rptb=%d RSA=0x%04x REA=0x%04x BRC=%d",
                        s->imr, s->ifr, s->rptb_active, s->rsa, s->rea, s->brc);
                C54_LOG("  A=0x%010llx B=0x%010llx T=0x%04x XPC=%d",
                        (unsigned long long)(s->a & 0xFFFFFFFFFFLL),
                        (unsigned long long)(s->b & 0xFFFFFFFFFFLL), s->t, s->xpc);
                C54_LOG("  AR0=%04x AR1=%04x AR2=%04x AR3=%04x AR4=%04x AR5=%04x AR6=%04x AR7=%04x",
                        s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                        s->ar[4], s->ar[5], s->ar[6], s->ar[7]);
                /* Code around the current PC; prog_fetch honours OVLY. */
                C54_LOG("  Code around PC:");
                for (int i = -4; i < 16; i++) {
                    uint16_t a = s->pc + i;
                    C54_LOG("  %c [0x%04x] = 0x%04x",
                            i == 0 ? '>' : ' ', a, prog_fetch(s, a));
                }
                C54_LOG("  ST0=0x%04x ST1=0x%04x PMST=0x%04x SP=0x%04x INTM=%d",
                        s->st0, s->st1, s->pmst, s->sp, !!(s->st1 & ST1_INTM));
                C54_LOG("  A=0x%010llx B=0x%010llx T=0x%04x",
                        (unsigned long long)(s->a & 0xFFFFFFFFFFLL),
                        (unsigned long long)(s->b & 0xFFFFFFFFFFLL), s->t);
                C54_LOG("  AR0=%04x AR1=%04x AR2=%04x AR3=%04x AR4=%04x AR5=%04x AR6=%04x AR7=%04x",
                        s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                        s->ar[4], s->ar[5], s->ar[6], s->ar[7]);
            }
        }

        /* BSP read entry points: these routines contain PORTR PA=0xF430 (read
         * a BSP sample), so a DSP that never visits them has a dead FB-det
         * chain. Targets come from static analysis of the PROM0 callers of the
         * 64 PORTR PA=0xF430 sites at 0x9b80+. */
        if (!s->rpt_active &&
            (s->pc == 0x9a78 || s->pc == 0x9aaf || s->pc == 0x9ad3 ||
             s->pc == 0x9b4c || s->pc == 0x8811)) {
            static unsigned bsp_visits[5];
            int idx = (s->pc == 0x9a78) ? 0 :
                      (s->pc == 0x9aaf) ? 1 :
                      (s->pc == 0x9ad3) ? 2 :
                      (s->pc == 0x9b4c) ? 3 : 4;
            if (bsp_visits[idx] < 5) {
                bsp_visits[idx]++;
                C54_LOG("BSP-ENTRY PC=0x%04x  A=0x%010llx ar0=%04x ar1=%04x "
                        "ar2=%04x ar3=%04x ar4=%04x SP=0x%04x insn=%u",
                        s->pc,
                        (unsigned long long)(s->a & 0xFFFFFFFFFFLL),
                        s->ar[0], s->ar[1], s->ar[2], s->ar[3], s->ar[4],
                        s->sp, s->insn_count);
            }
        }

        /* Writes to the dispatcher poll addresses data[0x4359] / data[0x3fab]
         * are traced in data_write. */

        /* Dispatcher hot loop at PROM0 0xb968-0xb9a4: the state machine the
         * DSP spins in while waiting for ARM tasks. Logs the first 8 visits
         * per PC, enough to show the whole conditional structure - which
         * addresses it polls and which constants it compares against. */
        if (s->pc >= 0xb968 && s->pc <= 0xb9a4 && !s->rpt_active) {
            static uint8_t disp_visits[64];
            int idx = s->pc - 0xb968;
            if (idx >= 0 && idx < 64 && disp_visits[idx] < 8) {
                disp_visits[idx]++;
                C54_LOG("DISP-TRACE PC=0x%04x op=0x%04x A=0x%010llx "
                        "B=0x%010llx ar0=%04x ar1=%04x ar2=%04x ar3=%04x "
                        "ar4=%04x ar5=%04x TC=%d",
                        s->pc, prog_fetch(s, s->pc),
                        (unsigned long long)(s->a & 0xFFFFFFFFFFLL),
                        (unsigned long long)(s->b & 0xFFFFFFFFFFLL),
                        s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                        s->ar[4], s->ar[5],
                        !!(s->st0 & ST0_TC));
            }
        }

        /* IRQ vector area 0xFFCC-0xFFE0 (INT3, TINT0 and BRINT0 slots):
         * captures the three 4-word handlers the INT3 dispatch lands on at
         * IPTR=0x1ff. First 4 visits per PC. */
        if (s->pc >= 0xFFCC && s->pc < 0xFFE0 && !s->rpt_active) {
            static uint8_t vec_visits[20];   /* index 0 = 0xffcc */
            int idx = s->pc - 0xFFCC;
            if (vec_visits[idx] < 4) {
                vec_visits[idx]++;
                C54_LOG("VEC-TRACE PC=0x%04x op=0x%04x SP=0x%04x A=0x%010llx "
                        "B=0x%010llx TC=%d INTM=%d ar7=%04x",
                        s->pc, prog_fetch(s, s->pc), s->sp,
                        (unsigned long long)(s->a & 0xFFFFFFFFFFLL),
                        (unsigned long long)(s->b & 0xFFFFFFFFFFLL),
                        !!(s->st0 & ST0_TC),
                        !!(s->st1 & ST1_INTM),
                        s->ar[7]);
            }
        }

        /* DSP init: one line per distinct PC in 0xE900-0xE960. */
        if (s->pc >= 0xE900 && s->pc < 0xE960 && !s->rpt_active) {
            static uint16_t seen_pcs[96];
            int idx = s->pc - 0xE900;
            if (!seen_pcs[idx]) {
                seen_pcs[idx] = 1;
                C54_LOG("INIT PC=0x%04x op=0x%04x SP=0x%04x BRC=%d rptb=%d RSA=0x%04x REA=0x%04x",
                        s->pc, prog_fetch(s, s->pc), s->sp, s->brc,
                        s->rptb_active, s->rsa, s->rea);
            }
        }

        /* SINT17 handler (0x8a00-0x8a5f). */
        if (s->pc >= 0x8a00 && s->pc < 0x8a60) {
            static int sint17_log = 0;
            if (sint17_log < 500) {
                C54_LOG("SINT17 PC=0x%04x op=0x%04x SP=0x%04x DP=0x%03x A=0x%010llx B=0x%010llx AR0=%04x",
                        s->pc, prog_fetch(s, s->pc), s->sp, dp(s),
                        (unsigned long long)(s->a & 0xFFFFFFFFFFLL),
                        (unsigned long long)(s->b & 0xFFFFFFFFFFLL), s->ar[0]);
                sint17_log++;
            }
        }

        /* Sample the PC every 1M instructions, to find stuck loops. */
        if (executed > 0 && (executed % 1000000) == 0) {
            static int sample_log = 0;
            if (sample_log < 20)
                C54_LOG("@%dM: PC=0x%04x op=0x%04x SP=0x%04x insn=%u",
                        executed/1000000, s->pc, prog_read(s, s->pc), s->sp, s->insn_count);
            sample_log++;
        }
        if (run_num <= 2 && executed < 2000) {
            C54_LOG("BOOT[%d.%d] PC=0x%04x op=0x%04x SP=0x%04x A=0x%010llx B=0x%010llx",
                    run_num, executed, s->pc, prog_fetch(s, s->pc), s->sp,
                    (unsigned long long)(s->a & 0xFFFFFFFFFFLL),
                    (unsigned long long)(s->b & 0xFFFFFFFFFFLL));
        }
        /* The RPTB end-of-body check lives below, after `s->pc += consumed`,
         * so the redirect to RSA is the last write to PC in this iteration.
         * Running it before the PC advance is off by one: the redirect sets
         * pc=RSA, then `s->pc += consumed` bumps it to RSA+1, and the first
         * body instruction is never re-executed (the PC histogram then shows
         * the body as [RSA+1..REA+1] instead of [RSA..REA]). */

        /* RPTB entry at 0x76FD: dump all AR values. */
        if (s->pc == 0x76FD) {
            static int rptb_entry_log = 0;
            if (rptb_entry_log < 30)
                C54_LOG("RPTB-ENTRY PC=0x76FD AR0=%04x AR1=%04x AR2=%04x AR3=%04x AR4=%04x AR5=%04x AR6=%04x AR7=%04x ARP=%d DP=%d BRC=%d SP=%04x",
                        s->ar[0], s->ar[1], s->ar[2], s->ar[3], s->ar[4], s->ar[5], s->ar[6], s->ar[7],
                        arp(s), dp(s), s->brc, s->sp);
            rptb_entry_log++;
        }
        if (s->pc == 0x03F0) {
            static int f3_log = 0;
            if (f3_log < 2) {
                C54_LOG("PC=0x03F0 op=0x%04x insn=%u SP=0x%04x IMR=0x%04x XPC=%d PMST=0x%04x",
                        prog_fetch(s, s->pc), s->insn_count, s->sp, s->imr, s->xpc, s->pmst);
                C54_LOG("  trail: %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x",
                        pc_ring[(pc_ring_idx-⟨1⟩)&255], pc_ring[(pc_ring_idx-⟨2⟩)&255],  ×9
    ⟨⟩ = (20,19) (18,17) (16,15) (14,13) (12,11) (10,9) (8,7) (6,5) (4,3)
                        pc_ring[(pc_ring_idx-2)&255], pc_ring[(pc_ring_idx-1)&255]);
                f3_log++;
            }
        }

        /* Boot trace. */
        if (g_boot_trace > 0) {
            C54_LOG("BOOT[%d] PC=0x%04x op=0x%04x SP=0x%04x PMST=0x%04x",
                    51 - g_boot_trace, s->pc, prog_fetch(s, s->pc), s->sp, s->pmst);
            g_boot_trace--;
        }
        }   /* C54X_SONDES : EB04 .. BOOT trace */

        /* Execute the instruction. */
        int consumed;
        uint16_t exec_pc = s->pc;
        /* [2026-09-23] Sondes pures FIRS-BANK, CORR-SLIDE, TERMINAL-DISP. */
        if (C54X_SONDES) {
        /* FIRS-BANK (CALYPSO_FIRS_BANK, default OFF, read-only). Is the PROM0
         * polyphase filter bank entered through its prologue or in the middle?
         * Eight FIRS in four pairs (pmad 0x64, 0x63, 0x62, 0x61), preceded by
         * a prologue that loads AR2/AR5 <- 0x0060 and by a six-entry dispatch
         * table at 0x8359..0x8363. Measured over two runs: the first FIRS
         * executed is always 0x8478 (the last stage), and nothing writes the
         * buffer 0x0060..0x0066, which is the FIRS Xmem. Counting each site
         * separates "never reached" from "reached, then silent". */
        {
            static int _fb = -1;
            if (_fb < 0) {
                _fb = calypso_gate("CALYPSO_FIRS_BANK", 0);
                fprintf(stderr, "[c54x] FIRS-BANK %s : prologue (0x8336/0x834b/0x834f), "
                        "table de dispatch (0x8359..0x8363) et six etages du banc\n",
                        _fb ? "ACTIVE" : "INACTIVE (defaut)");
            }
            if (_fb) {
                static const uint16_t sites[] = {
                    0x8336, 0x834b, 0x834f,                       /* prologue */
                    0x8359, 0x835b, 0x835d, 0x835f, 0x8361, 0x8363, /* dispatch */
                    0x8365, 0x8394, 0x83c9, 0x83ff, 0x8435, 0x846b  /* stages   */
                };
                static const char *quoi[] = {
                    "PROLOGUE stm #0x60,AR2", "PROLOGUE stm #0x60,AR5", "PROLOGUE branche table",
                    "DISPATCH->0x8365", "DISPATCH->0x8394", "DISPATCH->0x83c9",
                    "DISPATCH->0x83ff", "DISPATCH->0x8435", "DISPATCH->0x846b",
                    "ETAGE 0x8365", "ETAGE 0x8394", "ETAGE 0x83c9",
                    "ETAGE 0x83ff", "ETAGE 0x8435", "ETAGE 0x846b"
                };
                static unsigned long long hits[15];
                static unsigned shown[15];
                for (int k = 0; k < 15; k++) {
                    if (exec_pc != sites[k]) continue;
                    hits[k]++;
                    if (shown[k] < 3) {
                        shown[k]++;
                        fprintf(stderr, "[c54x] FIRS-BANK 0x%04x %-24s #%llu "
                                "AR2=0x%04x AR3=0x%04x AR5=0x%04x AR6=0x%04x insn=%u\n",
                                sites[k], quoi[k], (unsigned long long)hits[k],
                                s->ar[2], s->ar[3], s->ar[5], s->ar[6], s->insn_count);
                    }
                    break;
                }
                {   /* periodic summary: which sites were reached, which never */
                    static unsigned long long tick = 0;
                    if ((++tick % 4000000) == 0) {
                        fprintf(stderr, "[c54x] FIRS-BANK bilan :");
                        for (int k = 0; k < 15; k++)
                            fprintf(stderr, " %04x=%llu", sites[k],
                                    (unsigned long long)hits[k]);
                        fprintf(stderr, "\n");
                    }
                }
            }
        }
        /* CORR-SLIDE (CALYPSO_CORR_SLIDE, default OFF, read-only). Does the
         * correlator window actually slide? Measured: only 3 to 4 distinct
         * outputs over 50 offsets while the burst buffer is live, i.e. the
         * outputs come in identical runs. Shows AR1..AR5 and the accumulators
         * at the three key points of each RPTB 0x84b0..0x84c6 iteration. */
        {
            static int _cs = -1;
            if (_cs < 0) {
                _cs = calypso_gate("CALYPSO_CORR_SLIDE", 0);
                fprintf(stderr, "[c54x] CORR-SLIDE %s : AR1..AR5 + accumulateurs en "
                        "0x84b4 (entree tour), 0x84bc (store A) et 0x84c5 (store B)\n",
                        _cs ? "ACTIVE" : "INACTIVE (defaut)");
            }
            if (_cs && (exec_pc == 0x84b4 || exec_pc == 0x84bc || exec_pc == 0x84c5)) {
                static unsigned _n = 0;
                if (_n < 24) {
                    _n++;
                    const char *ou = (exec_pc == 0x84b4) ? "entree tour" :
                                     (exec_pc == 0x84bc) ? "store A    " : "store B    ";
                    fprintf(stderr, "[c54x] CORR-SLIDE #%2u 0x%04x %s | AR0=%d "
                            "AR1=0x%04x AR2=0x%04x AR3=0x%04x AR4=0x%04x AR5=0x%04x "
                            "| A=0x%010llx B=0x%010llx BK=0x%04x insn=%u\n",
                            _n, exec_pc, ou, (int)(int16_t)s->ar[0],
                            s->ar[1], s->ar[2], s->ar[3], s->ar[4], s->ar[5],
                            (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                            (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                            s->bk, s->insn_count);
                }
            }
        }
        if (exec_pc == 0xb40e || exec_pc == 0xb40f) {
            static unsigned tn = 0;
            if (tn++ < 8)
                fprintf(stderr, "[c54x] TERMINAL-DISP PC=0x%04x AR7=0x%04x data[AR7]=0x%04x "
                        "data[0x4387]=0x%04x data[0x43c0]=0x%04x A=0x%06llx SP=0x%04x insn=%u\n",
                        exec_pc, s->ar[7], s->data[s->ar[7]],
                        s->data[0x4387], s->data[0x43c0],
                        (unsigned long long)(s->a & 0xFFFFFFULL), s->sp, s->insn_count);
        }
        }   /* C54X_SONDES : FIRS-BANK .. TERMINAL-DISP */
        /* OVLY-TRACE: the frame handler 0x013b..0x0160 (DARAM overlay) derails
         * at the RET in 0x0157 with an empty stack. Traces pc/op/sp of the
         * first pass and dumps the overlay contents, to locate the imbalance
         * (a PSHM with no matching POPM, or a branch wrongly taken before the
         * POPMs). One-shot, one frame.
         * The probe (OVLY-DUMP / OVLY-TRACE) is behind the switch; the
         * TEST_3FCD crutch it contains stays active. */
        if (exec_pc >= 0x0100 && exec_pc <= 0x0160) {
            static unsigned ot = 0; static int dumped = 0;
            if (C54X_SONDES && !dumped) {
                dumped = 1;
                fprintf(stderr, "[c54x] OVLY-DUMP data[0x0100..0x0160]:");
                for (int a = 0x0100; a <= 0x0160; a++) fprintf(stderr, " %04x", s->data[a]);
                fprintf(stderr, "\n");
            }
            if (exec_pc == 0x0154) {
                /* @BEQUILLE - TEST_3FCD  (CALYPSO_TEST_3FCD, EXISTS, default OFF)
                 *   masks   : data[0x3fcd], the address the RET @0x0157 pops, is never
                 *             written; the firmware installs a handler in the neighbouring
                 *             cell data[0x3fce] (= 0xdf82). At the PSHD (0x0154) one is
                 *             derived from the other, which tests whether 0xdf82 is the
                 *             right target, i.e. whether the overlay vector table sits at
                 *             the wrong offset.
                 *   remove  : as soon as the overlay vector table is installed at the right
                 *             offset (data[0x3fcd] non-zero without forcing), or at once if
                 *             FIX_3FCD (same cell, PC 0x013b) is kept as the single
                 *             mechanism.
                 */
                static int t3 = -1;
                if (t3 < 0) t3 = calypso_gate("CALYPSO_TEST_3FCD", 0);
                if (t3 && s->data[0x3fcd] == 0 && s->data[0x3fce] != 0) {
                    static unsigned tn3 = 0;
                    if (tn3++ < 4)
                        fprintf(stderr, "[c54x] TEST-3FCD: data[0x3fcd] 0x0000 -> 0x%04x (=data[0x3fce]) insn=%u\n",
                                s->data[0x3fce], s->insn_count);
                    s->data[0x3fcd] = s->data[0x3fce];
                }
            }
            if (C54X_SONDES && ot++ < 90)
                fprintf(stderr, "[c54x] OVLY-TRACE pc=0x%04x op=0x%04x sp=0x%04x A=0x%06llx insn=%u\n",
                        exec_pc, prog_fetch(s, exec_pc), s->sp,
                        (unsigned long long)(s->a & 0xFFFFFFULL), s->insn_count);
        }
        if (exec_pc == 0xa4e4) {
            /* @BEQUILLE - INIT_435B (+ SEED_52FD)  (CALYPSO_INIT_435B_OFF=0 => ACTIVE;
             *              CALYPSO_SEED_52FD picks the value; the four .env profiles set 0)
             *   masks   : the DSP boot's initialisation of the shadow IMR data[0x435b].
             *             Task handlers OR and AND bits into that cell (the correlator at
             *             0xbd3c does ORM 0x10, and so on) and the go-live state machine
             *             0xa501/0xa582 propagates it into IMR. Nothing ever writes it here
             *             (STATE435B-WR stays empty), so IMR ends up 0 and the machine
             *             deadlocks. Injects 0x52ed, or 0x52fd under SEED_52FD, at
             *             exec_pc == 0xa4e4.
             *   remove  : when a firmware write to 0x435b is observed before 0xa4e4.
             *   TRAP    : the name says _OFF, but "=0" ENABLES it.
             */
            static int i435 = -1;
            if (i435 < 0) { const char *_e435 = calypso_getenv("CALYPSO_INIT_435B_OFF"); i435 = (_e435 && atoi(_e435)) ? 0 : 1; }  /* gate tests the VALUE: OFF=0 means active */
            if (i435 && s->data[0x435b] == 0) {
                static unsigned in = 0;
                if (in++ < 4)
                    fprintf(stderr, "[c54x] INIT-435B: data[0x435b] 0x0000 -> 0x52ed (masque IMR reset SANS bit4/clobber) insn=%u\n", s->insn_count);
                s->data[0x435b] = calypso_getenv("CALYPSO_SEED_52FD") ? 0x52fd : 0x52ed;   /* default 0x52ed: without bit 4 (TINT), which avoids the firmware clobber at 0xa509 that strips bit 12 (frame). 0x52fd sets bit 4 and breaks the frame, which shows the firmware does not use TINT0 */
            }
        }
        /* Pure probes SM-TRACE .. A4CD-BC (including CALA-WIDE, which does a
         * prog_fetch on every PROM0 instruction). */
        if (C54X_SONDES) {
        /* SM-TRACE: full path of the go-live state machine 0xa4e4-0xa5b5 once
         * d_dsp_page is aligned - where does it branch or loop back? The
         * deciding values are d_dsp_page (0x3fb0), data[0x09bc] (ARM flag) and
         * A (the dispatch target). */
        if (exec_pc >= 0xa4e4 && exec_pc <= 0xa5b8) {
            static unsigned st = 0;
            if (st++ < 70)
                fprintf(stderr, "[c54x] SM-TRACE pc=0x%04x op=0x%04x A=0x%06llx TC=%d "
                        "d[3fb0]=%04x d[09bc]=%04x d[3fe0]=%04x d[435b]=%04x insn=%u\n",
                        exec_pc, prog_fetch(s, exec_pc), (unsigned long long)(s->a & 0xFFFFFFULL),
                        (s->st0 & ST0_TC) ? 1 : 0, s->data[0x3fb0], s->data[0x09bc],
                        s->data[0x3fe0], s->data[0x435b], s->insn_count);
        }
        /* TERM-TRACE: the AR7 index computation at the mask-ROM terminal
         * 0xb405-0xb412. Does AR7 end up 0x4387 (idle) instead of 0x43c0
         * (go-live), i.e. is the result of LD #0x39 (0xb408) plus ADD #0x4387
         * (0xb409) lost or mis-stored into AR7? Logs A and AR0..7 at every
         * instruction of the zone. Gate CALYPSO_TERM_TRACE_OFF. */
        {
            static int _tt = -1;
            if (_tt < 0) _tt = calypso_getenv("CALYPSO_TERM_TRACE_OFF") ? 0 : 1;
            if (_tt && exec_pc >= 0xb400 && exec_pc <= 0xb414) {
                static unsigned _ttn = 0;
                if (_ttn++ < 60)
                    fprintf(stderr, "[c54x] TERM-TRACE pc=0x%04x op=0x%04x A=0x%06llx "
                            "AR[0..7]=%04x %04x %04x %04x %04x %04x %04x %04x insn=%u\n",
                            exec_pc, prog_fetch(s, exec_pc), (unsigned long long)(s->a & 0xFFFFFFULL),
                            s->ar[0], s->ar[1], s->ar[2], s->ar[3], s->ar[4], s->ar[5], s->ar[6], s->ar[7],
                            s->insn_count);
            }
        }
        /* CALA-TRACE-WIDE: every computed transfer (CALA/CALAD/BACC/FBACC/
         * FCALA/FCALAD - f4e2/f4e3/f4e6/f4e7/f5e2/f5e3/f5e6/f5e7/f6e6/f6e7)
         * over the whole PROM0 range, not just CALA over fragments. The point
         * is to catch ANY computed jump landing in the correlator range
         * (CORR_PC_LO..CORR_PC_HI); three static-scan passes failed to find
         * the reference. Two counters: hits inside the range are never capped
         * (strong signal), general hits are capped at 200 for context. */
        {
            static int _ctw = -1;
            if (_ctw < 0) { const char *_e = calypso_getenv("CALYPSO_D247_TRACE_OFF"); _ctw = (_e && atoi(_e)) ? 0 : 1; }
            if (_ctw && exec_pc >= 0x7000 && exec_pc <= 0xdfff) {   /* all of PROM0, no arbitrary sub-range */
                uint16_t _cop = prog_fetch(s, exec_pc);
                bool _is_xfer = (_cop==0xf4e2||_cop==0xf4e3||_cop==0xf4e6||_cop==0xf4e7||
                                  _cop==0xf5e2||_cop==0xf5e3||_cop==0xf5e6||_cop==0xf5e7||
                                  _cop==0xf6e6||_cop==0xf6e7);
                if (_is_xfer) {
                    uint16_t _tgt = (uint16_t)(s->a & 0xFFFF);
                    bool _in_corr = (_tgt >= CORR_PC_LO && _tgt < CORR_PC_HI);
                    if (_in_corr) {
                        fprintf(stderr, "[c54x] CALA-WIDE *** DANS-CORRELATEUR *** pc=0x%04x op=0x%04x "
                                "-> target=0x%04x task_md p0(0804)=%04x p1(0818)=%04x d_dsp_page(08e2)=%04x "
                                "d[4357]=%04x insn=%u\n",
                                exec_pc, _cop, _tgt, s->data[0x0804], s->data[0x0818],
                                s->data[0x08e2], s->data[0x4357], s->insn_count);
                    } else {
                        static unsigned _ctwn = 0;
                        if (_ctwn++ < 200)
                            fprintf(stderr, "[c54x] CALA-WIDE pc=0x%04x op=0x%04x -> target=0x%04x insn=%u\n",
                                    exec_pc, _cop, _tgt, s->insn_count);
                    }
                }
            }
        }
        /* INSTALL-TRACE: the 0xc7xx block installs the task handler table
         * (STL A -> d[4c5c] at 0xc803), and d[4c5c] == 0 means the FB
         * correlator is never dispatched. Says whether the block is reached
         * and what A holds at 0xc803; the neighbouring literals d[4c5a] and
         * d[4c5d] confirm the block ran. Gate CALYPSO_INSTALL_TRACE_OFF. */
        {
            static int _it = -1;
            if (_it < 0) _it = calypso_getenv("CALYPSO_INSTALL_TRACE_OFF") ? 0 : 1;
            if (_it && (exec_pc==0xc7fa || exec_pc==0xc801 || exec_pc==0xc803 ||
                        exec_pc==0xc805 || exec_pc==0xc7e2 || exec_pc==0xc827)) {
                static unsigned _itn = 0;
                if (_itn++ < 30)
                    fprintf(stderr, "[c54x] INSTALL-TRACE pc=0x%04x A=0x%04x "
                            "d[4c5a]=%04x d[4c5c]=%04x d[4c5d]=%04x d[3f5e]=%04x insn=%u\n",
                            exec_pc, (uint16_t)(s->a & 0xFFFF),
                            s->data[0x4c5a], s->data[0x4c5c], s->data[0x4c5d],
                            s->data[0x3f5e], s->insn_count);
            }
        }
        /* BACC-C827-SRC: where does the jump to 0xc827 come from, the one that
         * skips the handler table install 0xc7a0-0xc825? Records prev_pc, its
         * opcode, A and the ARs whenever 0xc827 is entered without
         * fall-through (prev is neither 0xc825 nor 0xc826). Gate
         * CALYPSO_BACC_C827_OFF. */
        {
            static uint16_t _pp827 = 0;
            static int _bsc = -1;
            if (_bsc < 0) _bsc = calypso_getenv("CALYPSO_BACC_C827_OFF") ? 0 : 1;
            if (_bsc && exec_pc == 0xc827 && _pp827 != 0xc825 && _pp827 != 0xc826 && _pp827 != 0xc827) {
                static unsigned _bn = 0;
                if (_bn++ < 15)
                    fprintf(stderr, "[c54x] BACC-C827-SRC from=0x%04x op@from=0x%04x A=0x%04x "
                            "AR[0..7]=%04x %04x %04x %04x %04x %04x %04x %04x insn=%u\n",
                            _pp827, prog_fetch(s, _pp827), (uint16_t)(s->a & 0xFFFF),
                            s->ar[0], s->ar[1], s->ar[2], s->ar[3], s->ar[4], s->ar[5], s->ar[6], s->ar[7],
                            s->insn_count);
            }
            _pp827 = exec_pc;
        }

/* PHASE-SM: the d[3f70] state machine (go-live -> operational).
         * 0xddeb does LD d[0x098a] then BC on A==0, which resets phase to 0;
         * 0xde86 does LD d[0x098c]; 0xde9c does ST #2. d[0x098a] and d[0x098c]
         * are the ARM handshake cells: the ARM must set them non-zero for the
         * machine to reach d[3f70] = 2. Logs the decision points and the
         * values. Gate CALYPSO_PHASE_SM_OFF. */
        {
            static int _ps = -1;
            if (_ps < 0) _ps = calypso_getenv("CALYPSO_PHASE_SM_OFF") ? 0 : 1;
            if (_ps && (exec_pc==0xddeb || exec_pc==0xde86 || exec_pc==0xde97 ||
                        exec_pc==0xde9c || exec_pc==0xde8b || exec_pc==0xdea8 || exec_pc==0xdddb)) {
                static unsigned _psn = 0;
                if (_psn++ < 40)
                    fprintf(stderr, "[c54x] PHASE-SM pc=0x%04x A=0x%04x d[3f70]=%04x "
                            "d[098a]=%04x d[098b]=%04x d[098c]=%04x d[098d]=%04x d[0fff]=%04x insn=%u\n",
                            exec_pc, (uint16_t)(s->a & 0xFFFF), s->data[0x3f70],
                            s->data[0x098a], s->data[0x098b], s->data[0x098c], s->data[0x098d],
                            s->data[0x0fff], s->insn_count);
                  /* Effective address of the LD. Direct addressing on the
                   * C54x is DP-relative: dma = (DP << 7) | offset7. At 0xde86
                   * the cell d[0x098c] holds 1 (seeded by BGEN) and yet A
                   * comes out 0x0000, 38 times in a row. Printing DP, the
                   * opcode and the computed address decides between "DP points
                   * at another page" and "right page, empty cell". Read-only,
                   * no behaviour change. */
                  {
                      uint16_t _op  = prog_fetch(s, exec_pc);
                      uint16_t _lk  = prog_fetch(s, (uint16_t)(exec_pc + 1));
                      int      _ind = (_op & 0x80) ? 1 : 0;
                      int      _mod = (_op >> 3) & 0x0F;
                      /* mod 0xF = *(lk): the address IS the long word. lk is
                       * printed for every other mode too - it costs nothing and
                       * avoids guessing again. */
                      static unsigned _ean = 0;
                      /* Cap: without it this probe floods the log (480898
                       * lines measured) and the truncation wipes the others.
                       * 40 lines, then one every 100000. */
                      _ean++;
                      if (_ean <= 40 || (_ean % 100000) == 0)
                      fprintf(stderr, "[c54x] PHASE-SM-EA #%u pc=0x%04x op=0x%04x ind=%d mod=0x%x "
                              "lk=0x%04x d[lk]=0x%04x AR0=0x%04x A=0x%04x insn=%u\n",
                              _ean, exec_pc, _op, _ind, _mod, _lk, s->data[_lk],
                              s->ar[0], (uint16_t)(s->a & 0xFFFF), s->insn_count);
                  }
            }
        }
        if (exec_pc == 0xa51c) {   /* go-live SM reads d_dsp_page @0x08d4 */
            static unsigned dp=0;
            if (dp++ < 12)
                fprintf(stderr, "[c54x] SM-DPAGE @0xa51c data[0x08d4]=0x%04x data[0x08E2]=0x%04x "
                        "data[0x435b]=0x%04x A=0x%06llx insn=%u\n",
                        s->data[0x08d4], s->data[0x08E2], s->data[0x435b],
                        (unsigned long long)(s->a & 0xFFFFFFULL), s->insn_count);
        }
        if (exec_pc == 0xa4cd) {   /* BC AEQ (0xf845): why is A == 0, which skips the RSBX INTM at 0xa4d0?
                                    * 0xaad5 reads AR0=data[0x434e], AR1=data[0x434f] and computes A. */
            static unsigned an = 0;
            if (an++ < 16)
                fprintf(stderr, "[c54x] A4CD-BC A=0x%06llx (AEQ %s -> %s) "
                        "d[434e]=%04x d[434f]=%04x d[*434e]=%04x d[*434f]=%04x d[3f70]=%04x insn=%u\n",
                        (unsigned long long)(s->a & 0xFFFFFFULL),
                        (s->a & 0xFFFFFFFFFFULL) == 0 ? "vrai" : "faux",
                        (s->a & 0xFFFFFFFFFFULL) == 0 ? "BRANCHE(skip enable)" : "fall-through(RSBX INTM)",
                        s->data[0x434e], s->data[0x434f],
                        s->data[s->data[0x434e] & 0xFFFF], s->data[s->data[0x434f] & 0xFFFF],
                        s->data[0x3f70], s->insn_count);
        }
        }   /* C54X_SONDES : SM-TRACE .. A4CD-BC */
        if (exec_pc == 0xb40f) {
            /* @BEQUILLE - MASKROM_GOLIVE  (CALYPSO_MASKROM_GOLIVE, EXISTS, default OFF)
             *   masks   : the launch vector mem[0x5ac8] at the stack base, pre-loaded on
             *             silicon by a mask-ROM absent from the dump. At 0, the RET of the
             *             idle BACC jumps to PC=0 (storm). The value is derived, not
             *             invented: mem[0x5ac8] = data[0x43c0], the go-live pointer the
             *             firmware itself wrote (0xa4c7 = ORM #0x3000,IMR, which arms the
             *             IMR).
             *   remove  : ALREADY REDUNDANT - the LD #k8u decode fix kills the storm
             *             natively (the stray << 16 set AR7 to 0x4387, idle, instead of
             *             0x43c0, go-live). This is now only an A/B guard; drop it.
             */
            static int mrg = -1;
            if (mrg < 0) mrg = calypso_gate("CALYPSO_MASKROM_GOLIVE", 0);
            if (mrg && s->data[0x5ac8] == 0 && s->data[0x43c0] != 0) {
                static unsigned mgn = 0;
                if (mgn++ < 4)
                    fprintf(stderr, "[c54x] MASKROM-GOLIVE: mem[0x5ac8] 0x0000 -> 0x%04x "
                            "(=data[0x43c0], pointeur go-live firmware) insn=%u\n",
                            s->data[0x43c0], s->insn_count);
                s->data[0x5ac8] = s->data[0x43c0];
            }
        }
        uint16_t exec_op = prog_fetch(s, s->pc);
        /* AR5-BAS: in a call, 0xeac9 `MVDD *AR2+,*AR5+` (under RPT #16) can
         * overwrite SP (SP-CORRUPT sp 0x5ac0 -> 0x0004) when AR5 points at the
         * MMRs. AR5 comes from the caller (PDROM 0xe88d..0xe8a9: MVDM 0x2d0d,AR5
         * then CALLD 0xea0c, MAR *+AR5(lk) in the delay slot). The state is
         * captured BEFORE execution, when AR5 is aberrant. */
        if (exec_pc == 0xeac9 && s->ar[5] < 0x0100) {
            static unsigned ar5b_n = 0;
            if (ar5b_n++ < 8) {
                uint16_t sp = s->sp;
                fprintf(stderr,
                    "[c54x] AR5-BAS pc=0xeac9 insn=%u AR0=%04x AR1=%04x AR2=%04x AR3=%04x "
                    "AR4=%04x AR5=%04x AR6=%04x AR7=%04x BK=%04x SP=%04x "
                    "pile=%04x %04x %04x %04x data[2d0d]=%04x rpt=%d/%u XPC=%u\n",
                    s->insn_count, s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                    s->ar[4], s->ar[5], s->ar[6], s->ar[7], s->bk, sp,
                    s->data[sp], s->data[(uint16_t)(sp + 1)],
                    s->data[(uint16_t)(sp + 2)], s->data[(uint16_t)(sp + 3)],
                    s->data[0x2d0d], s->rpt_active, (unsigned)s->rpt_count,
                    (unsigned)s->xpc);
            }
        }
        /* ANNEAU-EXEC: state BEFORE the instruction (see sp_corrupt_vidage).
         * Evaluated on every pass, hence on every RPT iteration, like AR5-BAS
         * just above. */
        C54xAnneauExec *ev_anneau = &g_anneau_exec[g_anneau_exec_idx++ % C54X_ANNEAU_N];
        ev_anneau->insn = s->insn_count; ev_anneau->pc = exec_pc; ev_anneau->op = exec_op;
        ev_anneau->xpc = (uint8_t)s->xpc; ev_anneau->sp = s->sp; ev_anneau->sp_apres = s->sp;
        ev_anneau->ar2 = s->ar[2]; ev_anneau->ar5 = s->ar[5]; ev_anneau->st0 = s->st0; ev_anneau->ar3 = s->ar[3]; ev_anneau->ar4 = s->ar[4]; ev_anneau->brc = s->brc;
        ev_anneau->rpt = s->rpt_active; ev_anneau->rpt_count = s->rpt_count;
        ev_anneau->intm = !!(s->st1 & ST1_INTM);
        ev_anneau->it = 0; ev_anneau->it_n = (uint16_t)g_c54x_it_prises;
        /* [trou-prog] a PC sliding through the empty program memory 0x2800..0x6fff (NOP) up to the
         * reset at 0x7000 -> DSP init, TCH state -1, DSP lost. Logged: the jump leading there. */
        /* [b81e] bitf *ar2(9),#0x8000 -> b829 orm #256,*(0x3fab): base 0xde57 of the TCH table.
         * Logged: which page AR2 designates when the bit is set. */
        if (exec_pc == 0xb81e && s->insn_count > 3000000) {
            static unsigned n_b81e;
            uint16_t a2 = s->ar[2];
            if (n_b81e++ < 60 || (s->data[(uint16_t)(a2 + 9)] & 0x8000))
                printf("  [b81e] ar2=%04x mot9=%04x 3fc1=%04x W0[9]=%04x W1[9]=%04x 3fba=%d 4364=%u xpc=%u insn=%u\n",
                       a2, s->data[(uint16_t)(a2 + 9)], s->data[0x3fc1], s->data[0x0809], s->data[0x081d],
                       (int16_t)s->data[0x3fba], s->data[0x4364], s->xpc, s->insn_count);
        }
        /* + the register zone (pc < 0x60): jump to 0x0000 xpc=1 then ret in a loop, SP-CORRUPT. */
        if (((exec_pc >= 0x2800 && exec_pc < 0x7000) || exec_pc < 0x0060) && s->insn_count > 3000000) {
            static unsigned n_trou; static uint16_t pc_prec_trou;
            uint16_t pp = g_anneau_exec[(g_anneau_exec_idx - 2) % C54X_ANNEAU_N].pc;
            bool pp_trou = (pp >= 0x2800 && pp < 0x7000) || pp < 0x0060;
            if (!pp_trou && n_trou < 4) {
                n_trou++;
                printf("  [trou-prog] entree dans la memoire vide pc=0x%04x depuis pc=0x%04x xpc=%u pmst=%04x SP=%04x pile=%04x %04x %04x insn=%u\n",
                       exec_pc, pp, s->xpc, s->pmst, s->sp, s->data[s->sp], s->data[(uint16_t)(s->sp + 1)],
                       s->data[(uint16_t)(s->sp + 2)], s->insn_count);
                c54x_anneau_vider_stdout("trou-prog", 512);   /* [2026-10-22 22:20] 120 -> 512 : remonter jusqu a l IT */
            }
            (void)pc_prec_trou;
        }
        /* Repeat state BEFORE execution: tells "the repeat was already
         * active" from "the instruction just executed IS the RPT that armed
         * it". Consumed by the RPT block at the end of the loop. */
        bool rpt_was_active = s->rpt_active;
        /* [2026-09-23] Sondes pures FBWATCH-*, derriere l'interrupteur. */
        if (C54X_SONDES) {
        /* FBWATCH-ALIVE canary: proves the probe family is armed, and samples
         * the foreground PC. If this line appears, g_fbwatch_on is 1 and the
         * silence of the other FBWATCH probes is real; if it does not, they
         * were simply dead. Fires every ~20M insns (~10 lines per run). */
        if (g_fbwatch_on > 0 && (s->insn_count % 20000000u) == 0) {
            fprintf(stderr, "[c54x] FBWATCH-ALIVE insn=%u PC=0x%04x INTM=%d SP=0x%04x\n",
                    s->insn_count, exec_pc, !!(s->st1 & ST1_INTM), s->sp);
        }
        /* FBWATCH-POLL: the foreground polls a flag through BITF at 0xf7af /
         * 0xf7b7 (RC NTC loops while TC == 0). Captures the flag address
         * (AR0..AR2 plus the data they point at) and TC, to identify the bit
         * that is never set, i.e. the hardware still to be modelled. */
        if (g_fbwatch_on > 0 && (exec_pc == 0xf7af || exec_pc == 0xf7b7)) {
            static unsigned wpoll = 0;
            if (wpoll++ < 30) {
                uint16_t mask = prog_fetch(s, exec_pc + 1);
                fprintf(stderr, "[c54x] FBWATCH-POLL pc=0x%04x mask=0x%04x | "
                        "AR0=0x%04x d=0x%04x | AR1=0x%04x d=0x%04x | AR2=0x%04x d=0x%04x | TC=%d insn=%u\n",
                        exec_pc, mask,
                        s->ar[0], s->data[s->ar[0]], s->ar[1], s->data[s->ar[1]],
                        s->ar[2], s->data[s->ar[2]], !!(s->st0 & ST0_TC), s->insn_count);
            }
        }
        /* FBWATCH: does the FB handler at 0x9ac0 run at all? */
        if (g_fbwatch_on > 0 && exec_pc == 0x9ac0) {
            static unsigned w9 = 0;
            if (w9++ < 40)
                fprintf(stderr, "[c54x] FBWATCH-9AC0 #%u insn=%u SP=0x%04x DP=0x%03x\n",
                        w9, s->insn_count, s->sp, s->st0 & 0x1FF);
        }
        /* FBWATCH-INITTAB: does the dispatch table init routine run (0xc704,
         * which fills data[0x4c24-0x4c5d], the BACC-A/CALA targets)? Zero hits
         * means the boot skips the setup pass entirely. */
        if (g_fbwatch_on > 0 && (exec_pc == 0xc704 || exec_pc == 0xc472)) {
            static unsigned wit = 0;
            if (wit++ < 10)
                fprintf(stderr, "[c54x] FBWATCH-INITTAB pc=0x%04x insn=%u SP=0x%04x\n",
                        exec_pc, s->insn_count, s->sp);
        }
        /* FBWATCH-DISP, producer versus consumer: does the CALAD dispatch at
         * 0x833b run once per frame, and which handler address does it compute
         * into A? No line at all means a dead dispatcher (producer side);
         * A never equal to 0x9ac0 means the jump table or formula never yields
         * the FB handler; A == 0x9ac0 means FB is dispatched but does not
         * detect (handler bug). High cap, to see the distribution. */
        if (g_fbwatch_on > 0 && exec_pc == 0x833b) {
            static unsigned wdp = 0;
            if (wdp++ < 120)
                fprintf(stderr, "[c54x] FBWATCH-DISP #%u insn=%u A_handler=0x%04x DP=0x%03x SP=0x%04x\n",
                        wdp, s->insn_count, (uint16_t)(s->a & 0xffff), s->st0 & 0x1FF, s->sp);
        }
        }   /* C54X_SONDES : FBWATCH-* */
        /* CORR-ENTRY tracker (CALYPSO_CORRELATOR_TRACE=1): captures the
         * out -> in transition of the FB-det range [0x8d00..0x9000). */
        if (!c54x_rapide) {
        corr_entry_track(s->pc, s);
        /* FBDB-PROBE (CALYPSO_FBDB_PROBE=1): traces B at 0xfbd9, A at 0xfbdb
         * (after the F2xx SUB) and A at 0xfbf3 (before STLM A,AR4). */
        fbdb_probe_check_pc(s->pc, s);
        /* FORCE-INTM-ONESHOT (CALYPSO_FORCE_INTM_ONESHOT=1): arbitration
         * probe - clears INTM ONCE while INTM=1 and BRINT0 is pending, so the
         * existing tracers show whether everything downstream is healthy. */
        force_intm_oneshot_check(s);
        /* STUCK-PROBE (CALYPSO_STUCK_PROBE=1): PC and XPC histogram while
         * INTM=1 and BRINT0 is pending. */
        stuck_probe_check(s);
        }

        /* CALA-70C3 forensic probes. The DSP loops for ever on CALA A at
         * PROM0[0x70c3] with A = 0x0001_70c3, a self-reference. A_H = 0x0001
         * cannot come from a sign-extended `LD Smem,A`, which yields A_H in
         * {0x0000, 0xFFFF}, so the writer is an upstream DLD or something that
         * composes H and L. Three probes:
         *   1. source of the jump to 0x70c3 (XPC:PC plus the opcode at
         *      prev_pc), first-hit only to escape post-runaway pollution (the
         *      XPC MMR is overwritten once SP rampages through
         *      data[0x18..0x1F]);
         *   2. a counter on LD@0x70c1 - zero confirms a direct jump that skips
         *      the LD;
         *   3. the last writer of A (the PC that put 0x0001_70c3 there).
         * On by default, about 3 branches per instruction. */
        static int      p70c3_first    = 0;
        static uint64_t p70c1_counter  = 0;
        static uint16_t p_last_a_pc    = 0xFFFF;
        static int64_t  p_last_a_val   = 0;
        int64_t a_before_exec = s->a;

        /* [2026-09-23] Sondes pures CALA-70C3, derriere l'interrupteur. */
        if (C54X_SONDES) {
        if (s->pc == 0x70c1) p70c1_counter++;

        if (s->pc == 0x70c3 && !p70c3_first) {
            p70c3_first = 1;
            uint16_t prev_pc = pc_ring[(pc_ring_idx - 2) & 255];
            uint16_t prev_op = prog_fetch(s, prev_pc);
            C54_LOG("PROBE-CALA70C3-FIRST insn=%u XPC=%u PC=0x%04x op=0x%04x "
                    "prev_pc=0x%04x prev_op=0x%04x "
                    "A=%010llx (A_G=0x%02x A_H=0x%04x A_L=0x%04x) "
                    "LD@70C1_count=%llu last_A_writer_pc=0x%04x last_A_val=%010llx "
                    "SP=0x%04x BK=0x%04x",
                    s->insn_count, s->xpc & 0x3, s->pc, prog_fetch(s, s->pc),
                    prev_pc, prev_op,
                    (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                    (uint8_t)((s->a >> 32) & 0xFF),
                    (uint16_t)((s->a >> 16) & 0xFFFF),
                    (uint16_t)(s->a & 0xFFFF),
                    (unsigned long long)p70c1_counter,
                    p_last_a_pc,
                    (unsigned long long)(p_last_a_val & 0xFFFFFFFFFFULL),
                    s->sp, s->bk);
            C54_LOG("PROBE-CALA70C3-TRAIL pc[-12..-1] = "
                    "%04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x",
                    pc_ring[(pc_ring_idx-13)&255], pc_ring[(pc_ring_idx-12)&255],
                    pc_ring[(pc_ring_idx-11)&255], pc_ring[(pc_ring_idx-10)&255],
                    pc_ring[(pc_ring_idx-⟨1⟩)&255],  pc_ring[(pc_ring_idx-⟨2⟩)&255],  ×3
    ⟨⟩ = (9,8) (7,6) (5,4)
                    pc_ring[(pc_ring_idx-3)&255],  pc_ring[(pc_ring_idx-2)&255]);
        }
        }   /* C54X_SONDES : CALA-70C3 */

        {
            /* DISP-ENTRY: predecessor = the PC executed on the previous
             * iteration. */
            static uint16_t s_last_run_pc = 0;
            static uint16_t s_last_run_op = 0;
            g_prev_pc = s_last_run_pc;
            g_prev2_op = g_prev_op;     /* second to last (BC after cmpm/bitf ; st) */
            g_prev_op = s_last_run_op;
            /* Reliable TC: set by CMPM/BITF, kept across ANDM/ORM/XORM/ADDM #lk and ST/STM #k
             * (except STM to ST0), lost on any other instruction. See c54x_tc_par_rmw (c54x_exec.c). */
            {
                uint16_t h = g_prev_op & 0xFF00;
                if ((g_prev_op & 0xFE00) == 0x6000) g_tc_fiable = 1;
                else if (!((h >= 0x6800 && h <= 0x6B00) || h == 0x7600 || (h == 0x7700 && (g_prev_op & 0xFF) != 0x06)))
                    g_tc_fiable = 0;
            }
            s_last_run_pc = s->pc;
            s_last_run_op = exec_op;
        }

        /* [2026-09-23] Sondes pures AR3-TRIP .. LOOPTRACE. */
        if (C54X_SONDES) {
        /* AR3-TRIP: catches the FIRST instruction that moves AR3 by a large
         * step. At this point s->ar[3] holds the result of the instruction
         * that just ran, i.e. g_prev_pc / g_prev_op. Legitimate
         * post-increments are +-1 or +-2, so a jump of 0x800 or more is a
         * suspect load or modification. delta == SP is flagged separately: it
         * is the "AR3 += SP" signature behind the derail. One subtract and one
         * compare per instruction, 80 lines. */
        {
            static uint16_t s_prev_ar3 = 0;
            static unsigned s_ar3trip = 0;
            uint16_t cur_ar3 = s->ar[3];
            uint16_t d = (uint16_t)(cur_ar3 - s_prev_ar3);
            uint16_t mag = (d & 0x8000) ? (uint16_t)(-d) : d;
            if (mag >= 0x0800 && s_ar3trip < 80) {
                s_ar3trip++;
                fprintf(stderr, "[c54x] AR3-TRIP #%u by PC=0x%04x op=0x%04x : "
                        "AR3 0x%04x -> 0x%04x (d=%+d) SP=0x%04x%s XPC=%u insn=%u\n",
                        s_ar3trip, g_prev_pc, g_prev_op,
                        s_prev_ar3, cur_ar3, (int16_t)d, s->sp,
                        (d == s->sp && s->sp != 0) ? "  <== delta==SP!" : "",
                        s->xpc, s->insn_count);
            }
            s_prev_ar3 = cur_ar3;
        }

        /* AR0-TRACE: AR0 is the index of the *AR3+0% at 0xb3d1 (AR3 += AR0),
         * and it is measured at about 0x5AC7, i.e. the value of SP - corrupt.
         * Logs EVERY change of AR0 during the init window (insn < 3000) with
         * the instruction that wrote it (g_prev_pc/op), which names the
         * corrupter. */
        {
            static uint16_t s_prev_ar0 = 0xFFFF;
            static unsigned s_ar0n = 0;
            if (s->ar[0] != s_prev_ar0 && s->insn_count < 3000 && s_ar0n < 60) {
                s_ar0n++;
                fprintf(stderr, "[c54x] AR0-TRACE #%u by PC=0x%04x op=0x%04x : "
                        "AR0 0x%04x -> 0x%04x BK=0x%04x SP=0x%04x DP=0x%03x insn=%u\n",
                        s_ar0n, g_prev_pc, g_prev_op, s_prev_ar0, s->ar[0],
                        s->bk, s->sp, (s->st0 & 0x1FF), s->insn_count);
            }
            s_prev_ar0 = s->ar[0];
        }

        /* One-shot dump of the full context at the first pass through 0xb3d1
         * (ADD *AR3+0%,A): AR0, AR3, BK, DP, ST1, A and data[AR3]. */
        if (s->pc == 0x3d1 + 0xb000) {
            static unsigned s_b3d1 = 0;
            if (s_b3d1 < 6) {
                s_b3d1++;
                uint16_t ea = s->ar[3];
                fprintf(stderr, "[c54x] B3D1-CTX #%u AR0=0x%04x AR3=0x%04x BK=0x%04x "
                        "DP=0x%03x ST1=0x%04x A=0x%010llx data[AR3]=0x%04x SP=0x%04x insn=%u\n",
                        s_b3d1, s->ar[0], s->ar[3], s->bk, (s->st0 & 0x1FF),
                        s->st1, (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                        s->data[ea], s->sp, s->insn_count);
            }
        }

        /* DETECTOR-TRACE: instruction-by-instruction trace of the FB detector
         * / correlator in [0xf074..0xf0c0]. In the ROM dump 0xf070-0xf0b0 is a
         * sigmoid table under XPC=0; the real execution is in another XPC
         * bank, and exec_op = prog_fetch(s->pc) honours the bank, so the real
         * opcode is shown. Logs PC, XPC (which bank), the opcode and A/B/T,
         * which carry the correlation arithmetic (accumulated sample*coeff).
         * A and B staying 0 means the arithmetic produces nothing (opcode
         * mis-emulated, or the sample is never read); A and B rising while
         * d_fb_det stays 0 means a threshold or decision bug - cross-check
         * with CALYPSO_FBDET_SENTINEL=2, which watches the writes to 0x08f8.
         * CALYPSO_DETTRACE=1, 800 lines. */
        {
            static int dettr_on = -1;
            if (dettr_on < 0) dettr_on = calypso_gate("CALYPSO_DETTRACE", 0);
            if (dettr_on && exec_pc >= 0xf074 && exec_pc <= 0xf0c0) {
                static unsigned dettr_n = 0;
                if (dettr_n < 800) {
                    dettr_n++;
                    fprintf(stderr, "[c54x] DETTRACE #%u PC=0x%04x XPC=%u op=0x%04x "
                            "A=0x%010llx B=0x%010llx T=0x%04x AR2=%04x AR3=%04x "
                            "AR5=%04x insn=%u\n",
                            dettr_n, exec_pc, s->xpc, exec_op,
                            (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                            (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                            s->t, s->ar[2], s->ar[3], s->ar[5], s->insn_count);
                    if (dettr_n == 800)
                        fprintf(stderr, "[c54x] DETTRACE capped at 800\n");
                }
            }
        }

        /* BACC-DISP: at the BACC dispatch (0xb40f) and the LD *AR7,A (0xb40e)
         * before it, captures A, AR7 and data[AR7], the source of the 0xf074
         * pointer. Tells whether the slot read (data[AR7]) already holds
         * 0xf074 (a wrong pointer) or A is corrupt some other way. */
        if (exec_pc == 0xb40f) {   /* BACC A = dispatch handler */
            uint16_t handler = (uint16_t)(s->a & 0xFFFF);
            static uint16_t seen[96]; static unsigned nseen = 0;
            bool dup = false;
            for (unsigned i = 0; i < nseen; i++)
                if (seen[i] == handler) { dup = true; break; }
            if (!dup && nseen < 96) {
                seen[nseen++] = handler;
                fprintf(stderr, "[c54x] BACC-DISP #%u handler=0x%04x AR7=0x%04x %sXPC=%u insn=%u\n",
                        nseen, handler, s->ar[7],
                        (handler == 0xab38) ? "(RET/noop) " : "<<REAL>> ",
                        s->xpc, s->insn_count);
            }
        }

        /* LOOPTRACE: the WHOLE main loop [0xb3a0..0xb410] - the prologue that
         * reads d_dsp_page at 0xb3cc plus the dispatcher at 0xb400 - together
         * with the caller (g_prev_pc). Shows how 0xb401 is reached the first
         * time, whether word[0] = 0x2900 at 0xb400 is executed or skipped, and
         * why data[SP] (the seeded return address) is empty when the idle RET
         * pops it and lands on PC=0. Measured: POST-BOOTSTUB-RET runs 640M
         * times (the PC=0 storm) from the very first dispatch, at insn 4398.
         * AR1 is included because the READA copy arms AR1=0x4387. 300 lines. */
        if (exec_pc >= 0xb3a0 && exec_pc <= 0xb410) {
            static unsigned de = 0;
            if (de < 300) {
                de++;
                fprintf(stderr, "[c54x] LOOPTRACE #%u from=0x%04x PC=0x%04x op=0x%04x "
                        "A=0x%06llx AR1=%04x AR7=%04x SP=%04x "
                        "stk[SP-1,SP,SP+1]=%04x,%04x,%04x insn=%u\n",
                        de, g_prev_pc, exec_pc, exec_op,
                        (unsigned long long)(s->a & 0xFFFFFF),
                        s->ar[1], s->ar[7], s->sp,
                        s->data[(uint16_t)(s->sp-1)], s->data[s->sp],
                        s->data[(uint16_t)(s->sp+1)], s->insn_count);
            }
        }
        }   /* C54X_SONDES : AR3-TRIP .. LOOPTRACE */

        {
            /* @BEQUILLE - SEED_5AC8 (+ SEED5AC8_VAL)  (CALYPSO_SEED5AC8, atoi>0, default
             *              OFF; _VAL defaults to 0x71f4, calypso_wire.env sets 0xa4c7)
             *   masks   : the population of mem[0x5ac8], the word the terminal RET (0xab38)
             *             pops to choose the go-live entry. Nothing writes it in this model,
             *             so the RET lands on PC=0 and storms.
             *   remove  : when it is known WHO writes mem[0x5ac8] on silicon. This is not a
             *             fix: in the ROM (PROM0.bin, little-endian) the boot at 0xb405 does
             *             ST #0xa4df,data[0x3f6d], so the go-live soft-vector is 0xa4df,
             *             which SKIPS both the IMR arming (0xa4c7 ORM #0x3000,IMR) and the
             *             enable (0xa4d0 RSBX INTM). Even a "correct" mem[0x5ac8] = 0x71f4
             *             therefore never arms the IMR; the real cause of the storm and of
             *             the missing enable is still open.
             */
            static int seed_on = -1;
            if (seed_on < 0) { const char *e = calypso_getenv("CALYPSO_SEED5AC8"); seed_on = (e && atoi(e) > 0) ? 1 : 0; }
            /* Wired to the DSP's own STM #0x5ac8,SP (PC=0xb382, op=0x7718),
             * so the seed follows the real stack init rather than an arbitrary
             * poke at the terminal BACC. Timing is safe: nothing writes
             * 0x5ac8 between 0xb382 and the RET at 0xab38. */
            if (seed_on && exec_pc == 0xb382) {
                /* Seed value, CALYPSO_SEED5AC8_VAL. The terminal RET pops
                 * mem[0x5ac8] to choose the go-live entry: 0x71f4 (default)
                 * goes through the trampoline to 0xa4df, which skips the
                 * RSBX INTM enable at 0xa4d0; 0xa4c7 enters at the ORM IMR, so
                 * the native RSBX INTM runs and the frame interrupt (delivered
                 * on bit 12) is actually taken. */
                static int sval = -1;
                if (sval < 0) { const char *e = calypso_getenv("CALYPSO_SEED5AC8_VAL");
                                sval = (e && *e) ? (int)strtoul(e, NULL, 0) : 0x71f4; }
                static unsigned sd = 0;
                if (sd < 8)
                    fprintf(stderr, "[c54x] SEED-5AC8 (cable@0xb382 STM SP) #%u : "
                            "mem[0x5ac8] 0x%04x->0x%04x SP=0x%04x op=0x%04x insn=%u\n",
                            ++sd, s->data[0x5ac8], (unsigned)sval, s->sp, exec_op, s->insn_count);
                s->data[0x5ac8] = (uint16_t)sval;
            }
        }
        /* [2026-09-23] Sondes pures GOLIVE-WATCH .. IMR-DYN. */
        if (C54X_SONDES) {
        /* GOLIVE-WATCH (ungated): does the firmware ever reach the go-live
         * routine (0xa4c9..0xa520) or the trampoline 0x71f4? Logs PC, opcode,
         * INTM, IMR and the soft-vector data[0x3f6d]. 120 lines. */
        if ((exec_pc >= 0xa4c9 && exec_pc <= 0xa520) || exec_pc == 0x71f4 || exec_pc == 0x71f6) {
            static unsigned gw = 0;
            if (gw++ < 120)
                fprintf(stderr, "[c54x] GOLIVE-WATCH #%u PC=0x%04x op=0x%04x INTM=%d "
                        "IMR=0x%04x data[0x3f6d]=0x%04x A=0x%06llx insn=%u\n",
                        gw, exec_pc, exec_op, (s->st1 & ST1_INTM) ? 1 : 0, s->imr,
                        s->data[0x3f6d], (unsigned long long)(s->a & 0xFFFFFF), s->insn_count);
        }
        /* AR0-DELTA (CALYPSO_AR0_DEBUG, read-only): every change of AR0 in the
         * boot window, which locates the instruction that corrupts it. AR0 is
         * expected near 0x5ac8, to write the go-live vector
         * mem[0x5ac8] = 0x71f4. */
        {
            static int ad_en = -1;
            static uint16_t ar0_prev = 0xFFFF;
            if (ad_en < 0) ad_en = calypso_gate("CALYPSO_AR0_DEBUG", 0);
            if (ad_en && s->insn_count < 12000 && s->ar[0] != ar0_prev
                && exec_pc != 0xb387) {   /* skip the fill loop, which floods the cap */
                static unsigned adn = 0;
                if (adn++ < 200)
                    fprintf(stderr, "[c54x] AR0-DELTA 0x%04x->0x%04x by PC=0x%04x "
                            "op=0x%04x AR3=0x%04x insn=%u\n",
                            ar0_prev, s->ar[0], exec_pc, exec_op, s->ar[3], s->insn_count);
                ar0_prev = s->ar[0];
            }
        }
        /* PROG-DUMP-B3D0 (CALYPSO_AR0_DEBUG, read-only, one-shot): dumps the
         * region that seeds data[0x3f6d] = 0xa4df (at 0xb405) and the terminal
         * BACC 0xb40f, to find the companion setup of mem[0x5ac8]. */
        static int _ar0dbg = -1; if (_ar0dbg < 0) _ar0dbg = calypso_getenv("CALYPSO_AR0_DEBUG") ? 1 : 0;
        if (_ar0dbg && exec_pc == 0xb405) {
            static int done = 0;
            if (!done) {
                done = 1;
                fprintf(stderr, "[c54x] PROG-DUMP @0xb405 SP=0x%04x AR0=0x%04x AR1=0x%04x "
                        "AR3=0x%04x data[0x5ac8]=0x%04x data[0x3f6d]=0x%04x data[0x4387]=0x%04x\n",
                        s->sp, s->ar[0], s->ar[1], s->ar[3],
                        s->data[0x5ac8], s->data[0x3f6d], s->data[0x4387]);
                for (uint16_t a = 0xb3d0; a <= 0xb414; a += 4)
                    fprintf(stderr, "[c54x] PROG[0x%04x..]= %04x %04x %04x %04x\n",
                            a, s->prog[a], s->prog[(uint16_t)(a+1)],
                            s->prog[(uint16_t)(a+2)], s->prog[(uint16_t)(a+3)]);
                /* the RET at 0xab38, target of the BACC */
                fprintf(stderr, "[c54x] PROG[0xab36..]= %04x %04x %04x %04x\n",
                        s->prog[0xab36], s->prog[0xab37], s->prog[0xab38], s->prog[0xab39]);
                /* Call site 0x71f2 (transfers to 0xb3a3 without pushing) plus
                 * the trampoline 0x71f4. It should be a CALL that pushes
                 * 0x71f4. */
                for (uint16_t a = 0x71ec; a <= 0x71f8; a += 4)
                    fprintf(stderr, "[c54x] PROG[0x%04x..]= %04x %04x %04x %04x\n",
                            a, s->prog[a], s->prog[(uint16_t)(a+1)],
                            s->prog[(uint16_t)(a+2)], s->prog[(uint16_t)(a+3)]);
                /* start of the routine at 0xb3a3, target of the transfer */
                fprintf(stderr, "[c54x] PROG[0xb3a0..]= %04x %04x %04x %04x %04x %04x\n",
                        s->prog[0xb3a0], s->prog[0xb3a1], s->prog[0xb3a2],
                        s->prog[0xb3a3], s->prog[0xb3a4], s->prog[0xb3a5]);
                /* the fill: setup 0xb384-0xb38c (STM AR0, RPT #k, ST #imm *AR0+) */
                fprintf(stderr, "[c54x] PROG[0xb384..]= %04x %04x %04x %04x %04x %04x %04x %04x %04x\n",
                        s->prog[0xb384], s->prog[0xb385], s->prog[0xb386], s->prog[0xb387],
                        s->prog[0xb388], s->prog[0xb389], s->prog[0xb38a], s->prog[0xb38b], s->prog[0xb38c]);
                /* result of the fill in memory: the constant and where it stops */
                fprintf(stderr, "[c54x] FILL-MEM data[0x5a00]=0x%04x [0x5ac5]=0x%04x [0x5ac6]=0x%04x "
                        "[0x5ac7]=0x%04x [0x5ac8]=0x%04x [0x5ac9]=0x%04x\n",
                        s->data[0x5a00], s->data[0x5ac5], s->data[0x5ac6],
                        s->data[0x5ac7], s->data[0x5ac8], s->data[0x5ac9]);
                /* vector store loop (0xb4c8-0xb4e0) and its AR setup */
                /* routine 0xa9ea (CALL at 0xb3f3, pushes 0xb3f5): locate its
                 * over-pop (PSHM/POPM imbalance) that drifts SP into the
                 * storm */
                for (uint16_t a = 0xa9ea; a <= 0xaa1a; a += 4)
                    fprintf(stderr, "[c54x] A9EA-PROG[0x%04x..]= %04x %04x %04x %04x\n",
                            a, s->prog[a], s->prog[(uint16_t)(a+1)],
                            s->prog[(uint16_t)(a+2)], s->prog[(uint16_t)(a+3)]);
                for (uint16_t a = 0xb4c8; a <= 0xb4e0; a += 4)
                    fprintf(stderr, "[c54x] VECLOOP-PROG[0x%04x..]= %04x %04x %04x %04x\n",
                            a, s->prog[a], s->prog[(uint16_t)(a+1)],
                            s->prog[(uint16_t)(a+2)], s->prog[(uint16_t)(a+3)]);
                /* go-live tail after 0xa582: does it install vec28, i.e. write
                 * 0x00f0? */
                for (uint16_t a = 0xa582; a <= 0xa5a2; a += 4)
                    fprintf(stderr, "[c54x] GOTAIL-PROG[0x%04x..]= %04x %04x %04x %04x\n",
                            a, s->prog[a], s->prog[(uint16_t)(a+1)],
                            s->prog[(uint16_t)(a+2)], s->prog[(uint16_t)(a+3)]);
                fprintf(stderr, "[c54x] VEC28-NOW data[0x00f0..0x00f3]= %04x %04x %04x %04x (doit brancher vers 0x7234)\n",
                        s->data[0x00f0], s->data[0x00f1], s->data[0x00f2], s->data[0x00f3]);
                for (uint16_t a = 0xb360; a <= 0xb384; a += 4)
                    fprintf(stderr, "[c54x] HANDLER-B360[0x%04x..]= %04x %04x %04x %04x\n",
                            a, s->prog[a], s->prog[(uint16_t)(a+1)],
                            s->prog[(uint16_t)(a+2)], s->prog[(uint16_t)(a+3)]);
                for (uint16_t a = 0x701c; a <= 0x7024; a += 4)
                    fprintf(stderr, "[c54x] RET701F-PROG[0x%04x..]= %04x %04x %04x %04x\n",
                            a, s->prog[a], s->prog[(uint16_t)(a+1)],
                            s->prog[(uint16_t)(a+2)], s->prog[(uint16_t)(a+3)]);
                for (uint16_t a = 0xa670; a <= 0xa680; a += 4)
                    fprintf(stderr, "[c54x] CALA671-PROG[0x%04x..]= %04x %04x %04x %04x\n",
                            a, s->prog[a], s->prog[(uint16_t)(a+1)],
                            s->prog[(uint16_t)(a+2)], s->prog[(uint16_t)(a+3)]);
                for (uint16_t a = 0xa4c0; a <= 0xa4e4; a += 4)
                    fprintf(stderr, "[c54x] GOLIVE-PROG[0x%04x..]= %04x %04x %04x %04x\n",
                            a, s->prog[a], s->prog[(uint16_t)(a+1)],
                            s->prog[(uint16_t)(a+2)], s->prog[(uint16_t)(a+3)]);
                fprintf(stderr, "[c54x] CTRL-CELLS data[0x098a]=0x%04x data[0x098c]=0x%04x "
                        "data[0x3f70]=0x%04x api[0x098a]=0x%04x\n",
                        s->data[0x098a], s->data[0x098c], s->data[0x3f70],
                        s->api_ram ? s->api_ram[0x098a-0x0800] : 0xffff);
            }
        }
        /* RUNTIME-DYN (read-only): dynamics of the state machine guarding the
         * scheduler at 0xa51c, which never runs (data[0x3fb0] == 0). Three
         * questions:
         *   (1) IMR-DYN   : does the DSP execute its IMR arming sites, and who
         *                   clears the IMR afterwards?
         *   (2) WAIT-TEST : is bit 1 of 0x3f70 (the wait-loop exit) ever set at
         *                   the test?
         *   (3) DE-BR     : does the super-loop reach the set-bit-1 branch at
         *                   0xde9c? */
        if (exec_pc == 0x76fc || exec_pc == 0xa509 || exec_pc == 0xb37e) {
            static unsigned id = 0;
            if (id++ < 40)
                fprintf(stderr, "[c54x] IMR-DYN PC=0x%04x IMR_before=0x%04x writes=0x%04x "
                        "INTM=%d insn=%u\n", exec_pc, s->imr,
                        s->prog[(uint16_t)(exec_pc + 1)],
                        (s->st1 & ST1_INTM) ? 1 : 0, s->insn_count);
        }
        }   /* C54X_SONDES : GOLIVE-WATCH .. IMR-DYN */
        {
            /* @BEQUILLE - KEEP_IMR (+ KEEP_IMR_VAL)  (CALYPSO_KEEP_IMR, EXISTS, default 1
             *              in hack/native/native_helped/wire; fallback value 0x52fd)
             *   masks   : the IMR clobber by 0xb37e (STM #0,IMR), which fires ~47 insns
             *             after go-live armed it, and by 0xa509 (which strips bit 12).
             *             s->imr is rewritten from the shadow data[0x435b] as soon as
             *             bit 5 drops, over the whole region [0xa4ca..0xdea0].
             *             Bit 5 is BRINT0 / vec 21, the "BSP buffer received" interrupt
             *             that wakes the correlator. Measured: go-live arms IMR=0x52fd
             *             178 times and 0xb37e wipes it each time; restoring only
             *             0x3050/0x0050 (bit 5 clear) masks BRINT0 for good, IFR bit 5
             *             stays pending for ever and the correlator is never dispatched.
             *             Hence the restore uses the real image, not a constant.
             *   remove  : when the firmware stops losing bit 5, that is when the go-live
             *             sequence 0xa4c7 / 0xa51b / 0xa582 runs in the right order.
             */
            static int ki = -1; static uint16_t kiv = 0;
            if (ki < 0) { ki = calypso_gate("CALYPSO_KEEP_IMR", 0);
                const char *e = calypso_getenv("CALYPSO_KEEP_IMR_VAL");
                kiv = (e && *e) ? (uint16_t)strtoul(e, NULL, 0) : 0x52fd; }
            if (ki && exec_pc >= 0xa4ca && exec_pc <= 0xdea0 && !(s->imr & 0x0020)) {
                uint16_t img = s->data[0x435b];            /* shadow IMR (= 0x52fd) */
                if (!(img & 0x0020)) img = kiv;            /* shadow without bit 5 -> fallback */
                s->imr = img;
                static unsigned kil = 0;
                if (kil++ < 8)
                    fprintf(stderr, "[c54x] KEEP-IMR re-arme IMR=0x%04x (bit5/BRINT0 preserve, "
                            "shadow d[435b]=0x%04x) @PC=0x%04x insn=%u\n",
                            s->imr, s->data[0x435b], exec_pc, s->insn_count);
            }
        }
        if (exec_pc == 0xa4d4) {
            static unsigned wt = 0; static uint16_t last = 0xffff;
            uint16_t fl = s->data[0x3f70];
            /* @BEQUILLE - FORCE_GOLIVE  (CALYPSO_FORCE_GOLIVE, atoi>0, default OFF;
             *              empty in hack.env)
             *   masks   : the go-live wait loop tests bit 1 of data[0x3f70], which only the
             *             setter at 0xde9c writes, and that setter is itself conditioned on
             *             the control cells 0x098a/0x098c that the ARM leaves at 0. The bit
             *             is set here, at the 0xa4d4 test.
             *   remove  : as soon as the ARM handshake (ARM2DSP_BGEN) gets the flow past
             *             0xddf5 and the native setter 0xde9c runs.
             */
            { static int fg = -1; if (fg < 0) { const char *e = calypso_getenv("CALYPSO_FORCE_GOLIVE"); fg = (e && atoi(e) > 0) ? 1 : 0; }
              if (fg && !(fl & 0x0002)) { s->data[0x3f70] = (uint16_t)(fl | 0x0002); fl = s->data[0x3f70];
                static unsigned fgc = 0; if (fgc++ < 8) fprintf(stderr, "[c54x] FORCE-GOLIVE 0x3f70 |= bit1 -> 0x%04x insn=%u\n", fl, s->insn_count); } }
            /* WAIT-TEST (probe) behind the switch; FORCE_GOLIVE above stays
             * active. */
            if (C54X_SONDES && ((fl & 0x0002) || fl != last)) {
                if (wt++ < 60)
                    fprintf(stderr, "[c54x] WAIT-TEST PC=0xa4d4 data[0x3f70]=0x%04x bit1=%d "
                            "insn=%u\n", fl, !!(fl & 2), s->insn_count);
                last = fl;
            }
        }

        if (exec_pc == 0x013b) {
            /* @BEQUILLE - ISR_TO_8341  (CALYPSO_ISR_TO_8341, EXISTS, default OFF)
             *   masks   : the overlay ISR prologue 0x013b derails and never reaches the FB
             *             LUT at 0x8341, which does the complete correlator setup (BRC, BK,
             *             data pointers) that the forced entry at 0x8d00 short-circuits into
             *             a dead MAC loop. The frame interrupt vectors 0x00f0 -> 0x7234
             *             (B 0x013b) -> the prologue, so s->pc is forced to 0x8341 when the
             *             interrupt comes from the frame scheduler (g_prev_pc == 0x7234).
             *   remove  : as soon as the 0x013b prologue ends at 0x8341 through its own flow
             *             (same condition as a successful FIX_3FCD).
             *   note    : calypso_wire.env unsets this EXPLICITLY - an empty ":=" under
             *             set -a would turn this EXISTS gate on. Never convert it to ":=".
             */
            static int r8 = -1;
            if (r8 < 0) r8 = calypso_gate("CALYPSO_ISR_TO_8341", 0);
            if (r8 && g_prev_pc == 0x7234) {
                static unsigned r8n = 0;
                if (r8n++ < 8)
                    fprintf(stderr, "[c54x] ISR-TO-8341 : frame ISR 0x013b -> 0x8341 "
                            "(LUT FB setup complet) prev=0x%04x insn=%u\n",
                            g_prev_pc, s->insn_count);
                s->pc = 0x8341;
                return 0;
            }
        }

        if (exec_pc == 0x8d00) {
            /* @BEQUILLE - CORR_SETUP (+ CORR_AR1/_AR4/_AR5)  (CALYPSO_CORR_SETUP, EXISTS,
             *              default OFF)
             *   masks   : the pointer setup the native LUT at 0x8341 performs before entering
             *             0x8d00 (STM #0x2f22,AR1 / #0x2be4,AR4 / #0x0060,AR5, disassembled at
             *             PROM0 0x8347/0x8349/0x834b). Without those pointers the MAC loops at
             *             0x8e8b/0x8e8c without concluding, because 0x8d00 is reached through
             *             BSP_DISPATCH_FB instead. The constants are injected at the
             *             correlator entry.
             *   remove  : when the native path goes through 0x8341 before 0x8d00.
             *   note    : EXISTS idiom - an empty ":=" would TURN IT ON, hence the explicit
             *             unset in calypso_wire.env. Measured ineffective: the ARs are
             *             rewritten before 0x8e8b.
             */
            static int cs = -1; static uint16_t a1 = 0, a4 = 0, a5 = 0;
            if (cs < 0) {
                cs = calypso_gate("CALYPSO_CORR_SETUP", 0);
                const char *e;
                a1 = (e = calypso_getenv("CALYPSO_CORR_AR1")) && *e ? (uint16_t)strtoul(e,0,0) : 0x2f22;
                a4 = (e = calypso_getenv("CALYPSO_CORR_AR4")) && *e ? (uint16_t)strtoul(e,0,0) : 0x2be4;
                a5 = (e = calypso_getenv("CALYPSO_CORR_AR5")) && *e ? (uint16_t)strtoul(e,0,0) : 0x0060;
            }
            if (cs) {
                s->ar[1] = a1; s->ar[4] = a4; s->ar[5] = a5;
                static unsigned csn = 0;
                if (csn++ < 8)
                    fprintf(stderr, "[c54x] CORR-SETUP @0x8d00 : AR1=0x%04x AR4=0x%04x "
                            "AR5=0x%04x (setup LUT 0x8341 injecte) insn=%u\n",
                            a1, a4, a5, s->insn_count);
            }
        }
        calypso_arm2dsp_on_dsp_step(s, exec_pc);

        if (exec_pc == 0xa4ca) {
            /* @BEQUILLE - POKE_A4C7_ONCE  (CALYPSO_POKE_A4C7_ONCE, atoi>0, default OFF)
             *   masks   : 0xa4c7 (ORM #0x3000,IMR, the ROM's own IMR arming) is never
             *             reached: the flow always enters the go-live wait loop directly at
             *             0xa4ca, three words later, and 0xa4c7 has zero hits. The FIRST
             *             arrival at 0xa4ca is redirected to 0xa4c7, so the CPU executes the
             *             real ROM instruction and falls through into 0xa4ca normally. No
             *             register or memory value is poked - only the entry PC, once.
             *             Answers: does IMR arm to 0x3000, does the frame interrupt then
             *             vector to vec 28, does d_fb_det become non-zero, and do
             *             data[0x3f70] and data[0x435b] populate as well (single-root-cause
             *             test)?
             *   remove  : as soon as the upstream path (0xa4cd BC AEQ, or the setter of
             *             d[434e]/d[434f]) falls through into 0xa4c7.
             *   note    : calypso_hack.env itself calls this "falsification, not a fix".
             */
            static int poke_en = -1;
            if (poke_en < 0) { const char *e = calypso_getenv("CALYPSO_POKE_A4C7_ONCE"); poke_en = (e && atoi(e) > 0) ? 1 : 0; }
            static int poke_done = 0;
            if (poke_en && !poke_done) {
                poke_done = 1;
                fprintf(stderr, "[c54x] POKE-A4C7-ONCE: redirecting PC 0xa4ca -> "
                        "0xa4c7 (let ROM's own ORM #0x3000,IMR run) insn=%u\n",
                        s->insn_count);
                s->pc = 0xa4c7;
                return 0;
            }
        }

        /* CALA-71DA (CALYPSO_CALA_71DA, read-only): the generic
         * save/dispatch/restore wrapper at 0x71c0-0x71f2 (19 PSHM; ST0=0,
         * ST1=0x6900; CALA at 0x71da; 19 POPM; RET) dispatches to the address
         * in A. Logs A just before the CALA, to tell whether this dispatcher
         * ever calls anything other than a no-op stub - the same closed
         * self-referential loop as data[0x4387] -> 0xab38. */
        if (C54X_SONDES && exec_pc == 0x71da) {   /* [2026-09-23] sonde pure */
            static int cala_en = -1;
            if (cala_en < 0) cala_en = calypso_gate("CALYPSO_CALA_71DA", 0);
            if (cala_en) {
                static unsigned cala_n = 0;
                static uint16_t last_target = 0xFFFF;
                static unsigned same_target_count = 0;
                uint16_t target = (uint16_t)(s->a & 0xFFFF);
                if (target == last_target) same_target_count++;
                else { same_target_count = 0; last_target = target; }
                if (cala_n < 100 || same_target_count == 0 || (cala_n % 2000) == 0) {
                    fprintf(stderr, "[c54x] CALA-71DA #%u target=0x%04x SP=0x%04x "
                            "repeat_run=%u insn=%u\n", cala_n, target, s->sp,
                            same_target_count, s->insn_count);
                }
                cala_n++;
            }
        }

        {
            /* @BEQUILLE - C54X_FORCE_IMR  (CALYPSO_C54X_FORCE_IMR=<hex>, default OFF)
             *   masks   : the IMR re-arming after the ROM's STM #0,IMR at 0xb37e (insn
             *             ~1047), which is never undone before the scheduler at 0xb41c, so
             *             the frame interrupt stays masked and the DSP spins; and the
             *             RSBX INTM the ROM only plays after go-live. The requested bits are
             *             ORed into the IMR on every step outside an ISR, and INTM is cleared
             *             inside [0xb380..0xb440]. A typical value is 0x52fd (bit 3 INT3,
             *             bit 5 BRINT0, ...). Falsifiable test of the chain
             *             IMR -> interrupt -> 0x0fff -> go-live.
             *   remove  : when the go-live state machine reaches 0xa582 and sets the IMR
             *             itself.
             */
            static int fimr = -1; static uint16_t fimrv = 0;
            if (fimr < 0) { const char *e = calypso_getenv("CALYPSO_C54X_FORCE_IMR");
                fimrv = (e && *e) ? (uint16_t)strtoul(e, NULL, 0) : 0; fimr = fimrv ? 1 : 0; }
            if (fimr && (s->imr & fimrv) != fimrv) {
                static unsigned fic = 0;
                if (fic++ < 8)
                    fprintf(stderr, "[c54x] FORCE-IMR 0x%04x |= 0x%04x @PC=0x%04x insn=%u\n",
                            s->imr, fimrv, exec_pc, s->insn_count);
                s->imr |= fimrv;
            }
            /* The idle scheduler loop b380-b440 must run with INTM=0 (waiting
             * for an interrupt), but the ROM only clears INTM (RSBX at 0xa51b)
             * after go-live - a chicken and egg. INTM is therefore cleared
             * outside ISRs only, inside the idle loop, so IRQ-LEVEL can serve
             * the latched frame interrupt. ISRs (low PCs, 0x7234) are left
             * alone. */
            if (fimr && exec_pc >= 0xb380 && exec_pc <= 0xb440 && (s->st1 & ST1_INTM) &&
                ((s->pmst >> PMST_IPTR_SHIFT) & 0x1FF) != 0x1FF) {
                static unsigned ftc = 0;
                if (ftc++ < 8)
                    fprintf(stderr, "[c54x] FORCE-INTM clr @PC=0x%04x IFR=0x%04x IMR=0x%04x insn=%u\n",
                            exec_pc, s->ifr, s->imr, s->insn_count);
                s->st1 &= ~ST1_INTM;
            }
        }

        {
            /* @BEQUILLE - FORCE_098  (CALYPSO_FORCE_098=<hexval>, default empty/OFF;
             *              hack.env)
             *   masks   : the ARM never sets the d_background handshake cells
             *             0x098a/0x098c that the phase state machine (0xddeb, 0xde86)
             *             reads back. They are written non-zero on the DSP side just BEFORE
             *             the LD *(0x098c) / *(0x098a) of the setters (0xde86 on the GO
             *             path, 0xde94, 0xb3e4), which tests whether the DSP then takes the
             *             GO branch at 0xddf5, sets bit 1 and goes live.
             *   remove  : as soon as CALYPSO_ARM2DSP_BGEN sets those cells through the ARM
             *             bridge, with the right causality; calypso_hack.env already
             *             declares the replacement.
             */
            static int f98 = -1; static uint16_t f98v = 0;
            if (f98 < 0) { const char *e = calypso_getenv("CALYPSO_FORCE_098");
                f98v = (e && *e) ? (uint16_t)strtoul(e, NULL, 0) : 0; f98 = f98v ? 1 : 0; }
            if (f98 && (exec_pc == 0xde86 || exec_pc == 0xde94 || exec_pc == 0xb3e4 ||
                        exec_pc == 0xa5bd)) {
                s->data[0x098a] = f98v; s->data[0x098c] = f98v;
                static unsigned f9c = 0;
                if (f9c++ < 12)
                    fprintf(stderr, "[c54x] FORCE-098 @0x%04x data[098a/c]=0x%04x insn=%u\n",
                            exec_pc, f98v, s->insn_count);
            }
        }
        if (exec_pc >= 0xa4ca && exec_pc <= 0xa575) {
            /* @BEQUILLE - GOLIVE_TASKW  (CALYPSO_GOLIVE_TASKW, EQ1, default OFF)
             *   masks   : the firmware clears d[0x3f92] with ST #0 at 0xa4c4 and should
             *             re-arm it with ORM #0x0800 at 0xa539, but that native setter is
             *             skipped (d[5a00] == 0x88), so the FB-task bit stays 0 for ever and
             *             the DSP scheduler never dispatches the correlator. The ORM is
             *             replayed here, and only once the ARM has actually ordered go-live
             *             (bit 15 of d[0x0810], set by the CTRLSYS wire): right ARM -> DSP
             *             causality, right bit (0x0800), and an OR so the other scheduler
             *             bits are not overwritten.
             *   remove  : as soon as 0xa539 really executes (its d[5a00] predicate holds the
             *             right value), which makes the replay redundant.
             *   note    : inert without ARM2DSP_CTRLSYS, which sets data[0x0810] bit 15.
             */
            static int gt = -1;
            if (gt < 0) { const char *e = calypso_getenv("CALYPSO_GOLIVE_TASKW");
                          gt = (e && *e == '1') ? 1 : 0; }
            if (gt && (s->data[0x0810] & 0x8000)) {
                s->data[0x3f92] |= 0x0800;   /* replay of ORM #0x0800 @0xa539 */
                static unsigned glg = 0;
                if (glg++ < 8)
                    fprintf(stderr, "[c54x] GO-LIVE-TASKW @0x%04x d[3f92]=0x%04x "
                            "(ORM 0xa539 rejoue, ARM 0810 bit15 set) insn=%u\n",
                            exec_pc, s->data[0x3f92], s->insn_count);
            }
        }
        /* [2026-09-23] Sondes pures SM-TRACE .. PBRET. */
        if (C54X_SONDES) {
        /* SM-TRACE (CALYPSO_SM_TRACE): instruction-by-instruction trace of the
         * handshake state machine 0xdde0-0xde9f (the re-clear route at 0xde8b
         * versus the setter at 0xde9c). Shows PC, opcode, A, TC and the five
         * cells 0x098a..0x098e at each step, to see where the flow leaves the
         * 0xde9c path and which values would route it there. 400 lines. */
        if (exec_pc >= 0xdde0 && exec_pc <= 0xde9f) {
            static int smt = -1; static unsigned smn = 0;
            if (smt < 0) smt = calypso_gate("CALYPSO_SM_TRACE", 0);
            if (smt && smn < 400) {
                smn++;
                fprintf(stderr, "[c54x] SM-TRACE PC=0x%04x op=0x%04x A=0x%04x TC=%d 098[a=%04x b=%04x c=%04x d=%04x e=%04x] insn=%u\n",
                        exec_pc, exec_op, (unsigned)(s->a & 0xFFFF),
                        (s->st0 & ST0_TC) ? 1 : 0,
                        s->data[0x098a], s->data[0x098b], s->data[0x098c],
                        s->data[0x098d], s->data[0x098e], s->insn_count);
            }
        }
        /* B3-TRACE (CALYPSO_B3_TRACE): the idle scheduler 0xb380-0xb440 polls
         * the flag word data[0x0fff] (0xb424 BITF 0x0fff,#2; 0xb427 BC NTC
         * 0xb41c) and should route into the go-live block 0xb3db-0xb3ef
         * (0xb3ef = ST #2,0x3f70). Traces PC plus data[0x0fff], d_dsp_page and
         * d[0x3f70], to show why the ARM command (d_dsp_page bit 1) never
         * reaches 0xb3db. 500 lines. */
        if (exec_pc >= 0xb380 && exec_pc <= 0xb440) {
            static int b3t = -1; static unsigned b3n = 0;
            if (b3t < 0) b3t = calypso_gate("CALYPSO_B3_TRACE", 0);
            if (b3t && b3n < 500) {
                b3n++;
                fprintf(stderr, "[c54x] B3-TRACE PC=0x%04x op=0x%04x fff=0x%04x "
                        "dsp_page=0x%04x 3f70=0x%04x TC=%d insn=%u\n",
                        exec_pc, exec_op, s->data[0x0fff], s->data[0x08E2],
                        s->data[0x3f70], (s->st0 & ST0_TC) ? 1 : 0, s->insn_count);
            }
        }
        if (exec_pc == 0xde97 || exec_pc == 0xde9c || exec_pc == 0xdddb || exec_pc == 0xde8b) {
            static unsigned db = 0;
            if (db++ < 50)
                fprintf(stderr, "[c54x] DE-BR PC=0x%04x data[0x3f70]=0x%04x A=0x%06llx TC=%d "
                        "insn=%u\n", exec_pc, s->data[0x3f70],
                        (unsigned long long)(s->a & 0xFFFFFF),
                        (s->st0 & ST0_TC) ? 1 : 0, s->insn_count);
        }
        /* HANDLER-PATH (read-only): the vec 28 handler runs but does not reach
         * the dispatch at 0xa51c. Traces the exact chain 0x00f0 (vector) ->
         * 0x7234 (handler) -> 0x013b (prologue) -> 0xa4e4 (scheduler) ->
         * 0xa4ff (CALL 0xb522) -> 0xa501/0xa507 -> 0xa51c (dispatch) or
         * 0xa509 (arm IMR), to see where it leaves the path. 120 lines. */
        switch (exec_pc) {
        case 0x00f0: case 0x7234: case 0x013b: case 0xa4e4: case 0xa4ff:
        case 0xb522: case 0xa501: case 0xa507: case 0xa51c: case 0xa509:
        case 0xa582: case 0x011e: case 0xa4c7: case 0xa4ca:
        case 0x703d: case 0xa9ea: case 0xb3ec: {
            static unsigned hp = 0;
            if (hp++ < 120)
                fprintf(stderr, "[c54x] HANDLER-PATH PC=0x%04x A=0x%06llx SP=0x%04x "
                        "INTM=%d d[0x435b]=0x%04x d[0x3f70]=0x%04x insn=%u\n",
                        exec_pc, (unsigned long long)(s->a & 0xFFFFFF), s->sp,
                        (s->st1 & ST1_INTM) ? 1 : 0, s->data[0x435b],
                        s->data[0x3f70], s->insn_count);
            break;
        }
        default: break;
        }
        /* ENTRY-A4CA (read-only): HOW the DSP enters the wait loop at 0xa4ca -
         * the exact caller (the previous exec_pc outside 0xa4ca..0xa4e2) and
         * the state. Tells a normal entry through go-live 0xa500 (INTM already
         * cleared) from a direct branch or soft-vector, which bypasses go-live
         * and leaves INTM set. 20 lines. */
        {
            static uint16_t prev_pc = 0;
            if (exec_pc == 0xa4ca && (prev_pc < 0xa4ca || prev_pc > 0xa4e2)) {
                static unsigned ea = 0;
                if (ea++ < 20)
                    fprintf(stderr, "[c54x] ENTRY-A4CA #%u FROM PC=0x%04x INTM=%d "
                            "IMR=0x%04x data[0x3f6d]=0x%04x d[434e]=%04x d[434f]=%04x "
                            "insn=%u\n",
                            ea, prev_pc, (s->st1 & ST1_INTM) ? 1 : 0, s->imr,
                            s->data[0x3f6d], s->data[0x434e], s->data[0x434f],
                            s->insn_count);
            }
            prev_pc = exec_pc;
        }
        /* B19D-WATCH (read-only): is the ARM-command dispatch 0xb19d -> go-live
         * 0xa500 ever reached? GOLIVE-WATCH already covers 0xa500 and never
         * sees anything past 0xa4e2, so this watches the upstream side. No hit
         * means the DSP never processes d_dsp_page and is stuck in the wrong
         * loop before go-live. 30 lines. */
        if (exec_pc >= 0xb19d && exec_pc <= 0xb1b0) {
            static unsigned bw = 0;
            if (bw++ < 30)
                fprintf(stderr, "[c54x] B19D-WATCH #%u PC=0x%04x op=0x%04x INTM=%d "
                        "A=0x%06llx data[0x08E2]=0x%04x insn=%u\n",
                        bw, exec_pc, exec_op, (s->st1 & ST1_INTM) ? 1 : 0,
                        (unsigned long long)(s->a & 0xFFFFFF),
                        s->data[0x08E2], s->insn_count);
        }
        /* AAD5-TRACE: the go-live/AFC loop at 0xa4ca never releases (the BC at
         * 0xa4cd is AEQ on A == 0), and A comes from CALL 0xaad5. Traces
         * 0xaad5-0xaae6, the code that sets A, instruction by instruction with
         * AR0/AR1/A/TC, the ring counters data[0x434e]/data[0x434f] and the
         * candidate flags data[0x3f70]/data[0x3f92]/data[0x435b]: a counter
         * that never advances points at a decode or data bug, an idle wait at
         * a missing interrupt (IMR == 0). The verdict at the 0xa4cd gate is
         * logged too. 100 lines. */
        if (exec_pc >= 0xaad5 && exec_pc <= 0xaae6) {
            static unsigned at = 0;
            if (at++ < 100)
                fprintf(stderr, "[c54x] AAD5 #%u PC=0x%04x op=0x%04x A=0x%06llx "
                        "AR0=%04x AR1=%04x BK=%04x TC=%d d[434e]=%04x d[434f]=%04x "
                        "insn=%u\n",
                        at, exec_pc, exec_op, (unsigned long long)(s->a & 0xFFFFFF),
                        s->ar[0], s->ar[1], s->bk, (s->st0 & ST0_TC) ? 1 : 0,
                        s->data[0x434e], s->data[0x434f], s->insn_count);
        }
        if (exec_pc == 0xa4cd) {     /* the BC AEQ gate: why is A == 0? */
            static unsigned gt = 0;
            if (gt++ < 30)
                fprintf(stderr, "[c54x] AFC-GATE #%u @0xa4cd A=0x%06llx (==0?%d) TC=%d "
                        "d[3f70]=%04x d[3f92]=%04x d[435b]=%04x d[3fde]=%04x insn=%u\n",
                        gt, (unsigned long long)(s->a & 0xFFFFFF),
                        ((s->a & 0xFFFFFFFFFFULL) == 0), (s->st0 & ST0_TC) ? 1 : 0,
                        s->data[0x3f70], s->data[0x3f92], s->data[0x435b],
                        s->data[0x3fde], s->insn_count);
        }

        /* INTM-CLEAR (ungated): the first INTM 1->0 transition of the run means
         * RSBX INTM finally executed, i.e. interrupts are globally enabled. */
        {
            static int prev_intm = -1;
            int now_intm = (s->st1 & ST1_INTM) ? 1 : 0;
            if (prev_intm == 1 && now_intm == 0) {
                static unsigned ic = 0;
                if (ic++ < 10)
                    fprintf(stderr, "[c54x] *** INTM-CLEAR #%u : INTM 1->0 @PC=0x%04x "
                            "IMR=0x%04x insn=%u — interruptions ARMEES ! ***\n",
                            ic, exec_pc, s->imr, s->insn_count);
            }
            prev_intm = now_intm;
        }

        /* MVDESYNC: operand words must never be executed as instructions.
         * Fires when the PC lands on an MVKD operand word (0xb3ce, 0xb3d1,
         * 0xb3d4) or an MVDK one (0xb3dd, 0xb3e0, 0xb3e3), which means a
         * residual mis-decode. Silence is the expected result. 40 lines. */
        if (exec_pc==0xb3ce||exec_pc==0xb3d1||exec_pc==0xb3d4||
            exec_pc==0xb3dd||exec_pc==0xb3e0||exec_pc==0xb3e3) {
            static unsigned md=0;
            if (md++<40)
                fprintf(stderr, "[c54x] MVDESYNC #%u PC=0x%04x op=0x%04x "
                        "(mot-operande execute = mis-decode!) from=0x%04x insn=%u\n",
                        md, exec_pc, exec_op, g_prev_pc, s->insn_count);
        }
        /* Values of the two CALA slots; the third is the BACC at 0xb40f,
         * already covered by BACC-DISP. */
        if (exec_pc==0xb3a5) {
            static unsigned p5=0;
            if (p5++<30)
                fprintf(stderr, "[c54x] CALA-SLOT1 @0xb3a5 data[0x0c36]=0x%04x "
                        "A=0x%06llx SP=0x%04x insn=%u\n",
                        s->data[0x0c36], (unsigned long long)(s->a & 0xFFFFFF),
                        s->sp, s->insn_count);
        }
        if (exec_pc==0xb3e6) {
            static unsigned p6=0;
            if (p6++<30)
                fprintf(stderr, "[c54x] CALA-SLOT2 @0xb3e6 A=0x%06llx "
                        "data[0x3f6b]=0x%04x SP=0x%04x insn=%u\n",
                        (unsigned long long)(s->a & 0xFFFFFF), s->data[0x3f6b],
                        s->sp, s->insn_count);
        }
        /* Does the init ever program IPTR=0x140 (vector base 0xa000, so INT3
         * lands at 0xa04c, the real frame handler)? One-shot. */
        {
            static int seen140=0;
            uint16_t iptr_now=(s->pmst >> PMST_IPTR_SHIFT) & 0x1FF;
            if (iptr_now==0x140 && !seen140) {
                seen140=1;
                fprintf(stderr, "[c54x] *** IPTR=0x140 REACHED *** PMST=0x%04x "
                        "PC=0x%04x insn=%u\n", s->pmst, exec_pc, s->insn_count);
            }
        }
        /* Post-bootstub-RET probe: prologue at 0x7013 (PSHM of the context) and
         * epilogue at 0x7020 (POPM ar1/st0/st1/pmst, then RET). An epilogue
         * that pops a stale context or return PC, with no matching prologue or
         * interrupt, means the stack desynchronised after the BACC.
         * retPC = data[SP+4], after the four POPMs. */
        if (exec_pc == 0x7013) {
            static unsigned pbp = 0;
            if (pbp++ < 40)
                fprintf(stderr, "[c54x] PBPRO #%u from=0x%04x SP=0x%04x insn=%u\n",
                        pbp, g_prev_pc, s->sp, s->insn_count);
        }
        if (exec_pc == 0x7020) {
            static unsigned pbr = 0;
            if (pbr++ < 40)
                fprintf(stderr, "[c54x] PBRET #%u from=0x%04x SP=0x%04x pop[ar1=0x%04x "
                        "st0=0x%04x st1=0x%04x pmst=0x%04x retPC=0x%04x] insn=%u\n",
                        pbr, g_prev_pc, s->sp,
                        s->data[s->sp], s->data[(uint16_t)(s->sp+1)],
                        s->data[(uint16_t)(s->sp+2)], s->data[(uint16_t)(s->sp+3)],
                        s->data[(uint16_t)(s->sp+4)], s->insn_count);
        }
        }   /* C54X_SONDES : SM-TRACE .. PBRET */

        {
            /* @BEQUILLE - GOLIVE_REDIRECT  (CALYPSO_DSP_GOLIVE_BOOT, EXISTS, default OFF)
             *   masks   : writes s->pc = 0xb3ec when the DSP reaches 0xb3ff, i.e. the
             *             go-live soft-vector choice the boot ROM does not make in this
             *             model. The DSP then runs the go-live path in the FOREGROUND,
             *             setting up its own context - this is a redirect, not a
             *             vectorisation onto an unset context. Side effect: inhibits
             *             VEC28-FORCE in c54x_interrupt_ex.
             *   remove  : when data[0x3f6d] is populated by the ROM path and points at
             *             0xa4c7.
             */
            static int g_golive = -1;
            if (g_golive < 0) g_golive = calypso_gate("CALYPSO_DSP_GOLIVE_BOOT", 0);
            if (g_golive) {
                static int gdone = 0;
                if (!gdone && s->pc == 0xb3ff) {
                    gdone = 1;
                    fprintf(stderr, "[c54x] GOLIVE-REDIRECT pc 0xb3ff(wait) -> 0xb3ec(go-live "
                            "path: set flags + CALL 0xa9ea + BACC 0x703d -> contexte + IMR) "
                            "IMR=0x%04x SP=0x%04x insn=%u\n",
                            s->imr, s->sp, s->insn_count);
                    s->pc = 0xb3ec;
                }
            }
        }
        /* [2026-09-23] Sondes pures DERAIL-ZERO, DERAIL-ORIGIN. */
        if (C54X_SONDES) {
        /* DERAIL-ZERO: entering 0x0000-0x0008 means a CALA through a null
         * function pointer (an empty SARAM dispatcher slot). Logs the CALA site
         * (g_prev_pc), the accumulators and the four ROM words BEFORE the CALA,
         * i.e. the `ld *(slot),b` whose slot address is the one to decode. */
        if (exec_pc <= 0x0008 && g_prev_pc > 0x0008) {
            static unsigned dz = 0;
            if (dz++ < 60) {
                uint16_t p⟨1⟩ = prog_fetch(s, (uint16_t)(g_prev_pc - ⟨2⟩));  ×4
    ⟨⟩ = (4,4) (3,3) (2,2) (1,1)
                fprintf(stderr, "[c54x] DERAIL-ZERO #%u PC=0x%04x from=0x%04x op=0x%04x "
                        "A=0x%010llx B=0x%010llx SP=0x%04x prevwords=[%04x %04x %04x %04x] "
                        "insn=%u\n",
                        dz, exec_pc, g_prev_pc, g_prev_op,
                        (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                        (unsigned long long)(s->b & 0xFFFFFFFFFFULL), s->sp,
                        p4, p3, p2, p1, s->insn_count);
                if (dz == 1) {
                    /* SP ring at the first storm: the last 64 push/pop events
                     * (pc, op, delta), which name the instruction that
                     * over-pops and drifts SP. */
                    fprintf(stderr, "[c54x] SP-RING-AT-STORM (64 derniers, ancien->recent ; MISMATCH = op qui touche SP a tort):\n");
                    for (int k = 63; k >= 0; k--) {
                        struct sp_evt *e = &g_spring[(g_spring_idx - 1 - k) & 63];
                        const char *mn = (e->op == 0xFC00) ? "RET" : classify_xfer_op(e->op);
                        int exp = 99;  /* 99 = not a transfer op (legitimate PSHM/POPM/FRAME/STM-SP, or unknown) */
                        if (e->op == 0xFC00) exp = +1;                                  /* RET */
                        else if (!strcmp(mn,"CALL")||!strcmp(mn,"CALLD")||!strcmp(mn,"CALA")) exp = -1;
                        else if (!strcmp(mn,"FCALL")||!strcmp(mn,"FCALLD")||!strcmp(mn,"FCALA")||!strcmp(mn,"FCALAD")) exp = -2;
                        else if (!strcmp(mn,"RETE")) exp = +1;
                        else if (!strcmp(mn,"FRET")||!strcmp(mn,"FRETD")) exp = +2;
                        else if (!strcmp(mn,"B")||!strcmp(mn,"BD")||!strcmp(mn,"BACC")||!strcmp(mn,"FB")||!strcmp(mn,"FBD")||!strcmp(mn,"FBACC")||!strcmp(mn,"FBACCD")) exp = 0;
                        const char *flag = "";
                        if (exp != 99 && e->delta != exp) flag = "  <<< MISMATCH (delta != mnemo)";
                        else if (exp == 99 && e->delta != 0) flag = "  <<< NON-XFER touche SP (PSHM/POPM/FRAME? ou parasite)";
                        fprintf(stderr, "  pc=0x%04x op=0x%04x %-7s delta=%+d(exp%+d) sp=0x%04x%s\n",
                                e->pc, e->op, mn, e->delta, exp, e->sp, flag);
                    }
                }
            }
        }

        /* DERAIL-ORIGIN: first entry into the garbage table area 0xf000-0xf0ff
         * from outside it, i.e. the jump or fall-through that derails. Logs
         * where it came from (g_prev_pc), the opcode, the ARs (already garbage
         * or not) and SP. */
        if (exec_pc >= 0xf000 && exec_pc <= 0xf0ff
            && (g_prev_pc < 0xf000 || g_prev_pc > 0xf0ff)) {
            static unsigned dor = 0;
            if (dor++ < 60) {
                uint16_t q2 = prog_fetch(s, (uint16_t)(g_prev_pc - 2));
                uint16_t q1 = prog_fetch(s, (uint16_t)(g_prev_pc - 1));
                fprintf(stderr, "[c54x] DERAIL-ORIGIN #%u into=0x%04x from=0x%04x op=0x%04x "
                        "prev=[%04x %04x] AR[2,3,4,5]=%04x,%04x,%04x,%04x SP=0x%04x XPC=%u "
                        "insn=%u\n",
                        dor, exec_pc, g_prev_pc, g_prev_op, q2, q1,
                        s->ar[2], s->ar[3], s->ar[4], s->ar[5], s->sp, s->xpc,
                        s->insn_count);
            }
        }
        }   /* C54X_SONDES : DERAIL-ZERO, DERAIL-ORIGIN */
        /* Silent capture of the LUT slot read at 0x834d
         * (LD (DP<<7|0x07)<<1,A). One compare per instruction and no log, so
         * no timing impact. Feeds the BLACKHOLE-CALA probe (self-CALA at
         * 0x70c3). */
        if (s->pc == 0x834d) {
            g_disp_lut_ea  = (uint16_t)(((s->st0 & 0x1FF) << 7) | 0x07);
            g_disp_lut_val = s->data[g_disp_lut_ea];
        }
        uint16_t sp_before_exec = s->sp;
        uint16_t ds_before = s->delay_slots;  /* delay slots are counted in WORDS */
        /* SHADOW-DADST pre-capture (read-only). Gated on the DADST/DSADT
         * opcode (0x5a/0x5b/0x5e/0x5f) everywhere, not on PC 0x9a80, which
         * varies between runs (narrow versus wide). This family falls into
         * SFTL (case 0x5). Captures the state BEFORE, for the A and B deltas,
         * the AR5 walk and the shadow-correct result; the logged PC shows
         * where it actually runs. */
        int64_t  sd_a0 = s->a, sd_b0 = s->b;
        uint16_t sd_ar5_0 = s->ar[5], sd_lhi = 0, sd_llo = 0;
        uint8_t  sd_sub = (exec_op >> 8) & 0xFF;
        int sd_armed = C54X_SONDES && (sd_sub == 0x5a || sd_sub == 0x5b
                        || sd_sub == 0x5e || sd_sub == 0x5f);   /* [2026-09-23] sonde */
        if (sd_armed) {
            sd_lhi = s->data[s->ar[5]];
            sd_llo = s->data[(uint16_t)(s->ar[5] + 1)];
        }
        /* PISTE-PC: step-by-step trace of a PC RANGE, with the real opcode as
         * the core sees it and the registers BEFORE execution. Env:
         * CALYPSO_PISTE_LO / CALYPSO_PISTE_HI / CALYPSO_PISTE_N (default 400).
         * Motivation: the stage that rewrites the soft-bit buffer at 0x2a00
         * emits 142 zeros on 7 of 12 SB jobs and sane values on the other 5.
         * Unlike the probes in c54x_mem.c, the PC printed here is the one
         * BEFORE the advance, so it is the instruction's real PC. */
        if (C54X_SONDES) {   /* [2026-09-23] sonde pure */
            static int pi_init = 0; static long pi_lo = -1, pi_hi = -1, pi_max = 400, pi_n = 0;
            if (!pi_init) { pi_init = 1;
                const char *l = calypso_getenv("CALYPSO_PISTE_LO"), *h = calypso_getenv("CALYPSO_PISTE_HI"),
                           *n = calypso_getenv("CALYPSO_PISTE_N");
                if (l && *l) pi_lo = strtol(l, NULL, 0);
                if (h && *h) pi_hi = strtol(h, NULL, 0); else pi_hi = pi_lo;
                if (n && *n) pi_max = strtol(n, NULL, 0);
            }
            if (pi_lo >= 0 && s->pc >= (uint16_t)pi_lo && s->pc <= (uint16_t)pi_hi
                && pi_n < pi_max) {
                pi_n++;
                fprintf(stderr, "[c54x] PISTE pc=0x%04x op=0x%04x A=0x%010llx B=0x%010llx "
                        "T=0x%04x AR0=%04x AR2=%04x AR3=%04x AR4=%04x AR5=%04x AR6=%04x AR7=%04x "
                        "BRC=%u rpt=%u insn=%u mot_suivant=0x%04x\n",
                        s->pc, exec_op, (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                        (unsigned long long)(s->b & 0xFFFFFFFFFFULL), s->t,
                        s->ar[0], s->ar[2], s->ar[3], s->ar[4], s->ar[5], s->ar[6], s->ar[7],
                        (unsigned)s->brc, (unsigned)s->rpt_count, (unsigned)s->insn_count,
                        prog_fetch(s, (uint16_t)(s->pc + 1)));
                /* Words POINTED AT by AR2..AR5, read straight from data[]
                 * (these addresses are internal RAM, outside the OVLY/MMR
                 * aliases), to see what a dual-operand mpy/mac really
                 * consumes. */
                { /* Direct addressing: DP, CPL, SP and both candidate
                   * addresses with their contents. A `ld Smem,T` that loads 0
                   * may be reading the right (empty) cell or the wrong one;
                   * only the resolved address decides. */
                    uint16_t dpv = s->st0 & 0x1FF;
                    uint16_t adp = (uint16_t)((dpv << 7) | (exec_op & 0x7F));
                    uint16_t asp = (uint16_t)(s->sp + (exec_op & 0x7F));
                    fprintf(stderr, "[c54x] PISTE   DP=0x%03x CPL=%d SP=%04x | direct@DP=0x%04x:%04x "
                            "direct@SP=0x%04x:%04x | indirect=%s\n",
                            dpv, (s->st1 & 0x4000) ? 1 : 0, s->sp,
                            adp, s->data[adp], asp, s->data[asp],
                            (exec_op & 0x80) ? "oui" : "non");
                }
                { /* Optional dump of a data range on each pass, to see a
                   * healthy job diverge from one that emits zeros. */
                    static int pd_init = 0; static long pd_lo = -1, pd_hi = -1;
                    if (!pd_init) { pd_init = 1;
                        const char *l = calypso_getenv("CALYPSO_PISTE_DUMP_LO");
                        const char *h = calypso_getenv("CALYPSO_PISTE_DUMP_HI");
                        if (l && *l) pd_lo = strtol(l, NULL, 0);
                        if (h && *h) pd_hi = strtol(h, NULL, 0); else pd_hi = pd_lo;
                    }
                    if (pd_lo >= 0) {
                        for (long a = pd_lo; a <= pd_hi; a += 8) {
                            fprintf(stderr, "[c54x] PISTE-RAM 0x%04lx:", a);
                            for (long k = a; k < a + 8 && k <= pd_hi; k++)
                                fprintf(stderr, " %04x", s->data[(uint16_t)k]);
                            fprintf(stderr, "\n");
                        }
                    }
                }
                fprintf(stderr, "[c54x] PISTE   *AR2[%04x]=%04x *AR3[%04x]=%04x "
                        "*AR4[%04x]=%04x *AR5[%04x]=%04x\n",
                        s->ar[2], s->data[s->ar[2]], s->ar[3], s->data[s->ar[3]],
                        s->ar[4], s->data[s->ar[4]], s->ar[5], s->data[s->ar[5]]);
            }
        }
        uint16_t t_avant_piste = s->t; uint16_t pc_avant_piste = s->pc;
        int64_t a_avant_piste = s->a, b_avant_piste = s->b;
        consumed = c54x_exec_one(s);
        ev_anneau->sp_apres = s->sp;                              /* ANNEAU-EXEC */
        ev_anneau->it = (g_c54x_it_prises != it_avant);
        ev_anneau->it_n = (uint16_t)g_c54x_it_prises;
        /* [2026-09-21] OVERFLOW AND OVM. The core computed 40-bit results and
         * never set OVA/OVB nor honoured OVM (grep: OVM appeared in comments
         * only). On silicon every ALU/MAC/shift result that leaves the 32-bit
         * range sets OVdst and, with OVM=1, is saturated to 0x7FFFFFFF /
         * 0xFF80000000 (SPRU172C 3.x "overflow handling"). The normal-burst
         * demodulator of the ROM sets and resets OVM 20 times per burst and
         * SATs 214 times: its arithmetic relies on this. Measured before the
         * fix: the demod of a normal burst was perfect or random depending on
         * the data (bursts 2 of SI3, 1 of SI1/SI4 only), the same burst in
         * four positions gave four results, and the ISA scorecard failed NEG,
         * ABS, RND on exactly these semantics. Applied after the instruction
         * on the accumulator(s) it wrote; dual 16-bit mode (C16) is exempt
         * ("unsaturated"). CALYPSO_C54X_OVM=0 restores the old behaviour. */
        {
            static int ovm_on = -1;
            if (ovm_on < 0) { const char *e = getenv("CALYPSO_C54X_OVM"); ovm_on = (e && *e == '0') ? 0 : 1; }
            /* CALYPSO_C54X_OVM_LOGIQUE=1: LOGICAL instructions do not saturate (SPRU172C: SFTL,
             * AND/OR/XOR, ROL/ROR/ROLTC do not depend on OVM). Saturating `sftl a,11` in the FR
             * codec (PDROM 0xe4c4, reading bits 0-2 of the a_dd_0 header with OVM=1) would turn
             * the sign-extended header c200 into 0x7fffffff -> bits "111" -> every frame judged
             * bad -> parameters replaced -> mute speech. */
            static int ovm_logique = -1;
            if (ovm_logique < 0) ovm_logique = calypso_gate("CALYPSO_C54X_OVM_LOGIQUE", 0);
            bool op_logique = ovm_logique && (
                (exec_op & 0xFC80) == 0xF080 ||                                    /* SFTL, AND/OR/XOR acc,shift */
                (exec_op & 0xFCF0) == 0xF030 || (exec_op & 0xFCF0) == 0xF040 ||    /* AND/OR #lk,shift */
                (exec_op & 0xFCF0) == 0xF050 ||                                    /* XOR #lk,shift */
                (exec_op & 0xFCFF) == 0xF063 || (exec_op & 0xFCFF) == 0xF064 ||    /* AND/OR #lk,16 */
                (exec_op & 0xFCFF) == 0xF065 ||                                    /* XOR #lk,16 */
                (exec_op & 0xFE00) == 0x1800 || (exec_op & 0xFE00) == 0x1A00 ||    /* AND/OR Smem */
                (exec_op & 0xFE00) == 0x1C00 ||                                    /* XOR Smem */
                (exec_op & 0xFEFC) == 0xF490);                                     /* ROR/ROL/ROLTC */
            if (ovm_on && !(s->st1 & ST1_C16) && !op_logique) {
                for (int k = 0; k < 2; k++) {
                    int64_t *acc = k ? &s->b : &s->a;
                    if (*acc == (k ? b_avant_piste : a_avant_piste)) continue;
                    int64_t v = sext40(*acc);
                    if (v > 0x7FFFFFFFLL || v < -0x80000000LL) {
                        s->st0 |= k ? ST0_OVB : ST0_OVA;
                        /* The saturated negative value must be -2^31 SIGN-EXTENDED, like every
                         * accumulator of the core (sext40): (int64_t)0xFF80000000LL is a POSITIVE
                         * int64, and a following arithmetic shift (STH src,ASM,Smem...) would yield
                         * +0x7fc0 instead of -64 (scaling of the SCH soft values, PROM0 0x7e6f mpyr /
                         * 0x7e70 sfta a,3 / 0x7e71 sth a,asm -> wrong SB as soon as the soft values
                         * saturate). CALYPSO_SAT_NEG_ANCIEN=1 = positive int64 variant. */
                        static int sat_neg_ancien = -1;
                        if (sat_neg_ancien < 0) { const char *e = getenv("CALYPSO_SAT_NEG_ANCIEN"); sat_neg_ancien = (e && *e == '1') ? 1 : 0; }
                        if (s->st1 & ST1_OVM) *acc = (v < 0) ? (sat_neg_ancien ? (int64_t)0xFF80000000LL : -0x80000000LL) : 0x7FFFFFFFLL;
                    }
                }
            }
        }
        /* PISTE-T: logs EVERY change of the T register inside an instruction
         * window (CALYPSO_T_LO / CALYPSO_T_HI, in insns). Motivation: every
         * mpy of the SB chain computes T*Smem with T == 0 while the memory
         * operands are sane, so who loads T and when has to be measured, not
         * guessed.
         * [2026-09-23] Sondes pures PISTE-T .. DECODE-AUDIT derriere
         * l'interrupteur. */
        if (C54X_SONDES) {
        {
            static int ti_init = 0; static long ti_lo = -1, ti_hi = -1;
            if (!ti_init) { ti_init = 1;
                const char *l = calypso_getenv("CALYPSO_T_LO"), *h = calypso_getenv("CALYPSO_T_HI");
                if (l && *l) ti_lo = strtol(l, NULL, 0);
                if (h && *h) ti_hi = strtol(h, NULL, 0);
            }
            int dans_fenetre = (ti_lo >= 0 && s->insn_count >= (unsigned)ti_lo
                                && s->insn_count <= (unsigned)ti_hi);
            if (dans_fenetre && s->t != t_avant_piste)
                fprintf(stderr, "[c54x] PISTE-T pc=0x%04x op=0x%04x T: 0x%04x -> 0x%04x insn=%u\n",
                        pc_avant_piste, exec_op, t_avant_piste, s->t, (unsigned)s->insn_count);
            /* Who zeroes: non-zero -> zero transitions of an accumulator. The
             * fault propagates step by step ("zero because its input is
             * zero"), so what matters is the FIRST zeroing. */
            if (dans_fenetre && a_avant_piste != 0 && s->a == 0)
                fprintf(stderr, "[c54x] PISTE-NUL pc=0x%04x op=0x%04x A: 0x%010llx -> 0 insn=%u\n",
                        pc_avant_piste, exec_op,
                        (unsigned long long)(a_avant_piste & 0xFFFFFFFFFFULL),
                        (unsigned)s->insn_count);
            if (dans_fenetre && b_avant_piste != 0 && s->b == 0)
                fprintf(stderr, "[c54x] PISTE-NUL pc=0x%04x op=0x%04x B: 0x%010llx -> 0 insn=%u\n",
                        pc_avant_piste, exec_op,
                        (unsigned long long)(b_avant_piste & 0xFFFFFFFFFFULL),
                        (unsigned)s->insn_count);
        }
        /* SP-COLLAPSE probe (read-only): catches the exact instruction that
         * collapses SP below 0x0800 (the first plus 30 more), which seeds the
         * whole cascade (boot stub, spin at 0xc6ac). */
        {
            static uint16_t sp_prev = 0xffff;
            static unsigned spc_n = 0;
            if (sp_prev >= 0x0800 && s->sp < 0x0800 && spc_n < 30) {
                spc_n++;
                fprintf(stderr, "[c54x] SP-COLLAPSE #%u exec_pc=0x%04x exec_op=0x%04x "
                        "prev_PC=0x%04x prev_op=0x%04x SP 0x%04x->0x%04x B=0x%010llx insn=%u\n",
                        spc_n, exec_pc, exec_op, s->last_exec_pc, s->last_exec_op,
                        sp_prev, s->sp,
                        (unsigned long long)(s->b & 0xFFFFFFFFFFULL), s->insn_count);
            }
            sp_prev = s->sp;
        }
        /* FEXX-ENTRY probe (read-only): captures the jump or vector that
         * transfers control INTO the ROM range 0xfe00-0xff7f (data table plus
         * the init at 0xff00), the real entry of the derail. The static CALL
         * 0xfe00 at 0xfdd5 never executes, so it is a COMPUTED branch;
         * prev_PC/op is the culprit and A is the target when it is a CALA.
         * First plus 20 more. */
        {
            static int fe_was_in = 0;
            static unsigned fe_n = 0;
            int fe_in = (exec_pc >= 0xfe00 && exec_pc < 0xff80);
            if (fe_in && !fe_was_in && fe_n < 20) {
                fe_n++;
                fprintf(stderr, "[c54x] FEXX-ENTRY #%u entered 0x%04x from prev_PC=0x%04x "
                        "prev_op=0x%04x A=0x%010llx SP=0x%04x insn=%u\n",
                        fe_n, exec_pc, s->last_exec_pc, s->last_exec_op,
                        (unsigned long long)(s->a & 0xFFFFFFFFFFULL), s->sp, s->insn_count);
            }
            fe_was_in = fe_in;
        }
        /* HIGHVEC-ENTRY probe (read-only): entry into the IPTR=0x1FF vector
         * page [0xFF80..0xFFFF], where interrupts land (vec 19 = 0xffcc,
         * vec 21 = 0xffd4). The decisive question is whether control arrives
         * through an INTERRUPT DISPATCH (prev = preempted foreground, d_irq
         * near 0) or through a firmware BRANCH/CALL (prev = a branch opcode
         * targeting here, d_irq large). Also pins the register regime (ARs, A,
         * B) at the entry point. */
        {
            static int hv_was_in = 0;
            static unsigned hv_n = 0;
            int hv_in = (exec_pc >= 0xff80);
            if (hv_in && !hv_was_in && hv_n < 24) {
                hv_n++;
                uint16_t hv_iptr = (s->pmst >> PMST_IPTR_SHIFT) & 0x1FF;
                uint64_t d_irq = (uint64_t)s->insn_count - g_last_intr_insn;
                int irq_driven = (d_irq <= 2 && g_last_intr_vec >= 0);
                fprintf(stderr, "[c54x] HIGHVEC-ENTRY #%u entered 0x%04x prev_PC=0x%04x "
                        "prev_op=0x%04x | %s d_irq=%llu lastvec=%d fg_pc=0x%04x | "
                        "IPTR=0x%03x INTM=%d SP=0x%04x | AR3=0x%04x AR4=0x%04x AR5=0x%04x "
                        "A=0x%010llx B=0x%010llx insn=%u\n",
                        hv_n, exec_pc, s->last_exec_pc, s->last_exec_op,
                        irq_driven ? "IRQ-DISPATCH" : "FW-BRANCH",
                        (unsigned long long)d_irq, g_last_intr_vec, g_last_intr_fg_pc,
                        hv_iptr, !!(s->st1 & ST1_INTM), s->sp,
                        s->ar[3], s->ar[4], s->ar[5],
                        (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                        (unsigned long long)(s->b & 0xFFFFFFFFFFULL), s->insn_count);
            }
            hv_was_in = hv_in;
        }
        /* REGIME-PIN probe (read-only): pins the register regime of the
         * CURRENT run without ambiguity - two different regimes have been
         * observed (AR5=0x80 with AR3=0x000b, and AR3=AR4=0x2ace frozen in the
         * I/Q buffer), and only one can be debugged at a time. Full register
         * file dump on the 1st, 10000th and 1000000th visit of the foreground
         * spin [0x82c0..0x82f0], the frozen IQ-READ loop. */
        if (exec_pc >= 0x82c0 && exec_pc <= 0x82f0) {
            static unsigned rp_n = 0;
            rp_n++;
            if (rp_n == 1 || rp_n == 10000 || rp_n == 1000000) {
                uint16_t rp_iptr = (s->pmst >> PMST_IPTR_SHIFT) & 0x1FF;
                fprintf(stderr, "[c54x] REGIME-PIN #%u @0x%04x | "
                        "AR0=%04x AR1=%04x AR2=%04x AR3=%04x AR4=%04x AR5=%04x AR6=%04x AR7=%04x | "
                        "A=0x%010llx B=0x%010llx T=0x%04x | SP=0x%04x ST0=0x%04x ST1=0x%04x "
                        "PMST=0x%04x IPTR=0x%03x INTM=%d insn=%u\n",
                        rp_n, exec_pc,
                        s->ar[0], s->ar[1], s->ar[2], s->ar[3], s->ar[4], s->ar[5], s->ar[6], s->ar[7],
                        (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                        (unsigned long long)(s->b & 0xFFFFFFFFFFULL), s->t,
                        s->sp, s->st0, s->st1, s->pmst, rp_iptr,
                        !!(s->st1 & ST1_INTM), s->insn_count);
            }
        }
        /* SQURA-A probe (read-only): watches A become positive. Logs A at the
         * two SQURA (0x76ff, 0x7700) and at the RCD LEQ decision in 0x75e8,
         * which is taken when A <= 0. With the blind MAC, A stayed <= 0, the
         * RCD was taken and the stack over-popped; with SQURA decoded, A must
         * become positive and the RCD must not be taken. */
        if (exec_pc == 0x76ff || exec_pc == 0x7700 || exec_pc == 0x75e8) {
            static unsigned sqa_n = 0;
            if (sqa_n < 30) {
                sqa_n++;
                int64_t av = (s->a & 0x8000000000ULL) ? (s->a | ~0xFFFFFFFFFFLL) : (s->a & 0xFFFFFFFFFFLL);
                fprintf(stderr, "[c54x] SQURA-A pc=0x%04x op=0x%04x A=0x%010llx (signed=%lld) "
                        "T=0x%04x AR2=0x%04x %sinsn=%u\n",
                        exec_pc, exec_op, (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                        (long long)av, s->t, s->ar[2],
                        (exec_pc == 0x75e8) ? (av <= 0 ? "RCD-PREND(A<=0) " : "RCD-passe(A>0) ") : "",
                        s->insn_count);
            }
        }
        /* DISP-A probe (read-only): how A evolves through the dispatcher
         * 0x80b0-0x80c9 - which LD sets A = 0xfe36 (the garbage handler of the
         * FCALAD at 0x80c8) and from which data cell. */
        if (exec_pc >= 0x80b0 && exec_pc < 0x80ca) {
            static unsigned da_n = 0;
            uint16_t alo = (uint16_t)(s->a & 0xFFFF);
            int garbage = (exec_pc == 0x80c2 && alo >= 0xfe00); /* the LD that derails */
            if (da_n < 40 || garbage) {
                da_n++;
                uint16_t dp = s->st0 & 0x1FF;
                fprintf(stderr, "[c54x] DISP-A pc=0x%04x op=0x%04x A.lo=0x%04x DP=0x%03x "
                        "AR1=0x%04x AR2=0x%04x AR3=0x%04x d[DP:9]=0x%04x insn=%u%s\n",
                        exec_pc, exec_op, alo, dp, s->ar[1], s->ar[2], s->ar[3],
                        s->data[(uint16_t)((dp << 7) | 0x09)], s->insn_count,
                        garbage ? "  <<< A=GARBAGE handler = LE DERAIL" : "");
            }
        }
        /* DP-AT-DISP probe (read-only): tracks the LAST instruction that
         * changes DP (ST0[8:0]) and, when the dispatcher is entered (0x80b0),
         * logs DP and its writer. Finds who sets DP = 0x124, the wrong page:
         * the coefficient table at 0x9200 instead of a handler. */
        {
            static uint16_t dp_prev = 0xFFFF, dp_set_pc = 0, dp_set_op = 0;
            uint16_t dp_now = s->st0 & 0x1FF;
            if (dp_now != dp_prev) { dp_set_pc = exec_pc; dp_set_op = exec_op; dp_prev = dp_now; }
            if (exec_pc == 0x80b0) {
                static unsigned dpw_n = 0;
                if (dpw_n < 20 || dp_now == 0x124) {
                    dpw_n++;
                    fprintf(stderr, "[c54x] DP-AT-DISP DP=0x%03x set_by_PC=0x%04x set_op=0x%04x insn=%u%s\n",
                            dp_now, dp_set_pc, dp_set_op, s->insn_count,
                            (dp_now == 0x124) ? "  <<< BAD DP (coefficient page = LE DERAIL)" : "");
                }
            }
        }
        /* SHADOW-DADST post-compute and oracle (read-only, logs only: the
         * first 40 during boot, then one in 2000 in steady state). */
        if (sd_armed) {
            static unsigned sd_n = 0;
            if (sd_n < 40 || (sd_n % 2000) == 0) {
                int64_t a1 = s->a, b1 = s->b;
                int asm5 = s->st1 & ST1_ASM_MASK; if (asm5 & 0x10) asm5 -= 32; /* signed 5-bit ASM */
                uint64_t a0u = sd_a0 & 0xFFFFFFFFFFULL;
                int64_t pure_sh = (asm5 >= 0) ? (int64_t)(a0u << asm5)
                                              : (int64_t)(a0u >> (-asm5));
                int shiftLike = ((a1 & 0xFFFFFFFFFFULL) == (pure_sh & 0xFFFFFFFFFFULL)); /* OBS1 */
                int16_t T = (int16_t)s->t, lhi = (int16_t)sd_lhi, llo = (int16_t)sd_llo;
                int is_dadst = (sd_sub == 0x5a || sd_sub == 0x5b);
                int32_t sh_hi = is_dadst ? (lhi + T) : (lhi - T); /* SPRU131 dual-16; sign to confirm */
                int32_t sh_lo = is_dadst ? (llo - T) : (llo + T);
                int64_t shadow = (((int64_t)sh_hi & 0xFFFF) << 16) | (uint16_t)sh_lo; /* OBS3 */
                fprintf(stderr,
                  "[c54x] SHADOW-DADST #%u pc=0x%04x op=0x%04x xpc=%u C16=%d FRCT=%d ASM=%d BK=0x%04x\n"
                  "[c54x]   A %010llx->%010llx dA=%lld shiftLike=%d  B %010llx->%010llx dB=%lld\n"
                  "[c54x]   AR5 0x%04x->0x%04x walked=%d  T=0x%04x  Lmem@AR5 hi=0x%04x lo=0x%04x\n"
                  "[c54x]   shadow=%s hi%+d lo%+d packed=%010llx insn=%u\n",
                  sd_n, exec_pc, exec_op, s->xpc & 0xFF,
                  !!(s->st1 & ST1_C16), !!(s->st1 & ST1_FRCT), asm5, s->bk,
                  (unsigned long long)a0u, (unsigned long long)(a1 & 0xFFFFFFFFFFULL),
                  (long long)(a1 - sd_a0), shiftLike,
                  (unsigned long long)(sd_b0 & 0xFFFFFFFFFFULL),
                  (unsigned long long)(b1 & 0xFFFFFFFFFFULL), (long long)(b1 - sd_b0),
                  sd_ar5_0, s->ar[5], (s->ar[5] != sd_ar5_0),                       /* OBS2 */
                  s->t, sd_lhi, sd_llo, is_dadst ? "DADST" : "DSADT", sh_hi, sh_lo,
                  (unsigned long long)(shadow & 0xFFFFFFFFFFULL), s->insn_count);
            }
            sd_n++;
        }

        /* DECODE-AUDIT: decoder inventory over the correlator overlay
         * 0x8000-0x9FFF. Logs once per distinct (PC, op) the raw opcode, the
         * next word and the LENGTH consumed (including lk_used), to diff
         * against doc/opcodes/tic54x_hi8_map.md. Check the length column
         * first: a wrong length desynchronises the whole following stream, as
         * the 0x86/0x87 bug did before it turned into an SP runaway. The point
         * is to clear the whole decode/length/mode class at once instead of
         * peeling off one bug at a time. Deduplicated by (PC, op), so the XPC
         * variants of one page are captured too. Gate
         * CALYPSO_DEBUG=DECODE-AUDIT; cost is negligible once covered. */
        static int da_lo = -1, da_hi = -1;
        static long long da_insn = -1;
        if (da_lo < 0) {
            const char *l = calypso_getenv("CALYPSO_DA_LO"); const char *h = calypso_getenv("CALYPSO_DA_HI");
            const char *n = calypso_getenv("CALYPSO_DA_INSN");
            da_lo = l ? (int)strtol(l, NULL, 0) : 0x8000;   /* correlator overlay by default */
            da_hi = h ? (int)strtol(h, NULL, 0) : 0x9FFF;   /* CALYPSO_DA_LO/HI widen it (e.g. 0x7000..0xFFFF) */
            da_insn = n ? strtoll(n, NULL, 0) : 0;          /* CALYPSO_DA_INSN: skip the boot, aim at the detection window (e.g. 250000000) */
        }
        if (exec_pc >= (uint16_t)da_lo && exec_pc <= (uint16_t)da_hi
            && s->insn_count >= (uint64_t)da_insn
            && calypso_debug_enabled("DECODE-AUDIT")) {
            static uint16_t da_op_seen[0x10000];
            static uint8_t  da_has[0x10000];
            static unsigned da_n = 0;
            uint16_t da_op = prog_fetch(s, exec_pc);
            /* Skip op == 0x0000: empty PROM holes, a control-flow runaway
             * rather than a DECODE bug. They used to consume the cap during
             * boot on the 0xCB00-0xD3FF sweep. */
            if (da_op != 0x0000 && da_n < 4000
                && (!da_has[exec_pc] || da_op_seen[exec_pc] != da_op)) {
                da_has[exec_pc] = 1;
                da_op_seen[exec_pc] = da_op;
                da_n++;
                fprintf(stderr, "[c54x] DECODE-AUDIT PC=0x%04x op=%04x op2=%04x "
                        "hi8=%02x len=%d XPC=%u insn=%u\n",
                        exec_pc, da_op, prog_fetch(s, exec_pc + 1),
                        (da_op >> 8) & 0xFF, consumed, s->xpc & 0xFF,
                        s->insn_count);
            }
        }
        }   /* C54X_SONDES : PISTE-T .. DECODE-AUDIT */
        /* INTM-TRANS: every toggle of the INTM bit (ST1 bit 11) with the PC
         * and opcode that caused it. Answers "does INTM ever reach 0, and if
         * so who re-arms it". Silent unless CALYPSO_INTM_TRANS is set. */
        {
            static int g_intm_prev_tr = -1, g_intm_tr_en = -1;
            if (g_intm_tr_en < 0) { const char *e = calypso_getenv("CALYPSO_INTM_TRANS");
                                    g_intm_tr_en = (e && *e != 0) ? 1 : 0; }
            int intm_now_tr = !!(s->st1 & ST1_INTM);
            if (intm_now_tr != g_intm_prev_tr && g_intm_tr_en) {
                fprintf(stderr, "[c54x] INTM-TRANS %d->%d PC=0x%04x op=0x%04x "
                        "IFR=0x%04x IMR=0x%04x insn=%u\n",
                        g_intm_prev_tr, intm_now_tr, exec_pc, exec_op,
                        s->ifr, s->imr, s->insn_count);
            }

            /* @BEQUILLE - INTM_ACK  (CALYPSO_INTM_ACK, default 0)
             *   masks   : three gestures that belong to the other side and never
             *             arrive. (a) The ARM firmware's "new IRQ agreement" epilogue
             *             does not reach our INTH on the native path - real hardware
             *             writes IRQ_CTRL bit 0 there. (b) The ARM write of
             *             d_dsp_page is swallowed by the 2-byte overlay registered at
             *             priority 10 on 0xFFD001A8, so the cell keeps its boot value
             *             and B_GSM_TASK never reaches the DSP - real firmware sets
             *             d_dsp_page = B_GSM_TASK|w_page in dsp_end_scenario().
             *             (c) No time base drives TINT0, because
             *             calypso_tint0_start() is never called. All three are
             *             triggered from the DSP here instead.
             *   remove  : once the d_dsp_page overlay is replaced by a real
             *             pass-through (the ARM writes, the DSP reads) and the ROM's
             *             TIMER0 is actually programmed (TCR TSS=0). Each gesture then
             *             goes back to its owner.
             *
             * Transition direction:
             *   0->1 (interrupt taken) -> ACK ARM: the DSP accepts the request raised
             *                       by the ARM/TPU; close the agreement on the INTH
             *                       side and set d_dsp_page = B_GSM_TASK|w_page.
             *   1->0 (RETE)       -> ACK DSP: the DSP has finished its ISR; release
             *                       the frame interrupt LEVEL hold and flip the read
             *                       page (mirror of r_page ^= 1).
             *   either            -> TINT0 tick (vec 20 / IMR bit 4).
             *
             * CALYPSO_INTM_ACK_SWAP=1 swaps the two directions.
             * CALYPSO_INTM_ACK_NO_TINT0=1 keeps the acks without the tick.
             */
            if (intm_now_tr != g_intm_prev_tr && g_intm_prev_tr >= 0) {
                static int _ia = -1, _iswap = -1, _int0 = -1;
                static int _iarm = -1, _idsp = -1, _idpage = -1;
                static unsigned _idpage_every = 0;
                static int _busy = 0;
                if (_ia < 0) {
                    _ia    = calypso_gate("CALYPSO_INTM_ACK", 0);
                    _iswap = calypso_gate("CALYPSO_INTM_ACK_SWAP", 0);
                    _int0  = calypso_getenv("CALYPSO_INTM_ACK_NO_TINT0") ? 0 : 1;
                    /* Each gesture is separately switchable, for A/B. Default
                     * follows INTM_ACK, except the d_dsp_page write, which is
                     * explicitly opt-in.
                     *   CALYPSO_INTM_ACK_NO_ARM=1    disable the ARM ack (INTH)
                     *   CALYPSO_INTM_ACK_NO_DSP=1    disable the DSP ack (level hold)
                     *   CALYPSO_INTM_ACK_DPAGE=1     enable the d_dsp_page write
                     *   CALYPSO_INTM_ACK_DPAGE_EVERY default 65536 insns (= the
                     *                                measured TIMER0 period = frame
                     *                                cadence)
                     *
                     * The period matters: writing d_dsp_page on every interrupt taken
                     * fires ~1000 times, whereas dsp_end_scenario() writes it ONCE PER
                     * FRAME (osmocom-bb layer1/sync.c -> calypso/dsp.c:471). That
                     * hammered the DSP synchronisation cell at ~50x the frame
                     * cadence. */
                    _iarm   = calypso_getenv("CALYPSO_INTM_ACK_NO_ARM") ? 0 : 1;
                    _idsp   = calypso_getenv("CALYPSO_INTM_ACK_NO_DSP") ? 0 : 1;
                    _idpage = calypso_gate("CALYPSO_INTM_ACK_DPAGE", 0);
                    { const char *_e = calypso_getenv("CALYPSO_INTM_ACK_DPAGE_EVERY");
                      _idpage_every = (_e && *_e) ? (unsigned)strtoul(_e, NULL, 0) : 65536u;
                      if (_idpage_every < 1) _idpage_every = 65536u; }
                    if (_ia)
                        fprintf(stderr, "[c54x] INTM_ACK=1 (BEQUILLE) : ack ARM sur "
                                "%s (INTH=%d dpage=%d/%u insn), ack DSP sur %s (=%d), "
                                "tick TINT0=%d\n",
                                _iswap ? "1->0 (RETE)" : "0->1 (IT prise)",
                                _iarm, _idpage, _idpage_every,
                                _iswap ? "0->1 (IT prise)" : "1->0 (RETE)", _idsp, _int0);
                }
                if (_ia && !_busy) {
                    _busy = 1;
                    int _entree = _iswap ? (intm_now_tr == 0) : (intm_now_tr == 1);

                    if (_entree) {
                        /* --- ACK ARM ------------------------------------- */
                        if (_iarm) calypso_inth_arm_ack();
                        /* d_dsp_page = B_GSM_TASK | w_page (dsp.c:471), at most
                         * once per frame - see the note above. */
                        if (_idpage) {
                            static unsigned _last = 0;
                            if (_last == 0 || (s->insn_count - _last) >= _idpage_every) {
                                static uint16_t _wpage = 0;
                                uint16_t _v = (uint16_t)(0x0002 | _wpage);
                                s->data[0x08D4] = _v;
                                if (s->api_ram) s->api_ram[0x08D4 - C54X_API_BASE] = _v;
                                _wpage ^= 1;                  /* w_page ^= 1 */
                                _last = s->insn_count;
                                static unsigned _n = 0;
                                if (_n++ < 40)
                                    fprintf(stderr, "[c54x] INTM_ACK ARM : IRQ agreement + "
                                            "d_dsp_page=0x%04x (B_GSM_TASK|w_page) PC=0x%04x "
                                            "insn=%u\n", _v, exec_pc, s->insn_count);
                            }
                        }
                    } else if (_idsp) {
                        /* --- ACK DSP ------------------------------------- */
                        g_frame_it_level = false;   /* release the LEVEL hold */
                        if (g_last_intr_vec >= 0) {
                            /* end of service: the source that was served drops */
                            if (g_last_intr_vec == 28)
                                c54x_ifr_clear(s, (uint16_t)(1u << 12), "intm-ack-dsp");
                            g_last_intr_vec = -1;
                        }
                        /* mirror of sync.c: r_page ^= 1 after consumption */
                        {
                            static uint16_t _rpage = 0;
                            _rpage ^= 1;
                            static unsigned _n = 0;
                            if (_n++ < 40)
                                fprintf(stderr, "[c54x] INTM_ACK DSP : level hold relache, "
                                        "r_page=%u PC=0x%04x insn=%u\n",
                                        _rpage, exec_pc, s->insn_count);
                        }
                    }

                    /* --- TINT0 tick (vec 20 / IMR bit 4) ------------------ */
                    if (_int0) {
                        static unsigned _n = 0;
                        if (_n++ < 40)
                            fprintf(stderr, "[c54x] INTM_ACK TINT tick "
                                    "IMR=0x%04x IFR=0x%04x insn=%u\n",
                                    s->imr, s->ifr, s->insn_count);
                        c54x_fire_tint(s);   /* honours the IMR; SPRU131 5.1: bit 3 / vec 19 */
                    }
                    _busy = 0;
                }
            }

            g_intm_prev_tr = intm_now_tr;
        }

        /* [2026-09-23] Sondes pures SP event ring (g_spring, g_sp_ledger,
         * SP-DANGER, ORPHAN) .. XPCWATCH, derriere l'interrupteur. */
        if (C54X_SONDES) {
        /* SP event ring: records every SP change (push or pop) with the PC and
         * opcode responsible. Feeds the BLACKHOLE-CALA dump. */
        if (s->sp != sp_before_exec) {
            struct sp_evt *e = &g_spring[g_spring_idx++ & 63];
            e->pc = exec_pc;
            e->op = prog_fetch(s, exec_pc);
            e->delta = (int16_t)(s->sp - sp_before_exec);
            e->sp = s->sp;
            g_sp_ledger.net_words += (int16_t)(sp_before_exec - s->sp);
            if ((int16_t)(s->sp - sp_before_exec) < 0) g_sp_ledger.sp_pushes++;
            else g_sp_ledger.sp_pops++;

            /* SP diving into the API RAM zone (0x0700-0x0a00) is corruption: a
             * CALLD push then clobbers d_fb_det / d_fb_mode (0x08f8, 0x08f9).
             * Logs the instruction that brings SP in (the transition from
             * outside the zone), the delta and the stack neighbourhood, which
             * names the offending SP setter. */
            {
                static uint32_t spdz_n = 0;
                int in_zone   = (s->sp >= 0x0700 && s->sp <= 0x0a00);
                int was_out   = (sp_before_exec < 0x0700 || sp_before_exec > 0x0a00);
                if (in_zone && was_out && spdz_n < 30) {
                    fprintf(stderr, "[c54x] SP-DANGER SP 0x%04x→0x%04x (delta=%+d) "
                            "PC=0x%04x op=0x%04x XPC=%u insn=%u\n",
                            sp_before_exec, s->sp,
                            (int)(int16_t)(s->sp - sp_before_exec),
                            exec_pc, prog_fetch(s, exec_pc), s->xpc, s->insn_count);
                    spdz_n++;
                }
            }

            /* Shadow stack: pairs pushes with pops (CALYPSO_ORPHAN). Names THE
             * orphan return (the over-pop), not the 15 victims at 0xc8be. */
            if (g_shadow_on < 0) {
                const char *eo = calypso_getenv("CALYPSO_ORPHAN");  /* its own env, outside CALYPSO_DEBUG */
                g_shadow_on = (eo && *eo) ? 1 : 0;
            }
            if (g_shadow_on) {
                uint16_t op = e->op;
                int16_t  d  = e->delta;
                int is_call = (op==0xF074||op==0xF274||op==0xF4E3||op==0xF4E7||op==0xF6E3);
                int is_ret  = (op==0xFC00||op==0xFE00||op==0xF4EB||op==0xF4E4||op==0xF6EB
                               ||(op&0xFF00)==0xFC00);   /* RET/RETD/RETE/FRET/RETED plus conditional RC */
                int is_pshm = ((op&0xFF00)==0x4A00||(op&0xFF00)==0x4B00);
                (void)is_call;
                if (d < 0) {                 /* PUSH: SP went down */
                    int words = -d, w;
                    char kind = is_pshm ? 'P' : 'C';   /* PSHM pushes data; anything else a return address */
                    for (w = 0; w < words; w++) {
                        if (g_shadow_depth >= 0 && g_shadow_depth < SHADOW_N) {
                            g_shadow[g_shadow_depth].pc   = exec_pc;
                            g_shadow[g_shadow_depth].op   = op;
                            g_shadow[g_shadow_depth].sp   = s->sp;
                            g_shadow[g_shadow_depth].kind = kind;
                        }
                        g_shadow_depth++;
                    }
                } else if (d > 0) {          /* POP: SP went up */
                    int words = d, w;
                    for (w = 0; w < words; w++) {
                        g_shadow_depth--;
                        if (is_ret) {
                            if (g_shadow_depth < 0) {
                                g_orphan_hits++;
                                if (g_orphan_hits <= 40) {
                                    /* Return target: RETD/RETED arm delayed_pc
                                     * (deferred commit); RET/FRET commit
                                     * immediately into s->pc. */
                                    uint16_t ret_tgt = (s->delay_slots ? s->delayed_pc : s->pc);
                                    /* Last real PUSH in g_spring (the missing
                                     * matching CALL): scan backwards for
                                     * delta < 0. */
                                    uint16_t lp_pc = 0, lp_op = 0; int lp_found = 0, scan;
                                    for (scan = 1; scan <= 64; scan++) {
                                        struct sp_evt *pe = &g_spring[(g_spring_idx - scan) & 63];
                                        if (pe->delta < 0) { lp_pc = pe->pc; lp_op = pe->op;
                                                             lp_found = 1; break; }
                                    }
                                    fprintf(stderr,
                                        "[c54x] ORPHAN-RETURN #%llu insn=%u pc=0x%04x op=0x%04x "
                                        "SP=0x%04x → ret_tgt=0x%04x  lastPUSH=%s(pc=0x%04x op=0x%04x) "
                                        "net_words=%lld — over-pop (pile vierge au-dessus de SP_base)\n",
                                        (unsigned long long)g_orphan_hits, s->insn_count,
                                        exec_pc, op, s->sp, ret_tgt,
                                        lp_found ? "" : "AUCUN", lp_pc, lp_op,
                                        (long long)g_sp_ledger.net_words);
                                    /* Slot this return reads: written (a
                                     * legitimate vector) or untouched (real
                                     * garbage)? */
                                    {
                                        uint16_t rs = (uint16_t)(s->sp - 1);
                                        if (rs >= STKSLOT_LO && rs <= STKSLOT_HI) {
                                            int si = rs - STKSLOT_LO;
                                            if (g_stkslot_written[si])
                                                fprintf(stderr, "[c54x]     slot 0x%04x ÉCRIT par "
                                                    "ST@pc=0x%04x op=0x%04x → VECTEUR LÉGIT (pas un bug)\n",
                                                    rs, g_stkslot_wpc[si], g_stkslot_wop[si]);
                                            else
                                                fprintf(stderr, "[c54x]     slot 0x%04x JAMAIS écrit "
                                                    "→ VIERGE = vrai over-pop garbage\n", rs);
                                        }
                                    }
                                    /* On the very first orphan, dump the whole
                                     * g_spring ring (reset to over-pop) so
                                     * pushes and pops can be counted directly:
                                     * structural imbalance versus a single
                                     * bug. */
                                    if (g_orphan_hits == 1) {
                                        int k;
                                        fprintf(stderr, "[c54x]   g_spring (anciens→récents, reset→#1):\n");
                                        for (k = 64; k >= 1; k--) {
                                            struct sp_evt *pe = &g_spring[(g_spring_idx - k) & 63];
                                            if (pe->pc == 0 && pe->op == 0 && pe->delta == 0) continue;
                                            fprintf(stderr, "[c54x]     pc=0x%04x op=0x%04x %s%d SP→0x%04x\n",
                                                    pe->pc, pe->op, pe->delta < 0 ? "PUSH" : "POP ",
                                                    pe->delta < 0 ? -pe->delta : pe->delta, pe->sp);
                                        }
                                    }
                                }
                            } else if (g_shadow[g_shadow_depth].kind != 'C') {
                                g_mismatch_hits++;
                                if (g_mismatch_hits <= 40)
                                    fprintf(stderr,
                                        "[c54x] MISMATCH-RETURN #%llu insn=%u pc=0x%04x op=0x%04x "
                                        "SP=0x%04x dépile kind='%c' poussé par pc=0x%04x op=0x%04x — "
                                        "return lit une valeur non-retour (PSHM)\n",
                                        (unsigned long long)g_mismatch_hits, s->insn_count,
                                        exec_pc, op, s->sp, g_shadow[g_shadow_depth].kind,
                                        g_shadow[g_shadow_depth].pc, g_shadow[g_shadow_depth].op);
                            }
                        }
                    }
                }
                if (g_shadow_depth < 0) g_shadow_depth = 0;  /* re-anchor after an orphan */
            }
        }

        /* B4B (CALYPSO_B4B): traces the flow AFTER the detector, from 0x9ac0
         * to 0xec07 (the frequency decision plus the write to 0x08f8) or into
         * a loop, plus a one-shot opcode dump from 0x9ac0 for disassembly. */
        {
            /* SCAN-08F8 (CALYPSO_SCAN_08F8): one-shot scan of the current bank
             * for instructions holding the word 0x08f8, the address of
             * d_fb_det. Says whether a writer of d_fb_det exists at all, and
             * at which PC. */
            static int _sc = -1; static int _scdone = 0;
            if (_sc < 0) _sc = calypso_gate("CALYPSO_SCAN_08F8", 0);
            if (_sc && !_scdone && exec_pc == 0x9ac0) {
                _scdone = 1;
                unsigned _hits = 0;
                for (uint32_t _p = 0x7000; _p <= 0xfffe; _p++) {
                    if (prog_fetch(s, (uint16_t)_p) == 0x08f8) {
                        fprintf(stderr, "[c54x] SCAN-08F8 word@0x%04x=0x08f8  prev=0x%04x prev2=0x%04x\n",
                                _p, prog_fetch(s, (uint16_t)(_p-1)), prog_fetch(s, (uint16_t)(_p-2)));
                        if (++_hits > 40) break;
                    }
                }
                fprintf(stderr, "[c54x] SCAN-08F8 total hits(bank%u)=%u\n", s->xpc, _hits);
            }
            static int _b4b = -1; static int _armed = 0; static unsigned _b4bn = 0; static int _opd = 0;
            if (_b4b < 0) _b4b = calypso_gate("CALYPSO_B4B", 0);
            if (_b4b && exec_pc == 0x9ac0) {
                _armed = 1;
                if (!_opd) { _opd = 1;
                    fprintf(stderr, "[c54x] B4B-OPDUMP 0x9ac0..0x9adf:");
                    for (int _k = 0; _k < 32; _k++) fprintf(stderr, " %04x", prog_fetch(s, (uint16_t)(0x9ac0 + _k)));
                    fprintf(stderr, "\n");
                }
            }
            if (_b4b && _armed && _b4bn < 600) {
                _b4bn++;
                fprintf(stderr, "[c54x] B4B-FLOW pc=0x%04x xpc=%u op=0x%04x A=%lld insn=%u\n",
                        s->pc, s->xpc, prog_fetch(s, s->pc), (long long)(s->a & 0xFFFFFFFFFFULL), s->insn_count);
                if ((s->pc == 0xec07) || (s->pc >= 0x8d00 && s->pc <= 0x8d10)) _armed = 0;
            }
        }
        if (exec_pc == 0xa076) {   /* MAC kernel: reads the operands (I/Q plus coefficients) */
            g_flow_armed = 1;   /* FLOWTRACE: arm the window around the detector */
            static int _b2k = -1; static unsigned _b2kn = 0;
            if (_b2k < 0) _b2k = calypso_gate("CALYPSO_B2AR", 0);
            /* The counter lives outside the gate: min/max must cover the
             * WHOLE run, not stop at the first iteration. */
            static uint16_t _ar5min = 0xffff, _ar5max = 0;
            static unsigned _ar5seen = 0, _ar5in = 0;
            if (s->ar[5] < _ar5min) _ar5min = s->ar[5];
            if (s->ar[5] > _ar5max) _ar5max = s->ar[5];
            _ar5seen++;
            if (s->ar[5] >= 0x2a00 && s->ar[5] < 0x2b28) _ar5in++;
            if (_b2k && (_ar5seen % 20000) == 0)
                fprintf(stderr, "[c54x] B2AR5-RANGE n=%u min=0x%04x max=0x%04x IN_BUF=%u (buf=0x2a00..0x2b27)\n",
                        _ar5seen, _ar5min, _ar5max, _ar5in);
            if (_b2k && _b2kn < 16) {
                _b2kn++;
                #define _INB(a) (((a) >= 0x2a00 && (a) < 0x2b28) ? "IN" : "oob")
                fprintf(stderr, "[c54x] B2KERN @0xa076 AR2=%04x[%d]%s AR3=%04x[%d]%s AR4=%04x[%d]%s AR5=%04x[%d]%s\n",
                        s->ar[⟨1⟩],(int)(int16_t)s->data[s->ar[⟨2⟩]],_INB(s->ar[⟨3⟩]),  ×3
    ⟨⟩ = (2,2,2) (3,3,3) (4,4,4)
                        s->ar[5],(int)(int16_t)s->data[s->ar[5]],_INB(s->ar[5]));
                #undef _INB
            }
        }
        {   /* ARWATCH (CALYPSO_ARWATCH): at five correlator PCs, shows which
             * AR point inside the burst buffer [0x2a00..0x2b28) - '*' in, '.'
             * out. */
            static int _aw = -1; static unsigned _awn = 0;
            if (_aw < 0) _aw = calypso_gate("CALYPSO_ARWATCH", 0);
            if (_aw && _awn < 60 &&
                (exec_pc == 0x8d00 || exec_pc == 0x8d1a || exec_pc == 0x8e5f ||
                 exec_pc == 0x8e8c || exec_pc == 0x8e97)) {
                _awn++;
                char _in[9]; int _k;
                for (_k = 0; _k < 8; _k++)
                    _in[_k] = (s->ar[_k] >= 0x2a00 && s->ar[_k] < 0x2b28) ? '*' : '.';
                _in[8] = 0;
                fprintf(stderr, "[c54x] ARWATCH pc=0x%04x AR0=%04x AR1=%04x AR2=%04x AR3=%04x "
                        "AR4=%04x AR5=%04x AR6=%04x AR7=%04x [%s] A=0x%06llx insn=%u\n",
                        exec_pc, s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                        s->ar[4], s->ar[5], s->ar[6], s->ar[7], _in,
                        (unsigned long long)(s->a & 0xFFFFFFULL), s->insn_count);
            }
        }
        {   /* DISPATCH-PROBE (CALYPSO_DISPATCH_PROBE=1, default 0, read-only).
             *
             * The ROM's RX arming routine, 0xa5cd, installs the trampoline
             * data[0x0158] and programs DMA2_AAD/ALGTH/CTRL(ENABLE=1). It
             * exists, it is complete, and it is NEVER executed. It is an entry
             * of the dispatch table, copied at init from PROGRAM memory into
             * DATA:
             *     0xb4b6 : program[0xaae7..0xab34] -> data[0x4387..0x43d4]
             * so program[0xab10] = 0xa5cd lands in data[0x43b0], index 41.
             *
             * The dispatcher repeats the same pattern in several places:
             *     sub #k1,A ; bc ...,AGT ; sub #k2,A ; add #0x4387,A
             *     stlm A,AR3 ; ld *AR3,A ; cala A
             * i.e. a range-based switch. The question is which value reaches A,
             * and static analysis cannot answer it. The switch sites are the
             * `add #0x4387`: A is read BEFORE the addition, giving the raw
             * index, and the handler it designates is resolved. */
            static int _dp = -1;
            if (_dp < 0) {
                _dp = calypso_gate("CALYPSO_DISPATCH_PROBE", 0);
                if (_dp)
                    fprintf(stderr, "[dispatch] sonde armee : 7 sites d'aiguillage "
                            "(add #0x4387), l'armement RX 0xa5cd, l'arm/desarm DMA "
                            "0xa620/0xa62e, le publieur d_fb_det 0x79e4, l'init de "
                            "table 0xb4b6, et les acces au handler courant "
                            "data[0x43d8] (0xb01d lit, 0xbb01 ecrit). "
                            "LECTURE SEULE.\n");
            }
            if (_dp) {
                /* --- the switch sites: A holds the index before the `add` --- */
                /* Five sites, not seven: at 0xb0f1 and 0xb0ff the ROM scan
                 * shows the operand 0x4387 preceded by opcode 0xf000
                 * (`ld #k16,A`), not 0xf200 (`add #k16,A`) as at the other
                 * five. Those two load the table base rather than switch, and
                 * A there already holds a resolved table address (0x43ac =
                 * base + 37), which is where the absurd index=17324 came
                 * from. */
                {   /* CHAIN-B05F: the missing link, traced step by step.
                     *
                     * Measured state: the ARM commands d_task_d = 0x0018 (24,
                     * ALLC); the DSP reads the cell and does see 0x0018 (35 of
                     * 41 samples); the table holds the right handler at index
                     * 41 (data[0x43b0] = 0xa5cd). Yet the five _sw[] switch
                     * sites below - the index resolution - are NEVER reached,
                     * so RX arming is never requested.
                     *
                     * In between, the block that reads d_task_d at 0xb05f runs
                     * a comparison chain (ROM dump):
                     *     0xb062: f130 7fff
                     *     0xb064: f210 000c   0xb066: f843 b077   ; tests 12
                     *     0xb068: f210 0022   0xb06a: f846 b077   ; tests 34
                     *     0xb06c: f210 001e   0xb06e: f842 b070   ; tests 30
                     *     0xb070: f200 4387                       ; table base
                     * The value read is 24 - neither 12, nor 34, nor 30.
                     *
                     * Decoding the branch conditions f843/f846/f842 by hand is
                     * exactly what produced three false leads out of three, so
                     * the path actually taken and the accumulator at each step
                     * are measured instead.
                     *
                     * The whole segment 0xb05f..0xb078 is traced, so a block
                     * that never executes leaves the probe silent for a
                     * different reason - and the periodic counter says so.
                     * 200 steps, plus one summary every 20 passes. */
                    if (exec_pc >= 0xb05f && exec_pc <= 0xb078) {
                        static unsigned long long _cn = 0, _pass = 0;
                        if (exec_pc == 0xb05f) _pass++;
                        if (_cn < 200) {
                            _cn++;
                            /* The ARs are printed because 0xb060 is
                             * `10e1 0000`, an INDIRECT LD: the block reads its
                             * task code THROUGH a pointer, and the measurement
                             * gives A = 0x5294, outside the range of the
                             * comparisons (12/30/34), hence the systematic
                             * bailout to 0xb077. The question is therefore what
                             * the register points at, not what the comparison
                             * yields. The cell pointed at by AR[ARP] is printed
                             * too. */
                            unsigned _arp = (s->st0 >> 13) & 7;
                            uint16_t _ea  = s->ar[_arp];
                            fprintf(stderr,
                                    "[dispatch] CHAIN-B05F pc=0x%04x op=0x%04x "
                                    "A=0x%06llx (bas=0x%04x) TC=%d ARP=%u "
                                    "AR[ARP]=0x%04x *AR[ARP]=0x%04x "
                                    "AR0=%04x AR1=%04x AR2=%04x AR3=%04x "
                                    /* data[0x7fff] tests one hypothesis. At
                                     * 0xb062, `f130 7fff` overwrites A with
                                     * 0x5294, a CONSTANT whatever the input
                                     * (checked before and after the api_ram
                                     * fix). A result independent of the operand
                                     * means the opcode is not computing, it is
                                     * READING. If data[0x7fff] holds 0x5294,
                                     * the long immediate is being treated as an
                                     * ADDRESS instead of a value - the same
                                     * class as the LDU *(0x0ffe) bug documented
                                     * at the head of the F1xx handler. If it
                                     * does not match, the hypothesis falls and
                                     * the handler itself must be
                                     * instrumented. */
                                    "data[0x7fff]=0x%04x "
                                    "passage=%llu insn=%u\n",
                                    exec_pc, prog_fetch(s, exec_pc),
                                    (unsigned long long)(s->a & 0xFFFFFFULL),
                                    (unsigned)(s->a & 0xFFFF),
                                    (s->st0 >> 12) & 1, _arp, _ea, s->data[_ea],
                                    s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                                    s->data[0x7fff],
                                    _pass, s->insn_count);
                            fflush(stderr);
                        } else if (exec_pc == 0xb05f && (_pass % 20) == 0) {
                            fprintf(stderr,
                                    "[dispatch] CHAIN-B05F resume : %llu passages en "
                                    "0xb05f, %llu pas traces (plafond)\n", _pass, _cn);
                            fflush(stderr);
                        }
                    }
                }
                static const uint16_t _sw[] = {0xb070,0xb08d,0xb0aa,0xb0be,0xb0d1};
                for (unsigned _i = 0; _i < sizeof(_sw)/sizeof(_sw[0]); _i++) {
                    if (exec_pc != _sw[_i]) continue;
                    uint16_t idx  = (uint16_t)(s->a & 0xFFFF);
                    uint16_t addr = (uint16_t)(0x4387 + idx);
                    uint16_t hnd  = s->data[addr];
                    /* Deduplicate by (site, index): a stable switch must not
                     * flood the log. */
                    static struct { uint16_t pc, idx; unsigned long long n; } _seen[64];
                    static int _n = 0;
                    int _k = -1;
                    for (int _j = 0; _j < _n; _j++)
                        if (_seen[_j].pc == exec_pc && _seen[_j].idx == idx) { _k = _j; break; }
                    if (_k >= 0) {
                        if (++_seen[_k].n % 20000 == 0)
                            fprintf(stderr, "[dispatch] site 0x%04x index=%u × %llu\n",
                                    exec_pc, idx, _seen[_k].n);
                    } else {
                        if (_n < 64) { _seen[_n].pc = exec_pc; _seen[_n].idx = idx;
                                       _seen[_n].n = 1; _n++; }
                        fprintf(stderr, "[dispatch] *** site 0x%04x  index=%u  "
                                "-> data[0x%04x] = 0x%04x%s  A=0x%06llx insn=%u\n",
                                exec_pc, idx, addr, hnd,
                                (hnd == 0xa5cd) ? "  <<< ARMEMENT RX !" :
                                (hnd == 0xab38) ? "  (slot vide, RET partage)" : "",
                                (unsigned long long)(s->a & 0xFFFFFFULL), s->insn_count);
                    }
                }
                /* --- every computed call or branch, with its target -------
                 * Measured: none of the five switch sites fires, and yet the
                 * table handler 0xa62e EXECUTES (A = 0xffa62e, the address
                 * itself, the signature of a cala). So another call path
                 * exists. 0xa62e is reachable neither by a direct CALL/B (no
                 * reference in the image) nor by fall-through (0xa62d is a
                 * ret), which leaves a computed branch.
                 * tic54x-opc.c gives four forms, mask 0xFEFF, bit 8 selecting
                 * the source accumulator:
                 *     bacc  0xF4E2   baccd 0xF6E2   cala 0xF4E3   calad 0xF6E3
                 * All four are covered, on A as well as B - the whole class,
                 * not just the expected case. Deduplicated by (calling PC,
                 * target). */
                {
                  uint16_t _op = prog_fetch(s, exec_pc);
                  uint16_t _base = (uint16_t)(_op & 0xFEFF);
                  const char *_kind = (_base==0xF4E2) ? "bacc"  :
                                      (_base==0xF6E2) ? "baccd" :
                                      (_base==0xF4E3) ? "cala"  :
                                      (_base==0xF6E3) ? "calad" : NULL;
                  if (_kind) {
                    int64_t _src = (_op & 0x0100) ? s->b : s->a;
                    uint16_t tgt = (uint16_t)(_src & 0xFFFF);
                    static struct { uint16_t pc, tgt; unsigned long long n; } _c[96];
                    static int _cn = 0;
                    int _k = -1;
                    for (int _j = 0; _j < _cn; _j++)
                        if (_c[_j].pc == exec_pc && _c[_j].tgt == tgt) { _k = _j; break; }
                    if (_k >= 0) {
                        if (++_c[_k].n % 20000 == 0)
                            fprintf(stderr, "[dispatch] %s 0x%04x -> 0x%04x × %llu\n",
                                    _kind, exec_pc, tgt, _c[_k].n);
                    } else {
                        if (_cn < 96) { _c[_cn].pc = exec_pc; _c[_cn].tgt = tgt;
                                        _c[_cn].n = 1; _cn++; }
                        const char *q = (tgt==0xa5cd) ? "  <<< ARMEMENT RX !"
                                      : (tgt==0xa62e) ? "  (desarmement DMA)"
                                      : (tgt==0xab38) ? "  (RET partage, slot vide)"
                                      : (tgt==0xb5a1) ? "  (greffe #2)"
                                      : (tgt==0xa4c7) ? "  (graine go-live)" : "";
                        fprintf(stderr, "[dispatch] *** %-5s depuis 0x%04x -> 0x%04x%s"
                                "  (%s)  data[0x43d8]=0x%04x insn=%u\n",
                                _kind, exec_pc, tgt, q,
                                (_op & 0x0100) ? "src=B" : "src=A",
                                s->data[0x43d8], s->insn_count);
                    }
                  }
                }

                /* --- the notable sites, simple pass counting --------------- */
                {
                    static const struct { uint16_t pc; const char *quoi; } _pt[] = {
                      {0xa5cd, "ARMEMENT RX (tremplin 0x0158 + DMA2_AAD)"},
                      {0xa5e8, "  ecriture DMA2_AAD"},
                      {0xa5f6, "  ecriture DMA2_CTRL (ENABLE=1)"},
                      {0xa620, "SPCR : RDMA_MASK=0 (mode DMA arme)"},
                      {0xa62e, "SPCR : RDMA_MASK=1 (mode DMA coupe)"},
                      {0xa601, "test data[0x435e] bit13 (verrou config DMA)"},
                      {0x79e4, "publication d_fb_det (data[0x08f8] |= 1)"},
                      {0x79e3, "  gate : data[@0x7e] doit valoir 4"},
                      {0xb4b6, "init : recopie de la table -> data[0x4387]"},
                      {0xb01d, "lecture du handler courant data[0x43d8]"},
                      {0xbb00, "ECRITURE du handler courant data[0x43d8]"},

                      /* The RX arming chain. Index 41
                       * (`ld #0x29,A ; call 0xa9ea`) leads to 0xa5cd. Four
                       * sites request it, none is reached. The sites
                       * themselves, the ENTRY of the block containing them and
                       * the LOADER that puts that entry into A upstream are all
                       * probed - otherwise it is impossible to say where the
                       * chain breaks. */
                      {0xb220, "site 1 : ld #0x29 -> ARMEMENT RX (AR3=0x00bf)"},
                      {0xb216, "  entree du bloc du site 1"},
                      {0xb2b4, "site 2 : ld #0x29 -> ARMEMENT RX (AR3=0x0097)"},
                      {0xb2aa, "  entree du bloc du site 2"},
                      {0xb330, "site 3 : ld #0x29 -> ARMEMENT RX (AR3=0x0030)"},
                      {0xb35d, "site 4 : ld #0x29 -> ARMEMENT RX (AR3=0x0040)"},
                      /* the `ld #k16,A` that load the entry of block 1, then 2 */
                      {0xaba9, "  chargeur -> 0xb216"},
                      {0xabc9, "  chargeur -> 0xb216"},
                      {0xabf⟨1⟩, "  chargeur -> 0xb2aa"},  ×3
    ⟨⟩ = (1) (5) (9)
                      {0xabfd, "  chargeur -> 0xb2aa"},
                      {0xb758, "  chargeur -> 0xb2aa"},
                      /* the dispatcher itself: how many times, with which index */
                      {0xa9ea, "helper index->handler (A = base+index apres le add)"},

                      /* The task queue. The main loop at 0xa4ca does
                       * `call 0xaad5` (dequeue) and, when A != 0, `calad A`.
                       * 0xaad5 dequeues from a ring:
                       *     AR1 = data[0x434f] (write)  AR0 = data[0x434e] (read)
                       *     empty -> A = 0 -> the loop spins doing nothing
                       * Measured: 31000 iterations with an empty queue. The
                       * question is therefore not why a given handler does not
                       * run, but WHO ENQUEUES. The enqueue is 0xaac3 (39
                       * callers), a ring of 14, the handler arrives in A, and
                       * overflow sets bit 5 of data[0x3f92]. */
                      {0xaac3, "EMPILEMENT d'une tache (A = handler empile)"},
                      {0xaaca, "  ecriture effective dans l'anneau"},
                      {0xaad1, "  ⚠ FILE PLEINE (data[0x3f92] bit5)"},
                      {0xaae3, "DEPILEMENT effectif (file non vide)"},
                      {0xaadd, "  file VIDE -> A=0, la boucle tourne a blanc"},
                      {0xa4cf, "boucle principale : calad du handler depile"},
                    };
                    for (unsigned _i = 0; _i < sizeof(_pt)/sizeof(_pt[0]); _i++) {
                        if (exec_pc != _pt[_i].pc) continue;
                        static unsigned long long _cnt[40];
                        unsigned long long c = ++_cnt[_i];
                        if (c == 1 || c % 20000 == 0 ||
                            exec_pc == 0xaac3 || exec_pc == 0xaad1)
                            fprintf(stderr, "[dispatch] PC=0x%04x ×%llu — %s "
                                    "(A=0x%06llx AR1=0x%04x AR2=0x%04x AR3=0x%04x "
                                    "d[0x3f92]=0x%04x file[r=0x%04x w=0x%04x]) insn=%u\n",
                                    exec_pc, c, _pt[_i].quoi,
                                    (unsigned long long)(s->a & 0xFFFFFFULL),
                                    s->ar[1], s->ar[2], s->ar[3],
                                    s->data[0x3f92], s->data[0x434e], s->data[0x434f],
                                    s->insn_count);
                    }
                }
            }
        }
        {   /* XPCWATCH (CALYPSO_XPCWATCH, default 0): does the DSP ever switch
             * program bank?
             *
             * The FB chain is traced end to end and stops in one place: the
             * task arms data[0x0158..0159] = "call 0x728a", that trampoline is
             * only reachable through interrupt slot 30 (data 0x00F8-0x00FB =
             * "fb 0x0158", copied from PDROM 0xe399 by the reada at 0xb4c9),
             * and that slot is never taken - IMR bit 14 (= vec 30 - 16, the
             * formula in calypso_dma.c:185) is permanently unmasked, yet the
             * IFR only ever takes 0x0020 and 0x0008. Both missing pieces live
             * in PROM1, loaded into PROGRAM at 0x18000, i.e. in a BANK: the
             * only reference in the whole silicon to 0xaae8 (base of the
             * handler table, table[5] = 0xab77) is at PROM1@0x1ab31, and a
             * fifth writer of 0x0158 at PROM1@0x19fe1. If XPC stays 0 for ever,
             * PROM1/2/3 are dead code here, which would explain the missing
             * handler installer, the publisher 0x79e4 never reached, and the
             * missing caller of slot 30 all at once.
             *
             * Measures (a) every XPC change with the PC and opcode that causes
             * it, 40 lines; (b) a periodic summary of the MASK of pages seen,
             * so that absence is a readable result rather than an inference:
             * "pages XPC vues = 0x00000001" means page 0 only, so no bank was
             * ever entered. Without that summary, zero lines from (a) would be
             * ambiguous between "no switching" and "probe not armed".
             * The far-branch family IS modelled (fb/fcall/fbacc) and the first
             * instruction of the run is an `fb` from the reset vector 0xff80,
             * so a motionless XPC is not a missing instruction. */
            static int _xw = -1; static uint16_t _xprev = 0xFFFF;
            static unsigned _xn = 0, _xbil = 0; static uint32_t _xvus = 0;
            if (_xw < 0) {
                _xw = calypso_gate("CALYPSO_XPCWATCH", 0);
                if (_xw)
                    fprintf(stderr, "[c54x] XPCWATCH arme (changements de banque "
                            "programme + bilan des pages vues)\n");
            }
            if (_xw) {
                uint16_t _x = (uint16_t)(s->xpc & 0xFF);
                if (_x < 32) _xvus |= (1u << _x);
                if (_x != _xprev) {
                    if (_xn++ < 40)
                        fprintf(stderr, "[c54x] XPCWATCH XPC 0x%02x -> 0x%02x PC=0x%04x "
                                "op=0x%04x insn=%u\n",
                                (unsigned)(_xprev == 0xFFFF ? 0u : _xprev), (unsigned)_x,
                                exec_pc, prog_fetch(s, exec_pc), s->insn_count);
                    _xprev = _x;
                }
                if ((s->insn_count % 5000000u) == 0 && _xbil < 12) {
                    _xbil++;
                    fprintf(stderr, "[c54x] XPCWATCH bilan insn=%u : pages XPC vues = "
                            "0x%08x (bit n = page n ; 0x00000001 = page 0 SEULE = aucune "
                            "banque jamais entree)\n",
                            s->insn_count, (unsigned)_xvus);
                }
            }
        }
        }   /* C54X_SONDES : SP event ring .. XPCWATCH */
        {   /* @BEQUILLE - FORCE_VEC  (CALYPSO_FORCE_VEC=<n>, VALUE, inert by default)
             *   masks   : the HARDWARE SOURCE of a DSP interrupt this model does not
             *             implement. Measured: the FB task arms
             *             data[0x0158..0159] = "call 0x728a" (13 times), that trampoline
             *             is only reachable through interrupt slot 30
             *             (data 0x00F8-0x00FB = "fb 0x0158", copied from PDROM 0xe399 by
             *             the reada at 0xb4c9), IMR bit 14 (= vec - 16) is permanently
             *             unmasked in every IMR measured, and the IFR NEVER takes
             *             anything but 0x0020/0x0008: nothing ever raises that bit. Four
             *             leads were eliminated by measurement (task/loop race, masked
             *             IMR, end-of-DMA interrupt, program bank never switched - XPC
             *             does see pages 0 AND 1).
             *   remove  : as soon as it is known WHICH hardware line carries this vector
             *             on the Calypso. This is a DOCUMENTATION gap (the DSP interrupt
             *             table is nowhere in the repository), not a bug to grep for.
             *   note    : this gate is not a fix, it is a decisive test - it says whether
             *             everything downstream of the vector is healthy. If it is, the
             *             problem reduces to identifying and wiring the source.
             *   WARNING : together with SEED5AC8 and DISPATCH_INSTALL that makes THREE
             *             simultaneous crutches: what is measured is no longer the native
             *             behaviour but what it would do with three holes plugged.
             *             Acceptable for a test, not as a configuration.
             *
             * Trigger: injection happens only while the FB handler is actually armed
             * (data[0x0159] == 0x728a), never in the window where the main loop has put
             * back its light handler 0x7242 - otherwise the test would measure the other
             * path. Cadence CALYPSO_FORCE_VEC_PERIOD insns (default 65536 = one per
             * frame), hard limit of 200 injections, 20 log lines. */
            static int _fv = -2, _fvbit = -1; static unsigned _fvn = 0, _fvper = 65536;
            static uint32_t _fvlast = 0;
            if (_fv == -2) {
                const char *e = calypso_getenv("CALYPSO_FORCE_VEC");
                _fv = (e && *e) ? (int)strtol(e, NULL, 0) : -1;
                const char *p = calypso_getenv("CALYPSO_FORCE_VEC_PERIOD");
                if (p && *p) _fvper = (unsigned)strtoul(p, NULL, 0);
                if (_fv >= 0) {
                    _fvbit = _fv - 16;
                    fprintf(stderr, "[c54x] FORCE-VEC arme : vecteur %d (IMR bit %d), "
                            "1 injection / %u insn quand data[0x0159]==0x728a, "
                            "plafond 200 (BEQUILLE, voir calypso_c54x.c)\n",
                            _fv, _fvbit, _fvper);
                }
            }
            if (_fv >= 16 && _fvbit >= 0 && _fvn < 200 &&
                (s->insn_count - _fvlast) >= _fvper &&
                s->data[0x0159] == 0x728a) {
                _fvlast = s->insn_count;
                if (++_fvn <= 20)
                    fprintf(stderr, "[c54x] FORCE-VEC #%u injection vec=%d bit=%d "
                            "IMR=0x%04x IFR=0x%04x INTM=%d PC=0x%04x insn=%u\n",
                            _fvn, _fv, _fvbit, s->imr, s->ifr,
                            !!(s->st1 & ST1_INTM), exec_pc, s->insn_count);
                c54x_interrupt_ex(s, _fv, _fvbit);
            }
        }
        /* Pure probes TRACEFROM .. TASKGO (SCANREF/SCANFB/SCAN43D8 switch
         * s->xpc for the duration of the scan and restore it). */
        if (C54X_SONDES) {
        {   /* TRACEFROM (CALYPSO_TRACEFROM=<pc>): dumps the opcodes at that PC,
             * then follows the control flow from it, printing each
             * discontinuity, until one of the landmarks 0xa076 (MAC kernel),
             * 0x79e4 (d_fb_det publisher) or 0x9ac0 (detector) is reached, or
             * 4000 instructions have passed. Three windows per run. */
            static int _tf = -1; static uint16_t _tfpc = 0; static int _tfd = 0;
            static int _tfn2 = 24;   /* CALYPSO_TRACEFROM_N: dump length */
            static int _tfarm = 0; static unsigned _tfn = 0, _tfr = 0; static uint16_t _tfp = 0;
            if (_tf < 0) { const char *e = calypso_getenv("CALYPSO_TRACEFROM");
                _tf = (e && *e) ? 1 : 0;
                if (_tf) _tfpc = (uint16_t)strtol(e, NULL, 0);
                const char *n = calypso_getenv("CALYPSO_TRACEFROM_N");
                if (n && *n) _tfn2 = atoi(n); }
            if (_tf) {
                if (exec_pc == _tfpc) {
                    if (!_tfd) { _tfd = 1;
                        fprintf(stderr, "[c54x] TRACEFROM-OPDUMP 0x%04x..+23:", _tfpc);
                        for (int _k = 0; _k < _tfn2; _k++) {
                            if ((_k % 8) == 0) fprintf(stderr, "\n  0x%04x:", _tfpc + _k);
                            fprintf(stderr, " %04x", prog_fetch(s, (uint16_t)(_tfpc + _k)));
                        }
                        fprintf(stderr, "\n"); }
                    if (_tfr < 3) { _tfr++; _tfarm = 1; _tfn = 0;
                        fprintf(stderr, "[c54x] TRACEFROM === entree 0x%04x #%u (task_md=%u) ===\n",
                                _tfpc, _tfr, (unsigned)(s->data[0x0804] ? s->data[0x0804] : s->data[0x0818])); }
                }
                if (_tfarm) {
                    if (++_tfn > 4000) { _tfarm = 0;
                        fprintf(stderr, "[c54x] TRACEFROM fin (4000 insn)\n"); }
                    else {
                        int _d = (int)s->pc - (int)_tfp;
                        if ((_d > 3 || _d < 0) && _tfn < 3000)
                            fprintf(stderr, "[c54x] TRACEFROM 0x%04x -> 0x%04x op=0x%04x A=0x%06llx\n",
                                    _tfp, s->pc, prog_fetch(s, s->pc),
                                    (unsigned long long)(s->a & 0xFFFFFFULL));
                        if (s->pc == 0xa076 || s->pc == 0x79e4 || s->pc == 0x9ac0) {
                            fprintf(stderr, "[c54x] TRACEFROM *** ATTEINT 0x%04x %s ***\n", s->pc,
                                    s->pc == 0xa076 ? "(kernel MAC)" :
                                    s->pc == 0x79e4 ? "(publisher d_fb_det)" : "(detecteur)");
                            _tfarm = 0; }
                    }
                }
                _tfp = s->pc;
            }
        }
        {   /* CORROUT (CALYPSO_CORROUT): on leaving the MAC kernel
             * (0xa070-0xa0a0), dumps the accumulators, T, the pointer ARs and
             * the workspace at 0x2c00 - what the correlator produced. */
            static int _co = -1; static int _in_k = 0; static unsigned _con = 0;
            if (_co < 0) _co = calypso_gate("CALYPSO_CORROUT", 0);
            if (_co) {
                if (exec_pc >= 0xa070 && exec_pc <= 0xa0a0) { _in_k = 1; }
                else if (_in_k) {   /* just LEFT the MAC kernel */
                    _in_k = 0;
                    if (_con < 40) {
                        _con++;
                        fprintf(stderr, "[c54x] CORROUT sortie noyau -> PC=0x%04x "
                                "A=0x%010llx B=0x%010llx T=%04x "
                                "AR3=%04x AR4=%04x AR5=%04x AR6=%04x insn=%u\n",
                                exec_pc,
                                (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                                (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                                s->t, s->ar[3], s->ar[4], s->ar[5], s->ar[6],
                                s->insn_count);
                        fprintf(stderr, "[c54x] CORROUT   wz[2c00..0f]=");
                        for (int _k = 0; _k < 16; _k++)
                            fprintf(stderr, " %04x", s->data[0x2c00 + _k]);
                        fprintf(stderr, "\n");
                    }
                }
            }
        }
        {   /* VECTAB (CALYPSO_VECTAB): one-shot dump of the interrupt vector
             * table 0x0080..0x00FF, classifying each slot and flagging those
             * whose target falls in the FB routine range. */
            static int _vt = -1; static int _vtdone = 0; static int _vtseen = 0;
            if (_vt < 0) _vt = calypso_gate("CALYPSO_VECTAB", 0);
            if (_vt && !_vtdone && exec_pc == 0xb01c && ++_vtseen >= 2) {
                _vtdone = 1;
                fprintf(stderr, "[c54x] VECTAB table de vecteurs 0x0080..0x00FF "
                        "(f880=B long, f4eb=RETE stub, f495=NOP)\n");
                for (int _v = 0; _v < 32; _v++) {
                    uint16_t _a = (uint16_t)(0x0080 + _v * 4);
                    uint16_t _w0 = s->data[_a], _w1 = s->data[_a + 1];
                    const char *_kind = (_w0 == 0xf4eb) ? "RETE (STUB)" :
                                        ((_w0 & 0xFF80) == 0xF880) ? "B long" :
                                        (_w0 == 0xf495) ? "NOP" : "?";
                    int _fb = ((_w0 & 0xFF80) == 0xF880) &&
                              (_w1 >= 0x7600 && _w1 <= 0x7a00);
                    fprintf(stderr, "[c54x] VECTAB   vec%-2d @0x%04x  w0=0x%04x w1=0x%04x  %-12s"
                            "%s%s\n", _v, _a, _w0, _w1, _kind,
                            _v == 19 ? "  [FRAME]" : _v == 21 ? "  [BRINT0]" : "",
                            _fb ? "   <<<< CIBLE DANS LA ZONE FB" : "");
                }
                fprintf(stderr, "[c54x] VECTAB fin ; code juste AVANT 0x76f8 "
                        "(0x76e8..0x76f7):");
                for (int _k = 0; _k < 16; _k++)
                    fprintf(stderr, " %04x", prog_fetch(s, (uint16_t)(0x76e8 + _k)));
                fprintf(stderr, "\n");
            }
        }
        {   /* SCANDATA (CALYPSO_SCANDATA, with _LO/_HI): one-shot scan of
             * data[] for cells pointing into the FB routine range. A total of 0
             * means the FB routine is unreachable by computed dispatch in this
             * image. */
            static int _sd2 = -1; static int _sddone = 0; static unsigned _sdhit = 0;
            static uint16_t _lo = 0x76f8, _hi = 0x79f0;
            if (_sd2 < 0) {
                _sd2 = calypso_gate("CALYPSO_SCANDATA", 0);
                const char *a = calypso_getenv("CALYPSO_SCANDATA_LO");
                const char *b = calypso_getenv("CALYPSO_SCANDATA_HI");
                if (a && *a) _lo = (uint16_t)strtol(a, NULL, 0);
                if (b && *b) _hi = (uint16_t)strtol(b, NULL, 0);
            }
            if (_sd2 && !_sddone && exec_pc == 0xb01c) {
                static int _seen = 0;
                if (++_seen >= 2) {   /* after the table init */
                    _sddone = 1;
                    fprintf(stderr, "[c54x] SCANDATA cellules data[] pointant dans "
                            "[0x%04x..0x%04x] (routine FB) :\n", _lo, _hi);
                    for (uint32_t _a = 0; _a < C54X_DATA_SIZE; _a++) {
                        uint16_t _v = s->data[_a];
                        if (_v < _lo || _v > _hi) continue;
                        fprintf(stderr, "[c54x] SCANDATA   data[0x%04x] = 0x%04x%s\n",
                                (unsigned)_a, _v,
                                (_a >= 0x4380 && _a <= 0x43ff) ? "   <<<< DANS LA TABLE DE DISPATCH" :
                                (_a >= 0x0800 && _a <  0x2800) ? "   (API RAM)" : "");
                        if (++_sdhit > 60) break;
                    }
                    fprintf(stderr, "[c54x] SCANDATA total=%u  (0 = routine FB inatteignable "
                            "par dispatch calcule dans cette image)\n", _sdhit);
                }
            }
        }
        {   /* DISPWATCH (CALYPSO_DISPWATCH): at the dispatch sites, shows A,
             * the three candidate table slots and the current d_task_md, with
             * FB tasks (5, 6, 8, 9) given their own, larger budget. */
            static int _dw = -1; static unsigned _dwn = 0, _dwfb = 0;
            if (_dw < 0) _dw = calypso_gate("CALYPSO_DISPWATCH", 0);
            if (_dw && (exec_pc == 0xb40f || exec_pc == 0xb01c || exec_pc == 0xb01e ||
                        exec_pc == 0xb0f0 || exec_pc == 0xb0f6)) {
                unsigned _md = s->data[0x0804] ? s->data[0x0804] : s->data[0x0818];
                int _isfb = (_md == 5 || _md == 6 || _md == 8 || _md == 9);
                int _emit = 0;
                if (_isfb) { if (_dwfb < 60) { _dwfb++; _emit = 1; } }
                else       { if (_dwn  < 20) { _dwn++;  _emit = 1; } }
                if (_emit) {
                    const char *_site = (exec_pc == 0xb40f) ? "BACC-terminal" :
                                        (exec_pc == 0xb01c) ? "LD-slot" :
                                        (exec_pc == 0xb01e) ? "CALA" :
                                        (exec_pc == 0xb0f0) ? "idx-calc" : "idx-LD";
                    fprintf(stderr, "[c54x] DISPWATCH %s pc=0x%04x A=0x%06llx "
                            "slot43c0=0x%04x slot4387=0x%04x slot43d8=0x%04x "
                            "task_md=%u%s insn=%u\n", _site, exec_pc,
                            (unsigned long long)(s->a & 0xFFFFFFULL),
                            s->data[0x43c0], s->data[0x4387], s->data[0x43d8],
                            _md, _isfb ? "  <<<< TACHE FB" : "", s->insn_count);
                }
            }
        }
        {   /* SCANREF (CALYPSO_SCANREF=<target>): one-shot scan of all four
             * program banks for words equal to the target, printing the two
             * preceding words so the referencing instruction can be
             * identified. */
            static int _sr = -1; static uint16_t _srt = 0; static int _srd = 0;
            if (_sr < 0) { const char *e = calypso_getenv("CALYPSO_SCANREF");
                _sr = (e && *e) ? 1 : 0;
                if (_sr) _srt = (uint16_t)strtol(e, NULL, 0); }
            if (_sr && !_srd && exec_pc == 0xb01c) {
                _srd = 1;
                uint16_t _sx = s->xpc; unsigned _tot = 0;
                fprintf(stderr, "[c54x] SCANREF cible=0x%04x (f074=CALL f272=RPTBD f820/f880=B "
                        "76f8=ST#imm 10f8=LD)\n", _srt);
                for (unsigned _bk = 0; _bk < 4; _bk++) {
                    s->xpc = (uint16_t)_bk; unsigned _h = 0;
                    for (uint32_t _p = 0x7000; _p <= 0xfffe; _p++) {
                        if (prog_fetch(s, (uint16_t)_p) != _srt) continue;
                        if (_p >= (uint32_t)_srt - 2 && _p <= (uint32_t)_srt + 2) continue;
                        fprintf(stderr, "[c54x] SCANREF bank%u @0x%04x prev2=0x%04x prev1=0x%04x\n",
                                _bk, _p, prog_fetch(s, (uint16_t)(_p-2)), prog_fetch(s, (uint16_t)(_p-1)));
                        _tot++;
                        if (++_h > 15) break;
                    }
                }
                s->xpc = _sx;
                fprintf(stderr, "[c54x] SCANREF total=%u sur 4 banks ; code @0x%04x..+15:", _tot, _srt);
                for (int _k = 0; _k < 16; _k++)
                    fprintf(stderr, " %04x", prog_fetch(s, (uint16_t)(_srt + _k)));
                fprintf(stderr, "\n");
            }
        }
        {   /* SCANFB (CALYPSO_SCANFB): same scan as SCANREF, over the four
             * fixed FB addresses (body, entry stub, subroutine, correlation). */
            static int _sf = -1; static int _sfd = 0;
            if (_sf < 0) _sf = calypso_gate("CALYPSO_SCANFB", 0);
            if (_sf && !_sfd && exec_pc == 0xb01c) {
                _sfd = 1;
                const uint16_t _tg[4] = { 0x7708, 0x76fb, 0x770d, 0x795f };
                const char *_nm[4] = { "corps FB", "stub entree", "sous-prog", "correlation" };
                uint16_t _sx = s->xpc;
                for (unsigned _bk = 0; _bk < 4; _bk++) {
                    s->xpc = (uint16_t)_bk;
                    for (int _t = 0; _t < 4; _t++) {
                        unsigned _h = 0;
                        for (uint32_t _p = 0x7000; _p <= 0xfffe; _p++) {
                            if (prog_fetch(s, (uint16_t)_p) == _tg[_t]) {
                                if (_p >= _tg[_t] - 2 && _p <= _tg[_t] + 2) continue;
                                fprintf(stderr, "[c54x] SCANFB bank%u ref 0x%04x (%s) @0x%04x "
                                        "prev2=0x%04x prev1=0x%04x\n", _bk, _tg[_t], _nm[_t], _p,
                                        prog_fetch(s, (uint16_t)(_p-2)), prog_fetch(s, (uint16_t)(_p-1)));
                                if (++_h > 12) break;
                            }
                        }
                    }
                }
                s->xpc = _sx;
                fprintf(stderr, "[c54x] SCANFB fin (f074=CALL, f272=BD, fc00=RET, 76f8=ST #imm)\n");
            }
        }
        {   /* FBENTRY (CALYPSO_FBENTRY): traces the FB routine range
             * 0x75e0-0x79f0 instruction by instruction, then the exit (with the
             * stack top, flagged when it lands in the vector page) and the ten
             * instructions after it. */
            static int _fe = -1; static int _dmp = 0; static int _in = 0; static int _after = 0;
            static unsigned _n = 0;
            if (_fe < 0) _fe = calypso_gate("CALYPSO_FBENTRY", 0);
            if (_fe) {
                if (exec_pc >= 0x75e0 && exec_pc <= 0x79f0) {   /* includes the subroutine at 0x75e8 */
                    if (!_dmp) { _dmp = 1;
                        fprintf(stderr, "[c54x] FBENTRY-OPDUMP 0x76f8..0x7730:");
                        for (int _k = 0; _k < 57; _k++)
                            fprintf(stderr, " %04x", prog_fetch(s, (uint16_t)(0x76f8 + _k)));
                        fprintf(stderr, "\n"); }
                    _in = 1;
                    if (_n < 250) { _n++;
                        fprintf(stderr, "[c54x] FBENTRY pc=0x%04x op=0x%04x op2=0x%04x A=0x%06llx "
                                "TC=%u AR1=%04x AR2=%04x ST0=0x%04x ST1=0x%04x\n",
                                exec_pc, prog_fetch(s, exec_pc), prog_fetch(s, (uint16_t)(exec_pc+1)),
                                (unsigned long long)(s->a & 0xFFFFFFULL), (unsigned)((s->st0 >> 12) & 1),
                                s->ar[1], s->ar[2], s->st0, s->st1); }
                } else if (_in) { _in = 0; _after = 10;
                    if (_n < 250) { _n++;
                        fprintf(stderr, "[c54x] FBENTRY *** SORTIE vers 0x%04x *** (op=0x%04x) "
                                "SP=0x%04x pile[SP]=0x%04x pile[SP+1]=0x%04x%s\n",
                                exec_pc, prog_fetch(s, exec_pc), s->sp,
                                s->data[s->sp], s->data[(uint16_t)(s->sp + 1)],
                                (exec_pc >= 0x0080 && exec_pc <= 0x00ff)
                                    ? "  [VECTEUR IT]" : ""); }
                } else if (_after > 0) { _after--;
                    if (_n < 250) { _n++;
                        fprintf(stderr, "[c54x] FBENTRY   apres-sortie pc=0x%04x op=0x%04x SP=0x%04x%s\n",
                                exec_pc, prog_fetch(s, exec_pc), s->sp,
                                (exec_pc == 0x7707 || exec_pc == 0x7708)
                                    ? "  <<<< RETOUR DANS LA ROUTINE FB" : ""); }
                }
            }
        }
        {   /* SCAN43D8 (CALYPSO_SCAN43D8): one-shot scan of the four program
             * banks for the word 0x43d8, classifying each reference as an
             * install (ST #imm) or a read (LD). */
            static int _s4 = -1; static int _s4done = 0;
            if (_s4 < 0) _s4 = calypso_gate("CALYPSO_SCAN43D8", 0);
            if (_s4 && !_s4done && exec_pc == 0xb01c) {
                _s4done = 1;
                unsigned _h = 0;
                uint16_t _savedxpc = s->xpc;
                for (unsigned _bk = 0; _bk < 4; _bk++) {
                s->xpc = (uint16_t)_bk;
                fprintf(stderr, "[c54x] SCAN43D8 --- bank %u ---\n", _bk);
                for (uint32_t _p = 0x7000; _p <= 0xfffe; _p++) {
                    if (prog_fetch(s, (uint16_t)_p) == 0x43d8) {
                        uint16_t _o2 = prog_fetch(s, (uint16_t)(_p-2));
                        uint16_t _o1 = prog_fetch(s, (uint16_t)(_p-1));
                        const char *_k = (_o2 == 0x76f8) ? "ST #imm -> INSTALLE"
                                       : (_o2 == 0x10f8) ? "LD -> LIT"
                                       : (_o1 == 0x76f8) ? "ST #imm (dec) -> INSTALLE" : "?";
                        fprintf(stderr, "[c54x] SCAN43D8 @0x%04x prev2=0x%04x prev1=0x%04x  %s\n",
                                _p, _o2, _o1, _k);
                        if (++_h > 60) break;
                    }
                }
                }
                s->xpc = _savedxpc;
                fprintf(stderr, "[c54x] SCAN43D8 total=%u sur 4 banks\n", _h);
                fprintf(stderr, "[c54x] SCAN43D8 code @0xbaf8..0xbb10:");
                for (int _k2 = 0; _k2 < 25; _k2++)
                    fprintf(stderr, " %04x", prog_fetch(s, (uint16_t)(0xbaf8 + _k2)));
                fprintf(stderr, "\n");
            }
        }
        {   /* SLOTSRC (CALYPSO_SLOTSRC): instruction trace of 0xaff0-0xb01d,
             * the code that computes the dispatch slot. */
            static int _ss = -1; static unsigned _ssn = 0;
            if (_ss < 0) _ss = calypso_gate("CALYPSO_SLOTSRC", 0);
            if (_ss && _ssn < 120 && exec_pc >= 0xaff0 && exec_pc <= 0xb01d) {
                _ssn++;
                fprintf(stderr, "[c54x] SLOTSRC pc=0x%04x op=0x%04x op2=0x%04x "
                        "A=0x%06llx AR1=%04x AR2=%04x AR3=%04x ST0=0x%04x insn=%u\n",
                        exec_pc, prog_fetch(s, exec_pc), prog_fetch(s, (uint16_t)(exec_pc+1)),
                        (unsigned long long)(s->a & 0xFFFFFFULL),
                        s->ar[1], s->ar[2], s->ar[3], (unsigned)s->st0, s->insn_count);
            }
        }
        {   /* FBCALL (CALYPSO_FBCALL): on each rising edge of d_task_md to 5
             * (FB), follows the control flow for up to 20000 instructions and
             * reports whether it reaches the FB routine. Three rounds. */
            static int _fc = -1;
            if (_fc < 0) _fc = calypso_gate("CALYPSO_FBCALL", 0);
            if (_fc) {
                static uint16_t _pmd = 0xffff, _ppc = 0; static int _arm = 0;
                static unsigned _steps = 0, _logged = 0, _rounds = 0;
                uint16_t _md = s->data[0x0804] ? s->data[0x0804] : s->data[0x0818];
                if (_md != _pmd) {
                    if (_md == 5 && _rounds < 3) {
                        _rounds++; _arm = 1; _steps = 0;
                        fprintf(stderr, "[c54x] FBCALL === tache FB #%u (d_task_md=5) ===\n", _rounds);
                    }
                    _pmd = _md;
                }
                if (_arm) {
                    if (++_steps > 20000) { _arm = 0;
                        fprintf(stderr, "[c54x] FBCALL fin (20000 insn) sans 0x7700\n"); }
                    else {
                        int _d = (int)s->pc - (int)_ppc;
                        if ((_d > 3 || _d < 0) && _logged < 300) {
                            _logged++;
                            fprintf(stderr, "[c54x] FBCALL 0x%04x -> 0x%04x op=0x%04x A=0x%06llx\n",
                                    _ppc, s->pc, prog_fetch(s, s->pc),
                                    (unsigned long long)(s->a & 0xFFFFFFULL));
                        }
                        if (s->pc >= 0x76f0 && s->pc <= 0x79f0) {
                            fprintf(stderr, "[c54x] FBCALL *** ROUTINE FB ATTEINTE 0x%04x ***\n", s->pc);
                            _arm = 0; }
                    }
                }
                _ppc = s->pc;
            }
        }
        {   /* DISPIDX (CALYPSO_DISPIDX): captures the dispatch index computed
             * at 0xb0f0 and, at 0xb0f6, resolves the table slot it designates,
             * flagging the FB task. */
            static int _di = -1; static unsigned _din = 0; static unsigned _idx = 0;
            if (_di < 0) _di = calypso_gate("CALYPSO_DISPIDX", 0);
            if (_di) {
                if (exec_pc == 0xb0f0) _idx = (unsigned)(s->a & 0xFFFF);
                if (exec_pc == 0xb0f6 && _din < 80) {
                    _din++;
                    unsigned _slotaddr = 0x4387 + _idx;
                    unsigned _slot = (_slotaddr < 0x10000) ? s->data[_slotaddr] : 0;
                    unsigned _md = s->data[0x0804] ? s->data[0x0804] : s->data[0x0818];
                    fprintf(stderr, "[c54x] DISPIDX idx=%u slot=data[0x%04x]=0x%04x "
                            "d_task_md=%u%s insn=%u\n", _idx, _slotaddr, _slot, _md,
                            (_md == 5) ? "  <<<< TACHE FB" : "", s->insn_count);
                }
            }
        }
        {   /* DISPTAB (CALYPSO_DISPTAB): dumps the dispatch table
             * data[0x4380..0x43cf] at the dispatcher; 0xab38 is the default
             * handler, so the FB slot is whatever differs. */
            static int _dd2 = -1; static unsigned _ddn2 = 0;
            if (_dd2 < 0) _dd2 = calypso_gate("CALYPSO_DISPTAB", 0);
            if (_dd2 && exec_pc == 0xb0f1 && _ddn2 < 4) {
                _ddn2++;
                fprintf(stderr, "[c54x] DISPTAB-DUMP #%u data[0x4380..0x43cf] :", _ddn2);
                for (int _k = 0; _k < 80; _k++) {
                    if ((_k % 16) == 0) fprintf(stderr, "\n  0x%04x:", 0x4380 + _k);
                    fprintf(stderr, " %04x", s->data[0x4380 + _k]);
                }
                fprintf(stderr, "\n  (0xab38 = handler par defaut ; on cherche le slot FB)\n");
            }
        }
        {   /* TASKGO (CALYPSO_TASKGO): on each rising edge of d_task_md to 5
             * (FB), traces the next 250 instructions and reports whether the FB
             * routine is reached, plus the highest PC seen. */
            static int _tg = -1;
            if (_tg < 0) _tg = calypso_gate("CALYPSO_TASKGO", 0);
            if (_tg) {
                static uint16_t _prev = 0xffff; static int _armed = 0;
                static unsigned _n = 0, _rounds = 0, _hi = 0;
                uint16_t _md = s->data[0x0804] ? s->data[0x0804] : s->data[0x0818];
                if (_md != _prev) {
                    if (_md == 5 && _rounds < 4) {
                        _rounds++; _armed = 1; _n = 0; _hi = 0;
                        fprintf(stderr, "[c54x] TASKGO front d_task_md -> 5 (FB) "
                                "md0804=%u md0818=%u PC=0x%04x insn=%u\n",
                                (unsigned)s->data[0x0804], (unsigned)s->data[0x0818],
                                s->pc, s->insn_count);
                    }
                    _prev = _md;
                }
                if (_armed && _n < 250) { _n++;
                    if (s->pc > _hi) _hi = s->pc;
                    fprintf(stderr, "[c54x] TASKGO-FLOW pc=0x%04x xpc=%u op=0x%04x A=0x%06llx\n",
                            s->pc, s->xpc, prog_fetch(s, s->pc),
                            (unsigned long long)(s->a & 0xFFFFFFULL));
                    if (s->pc >= 0x7700 && s->pc <= 0x79f0) {
                        fprintf(stderr, "[c54x] TASKGO *** ATTEINT LA ROUTINE FB 0x%04x ***\n", s->pc);
                        _armed = 0; }
                } else if (_armed) { _armed = 0;
                    fprintf(stderr, "[c54x] TASKGO fin (250 pas) sans 0x7700 ; PC max vu=0x%04x\n", _hi); }
            }
        }
        }   /* C54X_SONDES : TRACEFROM .. TASKGO */
        {   /* @BEQUILLE - DISPATCH_INSTALL  (CALYPSO_DISPATCH_INSTALL=0xNNNN,
             *              VALUE, unset by default = inert)
             *   masks   : the routine that ought to install a task handler in
             *             data[0x43d8]. Measured: that cell only ever receives the
             *             0xab38 plug (a RET), written once by 0xbb00, and nothing
             *             writes it again - confirmed statically (2 references across
             *             the 5 ROMs) and at runtime. 0xb01c re-reads it on EVERY
             *             frame and 0xb01e calls it, so the DSP acknowledges each task
             *             and does nothing.
             *   remove  : as soon as it is known WHICH routine should populate
             *             0x43d8. This gate is not a fix, it is a decisive test: it
             *             says whether the rest of the chain lights up once the
             *             handler is right. If it does, the question reduces to the
             *             installer.
             *
             * Measured candidates: 0xab77 = table[5], which reads d_task_md
             * (*AR1(0x0004)) and tests its bit 15 - the most plausible for the FB
             * task; 0xb284 = fcall 0x770a, the entry of the publisher's bank, never
             * reached otherwise.
             *
             * Only the plug is overwritten: if a real handler is ever installed, this
             * gate will not mask it, it will fall silent. */
            static int _di2 = -2, _ditask = -1; static unsigned _din2 = 0;
            if (_di2 == -2) {
                const char *e = calypso_getenv("CALYPSO_DISPATCH_INSTALL");
                _di2 = (e && *e) ? (int)strtoul(e, NULL, 0) : -1;
                const char *t = calypso_getenv("CALYPSO_DISPATCH_INSTALL_TASK");
                _ditask = (t && *t) ? atoi(t) : -1;   /* -1 = any task */
                if (_di2 >= 0)
                    fprintf(stderr, "[c54x] DISPATCH-INSTALL arme : data[0x43d8] "
                            "<- 0x%04x pour d_task_md=%s (BEQUILLE, voir "
                            "calypso_c54x.c)\n", (unsigned)_di2,
                            _ditask < 0 ? "toute tache non nulle" : "la tache demandee");
            }
            /* @BEQUILLE - FORCE_TASK  (CALYPSO_FORCE_TASK=<n>, default OFF)
             *   masks   : the L1's inability to SUSTAIN its CCCH command. It orders a
             *             task to the DSP in the ARM's place. Measured: task 24 is
             *             indeed commanded (7 to 34 times depending on the run) and
             *             burst reports are accepted since the burst-id fix, but the
             *             mobile does not camp, so it reselects about every 10 s, and
             *             each reselection does L1-RESET: d_dsp_page = 0 (17 times in
             *             a 65 s run), which wipes the engagement. The DSP never gets
             *             a long window.
             *             What it does NOT fabricate is the result: it sets the
             *             COMMAND, not a_cd, so WATCH-ACD remains an honest judge - if
             *             the DSP writes a_cd under this crutch, the DSP wrote it.
             *   remove  : when the mobile camps and commands the CCCH by itself.
             *
             * It does not fight the ARM: it only fills passes where both write pages
             * are empty (no command in flight), so a real ARM command always wins.
             * 20 log lines.
             */
            if (exec_pc == 0xb01c) {
                static int _ft = -2; static unsigned _ftn = 0;
                if (_ft == -2) {
                    const char *e = calypso_getenv("CALYPSO_FORCE_TASK");
                    _ft = (e && *e) ? (int)strtol(e, NULL, 0) : -1;
                    if (_ft >= 0)
                        fprintf(stderr, "[c54x] FORCE-TASK arme : d_task_md <- %d "
                                "quand l'ARM ne commande rien (BEQUILLE — la commande "
                                "est fabriquee, le RESULTAT ne l'est pas)\n", _ft);
                }
                /* A reception command is never a single word. osmocom-bb
                 * calypso/dsp.c:479 has three fields:
                 *     void dsp_load_rx_task(task, burst_id, tsc) {
                 *         db_w->d_task_d      = task;          // word 0
                 *         db_w->d_burst_d     = burst_id;      // word 1
                 *         db_w->d_ctrl_system |= tsc & 0x7;    // word 16
                 *     }
                 * d_task_md (word 4) is only used by FB/SB (prim_fbsb.c:279,379),
                 * where it comes with an NDB parameter (d_fb_mode).
                 *
                 * Layout (T_DB_MCU_TO_DSP, include/calypso/dsp_api.h:82):
                 *     word 0 d_task_d | 1 d_burst_d | 4 d_task_md | 16 d_ctrl_system
                 * Bases: page 0 = 0x0800, page 1 = 0x0814, hence
                 *     d_task_d      0x0800 / 0x0814
                 *     d_burst_d     0x0801 / 0x0815
                 *     d_ctrl_system 0x0810 / 0x0824
                 *
                 * The PAGE is the one the ARM designates: bit 0 of
                 * d_dsp_page = B_GSM_TASK | w_page (dsp.c:471).
                 *
                 * burst_id CYCLES 0,1,2,3: the firmware calls RX NB four times and
                 * only collects the data on the fourth answer (wiki
                 * HardwareCalypsoDSP). TSC: CALYPSO_FORCE_TASK_TSC, default 7 (the
                 * BCC measured on this network).
                 */
                if (_ft >= 0 && s->data[0x0800] == 0 && s->data[0x0814] == 0 &&
                    s->data[0x0804] == 0 && s->data[0x0818] == 0) {
                    static unsigned _fbid = 0;
                    static int _ftsc = -1;
                    if (_ftsc < 0) {
                        const char *t = calypso_getenv("CALYPSO_FORCE_TASK_TSC");
                        _ftsc = (t && *t) ? (int)strtol(t, NULL, 0) : 7;
                    }
                    /* Keep our OWN page counter: d_dsp_page is reset to 0 by
                     * every L1-RESET (30 times per minute, measured), so
                     * reading it back would always yield page 0. The firmware
                     * keeps w_page in ARM RAM and flips it at the end of each
                     * scenario; do the same. */
                    static unsigned _fpg = 0;
                    unsigned pg   = _fpg & 1u;
                    uint16_t base = pg ? 0x0814 : 0x0800;
                    uint16_t ctrl = pg ? 0x0824 : 0x0810;
                    uint16_t bid  = (uint16_t)(_fbid++ & 3u);

                    /* These three writes must hit BOTH data[] and api_ram[]:
                     * for any address inside the API window the DSP reads
                     * api_ram[], not data[]:
                     *     data_read_locked():
                     *       if (addr >= C54X_API_BASE && addr < ...+C54X_API_SIZE)
                     *           v = s->api_ram[addr - C54X_API_BASE];
                     * Writing data[] alone lands the forced task in an array
                     * nobody reads. Measured (LD-TRACE probe):
                     *     pc=0xb05f addr=0x0814 data[addr]=0x0018 val_lue=0x0000
                     * The resolved address is right and data[] does hold 24
                     * (ALLC), yet the read returns 0. The whole measured chain
                     * follows: the dispatcher compares 0 against the constants
                     * 12/30/34, bails out at the first branch to 0xb077, the
                     * index resolution is never reached, index 41 (RX arming,
                     * 0xa5cd) is never requested, and A_CD-WR stays 0.
                     *
                     * This makes the FORCE_TASK crutch above EFFECTIVE; it does
                     * not make it legitimate. */
                    s->data[base + 0] = (uint16_t)_ft;         /* d_task_d      */
                    s->data[base + 1] = bid;                   /* d_burst_d     */
                    s->data[ctrl]    |= (uint16_t)(_ftsc & 7); /* d_ctrl_system */
                    if (s->api_ram) {
                        s->api_ram[base + 0 - C54X_API_BASE] = (uint16_t)_ft;
                        s->api_ram[base + 1 - C54X_API_BASE] = bid;
                        s->api_ram[ctrl - C54X_API_BASE]    |= (uint16_t)(_ftsc & 7);
                    }

                    /* End of scenario. Without it, a page is filled that
                     * nobody opens. Exact port of dsp_end_scenario()
                     * (osmocom-bb calypso/dsp.c:466):
                     *     ndb->d_dsp_page = B_GSM_TASK | w_page;   // announce
                     *     w_page ^= 1;                             // flip
                     *     tpu_dsp_frameirq_enable(); tpu_frame_irq_en(1,1);
                     * B_GSM_TASK = (1 << 1) = 0x0002 (l1_environment.h:249) and
                     * the page is bit 0, so d_dsp_page is 0x0002 or 0x0003.
                     * The third part, the frame interrupt, is already faithful
                     * here: calypso_tpu.c tests TPU_CTRL_DSP_EN then
                     * ICTRL_DSP_FRAME active-low, exactly what the firmware
                     * sets. */
                    s->data[0x08D4] = (uint16_t)(0x0002u | pg);
                    if (s->api_ram) {
                        s->api_ram[0x08D4 - 0x0800] = (uint16_t)(0x0002u | pg);
                    }
                    _fpg ^= 1u;                                /* w_page ^= 1 */

                    if (_ftn++ < 20)
                        fprintf(stderr, "[c54x] FORCE-TASK #%u page=%u d_task_d<-%d "
                                "d_burst_d<-%u tsc=%d d_dsp_page<-0x%04x (fin de "
                                "scenario) insn=%u\n", _ftn, pg, _ft, bid, _ftsc,
                                (unsigned)(0x0002u | pg), s->insn_count);
                }
            }

            /* Mutual exclusion: in "init" mode the graft in
             * data_write_locked() does the work and this block must stay
             * quiet, otherwise the two fight each other. */
            static int _at_init = -1;
            if (_at_init < 0) { const char *m = calypso_getenv("CALYPSO_DISPATCH_INSTALL_AT");
                                _at_init = (m && strcmp(m, "init") == 0) ? 1 : 0; }
            if (_di2 >= 0 && !_at_init && exec_pc == 0xb01c) {
                uint16_t _md = s->data[0x0804] ? s->data[0x0804] : s->data[0x0818];
                /* Reinstall on EVERY dispatch of the targeted task, and only
                 * while the slot still holds the plug or our own value: if a
                 * real routine ever installs something else, fall silent
                 * instead of overwriting it. Installing once only would freeze
                 * the handler after the first pass, and firing on any task
                 * would measure the wrong one - task 1 (PM) arrives before task
                 * 5 (FB) - hence _TASK. */
                int _cible = (_ditask < 0) ? (_md != 0) : (_md == (uint16_t)_ditask);
                uint16_t _cur = s->data[0x43d8];
                if (_cible && (_cur == 0xab38 || _cur == (uint16_t)_di2)) {
                    if (_cur != (uint16_t)_di2) {
                        s->data[0x43d8] = (uint16_t)_di2;
                        if (_din2++ < 20)
                            fprintf(stderr, "[c54x] DISPATCH-INSTALL #%u data[0x43d8] "
                                    "0x%04x -> 0x%04x (d_task_md=%u) insn=%u\n",
                                    _din2, _cur, (unsigned)_di2, (unsigned)_md,
                                    s->insn_count);
                    } else {
                        s->data[0x43d8] = (uint16_t)_di2;
                    }
                } else if (_cur != 0xab38 && _cur != (uint16_t)_di2) {
                    static int _dit = 0;
                    if (!_dit++)
                        fprintf(stderr, "[c54x] DISPATCH-INSTALL : un VRAI handler "
                                "0x%04x est apparu dans data[0x43d8] — la bequille "
                                "se retire, cherchez qui l'a pose\n", _cur);
                }
            }
        }
        /* Pure probes DISPCALL .. B2SEQ-IN. DARAM-DUMP (/dev/shm file read by
         * tools/corr_iq.py) stays outside the switch. */
        if (C54X_SONDES) {
        {   /* DISPCALL: does the task dispatcher REALLY call the slot
             * data[0x43d8], and where does it land?
             *
             * The trigger is a condition, not an address to trace:
             * exec_pc == 0xb01c, the slot re-reader, which is known to execute
             * because the DISPATCH_INSTALL crutch already fires there. The PCs
             * ACTUALLY executed over the next CALYPSO_DISPCALL_N (default 10)
             * instructions are then recorded - no assumption about the ISA, nor
             * that 0xb01e is the CALA; this is the measured trajectory.
             *
             * What it decides:
             *   - the sequence passes through the slot value -> the handler IS
             *     called, and the question becomes what it does (follow up with
             *     CALYPSO_TRACEFROM=<slot value>);
             *   - it never enters -> that cell is not the one being called and
             *     the whole 0x43d8 lead falls;
             *   - complete silence -> 0xb01c is not reached, which is different
             *     again, and checkable because the DISPATCH_INSTALL arming line
             *     always prints.
             *
             * The trajectory repeats every frame, so identical runs are folded:
             * one line only when the sequence, the slot or d_task_md CHANGES,
             * carrying the number of repeats swallowed; 40 lines in total.
             * Gate CALYPSO_DISPCALL (default 0). */
            enum { DC_MAX = 24 };
            static int _dc = -1, _dcn2 = 10, _dc_armed = 0, _dc_k = 0, _dc_prevlen = -1;
            static uint16_t _dc_seq[DC_MAX], _dc_prev[DC_MAX];
            static uint16_t _dc_slot = 0, _dc_md = 0, _dc_pslot = 0xffff, _dc_pmd = 0xffff;
            static unsigned _dc_rep = 0, _dc_lines = 0;
            if (_dc < 0) {
                const char *_e = calypso_getenv("CALYPSO_DISPCALL_N");
                _dc = calypso_gate("CALYPSO_DISPCALL", 0);
                if (_e && *_e) _dcn2 = atoi(_e);
                if (_dcn2 < 2) _dcn2 = 2;
                if (_dcn2 > DC_MAX) _dcn2 = DC_MAX;
                if (_dc)
                    fprintf(stderr, "[c54x] DISPCALL arme : %d PC apres chaque passage "
                            "a 0xb01c, replie, 40 lignes max\n", _dcn2);
            }
            if (_dc) {
                if (exec_pc == 0xb01c) {          /* (re)arm */
                    _dc_armed = 1; _dc_k = 0;
                    _dc_slot = s->data[0x43d8];
                    _dc_md = s->data[0x0804] ? s->data[0x0804] : s->data[0x0818];
                }
                if (_dc_armed) {
                    _dc_seq[_dc_k++] = s->pc;     /* PC AFTER the executed instruction */
                    if (_dc_k >= _dcn2) {
                        int _i, _same;
                        _dc_armed = 0;
                        _same = (_dc_prevlen == _dc_k && _dc_slot == _dc_pslot
                                 && _dc_md == _dc_pmd);
                        if (_same)
                            for (_i = 0; _i < _dc_k; _i++)
                                if (_dc_seq[_i] != _dc_prev[_i]) { _same = 0; break; }
                        if (_same) {
                            _dc_rep++;
                        } else {
                            if (_dc_lines < 40) {
                                int _entre = 0, _fb = 0;
                                _dc_lines++;
                                for (_i = 0; _i < _dc_k; _i++) {
                                    if (_dc_seq[_i] == _dc_slot) _entre = 1;
                                    if (_dc_seq[_i] >= 0x7700 && _dc_seq[_i] <= 0x79f0) _fb = 1;
                                }
                                fprintf(stderr, "[c54x] DISPCALL slot=data[0x43d8]=0x%04x "
                                        "d_task_md=%u insn=%u (x%u identiques avant) :",
                                        _dc_slot, (unsigned)_dc_md, s->insn_count, _dc_rep);
                                for (_i = 0; _i < _dc_k; _i++)
                                    fprintf(stderr, " 0x%04x", _dc_seq[_i]);
                                fprintf(stderr, "  -> %s%s\n",
                                        _entre ? "*** ENTRE DANS LE SLOT ***"
                                               : "slot JAMAIS atteint",
                                        _fb ? " + banque FB 0x7700-0x79f0" : "");
                            }
                            _dc_rep = 0;
                            for (_i = 0; _i < _dc_k; _i++) _dc_prev[_i] = _dc_seq[_i];
                            _dc_prevlen = _dc_k; _dc_pslot = _dc_slot; _dc_pmd = _dc_md;
                        }
                    }
                }
            }
        }
        {   /* DMAQ: the firmware's DMA request queue, at the exact moment of
             * the test. Every indirect inference about it proved wrong ("0xaa83
             * is never reached" - no, the PC traces were gated; "the consumer is
             * never called" - it is; "6 reads at 0xaa87" - no, the mailbox
             * monitor folds identical repeated reads). This measures the
             * registers and cells directly at the PC of the `banz`, which is
             * what decides. Gate CALYPSO_DMAQ. */
            static int _dq = -1; static unsigned _dqn = 0;
            if (_dq < 0) _dq = calypso_gate("CALYPSO_DMAQ", 0);
            /* Trigger on a CONDITION, not on the address. Logging the three
             * PCs unconditionally burns the budget at startup (insn ~13M),
             * before any enqueue, and only ever measures "queue empty". Only
             * the moments with something to see are logged:
             *   - the queue is NOT empty at the test (d[433f] != d[433e]), or
             *   - the write pointer GOES BACKWARDS (a reset wiped the queue),
             * plus the very first pass, for a time anchor. */
            if (_dq && _dqn < 60) {
                static uint16_t _dqprev = 0;
                uint16_t _w = s->data[0x433f], _r = s->data[0x433e];
                int _interessant = (_w != _r) || (_dqprev && _w < _dqprev);
                if (exec_pc == 0xaa87 || exec_pc == 0xaa8c || exec_pc == 0xaa8d)
                    _dqprev = _w;
                if ((_interessant || _dqn == 0)
                    && (exec_pc == 0xaa87 || exec_pc == 0xaa8c || exec_pc == 0xaa8d)) {
                    _dqn++;
                    fprintf(stderr, "[c54x] DMAQ PC=0x%04x AR0=%04x AR1=%04x | "
                            "d[433e]=%04x d[433f]=%04x d[3fde]=%04x | %s insn=%u\n",
                            exec_pc, s->ar[0], s->ar[1],
                            s->data[0x433e], s->data[0x433f], s->data[0x3fde],
                            exec_pc == 0xaa8d
                                ? (s->ar[1] ? "banz -> DEPILE" : "banz -> VIDE")
                                : "",
                            s->insn_count);
                }
            }
        }
        {   /* AB38 (CALYPSO_AB38): dumps the opcodes at 0xab38, the shared RET
             * plug, and then follows the flow for 120 instructions to see
             * whether it ever reaches the FB routine. Three rounds. */
            static int _ab = -1;
            if (_ab < 0) _ab = calypso_gate("CALYPSO_AB38", 0);
            if (_ab) {
                static int _dumped = 0, _armed = 0; static unsigned _n = 0, _rounds = 0;
                if (exec_pc == 0xab38) {
                    if (!_dumped) { _dumped = 1;
                        fprintf(stderr, "[c54x] AB38-OPDUMP 0xab38..0xab4f:");
                        for (int _k = 0; _k < 24; _k++)
                            fprintf(stderr, " %04x", prog_fetch(s, (uint16_t)(0xab38 + _k)));
                        fprintf(stderr, "\n"); }
                    if (_rounds < 3) { _rounds++; _armed = 1; _n = 0; }
                }
                if (_armed && _n < 120) { _n++;
                    fprintf(stderr, "[c54x] AB38-FLOW pc=0x%04x xpc=%u op=0x%04x A=0x%06llx insn=%u\n",
                            s->pc, s->xpc, prog_fetch(s, s->pc),
                            (unsigned long long)(s->a & 0xFFFFFFULL), s->insn_count);
                    if (s->pc >= 0x7700 && s->pc <= 0x79f0) {
                        fprintf(stderr, "[c54x] AB38-FLOW *** ATTEINT LA ROUTINE FB 0x%04x ***\n", s->pc);
                        _armed = 0; }
                } else if (_armed) { _armed = 0;
                    fprintf(stderr, "[c54x] AB38-FLOW fin de fenetre (120 pas) sans atteindre 0x7700\n"); }
            }
        }
        {   /* FBROUTE (CALYPSO_FBROUTE): high-water mark of the PC inside the
             * FB routine range, plus the milestones 0x7725 (CALL correlator),
             * 0x798c (unconditional SNR) and 0x79e3/0x79e4 (the guard and the
             * d_fb_det publication), with the DP-relative cell dma(0x7e) the
             * guard wants at 4. */
            static int _fr = -1;
            if (_fr < 0) _fr = calypso_gate("CALYPSO_FBROUTE", 0);
            if (_fr) {
                {   /* FBGATE-TRACE: the last lock, step by step.
                     *
                     * Since the DMA delivers samples the FB routine executes
                     * (it was never entered before) and the state machine in
                     * the DP-relative cell @0x7e advances 0 -> 1 -> 2 -> 3. The
                     * ROM scan gives the only five writers of that cell:
                     *     0x77a8 ST #1   0x77ae ST #2   0x77b4 ST #3
                     *     0x795d ST #4   <- the only one satisfying the guard
                     *                       at 0x79e3
                     * The first four are measured (FBCNT-WR); 0x795d never is,
                     * because the high-water mark stops at 0x794e, fifteen
                     * words earlier.
                     *
                     * What sits at 0x794e (ROM dump):
                     *     0x7949 f310 0005   SUB #5
                     *     0x794b 6ff8 0c3d   absolute access to data[0x0c3d]
                     *     0x794e fc47        CONDITIONAL RETURN - always taken
                     * The routine is not short of data: it DECIDES to leave.
                     *
                     * Reading 0xfc47 as a conditional return (by analogy with
                     * 0xfc00 RET and the 0xfc45 in the dispatcher just before)
                     * and 6ff8 0c3d as an absolute access are both hypotheses,
                     * so the real path and the flags are measured instead of
                     * decoded by hand. If 0x794e is passed even once the
                     * condition is not constant; if it exits every time, the
                     * flags (TC, C) and data[0x0c3d] say which one decides.
                     * data[0x0c3d] is NOT in the range the DMA fills
                     * (0x0cce..0x0cce+296) - 145 words before it. */
                    if (exec_pc >= 0x7944 && exec_pc <= 0x7962) {
                        static unsigned _n = 0;
                        if (_n < 120) {
                            _n++;
                            fprintf(stderr,
                                    /* AR4 is what decides. Cross-disassembly
                                     * (binutils table plus tic54x-dis):
                                     *     0x7944 add *AR4, A ; 0x7945 sub #2, A
                                     *     0x794e rc ALEQ      (return if A <= 0)
                                     * Measured: *AR4 yields 1 then 2 (high
                                     * word), so A is -1 then 0 and the guard
                                     * exits every time; it needs *AR4 > 2. That
                                     * looks like a correlation counter that
                                     * must pass a threshold of 2 - the DSP does
                                     * detect something, just not enough. The
                                     * POINTER and the CELL it points at are
                                     * printed, because the question is now who
                                     * fills that buffer, not what the condition
                                     * evaluates to. */
                                    "[c54x] FBGATE pc=0x%04x op=0x%04x A=0x%06llx "
                                    "B=0x%06llx TC=%d C=%d AR4=0x%04x *AR4=0x%04x "
                                    "(data[]=0x%04x) data[0x0c3d]=0x%04x "
                                    "data[0x41fe]=0x%04x insn=%u\n",
                                    exec_pc, prog_fetch(s, exec_pc),
                                    (unsigned long long)(s->a & 0xFFFFFFULL),
                                    (unsigned long long)(s->b & 0xFFFFFFULL),
                                    (s->st0 >> 12) & 1, (s->st0 >> 11) & 1,
                                    /* Reading s->data[AR4] alone is wrong for
                                     * any address in the API window: the DSP
                                     * reads api_ram[], a DIFFERENT array (see
                                     * data_read_locked). AR4 holds 0x0cd0, well
                                     * inside both the API window and the DMA
                                     * buffer, so data[] alone shows 0x0000
                                     * while the cell is fed. BOTH views are
                                     * printed, so any divergence stays visible
                                     * instead of being assumed. */
                                    s->ar[4],
                                    (s->api_ram && s->ar[4] >= C54X_API_BASE &&
                                     s->ar[4] < C54X_API_BASE + C54X_API_SIZE)
                                        ? s->api_ram[s->ar[4] - C54X_API_BASE]
                                        : s->data[s->ar[4]],
                                    s->data[s->ar[4]],
                                    s->data[0x0c3d], s->data[0x41fe],
                                    s->insn_count);
                            fflush(stderr);
                        }
                    }
                }
                static unsigned _in = 0, _hi = 0, _n76fb = 0;
                if (exec_pc >= 0x7700 && exec_pc <= 0x79f0) {
                    _in++;
                    if (exec_pc > _hi) {
                        _hi = exec_pc;
                        fprintf(stderr, "[c54x] FBROUTE high-water PC=0x%04x (hits=%u) "
                                "[0x7725=CALL corr, 0x798c=SNR incond, 0x79e4=ORM d_fb_det]\n",
                                exec_pc, _in);
                    }
                }
                if (exec_pc == 0x76fb && _n76fb < 10) {
                    _n76fb++;
                    fprintf(stderr, "[c54x] FBROUTE ENTER @0x76fb (BD 0x7700) #%u insn=%u\n",
                            _n76fb, s->insn_count);
                }
                static unsigned _ms[5] = {0,0,0,0,0};
                const uint16_t _mpc[5] = {0x7720, 0x7725, 0x798c, 0x79e3, 0x79e4};
                for (int _k = 0; _k < 5; _k++) {
                    if (exec_pc == _mpc[_k] && _ms[_k]++ < 6) {
                        unsigned _dp = (unsigned)(s->st0 & 0x1FF);
                        unsigned _ea = _dp * 0x80 + 0x7e;   /* dma(0x7e), DP-relative */
                        fprintf(stderr, "[c54x] FBROUTE jalon PC=0x%04x #%u A=0x%06llx "
                                "DP=0x%03x dma(0x7e)=data[0x%04x]=0x%04x (garde veut 4) insn=%u\n",
                                exec_pc, _ms[_k], (unsigned long long)(s->a & 0xFFFFFFULL),
                                _dp, _ea, (_ea < 0x10000) ? s->data[_ea] : 0, s->insn_count);
                    }
                }
            }
        }
        if (exec_pc == 0x93a5) {   /* DARAM 0x2a00 consumer (AR3 post-increment) = the real correlator input */
            static int _b2c = -1; static unsigned _b2cn = 0;
            if (_b2c < 0) _b2c = calypso_gate("CALYPSO_B2SEQ", 0);
            if (_b2c && _b2cn < 8) { _b2cn++;
                fprintf(stderr, "[c54x] B2SEQ-IN 0x2a00@0x93a5 (I,Q)x16:");
                for (int _i = 0; _i < 16; _i++)
                    fprintf(stderr, " (%d,%d)", (int)(int16_t)s->data[0x2a00 + 2*_i], (int)(int16_t)s->data[0x2a00 + 2*_i + 1]);
                fprintf(stderr, "\n"); }
        }
        }   /* C54X_SONDES : DISPCALL .. B2SEQ-IN */
        {
            /* DARAM-DUMP (CALYPSO_DARAM_DUMP): writes the correlator input
             * buffer as binary IQ16, so tools/corr_iq.py --src bursts can
             * measure coherence and dphi instead of judging by eye. */
            static int _dd = -1; static FILE *_ddf = NULL; static unsigned _ddn = 0;
            static uint16_t _ddpc = 0x9ac0; static unsigned _ddmax = 200;
            static uint16_t _ddaddr = 0x2a00;   /* recorded base, see CALYPSO_DARAM_DUMP_ADDR */
            if (_dd < 0) {
                const char *e = calypso_getenv("CALYPSO_DARAM_DUMP");
                _dd = (e && *e && strcmp(e, "0")) ? 1 : 0;
                if (_dd) {
                    const char *path = (strcmp(e, "1") == 0) ? "/dev/shm/daram_2a00.cfile" : e;
                    const char *p = calypso_getenv("CALYPSO_DARAM_DUMP_PC");
                    if (p && *p) _ddpc = (uint16_t)strtol(p, NULL, 0);
                    const char *m = calypso_getenv("CALYPSO_DARAM_DUMP_MAX");
                    if (m && *m) _ddmax = (unsigned)atoi(m);
                    /* The recorded base must be configurable: the native
                     * profiles deliver at CALYPSO_BSP_DARAM_ADDR=0x4c00, and a
                     * hard-coded 0x2a00 records a buffer nobody feeds. This
                     * probe writes from the CPU thread, so it is coherent by
                     * construction - unlike the QEMU monitor, which reads
                     * outside that thread while the BSP rewrites the 296 words
                     * during the `xp`, giving coherence around 0.5 and a
                     * drifting FFT peak (two bursts spliced together). */
                    const char *ad = calypso_getenv("CALYPSO_DARAM_DUMP_ADDR");
                    if (ad && *ad) _ddaddr = (uint16_t)strtol(ad, NULL, 0);
                    fprintf(stderr, "[c54x] DARAM-DUMP base=0x%04x "
                            "(CALYPSO_DARAM_DUMP_ADDR)\n", _ddaddr);
                    _ddf = fopen(path, "wb");
                    fprintf(stderr, "[c54x] DARAM-DUMP armed pc=0x%04x max=%u -> %s (%s)\n",
                            _ddpc, _ddmax, path, _ddf ? "ok" : "FOPEN FAILED");
                }
            }
            /* Record only the passes that are searching for FCCH. Without
             * this guard the _ddmax budget is spent during boot, while
             * d_fb_mode (0x08f9) is 0, so the dump never shows the FB phase and
             * reads as "the buffer never contains FCCH".
             * CALYPSO_DARAM_DUMP_ANYMODE=1 records everything. */
            static int _ddany = -1;
            if (_ddany < 0) { const char *e = calypso_getenv("CALYPSO_DARAM_DUMP_ANYMODE");
                              _ddany = (e && atoi(e) > 0) ? 1 : 0; }
            if (_dd && _ddf && exec_pc == _ddpc && _ddn < _ddmax &&
                (_ddany || s->data[0x08f9] != 0)) {
                unsigned char hdr[12];
                unsigned nw = 296;   /* 296 words = 148 I/Q pairs */
                hdr[0]='I'; hdr[1]='Q'; hdr[2]='1'; hdr[3]='6';
                unsigned _fnv = calypso_daram_last_fn;   /* real GSM fn of the last deposit */
                hdr[4]=(unsigned char)(_fnv & 0xff); hdr[5]=(unsigned char)((_fnv >> 8) & 0xff);
                hdr[6]=(unsigned char)((_fnv >> 16) & 0xff); hdr[7]=(unsigned char)((_fnv >> 24) & 0xff);
                hdr[8]=0;
                hdr[9]=(unsigned char)(nw & 0xff); hdr[10]=(unsigned char)((nw >> 8) & 0xff);
                hdr[11]=0;
                fwrite(hdr, 1, 12, _ddf);
                for (unsigned _k = 0; _k < nw; _k++) {
                    uint16_t _v = s->data[_ddaddr + _k];
                    unsigned char _b[2]; _b[0]=(unsigned char)(_v & 0xff); _b[1]=(unsigned char)(_v >> 8);
                    fwrite(_b, 1, 2, _ddf);
                }
                /* DARAM-SANITY: in-run verdict on what was just dumped.
                 * coh = |Sum z[k+1].conj(z[k])| / Sum |z[k+1]||z[k]|;
                 * dphi = arg(Sum ...). The FB kernel wants FCCH at 1 SPS, so
                 * dphi = +pi/2 (+1.571); +0.393 means 4 SPS undecimated, and
                 * the verdict string names the remedy. */
                if (_ddn == 0 || (_ddn % 50) == 0) {
                    double ar = 0, ai = 0, dn = 0, en = 0;
                    unsigned np = nw / 2;
                    for (unsigned _k = 1; _k < np; _k++) {
                        double i0 = (int16_t)s->data[_ddaddr + 2*(_k-1)];
                        double q0 = (int16_t)s->data[_ddaddr + 2*(_k-1) + 1];
                        double i1 = (int16_t)s->data[_ddaddr + 2*_k];
                        double q1 = (int16_t)s->data[_ddaddr + 2*_k + 1];
                        ar += i1*i0 + q1*q0; ai += q1*i0 - i1*q0;
                        dn += sqrt((i0*i0 + q0*q0) * (i1*i1 + q1*q1));
                        en += i1*i1 + q1*q1;
                    }
                    double coh  = dn > 0 ? sqrt(ar*ar + ai*ai) / dn : 0.0;
                    double dphi = atan2(ai, ar);
                    double rms  = np > 1 ? sqrt(en / (double)(np - 1)) : 0.0;
                    double ad   = fabs(dphi);
                    const char *v;
                    if (rms < 1.0)                        v = "VIDE (buffer non alimente)";
                    else if (coh > 0.90 && ad > 1.37 && ad < 1.77)
                        v = (dphi > 0) ? "FCCH @1SPS OK -- c est ce que le kernel cherche"
                                       : "FCCH @1SPS MIROIR -> CALYPSO_DL_IQ_CONJ=1";
                    else if (coh > 0.90 && ad > 0.29 && ad < 0.49)
                        v = "4 SPS NON DECIME -> CALYPSO_BSP_IQ_DECIM=4 (et FB_IQ_OWNS=0)";
                    else if (coh > 0.90 && ad < 0.29)
                        v = "sur-echantillonne (>4 SPS) ou pas un ton -> verifier la source";
                    else                                  v = "BRUIT/DATA -- pas un ton FCCH";
                    static unsigned _prevwr = 0;
                    unsigned _dwr = calypso_daram_wr_count - _prevwr;
                    _prevwr = calypso_daram_wr_count;
                    fprintf(stderr, "[c54x] DARAM-SANITY rec=%u fn=%u depots_depuis=%u coh=%.3f "
                            "dphi=%+.3f (%+.2fxpi/2) rms=%.0f : %s%s\n", _ddn,
                            calypso_daram_last_fn, _dwr, coh, dphi, dphi / (M_PI/2), rms, v,
                            _dwr == 0 ? "  [BUFFER FIGE : aucun depot BSP depuis le dump precedent]" : "");
                }
                if (++_ddn >= _ddmax) {
                    fflush(_ddf); fclose(_ddf); _ddf = NULL;
                    fprintf(stderr, "[c54x] DARAM-DUMP done : %u records de %u mots\n", _ddn, nw);
                } else if ((_ddn % 20) == 0) {
                    fflush(_ddf);
                    fprintf(stderr, "[c54x] DARAM-DUMP rec=%u\n", _ddn);
                }
            }
        }
        /* [2026-09-23] Sondes pures DETECTOR-RUN .. BOOT-BRANCH. */
        if (C54X_SONDES) {
        if (exec_pc == 0x9ac0) {
            /* B2SEQ (CALYPSO_B2SEQ): dumps 16 (I,Q) pairs from 0x2a00, the
             * real correlator input fed by the BSP/ADC. An Fs/4 pattern is
             * FCCH: (a,0) (0,a) (-a,0) (0,-a) ...; a near-constant sequence is
             * DC with no tone, i.e. an empty input. */
            { static int _b2s = -1; static unsigned _b2sn = 0;
              if (_b2s < 0) _b2s = calypso_gate("CALYPSO_B2SEQ", 0);
              if (_b2s && _b2sn < 8) { _b2sn++;
                  fprintf(stderr, "[c54x] B2SEQ 0x2a00 (I,Q)x16:");
                  for (int _i = 0; _i < 16; _i++)
                      fprintf(stderr, " (%d,%d)", (int)(int16_t)s->data[0x2a00 + 2*_i], (int)(int16_t)s->data[0x2a00 + 2*_i + 1]);
                  fprintf(stderr, "\n"); } }
            static unsigned dr = 0;
            if (dr < 30 || (dr % 200) == 0)
                fprintf(stderr, "[c54x] DETECTOR-RUN #%u @0x9ac0 d_fb_mode[08f9]=0x%04x "
                        "d_fb_det[08f8]=0x%04x insn=%u\n",
                        dr, s->data[0x08f9], s->data[0x08f8], s->insn_count);
            dr++;
            /* B2AR (CALYPSO_B2AR): where the correlator ARs point and what
             * they read. AR5 and AR3 inside [0x2a00..0x2b27] means it reads the
             * FCCH; outside means it reads next to it, i.e. a badly
             * initialised pointer. */
            { static int _b2a = -1; static unsigned _b2an = 0;
              if (_b2a < 0) _b2a = calypso_gate("CALYPSO_B2AR", 0);
              if (_b2a && _b2an < 12) { _b2an++;
                  int _a3in = (s->ar[3] >= 0x2a00 && s->ar[3] < 0x2b28);
                  int _a5in = (s->ar[5] >= 0x2a00 && s->ar[5] < 0x2b28);
                  fprintf(stderr, "[c54x] B2AR @0x9ac0 AR2=%04x AR3=%04x[%d]%s AR4=%04x AR5=%04x[%d]%s\n",
                          s->ar[2], s->ar[3], (int)(int16_t)s->data[s->ar[3]], _a3in?"IN":"OOB",
                          s->ar[4], s->ar[5], (int)(int16_t)s->data[s->ar[5]], _a5in?"IN":"OOB"); } }
            /* B2 (CALYPSO_B2): magnitude of accumulators A and B plus the max
             * and its index over the 296 words of the input (0x2a00) and the
             * workspace (0x2c00). Tells "all zero" from "flat, no peak". */
            {
                static int _b2 = -1; static unsigned _b2n = 0;
                if (_b2 < 0) _b2 = calypso_gate("CALYPSO_B2", 0);
                if (_b2 && _b2n < 24) {
                    _b2n++;
                    int64_t A = ((int64_t)(s->a & 0xFFFFFFFFFFULL) << 24) >> 24;
                    int64_t B = ((int64_t)(s->b & 0xFFFFFFFFFFULL) << 24) >> 24;
                    uint16_t mi = 0, mw = 0; int xi = 0, xw = 0;
                    for (int i = 0; i < 296; i++) { int16_t v = (int16_t)s->data[0x2a00 + i]; uint16_t a = v < 0 ? (uint16_t)(-v) : (uint16_t)v; if (a > mi) { mi = a; xi = i; } }
                    for (int i = 0; i < 296; i++) { int16_t v = (int16_t)s->data[0x2c00 + i]; uint16_t a = v < 0 ? (uint16_t)(-v) : (uint16_t)v; if (a > mw) { mw = a; xw = i; } }
                    fprintf(stderr, "[c54x] B2 @0x9ac0 |A|=%lld |B|=%lld  in(2a00):max=%u@%d  ws(2c00):max=%u@%d\n",
                            (long long)(A < 0 ? -A : A), (long long)(B < 0 ? -B : B), mi, xi, mw, xw);
                }
            }
        }
        if (exec_pc == 0xec07) {
            static unsigned cr = 0;
            if (cr < 30) {
                int64_t a = (s->a & 0x8000000000LL) ? (int64_t)(s->a | ~0xFFFFFFFFFFLL) : (int64_t)s->a;
                int64_t b = (s->b & 0x8000000000LL) ? (int64_t)(s->b | ~0xFFFFFFFFFFLL) : (int64_t)s->b;
                /* angle = atan2(B,A) is computed at analysis time. */
                fprintf(stderr, "[c54x] CORR-ABG #%u A=%lld B=%lld | "
                        "AR2=%04x[%04x] AR3=%04x[%04x] AR4=%04x[%04x] AR5=%04x[%04x] insn=%u\n",
                        cr, (long long)a, (long long)b,
                        s->ar[2], s->data[s->ar[2]], s->ar[3], s->data[s->ar[3]],
                        s->ar[4], s->data[s->ar[4]], s->ar[5], s->data[s->ar[5]],
                        s->insn_count);
                cr++;
            }
        }

        /* CORR-PEAK probe: at the TOA store (PC=0x9ac0, STL A into
         * a_sync_demod) dumps A and B in full, the ARs, T and the input window
         * read (BSP buffer at 0x2a00), to see how the correlator derives the
         * TOA - peak offset, wrap, reference - from an otherwise correct FCCH.
         * 40 lines, negligible cost off-site. */
        if (exec_pc == 0xa0e7 || exec_pc == 0x9ac0) {
            static unsigned cp_log = 0;
            /* Fire only when real I/Q is present, otherwise the budget is spent
             * on the boot with an empty buffer. */
            if (cp_log < 40 && (s->data[0x2a00] || s->data[0x2a02] ||
                                s->data[0x2a04] || s->data[0x2a08] ||
                                s->data[0x2a10] || s->data[0x2a20])) {
                uint16_t a3 = s->ar[3];
                fprintf(stderr,
                    "[c54x] CORR-PEAK #%u PC=0x%04x A=%010llx B=%010llx T=%04x "
                    "AR2=%04x AR3=%04x AR4=%04x AR5=%04x\n"
                    "[c54x]   in@0x2a00: %04x %04x %04x %04x %04x %04x %04x %04x\n"
                    "[c54x]   in@0x2c00: %04x %04x %04x %04x %04x %04x %04x %04x\n"
                    "[c54x]   *AR3@0x%04x: %04x %04x %04x %04x %04x %04x %04x %04x insn=%u\n",
                    cp_log, exec_pc,
                    (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                    (unsigned long long)(s->b & 0xFFFFFFFFFFULL),
                    s->t, s->ar[2], a3, s->ar[4], s->ar[5],
                    s->data[0x2a00], s->data[0x2a01], s->data[0x2a02], s->data[0x2a03],
                    s->data[0x2a04], s->data[0x2a05], s->data[0x2a06], s->data[0x2a07],
                    s->data[0x2c00], s->data[0x2c01], s->data[0x2c02], s->data[0x2c03],
                    s->data[0x2c04], s->data[0x2c05], s->data[0x2c06], s->data[0x2c07],
                    a3,
                    s->data[a3], s->data[(uint16_t)(a3+1)], s->data[(uint16_t)(a3+2)], s->data[(uint16_t)(a3+3)],
                    s->data[(uint16_t)(a3+4)], s->data[(uint16_t)(a3+5)], s->data[(uint16_t)(a3+6)], s->data[(uint16_t)(a3+7)],
                    s->insn_count);
                fflush(stderr);
                cp_log++;
            }
        }

        /* Track A writes (probe 3, post-exec): exec_pc is the PC that just
         * executed, so if A changed, that opcode wrote it. */
        if (s->a != a_before_exec) {
            p_last_a_pc  = exec_pc;
            p_last_a_val = s->a;
        }
        /* End of the CALA-70C3 forensic probes. */

        /* BOOT-BRANCH probe: control-flow skeleton of the boot phase. Hunts
         * the opcode that branches OVER the DSP init code, which should set
         * SP=0x5AC8, AR4/AR5 and IMR=0xFFFF. The IMR-W trace localised the
         * collateral damage at PC=0xf03a (insn 262501) and PC=0x8ebc (insn
         * 5247868), through a stale ARx=0 turning STH B,*ARx+ into a write to
         * MMR_IMR; the bad branch happens earlier. Logs every PC discontinuity
         * (branch, call, return, interrupt entry) while insn_count <= 300000,
         * with SP/IMR/AR4/AR5, which shows when each register was armed, or
         * that it never was. */
        if (s->insn_count <= 300000) {
            /* c54x_exec_one does NOT advance s->pc for a sequential
             * instruction; `s->pc += consumed` further down does. So during
             * the exec itself only a real branch, CALL, RET or interrupt entry
             * modifies s->pc. */
            if (s->pc != exec_pc) {
                static unsigned boot_br_log;
                const unsigned LIMIT = 8000;
                if (boot_br_log < LIMIT) {
                    if (calypso_debug_enabled("BOOT-BRANCH")) fprintf(stderr,
                            "[c54x] BOOT-BRANCH #%u insn=%u %04x(op=%04x,c=%d) → %04x "
                            "SP=%04x IMR=%04x AR4=%04x AR5=%04x INTM=%d\n",
                            boot_br_log, s->insn_count,
                            exec_pc, exec_op, consumed, s->pc,
                            s->sp, s->imr, s->ar[4], s->ar[5],
                            !!(s->st1 & ST1_INTM));
                    boot_br_log++;
                    if (boot_br_log == LIMIT) {
                        if (calypso_debug_enabled("BOOT-BRANCH")) fprintf(stderr,
                                "[c54x] BOOT-BRANCH log capped at %u\n", LIMIT);
                    }
                }
            }
        }
        }   /* C54X_SONDES : DETECTOR-RUN .. BOOT-BRANCH */

        /* INT3-CYCLE-TRACE (CALYPSO_INT3_CYCLE_TRACE=1): records the branch
         * decisions taken during the INT3 ISR cycle. */
        if (!c54x_rapide) int3_cycle_track_branch(s, exec_pc, exec_op, consumed);

        /* SP changes, logged only after init (insn > 490M). */
        if (C54X_SONDES && s->sp != sp_before && s->insn_count > 490000000) {   /* [2026-09-23] sonde */
            static int sp_leak_log = 0;
            if (sp_leak_log < 100) {
                C54_LOG("SP %+d PC=0x%04x op=0x%04x SP 0x%04x→0x%04x insn=%u",
                        (int16_t)(s->sp - sp_before), exec_pc, exec_op, sp_before, s->sp, s->insn_count);
                sp_leak_log++;
            }
        }

        /* SP-FLOOR guard plus delta histogram. Trips on the FIRST descent of
         * SP below SP_FLOOR: that snapshot is taken BEFORE the MMR
         * self-corruption (SP lives at MMR data[0x18]), so it captures the
         * cause, not the crash. The running delta histogram identifies leaking
         * call/return pairs (a far push of 2 words against a near pop of 1
         * loses one word per pair). On by default, a few branches per
         * instruction. */
        #define SP_FLOOR 0x0080
        if (!c54x_rapide)   /* SP-delta stats, A-write ring, transfer ring, NOP guard, SP floor */
        {
            static int sp_floor_tripped = 0;
            static uint64_t sp_delta_pushf = 0;  /* delta == -2 */
            static uint64_t sp_delta_pushn = 0;  /* delta == -1 */
            static uint64_t sp_delta_popn  = 0;  /* delta == +1 */
            static uint64_t sp_delta_popf  = 0;  /* delta == +2 */
            static uint64_t sp_delta_other = 0;  /* anything else (jumps, LD#k,SP) */
            static uint64_t sp_delta_log_n = 0;

            int delta = (int)(int16_t)(s->sp - sp_before);
            if (delta != 0) {
                switch (delta) {
                    case -2: sp_delta_pushf++; break;
                    case -1: sp_delta_pushn++; break;
                    case  1: sp_delta_popn++;  break;
                    case  2: sp_delta_popf++;  break;
                    default: sp_delta_other++; break;
                }
                /* First 80 SP changes, then every 5000: enough to
                 * characterise the leaking call/return pair without drowning
                 * the log. */
                sp_delta_log_n++;
                if (sp_delta_log_n <= 80 || (sp_delta_log_n % 5000) == 0) {
                    C54_LOG("SP-Δ #%llu PC=0x%04x op=0x%04x XPC=%u Δ%+d  SP 0x%04x→0x%04x insn=%u",
                            (unsigned long long)sp_delta_log_n,
                            exec_pc, exec_op, s->xpc & 0x3,
                            delta, sp_before, s->sp, s->insn_count);
                }
            }

            /* Detectors, once per instruction AFTER exec_one. */
            /* (1) A-write ring: track each modification of s->a */
            if (s->a != pre_a) {
                awrite_log_push(pre_pc, pre_xpc, pre_op, pre_a, s->a, s->insn_count);
            }
            /* (2) Transfer ring : detect non-sequential PC change.
             *   exec_one returns `consumed` = instruction size in words. If
             *   new PC != pre_pc + consumed (and no delay-slot pending), it's
             *   a transfer. We also catch XPC changes (FAR transfers). */
            uint16_t expected_pc = (uint16_t)(pre_pc + consumed);
            if ((s->pc != expected_pc || (s->xpc & 0x3) != pre_xpc)
                && s->delay_slots == 0) {
                xfer_log_push(pre_pc, pre_xpc, pre_op,
                              s->pc, s->xpc & 0x3, pre_a, s->insn_count);
            }
            /* (3) NOP-region guard : trip ONCE at first entry into the
             * unmapped prog zone (PC <0x7000 in bank 0, outside OVLY DARAM
             * 0x80-0x27FF). Dumps trigger + transfer ring + A-write ring. */
            if (!g_nop_tripped && pc_in_nop_region(s, s->pc, s->xpc & 0x3)) {
                nop_guard_dump(s, s->pc, s->xpc & 0x3);
            }

            if (!sp_floor_tripped && s->sp < SP_FLOOR) {
                sp_floor_tripped = 1;
                long long net_push = (long long)(sp_delta_pushf*2 + sp_delta_pushn);
                long long net_pop  = (long long)(sp_delta_popf *2 + sp_delta_popn);
                C54_LOG("================================================");
                C54_LOG("SP-FLOOR TRIPPED  SP=0x%04x < 0x%04x  insn=%u",
                        s->sp, (unsigned)SP_FLOOR, s->insn_count);
                C54_LOG("  trigger PC=0x%04x op=0x%04x XPC=%u Δ%+d (SP 0x%04x→0x%04x)",
                        exec_pc, exec_op, s->xpc & 0x3, delta, sp_before, s->sp);
                C54_LOG("  ST1 INTM=%d  IFR=0x%04x  IMR=0x%04x",
                        !!(s->st1 & ST1_INTM), s->ifr, s->imr);
                C54_LOG("SP delta histogram :");
                C54_LOG("  push far  (Δ=-2) : %llu", (unsigned long long)sp_delta_pushf);
                C54_LOG("  push near (Δ=-1) : %llu", (unsigned long long)sp_delta_pushn);
                C54_LOG("  pop  near (Δ=+1) : %llu", (unsigned long long)sp_delta_popn);
                C54_LOG("  pop  far  (Δ=+2) : %llu", (unsigned long long)sp_delta_popf);
                C54_LOG("  other            : %llu", (unsigned long long)sp_delta_other);
                C54_LOG("  net push - pop   : %lld words (positive = SP leaked downward)",
                        net_push - net_pop);
                C54_LOG("================================================");
            }
        }
        #undef SP_FLOOR

        /* SP observability:
         *   (a) sp_trail[256]  : |delta| > 32 events (scheduler reloads, large
         *                        allocations)
         *   (b) sp_low watermark: every new low, coalesced per PC on powers of
         *                        ten
         * Gated to insn > 33754, i.e. after the normal init stack move
         * 0x9022 -> 0x5ac8.
         * [2026-09-23] Sondes pures (trail/sp_low, SP-CATASTROPHE) derriere
         * l'interrupteur. */
        if (C54X_SONDES) {
        {
            static int trap_armed = -1;
            if (trap_armed < 0) {
                const char *e = cdbg_env("TRAP-OOR"); (void)e;
                /* Forced off: this was the SP descent / SP-CATASTROPHE
                 * analysis, now resolved (0x70c3 self-CALA, DROM LUT, stub),
                 * and the companion site below set s->running = 0 at the 4.2M
                 * checkpoint, which halted the DSP under CALYPSO_DEBUG=ALL. */
                trap_armed = 0;
            }
            if (trap_armed && s->sp != sp_before && s->insn_count > 33754) {
                int16_t delta = (int16_t)(s->sp - sp_before);
                uint16_t a_low = (uint16_t)(s->a & 0xFFFF);

                /* Per-PC SP-HIST accounting lives at the top-of-loop
                 * chokepoint, which is bypass-proof. Calling it here too would
                 * double-count. */

                /* (a) trail: big jumps only, skipping push/pop noise of 1..32 */
                if (delta > 32 || delta < -32) {
                    unsigned k = g_sp_trail_idx & 255;
                    g_sp_trail[k].insn    = s->insn_count;
                    g_sp_trail[k].old_sp  = sp_before;
                    g_sp_trail[k].new_sp  = s->sp;
                    g_sp_trail[k].exec_pc = exec_pc;
                    g_sp_trail[k].exec_op = exec_op;
                    g_sp_trail[k].a_low   = a_low;
                    g_sp_trail_idx++;
                }

                /* (b) sp_low watermark: fires on any new low, including
                 * delta = -1. */
                if (s->sp < g_sp_low) {
                    g_sp_low = s->sp;
                    if (exec_pc == g_sp_low_pc) {
                        g_sp_low_hits_at_pc++;
                        unsigned n = g_sp_low_hits_at_pc;
                        bool milestone = (n == 1 || n == 10 || n == 100 ||
                                          n == 1000 || n == 10000 || n == 100000);
                        if (milestone) {
                            if (calypso_debug_enabled("SP-LOW")) fprintf(stderr,
                                "[c54x] SP-LOW #%u @pc=0x%04x op=0x%04x "
                                "sp 0x%04x->0x%04x A_low=0x%04x insn=%u\n",
                                n, exec_pc, exec_op,
                                sp_before, s->sp, a_low, s->insn_count);
                        }
                    } else {
                        g_sp_low_pc = exec_pc;
                        g_sp_low_hits_at_pc = 1;
                        g_sp_low_distinct_pcs++;
                        if (calypso_debug_enabled("SP-LOW")) fprintf(stderr,
                            "[c54x] SP-LOW NEW (#%u distinct) @pc=0x%04x op=0x%04x "
                            "sp 0x%04x->0x%04x A_low=0x%04x insn=%u\n",
                            g_sp_low_distinct_pcs, exec_pc, exec_op,
                            sp_before, s->sp, a_low, s->insn_count);
                    }
                }
            }
        }

        /* SP catastrophic delta tracer. SP has been seen going from 0x9c1e to
         * 0x0001 inside one window, losing ~40k stack words; the progressive
         * leak log above caps at 100 small deltas and misses that single
         * event. This block flags any |delta| > 256 in one instruction, never
         * capped, so the offending STM / PSHM / POPM / RETE-on-corrupt-stack /
         * FRAME-with-huge-offset is named the FIRST time it happens. The ARs
         * are included, to see whether the ST/LD destination resolved to an MMR
         * slot (for instance *AR = 0x18 = MMR_SP).
         *
         * The threshold is 256, not 100, to filter legitimate FRAME #imm8s,
         * which is a signed 8-bit value and can reach +-127. Real
         * catastrophes, from a dual-operand write into MMR_SP, are always
         * thousands of words. */
        {
            int32_t dsp = (int32_t)(int16_t)(s->sp - sp_before);
            if (dsp > 256 || dsp < -256) {
                if (calypso_debug_enabled("SP-CATASTROPHE")) fprintf(stderr,
                        "[c54x] SP-CATASTROPHE Δ=%+d PC=0x%04x op=0x%04x "
                        "SP 0x%04x → 0x%04x INTM=%d "
                        "AR0..7: %04x %04x %04x %04x %04x %04x %04x %04x "
                        "BK=%04x A=%010llx insn=%u\n",
                        (int)dsp, exec_pc, exec_op, sp_before, s->sp,
                        !!(s->st1 & ST1_INTM),
                        s->ar[0], s->ar[1], s->ar[2], s->ar[3],
                        s->ar[4], s->ar[5], s->ar[6], s->ar[7],
                        s->bk,
                        (unsigned long long)(s->a & 0xFFFFFFFFFFULL),
                        s->insn_count);
            }
        }
        }   /* C54X_SONDES : trail/sp_low, SP-CATASTROPHE */
        /* TRAP-OOR firing point: halt at a fixed insn checkpoint and dump the
         * trail plus sp_low, for offline analysis of the whole descent. No PC
         * whitelist and no SP edge can catch the clobber, because it lives in
         * legitimate code. Checkpoint set by CALYPSO_TRAP_CHECKPOINT (default
         * 4200000, just after the SP recovery 0x0008 -> 0x2900 at insn
         * 4.09M). */
        {
            static int trap_armed = -1;
            static int tripped = 0;
            static unsigned checkpoint = 0;
            if (trap_armed < 0) {
                const char *e = cdbg_env("TRAP-OOR"); (void)e;
                /* Forced off: setting s->running = 0 at the checkpoint halted
                 * the DSP under CALYPSO_DEBUG=ALL, and the analysis it served
                 * is resolved. */
                trap_armed = 0;
                const char *c = calypso_getenv("CALYPSO_TRAP_CHECKPOINT");
                checkpoint = (c && *c) ? (unsigned)strtoul(c, NULL, 0) : 4200000u;
            }
            if (trap_armed && !tripped && s->insn_count >= checkpoint) {
                tripped = 1;
                dsp_trap_dump(s, exec_pc, exec_op, sp_before, "CHECKPOINT");
                s->running = 0;
            }
        }

        /* DUAL-OP-INTERPRET diagnostic. Compares the decoder's AR field
         * interpretation (3-bit fields) with the SPRU172C dual-operand
         * encoding (2-bit AR fields plus an offset of 2, AR2..AR5 only). When
         * the two disagree on which AR is used and SP-CATASTROPHE has just
         * fired, the encoding is the suspect. 100 entries. */
        if (C54X_SONDES && (exec_op & 0xFC00) == 0xC800 && (   /* [2026-09-23] sonde */
             (int32_t)(int16_t)(s->sp - sp_before) > 100 ||
             (int32_t)(int16_t)(s->sp - sp_before) < -100)) {
            static unsigned dop_log;
            if (dop_log++ < 100) {
                int xar_cur = (exec_op >> 4) & 0x07;
                int yar_cur = exec_op & 0x07;
                int xar_spru = ((exec_op >> 4) & 0x03) + 2;
                int yar_spru = (exec_op & 0x03) + 2;
                int xmod_spru = (exec_op >> 6) & 0x03;
                int ymod_spru = (exec_op >> 2) & 0x03;
                fprintf(stderr,
                        "[c54x] DUAL-OP-INTERPRET op=0x%04x PC=0x%04x : "
                        "current_dec X=AR%d Y=AR%d (3bit) | "
                        "SPRU172C    X=AR%d Y=AR%d xmod=%d ymod=%d (2bit+2) | "
                        "AR%d_cur=%04x AR%d_spru=%04x | "
                        "AR%d_cur=%04x AR%d_spru=%04x\n",
                        exec_op, exec_pc,
                        xar_cur, yar_cur,
                        xar_spru, yar_spru, xmod_spru, ymod_spru,
                        xar_cur, s->ar[xar_cur],
                        xar_spru, s->ar[xar_spru],
                        yar_cur, s->ar[yar_cur],
                        yar_spru, s->ar[yar_spru]);
            }
        }

        /* Snapshot the just-executed PC/op into C54xState so other
         * tracers (in particular INTM-TRANS at top of next iteration)
         * can attribute post-instruction state changes to the cause. */
        s->last_exec_pc = exec_pc;
        s->last_exec_op = exec_op;


        /* RPT: after executing an instruction while a repeat is active,
         * re-execute the SAME instruction (do not advance PC) until the count
         * reaches 0. */
        if (s->rpt_active && !s->idle) {
            /* RPT #k repeats the next instruction k+1 times (TI SPRU172C).
             * The RPT handler sets rpt_active = 1, advances the PC and returns;
             * this block runs immediately afterwards with rpt_active already
             * true, so without the guard below the RPT instruction itself would
             * consume one repetition and the repeated instruction would run
             * only k times.
             *
             * Measured (READA-ITER probe on the table loader at 0xb4c4):
             *     rpt #0x02 ; reada *AR1+
             *     #1 addr=0x43d5 rpt_count=1   <- should be 2
             *     #2 addr=0x43d6 rpt_count=0
             *     (no #3)      -> data[0x43d7] NEVER initialised
             * The first loop of the same loader, `rpt #0x4d`, writes
             * 0x4387..0x43d3 = 77 words = k, whereas k+1 = 78 would reach
             * 0x43d4. Both loops are short by exactly one copy.
             *
             * Scope: EVERY RPT loop of the firmware, including the block copy
             * `rpt #0x0b ; mvdd` that feeds a_cd[3..14], which copied only 11
             * of the 12 L2 payload words. Handling it here, once, covers every
             * form (RPT #k8, #lk, Smem, RPTZ).
             *
             * This is not an error in the computation of rpt_count - the eight
             * setters are correct - it is the loop consuming one iteration too
             * many. */
            if (!rpt_was_active) {
                /* The instruction just executed IS the RPT, so it must
                 * not consume a repetition. The handler already advanced
                 * the PC, so loop again without decrementing. */
                s->cycles++;
                executed++;
                continue;
            }
            if (s->rpt_count > 0) {
                s->rpt_count--;
                /* Don't advance PC — re-execute same instruction next cycle */
                s->cycles++;
                executed++;
                if (s->rpt_count == 0) {
                    static int rpt_done_log = 0;
                    if (rpt_done_log < 10)
                        C54_LOG("RPT DONE PC=0x%04x op=0x%04x count_was=%d", s->pc, prog_fetch(s, s->pc), 0);
                    rpt_done_log++;
                }
                continue;
            } else {
                s->rpt_active = false;
                s->par_set = false;
            }
        }

        if (consumed > 0)
            s->pc += consumed;
        s->pc &= 0xFFFF;  /* the C54x PC is 16 bits (23 with XPC, but it wraps at 16) */
        /* consumed == 0 means PC was set by branch */

        /* SP-CORRUPT watchpoint: which instruction takes SP out of the valid
         * stack range [0x5900, 0x5c00]? One observed derail is 0xa58d leaving
         * SP = 0xc905 after a POPD. Logs the first inside -> outside
         * transition, whose pc/op is the culprit. */
        {
            static uint16_t g_wp_prev_sp = 0x5ac8;
            if ((s->sp < 0x5900 || s->sp > 0x5c00) &&
                (g_wp_prev_sp >= 0x5900 && g_wp_prev_sp <= 0x5c00)) {
                static unsigned wpn = 0;
                if (wpn++ < 20)
                    fprintf(stderr, "[c54x] SP-CORRUPT pc=0x%04x op=0x%04x sp 0x%04x -> 0x%04x insn=%u\n",
                            exec_pc, exec_op, g_wp_prev_sp, s->sp, s->insn_count);
                /* ANNEAU-EXEC + registers dump, first 3 SP-CORRUPT.
                 * insn_count == 0 is the reset artefact (g_wp_prev_sp starts at
                 * 0x5ac8, the reset sets SP=0x1100): excluded. */
                static unsigned wpd = 0;
                if (s->insn_count > 0 && wpd < 3) {
                    wpd++;
                    sp_corrupt_vidage(s, exec_pc, g_wp_prev_sp,
                                      g_c54x_it_prises != it_avant);
                }
            }
            g_wp_prev_sp = s->sp;
        }

        /* On-chip TIMER0 tick: the missing clock. Go-live arms IMR bit 4
         * (TINT, vec 20) and waits for the timer, but the TIM/PRD/TCR
         * registers alone never decrement, so TINT never fires. TIM is ticked
         * here (through the TDDR prescaler) once per instruction; on underflow
         * it reloads from PRD and fires TINT. Gate CALYPSO_DSP_TIMER_OFF for
         * A/B, on by default. The firmware starts the timer (clearing TSS) and
         * configures PRD/TDDR. */
        {
            {
                /* @BEQUILLE - TINT0_PERINSN  (CALYPSO_TINT0_PERINSN, EXISTS, default OFF)
                 *   masks   : the absence of a DSP time base. Fires TINT every 2000
                 *             insns, unrelated to the TDMA cadence.
                 *   remove  : superseded by the faithful TIMER0 tick just below.
                 *   TRAP    : this code IS executed; only the absence of the environment
                 *             variable keeps it quiet.
                 */
                static int _t0pi = -1; if (_t0pi < 0) _t0pi = calypso_getenv("CALYPSO_TINT0_PERINSN") ? 1 : 0;
        if (_t0pi) {
                    static unsigned _t0c = 0;
                    if (++_t0c >= 2000) { _t0c = 0; c54x_fire_tint(s); }
                }
            }
            static int _tmr = -1;
            if (_tmr < 0) _tmr = calypso_getenv("CALYPSO_DSP_TIMER_OFF") ? 0 : 1;
            /* @BEQUILLE - TINT0_MASTER  (CALYPSO_TINT0_MASTER, EXISTS, default OFF
             *              outside the WIRE profile - calypso.env/wire.env only set it
             *              under CALYPSO_WIRE=1)
             *   masks   : the ROM's TIMER0 configuration (TCR/PRD). The firmware stops the
             *             timer (TSS=1) in an init that never runs, so PRD is forced to
             *             0xFFFF and TIM is ticked DESPITE TSS. On real hardware TINT0 is
             *             the TDMA master clock.
             *   remove  : when the ROM's TIMER0 init sequence executes (TCR programmed,
             *             TSS=0).
             */
            static int _t0master = -1;
            if (_t0master < 0) _t0master = calypso_gate("CALYPSO_TINT0_MASTER", 0);
            /* Under TINT0_MASTER the ROM is modelled as having configured and
             * started the timer, so TIM ticks despite TSS. With PRD left at its
             * reset value the underflow comes every ~65536 insns, which is
             * about one TDMA frame at 13 MHz. The underflow fires TINT through
             * c54x_fire_tint(), which HONOURS the IMR - nothing is forced. */
            if (_t0master && s->data[PRD_ADDR] == 0) s->data[PRD_ADDR] = 0xFFFF;
            if (_tmr && (_t0master || !(s->data[TCR_ADDR] & TCR_TSS))) {
                if (s->timer_psc == 0) {
                    s->timer_psc = s->data[TCR_ADDR] & TCR_TDDR_MASK;
                    if (s->data[TIM_ADDR] == 0) {
                        s->data[TIM_ADDR] = s->data[PRD_ADDR];
                        static unsigned _tn = 0;
                        if (_tn++ < 8)
                            fprintf(stderr, "[c54x] DSP-TIMER TINT fire "
                                    "PRD=0x%04x TDDR=%u IMR=0x%04x INTM=%d insn=%u\n",
                                    s->data[PRD_ADDR], (unsigned)(s->data[TCR_ADDR] & TCR_TDDR_MASK),
                                    s->imr, (s->st1 & ST1_INTM) ? 1 : 0, s->insn_count);
                        c54x_fire_tint(s);   /* SPRU131 5.1: TINT = bit 3 / vec 19 */
                    } else {
                        s->data[TIM_ADDR]--;
                    }
                } else {
                    s->timer_psc--;
                }
            }
        }

        /* BRANCH-TRACE: consumed == 0 means the PC was set by a TAKEN branch,
         * call or return, since a sequential instruction goes through
         * `s->pc += consumed` above. Logs EVERY taken control transfer in the
         * boot/init window (insn < 6000), filtering out the PC=0 storm, with
         * the site, the branch opcode, the target and the condition state (TC,
         * A == 0, A, ARx, IMR, INTM). The point is to see WHICH conditional
         * branch diverts the firmware from installing vectors and enabling
         * interrupts into the idle guard. It discriminates (a) a taken BC/BANZ
         * whose target short-circuits the IPTR write, which is then never
         * reached, from (b) the code reaching that write with no effect, which
         * would be an MMR bug. 700 lines. */
        if (C54X_SONDES && consumed == 0 && exec_pc != 0 && s->pc != 0 && s->insn_count < 6000) {   /* [2026-09-23] sonde */
            static unsigned bt = 0;
            if (bt++ < 700) {
                uint64_t aa = s->a & 0xFFFFFFFFFFULL;
                fprintf(stderr, "[c54x] BRANCH-TRACE #%u site=0x%04x op=0x%04x -> "
                        "tgt=0x%04x TC=%d Az=%d A=0x%010llx AR1=%04x AR3=%04x "
                        "AR5=%04x SP=%04x IMR=%04x INTM=%d insn=%u\n",
                        bt, exec_pc, exec_op, s->pc,
                        (s->st0 & ST0_TC) ? 1 : 0, (aa == 0) ? 1 : 0,
                        (unsigned long long)aa, s->ar[1], s->ar[3], s->ar[5],
                        s->sp, s->imr, (s->st1 & ST1_INTM) ? 1 : 0, s->insn_count);
            }
        }

        /* Delayed-branch slot countdown, counted in WORDS, not instructions.
         * A delayed branch (RCD, CALLD, RETD, BD, CCD) sets delayed_pc and
         * delay_slots = 2; the following instructions execute as pipeline
         * slots, and the branch commits by forcing PC to delayed_pc once the
         * two words are done.
         *
         * Per SPRU172C the C54x ALWAYS has 2 words of delay: one 2-word
         * instruction OR two 1-word instructions. Decrementing once per
         * instruction executes only ONE delay-slot instruction, which is right
         * for a single 2-word slot (STM #k) but SKIPS the second of two 1-word
         * slots. When that second instruction is a PSHM/PSHD (PROM0 sites
         * 0xb53a, 0xc9a2, 0xcaab, the power-scan code the mobile runs during
         * cell search), the push is lost: ~58 words of cumulative over-pop,
         * POPM ST0 at 0x94f3 picks up the orphan 0x80fd, DP becomes 0x0fd, the
         * dispatcher at 0x8341 reads a garbage LUT, CALAD 0x70c3 is a
         * self-CALA, 0x70c4 (28868) lands in d_fb_det/a_pm, and rxlev/TOA are
         * poisoned into NO_CELL_FOUND. See doc/SP_CATASTROPHE_70c4_SEQUENCE.
         *
         * So decrement by the number of WORDS executed (consumed), and do NOT
         * count the arming iteration (ds_before == 0 is the branch itself; the
         * delay starts with the next instruction). */
        const uint16_t pc_sequentiel = (uint16_t)s->pc;   /* PC after `s->pc += consumed`, before any delayed commit */
        if (s->delay_slots > 0) {
            if (ds_before == 0) {
                /* The delayed branch's own iteration: decrement nothing;
                 * delay_slots (= 2) counts WORDS. */
            } else {
                int wexec = (consumed > 0) ? consumed : 1;
                if (s->delay_slots > wexec) s->delay_slots -= wexec;
                else                        s->delay_slots = 0;
                if (s->delay_slots == 0)
                    s->pc = s->delayed_pc;
            }
        }


        /* === RPTB (block repeat) end-of-body check ===
         * Must run AFTER PC advance and delayed-branch settle so the
         * redirect to RSA is the final word on s->pc for this iteration.
         * Triggers when PC has overshot REA (= reached REA+1 or beyond,
         * accounting for 2-word instructions at the body's tail). Skip
         * during RPT (single-instruction repeat has priority).
         *
         * [2026-10-03] Only a SEQUENTIAL step that crosses REA loops back: the
         * instruction just executed started at or before REA, the PC was
         * advanced by its own words (no taken branch, no delayed commit), and
         * now lies past REA. A call or branch to a target beyond REA does not
         * end the block on the C54x. The old `pc >= rea + 1` did: in the CHED
         * speech-quality loop (PROM0 `rptb 0x9b2a` at 0x9ad3), `calad b` to
         * the 0x9b2c table (`retd ; stl b,@0x62`) restarted the block at RSA
         * instead of calling, skipping the rest of the body (0x9ae6..0x9b2a)
         * on every iteration: the SD values tested against d_sd_min_thr_tchfs
         * were never computed, and B_BFI was set on 100 % of the speech
         * frames. CALYPSO_RPTB_GE=1 restores the old test. */
        static int rptb_ge = -1;
        if (rptb_ge < 0) { const char *e = getenv("CALYPSO_RPTB_GE"); rptb_ge = (e && *e == '1') ? 1 : 0; }
        const bool rptb_fin = rptb_ge
            ? (s->pc >= s->rea + 1)
            : (consumed > 0 && (uint16_t)s->pc == pc_sequentiel &&
               exec_pc <= s->rea && (uint16_t)s->pc > s->rea);
        if (s->rptb_active && !s->rpt_active && rptb_fin) {
            static int rptb_log = 0;
            if (rptb_log < 20) {
                C54_LOG("RPTB redirect PC=0x%04x→RSA=0x%04x REA=0x%04x BRC=%d",
                        s->pc, s->rsa, s->rea, s->brc);
                rptb_log++;
            }
            if (s->brc > 0) {
                s->brc--;
                s->pc = s->rsa;
            } else {
                s->rptb_active = false;
                { static int _re=0;
                  if (_re<50) {
                    C54_LOG("RPTB EXIT PC=0x%04x RSA=0x%04x REA=0x%04x insn=%u SP=0x%04x",
                            s->pc, s->rsa, s->rea, s->insn_count, s->sp);
                    _re++;
                  }
                }
                s->st1 &= ~ST1_BRAF;
            }
        }

        s->cycles++;
        s->insn_count++;

        executed++;

        /* SP-LEDGER: periodic dump to check that net_words tends to 0 over a
         * long run - the push/pop balance metric. About one compare per
         * instruction. */
        if (C54X_SONDES && s->insn_count - g_sp_ledger.last_dump_insn >= 20000000u) {   /* [2026-09-23] sonde */
            g_sp_ledger.last_dump_insn = s->insn_count;
            fprintf(stderr,
                "[c54x] SP-LEDGER insn=%u PC=0x%04x SP=0x%04x net_words=%lld pushes=%llu pops=%llu irq=%llu\n",
                s->insn_count, s->pc, s->sp, (long long)g_sp_ledger.net_words,
                (unsigned long long)g_sp_ledger.sp_pushes,
                (unsigned long long)g_sp_ledger.sp_pops,
                (unsigned long long)g_sp_ledger.irq_entries);
            fflush(stderr);
        }

        /* Yield throttle (CALYPSO_DSP_YIELD=N). The c54x runs synchronously
         * inside tdma_tick on the main thread; every N instructions c54x_run
         * returns so the main loop can pump I/O (osmocon) and then deliver
         * interrupts to the DSP.
         *   small N = frequent yields = fast osmocon, slower DSP
         *   large N or 0 = rare yields or off, the DSP keeps the whole budget
         *
         * The break MUST happen at a clean boundary: after `s->pc += consumed`
         * AND after the delay slots have committed (delay_slots == 0).
         * Otherwise (a) the current instruction is re-executed on re-entry,
         * double-popping, and (b) an interrupt delivered by the main loop would
         * land in the middle of a RETD/RCD delay slot and corrupt the deferred
         * return. Both lead to SP over-pop, garbage DP and the self-CALA at
         * 0x70c3. */
        {
            static int dsp_yield = -1;
            if (dsp_yield < 0) {
                const char *e = calypso_getenv("CALYPSO_DSP_YIELD");
                /* Default 32768 (2^15): the empirically tuned DSP / osmocon
                 * interrupt cadence, on by default. Only an explicit
                 * CALYPSO_DSP_YIELD=0 turns it off. */
                dsp_yield = (e && *e) ? atoi(e) : 32768;
                if (dsp_yield < 0) dsp_yield = 0;
                fprintf(stderr, "[c54x] CALYPSO_DSP_YIELD = %d insn/yield %s\n",
                        dsp_yield, dsp_yield ? "(variateur ON)" : "(OFF, legacy)");
            }
            /* Yield only at an INTERRUPTIBLE point. The yield hands control to
             * the main loop, which delivers the interrupt to the DSP, and on a
             * real C54x an interrupt is only taken with INTM=0, outside a
             * critical section. Breaking on a plain instruction counter lands
             * in the middle of a dispatch sequence (INTM=1, DP inherited before
             * the LDP), and the interrupt on resume then corrupts DP/ST0,
             * giving a CALAD into the LUT (the 0x9207 wedge) or a self-CALA.
             *
             * The guards are the four non-interruptible states of the C54x:
             *   delay_slots == 0 : not inside a delayed branch (RETD/RCD/CALLD/BD)
             *   !rpt_active      : not inside an RPT - a single repeat is NOT
             *                      interruptible on hardware until RC is
             *                      exhausted, whereas RPTB is
             *   INTM == 0        : interruptible, outside a critical section
             *   plus the break happening after the PC and delay commit, so
             *   never mid-instruction
             * The hard cap at 4x forces a yield when INTM stays 1 abnormally
             * long, to avoid starving the main loop; that case is traced
             * below. */
            if (dsp_yield > 0 && s->delay_slots == 0 && !s->rpt_active &&
                ((executed >= (unsigned)dsp_yield && !(s->st1 & ST1_INTM)) ||
                 executed >= (unsigned)dsp_yield * 4u)) {
                /* Evidence for the gate: log the first breaks and every
                 * cap-forced break (INTM=1, tolerated but illegal). Never
                 * seeing a cap-forced break over N runs while 0x9207
                 * disappears proves the gate rather than merely observing
                 * it. */
                if (calypso_debug_enabled("YIELD-BREAK")) {
                    static unsigned yb = 0;
                    int forced = (s->st1 & ST1_INTM) ? 1 : 0;
                    if (yb < 40 || forced)
                        fprintf(stderr, "[c54x] YIELD-BREAK #%u INTM=%d delay=%d rpt=%d pc=0x%04x exec=%u %s\n",
                                yb, forced, s->delay_slots, s->rpt_active, s->pc, executed,
                                forced ? "*** CAP-FORCED (INTM=1 illegal) ***" : "(safe)");
                    yb++;
                }
                break;   /* clean, interruptible boundary: the main loop serves I/O and interrupts */
            }
        }
    }
    return executed;
}

/* ================================================================
 * ROM loader
 * ================================================================ */
  ×3
/* ================================================================
 * Init / Reset / Interrupts
 * ================================================================ */

C54xState *c54x_init(void)
{
    C54xState *s = calloc(1, sizeof(C54xState));
    if (!s) return NULL;
    c54x_sondes_resoudre();   /* switch of the pure probes */
    return s;
}

void c54x_set_api_ram(C54xState *s, uint16_t *api_ram)
{
    s->api_ram = api_ram;
}

void c54x_set_initial_pc(C54xState *s, uint32_t pc)
{
    s->pc = pc;
    s->blob_loaded = true;
    C54_LOG("set_initial_pc: PC=0x%05x (blob_loaded=1)", pc);
}

/* ==========================================================================
 * c54x early boot. Only active under CALYPSO_DSP_RUN_C54X; it forces no
 * mailbox value, only WHEN the DSP boots.
 *
 * Ordering matters. The ARM posts its bootloader command (data[0x0fff] = cmd
 * 2 or 4, data[0x0ffe] = entry) as early as fn=0, +0.073 s. If the DSP boots
 * after that, its IDLE init at 0xb419 (`ST #1, *0xfff`) OVERWRITES that
 * 0x0002 and it spins for ever in 0xb41c. Booting here, at machine init and
 * therefore BEFORE the ARM vCPU runs, the DSP sets its IDLE and parks in
 * 0xb41c BEFORE the ARM write: 0xb419 does not run again (the PC persists
 * across wake-ups), the command survives, and the first wake-up consumes it.
 * ========================================================================== */
bool g_c54x_early_booted;

void c54x_early_boot(C54xState *s)
{
    static int run_c54x = -1;
    if (run_c54x < 0) {
        const char *e = calypso_getenv("CALYPSO_DSP_RUN_C54X");
        run_c54x = (e && *e == '1') ? 1 : 0;
    }
    if (!s || !run_c54x) {
        return;
    }

    uint16_t pc0 = s->pc;
    s->running = true;
    c54x_run(s, 2000);   /* reset (0xff80) -> 0xb419 (sets IDLE) -> park at 0xb41c */
    if (s->pc >= 0xb41c && s->pc <= 0xb428) {
        g_c54x_early_booted = true;   /* suppresses the re-reset in calypso_trx.c */
        fprintf(stderr, "[c54x-earlyboot] PARK pc=0x%04x (de 0x%04x) insn=%u "
                "data[0x0fff]=0x%04x data[0x0ffe]=0x%04x (attendu IDLE 0x0001)\n",
                s->pc, pc0, s->insn_count, s->data[0x0fff], s->data[0x0ffe]);
    } else {
        fprintf(stderr, "[c54x-earlyboot] WARN pas parque pc=0x%04x insn=%u "
                "-> execution continue\n", s->pc, s->insn_count);
    }
}

bool c54x_early_booted(void)
{
    return g_c54x_early_booted;
}

/* Current DSP mission, read from the API RAM:
 *   d_task_md, page 0 = data[0x0804], page 1 = data[0x0818]. */
uint16_t c54x_task_md(C54xState *s)
{
    if (!s) {
        return 0;
    }
    uint16_t md = s->data[0x0804];
    return md ? md : s->data[0x0818];
}

int c54x_load_blob_daram(C54xState *s, const char *path, uint16_t daram_addr)
{
    FILE *f = fopen(path, "rb");
    if (!f) {
        C54_LOG("load_blob_daram: cannot open '%s'", path);
        return -1;
    }
    int words = 0;
    uint32_t addr = daram_addr;
    uint8_t buf[2];
    while (addr < C54X_DATA_SIZE && fread(buf, 1, 2, f) == 2) {
        uint16_t w = buf[0] | ((uint16_t)buf[1] << 8);
        s->data[addr] = w;
        if (s->api_ram &&
            addr >= C54X_API_BASE && addr < C54X_API_BASE + C54X_API_SIZE)
            s->api_ram[addr - C54X_API_BASE] = w;
        addr++;
        words++;
    }
    fclose(f);
    C54_LOG("load_blob_daram: %d words at DARAM[0x%04x..0x%04x] from %s",
            words, daram_addr, addr - 1, path);
    return words;
}

int c54x_load_section(C54xState *s, const char *path,
                      uint32_t start_addr, bool is_program)
{
    FILE *f = fopen(path, "rb");
    if (!f) {
        C54_LOG("load_section: cannot open '%s'", path);
        return -1;
    }
    uint16_t *mem = is_program ? s->prog : s->data;
    uint32_t limit = is_program ? C54X_PROG_SIZE : C54X_DATA_SIZE;
    int words = 0;
    uint32_t addr = start_addr;
    uint8_t buf[2];
    while (addr < limit && fread(buf, 1, 2, f) == 2) {
        uint16_t w = buf[0] | ((uint16_t)buf[1] << 8);
        mem[addr] = w;
        if (!is_program && s->api_ram &&
            addr >= C54X_API_BASE && addr < C54X_API_BASE + C54X_API_SIZE)
            s->api_ram[addr - C54X_API_BASE] = w;
        addr++;
        words++;
    }
    fclose(f);
    C54_LOG("load_section: %d words at %s[0x%05x..0x%05x] from %s",
            words, is_program ? "prog" : "data",
            start_addr, addr - 1, path);
    return words;
}

int c54x_load_registers(C54xState *s, const char *path)
{
    FILE *f = fopen(path, "rb");
    if (!f) {
        C54_LOG("load_registers: cannot open '%s'", path);
        return -1;
    }
    int words = 0;
    uint8_t buf[2];
    /* First 0x20 words = MMR page → reset-override buffer (applied in
     * c54x_reset). Remaining words (0x20..0x5F = low scratch DARAM) →
     * data[] directly, like a section load. */
    while (words < C54X_DATA_SIZE && fread(buf, 1, 2, f) == 2) {
        uint16_t w = buf[0] | ((uint16_t)buf[1] << 8);
        if (words < 0x20)
            s->reg_init[words] = w;
        else
            s->data[words] = w;
        words++;
    }
    fclose(f);
    if (words >= 0x20)
        s->reg_init_valid = true;
    C54_LOG("load_registers: %d words from %s (MMR reset override %s)",
            words, path, s->reg_init_valid ? "ON" : "OFF (file too short)");
    return words;
}

void c54x_reset(C54xState *s)
{
    g_boot_trace = 50;
    s->blob_loaded = false;  /* explicit reset exits dsp-blob fixture mode */
    s->a = 0; s->b = 0;   /* mode c54x is a clean datasheet reset: A = B = 0.
                           * The snapshot has BL=0x60, applied in bin mode only. */
    /* Register reset state. The hard-coded values below are mode "c54x": a
     * clean datasheet reset, with the critical fields aligned on the silicon
     * snapshot and the benign or garbage ones at 0. The three modes selected
     * further down are orthogonal:
     *   c54x   = these hard-coded values only, independent of any file
     *   bin    = calypso_dsp.Registers.bin overrides everything, verbatim
     *            (the default)
     *   hybrid = bin for the operational registers, clean values forced for
     *            the critical ones (IFR=0, BRC/RSA/REA=0)
     *
     * The .bin is a mid-execution snapshot, taken after the bootloader
     * handshake. Field by field, judged on whether the value makes sense AT
     * RESET:
     *
     *   MMR        value                 class       note
     *   IMR  0x00  0x52FD                critical    IRQ mask, identical in 3 dumps
     *   IFR  0x01  0x0008                benign      INT3 pending; masked by INTM=1
     *   ST0  0x06  0x181F                critical    DP=0x1F
     *   ST1  0x07  0x2900                critical    INTM/SXM/XF
     *   A    08-0a 0x000000              ok
     *   B    0b-0d 0x000060              benign      BL=0x60, reloaded before use
     *   T    0x0E  0x0000                ok
     *   TRN  0x0F  0xFF75                benign      Viterbi, neutral at reset
     *   AR0  0x10  0x5AAD                load-bearing, see below
     *   AR1-5 11-15 invariant            critical    identical in 3 dumps (API RAM...)
     *   AR6  0x16  0xBAE6                benign      reloaded before use
     *   AR7  0x17  0x1E44                benign      likewise
     *   SP   0x18  0x1100                critical    post-handshake stack
     *   BK   0x19  0xFFF6                critical    circular buffer
     *   BRC  0x1A  0x8FD7                garbage     leftover of an RPTB in flight
     *   RSA  0x1B  0xD9EC                garbage     rptb_active is false at reset
     *   REA  0x1C  0xBBEF                garbage
     *   PMST 0x1D  0xFFA8                critical    IPTR=0x1FF, MP_MC, OVLY, DROM
     *   XPC  0x1E  0x0000                never overridden (runtime page, vector fetch)
     *
     * The critical fields (SP/ST0/ST1/PMST/IMR/AR1-5/BK) are identical between
     * the snapshot and the hard-coded values and are what drives the reset.
     *
     * AR values follow the silicon spec (doc/datasheets/README.md section 3),
     * cross-checked against 3 ROM dumps (3311/3416/3606) plus the local
     * osmocom dump:
     *   AR1=0x005F, AR2=0x0813, AR3=0x0014, AR4=0x0003, AR5=0x0014 (invariant)
     *   BK=0xFFF6
     *   AR6, AR7: no documented invariant, left at 0
     * A blanket memset to 0 is wrong: with AR2=0, the STL A,*AR2 at PC=0x9ac0
     * writes mem[0x00] = IMR, clearing it, which masks every FRAME/BRINT0
     * interrupt and leaves the DSP stuck in df9x. */
    memset(s->ar, 0, sizeof(s->ar));
    s->ar[0] = 0x5AAD;  /* silicon value. AR0 is read before being written at
                         * insn=1 (PC=0xb410, ORM data[*AR0]), shown by the
                         * AR-FIRSTUSE probe, so the reset value is
                         * load-bearing: the "neutral" 0xFF75 from the local
                         * dump made c54x and bin modes diverge on the very
                         * first boot instruction. */
    s->ar[1] = 0x005F;
    s->ar[2] = 0x0813;  /* API RAM pointer; 0 here clobbers the IMR */
    s->ar[⟨1⟩] = 0x⟨2⟩;  ×3
    ⟨⟩ = (3,0014) (4,0003) (5,0014)
    s->t = 0; s->trn = 0;   /* TRN=0; the snapshot has 0xff75, neutral at reset */
    s->sp = 0x1100; s->bk = 0xFFF6;  /* SP and BK aligned with silicon: 3 ROM dumps
                                 * (3311/3416/3606) plus the local one all show
                                 * SP=0x1100 after the bootloader handshake. The
                                 * firmware repoints it to its own stack (0x5AC8
                                 * observed) through its init sequence, as on real
                                 * silicon. Starting at 0x5AC8 short-circuits that
                                 * re-init and is the suspected root of the AR5/SP
                                 * overlap clobber at mem[0x3fbe].
                                 * See doc/datasheets/README.md sections 3-4. */
    /* BRC/RSA/REA = 0, the clean datasheet reset. The snapshot captures
     * leftovers of an RPTB in flight (BRC=0x8fd7 RSA=0xd9ec REA=0xbbef), which
     * are meaningless at reset and applied in bin mode only. rptb_active is
     * false (set below), so these registers are not consulted until an RPTB
     * reloads them. */
    s->brc = 0; s->rsa = 0; s->rea = 0;
    /* MMR reset values aligned with Calypso silicon (3 FreeCalypso ROM dumps
     * plus the local one). See doc/datasheets/README.md section 3. */
    s->st0  = 0x181F;                              /* DP=0x01F per silicon */
    s->st1  = ST1_INTM | ST1_SXM | ST1_XF;         /* 0x2900: INTM=1, SXM=1, XF=1 */
    s->pmst = 0xFFA8;                              /* IPTR=0x1FF, MP_MC=1, OVLY=1, DROM=1 */
    s->imr = 0x52FD;                               /* aligned with the local osmocom dump,
                                                    * post bootloader handshake. IMR=0 masks
                                                    * every interrupt: IRQ #2..#10 were seen
                                                    * with INTM=1 IMR=0x0000 IFR=0x28, so no
                                                    * handler ran, no dispatcher flag was
                                                    * written, and the DSP looped in df9x. */
    s->ifr = 0;        /* clean datasheet reset; the snapshot has 0x0008 (INT3
                        * pending), applied in bin mode only, and neutral
                        * anyway since INTM=1 masks it. */
    s->xpc = 0;
    /* Register reset mode selector, env CALYPSO_DSP_REG_MODE:
     *   "c54x"   -> the hard-coded values above ONLY; a loaded .bin is ignored
     *   "bin"    -> calypso_dsp.Registers.bin overrides everything, verbatim
     *   "hybrid" -> the .bin for the validated operational registers, but the
     *               clean values for the fields the .bin gets wrong at reset:
     *                 IFR         : leftover pending interrupt -> 0
     *                 BRC/RSA/REA : garbage from an RPTB in flight -> 0
     * The default is "bin" when a .bin is loaded, otherwise necessarily the
     * hard-coded values. Read once; reset is called twice (boot and
     * DSP_DL_STATUS_READY). */
    {
        static int reg_mode = -1;  /* 0=c54x 1=bin 2=hybrid */
        if (reg_mode < 0) {
            const char *e = calypso_getenv("CALYPSO_DSP_REG_MODE");
            if      (e && !strcasecmp(e, "c54x"))   reg_mode = 0;
            else if (e && !strcasecmp(e, "hybrid")) reg_mode = 2;
            else                                    reg_mode = 1; /* "bin"/default */
            C54_LOG("reset: CALYPSO_DSP_REG_MODE=%s → mode=%s",
                    e ? e : "(unset)",
                    reg_mode == 0 ? "c54x(hardcode)" :
                    reg_mode == 2 ? "hybrid" : "bin");
        }
        if (s->reg_init_valid && reg_mode != 0) {
            const uint16_t *r = s->reg_init;
            /* Operational registers, common to bin and hybrid */
            s->imr  = r[0x00];
            s->st0  = r[0x06];
            s->st1  = r[0x07];
            s->a    = ((int64_t)(r[0x0a] & 0xFF) << 32) |
                      ((uint32_t)r[0x09] << 16) | r[0x08];
            s->b    = ((int64_t)(r[0x0d] & 0xFF) << 32) |
                      ((uint32_t)r[0x0c] << 16) | r[0x0b];
            s->t    = r[0x0e];
            s->trn  = r[0x0f];
            for (int i = 1; i < 8; i++)   /* AR1..AR7; AR0 is handled below */
                s->ar[i] = r[0x10 + i];
            s->sp   = r[0x18];
            s->bk   = r[0x19];
            s->pmst = r[0x1d];
            if (reg_mode == 1) {
                /* Pure bin: the .bin verbatim, nothing forced. The snapshot
                 * IS the silicon state, so it is taken whole - forcing
                 * IFR/BRC/RSA/REA to 0 here would throw away real silicon
                 * state, INT3 pending in particular. rptb_active is false (set
                 * below), so BRC/RSA/REA are not consulted until an RPTB
                 * reloads them. */
                s->ifr   = r[0x01];
                s->ar[0] = r[0x10];
                s->brc   = r[0x1a];
                s->rsa   = r[0x1b];
                s->rea   = r[0x1c];
            } else { /* hybrid: operational registers from the .bin, critical
                      * fields forced to the clean datasheet values. */
                s->ifr   = 0x0000;   /* no interrupt pending at reset */
                s->ar[0] = r[0x10];  /* AR0 from the silicon snapshot, not a
                                      * hard-coded value: it is read before
                                      * being written at insn=1. */
                s->brc   = 0x0000;
                s->rsa   = 0x0000;
                s->rea   = 0x0000;
            }
            /* XPC is never overridden: it is a runtime page register, and the
             * reset vector fetch at IPTR*0x80 must come from page 0. */
            C54_LOG("reset: dsp-registers %s applied (SP=0x%04x PMST=0x%04x "
                    "ST0=0x%04x ST1=0x%04x IMR=0x%04x IFR=0x%04x AR0=0x%04x "
                    "BRC=0x%04x RSA=0x%04x REA=0x%04x)",
                    reg_mode == 2 ? "HYBRID" : "BIN",
                    s->sp, s->pmst, s->st0, s->st1, s->imr, s->ifr, s->ar[0],
                    s->brc, s->rsa, s->rea);
        } else {
            C54_LOG("reset: registres = hardcode C (mode c54x ou pas de .bin) "
                    "SP=0x%04x PMST=0x%04x IMR=0x%04x IFR=0x%04x AR0=0x%04x",
                    s->sp, s->pmst, s->imr, s->ifr, s->ar[0]);
        }
    }
    s->timer_psc = 0;
    s->data[TCR_ADDR] = TCR_TSS;  /* Timer stopped at reset (TSS=1) per HW spec */
    s->data[TIM_ADDR] = 0xFFFF;   /* TIM = max at reset */
    s->data[PRD_ADDR] = 0xFFFF;   /* PRD = max at reset */
    s->rpt_active = false;
    s->rptb_active = false; { static int _re=0; if (_re<50) { C54_LOG("RPTB EXIT PC=0x%04x RSA=0x%04x REA=0x%04x insn=%u SP=0x%04x", s->pc, s->rsa, s->rea, s->insn_count, s->sp); _re++; } }
    s->idle = false;
    s->running = true;
    s->cycles = 0;
    s->insn_count = 0;
    s->unimpl_count = 0;

    /* Boot ROM MVPD: copy PROM0 code to DARAM overlay.
     * On real Calypso, the internal boot ROM copies PROM0[0x7080..0x9FFF]
     * to DARAM data[0x0080..0x27FF] before jumping to user code.
     * This populates the DARAM code overlay that the DSP executes with OVLY=1.
     *
     * On real silicon, DARAM and API RAM share one physical memory in the
     * range 0x0800-0x27FF (DSP-words). Mirror the copy into api_ram so the
     * ARM-side view matches the DSP-side view from boot — without this
     * mirror, every ARM read into the overlay zone returns 0 while the
     * DSP executes the copied code, which silently splits the two views. */
    for (int i = 0; i < 0x2780; i++) {
        uint16_t addr = 0x0080 + i;
        uint16_t val = s->prog[0x7080 + i];
        s->data[addr] = val;
        if (s->api_ram &&
            addr >= C54X_API_BASE && addr < C54X_API_BASE + C54X_API_SIZE)
            s->api_ram[addr - C54X_API_BASE] = val;
    }

    /* prog[0xFF80..0xFF83] is left as the legitimate PROM1 mirror (0x56d0,
     * 0x9631, 0xf820, 0xff89 from the dump). The boot-init redirect is applied
     * at runtime and ONLY while SP still holds the silicon reset value 0x1100
     * - see the c54x_run main loop.
     *
     * A static override of prog[0xFF80] would intercept the silicon reset AND
     * every sequential firmware walk into 0xff80: the ROM has an ordinary
     * subroutine whose RET at 0xff79 pops 0xff7a, and the POPM/STM epilogue
     * walks through 0xff80. Each such walk would become a soft reset, SP would
     * be set to 0x5AC8, the state would derail and the DSP would sit in a
     * boot-reset cycle with FB never stabilising past the first 4 s. */

    /* Boot ROM stubs at 0x0000-0x007F. They prevent a stack runaway when a
     * computed call lands in the stub area, at no cost: reverting them to NOPs
     * gives an identical PC histogram and the same IMR behaviour.
     *
     * Per slot:
     *   - 0x0000/0x0001   : RET (0xFC00), pops ret_pc, BALANCED return
     *   - 0x02..0x7F      : FRET (0xF4E4), return-from-far
     *
     * RET at 0x0000 rather than IDLE or LDMM, because the firmware does near
     * CALAs to 0x0000 with A=0 (the null/default handler path; for instance
     * the LDU at 0xfa7e reads a table pointer of 0, then CALA A=0). A near
     * CALA pushes one word, RET pops one, so the return is balanced and the
     * boot continues.
     *   - IDLE does not return: it halts, slides 0x0000 -> 0x0002 into the
     *     FRET loop, and leaks SP (0x1106 -> 0x4d75).
     *   - LDMM SP,B + RET returns but sets SP = B, so B=0 gives SP=0 and pops
     *     garbage.
     * The rest stays FRET, so far calls (FCALA pushes 2) that land in the zone
     * are balanced by a 2-word pop. The firmware idles at its real point, the
     * IDLE of the TDMA slot table (doc/DSP_ROM_MAP.md). */
    for (int i = 0; i < 0x80; i++)
        s->prog[i] = 0xF4E4;  /* FRET: return-from-far (far call into a stub) */
    s->prog[0x0000] = 0xFC00;  /* RET: pops ret_pc, balances the near CALA to 0 */
    s->prog[0x0001] = 0xFC00;  /* RET: likewise */

    /* Reset vector: IPTR * 0x80 */
    uint16_t iptr = (s->pmst >> PMST_IPTR_SHIFT) & 0x1FF;
    s->pc = iptr * 0x80;  /* 0xFF80 for default PMST */

    C54_LOG("Reset: PC=0x%04x PMST=0x%04x SP=0x%04x prog[PC]=0x%04x",
            s->pc, s->pmst, s->sp, s->prog[s->pc]);

    /* Build identity dump: the silicon-aligned reset values this binary
     * actually uses. If they change, firmware behaviour changes, so a report
     * can be attributed to a code state by reading qemu.log. */
    C54_LOG("BUILD-IDENT silicon-reset: SP=0x%04x BK=0x%04x IMR=0x%04x "
            "ST0=0x%04x ST1=0x%04x PMST=0x%04x",
            s->sp, s->bk, s->imr, s->st0, s->st1, s->pmst);
    C54_LOG("BUILD-IDENT silicon-AR: AR0=0x%04x AR1=0x%04x AR2=0x%04x AR3=0x%04x "
            "AR4=0x%04x AR5=0x%04x AR6=0x%04x AR7=0x%04x",
            s->ar[0], s->ar[1], s->ar[2], s->ar[3],
            s->ar[4], s->ar[5], s->ar[6], s->ar[7]);
    /* Decoder fix flags: removing these fixes from the source changes or
     * removes this line, which identifies the binary that produced a run. */
    C54_LOG("BUILD-IDENT decoder-fixes: F1xx-FIRS-catch=REMOVED "
            "L3609-src-dst=FIXED F-AUDIT-v5=max-min-cmpl-rnd-roltc-fixed "
            "F2xx-ALU-block=ADDED-2026-05-25-night "
            "F3xx-INTR-mis-REMOVED-ADD-SUB-LD-ADDED "
            "PROBE-HIGHVEC-REGIME=2026-06-23 "
            "SQURA-0x38-FIX=2026-06-23 "
            "2026-05-25");

}

int g_c54x_int3_src = 0;   /* 1=trx 2=bsp 3=shunt: INT3 source, diagnostics only */


void c54x_interrupt_ex(C54xState *s, int vec, int imr_bit)
{
    if (vec < 0 || vec >= 32) return;
    if (imr_bit < 0 || imr_bit >= 16) return;
    s->ifr |= (1 << imr_bit);
    if (imr_bit == 12 && frame_it_level_on()) g_frame_it_level = true;  /* arm the frame LEVEL hold */

    /* Frame interrupt rate probe: each dispatch of the frame interrupt with
     * the insn delta since the previous one. A delta near 130 means
     * over-delivery (one per BSP burst), which drowns the DSP; near 256000 is
     * one per frame, which is correct. The frame interrupt is vec 28, not
     * vec 19 (19 is TINT). */
    if (vec == C54X_IT_TPU_FRAME_VEC) {
        static uint64_t last_i3 = 0;
        static unsigned i3n = 0;
        bool post_fb = s->insn_count > 160000u;   /* ~fn 1206: the FB order has been delivered */
        if (i3n < 40 || (post_fb && i3n < 100)) {
            i3n++;
            uint16_t real_page = s->api_ram ? s->api_ram[0x08E2 - 0x0800]
                                            : s->data[0x08E2];
            fprintf(stderr, "[c54x] INT3-RATE #%u src=%d insn=%u delta=%lld idle=%d "
                    "REAL_page(08D4)=0x%04x daram584=0x%04x task_md(058a)=0x%04x\n",
                    i3n, g_c54x_int3_src, s->insn_count,
                    (long long)((uint64_t)s->insn_count - last_i3), s->idle,
                    real_page, s->data[0x0584], s->data[0x058a]);
        }
        last_i3 = s->insn_count;
    }

    bool unmasked = (s->imr & (1 << imr_bit)) != 0;
    /* @BEQUILLE - FIX_BRINT0_UNMASK  (CALYPSO_FIXES=FIX_BRINT0_UNMASK, default OFF)
     *   masks   : the absence of any native arming of IMR bit 5 (BRINT0 / vec 21).
     *   remove  : as soon as the real arming branch is implemented, OR at once if
     *             the test shows the root cause is upstream (vector 21 installed as
     *             zero).
     *   WARNING : DIAGNOSTIC, to be removed, never to be confirmed. It does not
     *             count as a fix.
     * It answers one question: is BRINT0 the LAST lock or only the NEXT one? If
     * unmasking it artificially gets the DSP into the demodulator
     * (CALYPSO_WATCH_9F00_RD goes from 0 to non-zero), it was the last; otherwise
     * the root cause is upstream, the candidate being vector 21 installed as
     * zero. */
    if (imr_bit == 5 && !unmasked && calypso_fix_enabled("FIX_BRINT0_UNMASK")) {
        static unsigned _bu = 0;
        if (_bu++ < 5)
            fprintf(stderr, "[c54x] FIX_BRINT0_UNMASK : bit 5 demasque ARTIFICIELLEMENT "
                    "(IMR=0x%04x, IFR=0x%04x, PC=0x%04x) — diagnostic, pas un correctif\n",
                    s->imr, s->ifr, s->pc);
        unmasked = true;
    }

    /* SYNC-DISPATCH-PROBE (unconditional, capped). c54x_interrupt_ex dispatches
     * SYNCHRONOUSLY here, at the moment the interrupt is raised, when INTM=0,
     * and unlike c54x_irq_level_check's "IRQ-LEVEL take" it has no log of its
     * own. This says whether BRINT0 (vec 21) is in fact served silently by this
     * path on every raise, which would explain "never seen pending by the
     * poller" without any bug: the two mechanisms simply never overlap in the
     * window LEVELCHK-EMPIRICAL observes. */
    if (vec == 21) {
        static unsigned _sd = 0;
        if (_sd < 100) {
            _sd++;
            fprintf(stderr, "[c54x] SYNC-DISPATCH-PROBE #%u vec=21(BRINT0) imr_bit=%d "
                    "INTM=%d unmasked=%d idle=%d ifr_before=0x%04x PC=0x%04x insn=%u -> %s\n",
                    _sd, imr_bit, !!(s->st1 & ST1_INTM), unmasked, s->idle, s->ifr, s->pc,
                    s->insn_count,
                    s->idle ? (unmasked ? "DISPATCH(idle-wake)" : "STAYS-PENDING(idle,masked)") :
                    (!(s->st1 & ST1_INTM) && unmasked && s->delay_slots == 0) ? "DISPATCH(normal)" :
                    !unmasked ? "STAYS-PENDING(masked)" : "STAYS-PENDING(INTM=1-or-delay)");
        }
    }

    /* Per SPRU131: IDLE exits on ANY interrupt (masked or unmasked).
     * - Unmasked: branch to vector, set INTM=1
     * - Masked: just resume after IDLE, IFR bit stays set */
    if (s->idle) {
        s->idle = false;
        if (unmasked) {
            /* Service the interrupt: branch to vector */
            c54x_ifr_clear(s, (uint16_t)(1 << imr_bit), "vector-ex");
            s->sp--;
            data_write(s, s->sp, (uint16_t)(s->pc + 1));
            g_sp_ledger.irq_words_pushed++;
            s->sp--;
            data_write(s, s->sp, s->xpc);          /* XPC is always saved */
            g_sp_ledger.irq_words_pushed++;
            g_sp_ledger.irq_entries++;
            g_sp_ledger.net_words += 2;  /* PC and XPC pushed here, outside the exec ring */
            s->st1 |= ST1_INTM;
            /* Capture the preempted context; the foreground DP is unchanged. */
            g_last_intr_insn = s->insn_count; g_last_intr_vec = vec; { extern uint16_t g_piege_pile_sp; extern int g_piege_pile_on; if (!g_piege_pile_on) { g_piege_pile_sp = (uint16_t)(s->sp + 2); g_piege_pile_on = 1; } }
            g_last_intr_fg_pc = (uint16_t)(s->pc + 1); g_last_intr_fg_dp = dp(s);
            s->xpc = 0;                            /* fetch the vector from page 0 */
            uint16_t iptr = (s->pmst >> PMST_IPTR_SHIFT) & 0x1FF;
            s->pc = (iptr * 0x80) + vec * 4;
            g_c54x_it_prises++;   /* [2026-09-23] diagnostic ANNEAU-EXEC */
            if (vec == 28) {
                if (g_vec28_trace_en < 0)
                    g_vec28_trace_en = calypso_gate("CALYPSO_TRACE_VEC28_STACK", 0);
                if (g_vec28_trace_en && !g_vec28_tracing) {
                    g_vec28_tracing = true;
                    g_vec28_sp_entry = s->sp;
                    g_vec28_trace_pops = 0;
                    fprintf(stderr, "[c54x] VEC28-STACK-TRACE ARMED (idle-wake) "
                            "SP_entry=0x%04x data[SP]=0x%04x(expect XPC) "
                            "data[SP+1]=0x%04x(expect return PC) PC=0x%04x insn=%u\n",
                            s->sp, data_read(s, s->sp), data_read(s, (uint16_t)(s->sp + 1)),
                            s->pc, s->insn_count);
                }
            }
        }
        /* If masked: just wake, advance PC past IDLE */
        if (!unmasked) {
            s->pc++;  /* resume at instruction after IDLE */
        }
    } else if (!(s->st1 & ST1_INTM) && unmasked && s->delay_slots == 0) {
        /* Normal (non-IDLE) interrupt servicing.
         * The delay_slots == 0 guard is faithful to the C54x: an interrupt is
         * NOT recognised between a delayed branch (RETD/RCD/CALLD/BD) and its
         * two delay slots. Vectoring mid-delay would leave delay_slots armed,
         * so on return (RETE) the delayed_pc commit would happen in the wrong
         * context, over-popping SP, corrupting DP and reaching the self-CALA at
         * 0x70c3. The IFR bit stays set, so the interrupt is served on the next
         * call, at most ~2 instructions later, once delay_slots is back to 0. */
        /* FRAME-IT-PRIO (CALYPSO_FRAME_IT_PRIO): when the frame interrupt
         * (bit 12 / vec 28) is latched AND unmasked but a lower-priority
         * interrupt is about to be served - typically BRINT0, vec 21, which the
         * BSP over-delivers once per burst and which drowns the frame - serve
         * the FRAME first in this INTM=0 window. The requested interrupt's bit
         * stays pending and is served on the next edge. This drains a starved
         * frame interrupt (measured 5908 times pending against 1 taken) into
         * vec 28 -> scheduler 0x7234 -> dispatcher -> FB kernel. */
        if (vec != 28 && frame_it_prio_on() &&
            (s->ifr & (1u << 12)) && (s->imr & (1u << 12))) {
            static unsigned _fp = 0;
            if (_fp++ < 30)
                fprintf(stderr, "[c54x] FRAME-IT-PRIO override vec=%d->28 (frame latchee) "
                        "IFR=0x%04x IMR=0x%04x PC=0x%04x insn=%u\n",
                        vec, s->ifr, s->imr, s->pc, s->insn_count);
            vec = 28; imr_bit = 12;
            g_frame_it_level = false;   /* release the LEVEL hold, otherwise bit 12 re-asserts and vec 28 over-fires 7x per frame (139k INTM transitions) */
        }
        c54x_ifr_clear(s, (uint16_t)(1 << imr_bit), "vector-ex");
        s->sp--;
        data_write(s, s->sp, (uint16_t)s->pc);
        g_sp_ledger.irq_words_pushed++;
        s->sp--;
        data_write(s, s->sp, s->xpc);              /* XPC is always saved */
        g_sp_ledger.irq_words_pushed++;
        g_sp_ledger.irq_entries++;
        g_sp_ledger.net_words += 2;  /* PC and XPC pushed here, outside the exec ring */
        s->st1 |= ST1_INTM;
        /* Capture the preempted context; the foreground DP is unchanged. */
        g_last_intr_insn = s->insn_count; g_last_intr_vec = vec; { extern uint16_t g_piege_pile_sp; extern int g_piege_pile_on; if (!g_piege_pile_on) { g_piege_pile_sp = (uint16_t)(s->sp + 2); g_piege_pile_on = 1; } }
        g_last_intr_fg_pc = (uint16_t)s->pc; g_last_intr_fg_dp = dp(s);
        s->xpc = 0;                                /* fetch the vector from page 0 */
        uint16_t iptr = (s->pmst >> PMST_IPTR_SHIFT) & 0x1FF;
        s->pc = (iptr * 0x80) + vec * 4;
        g_c54x_it_prises++;   /* [2026-09-23] diagnostic ANNEAU-EXEC */
        if (vec == 28) {
            if (g_vec28_trace_en < 0)
                g_vec28_trace_en = calypso_gate("CALYPSO_TRACE_VEC28_STACK", 0);
            if (g_vec28_trace_en && !g_vec28_tracing) {
                g_vec28_tracing = true;
                g_vec28_sp_entry = s->sp;
                g_vec28_trace_pops = 0;
                fprintf(stderr, "[c54x] VEC28-STACK-TRACE ARMED (normal) "
                        "SP_entry=0x%04x data[SP]=0x%04x(expect XPC) "
                        "data[SP+1]=0x%04x(expect return PC) PC=0x%04x insn=%u\n",
                        s->sp, data_read(s, s->sp), data_read(s, (uint16_t)(s->sp + 1)),
                        s->pc, s->insn_count);
            }
        }
        /* Start the cycle trace on the frame interrupt (vec 28). */
        if (vec == C54X_IT_TPU_FRAME_VEC) {
            int3_cycle_start(s, s->pc);
        }
    }

    /* Log interrupts: first 20 + every 100th, so we can count them.
     * PMST/IPTR included so we can correlate which vector base the IRQ
     * lands at — INT3 at IPTR=0x1ff (vec=0xffcc) hits a garbage ROM stub,
     * INT3 at IPTR=0x140 (vec=0xa04c) hits the firmware's real handler. */
    static uint64_t int_log_count;
    int_log_count++;
    if (int_log_count <= 20 || (int_log_count % 100) == 0) {
        uint16_t iptr_now = (s->pmst >> PMST_IPTR_SHIFT) & 0x1FF;
        C54_LOG("IRQ #%llu vec=%d bit=%d: INTM=%d IMR=0x%04x IFR=0x%04x "
                "idle=%d PC=0x%04x PMST=0x%04x IPTR=0x%03x",
                (unsigned long long)int_log_count,
                vec, imr_bit, !!(s->st1 & ST1_INTM), s->imr, s->ifr,
                s->idle, s->pc, s->pmst, iptr_now);
    }
}

void c54x_wake(C54xState *s)
{
    s->idle = false;
}

/* Declared at FILE scope, to avoid -Wnested-externs. */
extern void calypso_twl3025_apply_phase(int16_t *iq_samples, int n_samples,
                                        uint32_t fn, uint8_t tn);
/* c54x_task_md(): declared in calypso_c54x.h, defined earlier in this file. */

void c54x_bsp_load(C54xState *s, const uint16_t *samples, int n)
{
    if (n > 2048) n = 2048;

    /* FEED-FP, leg 2 of 2 - OUTPUT. Leg 1 of 2 is in calypso_bsp.c
     * (bsp_trxd_readable): same gate CALYPSO_BSP_FINGERPRINT, same FNV-1a
     * hash, same cap. Here we fingerprint what leaves towards the RIF.
     *
     * Reading it: `identiques` close to the total = FROZEN burst. If the IN
     * leg varies and this one does not, the freeze is between the two. If
     * BOTH are frozen, look upstream of the UDP link (calypso-ipc-device).
     *
     * Caveat: this probe counts CONSECUTIVE repetitions, not distinct ones -
     * an A,B,A,B alternation would report identiques=0 while still being
     * pathological. Acceptable because the observed symptom is a CONSTANT,
     * but no conclusion beyond that case. */
    {
        static int fp_on = -1;
        if (fp_on < 0) fp_on = calypso_gate("CALYPSO_BSP_FINGERPRINT", 0);
        if (fp_on && n > 0) {
            uint32_t h = 2166136261u;
            unsigned nz = 0;
            for (int i = 0; i < n; i++) {
                h = (h ^ (samples[i] & 0xFF)) * 16777619u;
                h = (h ^ (samples[i] >> 8))   * 16777619u;
                if (samples[i]) nz++;
            }
            static uint32_t prev_h;
            static unsigned long long n_tot, n_same;
            n_tot++;
            if (n_tot > 1 && h == prev_h) n_same++;
            prev_h = h;
            if (n_tot <= 20 || (n_tot % 500) == 0)
                fprintf(stderr, "[c54x] FEED-FP OUT #%llu fp=%08x nz=%u/%d "
                        "identiques=%llu/%llu\n",
                        n_tot, h, nz, n, n_same, n_tot);
        }
    }

    memcpy(s->bsp_buf, samples, n * sizeof(uint16_t));
    s->bsp_len = n;
    s->bsp_pos = 0;

    /* RATE NORMALISATION - a SINGLE decimation point. The native feeds arrive
     * at INCONSISTENT rates: g_shunt.last_iq (shunt route, the SAME feed as
     * shunt_legit) is RAW @4SPS (1083333 Hz = 4x270833, undecimated, cf.
     * osmo-trx rx-sps=4), whereas the BSP deliver_buffered path already
     * decimates /4 to 1SPS. Depending on the active path c54x_bsp_load was
     * therefore handed 4SPS OR 1SPS, so the correlator and apply_phase (both
     * of which expect 1SPS, FCCH = +pi/2 per sample) mangled the 4SPS bursts
     * -> inconsistent dphi (tone folded onto DC). Decimation by DECIM happens
     * HERE, but ONLY if the input is oversampled (n indicates 4SPS: 148 sym
     * x4x2 = 1184 words, versus 296 @1SPS) -> never a double decimation,
     * output ALWAYS 1SPS, every path uniform (like shunt_legit).
     * DECIM = CALYPSO_BSP_IQ_DECIM (default 4). */
    {
        static int decim = -1;
        if (decim < 0) {
            const char *d = calypso_getenv("CALYPSO_BSP_IQ_DECIM");
            decim = (d && *d) ? atoi(d) : 4;
            if (decim < 1) decim = 1;
        }
        if (decim > 1 && n > 2 * 296) {          /* 4SPS detected (n ~1184) */
            int oc = 0;
            for (int k = 0; (k * decim) * 2 + 1 < n; k++) {
                s->bsp_buf[2 * oc]     = s->bsp_buf[2 * (k * decim)];
                s->bsp_buf[2 * oc + 1] = s->bsp_buf[2 * (k * decim) + 1];
                oc++;
            }
            n = oc * 2;                          /* n becomes the 1SPS count */
            s->bsp_len = n;
        }
    }

    /* AFC AT THE CONVERGENCE POINT. The VCXO rotation (apply_phase) used to be
     * applied only in calypso_bsp_deliver_buffered (calypso_bsp.c:1888), which
     * is NOT the live native path (measured: b-bsp-load-ok=0, the feed goes
     * elsewhere). The native DSP therefore received UNROTATED samples -> OPEN
     * AFC loop -> the firmware integrates the error with no effect on the
     * samples -> DAC runaway (-700 -> 4095, +34 kHz) -> FCCH rotation outside
     * the DSP capture range (+-20 kHz) -> nonsensical TOA. Here ALL feeds
     * converge (c54x_bsp_load -> RIF), so the rotation is applied ONCE, on
     * bsp_buf (mutable), and bsp_buf is what is fed to the RIF. See
     * CHAINE_RF_MATERIELLE.md:106: "apply_phase just before c54x_bsp_load".
     * tn=0 (FB/SB = TS0; a constant phase offset does not change the corrected
     * FREQUENCY). Inert when CALYPSO_TWL3025_AFC=0 (apply_phase returns
     * early). */
    calypso_twl3025_apply_phase((int16_t *)s->bsp_buf, n / 2,
                                calypso_trx_get_fn(), 0);
    /* NOISE INJECTOR (AWGN) on ALL downlink bursts, at the point where the feeds converge
     * (BSP synthesis, passthrough, replay). Synthetic bursts are perfect: the ROM scales its soft
     * values by a noise estimate (SB: PROM0 0x7e40 `squrs @0x22,a` = E - c^2; NB: 0x8168 ->
     * 0x82d0) and a zero noise breaks it.
     *   CALYPSO_BSP_BRUIT=<sigma>   standard deviation per I and Q component, in int16 sample units
     *   CALYPSO_BSP_SNR_DB=<dB>     or sigma computed per burst: P_signal / (2 sigma^2) = 10^(dB/10)
     * Deterministic draw (seed = fn, rank of the burst in the frame, CALYPSO_BSP_BRUIT_GRAINE): the
     * replay stays reproducible. Default: no noise. */
    {
        static double sigma_fixe = -2, snr_db = -999;
        static uint32_t graine0;
        if (sigma_fixe == -2) {
            const char *e = getenv("CALYPSO_BSP_BRUIT"), *r = getenv("CALYPSO_BSP_SNR_DB"), *g = getenv("CALYPSO_BSP_BRUIT_GRAINE");
            sigma_fixe = (e && *e) ? atof(e) : 0.0;
            if (r && *r) snr_db = atof(r);
            graine0 = (g && *g) ? (uint32_t)strtoul(g, NULL, 0) : 0x9e3779b9u;
            if (sigma_fixe > 0 || snr_db > -999)
                fprintf(stderr, "[c54x] INJECTEUR DE BRUIT : %s %g\n", sigma_fixe > 0 ? "sigma" : "SNR dB",
                        sigma_fixe > 0 ? sigma_fixe : snr_db);
        }
        if ((sigma_fixe > 0 || snr_db > -999) && n >= 2) {
            static uint32_t fn_prec = 0xffffffffu, rang;
            uint32_t fn = calypso_trx_get_fn();
            rang = (fn == fn_prec) ? rang + 1 : 0;
            fn_prec = fn;
            double sigma = sigma_fixe;
            if (sigma <= 0) {
                double p = 0;
                for (int k = 0; k < n; k++) { double v = (int16_t)s->bsp_buf[k]; p += v * v; }
                p /= (n / 2);                                  /* puissance complexe moyenne */
                sigma = sqrt(p / (2.0 * pow(10.0, snr_db / 10.0)));
            }
            uint32_t x = graine0 ^ (fn * 2654435761u) ^ (rang * 40503u + 1u);
            if (!x) x = 1;
            for (int k = 0; k + 1 < n; k += 2) {
                double u1, u2;
                x ^= x << 13; x ^= x >> 17; x ^= x << 5; u1 = ((x >> 8) + 1.0) / 16777217.0;
                x ^= x << 13; x ^= x >> 17; x ^= x << 5; u2 = (x >> 8) / 16777216.0;
                double r = sigma * sqrt(-2.0 * log(u1));
                double vi = (int16_t)s->bsp_buf[k]     + r * cos(2 * M_PI * u2);
                double vq = (int16_t)s->bsp_buf[k + 1] + r * sin(2 * M_PI * u2);
                if (vi > 32767) vi = 32767;
                if (vi < -32768) vi = -32768;
                if (vq > 32767) vq = 32767;
                if (vq < -32768) vq = -32768;
                s->bsp_buf[k] = (uint16_t)(int16_t)lrint(vi);
                s->bsp_buf[k + 1] = (uint16_t)(int16_t)lrint(vq);
            }
        }
    }

    /* The same burst feeds the RIF receive FIFO, which is the route the DSP
     * firmware actually reads it through (PORTR DRR after seeing SPCR).
     * bsp_buf stays in place for the probes and for the older PORTR PA=0xF430
     * path (CALYPSO_FIX_PORTR). What is fed is bsp_buf (= the samples rotated
     * by the AFC above), not `samples` (const, unrotated). */
    /* FN GATING - push to the RIF/correlator ONLY the bursts of an FCCH frame
     * during FB acquisition. Root cause: this feed had NO FN gate at all
     * (DIRECT_FEED bypasses the match), so the correlator buffer at 0x0cce
     * received EVERY burst (mostly non-FCCH), mixed and overwritten ->
     * VARIABLE magnitude (~100..28000, versus a constant one for FCCH) ->
     * flat correlation surface -> argmax at the window edge r39 -> TOA=39
     * (never 23). Feeding only the FCCH frames {1,11,21,31,41} (emulated
     * offset, cf. calypso_dsp_shunt.c:3710) leaves the correlator seeing only
     * the real tone. Gate CALYPSO_RIF_FCCH_ONLY (default OFF, reversible).
     * The gating applies ONLY in FB mode (d_task_md 5/8) so the other tasks
     * (SB/NB) are not starved of their burst. */
    {
        static int rif_fcch = -1;
        if (rif_fcch < 0) rif_fcch = calypso_gate("CALYPSO_RIF_FCCH_ONLY", 0);
        int _push = 1;
        if (rif_fcch) {
            /* Filter in FB (task_md 5/8) AND in IDLE (0): the RIF stage keeps
             * only the LAST burst pushed, so letting the non-FCCH idle bursts
             * through (87% of the time) makes the FB DMA drain a polluted
             * burst. NOT filtered in SB (6) / NB: those tasks need THEIR own
             * frame. */
            uint16_t _md = c54x_task_md(s);   /* FB=5, TCH_FB=8, SB=6 */
            if (_md == 5 || _md == 8 || _md == 0) {
                int _p = (int)(calypso_trx_get_fn() % 51u);
                /* Natively the mission does not switch to 6 on the SCH frame
                 * (the get_task_md fallback stays 5/0), so an FCCH-only filter
                 * also smothered the SCH -> 0x0e4e (the SB DMA destination)
                 * drained on a stale FCCH/idle burst -> garbage sb_toa. GSM
                 * 05.02: on the BCCH during acquisition, only FCCH (tone) and
                 * SCH (sync) carry meaning on TS0. So push FCCH AND SCH
                 * (= FCCH+1). The DMA routes by AAD (FCCH -> 0x0cce, SCH ->
                 * 0x0e4e) on DIFFERENT frames, so 0x0cce stays clean (FCCH
                 * only) and 0x0e4e finally receives a real SCH. */
                int _is_fcch = ((_p % 10) == 1) && (_p <= 41); /* FCCH in {1,11,21,31,41} */
                int _is_sch  = ((_p % 10) == 2) && (_p <= 42); /* SCH  in {2,12,22,32,42} */
                _push = _is_fcch || _is_sch;
            }
        }
        if (_push)
            calypso_rif_rx_burst(s, s->bsp_buf, n);
    }

    /* Confirm what the PORTR PA=0x0034 serving path will hand the DSP,
     * and also flag if the DSP consumed less than half of the previous
     * batch before a new one arrived (would indicate correlator starvation
     * or DSP never reading via PORTR at all). */
    static uint64_t load_count;
    load_count++;
    if (load_count <= 10 || (load_count % 1000) == 0) {
        C54_LOG("BSP LOAD #%llu n=%d: %04x %04x %04x %04x %04x %04x %04x %04x",
                (unsigned long long)load_count, n,
                n > ⟨1⟩ ? samples[⟨2⟩] : 0, n > ⟨3⟩ ? samples[⟨4⟩] : 0,  ×3
    ⟨⟩ = (0,0,1,1) (2,2,3,3) (4,4,5,5)
                n > 6 ? samples[6] : 0, n > 7 ? samples[7] : 0);
    }
}

4.54 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_c54x.h

16696 octets, 389 lignes → 383 lignes (1 groupes compactés)

/*
 * calypso_c54x.h — TMS320C54x DSP emulator for Calypso
 *
 * Emulates the C54x DSP core found in the TI Calypso baseband chip.
 * Loads ROM dump, executes instructions, shares API RAM with ARM.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#ifndef CALYPSO_C54X_H
#define CALYPSO_C54X_H

#include <stdint.h>
#include <stdbool.h>

/* Memory sizes (in 16-bit words) */
#define C54X_PROG_SIZE   0x40000  /* 256K words program space */
#define C54X_DATA_SIZE   0x10000  /* 64K words data space */
#define C54X_IO_SIZE     0x10000  /* 64K words I/O space */

/* API RAM: shared between ARM and DSP */
#define C54X_API_BASE    0x0800   /* DSP data address of API RAM */
#define C54X_API_SIZE    0x2000   /* 8K words */

/* DSP start address (after boot) */
#define C54X_DSP_START   0x7000

/* MMR addresses (data memory 0x00-0x1F) */
#define MMR_IMR   0x00
#define MMR_IFR   0x01
#define MMR_ST0   0x06
#define MMR_ST1   0x07
#define MMR_AL    0x08
#define MMR_AH    0x09
#define MMR_AG    0x0A
#define MMR_BL    0x0B
#define MMR_BH    0x0C
#define MMR_BG    0x0D
#define MMR_T     0x0E
#define MMR_TRN   0x0F
#define MMR_AR⟨1⟩   0x⟨2⟩  ×8
    ⟨⟩ = (0,10) (1,11) (2,12) (3,13) (4,14) (5,15) (6,16) (7,17)
#define MMR_SP    0x18
#define MMR_BK    0x19
#define MMR_BRC   0x1A
#define MMR_RSA   0x1B
#define MMR_REA   0x1C
#define MMR_PMST  0x1D
#define MMR_XPC   0x1E

/* Timer registers (memory-mapped at 0x0024-0x0026) */
#define TIM_ADDR  0x0024   /* Timer counter */
#define PRD_ADDR  0x0025   /* Timer period */
#define TCR_ADDR  0x0026   /* Timer control */

/* TCR bit positions (TMS320C54x hardware spec) */
#define TCR_TDDR_MASK  0x000F   /* bits 3:0 — prescaler reload value */
#define TCR_TSS        (1 << 4) /* bit 4 — Timer Stop Status (1=stopped) */
#define TCR_TRB        (1 << 5) /* bit 5 — Timer Reload (write 1 reloads) */
#define TCR_PSC_SHIFT  6        /* bits 9:6 — prescale counter */
#define TCR_PSC_MASK   (0xF << TCR_PSC_SHIFT)
#define TCR_SOFT       (1 << 10)
#define TCR_FREE       (1 << 11)

/* ST0 bit positions */
#define ST0_DP_MASK  0x01FF  /* bits 8-0: data page pointer */
#define ST0_OVB      (1 << 9)
#define ST0_OVA      (1 << 10)
#define ST0_C        (1 << 11)
#define ST0_TC       (1 << 12)
#define ST0_ARP_SHIFT 13
#define ST0_ARP_MASK (7 << ST0_ARP_SHIFT)

/* ST1 bit positions */
#define ST1_ASM_MASK 0x001F  /* bits 4-0: accumulator shift mode */
#define ST1_CMPT     (1 << 5)
#define ST1_FRCT     (1 << 6)
#define ST1_C16      (1 << 7)
#define ST1_SXM      (1 << 8)
#define ST1_OVM      (1 << 9)
#define ST1_INTM     (1 << 11)
#define ST1_HM       (1 << 12)
#define ST1_XF       (1 << 13)
#define ST1_CPL      (1 << 14)  /* compiler mode: direct addressing is SP-relative */
#define ST1_BRAF     (1 << 15)  /* block-repeat active. [2026-09-20] Was (1 << 14):
                                 * every RPTB/RPTBD raised CPL instead of BRAF. */

/* PMST bit positions (per SPRU131: SST=0 SMUL=1 CLKOFF=2 DROM=3 APTS=4 OVLY=5 MP/MC=6) */
#define PMST_SST     (1 << 0)
#define PMST_SMUL    (1 << 1)
#define PMST_CLKOFF  (1 << 2)
#define PMST_DROM    (1 << 3)
#define PMST_APTS    (1 << 4)
#define PMST_OVLY    (1 << 5)
#define PMST_MP_MC   (1 << 6)
#define PMST_IPTR_SHIFT 7
#define PMST_IPTR_MASK (0x1FF << PMST_IPTR_SHIFT)

/* Interrupt vectors */
#define C54X_INT_RESET   0
#define C54X_INT_NMI     1
/* ============================================================================
 * CALYPSO DSP INTERRUPT TABLE. Authority: CAL207 15.1 "DSP interrupts Mapping",
 * which gives each vector's hex Location; vec = Location / 4. CAL000 5.1: "The
 * DSP subchip owns 17 interrupt lines with 11 of which INT0n to INT10n are
 * dedicated for external peripherals."
 *
 * Do NOT substitute the generic TMS320C54x table from SPRU131: the Calypso DSP
 * subchip (S28C128) has its own peripheral mapping. The generic part has four
 * external lines (INT0..INT3) before TINT, Calypso has three (INT0n..INT2n), so
 * everything from bit 3 on shifts by one, and BRINT0/BXINT0/DMAC0 do not exist
 * here at all.
 *
 *  bit  vec  Loc.  line      source (CAL207 15.1)               trigger
 *  ---  ---  ----  --------  ---------------------------------  -------
 *   0    16  0x40  INT0n     RIF receive interrupt              level
 *   1    17  0x44  INT1n     RIF transmit interrupt             level
 *   2    18  0x48  INT2n     UART interrupt                     level
 *   3    19  0x4C  TINT      DSP timer
 *   4    20  0x50  RINT      port serie voix SP0 (VSP vers l'ABB) : ISR ROM 0x72d3 -> 0xc14a, 8 kHz
 *   5    21  0x54  XINT      SPI transmit interrupt
 *   6    22  0x58  INT4n     MCSI transmit interrupt            level
 *   7    23  0x5C  INT5n     MCSI frame duration error          level
 *   8    24  0x60  INT3n     MCSI receive interrupt             level
 *   9    25  0x64  AINT      API interrupt (ARM <-> DSP)
 *  10    26  0x68  INT6n     MCSI DAI interrupt                 level
 *  11    27  0x6C  INT7n     CYPHER interrupt                   edge
 *  12    28  0x70  INT8n     TPU FRAME interrupt                edge
 *  13    29  0x74  INT9n     TPU programmable interrupt         edge
 *  14    30  0x78  INT10n    DMA interrupt                      level
 *        1   0x04  nMIN      Rhea bus abort, or INT4n redirect (= NMI)
 *
 * The prose list in CAL000 5.1 disagrees from bit 6 on (it places AINT at bit
 * 12). The CAL207 Location column wins: it is an address, not an order to be
 * interpreted.
 *
 * Measurements backing this table:
 *  - ROM IMR reads 0x52ef (bits 0,1,2,3,5,6,7,9,12,14; 0x52ed on other runs,
 *    the same minus bit 1) = RIF rx/tx, UART, TINT, SPI tx, MCSI tx/err, AINT,
 *    TPU frame, DMA. The bits left masked are CYPHER (11), MCSI rx (8), TPU
 *    programmable (13) and SPI rx (4) - exactly what a GSM L1 does not use.
 *  - The ROM also does IMR |= 0x3000 = bits 12+13 = both TPU lines.
 *  - The firmware writes 0x0380 to CNTRL_REG (XIO:FA00), which selects edge vs
 *    level per channel (CAL207 15.2.1): bits 7, 8, 9 edge, matching exactly the
 *    three lines CAL207 15.1 marks as edge (INT7n/INT8n/INT9n). Channel N is
 *    therefore INTNn.
 *  - vec 20 / IMR bit 4 is the voice port SP0 (DRR 0x20, DXR 0x21, SPC 0x22); the ROM
 *    arms it at speech start (0xbd1e/0xbd47: SPC 0x08 then 0xC8, IMR |= 0x10). Modelled
 *    by calypso_vsp.c (CALYPSO_VSP=1).
 *
 * CAL000 5.1 on INT9n: "a facility offered to the DSP programmer in order to
 * allow the generation of a DSP interrupt at a dedicated time with a quarter of
 * GSM bit accuracy. The interrupt is set in a scenario by using a time-stamped
 * instruction." That is the TPU sequencer's MOVE TPUI_DSP_INT_PG.
 *
 * Formula: vec = imr_bit + 16.
 * ==========================================================================*/
#define C54X_IT_RIF_RX_VEC     16   /* INT0n  RIF receive              */
#define C54X_IT_RIF_RX_BIT      0
#define C54X_IT_RIF_TX_VEC     17   /* INT1n  RIF transmit             */
#define C54X_IT_RIF_TX_BIT      1
#define C54X_IT_UART_VEC       18   /* INT2n  UART                     */
#define C54X_IT_UART_BIT        2
#define C54X_IT_TINT_VEC       19   /* TINT   timer DSP                */
#define C54X_IT_TINT_BIT        3
#define C54X_IT_SPI_RX_VEC     20   /* RINT   SPI receive              */
#define C54X_IT_SPI_RX_BIT      4
#define C54X_IT_SPI_TX_VEC     21   /* XINT   SPI transmit             */
#define C54X_IT_SPI_TX_BIT      5
#define C54X_IT_MCSI_TX_VEC    22   /* INT4n  MCSI transmit     (0x58) */
#define C54X_IT_MCSI_TX_BIT     6
#define C54X_IT_MCSI_ERR_VEC   23   /* INT5n  MCSI frame dur.   (0x5C) */
#define C54X_IT_MCSI_ERR_BIT    7
#define C54X_IT_MCSI_RX_VEC    24   /* INT3n  MCSI receive      (0x60) */
#define C54X_IT_MCSI_RX_BIT     8
#define C54X_IT_API_VEC        25   /* AINT   API (ARM<->DSP)   (0x64) */
#define C54X_IT_API_BIT         9
#define C54X_IT_MCSI_DAI_VEC   26   /* INT6n  MCSI DAI          (0x68) */
#define C54X_IT_MCSI_DAI_BIT   10
#define C54X_IT_CRYPT_VEC      27   /* INT7n  CYPHER            (0x6C) */
#define C54X_IT_CRYPT_BIT      11
#define C54X_IT_TPU_FRAME_VEC  28   /* INT8n  TPU frame         (0x70) */
#define C54X_IT_TPU_FRAME_BIT  12
#define C54X_IT_TPU_PROG_VEC   29   /* INT9n  TPU programmable  (0x74) */
#define C54X_IT_TPU_PROG_BIT   13
#define C54X_IT_DMA_VEC        30   /* INT10n DMA               (0x78) */
#define C54X_IT_DMA_BIT        14

#define C54X_NUM_INTS        16

typedef struct C54xState {
    /* Accumulators (40-bit) stored as int64 for convenience */
    int64_t a;   /* A accumulator: bits 39-0 */
    int64_t b;   /* B accumulator: bits 39-0 */

    /* Auxiliary registers */
    uint16_t ar[8];

    /* Other registers */
    uint16_t t;      /* Temporary register */
    uint16_t trn;    /* Transition register (Viterbi) */
    uint16_t sp;
    uint16_t bk;     /* Circular buffer size */
    uint16_t brc;    /* Block repeat counter */
    uint16_t rsa;    /* Block repeat start address */
    uint16_t rea;    /* Block repeat end address */

    /* Status registers */
    uint16_t st0;
    uint16_t st1;
    uint16_t pmst;

    /* Interrupt registers */
    uint16_t imr;
    uint16_t ifr;

    /* Optional reset-state override loaded from calypso_dsp.Registers.bin via
     * `-M calypso,dsp-registers=<path>` (default-wired by run.sh, like the
     * other ROM sections). reg_init[i] = value for MMR index i (0x00..0x1F).
     * When reg_init_valid, c54x_reset() applies these AFTER its silicon
     * hardcode defaults, so the .bin snapshot is authoritative. */
    uint16_t reg_init[0x20];
    bool     reg_init_valid;

    /* Program counter */
    uint32_t pc;     /* 16-bit (or 23-bit with XPC) */
    uint16_t xpc;

    /* Timer0 prescale counter (PSC) — not memory-mapped directly */
    uint16_t timer_psc;

    /* DMA sub-register bank (6 channels × 4 regs) */
    uint16_t dma_subaddr;
    uint16_t dma_subregs[24];
    /* McBSP sub-register bank */
    uint16_t spsa;

    /* RPT state */
    uint16_t rpt_count;  /* remaining RPT iterations */
    uint16_t rpt_pc;     /* PC of repeated instruction */
    bool     rpt_active;
    bool     rpt_fresh;   /* RPT just armed: the first READA/MVPD read starts
                          * from the base address, not from a stale mvpd_src. */
    uint16_t par;        /* Program Address Register (for READA/WRITA/MACD/MACP) */
    bool     par_set;
    bool     lk_used;    /* resolve_smem consumed extra word for lk */
    uint16_t mvpd_src;   /* MVPD auto-increment source address during RPT */

    /* RPTB state */
    bool     rptb_active;

    /* Delayed-branch state (CALLD/RETD/BD/CCD/...): when set, the next
     * `delay_slots` instructions execute normally, then PC is forced to
     * `delayed_pc`. */
    uint16_t delayed_pc;
    uint8_t  delay_slots;

    /* Memory */
    uint16_t prog[C54X_PROG_SIZE];   /* Program memory */
    uint16_t data[C54X_DATA_SIZE];   /* Data memory */

    /* API RAM pointer (shared with ARM calypso_trx.c) */
    uint16_t *api_ram;  /* points into ARM's dsp_ram[] */

    /* DSP → ARM notify hook: called whenever the DSP writes to api_ram. */
    void (*api_write_cb)(void *opaque, uint16_t woff, uint16_t val);
    void  *api_write_cb_opaque;

    /* State */
    bool     running;
    bool     idle;       /* IDLE instruction executed */
    bool     blob_loaded; /* Test fixture: set by c54x_set_initial_pc().
                           * Suppresses the secondary c54x_reset() that
                           * normally fires when ARM writes DSP_DL_STATUS_READY,
                           * which would otherwise clobber the user's blob
                           * via the reset-time PROM→DARAM auto-copy. */
    uint64_t cycles;
    uint32_t insn_count;

    /* BSP (Baseband Serial Port) — burst sample buffer */
    uint16_t bsp_buf[2048]; /* burst I/Q samples from radio */
    int      bsp_len;       /* number of samples */
    int      bsp_pos;       /* read position */

    /* Debug */
    uint32_t unimpl_count;
    uint16_t last_unimpl;
    /* Last executed instruction snapshot — captured at end of each
     * c54x_run iteration. Used by the INTM-TRANS tracer (and others)
     * to attribute post-instruction state changes to the actual cause
     * PC/opcode rather than the post-advance PC. */
    uint16_t last_exec_pc;
    uint16_t last_exec_op;

    /* writer_kind : set by each opcode handler / external writer before
     * calling data_write. Logged in DATA-W-MMR trace to disambiguate
     * which path is responsible for stray writes to MMR (addr<=0x1F).
     * Reset to WK_UNKNOWN at the top of c54x_exec_one. */
    uint8_t  writer_kind;
} C54xState;

/* writer_kind enum — keep small, extend as needed */
enum {
    WK_UNKNOWN     = 0,
    WK_OPCODE_F3   = 1,   /* 0xF3xx family (SFTL/AND/OR/XOR/INTR/etc.) */
    WK_OPCODE_8x   = 2,   /* 0x80xx-0x8Fxx (STL/STH/STLM/STM/LD-Smem) */
    WK_OPCODE_77   = 3,   /* 0x77xx STM #lk, MMR */
    WK_OPCODE_76   = 4,   /* 0x76xx ST #lk, Smem */
    WK_OPCODE_PSHM = 5,   /* PSHM/POPM stack ops */
    WK_OPCODE_RET  = 6,   /* RET/RETI/RETD frame restore */
    WK_IRQ_ACK     = 7,   /* IRQ acknowledge / vector dispatch */
    WK_ARM_MMIO    = 8,   /* ARM-side write through shared region */
    WK_RESOLVE_AR  = 9,   /* resolve_smem AR-modify side effect */
    WK_OPCODE_OTHER= 10,  /* anything else inside an opcode handler */
};

/* Feed burst samples to BSP (called by calypso_trx) */
void c54x_bsp_load(C54xState *s, const uint16_t *samples, int n);

/* Create and initialize C54x state */
C54xState *c54x_init(void);

/* Link API RAM (shared memory with ARM) */
void c54x_set_api_ram(C54xState *s, uint16_t *api_ram);

/* Reset the DSP */
void c54x_reset(C54xState *s);

/* Execute N instructions (returns actual count executed) */
int c54x_run(C54xState *s, int n_insns);

/* Raise an interrupt */
/* Send interrupt: vec = vector number (for PC), imr_bit = bit in IMR/IFR */
void c54x_interrupt_ex(C54xState *s, int vec, int imr_bit);

/* Wake from IDLE */
void c54x_wake(C54xState *s);

/* Test fixture: override PC after reset.
 * Used by `-M calypso,dsp-blob=<path>` to start execution at a custom
 * address instead of the silicon-default reset vector (IPTR * 0x80). */
void c54x_set_initial_pc(C54xState *s, uint32_t pc);

/* Test fixture: load a raw binary blob into DARAM starting at daram_addr.
 * File bytes are read pairwise as little-endian DSP words.
 * Returns number of words loaded, or -1 on error. */
int  c54x_load_blob_daram(C54xState *s, const char *path, uint16_t daram_addr);

/* Explicit per-section ROM load: write raw LE 16-bit words from `path`
 * into either prog[] (when is_program=true) or data[] (when false),
 * starting at DSP word address `start_addr`. Used by the per-section
 * machine properties (dsp-prom0/prom1/prom2/prom3/drom/pdrom) to load
 * each ROM section at its silicon-correct DSP address.
 * Returns number of words loaded, or -1 on error. */
int  c54x_load_section(C54xState *s, const char *path,
                       uint32_t start_addr, bool is_program);

/* Load the DSP register snapshot (calypso_dsp.Registers.bin: raw LE 16-bit
 * words, MMR page 0x00..0x1F first) into reg_init[] so c54x_reset() applies
 * it as the reset state. Words >= 0x20 are written into data[] (low scratch).
 * Used by the `-M calypso,dsp-registers=<path>` machine property.
 * Returns number of words loaded, or -1 on error. */
int  c54x_load_registers(C54xState *s, const char *path);

/* [c54x-earlyboot] Boot the C54x at machine-init time, BEFORE the ARM vCPU
 * runs. ORDER IS LOAD-BEARING: the ARM posts its bootloader command
 * (data[0x0fff] = cmd, data[0x0ffe] = entry) from fn=0, and the DSP's own
 * init-IDLE at 0xb419 (ST #1,*0xfff) overwrites that command if the DSP boots
 * afterwards, leaving it spinning forever at 0xb41c. Booting here parks the DSP
 * on its IDLE before the ARM write, so the command survives. No mailbox value
 * is forced; only the timing of the boot is. Gate: CALYPSO_DSP_RUN_C54X=1. */
void c54x_early_boot(C54xState *s);

/* True if c54x_early_boot() actually parked the DSP. Gates the C54x re-reset in
 * calypso_trx.c, which would replay the PROM->DARAM copy and clobber the
 * bootloader command preserved above. */
bool c54x_early_booted(void);

/* Current DSP mission (d_task_md) read from API RAM: FB=5 SB=6 TCH_FB=8
 * TCH_SB=9, 0 = none. Page 0 = data[0x0804], page 1 = data[0x0818]. Used to
 * gate the inter-block wires (BSP BRINT0) on the FB/SB mission. */
uint16_t c54x_task_md(C54xState *s);

#endif /* CALYPSO_C54X_H */

4.55 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_dma.c

7836 octets, 216 lignes → 216 lignes

/*
 * calypso_dma.c - internal DMA controller of the TMS320C54x.
 *
 * See calypso_dma.h for the rationale, the mapping conflict and the explicit
 * list of what is NOT modelled.
 *
 * Configuration:
 *   CALYPSO_DMA=1            enable the module (default OFF)
 *   CALYPSO_DMA_VEC_BASE=30  DMA interrupt vector (default 30 = INT10n = IMR
 *                            bit 14 + 16, CAL000 5.1; shared by the 4 channels)
 *   CALYPSO_DMA_MAX_MOTS=... per-transfer safety cap (default 4096)
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#include "calypso_dma.h"
#include "calypso_c54x.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "hw/arm/calypso/calypso_debug.h"

int calypso_dma_actif = -1;   /* -1 = initialise on first access */

static int      g_init;
/* Per CAL000 5.1. Do NOT take the DMAC0 vector from the GENERIC C54x table
 * (SPRU131): on Calypso, IMR bit 7 is INT4n = MCSI TRANSMIT, and no DMAC0..3
 * vectors exist. The DSP sees a SINGLE DMA line, INT10n = bit 14 = vec 30, and
 * the four channels (CAL000 3.5.15) share it; the channel is read from the
 * registers, not from the interrupt number. */
static int      g_vec_base = C54X_IT_DMA_VEC;   /* INT10n, shared by the 4 channels */
static unsigned g_max_mots = 4096;
static unsigned g_log;

/* Module-local state: the sub-register bank in calypso_c54x.c only holds 4
 * registers per channel, so the missing ones are kept here. */
static uint16_t g_dmprec;                       /* last DMPREC written */
static uint16_t g_subaddr;                      /* DMSA */
static uint16_t g_sub[C54X_DMA_CANAUX * 5];     /* SRC/DST/CTR/SFC/MCR */
static uint8_t  g_arme[C54X_DMA_CANAUX];        /* channel enabled, transfer due */

void calypso_dma_init(void)
{
    const char *e;

    if (g_init) {
        return;
    }
    g_init = 1;

    e = calypso_getenv("CALYPSO_DMA");
    if (!e || !*e || !strcmp(e, "0")) {
        calypso_dma_actif = 0;
        return;
    }
    e = calypso_getenv("CALYPSO_DMA_VEC_BASE");
    if (e && *e) {
        g_vec_base = (int)strtol(e, NULL, 0);
    }
    e = calypso_getenv("CALYPSO_DMA_MAX_MOTS");
    if (e && *e) {
        g_max_mots = (unsigned)strtoul(e, NULL, 0);
    }

    calypso_dma_actif = 1;
    fprintf(stderr, "[dma] contrôleur DMA c54x ACTIF — mapping SPRU131 "
            "(DMPREC=0x54 DMSA=0x55 DMSDI=0x56 DMSDN=0x57), DMAC0=vec%d, "
            "plafond %u mots/transfert.\n"
            "[dma] ⚠️ transfert EN BLOC au tick de trame : la synchronisation "
            "par événement (DMSFC) n'est PAS modélisée.\n",
            g_vec_base, g_max_mots);
}

/* Readable name of a sub-register, for traces. */
static const char *dma_nom_sub(unsigned reg)
{
    static const char *n[5] = { "SRC", "DST", "CTR", "SFC", "MCR" };
    return (reg < 5) ? n[reg] : "?";
}

bool calypso_dma_mmr_write(C54xState *s, uint16_t addr, uint16_t val)
{
    if (calypso_dma_actif < 0) {
        calypso_dma_init();
    }
    if (!calypso_dma_actif) {
        return false;            /* off: the historical decode applies */
    }

    switch (addr) {
    case C54X_DMPREC: {
        uint16_t avant = g_dmprec;
        g_dmprec = val;
        /* Bits 0..5: DE0..DE5, per-channel enable. A rising edge arms the
         * transfer; it is NOT started here but at the tick, so the firmware
         * has finished writing its sub-registers first. */
        for (unsigned c = 0; c < C54X_DMA_CANAUX; c++) {
            uint16_t bit = (uint16_t)(1u << c);
            if ((val & bit) && !(avant & bit)) {
                g_arme[c] = 1;
                if (g_log < 40) {
                    g_log++;
                    fprintf(stderr, "[dma] canal %u ARMÉ par DMPREC=0x%04x "
                            "(SRC=0x%04x DST=0x%04x CTR=%u) PC=0x%04x\n",
                            c, val, g_sub[c * 5 + 0], g_sub[c * 5 + 1],
                            g_sub[c * 5 + 2], s->pc);
                }
            }
        }
        s->data[addr] = val;
        return true;
    }

    case C54X_DMSA:
        g_subaddr = val;
        s->data[addr] = val;
        return true;

    case C54X_DMSDI:
    case C54X_DMSDN:
        if (g_subaddr < C54X_DMA_CANAUX * 5) {
            g_sub[g_subaddr] = val;
            if (g_log < 40) {
                g_log++;
                fprintf(stderr, "[dma] canal %u %s = 0x%04x (sous-adr 0x%02x) "
                        "PC=0x%04x\n", g_subaddr / 5,
                        dma_nom_sub(g_subaddr % 5), val, g_subaddr, s->pc);
            }
        }
        s->data[addr] = val;
        if (addr == C54X_DMSDI) {
            g_subaddr++;          /* auto-increment; DMSDN has none */
        }
        return true;

    default:
        return false;
    }
}

void calypso_dma_tick(C54xState *s)
{
    if (calypso_dma_actif < 0) {
        calypso_dma_init();
    }
    if (!calypso_dma_actif || !s) {
        return;
    }

    for (unsigned c = 0; c < C54X_DMA_CANAUX; c++) {
        if (!g_arme[c]) {
            continue;
        }
        uint16_t src = g_sub[c * 5 + 0];
        uint16_t dst = g_sub[c * 5 + 1];
        uint16_t ctr = g_sub[c * 5 + 2];
        unsigned n   = ctr;

        /* Safety net: a bogus CTR (sub-registers not written yet, or a wrong
         * mapping) must not sweep the whole DSP memory. */
        if (n == 0 || n > g_max_mots) {
            if (g_log < 40) {
                g_log++;
                fprintf(stderr, "[dma] canal %u : CTR=%u hors plage [1..%u] — "
                        "transfert IGNORÉ (sous-registres incomplets ?)\n",
                        c, n, g_max_mots);
            }
            g_arme[c] = 0;
            continue;
        }

        /* [2026-09-19] Who writes a_sch? Not the core: a probe on the
         * instruction write path (c54x_mem.c, A_SCH-WR) counted ZERO stores to
         * 0x0837..0x083b / 0x084b..0x084f while the cells changed 60 times in
         * 35 s. This block copy is the remaining writer, and it is not visible
         * from the core side. a_sch[0] has been seen carrying plain numbers
         * (0x1111, 0x1388=5000) where only B_BLUD (bit15) and B_SCH_CRC (bit8)
         * mean anything -- so name the source address the words come from. */
        {
            unsigned d0 = dst, d1 = dst + n - 1;
            int touche = (d0 <= 0x083b && d1 >= 0x0837) || (d0 <= 0x084f && d1 >= 0x084b);
            if (touche) {
                static unsigned na;
                if (na < 40) {
                    unsigned off = (d0 <= 0x0837) ? (0x0837 - d0) : 0;
                    fprintf(stderr, "[dma] A_SCH canal %u : %u mots 0x%04x -> 0x%04x ; "
                            "source a_sch[0] = data[0x%04x] = 0x%04x\n",
                            c, n, src, dst, (uint16_t)(src + off),
                            s->data[(uint16_t)(src + off)]);
                    na++;
                }
            }
        }

        /* Block transfer, DATA space only (see the header: program and I/O
         * spaces are not modelled). */
        for (unsigned i = 0; i < n; i++) {
            s->data[(uint16_t)(dst + i)] = s->data[(uint16_t)(src + i)];
        }

        g_sub[c * 5 + 2] = 0;     /* CTR exhausted */
        g_arme[c] = 0;
        g_dmprec = (uint16_t)(g_dmprec & ~(1u << c));   /* channel disarms itself */
        s->data[C54X_DMPREC] = g_dmprec;

        if (g_log < 40) {
            g_log++;
            fprintf(stderr, "[dma] canal %u TRANSFÉRÉ %u mots 0x%04x -> 0x%04x, "
                    "interruption DMA INT10n (canal %u, vec %d, IMR bit %d)\n",
                    c, n, src, dst, c, g_vec_base, g_vec_base - 16);
        }

        /* Completion signal. CAL000 5.1: one INT10n line for all 4 channels,
         * IMR bit = vec - 16 (formula from calypso_c54x.h). */
        c54x_interrupt_ex(s, g_vec_base, g_vec_base - 16);
    }
}

4.56 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_dma.h

3191 octets, 70 lignes → 70 lignes

/*
 * calypso_dma.h - TMS320C54x on-chip DMA controller.
 *
 * Why this module exists. The DSP firmware queues transfer requests in a
 * 14-entry ring (read pointer data[0x433e], write pointer data[0x433f],
 * producer at 0xaa75, consumer at 0xaa87). [2026-07-29] The producer pushes,
 * the read pointer NEVER advances, the ring fills, the write pointer catches
 * the read pointer and the firmware raises DSP_ERR_DMA_PROG (bit 3 of
 * data[0x3f92], published in d_error_status = 0x08d5, printed by the ARM as
 * "DSP Error Status: 8", 605 times).
 *
 * The instructions in that sequence are all emulated correctly; what is
 * missing is the DMA controller itself. The core model accepts the
 * programming (sub-register bank in calypso_c54x.c) but performs no transfer
 * and never raises DMAC0..5, so nothing signals completion, the consumer
 * never has a reason to pop, and the queue saturates.
 *
 * ⚠️ MAPPING CONFLICT, to be settled by measurement.
 *   SPRU131 places DMPREC=0x54 DMSA=0x55 DMSDI=0x56 DMSDN=0x57.
 *   calypso_c54x.c uses 0x54 as DMSA, one register off; the DMAWATCH probe
 *   already flags this with its "DMPREC?(modele:DMSA)" label. This module
 *   implements the MANUAL mapping and stays OPTIONAL (CALYPSO_DMA=1, default
 *   OFF): while it is off, the existing decode runs unchanged.
 *
 * ⚠️ NOT MODELLED, and to stay that way until measured:
 *   - event synchronisation (DMSFC: McBSP, timer, external line) - transfers
 *     are done IN ONE BLOCK on the frame tick, not at the real rate;
 *   - transfers to/from PROGRAM space and I/O space (DMMCR);
 *   - priority interleaving between channels (DPRC);
 *   - ABU mode (hardware circular addressing per channel).
 *   Each of these is a deliberate approximation, not an oversight. The only
 *   goal here is to close the "program -> transfer -> signal" loop.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#ifndef CALYPSO_DMA_H
#define CALYPSO_DMA_H

#include <stdint.h>
#include <stdbool.h>

typedef struct C54xState C54xState;

/* MMR addresses per SPRU131. */
#define C54X_DMPREC   0x0054   /* control/priority, bits 0..5 = channel enable */
#define C54X_DMSA     0x0055   /* sub-address */
#define C54X_DMSDI    0x0056   /* data, auto-increments DMSA */
#define C54X_DMSDN    0x0057   /* data, no auto-increment */

/* Five sub-registers per channel (SPRU131, DMA chapter). The existing bank in
 * calypso_c54x.c only has four (SRC/DST/CTR/MCR): DMSFC is missing, which is
 * why event synchronisation cannot be modelled. */
#define C54X_DMA_CANAUX   6

/* Whether the module is enabled; tested inline on hot paths. */
extern int calypso_dma_actif;

/* Read the configuration (CALYPSO_DMA). Idempotent, called lazily. */
void calypso_dma_init(void);

/* Intercept an MMR write. Returns true when this module handled it, in which
 * case the caller does nothing more. Returns false when the module is off,
 * leaving the historical decode in place. */
bool calypso_dma_mmr_write(C54xState *s, uint16_t addr, uint16_t val);

/* Call once per TDMA frame: run the active channels and raise the
 * end-of-transfer interrupt. */
void calypso_dma_tick(C54xState *s);

#endif /* CALYPSO_DMA_H */

4.57 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_fbsb.c

5498 octets, 144 lignes → 144 lignes

/*
 * calypso_fbsb.c - QEMU-side FBSB state tracking (logs only)
 *
 * No host-side synthesis: FB/SB detection is driven entirely by the DSP
 * writing the NDB cells, and the ARM reads them directly. This file only
 * follows d_task_md transitions, counts dispatches and dumps the cells.
 *
 * The dump reads each cell in BOTH views, because they can disagree:
 *
 *   data[] : DSP view - what the correlator writes
 *   api[]  : ARM view - what the firmware actually reads (prim_fbsb.c:306
 *                       read_fb_result() reads api_ram, not data[])
 *
 * A divergence is not a probe artefact, it is the diagnostic: "data[] full,
 * api[] empty" means the result is computed and never reaches the firmware.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#include "calypso_fbsb.h"
#include "calypso_full_pcb.h"   /* DARAM lock helpers */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void calypso_fbsb_init(CalypsoFbsb *s, uint16_t *ndb_word_base,
                       uint16_t api_base, uint16_t *api_ram)
{
    if (!s) return;
    s->ndb       = ndb_word_base;
    s->api       = api_ram;
    s->api_base  = api_base;
    calypso_fbsb_reset(s);
}

void calypso_fbsb_reset(CalypsoFbsb *s)
{
    if (!s) return;
    /* Deliberately does NOT clear ndb / api / api_base: those are bindings,
     * not session state. */
    s->state       = FBSB_IDLE;
    s->fb0_attempt = 0;
    s->fb1_attempt = 0;
    s->sb_attempt  = 0;
    s->fn_started  = 0;
}

void calypso_fbsb_on_dsp_task_change(CalypsoFbsb *s, uint16_t d_task_md,
                                     uint64_t fn)
{
    /* Dedupe: task and state rarely change between frames, and printing one
     * line per call produced 2554 lines out of 20000, each with its own
     * fflush. Print transitions only, plus a periodic repeat count. */
    {
        static uint32_t l_key = 0xFFFFFFFFu; static unsigned long long rep = 0;
        uint32_t key = ((uint32_t)d_task_md << 8) ^ (uint32_t)(s ? s->state : 0xFF);
        if (key != l_key) {
            if (rep)
                fprintf(stderr, "[calypso-fbsb] on_dsp_task_change × %llu "
                        "(identique, non repete)\n", rep);
            l_key = key; rep = 0;
            fprintf(stderr, "[calypso-fbsb] on_dsp_task_change task=%u fn=%lu state=%d\n",
                    d_task_md, (unsigned long)fn, s ? (int)s->state : -1);
            fflush(stderr);
        } else if (++rep % 2000 == 0) {
            fprintf(stderr, "[calypso-fbsb] on_dsp_task_change × %llu "
                    "(task=%u state=%d, fn=%lu)\n",
                    rep, d_task_md, s ? (int)s->state : -1, (unsigned long)fn);
            fflush(stderr);
        }
    }
    if (!s) return;
    switch (d_task_md) {
    case DSP_TASK_FB:
        s->fb0_attempt++;   /* real count of FB task dispatches */
        s->state       = FBSB_FB0_SEARCH;
        s->fn_started  = fn;
        calypso_fbsb_dump(s, "FB0_SEARCH (real DSP path)");
        break;
    case DSP_TASK_SB:
        s->sb_attempt++;    /* real count of SB task dispatches */
        s->state      = FBSB_SB_SEARCH;
        s->fn_started = fn;
        calypso_fbsb_dump(s, "SB_SEARCH (real DSP path)");
        break;
    case DSP_TASK_ALLC: {
        static int log_once;
        if (!log_once++) {
            fprintf(stderr,
                    "[fbsb] ALLC task=24 fn=%lu — real DSP CCCH demod\n",
                    (unsigned long)fn);
            fflush(stderr);
        }
        break;
    }
    case DSP_TASK_NONE:
    default:
        break;
    }
}

/* Read one NDB cell in either view. `base` is s->api_base (0x0800) for both:
 * data[] is indexed from &data[0x0800] and api_ram from C54X_API_BASE, which
 * is the same value. Returns -1 when the view is absent (null pointer), so
 * that "not bound" stays distinguishable from "read as zero". */
static int fbsb_cell(const uint16_t *view, uint16_t base, uint16_t cell)
{
    return view ? (int)view[cell - base] : -1;
}

void calypso_fbsb_dump(const CalypsoFbsb *s, const char *tag)
{
    if (!s) return;
    static const char *names[] = {
        "IDLE", "FB0_SEARCH", "FB0_FOUND",
        "FB1_SEARCH", "FB1_FOUND",
        "SB_SEARCH",  "SB_FOUND",
        "DONE", "FAIL",
    };
    const uint16_t b = s->api_base;

    int d_det = fbsb_cell(s->ndb, b, NDB_D_FB_DET);
    int d_toa = fbsb_cell(s->ndb, b, NDB_A_SYNC_DEMOD_TOA);
    int d_pm  = fbsb_cell(s->ndb, b, NDB_A_SYNC_DEMOD_PM);
    int d_ang = fbsb_cell(s->ndb, b, NDB_A_SYNC_DEMOD_ANG);
    int d_snr = fbsb_cell(s->ndb, b, NDB_A_SYNC_DEMOD_SNR);

    int a_det = fbsb_cell(s->api, b, NDB_D_FB_DET);
    int a_toa = fbsb_cell(s->api, b, NDB_A_SYNC_DEMOD_TOA);
    int a_pm  = fbsb_cell(s->api, b, NDB_A_SYNC_DEMOD_PM);
    int a_ang = fbsb_cell(s->api, b, NDB_A_SYNC_DEMOD_ANG);
    int a_snr = fbsb_cell(s->api, b, NDB_A_SYNC_DEMOD_SNR);

    /* TOA and ANGLE are signed on the firmware side. */
    fprintf(stderr,
            "[fbsb] %s state=%s fb0_att=%u fb1_att=%u sb_att=%u "
            "data[](det=%d toa=%d pm=%d ang=%d snr=0x%04x) "
            "api[](det=%d toa=%d pm=%d ang=%d snr=0x%04x)%s\n",
            tag ? tag : "", names[s->state],
            s->fb0_attempt, s->fb1_attempt, s->sb_attempt,
            d_det, (int)(int16_t)d_toa, d_pm, (int)(int16_t)d_ang, d_snr & 0xFFFF,
            a_det, (int)(int16_t)a_toa, a_pm, (int)(int16_t)a_ang, a_snr & 0xFFFF,
            (d_det > 0 && a_det <= 0) ? "  <<<< DIVERGENCE data/api" : "");
    fflush(stderr);
}

4.58 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_fbsb.h

5148 octets, 121 lignes → 121 lignes

/*
 * calypso_fbsb.h - QEMU-side FBSB (FCCH/SCH burst search) tracking
 *
 * Follows the FBSB sequence by watching d_task_md transitions and reading
 * back the NDB cells. Detection itself belongs to the DSP; nothing here
 * synthesises a result (see calypso_fbsb.c).
 *
 * The state machine being mirrored is the firmware's, in osmocom-bb
 * src/target/firmware/layer1/prim_fbsb.c:
 *
 *   IDLE
 *     │
 *     │  ARM writes d_task_md = FB_DSP_TASK (mode 0)
 *     ▼
 *   FB0_SEARCH ── correlator finds burst ──► FB0_FOUND
 *     │                                         │
 *     │ 12 attempts no FB                       │ ferr small enough
 *     │                                         ▼
 *     ▼                                       FB1_SEARCH ──► FB1_FOUND
 *   FAIL (result=255)                            │              │
 *                                                ▼              ▼
 *                                              FAIL          SB_SEARCH ──► SB_FOUND
 *                                                                │            │
 *                                                                ▼            ▼
 *                                                              FAIL        SUCCESS
 *
 * NDB cells we read/write (offsets in DSP data words from API base 0x0800).
 * Layout from osmocom-bb include/calypso/dsp_api.h:202-204 (d_fb_det,
 * d_fb_mode, then a_sync_demod[4]); the #defines below are authoritative:
 *   d_dsp_page         0x08D4    (page toggle from ARM)
 *   d_fb_det           0x08F8    (DSP -> ARM: non-zero = FB found)
 *   d_fb_mode          0x08F9    (ARM -> DSP: 0 = wideband search, 1 = narrow)
 *   a_sync_demod[0]    0x08FA    D_TOA   - time of arrival
 *   a_sync_demod[1]    0x08FB    D_PM    - power measurement
 *   a_sync_demod[2]    0x08FC    D_ANGLE - frequency phase angle
 *   a_sync_demod[3]    0x08FD    D_SNR   - signal-to-noise ratio
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#ifndef CALYPSO_FBSB_H
#define CALYPSO_FBSB_H

#include <stdint.h>
#include <stdbool.h>

/* NDB cell offsets - DSP data word addresses.
 *
 * Anchored on osmocom-bb dsp_api.h:18, BASE_API_NDB = 0xFFD001A8: NDB starts
 * at DSP word 0x0800 + 0x1A8/2 = 0x08D4, which is d_dsp_page (field 0 of
 * T_NDB_MCU_DSP); d_dsp_state is field 14, hence 0x08E2. Cross-checked at
 * runtime (the only ARM write to 0x08E2 is dsp.c:215, ndb->d_dsp_state = 3 =
 * C_DSP_IDLE3) and against the DSP ROM, which reads 0x08D4 as the page
 * selector and never references 0x08E2 in any of the 5 program ROMs.
 * An off-by-one-word here is read as a real address: keep both constants and
 * the comment above in agreement. */
#define NDB_D_DSP_PAGE       0x08D4
#define NDB_D_DSP_STATE      0x08E2
#define NDB_D_FB_DET         0x08F8
#define NDB_D_FB_MODE        0x08F9
#define NDB_A_SYNC_DEMOD_TOA 0x08FA
#define NDB_A_SYNC_DEMOD_PM  0x08FB
#define NDB_A_SYNC_DEMOD_ANG 0x08FC
#define NDB_A_SYNC_DEMOD_SNR 0x08FD

/* d_task_md values used by the firmware (subset).
 * From osmocom-bb dsp_api.h, verified against l1s_pm_cmd / l1s_fbdet_cmd. */
#define DSP_TASK_NONE       0
#define DSP_TASK_PM         1   /* PM_DSP_TASK (power measurement, NOT FB) */
#define DSP_TASK_FB         5   /* FB_DSP_TASK (frequency burst, idle) */
#define DSP_TASK_SB         6   /* SB_DSP_TASK (sync burst, idle)      */
#define DSP_TASK_TCH_FB     8   /* TCH_FB_DSP_TASK (dedicated) */
#define DSP_TASK_TCH_SB     9   /* TCH_SB_DSP_TASK (dedicated) */
#define DSP_TASK_ALLC      24   /* ALLC_DSP_TASK (CCCH read while FULL BCCH/CCCH) */

/* FBSB orchestration state. One instance per Calypso. */
typedef enum {
    FBSB_IDLE = 0,
    FBSB_FB0_SEARCH,
    FBSB_FB0_FOUND,
    FBSB_FB1_SEARCH,
    FBSB_FB1_FOUND,
    FBSB_SB_SEARCH,
    FBSB_SB_FOUND,
    FBSB_DONE,
    FBSB_FAIL,
} CalypsoFbsbState;

typedef struct CalypsoFbsb {
    CalypsoFbsbState state;
    uint16_t        *ndb;          /* points into ARM dsp_ram[] (word-addressed) */
    uint16_t         api_base;     /* DSP-side word base (0x0800) */

    /* Per-attempt counters mirroring prim_fbsb.c, incremented for real in
     * calypso_fbsb_on_dsp_task_change(). */
    uint8_t          fb0_attempt;
    uint8_t          fb1_attempt;
    uint8_t          sb_attempt;

    /* ARM view of the same cells: `api` points at api_ram, i.e. what the
     * firmware actually reads (prim_fbsb.c read_fb_result), while `ndb` points
     * at the DSP-side data[]. The two views can diverge, and that divergence
     * is the diagnostic. */
    uint16_t        *api;

    /* Bookkeeping. */
    uint64_t         fn_started;
} CalypsoFbsb;

/* Lifecycle. */
void calypso_fbsb_init(CalypsoFbsb *s, uint16_t *ndb_word_base,
                       uint16_t api_base, uint16_t *api_ram);
void calypso_fbsb_reset(CalypsoFbsb *s);

/* Hooks. */
void calypso_fbsb_on_dsp_task_change(CalypsoFbsb *s, uint16_t d_task_md,
                                     uint64_t fn);

/* Trace helper: single-line dump of current state. */
void calypso_fbsb_dump(const CalypsoFbsb *s, const char *tag);

#endif /* CALYPSO_FBSB_H */

4.59 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_full_pcb.c

6622 octets, 210 lignes → 210 lignes

/*
 * calypso_full_pcb.c — Calypso PCB-level orchestrator (locks + async log)
 *
 * Provides shared mutexes used by the autonomous components (TRX, BSP,
 * DSP, FBSB) for cross-thread DARAM/API RAM access, plus an async log
 * queue that defers fprintf off the TCG main thread.
 *
 * Does NOT own any timer or clock. Timing (TDMA tick, BSP drain, etc.)
 * is owned by the respective .c file (calypso_trx.c, calypso_bsp.c).
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "qemu/osdep.h"
#include "qemu/thread.h"
#include "qemu/log.h"
#include "qemu/atomic.h"
#include "hw/irq.h"
#include "exec/cpu-common.h"
#include "hw/core/cpu.h"

#include "calypso_full_pcb.h"

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>

#include "hw/arm/calypso/calypso_debug.h"
#define PCB_LOG(fmt, ...) \
    do { if (calypso_debug_enabled("PCB")) \
        fprintf(stderr, "[pcb] " fmt "\n", ##__VA_ARGS__); } while (0)

/* === Shared locks (extern in .h) ========================================= */
QemuMutex calypso_pcb_daram_lock;
QemuMutex calypso_pcb_api_ram_lock;
QemuMutex calypso_pcb_sim_lock;
QemuMutex calypso_pcb_bsp_q_lock;
QemuMutex calypso_pcb_tpu_lock;

/* === PCB state =========================================================== */
struct CalypsoPcb {
    qemu_irq inth_inputs[CALYPSO_IRQ_MAX];
    uint64_t irq_raised[CALYPSO_IRQ_MAX];
    uint64_t irq_lowered[CALYPSO_IRQ_MAX];
};

static CalypsoPcb *g_pcb = NULL;

/* === Async log queue =====================================================
 * High-freq fprintf sites from ARM TCG main thread enqueue here, a
 * dedicated drain thread writes to stderr. Avoids stdio-lock+write
 * blocking TCG (~10-100µs per inline fprintf cumulating to jitter). */
#define ASYNC_LOG_QSIZE  4096
#define ASYNC_LOG_MAXMSG 256
typedef struct { char msg[ASYNC_LOG_MAXMSG]; } AsyncLogEntry;

static AsyncLogEntry async_log_q[ASYNC_LOG_QSIZE];
static unsigned     async_log_head = 0;
static unsigned     async_log_tail = 0;
static unsigned     async_log_dropped = 0;
static QemuMutex    async_log_lock;
static QemuCond     async_log_cond;
static QemuThread   async_log_thread;
static bool         async_log_inited = false;
static bool         async_log_stop = false;

static void *async_log_drain_fn(void *unused)
{
    (void)unused;
    qemu_mutex_lock(&async_log_lock);
    while (!async_log_stop) {
        while (async_log_head == async_log_tail && !async_log_stop) {
            qemu_cond_wait(&async_log_cond, &async_log_lock);
        }
        while (async_log_head != async_log_tail) {
            char msg[ASYNC_LOG_MAXMSG];
            memcpy(msg, async_log_q[async_log_head].msg, ASYNC_LOG_MAXMSG);
            async_log_head = (async_log_head + 1) % ASYNC_LOG_QSIZE;
            unsigned dropped_snap = async_log_dropped;
            async_log_dropped = 0;
            qemu_mutex_unlock(&async_log_lock);
            fputs(msg, stderr);
            if (dropped_snap > 0) {
                fprintf(stderr, "[pcb] async_log dropped %u msgs (queue full)\n",
                        dropped_snap);
            }
            qemu_mutex_lock(&async_log_lock);
        }
    }
    qemu_mutex_unlock(&async_log_lock);
    return NULL;
}

static void async_log_init(void)
{
    if (async_log_inited) return;
    qemu_mutex_init(&async_log_lock);
    qemu_cond_init(&async_log_cond);
    async_log_inited = true;
    qemu_thread_create(&async_log_thread, "cal-asynclog",
                       async_log_drain_fn, NULL, QEMU_THREAD_JOINABLE);
    PCB_LOG("async log queue armed (size=%d)", ASYNC_LOG_QSIZE);
}

void calypso_async_log(const char *fmt, ...)
{
    if (!async_log_inited) {
        async_log_init();
    }
    char buf[ASYNC_LOG_MAXMSG];
    va_list ap;
    va_start(ap, fmt);
    int n = vsnprintf(buf, sizeof(buf), fmt, ap);
    va_end(ap);
    if (n < 0) return;

    qemu_mutex_lock(&async_log_lock);
    unsigned next = (async_log_tail + 1) % ASYNC_LOG_QSIZE;
    if (next == async_log_head) {
        async_log_dropped++;
    } else {
        memcpy(async_log_q[async_log_tail].msg, buf, ASYNC_LOG_MAXMSG);
        async_log_tail = next;
        qemu_cond_signal(&async_log_cond);
    }
    qemu_mutex_unlock(&async_log_lock);
}

/* === Init ================================================================ */
CalypsoPcb *calypso_pcb_init(qemu_irq *inth_inputs)
{
    if (g_pcb) {
        PCB_LOG("WARN: calypso_pcb_init called twice — returning existing");
        return g_pcb;
    }

    g_pcb = g_new0(CalypsoPcb, 1);

    async_log_init();

    qemu_mutex_init(&calypso_pcb_daram_lock);
    qemu_mutex_init(&calypso_pcb_api_ram_lock);
    qemu_mutex_init(&calypso_pcb_sim_lock);
    qemu_mutex_init(&calypso_pcb_bsp_q_lock);
    qemu_mutex_init(&calypso_pcb_tpu_lock);

    if (inth_inputs) {
        for (int i = 0; i < CALYPSO_IRQ_MAX; i++) {
            g_pcb->inth_inputs[i] = inth_inputs[i];
        }
    }

    PCB_LOG("init OK (locks armed, IRQ table %s)",
            inth_inputs ? "copied from INTH" : "none");
    return g_pcb;
}

/* No-op kept for API compat with calypso_soc.c. */
void calypso_pcb_start_threads(CalypsoPcb *pcb) { (void)pcb; }
void calypso_pcb_stop_threads(CalypsoPcb *pcb)  { (void)pcb; }

/* === DARAM cross-thread helpers ========================================== */
#include "calypso_c54x.h"

uint16_t calypso_dsp_daram_read(void *dsp_void, uint16_t addr)
{
    C54xState *dsp = (C54xState *)dsp_void;
    qemu_mutex_lock(&calypso_pcb_daram_lock);
    uint16_t v = dsp->data[addr];
    qemu_mutex_unlock(&calypso_pcb_daram_lock);
    return v;
}

void calypso_dsp_daram_write(void *dsp_void, uint16_t addr, uint16_t val)
{
    C54xState *dsp = (C54xState *)dsp_void;
    qemu_mutex_lock(&calypso_pcb_daram_lock);
    dsp->data[addr] = val;
    qemu_mutex_unlock(&calypso_pcb_daram_lock);
}

void calypso_pcb_daram_lock_acquire(void)
{
    qemu_mutex_lock(&calypso_pcb_daram_lock);
}

void calypso_pcb_daram_lock_release(void)
{
    qemu_mutex_unlock(&calypso_pcb_daram_lock);
}

/* === IRQ helpers ========================================================= */
void calypso_pcb_raise_irq(CalypsoPcb *pcb, int irq_nr)
{
    if (!pcb || irq_nr < 0 || irq_nr >= CALYPSO_IRQ_MAX) return;
    qatomic_inc(&pcb->irq_raised[irq_nr]);
    if (pcb->inth_inputs[irq_nr]) {
        qemu_irq_raise(pcb->inth_inputs[irq_nr]);
    }
}

void calypso_pcb_lower_irq(CalypsoPcb *pcb, int irq_nr)
{
    if (!pcb || irq_nr < 0 || irq_nr >= CALYPSO_IRQ_MAX) return;
    qatomic_inc(&pcb->irq_lowered[irq_nr]);
    if (pcb->inth_inputs[irq_nr]) {
        qemu_irq_lower(pcb->inth_inputs[irq_nr]);
    }
}

4.60 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_full_pcb.h

4903 octets, 106 lignes → 106 lignes

/*
 * calypso_full_pcb.h - Calypso PCB-level threading orchestrator
 *
 * Interface between the standalone components (DSP/BSP/TPU/SIM/IOTA) and the
 * ARM TCG main thread. Wires the IRQs through the osmocom-bb map (irq.h) and
 * provides the shared locks (DARAM, API RAM, MMR).
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#ifndef HW_ARM_CALYPSO_FULL_PCB_H
#define HW_ARM_CALYPSO_FULL_PCB_H

#include "qemu/osdep.h"
#include "qemu/thread.h"
#include "hw/irq.h"

/* === IRQ map (mirrors osmocom-bb include/calypso/irq.h) ==================
 * Canonical reference: OsmocomBB firmware. Used by qemu_irq dispatching from
 * device threads back to the ARM (through INTH). */
#define CALYPSO_IRQ_WATCHDOG        0
#define CALYPSO_IRQ_TIMER1          1
#define CALYPSO_IRQ_TIMER2          2
#define CALYPSO_IRQ_TSP_RX          3
#define CALYPSO_IRQ_TPU_FRAME       4   /* TDMA frame tick -> ARM frame_irq */
#define CALYPSO_IRQ_TPU_PAGE        5
#define CALYPSO_IRQ_SIMCARD         6   /* SIM IT_RX/WT/OV -> sim_irq_handler */
#define CALYPSO_IRQ_UART_MODEM      7   /* osmocon L1CTL */
#define CALYPSO_IRQ_KEYPAD_GPIO     8
#define CALYPSO_IRQ_RTC_TIMER       9
#define CALYPSO_IRQ_RTC_ALARM_I2C  10
#define CALYPSO_IRQ_ULPD_GAUGING   11
#define CALYPSO_IRQ_EXTERNAL       12
#define CALYPSO_IRQ_SPI            13
#define CALYPSO_IRQ_DMA            14   /* BSP DMA done -> ARM */
#define CALYPSO_IRQ_API            15   /* DSP<->ARM mailbox done */
#define CALYPSO_IRQ_SIM_DETECT     16
#define CALYPSO_IRQ_EXTERNAL_FIQ   17
#define CALYPSO_IRQ_UART_IRDA      18
#define CALYPSO_IRQ_ULPD_GSM_TIMER 19
#define CALYPSO_IRQ_GEA            20
#define CALYPSO_IRQ_MAX            21

/* === Shared locks (taken and released by the thread entry points) ========
 * Canonical ordering, required to avoid deadlock:
 *   daram_lock < api_ram_lock < sim_lock < bsp_q_lock < tpu_lock
 * Always acquire in that order and release in the reverse one. */
extern QemuMutex calypso_pcb_daram_lock;    /* DARAM 0x0000-0x27FF */
extern QemuMutex calypso_pcb_api_ram_lock;  /* API mailbox 0x0800-0x0FFF */
extern QemuMutex calypso_pcb_sim_lock;      /* SIM controller it/fifo */
extern QemuMutex calypso_pcb_bsp_q_lock;    /* BSP UDP queue */
extern QemuMutex calypso_pcb_tpu_lock;      /* TPU registers + scenarios */

typedef struct CalypsoPcb CalypsoPcb;

/* === Public API ========================================================== */

/* Initialize PCB orchestrator: locks + IRQ routing table.
 * Called once during SoC init (from calypso_soc.c). */
CalypsoPcb *calypso_pcb_init(qemu_irq *inth_inputs);

/* No-op stubs kept for API compat with calypso_soc.c. */
void calypso_pcb_start_threads(CalypsoPcb *pcb);
void calypso_pcb_stop_threads(CalypsoPcb *pcb);

/* IRQ helper : raise/lower a Calypso IRQ. Thread-safe. */
void calypso_pcb_raise_irq(CalypsoPcb *pcb, int irq_nr);
void calypso_pcb_lower_irq(CalypsoPcb *pcb, int irq_nr);

/* === Async log queue =====================================================
 * For the high-frequency log sites (UART IER, TDMA tick, ...) that fire from
 * the ARM TCG main thread, where an inline fprintf stalls TCG on the stdio
 * lock plus the write syscall. This queue defers them to a dedicated drain
 * thread: TCG only enqueues under a short mutex and carries on. */
void calypso_async_log(const char *fmt, ...) __attribute__((format(printf,1,2)));

/* === DARAM access helpers (cross-thread safety) ==========================
 *
 * Wrappers for the sites outside c54x.c that read or write dsp->data[]: BSP IQ
 * burst writes, TRX/FBSB API RAM mailbox mirror, and so on. DSP-side accesses
 * are already locked by data_read/data_write in calypso_c54x.c; unlocked
 * external writes race with them as soon as the DSP runs in its own thread,
 * corrupting DARAM non-deterministically.
 *
 * Usage:
 *   - Single access: calypso_dsp_daram_read(dsp, addr) / _write(dsp, a, v)
 *     wrap lock + access + unlock (implemented in calypso_full_pcb.c).
 *   - Burst (loops of 100+ iterations): take the lock once with the
 *     pcb_daram_lock_* functions and access dsp->data[] directly in between.
 *
 * Cost: an uncontended qemu_mutex is ~20-30ns; at an estimated ~300k ops/sec
 * overall that is under 1% of wall time. */

/* Burst lock helpers for sections that access dsp->data[] in a loop: acquire
 * once, access directly in between, release once.
 *
 * IMPORTANT: never return or break out of the section without releasing. */
void calypso_pcb_daram_lock_acquire(void);
void calypso_pcb_daram_lock_release(void);

/* Single-access helpers wrapping lock + access + unlock.
 * dsp_void is a C54xState*, typed void* to avoid pulling calypso_c54x.h into
 * every .c that includes this header. */
uint16_t calypso_dsp_daram_read(void *dsp_void, uint16_t addr);
void     calypso_dsp_daram_write(void *dsp_void, uint16_t addr, uint16_t val);

#endif /* HW_ARM_CALYPSO_FULL_PCB_H */

4.61 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_gmsk.c

7869 octets, 174 lignes → 174 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * calypso_gmsk.c - GSM GMSK modulator (45.004): the bursts handed to the DSP,
 * synthetic cell (c54x_exe) and BTS bursts alike (calypso_bsp.c).
 *
 * Differential coding d_i = b_i XOR b_{i-1} (b_{-1} = 1), alpha_i = 1 - 2 d_i,
 * phase(t) = (pi/2) * sum_i alpha_i * q(t - iT), with q the integral of the
 * gaussian pulse BT = 0.3 truncated to 3T. Internally oversampled 16x, then one
 * sample per symbol is returned at iT + decalage*T.
 *
 * [2026-09-17] 148 zero bits give exactly +pi/2 per symbol, i.e. the FCCH: the
 * ROM FB detector locked on that waveform.
 */
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include "calypso_gmsk.h"

#define OS 16          /* internal oversampling factor */
#define L  3           /* pulse length, in symbols */
#define BT 0.3
#define GMSK_MAX_BITS 512   /* longest burst gmsk_moduler() accepts */

static double q_tab[L * OS + 1];   /* q(t) for t in [-1.5T, 1.5T], step T/OS */
static int q_pret;

static double Qf(double x) { return 0.5 * erfc(x / sqrt(2.0)); }

static void q_init(void)
{
    /* g(t) = (1/T) [ Q(2 pi BT (t - T/2) / (T sqrt(ln 2))) - Q(2 pi BT (t + T/2) / (T sqrt(ln 2))) ]
     * q(t) = integral of g from -inf to t, with q(-1.5T) = 0 and q(+1.5T) = 1 (T = 1). */
    double k = 2.0 * M_PI * BT / sqrt(log(2.0));
    double acc = 0.0, dt = 1.0 / OS;
    for (int i = 0; i <= L * OS; i++) {
        double t = -1.5 + i * dt;
        double g = Qf(k * (t - 0.5)) - Qf(k * (t + 0.5));
        q_tab[i] = acc;
        acc += g * dt;
    }
    /* normalise exactly to 1 */
    for (int i = 0; i <= L * OS; i++) q_tab[i] /= acc;
    q_pret = 1;
}

static double q_de(double t)   /* t in symbols, relative to the pulse centre */
{
    if (t <= -1.5) return 0.0;
    if (t >= 1.5) return 1.0;
    double x = (t + 1.5) * OS;
    int i = (int)x; double f = x - i;
    if (i >= L * OS) return 1.0;
    return q_tab[i] * (1 - f) + q_tab[i + 1] * f;
}

void gmsk_moduler(const uint8_t *bits, int n, int amp, double phase0, double decalage, int16_t *iq)
{
    /* [2026-09-20] CELLULE_MSK_DUR=1: the BSP's hard modulator (calypso_bsp.c
     * cos_tab/sin_tab, +-90 degrees per bit, samples on the phase points, full
     * scale), to test on the replay what the BTS bursts look like to the ROM. */
    static int dur = -1;
    if (dur < 0) { const char *e = getenv("CELLULE_MSK_DUR"); dur = (e && *e == '1') ? 1 : 0; }
    if (dur) {
        static const int16_t ct[4] = { 0x7FFE, 0, -0x7FFE, 0 }, st[4] = { 0, 0x7FFE, 0, -0x7FFE };
        int ph = 0;
        for (int i = 0; i < n; i++) { iq[2*i] = ct[ph]; iq[2*i+1] = st[ph]; ph = (ph + (bits[i] ? 3 : 1)) & 3; }
        (void)amp; (void)phase0; (void)decalage;
        return;
    }
    if (!q_pret) q_init();
    if (n > GMSK_MAX_BITS) n = GMSK_MAX_BITS;   /* alpha[] bound; callers pass 148 */
    int prev = 1;                       /* b_{-1} = 1 (45.004 2.2) */
    double alpha[GMSK_MAX_BITS];
    for (int i = 0; i < n; i++) {
        int d = (bits[i] & 1) ^ prev;
        prev = bits[i] & 1;
        alpha[i] = 1.0 - 2.0 * d;
    }
    for (int k = 0; k < n; k++) {
        double t = k + decalage;        /* sampling instant, in symbols */
        double ph = phase0;
        for (int i = 0; i < n; i++) {
            double dt = t - i;
            if (dt <= -1.5) break;      /* later symbols have not started contributing yet */
            ph += (M_PI / 2.0) * alpha[i] * q_de(dt);
        }
        iq[2 * k]     = (int16_t)lrint(amp * cos(ph));
        iq[2 * k + 1] = (int16_t)lrint(amp * sin(ph));
    }
}

void gmsk_elargir(int16_t *iq, int n, double a)
{
    if (a == 0.0 || n <= 0 || n > GMSK_MAX_BITS) return;
    double xi[GMSK_MAX_BITS], xq[GMSK_MAX_BITS];
    for (int k = 0; k < n; k++) { xi[k] = iq[2*k]; xq[k] = iq[2*k+1]; }
    for (int k = 0; k < n; k++) {
        double yi = xi[k], yq = xq[k];
        if (k > 0)     { yi += a * xi[k-1]; yq += a * xq[k-1]; }
        if (k < n - 1) { yi += a * xi[k+1]; yq += a * xq[k+1]; }
        yi /= (1 + 2 * a); yq /= (1 + 2 * a);   /* keep the peak amplitude */
        if (yi > 32767) yi = 32767;
        if (yi < -32768) yi = -32768;
        if (yq > 32767) yq = 32767;
        if (yq < -32768) yq = -32768;
        iq[2*k] = (int16_t)lrint(yi); iq[2*k+1] = (int16_t)lrint(yq);
    }
}

/* The receive chain, not a three-coefficient box.
 * gmsk_elargir() imitates the receive filter by y = (x[n-1] + x[n] + x[n+1])/3:
 * three equal coefficients, a channel much longer and more symmetric than what
 * a real Calypso sees (causal analog baseband filter, ~100 kHz). The data bits
 * bordering the training sequence (c38, c40) decide the ROM's channel estimate,
 * so the ISI at the edges matters. Here: exact GMSK BT=0.3 at OS samples per
 * symbol, 3rd-order Butterworth low-pass (bilinear), cutoff fc in kHz (symbol =
 * 270.833 kHz), then decimation at instant `decalage` of the symbol. The group
 * delay of the filter is left as is: the ROM realigns its TOA. */
#define OS_F 8
static void butter3(double fc_norm, double b[4], double a[4])
{
    /* analog 3rd-order Butterworth: (s+1)(s^2+s+1), bilinear, fs = 1, fc_norm = fc/fs */
    double wc = tan(M_PI * fc_norm);                 /* pre-warping */
    /* H(s) = 1 / ((s/wc + 1)((s/wc)^2 + s/wc + 1)); go through the s-domain coefficients */
    double k = wc;
    /* denominator in s: s^3 + 2 wc s^2 + 2 wc^2 s + wc^3; numerator: wc^3 */
    double A3 = 1, A2 = 2 * k, A1 = 2 * k * k, A0 = k * k * k, B0 = k * k * k;
    /* bilinear s = (1 - z^-1)/(1 + z^-1) (fs normalised to 2 in tan above) */
    double d0 = A3 + A2 + A1 + A0;
    a[0] = 1.0;
    a[1] = (-3 * A3 - A2 + A1 + 3 * A0) / d0;
    a[2] = (3 * A3 - A2 - A1 + 3 * A0) / d0;
    a[3] = (-A3 + A2 - A1 + A0) / d0;
    b[0] = B0 / d0; b[1] = 3 * B0 / d0; b[2] = 3 * B0 / d0; b[3] = B0 / d0;
}
void gmsk_moduler_filtre(const uint8_t *bits, int n, int amp, double phase0, double decalage, double fc_khz, int16_t *iq)
{
    if (!q_pret) q_init();
    if (n > GMSK_MAX_BITS) n = GMSK_MAX_BITS;
    int prev = 1;
    double alpha[GMSK_MAX_BITS];
    for (int i = 0; i < n; i++) { int d = (bits[i] & 1) ^ prev; prev = bits[i] & 1; alpha[i] = 1.0 - 2.0 * d; }
    /* oversampling: (n + 3) symbols of trajectory to let the filter settle and the tail out */
    int N = (n + 3) * OS_F;
    double *xi = malloc(sizeof(double) * N), *xq = malloc(sizeof(double) * N);
    if (!xi || !xq) { free(xi); free(xq); gmsk_moduler(bits, n, amp, phase0, decalage, iq); return; }
    for (int m = 0; m < N; m++) {
        double t = (double)m / OS_F - 1.5;            /* the first symbol starts at t = 0 */
        double ph = phase0;
        for (int i = 0; i < n; i++) {
            double dt = t - i;
            if (dt <= -1.5) break;
            ph += (M_PI / 2.0) * alpha[i] * q_de(dt);
        }
        xi[m] = cos(ph); xq[m] = sin(ph);
    }
    double b[4], a[4]; butter3(fc_khz / (270.833 * OS_F), b, a);
    double yi[4] = {0}, yq[4] = {0}, ui[4] = {0}, uq[4] = {0};
    for (int m = 0; m < N; m++) {
        ui[0] = xi[m]; uq[0] = xq[m];
        double oi = b[0]*ui[0] + b[1]*ui[1] + b[2]*ui[2] + b[3]*ui[3] - a[1]*yi[1] - a[2]*yi[2] - a[3]*yi[3];
        double oq = b[0]*uq[0] + b[1]*uq[1] + b[2]*uq[2] + b[3]*uq[3] - a[1]*yq[1] - a[2]*yq[2] - a[3]*yq[3];
        yi[0] = oi; yq[0] = oq;
        for (int j = 3; j > 0; j--) { ui[j] = ui[j-1]; uq[j] = uq[j-1]; yi[j] = yi[j-1]; yq[j] = yq[j-1]; }
        xi[m] = oi; xq[m] = oq;                       /* filter in place */
    }
    for (int k = 0; k < n; k++) {
        double t = k + decalage + 1.5;                /* same origin as the trajectory */
        int m = (int)lrint(t * OS_F); if (m < 0) m = 0; if (m >= N) m = N - 1;
        iq[2 * k]     = (int16_t)lrint(amp * xi[m]);
        iq[2 * k + 1] = (int16_t)lrint(amp * xq[m]);
    }
    free(xi); free(xq);
}

4.62 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_gmsk.h

1292 octets, 20 lignes → 20 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef CALYPSO_GMSK_H
#define CALYPSO_GMSK_H
#include <stdint.h>
/* Modulates n bits as GMSK BT=0.3 (3GPP 45.004), one complex sample per symbol,
 * interleaved int16 I,Q. phase0 = initial phase (rad); decalage = sampling
 * instant inside the symbol, as a fraction in [0,1[ (0.5 = centre). */
void gmsk_moduler(const uint8_t *bits, int n, int amp, double phase0, double decalage, int16_t *iq);
/* [2026-09-21] Symmetric widening of a 1-sample/symbol burst, in place:
 * y[n] = (x[n] + a (x[n-1] + x[n+1])) / (1 + 2a), n = number of complex samples.
 * Why: the ROM's NB equaliser zeroes every channel tap whose energy is below
 * 1/16 of the window energy (PROM0 0x7f0c-0x7f1c, i.e. 25 % in amplitude);
 * the +-1 taps of a plain GMSK at 1 sps sit exactly on that threshold and the
 * decision flips with the data. a = 0.3 puts them near 55 %. A receiver's
 * channel filter does the same on silicon. Measured: SI1-4 decode with it,
 * ~40 % of the bursts without. */
void gmsk_elargir(int16_t *iq, int n, double a);
/* oversampled GMSK + 3rd-order Butterworth low-pass (fc in kHz) + decimation at `decalage` */
void gmsk_moduler_filtre(const uint8_t *bits, int n, int amp, double phase0, double decalage, double fc_khz, int16_t *iq);
#endif

4.63 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_l1_dsp_register.c

1160 octets, 31 lignes → 31 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * Registers the C54x layer 1 with the platform.
 *
 * [2026-09-16] State: the DSP core is ported and linked, but the seam is not
 * wired yet. The vtable entries stay NULL until calypso_dsp_read(),
 * calypso_dsp_write() and calypso_tdma_tick() are extracted from qosmo-dsp's
 * calypso_trx.c, where they are ~1300 lines entangled with that fork's
 * CalypsoTRX struct and its dsp / dsp_ram fields, which the common platform
 * does not have.
 *
 * Registering with the name alone is deliberate: "couche 1 : c54x" under
 * -M help states what is actually LINKED, and the base test checks that
 * string. Skipping the registration would print "aucune" and make a
 * half-finished build look like a bare base.
 */
#include "qemu/osdep.h"
#include "qemu/module.h"
#include "hw/arm/calypso/calypso_l1_ops.h"

static const CalypsoL1Ops c54x_ops = {
    .name = "c54x",
    /* .init, .frame_tick, .api_read_override, .api_write_observed, ...:
     * see the header comment above. */
};

static void calypso_l1_dsp_register_type(void)
{
    calypso_l1_register(&c54x_ops);
}
type_init(calypso_l1_dsp_register_type)

4.64 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_mailbox.c

13925 octets, 388 lignes → 388 lignes

/*
 * calypso_mailbox.c - full ARM <-> DSP mailbox monitor.
 *
 * See calypso_mailbox.h for the rationale. Configuration:
 *
 *   CALYPSO_MAILBOX=1|all     everything crossing the mailbox window
 *                             (0x0800..0x0FFF)
 *   CALYPSO_MAILBOX=w         writes only (both directions)
 *   CALYPSO_MAILBOX=r         reads only
 *   CALYPSO_MAILBOX_CELLS=0x098b,0x43d8,...   ADDS cells outside the window,
 *                             watched in both directions and on both sides
 *   CALYPSO_MAILBOX_RANGES=lo-hi,...          ADDS address ranges, bounds
 *                             included
 *   CALYPSO_MAILBOX_ONLY=1    trace ONLY the cells and ranges listed above
 *   CALYPSO_MAILBOX_BRUT=1    disable folding, log every event
 *   CALYPSO_MAILBOX_FILE=...  defaults to $LOG_DIR/mailbox.log, else
 *                             /tmp/calypso/logs/mailbox.log
 *   CALYPSO_MAILBOX_MAX=N     safety cap (default 5 000 000 lines, 0 = none)
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#include "calypso_mailbox.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "hw/arm/calypso/calypso_debug.h"

int calypso_mbx_actif = -1;   /* -1 = initialise on first access */
const char *calypso_mbx_suffixe = "";

static FILE    *g_f;
static int      g_init;
static int      g_ecr = 1, g_lec = 1;      /* writes / reads */
static int      g_only;                    /* 1 = _CELLS/_RANGES only */
static uint16_t g_cells[32];
static int      g_ncells;
/* Arbitrary RANGES: the API window alone left whole regions of the model out of
 * reach (the correlation reference at 0x2cea.., the burst buffer at 0x2a00..,
 * the scratch-pad at 0x0060..), and 32 single cells cannot cover a 128-word
 * region. */
static struct { uint16_t lo, hi; } g_ranges[16];
static int      g_nranges;
static unsigned long g_n, g_max = 5000000UL;

/* PER-CELL folding: log on CHANGE, not on non-consecutive repeats.
 *
 * Folding identical CONSECUTIVE events folds nothing here: the DSP background
 * loop polls two cells in alternation (0xde86 ld *(0x098c) then 0xddfd ld
 * *(0x098d)), so no two successive lines are ever identical. Measured that way:
 * 291 MB and 3 690 446 lines in a few minutes, and QEMU killed.
 *
 * Per cell is the right granularity: a read returning the SAME value at the
 * SAME PC carries no new information, even with other accesses interleaved. So
 * keep, per word, the last logged value and context, and emit only on change,
 * summarising the run just closed as "x N". CALYPSO_MAILBOX_BRUT=1 turns all
 * folding off. */
static int       g_brut;
static uint16_t *g_dval;      /* last logged value, per word */
static uint32_t *g_dctx;      /* last logged context, per word */
static uint32_t *g_drep;      /* length of the run in progress, per word */
static uint8_t  *g_dvu;       /* has this word been logged at all? */
static uint8_t  *g_dsens;     /* direction of the last logged event */

/* Names for the cells that keep coming up in diagnosis: a readable trace saves
 * looking up "0x08fa, what was that again" on every read. */
static const char *mbx_nom(uint16_t m)
{
    switch (m) {
    case 0x0804: return "d_task_md/wp0";
    case 0x0818: return "d_task_md/wp1";
    case 0x0828: return "d_task_d/rp0";
    case 0x0829: return "d_burst_d/rp0";
    case 0x083C: return "d_task_d/rp1";
    case 0x083D: return "d_burst_d/rp1";
    case 0x0810: return "d_ctrl_system";
    /* NDB+0 = 0x08D4 = d_dsp_page; NDB+14 = 0x08E2 = d_dsp_state, where the
     * ARM writes 3 = C_DSP_IDLE3 from dsp.c:215. See calypso_fbsb.h. */
    case 0x08D4: return "d_dsp_page";
    case 0x08D5: return "d_error_status";
    case 0x08E2: return "d_dsp_state";
    case 0x08F8: return "d_fb_det";
    case 0x08F9: return "d_fb_mode";
    case 0x08FA: return "a_sync[TOA]";
    case 0x08FB: return "a_sync[PM]";
    case 0x08FC: return "a_sync[ANGLE]";
    case 0x08FD: return "a_sync[SNR]";
    case 0x098A: return "d_backgnd_en";
    case 0x098B: return "d_backgnd_?b";
    case 0x098C: return "d_backgnd_st";
    case 0x098D: return "d_backgnd_?d";
    case 0x098E: return "tab_handlers";
    case 0x0FFF: return "d_task_word";
    case 0x43D8: return "slot_handler";
    default:     return "";
    }
}

static void mbx_ecrire(CalypsoMbxSens sens, uint16_t mot, uint16_t val,
                       uint16_t avant, uint32_t ctx, uint32_t fn, uint32_t insn,
                       unsigned long rep);

void calypso_mbx_init(void)
{
    const char *e;

    if (g_init) {
        return;
    }
    g_init = 1;

    e = calypso_getenv("CALYPSO_MAILBOX");
    if (!e || !*e || !strcmp(e, "0")) {
        calypso_mbx_actif = 0;        /* off: zero cost on the hot paths */
        return;
    }
    if (!strcmp(e, "w") || !strcmp(e, "W")) {
        g_lec = 0;
    } else if (!strcmp(e, "r") || !strcmp(e, "R")) {
        g_ecr = 0;
    }

    e = calypso_getenv("CALYPSO_MAILBOX_CELLS");
    if (e && *e) {
        const char *p = e;
        while (*p && g_ncells < 32) {
            char *fin = NULL;
            long v;
            while (*p == ',' || *p == ' ') {
                p++;
            }
            if (!*p) {
                break;
            }
            v = strtol(p, &fin, 0);
            if (fin == p) {
                break;                /* nothing parseable: stop here */
            }
            g_cells[g_ncells++] = (uint16_t)v;
            p = fin;
        }
    }
    /* CALYPSO_MAILBOX_RANGES=lo-hi,lo-hi,... , bounds INCLUDED. Same tolerant
     * parse as _CELLS: stop at the first unreadable token rather than guess.
     * Bounds are swapped back into order when needed. */
    e = calypso_getenv("CALYPSO_MAILBOX_RANGES");
    if (e && *e) {
        const char *p = e;
        while (*p && g_nranges < 16) {
            char *fin = NULL;
            long lo, hi;
            while (*p == ',' || *p == ' ') {
                p++;
            }
            if (!*p) {
                break;
            }
            lo = strtol(p, &fin, 0);
            if (fin == p) {
                break;
            }
            p = fin;
            if (*p == '-' || *p == ':') {
                p++;
                hi = strtol(p, &fin, 0);
                if (fin == p) {
                    break;
                }
                p = fin;
            } else {
                hi = lo;              /* a lone bound = a single cell */
            }
            if (lo > hi) { long t = lo; lo = hi; hi = t; }
            g_ranges[g_nranges].lo = (uint16_t)lo;
            g_ranges[g_nranges].hi = (uint16_t)hi;
            g_nranges++;
        }
    }

    e = calypso_getenv("CALYPSO_MAILBOX_ONLY");
    g_only = (e && *e == '1');

    e = calypso_getenv("CALYPSO_MAILBOX_BRUT");
    g_brut = (e && *e == '1');

    e = calypso_getenv("CALYPSO_MAILBOX_MAX");
    if (e && *e) {
        g_max = strtoul(e, NULL, 0);
    }

    e = calypso_getenv("CALYPSO_MAILBOX_FILE");
    if (!e || !*e) {
        static char def[512];
        /* The repo exports LOG_DIR (paths.env). CALYPSO_LOG_DIR is accepted as
         * a fallback, but LOG_DIR wins. */
        const char *d = calypso_getenv("LOG_DIR");
        if (!d || !*d) d = calypso_getenv("CALYPSO_LOG_DIR");
        snprintf(def, sizeof(def), "%s/mailbox.log",
                 (d && *d) ? d : "/tmp/calypso/logs");
        e = def;
    }
    if (calypso_mbx_suffixe && *calypso_mbx_suffixe) {
        /* mailbox.log -> mailbox-arm.log; without ".log": suffix appended */
        static char avec[600];
        size_t n = strlen(e);
        if (n > 4 && !strcmp(e + n - 4, ".log")) {
            snprintf(avec, sizeof(avec), "%.*s-%s.log", (int)(n - 4), e, calypso_mbx_suffixe);
        } else {
            snprintf(avec, sizeof(avec), "%s-%s", e, calypso_mbx_suffixe);
        }
        e = avec;
    }
    g_f = fopen(e, "w");
    if (!g_f) {
        fprintf(stderr, "[mbx] ouverture impossible : %s — moniteur INACTIF\n", e);
        calypso_mbx_actif = 0;
        return;
    }
    /* FULL buffering, and wide. Under _IOLBF every line was a syscall, and
     * 3.7 M write() calls were enough to bring QEMU to its knees. Flush
     * periodically (see mbx_ecrire) so an abrupt exit loses one window at
     * most. */
    setvbuf(g_f, NULL, _IOFBF, 1 << 20);

    g_dval = calloc(0x10000, sizeof(*g_dval));
    g_dctx = calloc(0x10000, sizeof(*g_dctx));
    g_drep = calloc(0x10000, sizeof(*g_drep));
    g_dvu  = calloc(0x10000, sizeof(*g_dvu));
    g_dsens = calloc(0x10000, sizeof(*g_dsens));
    if (!g_dval || !g_dctx || !g_drep || !g_dvu || !g_dsens) {
        fprintf(stderr, "[mbx] allocation impossible — moniteur INACTIF\n");
        calypso_mbx_actif = 0;
        return;
    }

    fprintf(g_f,
        "# moniteur mailbox ARM<->DSP — un evenement par ligne, format stable\n"
        "# sens : ARM>WR (commande)  ARM<RD (resultat)  DSP>WR (resultat)  DSP<RD (commande)\n"
        "# %-10s %-8s %-6s %-6s %-14s %s\n",
        "insn", "fn", "sens", "mot", "nom", "valeur");
    fflush(g_f);

    fprintf(stderr, "[mbx] moniteur mailbox ACTIF -> %s "
            "(ecr=%d lec=%d cellules_sup=%d only=%d max=%lu)\n",
            e, g_ecr, g_lec, g_ncells, g_only, g_max);

    /* Announce what is ACTUALLY covered: without this there is no telling
     * "the cell never moved" from "the cell was never watched". */
    {
        int i;
        fprintf(stderr, "[mailbox] couverture : %s",
                g_only ? "SEULEMENT les cellules et plages listees"
                       : "fenetre API 0x0800-0x0FFF");
        for (i = 0; i < g_ncells; i++) {
            fprintf(stderr, " + 0x%04x", g_cells[i]);
        }
        for (i = 0; i < g_nranges; i++) {
            fprintf(stderr, " + [0x%04x-0x%04x, %u mots]",
                    g_ranges[i].lo, g_ranges[i].hi,
                    (unsigned)(g_ranges[i].hi - g_ranges[i].lo + 1));
        }
        fprintf(stderr, " | sens : %s%s | garde-fou : %lu ligne(s)%s\n",
                g_ecr ? "ecritures " : "", g_lec ? "lectures" : "",
                g_max, g_max ? "" : " (illimite)");
    }
    calypso_mbx_actif = 1;
}

/* In the mailbox window, or in the extra cell/range list? */
static int mbx_retenu(uint16_t mot)
{
    int i;

    for (i = 0; i < g_ncells; i++) {
        if (g_cells[i] == mot) {
            return 1;
        }
    }
    for (i = 0; i < g_nranges; i++) {
        if (mot >= g_ranges[i].lo && mot <= g_ranges[i].hi) {
            return 1;
        }
    }
    if (g_only) {
        return 0;
    }
    return (mot >= 0x0800 && mot <= 0x0FFF);
}

void calypso_mbx_evt(CalypsoMbxSens sens, uint16_t mot, uint16_t val,
                     uint16_t avant, uint32_t ctx, uint32_t fn, uint32_t insn)
{
    int est_ecr;

    if (!g_init) {
        calypso_mbx_init();
    }
    if (!g_f) {
        return;
    }

    est_ecr = (sens == MBX_ARM_WR || sens == MBX_DSP_WR);
    if ((est_ecr && !g_ecr) || (!est_ecr && !g_lec)) {
        return;
    }
    if (!mbx_retenu(mot)) {
        return;
    }

    if (g_max && g_n >= g_max) {
        if (g_n == g_max) {
            g_n++;
            fprintf(g_f, "# PLAFOND %lu lignes atteint — trace interrompue. "
                         "CALYPSO_MAILBOX_MAX=0 pour l'enlever.\n", g_max);
            fflush(g_f);
        }
        return;
    }
    g_n++;

    if (g_brut) {
        mbx_ecrire(sens, mot, val, avant, ctx, fn, insn, 0);
        return;
    }

    /* A write that CHANGES the value is always an event: it passes
     * unconditionally and becomes the new reference.
     *
     * Writes that change nothing do not: measured on a native run, 87 174 of
     * 92 150 lines were the DSP rewriting 0x0000 -> 0x0000 from the same PC
     * (@0xb446), once per frame. A write with no effect from the same PC
     * carries no more information than a read, so both follow the same rule. */
    if (est_ecr && avant != val) {
        if (g_drep[mot] > 1) {
            mbx_ecrire(g_dsens[mot], mot, g_dval[mot], g_dval[mot], g_dctx[mot],
                       fn, insn, g_drep[mot] - 1);
        }
        g_drep[mot] = 0;
        mbx_ecrire(sens, mot, val, avant, ctx, fn, insn, 0);
        g_dval[mot]  = val;
        g_dctx[mot]  = ctx;
        g_dsens[mot] = sens;
        g_dvu[mot]   = 1;
        return;
    }

    /* Otherwise - a read, or a write with no effect: nothing new if both the
     * value and the context match. Just count it. */
    if (g_dvu[mot] && g_dval[mot] == val && g_dctx[mot] == ctx) {
        g_drep[mot]++;
        g_n--;                        /* does not count against the cap */
        return;
    }

    /* Change: close the previous run with a summary, then log. */
    if (g_drep[mot] > 1) {
        mbx_ecrire(g_dsens[mot], mot, g_dval[mot], g_dval[mot], g_dctx[mot],
                   fn, insn, g_drep[mot] - 1);
    }
    g_drep[mot]  = 1;
    g_dval[mot]  = val;
    g_dctx[mot]  = ctx;
    g_dsens[mot] = sens;
    g_dvu[mot]   = 1;
    mbx_ecrire(sens, mot, val, avant, ctx, fn, insn, 0);
}

static void mbx_ecrire(CalypsoMbxSens sens, uint16_t mot, uint16_t val,
                       uint16_t avant, uint32_t ctx, uint32_t fn, uint32_t insn,
                       unsigned long rep)
{
    static const char *nom_sens[] = { "ARM>WR", "ARM<RD", "DSP>WR", "DSP<RD" };
    char suffixe[32] = "";

    if (rep) {
        snprintf(suffixe, sizeof(suffixe), "  x%lu", rep + 1);
    }
    if ((g_n & 0x3FF) == 0) {
        fflush(g_f);                  /* at worst a 1024-line window */
    }
    if (sens == MBX_ARM_WR || sens == MBX_DSP_WR) {
        fprintf(g_f, "%-12u %-8u %-6s 0x%04x %-14s 0x%04x -> 0x%04x  @0x%04x%s\n",
                insn, fn, nom_sens[sens], mot, mbx_nom(mot), avant, val, ctx,
                suffixe);
    } else {
        fprintf(g_f, "%-12u %-8u %-6s 0x%04x %-14s = 0x%04x            @0x%04x%s\n",
                insn, fn, nom_sens[sens], mot, mbx_nom(mot), val, ctx, suffixe);
    }
}

4.65 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_mailbox.h

3121 octets, 71 lignes → 71 lignes

/*
 * calypso_mailbox.h - full monitor of the ARM <-> DSP mailbox.
 *
 * The mailbox (API RAM, DSP words 0x0800..0x0FFF) is the only contact point
 * between the ARM and the c54x: which task is commanded, who writes d_burst_d,
 * which cell the dispatcher polls, are all events on that boundary. Point
 * probes wired to one address or one value report zero when they miss, and
 * zero reads like an answer; this module traces the whole flow, both
 * directions, all the time.
 *
 * Output goes to its own file, not stderr: a full stream on stderr drowns
 * qemu.log and truncates the neighbouring probes (measured once at 480898
 * lines / 35 MB with the surrounding traces lost). The two journals stay
 * independent.
 *
 * The record format is uniform so two runs are diffable: capture a sequence
 * under `shunt_legit`, which works and therefore exercises the real command
 * sequence, then capture it natively; the delta is the list of what is not
 * wired yet.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#ifndef CALYPSO_MAILBOX_H
#define CALYPSO_MAILBOX_H

#include <stdint.h>

/* Access direction, seen from the mailbox. */
typedef enum {
    MBX_ARM_WR = 0,   /* ARM writes  (command) */
    MBX_ARM_RD,       /* ARM reads   (result)  */
    MBX_DSP_WR,       /* DSP writes  (result)  */
    MBX_DSP_RD,       /* DSP reads   (command) */
} CalypsoMbxSens;

/* Tested inline on the hot paths (the c54x data_read / data_write run on every
 * instruction): with the monitor off the cost is one int load.
 *
 * Three states are required: -1 = not initialised, 0 = off, 1 = on. Init is
 * lazy, so a flag starting at 0 would keep the inline wrapper from ever calling
 * calypso_mbx_evt(), calypso_mbx_init() would never run, the flag would stay 0
 * and no file would be created. With -1 the first access goes through, init
 * decides, and later accesses are filtered. */
extern int calypso_mbx_actif;

/* Two processes each hold one end of the mailbox: c54x_exe (DSP) and QEMU
 * (ARM, API window of calypso_trx.c). The side that sets this suffix BEFORE
 * the first event writes to "<file>-<suffix>.log" instead of "<file>.log", so
 * as not to overwrite the other. Empty by default (DSP). */
extern const char *calypso_mbx_suffixe;

/* Opens the file and reads the configuration. Idempotent, called lazily on the
 * first event, so there is nothing to schedule at startup. */
void calypso_mbx_init(void);

/* Records one event. `avant` is meaningful for writes only (0 otherwise).
 * `ctx` is the DSP PC, or the MMIO offset on the ARM side. */
void calypso_mbx_evt(CalypsoMbxSens sens, uint16_t mot, uint16_t val,
                     uint16_t avant, uint32_t ctx, uint32_t fn, uint32_t insn);

/* Inline wrapper: the flag test avoids the call when the monitor is off. */
static inline void calypso_mbx(CalypsoMbxSens sens, uint16_t mot, uint16_t val,
                               uint16_t avant, uint32_t ctx, uint32_t fn,
                               uint32_t insn)
{
    if (calypso_mbx_actif) {
        calypso_mbx_evt(sens, mot, val, avant, ctx, fn, insn);
    }
}

#endif /* CALYPSO_MAILBOX_H */

4.66 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_rhea_dma.c

41427 octets, 837 lignes → 837 lignes

/*
 * calypso_rhea_dma.c - Calypso RHEA DMA controller, MCU side (FFFF:FC00).
 *
 * WHY THIS FILE EXISTS
 * --------------------
 * Measured: the DSP firmware drives the RIF correctly (the two-write RRST
 * sequence of §12.6, SPCR read-back, reaction on a non-empty FIFO) but keeps
 * both RINT_MASK and RDMA_MASK at 1 for the whole run and never reads DRR. It
 * sees that data is there and does not take it through the serial port, so the
 * data must arrive in memory - §3.7.1, "API interface for radio data in DMA
 * mode (buffered mode with data block transfer)".
 *
 * [2026-08-03, run native_twl] The ARM writes exactly one thing in this window:
 *     ALLOC_CONFIG <- 0x000C  ->  channel1=DSP channel2=DSP channel3/4=ARM
 * It hands both RIF channels to the DSP and keeps the two UART channels.
 * Crossed with §6 Table 2 (channels 0..3: 0=RIF_DMA_REQ_X, 1=RIF_DMA_REQ_R,
 * 2/3=UARTs) and §11 (registers DMA1..DMA4), the consistent reading is
 * DMA1=RIF TX, DMA2=RIF RX, DMA3/DMA4=UARTs - §6 numbers from 0, §11 from 1.
 * The alternative (DMA1=RIF RX, DMA2=UART) would hand a UART channel to the
 * DSP, which makes no sense for this firmware; it stays possible and is settled
 * by the RAD/AAD values actually written.
 *
 * Consequence (§11.3): "The DMA transfer configuration registers are connected
 * to either the ARM Rhea bus OR TO THE DSP RHEA BUS regarding the corresponding
 * DMA_ALLOC flag." The ARM therefore never writes DMA1_AAD: the registers of
 * the handed-over channels live at XIO:FC10 / XIO:FC20, on the DSP side. Hence
 * the calypso_rhea_dma_xio() bridge below - same register bank, second bus.
 *
 * WHAT THIS MODULE DOES
 * Stores and returns the §11 registers with their reset values, and logs every
 * decoded write. DMA_START itself runs no transfer: the RX transfer happens in
 * calypso_rhea_dma_rx_request() when a burst reaches the RIF (see the block
 * above that function).
 *
 * MAP (§11.1, Table 18) - 0x100 window from FFFF:FC00:
 *   +0x00 CONTROLLER_CONFIG   6b R/W   DMA_BURST(4:2)=1, PRIORITY_ENABLE(5)=1
 *   +0x02 ALLOC_CONFIG        4b R/W   reset 1111 = all 4 channels owned by ARM
 *   +0x10 DMA1_RAD           16b R/W   RHEA_START(10:0) + RHEA_CS(15:11)
 *   +0x12 DMA1_RDPTH         11b R/W   Rhea buffer depth, in BYTES
 *   +0x14 DMA1_AAD           12b R/W   start of the RX buffer in the API,
 *                                      "The address is always expressed in Bytes"
 *   +0x16 DMA1_ALGTH         12b R/W   API page length, in bytes
 *   +0x18 DMA1_CTRL          13b       reset 0x04A2 (see below)
 *   +0x1A DMA1_CUR_OFFSET_API 12b R    current offset
 *   ... channels 2/3/4 at +0x20 / +0x30 / +0x40.
 *
 * DMAn_CTRL (§11.3.5) - the documented reset string "0 0100 1?10 0010" resolves
 * field by field to 0x04A2, which validates the field list:
 *   0 ENABLE=0 - 1 IDLE=1 (R) - 2 ONE_SHOT=0 - 3 FIFO_MODE=0 - 4 CURRENT_PAGE=0
 *   5 MAS=1 (16-bit transfers) - 6 DMA_START (W, always reads back 0)
 *   7 IRQ_MODE=1 - 8 IRQ_STATE=0 (R) - 9 RHEA_ERROR=0 (R)
 *   10 DIRECTION=1 -> "Transactions are done on Rhea -> API" - 12:11 PRIORITY=00
 * The "?" in the doc is DMA_START, which always reads back zero. DIRECTION=1 at
 * reset confirms the expected direction for radio reception: peripheral -> API.
 *
 * Doc inconsistency, flagged rather than hidden: Table 18 gives
 * CONTROLLER_CONFIG reset as "11 111?" while the §11.2.1 field list gives
 * DMA_BURST=0x1 and PRIORITY_ENABLE=1, i.e. 0b100100 = 0x24. The field list is
 * the more precise of the two and is what CTRLCFG_RESET follows.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#include "qemu/osdep.h"
#include "hw/arm/calypso/calypso_debug.h"
#include "calypso_rhea_dma.h"
#include "calypso_rif.h"
#include "calypso_c54x.h"
#include "calypso_bsp.h"

#include <stdio.h>
#include <stdlib.h>

#define RD_CTRL_CFG   0x00
#define RD_ALLOC_CFG  0x02
#define RD_CH_BASE(n) (0x10 + 0x10 * (n))   /* n = 0..3 -> 0x10 0x20 0x30 0x40 */
#define RD_RAD        0x0
#define RD_RDPTH      0x2
#define RD_AAD        0x4
#define RD_ALGTH      0x6
#define RD_CTRL       0x8
#define RD_CUR_OFF    0xA

#define CTRL_RESET    0x04A2
#define CTRLCFG_RESET 0x0024
#define ALLOC_RESET   0x000F

/* DMAn_CTRL fields */
#define CTRL_ENABLE       (1u << 0)
#define CTRL_IDLE         (1u << 1)
#define CTRL_ONE_SHOT     (1u << 2)
#define CTRL_FIFO_MODE    (1u << 3)
#define CTRL_CURRENT_PAGE (1u << 4)
#define CTRL_MAS          (1u << 5)
#define CTRL_DMA_START    (1u << 6)
#define CTRL_IRQ_MODE     (1u << 7)
#define CTRL_IRQ_STATE    (1u << 8)
#define CTRL_RHEA_ERROR   (1u << 9)
#define CTRL_DIRECTION    (1u << 10)

/* Read-only bits: kept by the model, never overwritten by the ARM. */
#define CTRL_RO_MASK  (CTRL_IDLE | CTRL_IRQ_STATE | CTRL_RHEA_ERROR)

/* HOW MANY TRANSFERS COMPLETE, HOW MANY THE ROM ACKNOWLEDGES.
 * The ROM closes a transfer by READING DMAn_CTRL (IRQ_STATE clears on read,
 * CAL207 §11.3.5). If it reads less often than the DMA completes, its request
 * queues -- 14 circular entries, 0xaa83/0xaaad/0xaad1 -- overflow: that is a
 * permanent `DSP Error Status: 24` (DMA_PROG|DMA_TASK). */
static unsigned long long g_rd_fini, g_rd_acquitte;

static struct {
    bool     init;
    uint16_t ctrl_cfg, alloc_cfg;
    struct { uint16_t rad, rdpth, aad, algth, ctrl, cur_off; } ch[4];
    unsigned n_wr, n_rd, n_start;
} rd;
static C54xState *rd_dsp;   /* the DSP served by the last request (for pumps) */
/* [2026-09-20] TOA ORIGIN. On silicon the FB search starts consuming the sample
 * stream at a TPU-timed instant inside the frame, and the firmware's constant
 * 23 (prim_fbsb.c "toa -= 23") encodes where a burst aligned on the frame then
 * lands. The bench has no TPU: the DSP arms DMA2 at the frame interrupt and the
 * frame's samples follow, so a burst at frame position 0 read TOA = 144. The
 * gap is closed HERE, where the timing lives: when the channel is armed in
 * continuous mode, the first CALYPSO_RHEA_DMA_ARM_SKIP words of the stream
 * (default 242 = 121 symbols = 144 - 23) are consumed without being stored. */
static int rd_skip_pending;
static int arm_skip_words(void)
{
    static int v = -1;
    /* Default 0 since the stream carries whole 1250-symbol frames: measured
     * TOA = 23 + 9 x 1250 + 7 for an FCCH nine frames after the command, i.e.
     * the ROM's own origin already matches the firmware's 23. (242 was the
     * calibration for the earlier 156-symbol frames.) */
    if (v < 0) { const char *e = calypso_getenv("CALYPSO_RHEA_DMA_ARM_SKIP"); v = (e && *e) ? atoi(e) : 0; if (v < 0) v = 0; }
    return v;
}

static bool rhea_dma_on(void)
{
    static int on = -1;
    if (on < 0) {
        on = calypso_gate("CALYPSO_RHEA_DMA", 1);
        if (!on)
            fprintf(stderr, "[rhea-dma] CALYPSO_RHEA_DMA=0 : retour au stub muet "
                    "(lectures a 0, ecritures jetees) — comportement d'avant le 03/08\n");
    }
    return on != 0;
}

static void rhea_dma_init(void)
{
    if (rd.init)
        return;
    rd.init = true;
    rd.ctrl_cfg  = CTRLCFG_RESET;
    rd.alloc_cfg = ALLOC_RESET;
    for (int i = 0; i < 4; i++)
        rd.ch[i].ctrl = CTRL_RESET;
    fprintf(stderr, "[rhea-dma] controleur DMA RHEA arme (CAL207 §11) @0xFFFFFC00 : "
            "4 canaux, reset CTRL=0x%04x (DIRECTION=1 Rhea->API, MAS=1 16 bits, "
            "IRQ_MODE=1, ENABLE=0). Canaux RIF : DMA1=TX DMA2=RX (§6 Table 2). "
            "ENREGISTREMENT SEUL : aucun transfert n'est execute.\n", CTRL_RESET);
}

/* Translates the API address (in BYTES, §11.3.3) into both useful frames. */
static void log_aad(int n, uint16_t aad)
{
    uint32_t byte = aad & 0x0FFF;
    fprintf(stderr, "[rhea-dma] *** DMA%d_AAD = 0x%03x octets  ->  mot DSP 0x%04x  "
            "(= ARM 0x%08x). C'est la DESTINATION du tampon de reception dans la "
            "memoire API.\n",
            n + 1, byte, (unsigned)(0x0800 + byte / 2), 0xFFD00000u + byte);
}

static void log_ctrl(int n, uint16_t v)
{
    fprintf(stderr, "[rhea-dma] DMA%d_CTRL <- 0x%04x : ENABLE=%d ONE_SHOT=%d "
            "FIFO_MODE=%d PAGE=%d MAS=%d(%s) DMA_START=%d IRQ_MODE=%d DIRECTION=%d(%s) "
            "PRIORITY=%d PC=0x%04x\n",
            n + 1, v,
            !!(v & CTRL_ENABLE), !!(v & CTRL_ONE_SHOT), !!(v & CTRL_FIFO_MODE),
            !!(v & CTRL_CURRENT_PAGE), !!(v & CTRL_MAS),
            (v & CTRL_MAS) ? "16b" : "8b",
            !!(v & CTRL_DMA_START), !!(v & CTRL_IRQ_MODE),
            !!(v & CTRL_DIRECTION),
            (v & CTRL_DIRECTION) ? "Rhea->API" : "API->Rhea",
            (v >> 11) & 3, rd_dsp ? (rd_dsp->pc & 0xffff) : 0);
}

uint64_t calypso_rhea_dma_read(void *opaque, hwaddr off, unsigned size)
{
    (void)opaque; (void)size;
    if (!rhea_dma_on())
        return 0;
    rhea_dma_init();

    uint16_t v = 0;
    if (off == RD_CTRL_CFG)       v = rd.ctrl_cfg;
    else if (off == RD_ALLOC_CFG) v = rd.alloc_cfg;
    else {
        for (int n = 0; n < 4; n++) {
            hwaddr b = RD_CH_BASE(n);
            if (off < b || off > b + RD_CUR_OFF)
                continue;
            switch (off - b) {
            case RD_RAD:     v = rd.ch[n].rad;     break;
            case RD_RDPTH:   v = rd.ch[n].rdpth;   break;
            case RD_AAD:     v = rd.ch[n].aad;     break;
            case RD_ALGTH:   v = rd.ch[n].algth;   break;
            /* DMA_START: "Reading of this bit is always equal to zero" (§11.3.5) */
            case RD_CTRL:
                v = rd.ch[n].ctrl & (uint16_t)~CTRL_DMA_START;
                /* ═════════════════════════════════════════════════════════════
                 * IRQ_STATE and RHEA_ERROR are cleared on read, CAL207 §11.3.5
                 * for both bits: "0 = cleared after being read". Leaving them
                 * sticky makes the firmware re-read them forever as if the event
                 * kept happening.
                 * ═════════════════════════════════════════════════════════════ */
                if (rd.ch[n].ctrl & (CTRL_IRQ_STATE | CTRL_RHEA_ERROR)) {
                    static unsigned long long n_clr;
                    if (rd.ch[n].ctrl & CTRL_IRQ_STATE) {
                        g_rd_acquitte++;
                        if ((g_rd_acquitte % 200) == 1) {
                            /* VERBOSITY LEVEL. verbosite.c classifies by
                             * keywords: this line only contains "DMA", hence
                             * level 3, invisible with the default -v. Rather
                             * than forcing an "ERR" on a healthy line (it would
                             * come out at level 0 on every normal run), ONLY the
                             * anomaly is promoted: acknowledgement lag = WARN,
                             * hence level 1. */
                            double pc = g_rd_fini ? 100.0 * g_rd_acquitte / g_rd_fini : 0.0;
                            bool mauvais = g_rd_fini > 100 && pc < 90.0;
                            fprintf(stderr, "[rhea-dma] %sbilan : %llu transferts finis, "
                                    "%llu acquittes par la ROM (%.0f%%)\n",
                                    mauvais ? "WARN " : "", g_rd_fini, g_rd_acquitte, pc);
                        }
                    }
                    /* Cap of 4000 lines with the counters attached: the ratio
                     * "completed / acknowledged transfers" tells whether the
                     * ROM's queues overflow. */
                    if (n_clr++ < 4000)
                        fprintf(stderr, "[rhea-dma] DMA%d_CTRL lu = 0x%04x : acquitte "
                                "(finis=%llu acquittes=%llu)\n",
                                n + 1, v, g_rd_fini, g_rd_acquitte);
                    rd.ch[n].ctrl &= (uint16_t)~(CTRL_IRQ_STATE | CTRL_RHEA_ERROR);
                }
                break;
            case RD_CUR_OFF: v = rd.ch[n].cur_off; break;
            default: break;
            }
            break;
        }
    }
    if (rd.n_rd++ < 40)
        fprintf(stderr, "[rhea-dma] RD  +0x%02x = 0x%04x\n", (unsigned)off, v);
    return v;
}

void calypso_rhea_dma_write(void *opaque, hwaddr off, uint64_t val, unsigned size)
{
    (void)opaque; (void)size;
    if (!rhea_dma_on())
        return;
    rhea_dma_init();

    uint16_t v = (uint16_t)val;
    rd.n_wr++;

    if (off == RD_CTRL_CFG) {
        rd.ctrl_cfg = v & 0x003F;
        fprintf(stderr, "[rhea-dma] CONTROLLER_CONFIG <- 0x%04x (DMA_BURST=%d "
                "PRIORITY_ENABLE=%d)\n", v, (v >> 2) & 7, !!(v & 0x20));
        return;
    }
    if (off == RD_ALLOC_CFG) {
        rd.alloc_cfg = v & 0x000F;
        fprintf(stderr, "[rhea-dma] ALLOC_CONFIG <- 0x%04x : canal1=%s canal2=%s "
                "canal3=%s canal4=%s\n", v,
                (v & 1) ? "ARM" : "DSP", (v & 2) ? "ARM" : "DSP",
                (v & 4) ? "ARM" : "DSP", (v & 8) ? "ARM" : "DSP");
        return;
    }

    for (int n = 0; n < 4; n++) {
        hwaddr b = RD_CH_BASE(n);
        if (off < b || off > b + RD_CUR_OFF)
            continue;
        switch (off - b) {
        case RD_RAD:
            rd.ch[n].rad = v;
            fprintf(stderr, "[rhea-dma] DMA%d_RAD   <- 0x%04x (RHEA_START=0x%03x "
                    "RHEA_CS=%d)\n", n + 1, v, v & 0x7FF, (v >> 11) & 0x1F);
            break;
        case RD_RDPTH:
            rd.ch[n].rdpth = v & 0x07FF;
            fprintf(stderr, "[rhea-dma] DMA%d_RDPTH <- %u octets\n", n + 1, v & 0x7FF);
            break;
        case RD_AAD:
            /* Logged unmasked: AAD is the answer to "where must the burst land". */
            rd.ch[n].aad = v & 0x0FFF;
            log_aad(n, v);
            break;
        case RD_ALGTH:
            rd.ch[n].algth = v & 0x0FFF;
            fprintf(stderr, "[rhea-dma] DMA%d_ALGTH <- %u octets (= %u mots de page API) PC=0x%04x\n",
                    n + 1, v & 0xFFF, (v & 0xFFF) / 2, rd_dsp ? (rd_dsp->pc & 0xffff) : 0);
            break;
        case RD_CTRL: {
            uint16_t keep = rd.ch[n].ctrl & CTRL_RO_MASK;
            uint16_t before = rd.ch[n].ctrl;
            rd.ch[n].ctrl = (uint16_t)((v & ~CTRL_RO_MASK & 0x1FFF) | keep);
            /* PROM0 0xa640 is an explicit channel disarm, not polling:
             *     portr *(0x4356), 0xfc28    ; read DMA2_CTRL
             *     andm  *(0x4356), #0xfffe   ; clear bit 0 = ENABLE
             *     portw *(0x4356), 0xfc28    ; write back
             * The write is inert here only because ENABLE is already 0. Count
             * those passes instead of repeating them, and log everything that
             * CHANGES the state or sets ENABLE/DMA_START. */
            /* [dma-arm] every arm/disarm of the RX channel in TI dedicated mode
             * (CALYPSO_BSP_DMA_TRACE=1): when the ROM opens the NB (151) or measurement
             * (64) window relative to the bench's deposit. */
            if (n == 1 && ((v ^ before) & (CTRL_ENABLE | CTRL_ONE_SHOT))) {
                extern int calypso_bsp_dma_trace_on(void); extern uint32_t calypso_trx_get_fn(void);
                if (calypso_bsp_dma_trace_on())
                    printf("  [dma-arm] tick=%u insn=%u pc=0x%04x %s one_shot=%d fenetre=%u\n", calypso_trx_get_fn(),
                           rd_dsp ? rd_dsp->insn_count : 0, rd_dsp ? (rd_dsp->pc & 0xffff) : 0,
                           (v & CTRL_ENABLE) ? "ARME" : "desarme", !!(v & CTRL_ONE_SHOT), rd.ch[n].algth / 4);
            }
            if (n == 1 && (v & CTRL_ENABLE) && !(before & CTRL_ENABLE) && !(v & CTRL_ONE_SHOT))
                rd_skip_pending = arm_skip_words();      /* FB search armed: set the TOA origin */
            bool inerte = (rd.ch[n].ctrl == before) && !(v & CTRL_DMA_START);
            if (inerte) {
                static unsigned long long n_rmw[4];
                if (n_rmw[n]++ == 0 || (n_rmw[n] % 20000) == 0)
                    fprintf(stderr, "[rhea-dma] DMA%d_CTRL DESARMEMENT x%llu "
                            "(0xa643 andm #0xfffe = efface ENABLE ; sans effet ici, "
                            "ENABLE valait deja 0 — relu/reecrit 0x%04x)\n",
                            n + 1, n_rmw[n], v);
                break;
            }
            log_ctrl(n, v);
            if (v & CTRL_DMA_START) {
                rd.n_start++;
                fprintf(stderr, "[rhea-dma] *** DMA%d DMA_START #%u — l'ARM DEMANDE un "
                        "transfert. Ce module NE L'EXECUTE PAS (instrument de lecture). "
                        "Parametres courants : RAD=0x%04x RDPTH=%u AAD=0x%03x "
                        "(mot DSP 0x%04x) ALGTH=%u\n",
                        n + 1, rd.n_start, rd.ch[n].rad, rd.ch[n].rdpth,
                        rd.ch[n].aad, (unsigned)(0x0800 + rd.ch[n].aad / 2),
                        rd.ch[n].algth);
            }
            break;
        }
        case RD_CUR_OFF:
            /* Read-only (§11.3.6): the write is ignored. */
            break;
        default:
            break;
        }
        return;
    }

    fprintf(stderr, "[rhea-dma] WR  +0x%02x = 0x%04x (hors carte §11)\n",
            (unsigned)off, v);
}

/* ═══════════════════════════════════════════════════════════════════════════
 * THE TRANSFER. On a burst, once the firmware has selected DMA mode (RDMA_MASK=0,
 * its own decision in SPCR - nothing is forced here):
 *   1. drain the RIF receiver into API memory at DMA2_AAD;
 *   2. bound the transfer by DMA2_ALGTH (page length, in BYTES, §11.3.3);
 *   3. set IRQ_STATE, clear DMA_START, raise INT10n when IRQ_MODE=1.
 *
 * Deliberately not done here:
 *   - nothing is armed by this code: with ENABLE=0 there is no transfer. The
 *     point is to serve a firmware request, not to manufacture one;
 *   - SPCR and the masks are left alone: the IT/DMA choice belongs to the
 *     firmware.
 *
 * The firmware itself reports the missing transfer by raising
 * DSP_ERR_DMA_PROG | DSP_ERR_DMA_TASK (d_error_status = 24) on every frame.
 *
 * [2026-09-18] Running the transfer is unconditional. It used to sit behind a
 * gate defaulting to off, as a non-regression precaution; without the transfer
 * the DSP correlates over a buffer nobody fills. Deterministic replay, this
 * behaviour the only change (1200 frames, synthetic cell):
 *     off : FB accepted   0, SB attempted   0, CRC OK  0
 *     on  : FB accepted 119, SB attempted 115, CRC OK  2
 * ═══════════════════════════════════════════════════════════════════════════ */

static void rhea_abort_impl(const char *why)
{
    rd.ch[1].ctrl |= CTRL_RHEA_ERROR;
    static unsigned long long n_ab;
    if (n_ab++ < 20)
        fprintf(stderr, "[rhea-dma] RHEA_ERROR pose : %s (CAL207 §11.3.5, efface a la lecture)\n", why);
}
#define rhea_abort(w) rhea_abort_impl(w)

/* DARAM address the ROM actually programs into DMA2 (the RX channel). AAD is in
 * BYTES from the base of the API window; the DSP word is 0x800 + (AAD&0xFFF)/2,
 * the same conversion as the transfer itself. Returns 0 when the channel is not
 * armed. The BSP uses it to drop the burst WHERE the DSP will read it, instead
 * of at a frozen constant. */
uint16_t calypso_rhea_dma_get_daram(void)
{
    if (!(rd.ch[1].ctrl & CTRL_ENABLE) && rd.ch[1].aad == 0) return 0;
    if (rd.ch[1].aad == 0) return 0;
    return (uint16_t)(0x800u + ((rd.ch[1].aad & 0x0FFFu) / 2u));
}

/* Page length actually programmed by the DSP, in words. ALGTH is in bytes (see
 * max_words below: algth/2). The BSP sizes its burst drop with this instead of a
 * frozen 296-word cap, which truncated the 380-word SB window and cut off the
 * second data block. */
uint16_t calypso_rhea_dma_get_len_words(void)
{
    if (rd.ch[1].algth == 0) return 0;
    return (uint16_t)(rd.ch[1].algth / 2u);
}

/* INT10n is a LEVEL line (CAL000 §5.1): asserted while any channel holds
 * IRQ_STATE. */
bool calypso_rhea_dma_irq_level(void)
{
    if (!rhea_dma_on() || !rd.init)
        return false;
    for (int i = 0; i < 4; i++)
        if (rd.ch[i].ctrl & CTRL_IRQ_STATE)
            return true;
    return false;
}

void calypso_rhea_dma_rx_request(C54xState *s)
{
    if (!rhea_dma_on())
        return;
    rhea_dma_init();

    const int n = 1;                 /* DMA2 = RIF RX channel (§6 Table 2) */
    uint16_t ctrl = rd.ch[n].ctrl;
    if (s) rd_dsp = s;

    /* ═══════════════════════════════════════════════════════════════════════
     * The IDLE bit is driven by the HARDWARE, CAL207 §11.3.5: "1 IDLE: 0 = DMA
     * transfer is running / 1 = DMA channel is idle", access R, reset 1. Never
     * clearing it leaves the DSP unable to tell "transfer running" from
     * "transfer done".
     *
     * What that costs, traced back into the ROM: 0xaa83 / 0xaaad / 0xaad1 set
     * DMA_PROG / DMA_TASK / DMA_PEND. All three have the same shape, a 14-entry
     * CIRCULAR queue (`stm #0x000e`; `stl *AR0+%`; `banz`), and the bit is
     * raised when it OVERFLOWS: the DSP piles up requests nothing ever closes.
     * Counted over one run: DMA_PROG 948 times, DMA_PEND 541. On the osmocom
     * side the L1 only prints and clears d_error_status, so the witness is a
     * DSP report, not something the ARM corrects.
     *
     * ⚠️ The bit is R in the doc: the firmware must never write it. CTRL_RO_MASK
     * already protects it from software writes; only internal state is touched
     * here, which is the hardware's role.
     * ORDERING: it is cleared further down, AFTER every validation, so that an
     * early return never leaves the channel marked "running" with no transfer
     * having taken place.
     * ═══════════════════════════════════════════════════════════════════════ */

    /* Only serve what the firmware has actually armed. */
    if (!(ctrl & CTRL_ENABLE)) {
        static unsigned long long n_off;
        if (n_off++ == 0 || (n_off % 5000) == 0)
            fprintf(stderr, "[rhea-dma] requete RX x%llu ignoree : DMA2 ENABLE=0 "
                    "(le firmware n'a pas arme ce canal — on ne l'arme pas a sa "
                    "place)\n", n_off);
        return;
    }
    if (!(ctrl & CTRL_DIRECTION)) {
        static unsigned n_dir;
        if (n_dir++ < 5)
            fprintf(stderr, "[rhea-dma] requete RX ignoree : DMA2 DIRECTION=0 "
                    "(API->Rhea), ce n'est pas une reception\n");
        return;
    }

    /* ALGTH is a length in BYTES (§11.3.3); API memory is addressed in words. */
    int max_words = rd.ch[n].algth ? (int)(rd.ch[n].algth / 2) : 0;
    if (max_words <= 0) {
        rhea_abort("ALGTH nulle");   /* BEFORE the if: the hardware state must not
                                      * depend on a log cap. */
        static unsigned n_len;
        if (n_len++ < 5)
            fprintf(stderr, "[rhea-dma] requete RX ignoree : DMA2_ALGTH=%u — aucune "
                    "longueur de page programmee\n", rd.ch[n].algth);
        return;
    }

    uint16_t *api = s ? s->api_ram : NULL;
    if (!api) {
        rhea_abort("memoire API absente");
        static unsigned n_api;
        if (n_api++ < 5)
            fprintf(stderr, "[rhea-dma] requete RX ignoree : memoire API absente\n");
        return;
    }

    /* AAD is a BYTE offset from the API base -> word index into api_ram: the DSP
     * word is 0x0800 + aad/2 and the api_ram index is that word minus
     * C54X_API_BASE, so exactly aad/2. */
    unsigned dst_idx = (rd.ch[n].aad & 0x0FFF) / 2;
    if (dst_idx + (unsigned)max_words > C54X_API_SIZE) {
        max_words = (int)(C54X_API_SIZE - dst_idx);
        static unsigned n_clip;
        if (max_words <= 0) {
            rhea_abort("AAD hors fenetre API");
            if (n_clip++ < 5)
                fprintf(stderr, "[rhea-dma] requete RX ignoree : AAD=0x%03x hors "
                        "memoire API\n", rd.ch[n].aad);
            return;
        }
        if (n_clip++ < 5)
            fprintf(stderr, "[rhea-dma] transfert TRONQUE a %d mots : AAD=0x%03x + "
                    "ALGTH depasse la fenetre API\n", max_words, rd.ch[n].aad);
    }

    static uint16_t buf[4096];
    if (max_words > (int)(sizeof(buf) / sizeof(buf[0])))
        max_words = (int)(sizeof(buf) / sizeof(buf[0]));

    /* ─────────────────────────────────────────────────────────────────────────
     * PAGE CHAINING (§11.3.5 CURRENT_PAGE). One page per request is not enough:
     * with a 96-word page (ALGTH=192) against a 296-word burst, the leftover
     * 200 words wait in the RIF staging area and the next burst finds them
     * there -> RSRFULL -> overrun, measured at 11392 of 13000 bursts (84%). At
     * that loss rate, assembling 4 consecutive bursts (one CCCH block) is
     * impossible, which is enough to explain SI=0 without blaming demodulation.
     * A burst therefore needs about 3 pages, drained in one pass. */
    rd.ch[n].ctrl &= (uint16_t)~CTRL_IDLE;      /* 0 = transfer running */
    /* [2026-09-19] What the receiver already held BEFORE this transfer, and how
     * each page filled. A transfer that opens on a non-zero level starts on the
     * tail of an earlier burst, so the real burst begins one page further in —
     * which is what a TOA quantised to multiples of 48 samples (one 96-word
     * page) looks like from the correlator. */
    int niveau_avant = calypso_rif_level();
    int got_page[8] = {0};
    int premier_nz = -1;
    int total = 0, pages = 0;
    /* Non-zero words are accumulated page by page: `buf` holds ONLY the last
     * page (max_words words), so scanning buf[0..total[ would read leftovers. */
    int nz_total = 0;
    uint16_t head8[8] = {0};
    /* [2026-09-20] STREAM SEMANTICS. The RIF is a continuous sample stream and
     * the DMA fires when a PAGE is full, not when a burst was handed over:
     *   - double-buffer mode (ONE_SHOT=0, the FB search: ALGTH=192 -> 96-word
     *     pages at 0x0cce and 0x0d2e): only FULL pages are transferred, a partial
     *     tail stays in the receiver for the next frame; at most the two pages
     *     are filled per request, then ONE interrupt, and the ROM's ISR consumes
     *     both halves (0xb2c1 selects the half by the parity of 0x3fb4, which it
     *     advances by 2 per interrupt = 2 x 48 samples). Refilling page 0 before
     *     the ISR ran would corrupt the half being processed, so the caller
     *     pumps the next pages once the DSP is idle (calypso_rhea_dma_pump).
     *   - one-shot mode (the SB window, ALGTH=764): the single page takes what is
     *     there, partial or not, and interrupts.
     * Measured before: a 312-word frame drained in one pass raised one interrupt,
     * the ROM counted 2 blocks per frame instead of 3.25 and its TOA advanced 96
     * per frame where the firmware divides by 156. */
    const bool one_shot = (ctrl & CTRL_ONE_SHOT) != 0;
    /* [2026-09-20] The double buffer is not refilled while its completion is
     * still pending: IRQ_STATE is cleared when the ISR reads DMA2_CTRL, and
     * until then both pages belong to the DSP. Transferring anyway raised a
     * second INT10n on top of the first (one interrupt lost) and overwrote the
     * pages under the ISR. Measured: 13 pairs handed over per frame, the ROM
     * counted 6 (0x3fb4 += 12 instead of 26). The words wait in the receiver;
     * calypso_rhea_dma_pump() moves them once the DSP is idle. */
    if (!one_shot && (ctrl & CTRL_IRQ_STATE)) {
        /* Restore IDLE before returning. It was cleared above ("transfer in
         * progress") and EVERY other exit path sets it back: skip_pending,
         * nothing-to-transfer, end of transfer. Without it the channel would
         * announce forever a transfer that will not happen -- the ROM, which
         * polls IDLE to know whether it may reprogram the channel, would then
         * wait endlessly. */
        rd.ch[n].ctrl |= CTRL_IDLE;
        return;
    }
    if (!one_shot && rd_skip_pending > 0) {
        static uint16_t poubelle[256];
        while (rd_skip_pending > 0) {
            int m = rd_skip_pending < 256 ? rd_skip_pending : 256;
            int g = calypso_rif_drain(poubelle, m);
            if (g <= 0) break;
            rd_skip_pending -= g;
        }
        if (rd_skip_pending > 0) {              /* origin not reached yet: nothing to store */
            rd.ch[n].ctrl |= CTRL_IDLE;
            return;
        }
    }
    for (;;) {
        if (!one_shot && calypso_rif_level() < max_words)
            break;                                /* wait for a full page */
        int got = calypso_rif_drain(buf, max_words);
        if (got <= 0)
            break;

        /* Pages are written CONTIGUOUSLY, page k -> dst_idx + k*96, not
         * ping-ponged. CAL207 §11.3.5 DMA_CTRL bit4 says the destination follows
         * CURRENT_PAGE ("0 = first API page, 1 = second API page, automatically
         * updated during the transfer"), i.e. page 0 = AAD (dst_idx), page 1 =
         * AAD+ALGTH. But the FB correlator reads a CONTIGUOUS 296-word buffer
         * (=148 IQ at 1 SPS, disasm PROM 0xb2c4=0x0cce / 0xb2c9=0x0d2e), and
         * two 96-word pages fold a 296-word burst back on itself: page 2 over
         * page 0, page 3 over page 1 -> corrupted FCCH, correlator peaking at
         * the edge (TOA=39). The hardware double buffer is a REAL-TIME artifact,
         * the DSP consuming page 0 while the DMA fills page 1; it has no purpose
         * when the whole burst is drained in one synchronous pass. Contiguous
         * pages give a flat [0x0cce..0x0df6) the correlator can read whole.
         * A/B: CALYPSO_RHEA_DMA_PINGPONG=1 restores the 2-page ping-pong. */
        /* [2026-09-20] Ping-pong is the default again: page 0 at AAD, page 1 at
         * AAD+ALGTH, following CURRENT_PAGE (§11.3.5). The FB ROM reads exactly
         * those two 96-word halves (0x0cce / 0x0d2e); contiguous pages 2 and 3
         * landed past them. CALYPSO_RHEA_DMA_CONTIGU=1 restores the old layout. */
        unsigned pdst = dst_idx;
        if (rd.ch[n].ctrl & CTRL_CURRENT_PAGE)
            pdst = dst_idx + (unsigned)max_words;   /* 2nd API page = AAD+ALGTH */
        {
            static int contigu = -1;
            if (contigu < 0) contigu = calypso_getenv("CALYPSO_RHEA_DMA_CONTIGU") ? 1 : 0;
            if (contigu) pdst = dst_idx + (unsigned)(pages * max_words);
        }
        /* [2026-09-19] Does the burst DMA ever land ON a_sch? a_sch[0..4] sit at
         * API words 0x37..0x3b (R page 0) and 0x4b..0x4f (R page 1). The cell
         * carries plain numbers (0x1111, 0x1388) that no firmware and no DSP
         * probe accounts for -- and an I/Q sample is exactly a plain number.
         * If a burst is deposited over the SB result, that is the carnage. */
        if ((pdst <= 0x3b && pdst + (unsigned)got > 0x37) ||
            (pdst <= 0x4f && pdst + (unsigned)got > 0x4b)) {
            static unsigned nov;
            if (nov++ < 20)
                fprintf(stderr, "[rhea-dma] *** ECRASEMENT a_sch : depot %u mots en "
                        "0x%04x..0x%04x recouvre a_sch (0x37..0x3b / 0x4b..0x4f) ***\n",
                        got, pdst, pdst + got - 1);
        }
        for (int i = 0; i < got; i++)
            if (pdst + (unsigned)i < C54X_API_SIZE)
                api[pdst + i] = buf[i];

        for (int i = 0; i < got; i++)
            if (buf[i]) nz_total++;
        if (pages == 0)
            for (int i = 0; i < 8 && i < got; i++) head8[i] = buf[i];
        /* [2026-09-19] Where the burst actually STARTS inside the window. The
         * cell injector leaves marge=21 samples of silence on each side of a
         * 148-symbol burst in a 190-sample window, so the first non-zero word
         * is expected at index 42 (21 samples x 2 words). Anything else means
         * the SB task is handed a burst that does not sit where it looks. */
        if (premier_nz < 0)
            for (int i = 0; i < got; i++)
                if (buf[i]) { premier_nz = total + i; break; }

        if (pages < 8) got_page[pages] = got;
        total += got;
        pages++;
        rd.ch[n].cur_off = (uint16_t)(got * 2);
        rd.ch[n].ctrl ^= CTRL_CURRENT_PAGE;      /* §11.3.5: next page */

        if (one_shot)
            break;                                /* a single page, then the interrupt */
        if (!(rd.ch[n].ctrl & CTRL_CURRENT_PAGE))
            break;                                /* both pages full: interrupt, let the ISR consume */
        if (got < max_words)
            break;                                /* receiver emptied before the page filled */
        if (pages >= 64) {                        /* guard: never loop forever */
            static unsigned n_cap;
            if (n_cap++ < 5)
                fprintf(stderr, "[rhea-dma] ⚠ plafond de 64 pages atteint sur une "
                        "requete (burst anormalement long ?) — reste non draine\n");
            break;
        }
    }

    if (total <= 0) {
        rd.ch[n].ctrl |= CTRL_IDLE;            /* nothing transferred -> idle */
        static unsigned long long n_empty;
        if (n_empty++ == 0 || (n_empty % 5000) == 0)
            fprintf(stderr, "[rhea-dma] requete RX x%llu : recepteur RIF vide, "
                    "rien a transferer\n", n_empty);
        return;
    }
    int got = total;

    /* §11.3.5: transfer complete -> IRQ_STATE set, DMA_START drops. */
    if (!(rd.ch[n].ctrl & CTRL_IRQ_STATE)) {
        g_rd_fini++;   /* one more transfer the ROM will have to acknowledge */
    }
    rd.ch[n].ctrl = (uint16_t)((rd.ch[n].ctrl | CTRL_IRQ_STATE | CTRL_IDLE)
                               & ~CTRL_DMA_START);   /* done -> IDLE=1 */
    if (rd.ch[n].ctrl & CTRL_ONE_SHOT) {
        rd.ch[n].ctrl &= (uint16_t)~CTRL_ENABLE;
        calypso_rif_flush();             /* window closed: the radio stops receiving */
    }

    {
        static unsigned long long n_ok;
        n_ok++;
        /* [2026-09-19] The frame number was missing, and without it a transfer
         * cannot be put side by side with the firmware's own "=>FB @ FNR X" in
         * osmocon.log — which is the only way to tell whether the SB window of
         * frame X+1 really receives the SCH burst. calypso_daram_last_fn is set
         * by calypso_bsp_rx_burst() for exactly this.
         *
         * A 296-word transfer is an ordinary burst; anything else is a window
         * the firmware armed on purpose (380 words = the SB window), so those
         * are traced one by one up to 2000 instead of one in 500. */
        static unsigned long long n_sb;
        bool fenetre = (got != 296);
        if (n_ok <= 20 || (n_ok % 500) == 0 || (fenetre && n_sb++ < 2000))
            fprintf(stderr, "[rhea-dma] *** TRANSFERT RX #%llu fn=%u : %d mots RIF -> "
                    "api_ram[0x%04x..0x%04x] en %d page(s) (mot DSP 0x%04x, ARM 0x%08x) "
                    "ALGTH=%u IRQ_MODE=%d ONE_SHOT=%d\n",
                    n_ok, calypso_bsp_get_last_fn(),
                    got, dst_idx, dst_idx + (got > max_words ? max_words : got) - 1, pages,
                    (unsigned)(C54X_API_BASE + dst_idx),
                    0xFFD00000u + (rd.ch[n].aad & 0x0FFF),
                    rd.ch[n].algth, !!(rd.ch[n].ctrl & CTRL_IRQ_MODE),
                    !!(rd.ch[n].ctrl & CTRL_ONE_SHOT));
        /* Sample the payload, not just the plumbing, on the SAME cadence as the
         * TRANSFERT line above, so every sample matches a logged transfer.
         * Capping this at the first 10 transfers only shows startup, where the
         * input really is empty (measured: bursts #1..#5 at nz=0/2368, then #6
         * at nz=2211/2368) and invites the wrong conclusion that the DMA carries
         * only zeros. */
        if (n_ok <= 20 || (n_ok % 500) == 0 || (fenetre && n_sb <= 2000)) {
            fprintf(stderr, "[rhea-dma]     RIF avant = %d mots ; pages = "
                    "%d+%d+%d+%d+%d+%d+%d+%d ; 1er mot non nul a l'index %d "
                    "(attendu 42)\n", niveau_avant,
                    got_page[0], got_page[1], got_page[2], got_page[3],
                    got_page[4], got_page[5], got_page[6], got_page[7], premier_nz);
            fprintf(stderr, "[rhea-dma]     contenu : %d/%d mots non nuls ; "
                    "8 premiers = %04x %04x %04x %04x %04x %04x %04x %04x\n",
                    nz_total, total,
                    head8[0], head8[1], head8[2], head8[3],
                    head8[4], head8[5], head8[6], head8[7]);
        }
    }

    /* §11.3.5 IRQ_MODE: the firmware decides whether it wants the interrupt. */
    if ((rd.ch[n].ctrl & CTRL_IRQ_MODE) && s) {
        static unsigned long long n_it;
        n_it++;
        if (n_it <= 10 || (n_it % 500) == 0)
            fprintf(stderr, "[rhea-dma] end-DMA -> INT10n (vec%d/bit%d) #%llu\n",
                    C54X_IT_DMA_VEC, C54X_IT_DMA_BIT, n_it);
        c54x_interrupt_ex(s, C54X_IT_DMA_VEC, C54X_IT_DMA_BIT);
    }
}

bool calypso_rhea_dma_rx_armed(void)
{
    if (!rhea_dma_on()) return true;      /* no DMA model: legacy path, keep everything */
    rhea_dma_init();
    uint16_t ctrl = rd.ch[1].ctrl;
    return (ctrl & CTRL_ENABLE) && (ctrl & CTRL_DIRECTION);
}

bool calypso_rhea_dma_one_shot(void)
{
    if (!rhea_dma_on()) return false;
    rhea_dma_init();
    uint16_t ctrl = rd.ch[1].ctrl;
    return (ctrl & CTRL_ENABLE) && (ctrl & CTRL_ONE_SHOT);
}

/* Stream pump: transfer the next full page(s) of the receiver into the
 * double buffer, if the channel is armed in continuous mode and enough words
 * are waiting. Called by the frame loop once the DSP has gone idle, i.e. once
 * its ISR has consumed the previous pair. Returns 1 when a transfer happened. */
int calypso_rhea_dma_pump(C54xState *s)
{
    if (!rhea_dma_on()) return 0;
    rhea_dma_init();
    if (s) rd_dsp = s; else s = rd_dsp;
    uint16_t ctrl = rd.ch[1].ctrl;
    if (!(ctrl & CTRL_ENABLE) || !(ctrl & CTRL_DIRECTION) || (ctrl & CTRL_ONE_SHOT))
        return 0;
    int page = rd.ch[1].algth / 2;
    if (page <= 0) return 0;
    int besoin = (ctrl & CTRL_CURRENT_PAGE) ? page : 2 * page;
    if (calypso_rif_level() < besoin) return 0;
    int avant = calypso_rif_level();
    calypso_rhea_dma_rx_request(s);
    return calypso_rif_level() < avant;
}

/* XIO bridge: the SAME register bank, seen from the DSP Rhea bus. §11.1 gives
 * both addresses for every register (FFFF:FCxx and XIO:FCxx); which bus reaches
 * them depends on DMA_ALLOC. Since the ARM hands the RIF channels to the DSP
 * (measured ALLOC_CONFIG=0x000C), the receive transfer is programmed through
 * here. */
bool calypso_rhea_dma_xio(bool write, uint16_t pa, uint16_t *val, uint16_t pc)
{
    if (pa < 0xFC00 || pa > 0xFCFF)
        return false;
    if (!rhea_dma_on())
        return false;
    hwaddr off = pa - 0xFC00;
    /* Dedupe, do not cap. The CTRL polling loop (a640/a646/a652) saturates a
     * fixed line cap within a few frames, and a journal truncated by its own
     * instrument makes "AAD is never written" unprovable. Each (direction, PA,
     * PC) triple is logged ONCE and then summarised periodically, so a first AAD
     * write always shows up however late it comes. */
    {
        struct seen { uint16_t pa, pc; uint8_t wr; unsigned long long n; };
        static struct seen tab[64];
        static int ntab = 0;
        int i, found = -1;
        for (i = 0; i < ntab; i++)
            if (tab[i].pa == pa && tab[i].pc == pc && tab[i].wr == (write ? 1 : 0))
                { found = i; break; }
        if (found < 0) {
            if (ntab < 64) { tab[ntab].pa = pa; tab[ntab].pc = pc;
                             tab[ntab].wr = write ? 1 : 0; tab[ntab].n = 1;
                             found = ntab++; }
            fprintf(stderr, "[rhea-dma] XIO %s PA=0x%04x (+0x%02x) %s0x%04x PC=0x%04x "
                    "— NOUVEAU site (acces DSP, canal cede par ALLOC_CONFIG)\n",
                    write ? "PORTW" : "PORTR", pa, (unsigned)off,
                    write ? "<- " : "-> ", val ? *val : 0, pc);
        } else {
            tab[found].n++;
            if ((tab[found].n % 20000) == 0)
                fprintf(stderr, "[rhea-dma] XIO %s PA=0x%04x PC=0x%04x : %llu passages "
                        "(valeur courante 0x%04x)\n", write ? "PORTW" : "PORTR",
                        pa, pc, tab[found].n, val ? *val : 0);
        }
    }
    if (write) {
        calypso_rhea_dma_write(NULL, off, *val, 2);
    } else {
        *val = (uint16_t)calypso_rhea_dma_read(NULL, off, 2);
    }
    return true;
}

4.67 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_rhea_dma.h

2578 octets, 56 lignes → 56 lignes

/*
 * calypso_rhea_dma.h - Calypso RHEA DMA controller, MCU side (FFFF:FC00).
 * Source: CAL207 11 (ti-calypso2.pdf).
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#ifndef CALYPSO_RHEA_DMA_H
#define CALYPSO_RHEA_DMA_H

#include "exec/hwaddr.h"
#include <stdint.h>
#include <stdbool.h>

#define CALYPSO_RHEA_DMA_BASE 0xFFFFFC00

uint64_t calypso_rhea_dma_read(void *opaque, hwaddr off, unsigned size);
void     calypso_rhea_dma_write(void *opaque, hwaddr off, uint64_t val, unsigned size);

/* Same register bank seen from the DSP's Rhea bus (XIO:FC00..FCFF, CAL207
 * 11.1). Returns true if PA falls inside the DMA window. */
bool     calypso_rhea_dma_xio(bool write, uint16_t pa, uint16_t *val, uint16_t pc);

/* Receive request: the RIF signals that a burst is available and that the
 * firmware picked DMA mode (RDMA_MASK=0). The controller then drains the
 * receiver into API memory at DMA2_AAD, sets IRQ_STATE and raises INT10n if
 * IRQ_MODE asks for it.
 *
 * Gate CALYPSO_RHEA_DMA_XFER (default 0): without it the module stays the
 * read-only instrument it was, logging without transferring anything. Enabling
 * it changes the module's NATURE (from instrument to piece of hardware), hence
 * opt-in. */
struct C54xState;
void     calypso_rhea_dma_rx_request(struct C54xState *s);

/* [2026-09-20] Stream pump: hand the next full page pair of the receiver to
 * the double buffer (continuous mode only), once the DSP is idle. Returns 1
 * when a transfer took place. The frame loop calls it until it returns 0. */
int      calypso_rhea_dma_pump(struct C54xState *s);
/* True while DMA2 is armed as a one-shot window (the SB task: ALGTH=764). The
 * bench delivers the burst as an aligned window block in that case, and as a
 * 156.25-symbol frame of the continuous stream otherwise. */
bool     calypso_rhea_dma_one_shot(void);
/* True while DMA2 is armed for reception at all: the RIF drops samples
 * arriving outside a receive window (the radio is off between windows). */
bool     calypso_rhea_dma_rx_armed(void);

/* Level of the INT10n line. CAL000 5.1: "INT10n (level) -> DMA interrupt". The
 * line stays asserted as long as the channel has its IRQ_STATE set; READING the
 * register is what clears it (CAL207 11.3.5). Without that, the model treats the
 * interrupt as edge-triggered and loses it whenever it arrives with INTM=1 -
 * which is 15 times out of 15 in the measured runs. */
uint16_t calypso_rhea_dma_get_daram(void);
uint16_t calypso_rhea_dma_get_len_words(void);
bool     calypso_rhea_dma_irq_level(void);

#endif /* CALYPSO_RHEA_DMA_H */

4.68 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_rif.c

22793 octets, 539 lignes → 539 lignes

/*
 * calypso_rif.c - Calypso Radio InterFace (RIF), DSP side (XIO space).
 *
 * Every PORTR instruction of the DSP firmware reads XIO:0003 (SPCR): the
 * PORTR-ANY probe reports `PA=0x0003 addr=0x000e PC=0xa675` on 30 samples out
 * of 30. Without a RIF model PORTR is a no-op (outside PA=0xF430/0x0034 under
 * gate CALYPSO_FIX_PORTR, default 0, which this firmware never uses), so the
 * receive handler tests a stale word and returns: the DSP does take its RX
 * interrupt (PC=0x00c0, 16330 entries) but produces nothing (fb0_att=37,
 * A_CD-WR=0).
 *
 * SILICON (CAL207 §12.1, Table 19):
 *
 *   Register  XIO      ARM         Access     Reset
 *   DXR       0x0000   FFFF:7000   16b R/W    undefined
 *   DRR       0x0001   FFFF:7002   16b R      undefined
 *   SPCX      0x0002   n.a.        15b        000 0101 1001 1110 = 0x059E
 *   SPCR      0x0003   n.a.        15b        011 1100 1010 0010 = 0x3CA2
 *
 * SPCR (§12.6), field by field; the fields add up to exactly the documented
 * reset value 0x3CA2, which confirms the reading:
 *
 *   0  DLB          0   digital local loopback
 *   1  RRST         1   receiver reset (0 -> clears RSRFULL and RRDY)
 *   2  RRDY         0   "Unused" per §12.6, yet §12.3 says "DRR is updated
 *                       when RSR is ready to be read and RRDY=1". The document
 *                       contradicts itself; see CALYPSO_RIF_RRDY_UNUSED below.
 *   3  RSRFULL      0   a word sits in RSR and DRR has not been read yet. Per
 *                       the doc it is NOT cleared by a DRR read: "This allows
 *                       keeping trace of a reception problem".
 *   4  ALMOST_FULL  0   1 = receive FIFO non empty (at THRESHOLD)
 *   5  FIFO_EMPTY   1   FIFO empty. The prose says "0 = receive FIFO is
 *                       empty", but the reset value is 1 AND the FIFO is empty
 *                       at reset: the prose is inverted, follow the reset.
 *   6  FIFO_FULL    0   FIFO full
 *   9:7 THRESHOLD 001   threshold of the "FIFO non empty" flag, maximum 4
 *   10 XINT_MASK    1   1 = transmit interrupt masked
 *   11 RINT_MASK    1   1 = RECEIVE interrupt masked
 *   12 XDMA_MASK    1   1 = transmit DMA request masked
 *   13 RDMA_MASK    1   1 = RECEIVE DMA request masked
 *   14 CLKLB        0   TX/RX clocks from the same internal source
 *
 * §3.7.1 has the DSP exchange with the RIF "either through XIO (word by word,
 * an interrupt is sent to the DSP), or through the API in DMA mode (a DMA
 * request and an end-DMA request are sent to the ARM)". Which of the two is
 * not this model's call: RINT_MASK (bit 11) and RDMA_MASK (bit 13) decide it,
 * and the FIRMWARE programs them. Both are masked at reset, so something must
 * open them; that choice is obeyed here instead of being forced by a gate.
 *
 * RX interrupt vector: INT0n = IMR bit 0 = vector 16 (CAL000 §5.1). The IMR
 * measured on this firmware (0x52ef) does have bit 0 open.
 *
 * NOT modelled here, and assumed:
 *   - the ARM aliases FFFF:7000 / FFFF:7002 (DXR/DRR seen from the MCU): the
 *     osmocom firmware does not touch them, so they are not wired.
 *   - transmission (DXR/SPCX): writes are accepted and counted, nothing is
 *     sent. TX goes through the TPU/TSP today.
 *   - XSR/RSR are not accessible (§12.4) and are not exposed.
 *   - the exact FIFO depth, which the doc does not give; THRESHOLD is capped
 *     at 4, so 4 is used. A full burst waits in a staging buffer and feeds the
 *     FIFO as DRR is read.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#include "qemu/osdep.h"
#include "hw/arm/calypso/calypso_debug.h"
#include "calypso_c54x.h"
#include "calypso_rif.h"
#include "calypso_rhea_dma.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* --- SPCR fields (§12.6) --- */
#define SPCR_DLB          (1u << 0)
#define SPCR_RRST         (1u << 1)
#define SPCR_RRDY         (1u << 2)
#define SPCR_RSRFULL      (1u << 3)
#define SPCR_ALMOST_FULL  (1u << 4)
#define SPCR_FIFO_EMPTY   (1u << 5)
#define SPCR_FIFO_FULL    (1u << 6)
#define SPCR_THRESHOLD_SH 7
#define SPCR_THRESHOLD_M  (7u << SPCR_THRESHOLD_SH)
#define SPCR_XINT_MASK    (1u << 10)
#define SPCR_RINT_MASK    (1u << 11)
#define SPCR_XDMA_MASK    (1u << 12)
#define SPCR_RDMA_MASK    (1u << 13)
#define SPCR_CLKLB        (1u << 14)

/* R/W bits are kept verbatim on write; the rest is hardware state recomputed
 * on read. */
#define SPCR_RW_MASK  (SPCR_DLB | SPCR_RRST | SPCR_THRESHOLD_M | SPCR_XINT_MASK \
                       | SPCR_RINT_MASK | SPCR_XDMA_MASK | SPCR_RDMA_MASK | SPCR_CLKLB)

#define SPCR_RESET   0x3CA2
#define SPCX_RESET   0x059E

#define RIF_FIFO_DEPTH  4        /* THRESHOLD is capped at 4 (§12.6) */
#define RIF_STAGE_MAX   8192     /* [2026-09-20] a full 1250-symbol frame is 2500 words */

static struct {
    bool     init;
    uint16_t spcr;               /* R/W fields only */
    uint16_t spcx;
    uint16_t dxr;

    uint16_t fifo[RIF_FIFO_DEPTH];
    int      fifo_n;

    uint16_t stage[RIF_STAGE_MAX];   /* burst waiting to enter the FIFO */
    int      stage_n;
    int      stage_pos;

    bool     rsrfull;            /* word shifted in while DRR was unread */
    uint16_t drr;                /* last word popped from the FIFO */
    bool     drr_valid;

    unsigned n_burst, n_drr_rd, n_spcr_rd, n_spcr_wr, n_int, n_dma, n_overrun;
    unsigned n_wr_arme;      /* SPCR writes that CLEAR a receive mask */
    unsigned n_wr_rrst_on;   /* SPCR writes that release RRST */

    /* [2026-08-04] @BEQUILLE RIF_BACKPRESSURE: bursts refused for lack of
     * room, and bursts forced through by the anti-stall valve. `bp_run` =
     * consecutive refusals in progress. */
    unsigned n_bp_skip, n_bp_force, bp_run;
    unsigned n_muets;        /* bursts arriving with no receive window open */
} rif;

/* Discard whatever the receiver still holds (a one-shot window just closed:
 * the radio stops, the rest of the frame is never received). */
void calypso_rif_flush(void)
{
    rif.fifo_n = 0;
    rif.stage_pos = rif.stage_n = 0;
}

bool calypso_rif_on(void)
{
    static int on = -1;
    if (on < 0) {
        on = calypso_gate("CALYPSO_RIF_XIO", 1);
        if (!on)
            fprintf(stderr, "[rif] CALYPSO_RIF_XIO=0 : RIF non modelise, PORTR/PORTW "
                    "redeviennent des no-op (comportement d'avant le 03/08)\n");
    }
    return on != 0;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * @BEQUILLE — RIF_BACKPRESSURE  (CALYPSO_RIF_BACKPRESSURE, default 0)
 *
 *   (1) THIS IS A CRUTCH. The silicon has no back-pressure: when a burst
 *       arrives while the previous one is unread, the receiver OVERWRITES it
 *       and sets RSRFULL (§12.6). Refusing delivery is a DEVIATION from the
 *       hardware, not a model fix.
 *
 *   (2) WHAT IT MASKS: the c54x interpreter is ~96x too slow. Measured
 *       [2026-08-04] on a native_twl run: 73500 bursts pushed for 54492
 *       overruns — 74% of the bursts overwrite an unconsumed predecessor, and
 *       the DSP drains only ~4 words (RIF_FIFO_DEPTH) out of 296 before the
 *       next overwrite. The DMA therefore carries zeros, `a_cd` is published
 *       empty, and an SI only comes out when fill/drain/RRST happen to line
 *       up. While this gate is 1 the figure measured is NOT the real DSP
 *       throughput, only that of a bench slowed down to suit it.
 *
 *   (3) WHEN TO REMOVE IT: as soon as the DSP consumes a whole burst within
 *       one frame, that is, once the interpreter is fast enough (JIT, or a
 *       revised per-frame budget). Criterion: `overrun` must fall close to 0
 *       with the gate at 0. A non-zero `n_bp_force` means the DSP still does
 *       not consume and the valve had to force: the crutch is not enough, and
 *       that is not progress.
 *
 * RESTRICTED SCOPE: back-pressure applies ONLY when a drain path is armed
 * (RDMA or RINT unmasked). With an unarmed receiver nothing will ever empty
 * the staging buffer, so refusing would discard fresh bursts to protect a dead
 * one; in that case overwrite, like the hardware.
 *
 * ANTI-STALL VALVE: without it, a DSP that stops reading would freeze
 * reception forever. After CALYPSO_RIF_BP_MAX_SKIP consecutive refusals
 * (default 200) the next burst is forced through and logged.
 * ═══════════════════════════════════════════════════════════════════════════ */
static bool rif_backpressure_on(void)
{
    static int on = -1;
    if (on < 0) {
        on = calypso_gate("CALYPSO_RIF_BACKPRESSURE", 0);
        fprintf(stderr, "[rif] BEQUILLE contre-pression %s "
                "(CALYPSO_RIF_BACKPRESSURE=%d) — %s\n",
                on ? "ACTIVE" : "inactive", on,
                on ? "un burst n'ecrase plus un predecesseur non lu ; "
                     "DEVIATION du §12.6, le debit mesure n'est plus celui du DSP"
                   : "comportement materiel conserve : ecrasement + RSRFULL (defaut)");
    }
    return on != 0;
}

/* NUMERIC value: `calypso_gate` only returns 0/1, so it does not fit here.
 * getenv+atoi idiom, like the other thresholds of this model. */
static unsigned rif_bp_max_skip(void)
{
    static int v = -1;
    if (v < 0) {
        const char *e = calypso_getenv("CALYPSO_RIF_BP_MAX_SKIP");
        v = e ? atoi(e) : 200;
        if (v <= 0)
            v = 200;   /* a valve at 0 would be a guaranteed stall */
    }
    return (unsigned)v;
}

/* §12.6 marks bit 2 "Unused" while §12.3 relies on it. RRDY is exposed as "data
 * is available", the only reading that makes §12.3 consistent; the gate restores
 * the letter of §12.6 if a run needs it. */
static bool rif_rrdy_unused(void)
{
    static int u = -1;
    if (u < 0) u = calypso_gate("CALYPSO_RIF_RRDY_UNUSED", 0);
    return u != 0;
}

static void rif_init(void)
{
    if (rif.init)
        return;
    rif.init = true;
    rif.spcr = SPCR_RESET & SPCR_RW_MASK;
    rif.spcx = SPCX_RESET;
    fprintf(stderr, "[rif] RIF XIO arme (CAL207 §12) : DXR@0x0000 DRR@0x0001 "
            "SPCX@0x0002=0x%04x SPCR@0x0003=0x%04x ; RX IT = INT0n vec16/bit0, "
            "choisie par SPCR.RINT_MASK (bit11) / RDMA_MASK (bit13), tous deux "
            "MASQUES au reset — c'est au firmware de les ouvrir.\n",
            SPCX_RESET, SPCR_RESET);
}

static int rif_threshold(void)
{
    int t = (rif.spcr & SPCR_THRESHOLD_M) >> SPCR_THRESHOLD_SH;
    if (t < 1) t = 1;
    if (t > RIF_FIFO_DEPTH) t = RIF_FIFO_DEPTH;
    return t;
}

/* Pour the staging buffer into the FIFO while there is room. */
static void rif_refill(void)
{
    while (rif.fifo_n < RIF_FIFO_DEPTH && rif.stage_pos < rif.stage_n)
        rif.fifo[rif.fifo_n++] = rif.stage[rif.stage_pos++];
}

/* Hardware state recomposed on every SPCR read. */
static uint16_t rif_spcr_read(void)
{
    uint16_t v = rif.spcr & SPCR_RW_MASK;
    if (rif.fifo_n == 0)
        v |= SPCR_FIFO_EMPTY;
    if (rif.fifo_n >= rif_threshold())
        v |= SPCR_ALMOST_FULL;
    if (rif.fifo_n >= RIF_FIFO_DEPTH)
        v |= SPCR_FIFO_FULL;
    if (rif.rsrfull)
        v |= SPCR_RSRFULL;
    if (!rif_rrdy_unused() && rif.fifo_n > 0)
        v |= SPCR_RRDY;
    return v;
}

bool calypso_rif_portr(C54xState *s, uint16_t pa, uint16_t *out)
{
    (void)s;
    if (!calypso_rif_on())
        return false;
    rif_init();

    switch (pa) {
    case RIF_XIO_DRR:
        rif_refill();
        if (rif.fifo_n > 0) {
            rif.drr = rif.fifo[0];
            memmove(&rif.fifo[0], &rif.fifo[1],
                    (size_t)(rif.fifo_n - 1) * sizeof(uint16_t));
            rif.fifo_n--;
            rif.drr_valid = true;
            rif_refill();
        }
        /* On an empty FIFO, DRR keeps its last value: the doc defines no
         * read-empty value ("Undefined" at reset), and returning 0 would
         * fabricate a sample. */
        *out = rif.drr;
        if (rif.n_drr_rd++ < 20)
            fprintf(stderr, "[rif] DRR read #%u = 0x%04x (fifo=%d etage=%d/%d) "
                    "PC=0x%04x\n", rif.n_drr_rd, *out, rif.fifo_n,
                    rif.stage_pos, rif.stage_n, s ? s->pc : 0);
        return true;

    case RIF_XIO_SPCR:
        *out = rif_spcr_read();
        if (rif.n_spcr_rd++ < 20)
            fprintf(stderr, "[rif] SPCR read #%u = 0x%04x (fifo=%d empty=%d "
                    "almost=%d rsrfull=%d RINT_MASK=%d RDMA_MASK=%d) PC=0x%04x\n",
                    rif.n_spcr_rd, *out, rif.fifo_n,
                    !!(*out & SPCR_FIFO_EMPTY), !!(*out & SPCR_ALMOST_FULL),
                    !!(*out & SPCR_RSRFULL), !!(*out & SPCR_RINT_MASK),
                    !!(*out & SPCR_RDMA_MASK), s ? s->pc : 0);
        return true;

    case RIF_XIO_SPCX:
        *out = rif.spcx;
        return true;

    case RIF_XIO_DXR:
        *out = rif.dxr;   /* R/W per §12.2 */
        return true;

    default:
        return false;
    }
}

bool calypso_rif_portw(C54xState *s, uint16_t pa, uint16_t val)
{
    if (!calypso_rif_on())
        return false;
    rif_init();

    switch (pa) {
    case RIF_XIO_SPCR: {
        uint16_t before = rif.spcr;
        rif.spcr = val & SPCR_RW_MASK;
        /* §12.6: "Writing a zero to RRST clears the RSRFULL bit and RRDY bit".
         * The FIFO is flushed too: the receiver is in reset. */
        if ((before & SPCR_RRST) && !(val & SPCR_RRST)) {
            rif.rsrfull = false;
            rif.fifo_n = 0;
            rif.stage_pos = rif.stage_n;
            rif.drr_valid = false;
        }
        rif.n_spcr_wr++;
        if (!(val & SPCR_RINT_MASK) || !(val & SPCR_RDMA_MASK)) rif.n_wr_arme++;
        if (val & SPCR_RRST) rif.n_wr_rrst_on++;
        if (rif.n_spcr_wr <= 20 || (rif.n_spcr_wr % 200) == 0)
            fprintf(stderr, "[rif] SPCR write #%u 0x%04x -> RRST=%d THRESHOLD=%d "
                    "RINT_MASK=%d RDMA_MASK=%d XINT_MASK=%d PC=0x%04x\n",
                    rif.n_spcr_wr, val, !!(val & SPCR_RRST), rif_threshold(),
                    !!(val & SPCR_RINT_MASK), !!(val & SPCR_RDMA_MASK),
                    !!(val & SPCR_XINT_MASK), s ? s->pc : 0);
        return true;
    }
    case RIF_XIO_SPCX:
        rif.spcx = val;
        return true;

    case RIF_XIO_DXR:
        /* TX is not modelled (see header): accept and count. */
        rif.dxr = val;
        return true;

    case RIF_XIO_DRR:
        /* §12.3: "DRR cannot be written via the RHEA interface." */
        return true;

    default:
        return false;
    }
}

/* [2026-09-19] How many words the receiver still holds: the FIFO plus the part
 * of the staging buffer not yet poured into it. A transfer that starts with a
 * non-zero level begins on LEFTOVERS from an earlier burst, which pushes the
 * real burst one page further into DARAM — the mechanism behind the TOA ladder
 * in steps of 48 samples (= one 96-word DMA page) measured on this bench. */
int calypso_rif_level(void)
{
    if (!calypso_rif_on()) return 0;
    rif_init();
    int reste = rif.stage_n - rif.stage_pos;
    if (reste < 0) reste = 0;
    return rif.fifo_n + reste;
}

int calypso_rif_drain(uint16_t *dst, int max)
{
    if (!calypso_rif_on() || !dst || max <= 0)
        return 0;
    rif_init();

    int got = 0;
    while (got < max) {
        if (rif.fifo_n == 0) {
            rif_refill();          /* same refill as a DRR read */
            if (rif.fifo_n == 0)
                break;             /* receiver empty: return what we have */
        }
        dst[got++] = rif.fifo[0];
        memmove(&rif.fifo[0], &rif.fifo[1],
                (size_t)(rif.fifo_n - 1) * sizeof(uint16_t));
        rif.fifo_n--;
    }
    /* Receiver drained: RSRFULL no longer applies (§12.6). */
    if (got && rif.fifo_n == 0 && rif.stage_pos >= rif.stage_n)
        rif.rsrfull = false;
    return got;
}

void calypso_rif_rx_burst(C54xState *s, const uint16_t *w, int n)
{
    if (!calypso_rif_on() || !w || n <= 0)
        return;
    rif_init();

    if (n > RIF_STAGE_MAX)
        n = RIF_STAGE_MAX;

    /* [2026-09-20] The receiver is a STREAM: words not yet drained stay ahead of
     * the new ones (the hardware shift register does not forget them because a
     * new burst arrived). The DMA now transfers full pages only and leaves the
     * tail of a frame in here, so this append is what carries the 156.25
     * symbols per frame across frame boundaries. Overrun is a real overflow of
     * the staging area. CALYPSO_RIF_REPLACE=1 restores the old
     * replace-and-backpressure behaviour for A/B. */
    static int remplace = -1;
    if (remplace < 0) remplace = calypso_getenv("CALYPSO_RIF_REPLACE") ? 1 : 0;
    if (!remplace && !calypso_rhea_dma_rx_armed()) {
        /* [2026-09-20] No receive window open (DMA2 disabled, or its one-shot
         * window already filled): on silicon the radio is off between windows
         * and those samples never exist. Queueing them would put a stale
         * backlog in front of the next window and shift every ToA. */
        rif.n_muets++;
        return;
    }
    if (!remplace) {
        int reste = rif.stage_n - rif.stage_pos;
        if (reste < 0) reste = 0;
        if (reste && rif.stage_pos)
            memmove(rif.stage, rif.stage + rif.stage_pos, (size_t)reste * sizeof(uint16_t));
        rif.stage_n = reste; rif.stage_pos = 0;
        if (rif.stage_n + n > RIF_STAGE_MAX) {
            rif.rsrfull = true;
            if (rif.n_overrun++ < 10)
                fprintf(stderr, "[rif] OVERRUN #%u : %d mots en attente + %d nouveaux > %d, "
                        "les plus anciens sont perdus\n", rif.n_overrun, reste, n, RIF_STAGE_MAX);
            int perte = rif.stage_n + n - RIF_STAGE_MAX;
            memmove(rif.stage, rif.stage + perte, (size_t)(rif.stage_n - perte) * sizeof(uint16_t));
            rif.stage_n -= perte;
        }
        memcpy(rif.stage + rif.stage_n, w, (size_t)n * sizeof(uint16_t));
        rif.stage_n += n;
        rif_refill();
        rif.n_burst++;
    } else {
    /* The previous burst was not fully consumed: a real receiver overrun,
     * counted rather than hidden. */
    if (rif.stage_pos < rif.stage_n) {
        rif.rsrfull = true;
        if (rif.n_overrun++ < 10)
            fprintf(stderr, "[rif] OVERRUN #%u : %d mots du burst precedent non lus "
                    "(RSRFULL pose, cf. §12.6 : le bit garde la trace du probleme)\n",
                    rif.n_overrun, rif.stage_n - rif.stage_pos);

        /* @BEQUILLE RIF_BACKPRESSURE — see rif_backpressure_on() above.
         * REFUSE the new burst instead of overwriting, so the DSP has time to
         * consume the 296 words instead of the ~4 it otherwise drains. RSRFULL
         * stays set: the firmware keeps the trace of the problem.
         * [2026-08-04] Apply back-pressure ONLY when a drain path is armed.
         * Measured: `RDMA_MASK=1` on 36 of the 52 sampled `[rif] burst #`
         * lines, so the receiver is unarmed half of the time. Refusing a burst
         * then is HARMFUL: the stale staging buffer is never drained, so fresh
         * bursts are dropped until the valve forces one through (measured: 38
         * BP-FORCE, with `292/296 mots encore a lire` unchanged after 200
         * refusals). With nothing armed, the hardware behaviour — overwrite —
         * is the right one. */
        bool drain_arme = !(rif.spcr & SPCR_RDMA_MASK) ||
                          !(rif.spcr & SPCR_RINT_MASK);
        if (rif_backpressure_on() && drain_arme) {
            if (++rif.bp_run <= rif_bp_max_skip()) {
                rif.n_bp_skip++;
                if (rif.n_bp_skip <= 10 || (rif.n_bp_skip % 2000) == 0)
                    fprintf(stderr, "[rif] BP-SKIP #%u : burst refuse, %d/%d mots "
                            "du precedent encore a lire (refus consecutifs %u/%u)\n",
                            rif.n_bp_skip, rif.stage_n - rif.stage_pos,
                            rif.stage_n, rif.bp_run, rif_bp_max_skip());
                return;          /* no delivery, hence no notification */
            }
            /* Valve: the DSP has stopped consuming, so force the burst
             * through rather than freeze reception. A non-zero counter here
             * CONTRADICTS any claim of progress. */
            rif.n_bp_force++;
            fprintf(stderr, "[rif] BP-FORCE #%u : %u refus consecutifs atteints "
                    "(CALYPSO_RIF_BP_MAX_SKIP), burst impose — le DSP ne draine "
                    "plus, la contre-pression ne suffit pas\n",
                    rif.n_bp_force, rif.bp_run);
        }
    }
    rif.bp_run = 0;

    memcpy(rif.stage, w, (size_t)n * sizeof(uint16_t));
    rif.stage_n = n;
    rif.stage_pos = 0;
    rif.fifo_n = 0;
    rif_refill();
    rif.n_burst++;
    }   /* CALYPSO_RIF_REPLACE */

    /* §12.6 bits 11/13: the FIRMWARE picked the mode, not this model. */
    bool rint = !(rif.spcr & SPCR_RINT_MASK);
    bool rdma = !(rif.spcr & SPCR_RDMA_MASK);

    if (rint && s) {
        rif.n_int++;
        c54x_interrupt_ex(s, C54X_IT_RIF_RX_VEC, C54X_IT_RIF_RX_BIT);
    }
    if (rdma) {
        rif.n_dma++;
        /* The end-of-DMA request of §3.7.1 is served, not just counted. */
        calypso_rhea_dma_rx_request(s);
    }

    if (rif.n_burst <= 10 || (rif.n_burst % 500) == 0)
        fprintf(stderr, "[rif] BILAN bursts=%u notifies=%u (IT=%u DMA=%u) muets=%u ; "
                "ecritures SPCR=%u dont %u liberent un masque de reception, "
                "%u relachent RRST\n",
                rif.n_burst, rif.n_int + rif.n_dma, rif.n_int, rif.n_dma,
                rif.n_burst - rif.n_int - rif.n_dma,
                rif.n_spcr_wr, rif.n_wr_arme, rif.n_wr_rrst_on);
    if (rif.n_burst <= 10 || (rif.n_burst % 500) == 0)
        fprintf(stderr, "[rif] burst #%u n=%d -> FIFO %d/%d ; RINT_MASK=%d "
                "RDMA_MASK=%d => %s (IT=%u DMA=%u overrun=%u)\n",
                rif.n_burst, n, rif.fifo_n, RIF_FIFO_DEPTH,
                !rint, !rdma,
                rint ? "INT0n vec16/bit0"
                     : (rdma ? "requete DMA (end-DMA vers l'ARM, §3.7.1)"
                             : "AUCUNE notification — les deux masques sont poses, "
                               "le firmware n'a pas arme le recepteur"),
                rif.n_int, rif.n_dma, rif.n_overrun);

    if (rif_backpressure_on() && (rif.n_burst <= 10 || (rif.n_burst % 500) == 0))
        fprintf(stderr, "[rif] BP-BILAN burst=%u overrun=%u refuses=%u imposes=%u\n",
                rif.n_burst, rif.n_overrun, rif.n_bp_skip, rif.n_bp_force);
}

4.69 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_rif.h

1996 octets, 47 lignes → 47 lignes

/*
 * calypso_rif.h - Calypso Radio InterFace (RIF), DSP view (XIO space).
 *
 * Sources: CAL207 "Register Mapping" §12 (ti-calypso2.pdf), CAL000 §3.7.1 and
 * §5.1 (ti-calypso1.pdf).
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#ifndef CALYPSO_RIF_H
#define CALYPSO_RIF_H

#include <stdint.h>
#include <stdbool.h>
#include "calypso_c54x.h"

/* XIO addresses, §12.1 (Table 19). */
#define RIF_XIO_DXR    0x0000   /* Transmit Data Register  - 16b R/W          */
#define RIF_XIO_DRR    0x0001   /* Receive Data Register   - 16b R            */
#define RIF_XIO_SPCX   0x0002   /* Control Register (TX)   - 15b, reset 0x059E */
#define RIF_XIO_SPCR   0x0003   /* Control Register (RX)   - 15b, reset 0x3CA2 */

/* True when the module is enabled (CALYPSO_RIF_XIO, default 1). */
bool calypso_rif_on(void);

/* PORTR PA,Smem / PORTW Smem,PA: return true when PA belongs to the RIF.
 * portr stores the value read in *out. */
bool calypso_rif_portr(C54xState *s, uint16_t pa, uint16_t *out);
bool calypso_rif_portw(C54xState *s, uint16_t pa, uint16_t val);

/* A burst arrives from the radio front end: the words are staged and flow into
 * the receive FIFO as the DSP reads DRR. */
void calypso_rif_rx_burst(C54xState *s, const uint16_t *w, int n);

/* DMA drain path. In DMA mode (RDMA_MASK=0) the DSP does not read DRR word by
 * word: the RHEA controller empties the receiver into API memory (CAL000
 * §3.7.1, "an end-DMA request is sent"). This is the only way
 * calypso_rhea_dma.c takes samples out; the FIFO and the staging area stay
 * private to the RIF.
 *
 * Returns the number of words actually copied (0 when the receiver is empty)
 * and refills the FIFO from staging, exactly as a DRR read does. No side effect
 * on SPCR: notification is the caller's decision, not the RIF's. */
int calypso_rif_drain(uint16_t *dst, int max);
int calypso_rif_level(void);
void calypso_rif_flush(void);   /* [2026-09-20] receive window closed: drop the rest */

#endif /* CALYPSO_RIF_H */

4.70 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_tint0.c

3975 octets, 130 lignes → 130 lignes

/*
 * calypso_tint0.c -- TINT0 master clock for Calypso GSM virtualization
 *
 * Emulates the C54x DSP Timer 0 as a QEMU virtual timer.
 * On real hardware, Timer 0 runs off the 13 MHz DSP clock divided by
 * (PRD+1)*(TDDR+1) to produce a 4.615 ms TDMA frame tick (TINT0).
 * TINT0 drives the entire Calypso timing: DSP frame processing,
 * TPU sync, ARM frame IRQ, and UART polling.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#include "qemu/osdep.h"
#include "qemu/timer.h"
#include "qemu/main-loop.h"
#include "hw/irq.h"
#include "calypso_tint0.h"
#include "calypso_c54x.h"
#include "hw/core/cpu.h"
#include <stdlib.h>  /* getenv */
#include "hw/arm/calypso/calypso_debug.h"

#define TINT0_LOG(fmt, ...) \
    fprintf(stderr, "[tint0] " fmt "\n", ##__VA_ARGS__)

/* calypso_trx.c implements the actual frame work (DSP run, IRQs, UART) */
extern void calypso_tint0_do_tick(uint32_t fn);
/* The orch CLK->BTS is driven from TINT0, at 2x rate via an internal half-tick. */

/* ---- State ---- */
static struct {
    QEMUTimer *timer;
    uint32_t   fn;
    bool       running;
    bool       tpu_en_pending;
} tint0;

/* ---- Timer callback (fires every 4.615ms) ---- */
static void tint0_tick_cb(void *opaque)
{
    tint0.fn = (tint0.fn + 1) % GSM_HYPERFRAME;

    /* In the current Calypso machine calypso_tint0_start() is never called:
     * the frame tick comes from calypso_tdma_tick() in calypso_trx.c. */

    /* No forced page tic-toc here: the DSP itself writes d_dsp_page
     * each frame (PC=0xf321 / 0xf5ec); the trx api hook mirrors the
     * value into ARM space. We let the firmware drive the toggle. */

    /* Delegate frame work to calypso_trx */
    calypso_tint0_do_tick(tint0.fn);

    /* Re-arm the timer, gated by CALYPSO_PCB_TICK_THREADS. When threading is
     * active the PCB spawns a self-pacing tint0 thread, so the QEMUTimer must
     * NOT be re-armed or every frame ticks twice. */
    {
        static int pcb_threaded = -1;
        if (pcb_threaded < 0) {
            const char *e = calypso_getenv("CALYPSO_PCB_TICK_THREADS");
            pcb_threaded = (e && e[0] == '1') ? 1 : 0;
        }
        if (!pcb_threaded && tint0.running) {
            timer_mod_ns(tint0.timer,
                         qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + TINT0_PERIOD_NS);
        }
    }

    /* Kick ARM CPU to process pending IRQs */
qemu_notify_event();
}

/* Public invoker for the PCB tick thread: runs the same body as the QEMUTimer
 * callback. Call it with the BQL held. */
void calypso_tint0_tick_invoke(void);
void calypso_tint0_tick_invoke(void)
{
    tint0_tick_cb(NULL);
}

/* ---- Public API ---- */

void calypso_tint0_start(void)
{
    if (tint0.running) return;

    if (!tint0.timer) {
        tint0.timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, tint0_tick_cb, NULL);
    }

    tint0.running = true;
    /* Do NOT force tint0.fn = 0 here. A real GSM BTS never restarts the frame
     * counter at 0, it only advances; resetting it on every TINT0 start makes
     * the firmware believe it just synchronized to a fresh hyperframe. Whoever
     * owns the master clock seeds fn through calypso_tint0_set_fn() from a
     * network-derived source before calling start; otherwise fn keeps whatever
     * the static struct holds (0 on first boot only). */
    TINT0_LOG("started (period=%.3f ms, IFR bit %d, vec %d) fn=%u",
              TINT0_PERIOD_NS / 1e6, TINT0_IFR_BIT, TINT0_VEC, tint0.fn);
    timer_mod_ns(tint0.timer,
                 qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + TINT0_PERIOD_NS);
}

void calypso_tint0_tpu_en(void)
{
    tint0.tpu_en_pending = true;
}

bool calypso_tint0_tpu_en_pending(void)
{
    return tint0.tpu_en_pending;
}

void calypso_tint0_tpu_en_clear(void)
{
    tint0.tpu_en_pending = false;
}

uint32_t calypso_tint0_fn(void)
{
    return tint0.fn;
}

void calypso_tint0_set_fn(uint32_t fn)
{
    tint0.fn = fn % GSM_HYPERFRAME;
}

bool calypso_tint0_running(void)
{
    return tint0.running;
}

4.71 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_tint0.h

1827 octets, 51 lignes → 51 lignes

/*
 * calypso_tint0.h -- TINT0 master clock for Calypso GSM virtualization
 *
 * On real Calypso hardware, the C54x DSP Timer 0 (TIM/PRD/TCR at
 * DSP addresses 0x0024-0x0026) generates TINT0 (IFR bit 4, vector 20).
 * The DSP ROM configures Timer 0 to fire at TDMA frame rate (4.615ms).
 * TINT0 is the master clock that synchronizes everything:
 *
 *   TINT0 (DSP Timer 0, 4.615ms)
 *     +-- DSP: SINT17 frame interrupt -> process GSM tasks
 *     +-- TPU: frame sync -> burst scheduling
 *     +-- ARM: TPU_FRAME IRQ -> L1 scheduler
 *     +-- ARM: API IRQ -> read DSP results
 *     +-- UART: poll TX/RX
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#ifndef CALYPSO_TINT0_H
#define CALYPSO_TINT0_H

#include <stdint.h>
#include <stdbool.h>

/* Hardware constants from TMS320C54x Timer 0 */
#define TINT0_PERIOD_US     4615         /* 4.615 ms per TDMA frame */
#define TINT0_PERIOD_NS     4615000ULL   /* in nanoseconds */
#define GSM_HYPERFRAME      2715648      /* GSM hyperframe modulus */

/* C54x Timer 0 registers (DSP data addresses) */
#define TINT0_TIM_ADDR      0x0024       /* Timer counter */
#define TINT0_PRD_ADDR      0x0025       /* Timer period */
#define TINT0_TCR_ADDR      0x0026       /* Timer control */

/* C54x IFR/IMR bit for TINT0 */
#define TINT0_IFR_BIT       4            /* IFR/IMR bit 4 */
#define TINT0_VEC           20           /* Interrupt vector 20 (offset 0x50) */

/* Start the master clock */
void calypso_tint0_start(void);

/* Notify that TPU_CTRL_EN was written (ARM requests frame processing) */
void calypso_tint0_tpu_en(void);
bool calypso_tint0_tpu_en_pending(void);
void calypso_tint0_tpu_en_clear(void);

/* Frame number access */
uint32_t calypso_tint0_fn(void);
void calypso_tint0_set_fn(uint32_t fn);
bool calypso_tint0_running(void);

#endif /* CALYPSO_TINT0_H */

4.72 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_tint0_glue.c

487 octets, 15 lignes → 15 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * TINT0 tick shim. calypso_tint0.c declares calypso_tint0_do_tick() extern and
 * calls it; the frame counter lives in the platform, so the tick is forwarded
 * to the exported calypso_trx_force_tick() symbol instead of poking a local
 * TRX state.
 */
#include "qemu/osdep.h"
#include "hw/arm/calypso/calypso_api.h"

void calypso_tint0_do_tick(uint32_t fn);
void calypso_tint0_do_tick(uint32_t fn)
{
    calypso_trx_force_tick(fn);
}

4.73 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_twl3025.c

11997 octets, 257 lignes → 257 lignes

/*
 * calypso_twl3025.c - TWL3025 (Iota) ABB AFC chip model
 *
 * Minimal TWL3025 (analog baseband) model, enough to propagate the firmware
 * AFC down to the BSP samples in near real time.
 *
 * Silicon chain:
 *   firmware afc_load_dsp() writes dsp_api.db_w->d_afc = dac_value
 *   the DSP reads d_afc and serialises it to the TWL3025 over the TSP
 *   the TWL3025 decodes the register write into the AFC DAC (13-bit, +/-4096)
 *   the DAC drives the 13 MHz VCXO, shifting the baseband frequency
 *
 * In QEMU:
 *   calypso_twl3025_set_afc_dac()   called from the TSP chain with the DAC
 *                                   value written by the firmware
 *   calypso_twl3025_apply_phase()   rotates the BSP samples by
 *                                   dac_value * afc_slope (Hz) at the 270.833
 *                                   kHz baseband rate, with a deterministic
 *                                   offset derived from FN/TN
 *
 * The model is ARMED BY DEFAULT (chip-level behaviour, no env gate).
 * CALYPSO_TWL3025_AFC_HZ=N overrides it with a constant offset (diagnostic:
 * it short-circuits the DAC chain).
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "qemu/osdep.h"
#include <math.h>

#include "calypso_twl3025.h"
#include "hw/arm/calypso/calypso_debug.h"

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

/* TWL3025 vcxocal constants (osmocom-bb board afcparams.c):
 *   afc_slope              board calibration; 287 on Compal E88, 454 on gta0x
 *   afc_initial_dac_value  -700 on E88, compensating the physical crystal trim
 *   d_afc range            +/-4095 (13-bit signed DAC)
 *
 * In QEMU the osmo-bts samples already arrive at the exact carrier frequency,
 * i.e. as if silicon had already applied the -700 calibration, so the firmware
 * DAC value is read RELATIVE to that baseline:
 *   effective_dac = dac_value - AFC_INITIAL_DAC_VALUE
 * DAC=-700 (ideal calibration) -> effective 0 -> no rotation
 * DAC=0    (+700 LSB above calibration) -> effective +700 -> +200900 Hz
 *
 * [2026-05-30] The modelled VCXO sensitivity must match the osmocom
 * afc_correct() loop:
 *   delta_LSB = (AFC_NORM_FACTOR_GSM * freq_error_Hz) / afc_slope
 *   AFC_NORM_FACTOR_GSM = 2^15/947 = 34.6
 * so unity loop gain requires Hz/LSB = afc_slope / norm_factor. Using the raw
 * slope as Hz/LSB gives a loop gain around 22: the DAC oscillates, the FCCH is
 * rotated by +/-150 kHz, far outside the +/-20 kHz DSP capture range, and FB is
 * never detected. */
#define TWL3025_AFC_NORM_FACTOR_GSM     (32768.0 / 947.0)
#define TWL3025_AFC_SLOPE               287.0  /* compal_e88 board = 287, not 454 (gta0x) */
#define TWL3025_AFC_SLOPE_HZ_PER_LSB    (TWL3025_AFC_SLOPE / TWL3025_AFC_NORM_FACTOR_GSM)
/* ⚠️ The model must boot at DAC=-700, not 0: osmocom afc_reset() starts at
 * afc_initial_dac_value and converges from there. Booting at dac=0 against the
 * -700 baseline means effective=+700 = +200 kHz of spurious offset before the
 * firmware loads its calibration, which puts the FCCH outside the +/-20 kHz DSP
 * capture range so it is never detected. twl3025_lazy_env() therefore inits the
 * DAC to -700 and set_afc_dac() filters the firmware's 0 writes. */
#define TWL3025_AFC_INITIAL_DAC_VALUE   (-700)
#define GSM_SAMPLE_RATE_HZ              270833.0  /* 1 sps GSM baseband */

/* GSM TDMA timing constants (BSP rate of 1 sample per bit):
 *   1 frame = 4.615 ms = 1250 samples
 *   1 slot  = 156.25 symbols, rounded down to 156 samples
 */
#define SAMPLES_PER_FRAME   1250U
#define SAMPLES_PER_SLOT    156U

static struct {
    int16_t  dac_value;     /* current DAC register value (last firmware write) */
    int      force_hz;      /* CALYPSO_TWL3025_AFC_HZ override (diag, default 0 = off) */
    bool     env_loaded;
    int      afc_enabled;   /* CALYPSO_TWL3025_AFC (default 1; 0 disables AFC rotation) */
    uint64_t dac_writes;    /* diag counter */
    uint64_t apply_calls;
} twl;

static void twl3025_lazy_env(void)
{
    if (twl.env_loaded) return;
    twl.env_loaded = true;
    /* @BEQUILLE - TWL3025_AFC_HZ  (CALYPSO_TWL3025_AFC_HZ, VALUE; 0 = inert)
     *   masque  : the closed AFC loop (firmware DAC -> Hz/LSB slope -> sample
     *             rotation). A non-zero value pins a constant offset in Hz and
     *             suppresses convergence.
     *   retirer : never needed - setting 0 (or leaving it unset) is enough, this
     *             is a diagnostic tool. Note that calypso_hack.env sets it to 0
     *             AND exports it: harmless as a value, but present in the
     *             environment.
     */
    const char *h = calypso_getenv("CALYPSO_TWL3025_AFC_HZ");
    twl.force_hz = (h && *h) ? atoi(h) : 0;
    /* Master gate of the AFC loop (RX sample rotation by the VCXO offset).
     * Default ON: the AFC is part of the nominal osmocom path. Opt out with
     * CALYPSO_TWL3025_AFC=0 to deliver raw I/Q. */
    const char *ae = calypso_getenv("CALYPSO_TWL3025_AFC");
    twl.afc_enabled = (ae && *ae == '0') ? 0 : 1;
    /* Start at the calibration point (-700), the "nominal VCXO" in QEMU: the
     * firmware starts its AFC at afc_initial_dac_value and converges. */
    twl.dac_value = TWL3025_AFC_INITIAL_DAC_VALUE;
    fprintf(stderr,
            "[twl3025] chip model armed (slope=%.1f Hz/LSB, AFC=%s, force_hz=%d)\n",
            TWL3025_AFC_SLOPE_HZ_PER_LSB,
            twl.afc_enabled ? "ON(opt-out CALYPSO_TWL3025_AFC=0)" : "OFF",
            twl.force_hz);
}

void calypso_twl3025_set_afc_dac(int16_t dac_value)
{
    twl3025_lazy_env();
    if (dac_value > 4095)  dac_value = 4095;
    if (dac_value < -4096) dac_value = -4096;

    /* Filter the dual-write 0-clobber pattern: the firmware writes d_afc to BOTH
     * dsp_api pages each frame (page A = inherited init 0, page B = the real
     * value). On silicon the TWL3025 register is sticky through TSP
     * serialisation, so the transient 0 never propagates before the real value;
     * this model sees both writes as direct MMIO and would oscillate 0 <-> -700.
     *
     * Heuristic: ignore a dac=0 write when the current value is already set. A
     * legitimate firmware reset goes through afc_reset, which writes
     * afc_initial_dac_value (-700 on Compal E88), not 0. */
    if (dac_value == 0 && twl.dac_value != 0) {
        return;
    }

    if (twl.dac_value != dac_value) {
        twl.dac_writes++;
        /* Throttled log: first 20 writes then every 1000, enough to watch
         * convergence without drowning the AFC log. */
        if (twl.dac_writes <= 20 || (twl.dac_writes % 1000) == 0) {
            fprintf(stderr,
                "[twl3025] DAC %d → %d (%.1f Hz, write #%" PRIu64 ")\n",
                twl.dac_value, dac_value,
                dac_value * TWL3025_AFC_SLOPE_HZ_PER_LSB,
                twl.dac_writes);
        }
        twl.dac_value = dac_value;
    }
}

int16_t calypso_twl3025_get_afc_dac(void)
{
    return twl.dac_value;
}

double calypso_twl3025_get_afc_hz(void)
{
    twl3025_lazy_env();
    if (twl.force_hz != 0) return (double)twl.force_hz;
    /* Effective DAC = firmware write minus the calibration baseline (-700 on
     * E88), so a firmware reset lands on effective 0. */
    int32_t effective_dac = (int32_t)twl.dac_value - TWL3025_AFC_INITIAL_DAC_VALUE;

    /* [2026-08-22] Band-aware VCXO slope. osmocom afc_correct() picks
     * AFC_NORM_FACTOR by band: GSM/850 -> 2^15/947 (34.6), DCS/PCS -> 2^15/1894
     * (17.3, half). Physically the 13 MHz VCXO is multiplied up to the LO (900
     * vs 1800 MHz), so one DAC LSB shifts the baseband twice as far in DCS. For
     * unity loop gain the emulated Hz/LSB must follow: GSM = slope/34.6 = 8.29,
     * DCS = slope/17.3 = 16.6. Hardcoding the GSM value gave loop gain 0.5 in
     * DCS (ARFCN 514): the AFC under-corrected, converged too slowly, and the
     * FBSB reset cut it short, leaving TOA stuck at r39=39 instead of 23.
     * Selected by CALYPSO_TWL3025_AFC_BAND (default GSM; DCS/1800/PCS -> x2). */
    static double hz_lsb = 0.0;
    if (hz_lsb == 0.0) {
        const char *b = calypso_getenv("CALYPSO_TWL3025_AFC_BAND");
        int dcs = (b && (b[0]=='D' || b[0]=='d' || b[0]=='P' || b[0]=='p' ||
                         (b[0]=='1' && b[1]=='8')));   /* DCS / PCS / 1800 */
        hz_lsb = dcs ? (TWL3025_AFC_SLOPE / (TWL3025_AFC_NORM_FACTOR_GSM / 2.0))
                     : TWL3025_AFC_SLOPE_HZ_PER_LSB;
    }
    return (double)effective_dac * hz_lsb;
}

double calypso_twl3025_get_afc_phase_step(void)
{
    twl3025_lazy_env();
    if (!twl.afc_enabled) return 0.0;   /* CALYPSO_TWL3025_AFC=0: no AFC rotation */
    double hz = calypso_twl3025_get_afc_hz();
    if (hz == 0.0) return 0.0;
    /* Phase step per sample = +/-2*pi * freq / fs.
     * [2026-08-22] The sign is +. Once the AFC loop was really closed
     * (apply_phase moved into c54x_bsp_load, where the feeds converge), the
     * negative sign acted as POSITIVE feedback and the DAC ran away from -700 to
     * 4095 (+34 kHz) instead of converging: it does not match the sign of the
     * emulated DSP freq_error measurement. A/B with
     * CALYPSO_TWL3025_AFC_SIGN_OLD=1 to restore the old minus sign. */
    /* [2026-09-20] Sign is MINUS again. The 2026-08-22 flip to plus was measured
     * against a DSP whose FB angle was garbage (ADD/SUB Smem,16, the 0xF4 group
     * and CPL addressing were wrong, see c54x_exe tools/isa_test). With the ISA
     * fixed, plus is POSITIVE feedback on the deterministic replay (df -146 ->
     * -182 -> -456 -> -799 -> -1714 -> -3421 Hz, doubling per FB) and minus
     * converges to |df| < 110 Hz. CALYPSO_TWL3025_AFC_SIGN_PLUS=1 for A/B. */
    static int sign_plus = -1;
    if (sign_plus < 0) sign_plus = calypso_getenv("CALYPSO_TWL3025_AFC_SIGN_PLUS") ? 1 : 0;
    return (sign_plus ? 1.0 : -1.0) * 2.0 * M_PI * hz / GSM_SAMPLE_RATE_HZ;
}

void calypso_twl3025_apply_phase(int16_t *iq_samples, int n_samples,
                                 uint32_t fn, uint8_t tn)
{
    twl3025_lazy_env();
    twl.apply_calls++;

    double step = calypso_twl3025_get_afc_phase_step();
    /* AFC-APPLY probe: confirms this function runs per burst and with which
     * offset. A large offset (e.g. +200900 Hz) applied to every burst pushes the
     * FCCH outside the +/-20 kHz DSP capture range, so FB is never detected and
     * FBSB loops. */
    if (calypso_debug_enabled("AFC-APPLY") &&
        (twl.apply_calls <= 20 || (twl.apply_calls % 2000) == 0)) {
        fprintf(stderr, "[twl3025] AFC-APPLY #%llu hz=%.1f dac=%d step=%.6f "
                "n=%d fn=%u tn=%u\n",
                (unsigned long long)twl.apply_calls,
                calypso_twl3025_get_afc_hz(), twl.dac_value, step,
                n_samples, fn, tn);
        fflush(stderr);
    }
    if (step == 0.0) return;   /* DAC at baseline and no force_hz: no-op */

    /* Absolute sample offset from FN=0,TN=0 is the system reference phase, which
     * keeps the phase continuous across bursts: burst N+1 starts at
     * (N+1) * 1250 + tn * 156, so cos/sin stay coherent. */
    uint64_t sample_offset = (uint64_t)fn * SAMPLES_PER_FRAME
                           + (uint64_t)tn * SAMPLES_PER_SLOT;

    for (int i = 0; i < n_samples; i++) {
        double ph = step * (double)(sample_offset + (uint64_t)i);
        double c = cos(ph), s = sin(ph);
        int16_t I = iq_samples[2 * i];
        int16_t Q = iq_samples[2 * i + 1];
        double new_I = I * c - Q * s;
        double new_Q = I * s + Q * c;
        if (new_I >  32767.0) new_I =  32767.0;
        if (new_I < -32768.0) new_I = -32768.0;
        if (new_Q >  32767.0) new_Q =  32767.0;
        if (new_Q < -32768.0) new_Q = -32768.0;
        iq_samples[2 * i]     = (int16_t)new_I;
        iq_samples[2 * i + 1] = (int16_t)new_Q;
    }
}

void calypso_twl3025_reset(void)
{
    twl.dac_value   = 0;
    twl.dac_writes  = 0;
    twl.apply_calls = 0;
    /* env_loaded kept: the environment is not reloaded on reset (chip state). */
}

4.74 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_twl3025.h

1964 octets, 49 lignes → 49 lignes

/*
 * calypso_twl3025.h — TWL3025 ABB AFC propagation API.
 *
 * Minimal model that propagates the firmware AFC (Automatic Frequency
 * Correction) down to the BSP samples. See calypso_twl3025.c for the full
 * chain.
 *
 * Enabled by CALYPSO_TWL3025_AFC=1; CALYPSO_TWL3025_AFC_HZ=N forces a constant
 * offset in Hz for tests.
 */
#ifndef CALYPSO_TWL3025_H
#define CALYPSO_TWL3025_H

#include "qemu/osdep.h"

/* Set the DAC value (TWL3025 register as decoded from the DSP TSP stream).
 * Called when a d_afc write is seen on the QEMU side. */
void calypso_twl3025_set_afc_dac(int16_t dac_value);

/* Read the current DAC (signed, +/-4095). */
int16_t calypso_twl3025_get_afc_dac(void);

/* Get current AFC offset in Hz (DAC * slope). */
double calypso_twl3025_get_afc_hz(void);

/* Phase step per sample (rad) for the sample rotation in the BSP receive
 * path. Returns 0.0 when AFC propagation is disabled (CALYPSO_TWL3025_AFC
 * != 1). Sign convention: a positive VCXO pull shifts the received samples the
 * other way, so the step is the rotation to apply to the raw samples to match
 * the local oscillator. */
double calypso_twl3025_get_afc_phase_step(void);

/* Apply the AFC rotation in place to N I/Q samples (interleaved int16).
 * No-op when AFC is disabled or dac_value == 0.
 *
 * [2026-05-28] The phase of sample i comes from (fn, tn, i) alone, never from
 * an accumulator:
 *   sample_offset = fn * SAMPLES_PER_FRAME + tn * SAMPLES_PER_SLOT
 *   phase(i) = step * (sample_offset + i)
 * That removes drift (each burst recomputes from fn), tolerates bursts that
 * arrive out of order or duplicated, and accounts for the inter-burst TDMA
 * gaps for free, since fn has advanced. */
void calypso_twl3025_apply_phase(int16_t *iq_samples, int n_samples,
                                 uint32_t fn, uint8_t tn);

/* Reset the DAC and the sample counters. Call on a DSP/BSP reset. */
void calypso_twl3025_reset(void);

#endif /* CALYPSO_TWL3025_H */

4.75 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_vsp.c

6972 octets, 151 lignes → 151 lignes

/* calypso_vsp.c - voice serial port (VSP) of the Calypso DSP.
 *
 * The 3606 ROM contains the whole voice path: DL decoder (task 0x7355, codec
 * through the dispatcher *(0x3f64) code 43), UL encoder (task 0x7400, code 34),
 * and the per-sample ISR 0xc14a (vector 20, IMR bit 4) which reads DRR (data
 * 0x20, mvkd 0x20) and writes DXR (data 0x21, mvmd bh,0x21) at 8 kHz, with a
 * 31-coefficient FIR and the d_audio_gain_ul/dl gains. On the Calypso this SP0
 * port is wired to the ABB (TWL3025, voice codec) through the VSP. Without this
 * model MMR 0x20-0x22 are inert, IRQ 20 is never raised, the 0x3e00 (UL) /
 * 0x4000 (DL) rings are never filled nor drained, and speech goes through GAPK
 * on the host.
 *
 * This model does not hook data_read/data_write (c54x_rapide fast path): for
 * every sample it sets DRR, raises IRQ 20 if the ROM has armed the port (IMR
 * bit 4, SPC = 0xC8: XRST|RRST), runs the ISR, then collects DXR. Rate: 960
 * samples per 26 frames (exactly 8 kHz relative to the fn), i.e. 36 or 37 per
 * frame -- the ROM's regulation (0xc4f7, ring gap > 60) skips frames if the
 * rate drifts.
 *
 * Exchange with the host: two shm rings (calypso_vsp.h), non-blocking; an
 * empty UL ring yields silence, a full DL ring overwrites the oldest sample.
 * pont/vsp_audio.py connects them to PulseAudio (gsm_mic.monitor -> UL, DL ->
 * gsm_audio), the same points as GAPK and as the "voice" stage of banc-max.
 *
 * CALYPSO_VSP=1 enables it (default 0: GAPK on the host). The mobile must then
 * run with `io-handler l1phy` (BANC_VOCODEUR=rom) so that the firmware lifts
 * the vocoder mutes (prim_tch.c:101-111). */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include "calypso_vsp.h"
#include "c54x_internal.h"
#include "hw/arm/calypso/calypso_debug.h"

typedef struct {
    uint32_t ecrit;
    uint32_t taille;
    int16_t  pcm[VSP_SHM_N];
} VspAnneau;

static VspAnneau *anneau_ouvrir(const char *nom)
{
    int fd = open(nom, O_RDWR | O_CREAT, 0666);
    if (fd < 0) return NULL;
    if (ftruncate(fd, sizeof(VspAnneau)) < 0) { close(fd); return NULL; }
    void *p = mmap(NULL, sizeof(VspAnneau), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);
    if (p == MAP_FAILED) return NULL;
    VspAnneau *a = p;
    a->taille = VSP_SHM_N;
    return a;
}

bool calypso_vsp_actif(void)
{
    static int on = -1;
    if (on < 0) {
        const char *e = calypso_getenv("CALYPSO_VSP");
        on = (e && *e == '1') ? 1 : 0;
        if (on) printf("  [vsp] port voix SP0 modelise (CALYPSO_VSP=1) : DRR 0x20 <- %s, DXR 0x21 -> %s, IT 20\n",
                       VSP_SHM_UL, VSP_SHM_DL);
    }
    return on;
}

static struct {
    VspAnneau *ul, *dl;
    uint32_t   lu_ul;          /* read position in the UL ring */
    unsigned   reste;          /* 960/26 accumulator */
    unsigned long rint, arme, desarme, sous_ul, trames, saut_ul;
    long       dl_max, ul_max;
    uint32_t   file_ul_max;    /* largest UL read lag over the period */
} v;

long calypso_vsp_trame(C54xState *dsp, calypso_vsp_courir_fn courir, uint32_t fn)
{
    if (!calypso_vsp_actif() || !dsp || !dsp->running) return 0;
    {   /* CALYPSO_VSP_SHM=0: no rings (offline replay, without touching the bench) */
        static int shm = -1;
        if (shm < 0) { const char *e = calypso_getenv("CALYPSO_VSP_SHM"); shm = !(e && *e == '0'); }
        if (shm && !v.ul) v.ul = anneau_ouvrir(VSP_SHM_UL);
        if (shm && !v.dl) v.dl = anneau_ouvrir(VSP_SHM_DL);
    }
    v.trames++;
    v.reste += 960;
    unsigned n = v.reste / 26;
    v.reste %= 26;
    long fait = 0;
    for (unsigned i = 0; i < n; i++) {
        uint16_t spc = dsp->data[VSP_SPC];
        bool arme = (dsp->imr & (1u << VSP_IT_BIT)) && (spc & 0xC0) == 0xC0;
        if (!arme) { v.desarme++; continue; }
        v.arme++;
        /* microphone sample -> DRR */
        int16_t ech = 0;
        if (v.ul) {
            uint32_t ecrit = __atomic_load_n(&v.ul->ecrit, __ATOMIC_ACQUIRE);
            /* BOUNDED read lag. A DSP running late at startup (camping, ~1000 slow
             * frames) would otherwise leave the microphone up to VSP_SHM_N (2 s) behind
             * for the whole life of the stack. CALYPSO_VSP_UL_MAX_MS (default 120) bounds
             * that lag: beyond it the read position jumps to half of it, and the jump
             * is counted. */
            static uint32_t ul_max_ech;
            if (!ul_max_ech) { const char *e = calypso_getenv("CALYPSO_VSP_UL_MAX_MS");
                ul_max_ech = (uint32_t)((e && *e ? atoi(e) : 120) * 8); if (ul_max_ech < 160) ul_max_ech = 160; }
            uint32_t file = ecrit - v.lu_ul;
            if (file > v.file_ul_max) v.file_ul_max = file;
            if (file > ul_max_ech) { v.lu_ul = ecrit - ul_max_ech / 2; v.saut_ul++; }
            if (v.lu_ul != ecrit) ech = v.ul->pcm[v.lu_ul++ % VSP_SHM_N];
            else v.sous_ul++;
            if (abs(ech) > v.ul_max) v.ul_max = abs(ech);
        }
        if (!v.ul) {   /* CALYPSO_VSP_UL_FICHIER=path: microphone read from an s16le file (replay) */
            static FILE *fu; static int lu;
            if (!lu) { lu = 1; const char *e = calypso_getenv("CALYPSO_VSP_UL_FICHIER"); if (e && *e) fu = fopen(e, "rb"); }
            if (fu && fread(&ech, 2, 1, fu) != 1) { rewind(fu); if (fread(&ech, 2, 1, fu) != 1) ech = 0; }
        }
        dsp->data[VSP_DRR] = (uint16_t)ech;
        c54x_interrupt_ex(dsp, VSP_IT_VEC, VSP_IT_BIT);
        if (dsp->idle && (dsp->ifr & dsp->imr) && !(dsp->st1 & 0x800)) dsp->idle = false;
        uint32_t avant = dsp->insn_count;
        if (!dsp->idle && courir) courir(dsp, 400);
        fait += dsp->insn_count - avant;
        v.rint++;
        /* DXR -> loudspeaker sample */
        int16_t sortie = (int16_t)dsp->data[VSP_DXR];
        {   /* CALYPSO_VSP_DL_FICHIER=path: raw s16le copy of the output (replay, offline analysis) */
            static FILE *f; static int lu;
            if (!lu) { lu = 1; const char *e = calypso_getenv("CALYPSO_VSP_DL_FICHIER"); if (e && *e) f = fopen(e, "wb"); }
            if (f) fwrite(&sortie, 2, 1, f);
        }
        if (abs(sortie) > v.dl_max) v.dl_max = abs(sortie);
        if (v.dl) {
            uint32_t w = v.dl->ecrit;
            v.dl->pcm[w % VSP_SHM_N] = sortie;
            __atomic_store_n(&v.dl->ecrit, w + 1, __ATOMIC_RELEASE);
        }
    }
    if (v.trames % 1000 == 0) {
        printf("  [vsp] fn=%u sur 1000 trames : rint=%lu (arme %lu, desarme %lu) sous-alim UL=%lu | crete UL %ld DL %ld | SPC=0x%04x IMR=0x%04x | retard micro max %u ms, sauts %lu\n",
               fn, v.rint, v.arme, v.desarme, v.sous_ul, v.ul_max, v.dl_max, dsp->data[VSP_SPC], dsp->imr,
               v.file_ul_max / 8, v.saut_ul);
        fflush(stdout);
        v.rint = v.arme = v.desarme = v.sous_ul = v.saut_ul = 0; v.dl_max = v.ul_max = 0; v.file_ul_max = 0;
    }
    return fait;
}

4.76 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-dsp/calypso_vsp.h

1465 octets, 33 lignes → 33 lignes

/* calypso_vsp.h - voice serial port (VSP) of the Calypso DSP: C54x SP0 to the ABB.
 * See calypso_vsp.c. */
#ifndef CALYPSO_VSP_H
#define CALYPSO_VSP_H

#include <stdbool.h>
#include "calypso_c54x.h"

/* Memory-mapped registers of serial port SP0 (C54x). */
#define VSP_DRR   0x0020   /* receive: microphone sample (ABB -> DSP) */
#define VSP_DXR   0x0021   /* transmit: speaker sample (DSP -> ABB) */
#define VSP_SPC   0x0022   /* control: 0x08 reset, 0xC8 running (ROM 0xbd33/0xbd37) */

/* Voice port interrupt: IMR bit 4, vector 20 (ROM ISR 0x72d3 -> 0xc14a). */
#define VSP_IT_VEC  20
#define VSP_IT_BIT  4

/* Exchange rings with the host (pont/vsp_audio.py): 16-bit PCM, 8 kHz, mono.
 * Header: uint32 written (count of samples written), uint32 size. */
#define VSP_SHM_UL  "/dev/shm/calypso_pcm_ul"   /* microphone -> DSP (written by the host) */
#define VSP_SHM_DL  "/dev/shm/calypso_pcm_dl"   /* DSP -> speaker (written here) */
#define VSP_SHM_N   16000                        /* 2 s */

typedef int (*calypso_vsp_courir_fn)(C54xState *dsp, int budget);

bool calypso_vsp_actif(void);
/* One TDMA frame: delivers the samples due (960 per 26 frames, 36 or 37),
 * each = DRR, interrupt 20, ROM ISR, DXR read-out. Returns the number of
 * instructions executed. No effect if CALYPSO_VSP != 1 or if the ROM has not
 * armed the port (IMR bit 4 and SPC running). */
long calypso_vsp_trame(C54xState *dsp, calypso_vsp_courir_fn courir, uint32_t fn);

#endif

4.77 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-grgsm/calypso_l1.h

680 octets, 22 lignes → 22 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef HW_ARM_CALYPSO_L1_H
#define HW_ARM_CALYPSO_L1_H

#include <stdint.h>
#include <stdbool.h>

void calypso_l1_init(const char *firmware_elf);
void calypso_l1_frame_tick(void);
void calypso_l1_page_written(uint16_t d_dsp_page);
void calypso_l1_burst_written(uint16_t d_burst_d);
void calypso_l1_rach_written(uint16_t d_rach, uint32_t fn);
bool calypso_l1_read_override(uint32_t off, uint16_t *out);
bool calypso_l1_si_valid(void);
uint32_t calypso_l1s_fn(void);

void calypso_l1_dcch_set(int kind, int ss);
void calypso_l1_dcch_active(void);
void calypso_l1_dcch_is_tch(bool on);
void calypso_l1_rach_conf(uint32_t fn);

#endif

4.78 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-grgsm/calypso_l1_grgsm.c

33630 octets, 1231 lignes → 1226 lignes (4 groupes compactés)

/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "qemu/osdep.h"
#include "qemu/main-loop.h"
#include "qemu/bswap.h"
#include "exec/cpu-common.h"
#include "hw/arm/calypso/calypso_api.h"
#include "calypso_l1.h"
#include "calypso_l1ctl_tap.h"
#include "hw/arm/calypso/calypso_trf6151.h"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#define GSMTAP_PORT             4730
#define SCH_PORT                4731
#define GSMTAP_HDR_LEN          16
#define GSMTAP_TYPE_UM          0x01
#define GSMTAP_CHANNEL_BCCH     0x01
#define GSMTAP_CHANNEL_AGCH     0x04
#define GSMTAP_CHANNEL_SDCCH4   0x07
#define GSMTAP_CHANNEL_TCH_F    0x09
#define GSMTAP_CHANNEL_ACCH     0x80
#define GSMTAP_CHANNEL_SACCH    (GSMTAP_CHANNEL_SDCCH4 | GSMTAP_CHANNEL_ACCH)
#define GSMTAP_CHANNEL_TCH_ACCH (GSMTAP_CHANNEL_TCH_F | GSMTAP_CHANNEL_ACCH)
/* SI rotation slots: SI1..SI6 (0x19..0x1e), then SI2bis, SI2ter, SI2quater,
 * SI13 - see si_slot(). */
#define SI_SLOTS                10

#define SHM_RACH        "/dev/shm/calypso_rach"
#define SHM_SDCCH_UL    "/dev/shm/calypso_sdcch_ul"
#define SHM_FACCH_UL    "/dev/shm/calypso_tch_facch_ul"
#define SHM_TSACCH_UL   "/dev/shm/calypso_tch_sacch_ul"
#define SHM_TCH_UL      "/dev/shm/calypso_tch_ul"
#define SHM_TCH_DL      "/dev/shm/calypso_tch_dl"
#define SHM_TCH_CFG     "/dev/shm/calypso_tch_cfg"
#define SHM_KC          "/dev/shm/calypso_kc_l1"

#define TOA_ON_TIME     23
#define SNR_NOMINAL     0x7000
#define RF_SERVING_DBM  (-60)
#define RF_SILENT_DBM   (-110)

#define SB_MAX_AGE_FRAMES   104
#define AGCH_TTL_TICKS      100
#define SDCCH_TTL_TICKS     4000
#define SDCCH_MAX_PRESENT   8
#define SDCCH_RING_N        32
#define SDCCH_UL_DEDUP_TICKS 60
#define SDCCH_UL_WINDOW_OFS 6
#define DCCH_GUARD_TICKS    108
#define TCH_DL_Q_N          8
#define TCH_DL_PREFETCH     4
#define TCH_DL_SLOT         48
#define TCH_DL_HOLD         5
#define TCH_TTL_TICKS       26
#define TCH_UL_SLOTS        16
#define TCH_UL_SLOT_SZ      64
#define KC_PUBLISH_EVERY    22
/* Delay, in publish_kc() calls (~100 ms each), before announcing a switch to
 * cleartext. The firmware clears d_a5mode on every DM_REL_REQ, including during
 * an Assignment Command where the Kc is reloaded right after, while the BTS
 * never stops ciphering. */
#define KC_CLEAR_GRACE      5
#define KC_RECLEN           32

#define L1_LOG(fmt, ...) fprintf(stderr, "[l1] " fmt "\n", ##__VA_ARGS__)

struct sdcch_entry {
    uint8_t  l2[23];
    uint32_t fn;
    uint32_t tick;
    uint16_t reps;
};

static struct {
    bool     pending;
    uint8_t  page;
    uint16_t d_task_md, d_task_d, d_task_u, d_task_ra, d_burst_d, d_fn;
    uint32_t tick;

    uint8_t  si_set[SI_SLOTS][23];
    bool     si_have[SI_SLOTS];
    uint8_t  si_buf[23];
    bool     si_valid;
    int      si_rr;
    unsigned si_rot;
    uint8_t  sacch_buf[23];
    bool     sacch_have;
    bool     sacch_real;

    int      serving_arfcn;
    uint8_t  sb_bsic;
    uint32_t sb_fn;
    bool     sb_valid;
    uint32_t sb_capture_fn;

    uint8_t  agch_buf[23];
    bool     agch_valid;
    uint32_t agch_tick;

    struct sdcch_entry sdcch_ring[SDCCH_RING_N];
    uint32_t sdcch_head, sdcch_tail;
    uint32_t sdcch_last_fn;
    bool     sdcch_valid;
    uint8_t  sdcch_ss;
    bool     sdcch_ss_set;
    bool     sdcch_ch8;

    bool     dcch_guard_armed;
    uint32_t dcch_guard_tick;
    bool     dcch_is_tch;

    uint8_t  tch_dl_q[TCH_DL_Q_N][33];
    uint32_t tch_dl_q_seq[TCH_DL_Q_N];
    unsigned tch_dl_q_head, tch_dl_q_tail;
    uint32_t tch_dl_seq;
    uint8_t  facch_dl[23];
    bool     facch_dl_valid;
    uint32_t facch_dl_tick;
    uint8_t  tsacch_dl[23];
    bool     tsacch_dl_valid;
    uint32_t tsacch_dl_tick;
    bool     tch_cfg_valid;

    uint8_t  last_ra;
    uint32_t rach_conf_fn[256];

    uint32_t l1s_addr;
    uint32_t last_rach_addr;
} g;

static uint32_t elf_symbol(const char *path, const char *want)
{
    FILE *f = fopen(path, "rb");
    if (!f) {
        return 0;
    }
    fseek(f, 0, SEEK_END);
    long sz = ftell(f);
    fseek(f, 0, SEEK_SET);
    if (sz < 52 || sz > (64L << 20)) {
        fclose(f);
        return 0;
    }
    uint8_t *b = g_malloc((size_t)sz);
    size_t got = fread(b, 1, (size_t)sz, f);
    fclose(f);
    uint32_t ret = 0;
    if (got == (size_t)sz && b[0] == 0x7f && b[1] == 'E' && b[2] == 'L' &&
        b[3] == 'F' && b[4] == 1) {
#define R16(o) ((uint32_t)b[o] | ((uint32_t)b[(o) + 1] << 8))
#define R32(o) (R16(o) | (R16((o) + 2) << 16))
        uint32_t shoff = R32(0x20), shent = R16(0x2e), shnum = R16(0x30);
        for (uint32_t si = 0; si < shnum; si++) {
            uint32_t sh = shoff + si * shent;
            if ((long)(sh + 40) > sz) {
                break;
            }
            if (R32(sh + 4) != 2) {
                continue;
            }
            uint32_t symoff = R32(sh + 0x10), symsz = R32(sh + 0x14);
            uint32_t link = R32(sh + 0x18), entsz = R32(sh + 0x24);
            uint32_t strsh = shoff + link * shent;
            if ((long)(strsh + 40) > sz || entsz < 16) {
                break;
            }
            uint32_t stroff = R32(strsh + 0x10), strsz = R32(strsh + 0x14);
            for (uint32_t o = 0; o + 16 <= symsz && (long)(symoff + o + 16) <= sz;
                 o += entsz) {
                uint32_t ni = R32(symoff + o);
                if (ni < strsz && !strcmp((const char *)(b + stroff + ni), want)) {
                    ret = R32(symoff + o + 4);
                    break;
                }
            }
            break;
        }
#undef R16
#undef R32
    }
    g_free(b);
    return ret;
}

static uint32_t arm_read32(uint32_t addr)
{
    uint32_t v = 0;
    if (!addr) {
        return 0;
    }
    cpu_physical_memory_read(addr, &v, sizeof(v));
    return le32_to_cpu(v);
}

uint32_t calypso_l1s_fn(void)
{
    return arm_read32(g.l1s_addr);
}

static uint32_t last_rach_fn(void)
{
    return arm_read32(g.last_rach_addr);
}

static int tc51(long fn)
{
    return (int)((fn % 51 + 51) % 51);
}

static void ndb_put_l2(uint16_t *w, const uint8_t *l2)
{
    for (int i = 0; i < 23; i += 2) {
        uint8_t lo = l2[i], hi = (i + 1 < 23) ? l2[i + 1] : 0x2B;
        w[3 + i / 2] = (uint16_t)(lo | (hi << 8));
    }
}

static void ndb_put_fr(uint16_t *w, const uint8_t *fr)
{
    for (int i = 0; i < 32; i += 2) {
        w[3 + i / 2] = (uint16_t)(((uint16_t)fr[i] << 8) | fr[i + 1]);
    }
    w[3 + 16] = (uint16_t)((uint16_t)fr[32] << 8);
}

static void ndb_block(unsigned off, uint16_t hdr, const uint8_t *l2)
{
    uint16_t *w = api_ndb(off);
    w[0] = hdr;
    w[1] = 0;
    w[2] = 0;
    ndb_put_l2(w, l2);
}

static void rp_serv_demod(uint8_t page, uint16_t toa, uint16_t pm)
{
    uint16_t *d = api_rp(page, RP_A_SERV_DEMOD);
    d[D_TOA] = toa;
    d[D_PM] = pm;
    d[D_ANGLE] = 0;
    d[D_SNR] = SNR_NOMINAL;
}

static bool on_serving_cell(void)
{
    int tuned = calypso_trf6151_arfcn();
    return tuned < 0 || tuned == g.serving_arfcn;
}

static uint16_t apm_nominal(void)
{
    return calypso_trf6151_apm_for_rf(on_serving_cell() ? RF_SERVING_DBM : RF_SILENT_DBM);
}

static uint32_t encode_sb(uint8_t bsic, uint32_t fn)
{
    uint16_t t1 = (uint16_t)(fn / (26u * 51u));
    uint8_t t2 = (uint8_t)(fn % 26u);
    uint8_t t3 = (uint8_t)(fn % 51u);
    uint8_t t3p = (t3 == 0) ? 0 : ((t3 - 1) / 10);
    uint32_t sb = 0;
    sb |= ((uint32_t)(bsic & 0x3f)) << 2;
    sb |= ((uint32_t)(t1 & 0x001)) << 23;
    sb |= ((uint32_t)(t1 & 0x1fe)) << 7;
    sb |= ((uint32_t)(t1 & 0x600)) >> 9;
    sb |= ((uint32_t)(t2 & 0x1f)) << 18;
    sb |= ((uint32_t)(t3p & 1)) << 24;
    sb |= ((uint32_t)(t3p & 6)) << 15;
    return sb;
}

static void dispatch_pm(uint8_t page)
{
    uint16_t pm = apm_nominal();
    uint16_t *a = api_rp(page, RP_A_PM);
    a[⟨1⟩] = pm;  ×3
    ⟨⟩ = (0) (1) (2)
    *api_rp(page, RP_D_TASK_MD) = PM_DSP_TASK;
}

static void dispatch_sb(uint8_t page)
{
    if (!g.sb_valid || !on_serving_cell()) {
        return;
    }
    if ((int)(calypso_trx_get_fn() - g.sb_capture_fn) > SB_MAX_AGE_FRAMES) {
        return;
    }
    uint32_t sb = encode_sb(g.sb_bsic, g.sb_fn);
    uint16_t *a = api_rp(page, RP_A_SCH);
    a[⟨1⟩] = 0;  ×3
    ⟨⟩ = (0) (1) (2)
    a[3] = (uint16_t)(sb & 0xFFFF);
    a[4] = (uint16_t)(sb >> 16);
    rp_serv_demod(page, TOA_ON_TIME, apm_nominal());
    *api_rp(page, RP_D_TASK_MD) = SB_DSP_TASK;
}

static void present_ccch(uint8_t page, const uint8_t *l2)
{
    ndb_block(NDB_A_CD, 0, l2);
    *api_rp(page, RP_D_TASK_D) = ALLC_DSP_TASK;
    rp_serv_demod(page, TOA_ON_TIME, apm_nominal());
}

static void rotate_si(void)
{
    for (int k = 1; k <= SI_SLOTS; k++) {
        int s = (g.si_rr + k) % SI_SLOTS;
        if (g.si_have[s]) {
            g.si_rr = s;
            return;
        }
    }
}

static bool dcch_si_guard(void)
{
    if (!g.dcch_guard_armed) {
        return false;
    }
    if (g.tick - g.dcch_guard_tick > DCCH_GUARD_TICKS) {
        g.dcch_guard_armed = false;
        calypso_l1ctl_tap_channel_released();
        L1_LOG("canal dedie libere");
        return false;
    }
    return true;
}

static void dispatch_allc(uint8_t page)
{
    if (g.agch_valid) {
        if (g.tick - g.agch_tick > AGCH_TTL_TICKS) {
            g.agch_valid = false;
        } else {
            int tc = tc51(calypso_l1s_fn());
            if ((tc >= 6 && tc <= 9) || (tc >= 12 && tc <= 19)) {
                present_ccch(page, g.agch_buf);
                return;
            }
        }
    }

    int lo = g.sdcch_ss_set ? (g.sdcch_ch8 ? 0 : 22) : 0;
    int hi = g.sdcch_ss_set ? (g.sdcch_ch8 ? 31 : 39) : 39;
    while (g.sdcch_tail != g.sdcch_head) {
        struct sdcch_entry *e = &g.sdcch_ring[g.sdcch_head % SDCCH_RING_N];
        if (g.tick - e->tick > SDCCH_TTL_TICKS) {
            g.sdcch_head++;
            continue;
        }
        int tc = tc51(calypso_l1s_fn());
        if (tc < lo || tc > hi) {
            break;
        }
        present_ccch(page, e->l2);
        e->reps++;
        if (g.d_burst_d >= 3 || e->reps >= SDCCH_MAX_PRESENT) {
            g.sdcch_head++;
        }
        if (g.sdcch_tail == g.sdcch_head) {
            g.sdcch_valid = false;
        }
        return;
    }
    if (g.sdcch_tail == g.sdcch_head) {
        g.sdcch_valid = false;
    }

    if (g.sacch_have) {
        int tc = tc51(calypso_l1s_fn());
        if (tc >= 42 && tc <= 46) {
            present_ccch(page, g.sacch_buf);
            return;
        }
    }

    if (g.sdcch_valid) {
        return;
    }
    if (g.d_burst_d == 0) {
        rotate_si();
        memcpy(g.si_buf, g.si_set[g.si_rr], 23);
    }
    if (g.dcch_guard_armed) {
        *api_ndb(NDB_A_CD) = 0x0003;
        *api_rp(page, RP_D_TASK_D) = ALLC_DSP_TASK;
        return;
    }
    ndb_block(NDB_A_CD, 0, g.si_buf);
    for (uint8_t p = 0; p < 2; p++) {
        *api_rp(p, RP_D_TASK_D) = ALLC_DSP_TASK;
        rp_serv_demod(p, TOA_ON_TIME, apm_nominal());
    }
}

static bool tch_fresh(bool valid, uint32_t tick)
{
    return valid && (g.tick - tick <= TCH_TTL_TICKS);
}

static void tch_serv_demod(void)
{
    uint16_t pm = apm_nominal();
    rp_serv_demod(0, TOA_ON_TIME, pm);
    rp_serv_demod(1, TOA_ON_TIME, pm);
}

static unsigned tch_dl_depth(void)
{
    return g.tch_dl_q_tail - g.tch_dl_q_head;
}

static void tch_dl_push(const uint8_t *fr, uint32_t seq)
{
    unsigned i = g.tch_dl_q_tail % TCH_DL_Q_N;
    memcpy(g.tch_dl_q[i], fr, 33);
    g.tch_dl_q_seq[i] = seq;
    g.tch_dl_q_tail++;
}

static void tch_dl_poll(void)
{
    static int fd = -2;
    if (fd == -2) {
        fd = open(SHM_TCH_DL, O_CREAT | O_RDWR, 0644);
    }
    if (fd < 0) {
        return;
    }
    uint8_t hdr[8];
    if (pread(fd, hdr, sizeof(hdr), 0) != (ssize_t)sizeof(hdr)) {
        return;
    }
    uint32_t w_seq, n_slots;
    memcpy(&w_seq, hdr, 4);
    memcpy(&n_slots, hdr + 4, 4);
    if (n_slots == 0 || n_slots > 4096 || w_seq == 0 || w_seq == g.tch_dl_seq) {
        return;
    }
    if (w_seq < g.tch_dl_seq) {
        g.tch_dl_seq = 0;
        g.tch_dl_q_head = g.tch_dl_q_tail = 0;
    }
    if (tch_dl_depth() >= TCH_DL_PREFETCH) {
        return;
    }
    uint32_t next = g.tch_dl_seq + 1;
    if (g.tch_dl_seq == 0 || (w_seq - next) >= n_slots) {
        uint32_t behind = (w_seq > next) ? (w_seq - next) : 0;
        if (g.tch_dl_seq && behind >= n_slots) {
            static unsigned nlog;
            if (nlog++ < 10) {
                L1_LOG("TCH DL : %u trames sautees (anneau de %u)",
                       behind - (n_slots - 1), n_slots);
            }
        }
        next = w_seq;
    }
    uint8_t buf[TCH_DL_SLOT];
    off_t off = 8 + (off_t)((next - 1) % n_slots) * TCH_DL_SLOT;
    if (pread(fd, buf, sizeof(buf), off) != (ssize_t)sizeof(buf)) {
        return;
    }
    uint32_t sseq;
    memcpy(&sseq, buf, 4);
    if (sseq != next) {
        return;
    }
    g.tch_dl_seq = next;
    tch_dl_push(buf + 8, next);
}

static void dispatch_tch_dl(void)
{
    tch_serv_demod();
    static unsigned waited;
    bool ready = true;
    if (*api_ndb(NDB_A_DD_0) & B_BLUD) {
        if (waited < TCH_DL_HOLD) {
            waited++;
            ready = false;
        }
    }
    if (ready) {
        waited = 0;
        if (g.tch_dl_q_head != g.tch_dl_q_tail) {
            unsigned i = g.tch_dl_q_head++ % TCH_DL_Q_N;
            uint16_t *w = api_ndb(NDB_A_DD_0);
            w[0] = B_BLUD;
            w[1] = 0;
            w[2] = 0;
            ndb_put_fr(w, g.tch_dl_q[i]);
        }
    }
    if (tch_fresh(g.facch_dl_valid, g.facch_dl_tick)) {
        ndb_block(NDB_A_FD, B_BLUD, g.facch_dl);
        g.facch_dl_valid = false;
    }
}

static void dispatch_tch_sacch(void)
{
    tch_serv_demod();
    if (!tch_fresh(g.tsacch_dl_valid, g.tsacch_dl_tick)) {
        return;
    }
    ndb_block(NDB_A_CD, B_BLUD, g.tsacch_dl);
    g.tsacch_dl_valid = false;
}

static void dcch_sacch_present(void)
{
    static const uint8_t base4[4] = { 42, 46, 93, 97 };
    static const uint8_t base8[8] = { 32, 36, 40, 44, 83, 87, 91, 95 };
    if (g.dcch_is_tch) {
        return;
    }
    uint32_t b;
    if (g.sdcch_ch8) {
        b = base8[(g.sdcch_ss / 4u) & 7u];
    } else {
        unsigned idx = 0;
        switch (g.sdcch_ss) {
        case ⟨1⟩: idx = ⟨2⟩; break;  ×3
    ⟨⟩ = (26,1) (32,2) (36,3)
        default: break;
        }
        b = base4[idx];
    }
    uint32_t f102 = calypso_trx_get_fn() % 102u;
    if (f102 < b || f102 > b + 3u) {
        return;
    }
    if (*api_ndb(NDB_A_CD) & B_BLUD) {
        return;
    }
    if (!g.sacch_have) {
        return;
    }
    ndb_block(NDB_A_CD, B_BLUD, g.sacch_buf);
}

static int sideband_open(const char *path, off_t len)
{
    int fd = open(path, O_CREAT | O_RDWR, 0644);
    if (fd >= 0 && ftruncate(fd, len) < 0) {
        close(fd);
        return -1;
    }
    return fd;
}

static void sideband_write(int fd, const void *buf, size_t n, off_t off)
{
    if (fd >= 0 && pwrite(fd, buf, n, off) < 0) {
        return;
    }
}

static void publish_l2(int *fdp, const char *path, uint32_t *seq, const uint8_t *l2,
                       uint16_t task_u)
{
    if (*fdp == -2) {
        *fdp = sideband_open(path, 48);
    }
    uint32_t fn = calypso_trx_get_fn(), l1s = calypso_l1s_fn();
    uint8_t buf[48] = {0};
    (*seq)++;
    memcpy(buf + 0, seq, 4);
    memcpy(buf + 4, &l1s, 4);
    memcpy(buf + 8, &fn, 4);
    memcpy(buf + 12, &task_u, 2);
    buf[14] = (uint8_t)(l1s % 51);
    memcpy(buf + 16, l2, 23);
    sideband_write(*fdp, buf, sizeof(buf), 0);
}

static void publish_speech(const uint8_t *fr)
{
    static int fd = -2;
    static uint32_t seq;
    if (fd == -2) {
        fd = sideband_open(SHM_TCH_UL, 8 + TCH_UL_SLOTS * TCH_UL_SLOT_SZ);
        uint32_t hdr[2] = { 0, TCH_UL_SLOTS };
        sideband_write(fd, hdr, sizeof(hdr), 0);
    }
    uint32_t fn = calypso_trx_get_fn(), l1s = calypso_l1s_fn();
    uint8_t buf[TCH_UL_SLOT_SZ] = {0};
    seq++;
    memcpy(buf + 0, &seq, 4);
    memcpy(buf + 4, &l1s, 4);
    memcpy(buf + 8, &fn, 4);
    memcpy(buf + 16, fr, 33);
    sideband_write(fd, buf, sizeof(buf), 8 + (off_t)((seq - 1) % TCH_UL_SLOTS) * TCH_UL_SLOT_SZ);
    sideband_write(fd, &seq, 4, 0);
}

static bool take_ul(unsigned off, uint8_t *out, int n)
{
    uint16_t *w = api_ndb(off);
    if (!(w[0] & B_BLUD)) {
        return false;
    }
    for (int i = 0; i < n; i += 2) {
        uint16_t v = w[3 + i / 2];
        uint8_t first = (n == 33) ? (uint8_t)(v >> 8) : (uint8_t)(v & 0xff);
        uint8_t second = (n == 33) ? (uint8_t)(v & 0xff) : (uint8_t)(v >> 8);
        out[i] = first;
        if (i + 1 < n) {
            out[i + 1] = second;
        }
    }
    w[0] &= (uint16_t)~B_BLUD;
    return true;
}

static bool capture_tch_ul(uint16_t task_u)
{
    static int fd_facch = -2, fd_sacch = -2;
    static uint32_t seq_facch, seq_sacch;
    uint8_t l2[23], fr[33];
    switch (task_u & 0x7FFF) {
    case TCHT_DSP_TASK:
        if (take_ul(NDB_A_FU, l2, 23)) {
            publish_l2(&fd_facch, SHM_FACCH_UL, &seq_facch, l2, task_u);
        }
        if (take_ul(NDB_A_DU_1, fr, 33)) {
            publish_speech(fr);
        }
        return true;
    case TCHA_DSP_TASK:
        if (take_ul(NDB_A_CU, l2, 23)) {
            publish_l2(&fd_sacch, SHM_TSACCH_UL, &seq_sacch, l2, task_u);
        }
        return true;
    case TCHD_DSP_TASK:
        return true;
    default:
        return false;
    }
}

static void capture_sdcch_ul(uint16_t task_u)
{
    static int fd = -2;
    static uint32_t seq;
    static uint8_t last[23];
    static uint32_t last_tick;
    static bool have_last;

    uint8_t win[30];
    const uint8_t *src = (const uint8_t *)api_ndb(NDB_A_CU + SDCCH_UL_WINDOW_OFS);
    memcpy(win, src, sizeof(win));
    int kk = 0;
    for (int j = 0; j <= 6; j++) {
        uint8_t a = win[j], c = win[j + 1], l = win[j + 2];
        int sapi = (a >> 2) & 7;
        bool addr_ok = (a & 0x01) && ((a & 0x60) == 0) && (sapi == 0 || sapi == 3);
        bool ctrl_ok = (c != 0x2b) && (c != 0xff);
        bool len_ok = (l & 0x01) && ((l >> 2) <= 20);
        if (addr_ok && ctrl_ok && len_ok) {
            kk = j;
            break;
        }
    }
    const uint8_t *l2 = win + kk;
    if (have_last && !memcmp(last, l2, 23) && g.tick - last_tick < SDCCH_UL_DEDUP_TICKS) {
        return;
    }
    memcpy(last, l2, 23);
    last_tick = g.tick;
    have_last = true;
    if (l2[1] == 0x03) {
        return;
    }
    publish_l2(&fd, SHM_SDCCH_UL, &seq, l2, task_u);
}

static void l1_reset(void)
{
    static unsigned n;
    if (n++ < 3) {
        L1_LOG("reset L1 (d_dsp_page=0)");
    }
    g.agch_valid = false;
    g.sdcch_valid = false;
    g.sdcch_ss_set = false;
    g.dcch_guard_armed = false;
    calypso_l1ctl_tap_channel_released();
}

void calypso_l1_page_written(uint16_t v)
{
    if (!(v & B_GSM_TASK)) {
        if (v == 0) {
            l1_reset();
        }
        return;
    }
    uint8_t page = (v & B_GSM_PAGE) ? 1 : 0;
    g.page = page;
    g.d_task_d = *api_wp(page, WP_D_TASK_D);
    g.d_task_u = *api_wp(page, WP_D_TASK_U);
    g.d_task_md = *api_wp(page, WP_D_TASK_MD);
    g.d_task_ra = *api_wp(page, WP_D_TASK_RA);
    g.d_fn = *api_wp(page, WP_D_FN);
    if (g.d_fn == 0) {
        g.d_fn = (uint16_t)(calypso_trx_get_fn() & 0xFFFF);
    }
    g.pending = true;

    if (g.dcch_guard_armed &&
        (g.d_task_u == DUL_DSP_TASK || g.d_task_u == TCHT_DSP_TASK ||
         g.d_task_u == TCHA_DSP_TASK)) {
        g.dcch_guard_tick = g.tick;
    }
    if (g.d_task_u != 0 && !capture_tch_ul(g.d_task_u)) {
        capture_sdcch_ul(g.d_task_u);
    }
}

void calypso_l1_burst_written(uint16_t d_burst_d)
{
    g.d_burst_d = (uint16_t)(d_burst_d & 3);
}

void calypso_l1_rach_written(uint16_t d_rach, uint32_t fn)
{
    static int fd = -2;
    static uint32_t seq;
    if (fd == -2) {
        fd = sideband_open(SHM_RACH, 16);
    }
    uint8_t ra = (uint8_t)(d_rach >> 8);
    uint8_t bsic = (uint8_t)((d_rach & 0xFF) >> 2);
    uint8_t buf[16] = {0};
    seq++;
    memcpy(buf + 0, &seq, 4);
    buf[4] = ra;
    buf[5] = bsic;
    memcpy(buf + 8, &fn, 4);
    sideband_write(fd, buf, sizeof(buf), 0);
    g.last_ra = ra;
}

void calypso_l1_rach_conf(uint32_t fn)
{
    g.rach_conf_fn[g.last_ra] = fn;
}

bool calypso_l1_read_override(uint32_t off, uint16_t *out)
{
    if (!g.sb_valid || !on_serving_cell()) {
        switch (off) {
        case API_NDB + NDB_D_FB_DET:
        case API_NDB + NDB_A_SYNC_DEMOD + 2 * D_TOA:
        case API_NDB + NDB_A_SYNC_DEMOD + 2 * D_PM:
        case API_NDB + NDB_A_SYNC_DEMOD + 2 * D_ANGLE:
        case API_NDB + NDB_A_SYNC_DEMOD + 2 * D_SNR:
            *out = 0;
            return true;
        default:
            return false;
        }
    }
    switch (off) {
    case API_NDB + NDB_D_FB_DET:
        *out = 1;
        return true;
    case API_NDB + NDB_A_SYNC_DEMOD + 2 * D_PM:
        *out = apm_nominal();
        return true;
    case API_NDB + NDB_A_SYNC_DEMOD + 2 * D_TOA:
    case API_NDB + NDB_A_SYNC_DEMOD + 2 * D_ANGLE:
    case API_R_PAGE(0) + RP_A_SERV_DEMOD + 2 * D_TOA:
    case API_R_PAGE(1) + RP_A_SERV_DEMOD + 2 * D_TOA:
        *out = 0;
        return true;
    case API_NDB + NDB_A_SYNC_DEMOD + 2 * D_SNR:
        *out = SNR_NOMINAL;
        return true;
    default:
        return false;
    }
}

bool calypso_l1_si_valid(void)
{
    return g.si_valid;
}

void calypso_l1_dcch_set(int kind, int ss)
{
    static const uint8_t b4[4] = { 22, 26, 32, 36 };
    uint8_t base = kind ? (uint8_t)((ss & 7) * 4) : b4[ss & 3];
    if (g.sdcch_ss_set && g.sdcch_ss == base && g.sdcch_ch8 == (kind != 0)) {
        return;
    }
    g.sdcch_ss = base;
    g.sdcch_ss_set = true;
    g.sdcch_ch8 = (kind != 0);
    L1_LOG("canal dedie SDCCH/%d SS=%d : fenetre a_cd fn%%51 %u-%u",
           kind ? 8 : 4, ss, base, base + 3);
}

void calypso_l1_dcch_active(void)
{
    g.dcch_guard_tick = g.tick;
    g.dcch_guard_armed = true;
}

void calypso_l1_dcch_is_tch(bool on)
{
    g.dcch_is_tch = on;
}

static void feed_agch(const uint8_t *l2, int len)
{
    uint8_t mt = l2[2];
    bool imm = (mt == 0x3f || mt == 0x3a || mt == 0x3b);
    uint8_t cur = g.agch_buf[2];
    bool cur_imm = (cur == 0x3f || cur == 0x3a || cur == 0x3b);
    if (!imm && g.agch_valid && cur_imm && g.tick - g.agch_tick <= AGCH_TTL_TICKS) {
        return;
    }
    int n = len < 23 ? len : 23;
    memcpy(g.agch_buf, l2, n);
    memset(g.agch_buf + n, 0x2B, 23 - n);

    if (mt == 0x3f && n >= 10) {
        uint8_t ra = g.agch_buf[7];
        uint32_t lr = last_rach_fn();
        uint32_t memo = lr ? lr : g.rach_conf_fn[ra];
        if (memo) {
            uint16_t t1p = (uint16_t)((memo / 1326u) % 32u);
            uint8_t t2 = (uint8_t)(memo % 26u);
            uint8_t t3 = (uint8_t)(memo % 51u);
            g.agch_buf[8] = (uint8_t)((t1p << 3) | ((t3 >> 3) & 7));
            g.agch_buf[9] = (uint8_t)(((t3 & 7) << 5) | (t2 & 0x1f));
        }
    }
    g.agch_valid = true;
    g.agch_tick = g.tick;
}

static void feed_sdcch(const uint8_t *l2, int len, uint32_t fn)
{
    if (fn && fn == g.sdcch_last_fn) {
        return;
    }
    g.sdcch_last_fn = fn;
    if (g.sdcch_tail - g.sdcch_head >= SDCCH_RING_N) {
        static unsigned nlog;
        if (nlog++ < 5) {
            L1_LOG("SDCCH DL : anneau plein, plus ancien bloc abandonne");
        }
        g.sdcch_head++;
    }
    struct sdcch_entry *e = &g.sdcch_ring[g.sdcch_tail % SDCCH_RING_N];
    int n = len < 23 ? len : 23;
    memcpy(e->l2, l2, n);
    memset(e->l2 + n, 0x2B, 23 - n);
    e->fn = fn;
    e->tick = g.tick;
    e->reps = 0;
    g.sdcch_tail++;
    g.sdcch_valid = true;
}

static void feed_sacch(const uint8_t *l2, int len)
{
    int n = len < 23 ? len : 23;
    bool si56 = false;
    for (int i = 2; i + 1 < n && i < 8; i++) {
        if (l2[i] == 0x06 && (l2[i + 1] == 0x1d || l2[i + 1] == 0x1e)) {
            si56 = true;
            break;
        }
    }
    if (!si56) {
        return;
    }
    memcpy(g.sacch_buf, l2, n);
    memset(g.sacch_buf + n, 0x2b, 23 - n);
    g.sacch_have = true;
    g.sacch_real = true;
}

static void feed_l2_23(uint8_t *dst, bool *valid, uint32_t *tick,
                       const uint8_t *l2, int len)
{
    int n = len < 23 ? len : 23;
    memcpy(dst, l2, n);
    memset(dst + n, 0x2B, 23 - n);
    *valid = true;
    *tick = g.tick;
}

/* The BCCH SIs that the rotation serves to the firmware: SI1..SI6
 * (0x19..0x1e) plus SI2bis (0x02), SI2ter (0x03), SI2quater (0x07) and SI13
 * (0x00). As soon as BTS 0 has a neighbour in another band (BTS 1 DCS1800,
 * ARFCN 614), osmo-bsc emits an SI2ter and announces it in the SI3 rest
 * octets; the mobile (gsm322.c, gsm322_c_scan_sysinfo_bcch) then requires
 * that SI2ter before selecting the cell. SI13 is used by GPRS (data bar). */
static int si_slot(uint8_t mt)
{
    if (mt >= 0x19 && mt <= 0x1e) {
        return mt - 0x19;           /* SI1..SI6 */
    }
    switch (mt) {
    case 0x02: return 6;            /* SI2bis */
    case 0x03: return 7;            /* SI2ter */
    case 0x07: return 8;            /* SI2quater */
    case 0x00: return 9;            /* SI13 */
    default:   return -1;
    }
}

static void feed_si(const uint8_t *l2, int len)
{
    int n = len < 23 ? len : 23;
    int slot = -1;
    if (n >= 3 && l2[1] == 0x06) {
        slot = si_slot(l2[2]);
    }
    if (slot >= 0) {
        memcpy(g.si_set[slot], l2, n);
        memset(g.si_set[slot] + n, 0x2B, 23 - n);
        g.si_have[slot] = true;
    }
    if (slot == 2 && n >= 10 && !g.sacch_real) {
        uint8_t *s6 = g.sacch_buf;
        memset(s6, 0x2b, 23);
        s6[⟨1⟩] = 0x⟨2⟩;  ×4
    ⟨⟩ = (0,00) (1,00) (2,03) (3,03)
        s6[4] = (uint8_t)((11 << 2) | 0x01);
        s6[5] = 0x06;
        s6[6] = 0x1e;
        memcpy(s6 + 7, l2 + 3, 7);
        s6[14] = 0x0f;
        s6[15] = 0xff;
        g.sacch_have = true;
    }
    memcpy(g.si_buf, l2, n);
    memset(g.si_buf + n, 0x2B, 23 - n);
    g.si_valid = true;
}

static void gsmtap_readable(void *opaque)
{
    int fd = (int)(intptr_t)opaque;
    uint8_t buf[512];
    for (;;) {
        ssize_t n = recv(fd, buf, sizeof(buf), MSG_DONTWAIT);
        if (n < GSMTAP_HDR_LEN + 3) {
            if (n < 0) {
                break;
            }
            continue;
        }
        if (buf[2] != GSMTAP_TYPE_UM) {
            continue;
        }
        uint8_t sub = buf[12];
        const uint8_t *l2 = buf + GSMTAP_HDR_LEN;
        int len = (int)n - GSMTAP_HDR_LEN;
        uint8_t mt = l2[2];
        switch (sub) {
        case GSMTAP_CHANNEL_BCCH:
            if (l2[1] == 0x06 && si_slot(mt) >= 0) {
                feed_si(l2, len);
            }
            break;
        case GSMTAP_CHANNEL_AGCH:
            if (l2[1] == 0x06 && (mt == 0x3f || mt == 0x39 || mt == 0x3a ||
                                  mt == 0x21 || mt == 0x22 || mt == 0x24)) {
                feed_agch(l2, len);
            }
            break;
        case GSMTAP_CHANNEL_SDCCH4:
            feed_sdcch(l2, len, ldl_be_p(buf + 8));
            break;
        case GSMTAP_CHANNEL_SACCH:
            if (len >= 7) {
                feed_sacch(l2, len);
            }
            break;
        case GSMTAP_CHANNEL_TCH_F:
            feed_l2_23(g.facch_dl, &g.facch_dl_valid, &g.facch_dl_tick, l2, len);
            break;
        case GSMTAP_CHANNEL_TCH_ACCH:
            feed_l2_23(g.tsacch_dl, &g.tsacch_dl_valid, &g.tsacch_dl_tick, l2, len);
            break;
        default:
            break;
        }
    }
}

static void sch_readable(void *opaque)
{
    int fd = (int)(intptr_t)opaque;
    uint8_t buf[64];
    for (;;) {
        ssize_t n = recv(fd, buf, sizeof(buf), MSG_DONTWAIT);
        if (n < 0) {
            break;
        }
        if (n < 16 || memcmp(buf, "SCH2", 4) != 0) {
            continue;
        }
        int32_t bsic = (int32_t)ldl_le_p(buf + 4);
        int32_t fn = (int32_t)ldl_le_p(buf + 8);
        int32_t arfcn = (int32_t)ldl_le_p(buf + 12);
        bool first = !g.sb_valid;
        g.sb_bsic = (uint8_t)(bsic & 0x3f);
        g.sb_fn = (uint32_t)fn;
        g.serving_arfcn = arfcn;
        g.sb_valid = true;
        g.sb_capture_fn = calypso_trx_get_fn();
        if (first) {
            L1_LOG("synchro gr-gsm : ARFCN=%d BSIC=%u FN=%u", arfcn, g.sb_bsic, g.sb_fn);
        }
        calypso_trx_autosync_fn((uint32_t)fn);
    }
}

static int udp_listen(uint16_t port, IOHandler *handler)
{
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0) {
        return -1;
    }
    int one = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
    struct sockaddr_in sa = {
        .sin_family = AF_INET,
        .sin_port = htons(port),
        .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
    };
    if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
        L1_LOG("bind udp/%u : %s", port, strerror(errno));
        close(fd);
        return -1;
    }
    qemu_set_fd_handler(fd, handler, NULL, (void *)(intptr_t)fd);
    return fd;
}

static void poll_tch_cfg(void)
{
    static int fd = -2;
    static uint32_t last;
    if (fd == -2) {
        fd = open(SHM_TCH_CFG, O_CREAT | O_RDWR, 0644);
    }
    if (fd < 0) {
        return;
    }
    uint8_t b[16];
    if (pread(fd, b, sizeof(b), 0) != (ssize_t)sizeof(b)) {
        return;
    }
    uint32_t seq;
    memcpy(&seq, b, 4);
    if (seq == 0) {
        if (g.tch_cfg_valid) {
            g.tch_cfg_valid = false;
            g.facch_dl_valid = g.tsacch_dl_valid = false;
            L1_LOG("canal TCH libere");
        }
        last = 0;
        return;
    }
    if (seq == last) {
        return;
    }
    last = seq;
    g.tch_cfg_valid = true;
    g.facch_dl_valid = g.tsacch_dl_valid = false;
    L1_LOG("canal TCH : TN=%u TSC=%u ARFCN=%u", b[4], b[5], lduw_le_p(b + 6));
}

static void publish_kc(void)
{
    static int fd = -1;
    static uint32_t seq;
    static uint8_t last[KC_RECLEN];
    static bool have_last;
    static int tick;
    static int clear_pending;
    if (++tick < KC_PUBLISH_EVERY) {
        return;
    }
    tick = 0;

    uint16_t mode = *api_ndb(NDB_D_A5MODE);
    const uint16_t *kw = api_ndb(NDB_A_KC);
    uint8_t rec[KC_RECLEN] = {0};
    bool nul = true;
    for (int i = 0; i < 4; i++) {
        rec[6 + 6 - 2 * i] = (uint8_t)(kw[i] >> 8);
        rec[6 + 7 - 2 * i] = (uint8_t)(kw[i] & 0xFF);
    }
    for (int i = 6; i < 14; i++) {
        if (rec[i]) {
            nul = false;
        }
    }
    uint8_t algo = (mode >= 1 && mode <= 3 && !nul) ? (uint8_t)mode : 0;
    if (!algo) {
        memset(rec + 6, 0, 8);
    }
    rec[4] = algo;
    rec[5] = algo ? 8 : 0;
    rec[14] = 0xFF;
    if (have_last && !memcmp(last + 4, rec + 4, KC_RECLEN - 4)) {
        clear_pending = 0;
        return;
    }
    if (!algo && have_last && last[4]) {
        /* Ciphered -> clear: wait and see whether the Kc comes back
         * (assignment in progress). */
        if (++clear_pending < KC_CLEAR_GRACE) {
            return;
        }
    }
    clear_pending = 0;
    if (fd < 0) {
        fd = open(SHM_KC, O_WRONLY | O_CREAT, 0666);
        if (fd < 0) {
            return;
        }
    }
    seq++;
    memcpy(rec, &seq, 4);
    if (pwrite(fd, rec, sizeof(rec), 0) != (ssize_t)sizeof(rec)) {
        close(fd);
        fd = -1;
        seq--;
        return;
    }
    memcpy(last, rec, sizeof(rec));
    have_last = true;
    if (algo) {
        L1_LOG("chiffrement A5/%u actif (Kc publie, seq=%u)", algo, seq);
    } else {
        L1_LOG("couche 1 en clair (seq=%u)", seq);
    }
}

void calypso_l1_frame_tick(void)
{
    publish_kc();
    tch_dl_poll();
    poll_tch_cfg();

    if (g.sb_valid) {
        dispatch_sb(0);
        dispatch_sb(1);
        bool imm_pending = g.agch_valid &&
            (g.agch_buf[2] == 0x3f || g.agch_buf[2] == 0x3a || g.agch_buf[2] == 0x3b);
        if (g.si_valid && !g.sdcch_valid && !dcch_si_guard() && !g.tch_cfg_valid &&
            !imm_pending) {
            if ((g.si_rot++ & 7u) == 0) {
                rotate_si();
            }
            ndb_block(NDB_A_CD, 0, g.si_set[g.si_rr]);
            tch_serv_demod();
        } else if (dcch_si_guard() && !tch_fresh(g.tsacch_dl_valid, g.tsacch_dl_tick) &&
                   !g.tch_cfg_valid) {
            dcch_sacch_present();
        }
    }

    if (!g.pending) {
        return;
    }
    g.tick++;

    uint8_t page = g.page;
    uint16_t md = g.d_task_md, td = g.d_task_d;
    if (md == PM_DSP_TASK) {
        dispatch_pm(page);
    }
    if (md == SB_DSP_TASK) {
        dispatch_sb(page);
    }
    if (td == ALLC_DSP_TASK) {
        dispatch_allc(page);
    }
    switch (td & 0x7FFF) {
    case TCHT_DSP_TASK:
        dispatch_tch_dl();
        break;
    case TCHA_DSP_TASK:
        dispatch_tch_sacch();
        break;
    default:
        break;
    }
    g.pending = false;
}

void calypso_l1_init(const char *firmware_elf)
{
    memset(&g, 0, sizeof(g));
    g.serving_arfcn = -1;
    if (firmware_elf) {
        g.l1s_addr = elf_symbol(firmware_elf, "l1s");
        g.last_rach_addr = elf_symbol(firmware_elf, "last_rach");
    }
    if (!g.l1s_addr || !g.last_rach_addr) {
        L1_LOG("symboles firmware introuvables (l1s=0x%x last_rach=0x%x) : "
               "fenetres CCCH et req-ref degradees", g.l1s_addr, g.last_rach_addr);
    }
    udp_listen(GSMTAP_PORT, gsmtap_readable);
    udp_listen(SCH_PORT, sch_readable);
    L1_LOG("backend gr-gsm : GSMTAP udp/%d, SCH udp/%d", GSMTAP_PORT, SCH_PORT);
}

4.79 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-grgsm/calypso_l1_grgsm_register.c

1345 octets, 35 lignes → 35 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * Registers the gr-gsm layer 1 with the platform.
 *
 * [2026-09-16] This file is the whole cost of moving qosmo-grgsm's L1 under the
 * vtable: calypso_l1_grgsm.c and calypso_l1ctl_tap.c are reused UNCHANGED apart
 * from the paths of their two private #includes. That is the check on where the
 * seam sits - a badly placed one would have forced edits inside the L1 itself.
 */
#include "qemu/osdep.h"
#include "qemu/module.h"
#include "hw/arm/calypso/calypso_l1_ops.h"
#include "calypso_l1.h"
#include "calypso_l1ctl_tap.h"

static const CalypsoL1Ops grgsm_ops = {
    .name              = "grgsm",
    .init              = calypso_l1_init,
    .frame_tick        = calypso_l1_frame_tick,
    .api_read_override = calypso_l1_read_override,
    .si_valid          = calypso_l1_si_valid,
    .l1s_fn            = calypso_l1s_fn,
    .burst_written     = calypso_l1_burst_written,
    .rach_written      = calypso_l1_rach_written,
    .page_written      = calypso_l1_page_written,
    .uart_tx_byte      = calypso_l1ctl_tap_tx_byte,
};

/* type_init runs before calypso_machine_init(), which is the only ordering
 * constraint calypso_l1_register() imposes. */
static void calypso_l1_grgsm_register_type(void)
{
    calypso_l1_register(&grgsm_ops);
}
type_init(calypso_l1_grgsm_register_type)

4.80 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-grgsm/calypso_l1ctl_tap.c

3175 octets, 124 lignes → 124 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "qemu/osdep.h"
#include "qemu/bswap.h"
#include "calypso_l1.h"
#include "calypso_l1ctl_tap.h"
#include <fcntl.h>
#include <unistd.h>

#define SERCOMM_FLAG        0x7E
#define SERCOMM_ESCAPE      0x7D
#define SERCOMM_ESCAPE_XOR  0x20
#define SERCOMM_DLCI_L1CTL  5

#define L1CTL_DATA_IND      0x03
#define L1CTL_RACH_CONF     0x0c
#define L1CTL_DATA_CONF     0x0f

#define SHM_DCCH_CFG        "/dev/shm/calypso_dcch_cfg"
#define DCCH_RELEASED       0xFF

static struct {
    enum { SC_IDLE, SC_IN_FRAME, SC_ESCAPE } state;
    uint8_t buf[512];
    int len;
    uint8_t last_chan_nr;
    uint32_t dcch_seq;
} tap = { .last_chan_nr = 0xFF };

static void dcch_cfg_publish(int kind, int ss, uint8_t chan_nr)
{
    uint8_t b[16] = {0};
    tap.dcch_seq++;
    memcpy(b, &tap.dcch_seq, 4);
    b[4] = (uint8_t)kind;
    b[5] = (uint8_t)ss;
    b[6] = chan_nr & 0x07;
    b[7] = chan_nr;
    int fd = open(SHM_DCCH_CFG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd < 0) {
        return;
    }
    if (write(fd, b, sizeof(b)) < 0) {
        close(fd);
        return;
    }
    close(fd);
}

static void frame_complete(void)
{
    if (tap.len < 3 || tap.buf[0] != SERCOMM_DLCI_L1CTL) {
        return;
    }
    const uint8_t *payload = &tap.buf[2];
    int plen = tap.len - 2;
    uint8_t type = payload[0];

    if (type == L1CTL_RACH_CONF && plen >= 12) {
        calypso_l1_rach_conf(ldl_be_p(payload + 8));
        return;
    }
    if ((type != L1CTL_DATA_CONF && type != L1CTL_DATA_IND) || plen < 5) {
        return;
    }
    uint8_t chan_nr = payload[4];
    int kind = -1, ss = 0;
    if ((chan_nr & 0xE0) == 0x20) {
        kind = 0;
        ss = (chan_nr >> 3) & 0x03;
    } else if ((chan_nr & 0xC0) == 0x40) {
        kind = 1;
        ss = (chan_nr >> 3) & 0x07;
    }
    bool tch = ((chan_nr & 0xF8) == 0x08) || ((chan_nr & 0xF0) == 0x10);
    bool dedicated = tch || kind >= 0;
    if (dedicated) {
        calypso_l1_dcch_active();
        calypso_l1_dcch_is_tch(tch);
    }
    if (kind >= 0 && chan_nr != tap.last_chan_nr) {
        tap.last_chan_nr = chan_nr;
        dcch_cfg_publish(kind, ss, chan_nr);
        calypso_l1_dcch_set(kind, ss);
    }
}

void calypso_l1ctl_tap_tx_byte(uint8_t byte)
{
    switch (tap.state) {
    case SC_IDLE:
        if (byte == SERCOMM_FLAG) {
            tap.state = SC_IN_FRAME;
            tap.len = 0;
        }
        break;
    case SC_IN_FRAME:
        if (byte == SERCOMM_FLAG) {
            if (tap.len > 0) {
                frame_complete();
            }
            tap.len = 0;
        } else if (byte == SERCOMM_ESCAPE) {
            tap.state = SC_ESCAPE;
        } else if (tap.len < (int)sizeof(tap.buf)) {
            tap.buf[tap.len++] = byte;
        }
        break;
    case SC_ESCAPE:
        if (tap.len < (int)sizeof(tap.buf)) {
            tap.buf[tap.len++] = byte ^ SERCOMM_ESCAPE_XOR;
        }
        tap.state = SC_IN_FRAME;
        break;
    }
}

void calypso_l1ctl_tap_channel_released(void)
{
    if (tap.last_chan_nr == 0xFF) {
        return;
    }
    tap.last_chan_nr = 0xFF;
    dcch_cfg_publish(DCCH_RELEASED, 0, 0);
}

4.81 https://github.com/bbaranoff/qosmo/blob/main/hw/arm/calypso/l1-grgsm/calypso_l1ctl_tap.h

241 octets, 10 lignes → 10 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef HW_ARM_CALYPSO_L1CTL_TAP_H
#define HW_ARM_CALYPSO_L1CTL_TAP_H

#include <stdint.h>

void calypso_l1ctl_tap_tx_byte(uint8_t byte);
void calypso_l1ctl_tap_channel_released(void);

#endif

4.82 https://github.com/bbaranoff/qosmo/blob/main/hw/char/calypso_uart.c

16295 octets, 668 lignes → 666 lignes (1 groupes compactés)

/* SPDX-License-Identifier: GPL-2.0-or-later */

#include "qemu/osdep.h"
#include "hw/sysbus.h"
#include "hw/irq.h"
#include "chardev/char-fe.h"
#include "qemu/log.h"
#include "qemu/timer.h"
#include "qemu/main-loop.h"
#include "hw/core/cpu.h"
#include "hw/qdev-properties.h"
#include "hw/qdev-properties-system.h"
#include "hw/arm/calypso/calypso_uart.h"
#include "hw/arm/calypso/calypso_trx.h"
#include "hw/arm/calypso/calypso_l1_ops.h"

#define REG_RBR_THR   0x00
#define REG_IER       0x01
#define REG_IIR_FCR   0x02
#define REG_LCR       0x03
#define REG_MCR       0x04
#define REG_LSR       0x05
#define REG_MSR       0x06
#define REG_SPR       0x07
#define REG_MDR1      0x08
#define REG_SCR       0x10
#define REG_SSR       0x11

#define IER_RX_DATA   (1 << 0)
#define IER_TX_EMPTY  (1 << 1)
#define IER_RX_LINE   (1 << 2)

#define IIR_NO_INT    0x01
#define IIR_RX_LINE   0x06
#define IIR_RX_DATA   0x04
#define IIR_TX_EMPTY  0x02

#define LCR_DLAB      (1 << 7)
#define LCR_CONF_BF   0xBF

#define LSR_DR        (1 << 0)
#define LSR_OE        (1 << 1)
#define LSR_THRE      (1 << 5)
#define LSR_TEMT      (1 << 6)

#define MSR_CTS       (1 << 4)
#define MSR_DSR       (1 << 5)
#define MSR_DCD       (1 << 7)

#define FCR_FIFO_EN   (1 << 0)
#define FCR_RX_RESET  (1 << 1)
#define FCR_TX_RESET  (1 << 2)

#define SSR_TX_FIFO_FULL  (1 << 0)

static void fifo_reset(CalypsoUARTState *s)
{
    s->rx_head = 0;
    s->rx_tail = 0;
    s->rx_count = 0;
}

static void fifo_push(CalypsoUARTState *s, uint8_t data)
{
    if (s->rx_count >= CALYPSO_UART_RX_FIFO_SIZE) {
        s->lsr |= LSR_OE;
        fprintf(stderr,
                "[UART:%s] RX FIFO OVERFLOW drop=0x%02x count=%u size=%u\n",
                s->label ? s->label : "?",
                data,
                (unsigned)s->rx_count,
                (unsigned)CALYPSO_UART_RX_FIFO_SIZE);
        return;
    }

    s->rx_fifo[s->rx_head] = data;
    s->rx_head = (s->rx_head + 1) % CALYPSO_UART_RX_FIFO_SIZE;
    s->rx_count++;
}

static uint8_t fifo_pop(CalypsoUARTState *s)
{
    uint8_t data = 0;

    if (s->rx_count == 0) {
        return 0;
    }

    data = s->rx_fifo[s->rx_tail];
    s->rx_tail = (s->rx_tail + 1) % CALYPSO_UART_RX_FIFO_SIZE;
    s->rx_count--;

    return data;
}

static void calypso_uart_update_irq(CalypsoUARTState *s)
{
    uint8_t iir = IIR_NO_INT;
    bool want = false;

    if ((s->ier & IER_RX_LINE) && (s->lsr & LSR_OE)) {
        iir = IIR_RX_LINE;
        want = true;
    } else if ((s->ier & IER_RX_DATA) && (s->lsr & LSR_DR)) {
        iir = IIR_RX_DATA;
        want = true;
    } else if ((s->ier & IER_TX_EMPTY) && s->thr_empty_pending) {
        iir = IIR_TX_EMPTY;
        want = true;
    }
    /* RX wake-up (TI UART driver, serialswitch_core.c): in sleep the firmware
     * clears ERBI and arms SCR[4]; on the first RX activity the UART raises the
     * interrupt with SSR[1] (RX_CTS_WAKE_UP_STS), the firmware disarms it (SCR[4])
     * and re-enables reception. */
    if (s->wakeup_pending && (s->scr & 0x10)) {
        want = true;
    }

    s->iir = iir;

    qemu_irq_lower(s->irq);
    if (want) {
        qemu_irq_raise(s->irq);
    }
}

void calypso_uart_kick_rx(CalypsoUARTState *s)
{
    if (s->rx_count > 0 && (s->lsr & LSR_DR)) {

        qemu_irq_lower(s->irq);
        calypso_uart_update_irq(s);
    }
}

void calypso_uart_poll_backend(CalypsoUARTState *s)
{
    qemu_chr_fe_accept_input(&s->chr);
}

void calypso_uart_kick_tx(CalypsoUARTState *s)
{

    calypso_uart_update_irq(s);
}

static bool uart_debug(void)
{
    static int d = -1;
    if (d < 0) { const char *e = getenv("CALYPSO_UART_DEBUG"); d = (e && *e && strcmp(e, "0")) ? 1 : 0; }
    return d;
}

void calypso_uart_inject_raw(CalypsoUARTState *s, const uint8_t *buf, int len)
{
    if (!s) return;
    if (uart_debug()) {
        fprintf(stderr, "[uart %s] rx %d octets (ier=0x%02x lsr=0x%02x lcr=0x%02x mdr1=0x%02x fifo=%d) :",
                s->label ? s->label : "?", len, s->ier, s->lsr, s->lcr, s->mdr1, s->rx_count);
        for (int i = 0; i < len && i < 16; i++) fprintf(stderr, " %02x", buf[i]);
        fprintf(stderr, "\n");
    }
    for (int i = 0; i < len; i++) {
        fifo_push(s, buf[i]);
    }
    if (s->rx_count > 0) {
        s->lsr |= LSR_DR;
        if (s->scr & 0x10) {
            s->wakeup_pending = true;
        }
        calypso_uart_update_irq(s);
    }
}

void calypso_uart_force_init(CalypsoUARTState *s)
{

    if (s->mdr1 != 0x00) {
        s->mdr1 = 0x00;
        s->scr = 0x01;
    }
    s->ier = 0x03;
    calypso_uart_update_irq(s);
}

#define UART_RX_POLL_NS  (10 * 1000 * 1000)

static void calypso_uart_rx_poll(void *opaque)
{
    CalypsoUARTState *s = (CalypsoUARTState *)opaque;

    qemu_chr_fe_accept_input(&s->chr);

    timer_mod(s->rx_poll_timer,
              qemu_clock_get_ms(QEMU_CLOCK_REALTIME) + 5);
}

typedef enum {
    ROM_IDLE,
    ROM_AFTER_3C,
    ROM_BLOCK_DATA,
    ROM_CHK_DATA,
    ROM_BR_DATA,
    ROM_PASSTHROUGH,
} RomloadState;

static struct {
    RomloadState state;
    int          needed;
    uint16_t     payload_size;
} romload = {
    .state = ROM_IDLE,
    .payload_size = 1024,
};

/* Real firmware in flash (FreeCalypso Magnetite, calypso_mb.c without
 * -kernel): no romload, the stub must not swallow anything, so that fc-fsio /
 * fc-shell (ETM through rvinterf) get through instead of ROM_IDLE eating it. */
void calypso_uart_romload_passthrough(void)
{
    romload.state = ROM_PASSTHROUGH;
}

static bool romload_stub_eat(CalypsoUARTState *s, uint8_t b)
{
    if (romload.state == ROM_PASSTHROUGH) {
        return false;
    }

    switch (romload.state) {
    case ROM_IDLE:
        if (b == 0x3c) {
            romload.state = ROM_AFTER_3C;
        }

        return true;

    case ROM_AFTER_3C: {
        if (b == 0x69) {

            uint8_t ack[6] = {
                0x3e, 0x69,
                0x3e, 0x70,
                (uint8_t)((romload.payload_size + 10) & 0xFF),
                (uint8_t)(((romload.payload_size + 10) >> 8) & 0xFF),
            };
            qemu_chr_fe_write_all(&s->chr, ack, sizeof(ack));
            romload.state = ROM_IDLE;
        } else if (b == 0x77) {

            romload.state  = ROM_BLOCK_DATA;
            romload.needed = 8 + romload.payload_size;
        } else if (b == 0x63) {
            romload.state  = ROM_CHK_DATA;
            romload.needed = 1;
        } else if (b == 0x62) {
            romload.state  = ROM_BR_DATA;
            romload.needed = 4;
        } else {

            romload.state = ROM_IDLE;
        }
        return true;
    }

    case ROM_BLOCK_DATA:
        if (--romload.needed == 0) {
            uint8_t ack[2] = { 0x3e, 0x77 };
            qemu_chr_fe_write_all(&s->chr, ack, sizeof(ack));
            romload.state = ROM_IDLE;
        }
        return true;

    case ROM_CHK_DATA:
        if (--romload.needed == 0) {

            uint8_t ack[3] = { 0x3e, 0x63, b };
            qemu_chr_fe_write_all(&s->chr, ack, sizeof(ack));
            romload.state = ROM_IDLE;
        }
        return true;

    case ROM_BR_DATA:
        if (--romload.needed == 0) {
            uint8_t ack[2] = { 0x3e, 0x62 };
            qemu_chr_fe_write_all(&s->chr, ack, sizeof(ack));
            romload.state = ROM_PASSTHROUGH;
        }
        return true;

    case ROM_PASSTHROUGH:
        return false;
    }
    return false;
}

int calypso_uart_can_receive(void *opaque)
{
    CalypsoUARTState *s = (CalypsoUARTState *)opaque;
    return CALYPSO_UART_RX_FIFO_SIZE - s->rx_count;
}

void calypso_uart_receive(void *opaque, const uint8_t *buf, int size)
{
    CalypsoUARTState *s = (CalypsoUARTState *)opaque;

    if (s->label && !strcmp(s->label, "irda")) {
        for (int i = 0; i < size; i++)
            fifo_push(s, buf[i]);
        if (s->rx_count > 0)
            s->lsr |= LSR_DR;
        calypso_uart_update_irq(s);
        return;
    }

    if (s->label && !strcmp(s->label, "modem")) {
        uint8_t passthrough[CALYPSO_UART_RX_FIFO_SIZE];
        int     pt_len = 0;
        for (int i = 0; i < size; i++) {
            if (!romload_stub_eat(s, buf[i])) {
                passthrough[pt_len++] = buf[i];
            }
        }
        if (pt_len > 0) {
            calypso_uart_inject_raw(s, passthrough, pt_len);
        }

        qemu_chr_fe_accept_input(&s->chr);
        return;
    }

    calypso_uart_inject_raw(s, buf, size);

    if (s->rx_count > 0) {
        s->lsr |= LSR_DR;
    }

    calypso_uart_update_irq(s);
}

static uint64_t calypso_uart_read(void *opaque, hwaddr offset, unsigned size)
{
    CalypsoUARTState *s = CALYPSO_UART(opaque);
    uint64_t val = 0;

    switch (offset) {
    case REG_RBR_THR:
        if (s->lcr & LCR_DLAB) {
            val = s->dll;
        } else {

            val = fifo_pop(s);

            if (s->rx_count > 0) {
                s->lsr |= LSR_DR;
            } else {
                s->lsr &= ~LSR_DR;
            }

            calypso_uart_update_irq(s);
        }
        break;

    case REG_IER:
        if (s->lcr & LCR_DLAB) {
            val = s->dlh;
        } else {
            val = s->ier;
        }
        break;

    case REG_IIR_FCR:
        if (s->lcr == LCR_CONF_BF) {
            val = s->efr;
        } else {
            val = s->iir;
            if ((s->iir & 0x0F) == IIR_TX_EMPTY) {

                s->tx_empty_reads++;
                if (s->tx_empty_reads >= 2) {
                    s->thr_empty_pending = false;
                    s->tx_empty_reads = 0;
                    calypso_uart_update_irq(s);
                }
            }
        }
        break;

    case REG_LCR:
        val = s->lcr;
        break;

    case REG_MCR:
        if (s->lcr == LCR_CONF_BF) {
            val = s->xon1;
        } else {
            val = s->mcr;
        }
        break;

    case REG_LSR:
        if (s->lcr == LCR_CONF_BF) {
            val = s->xon2;
        } else {
            val = s->lsr;
            s->lsr &= ~LSR_OE;
        }
        break;

    case REG_MSR:
        if (s->lcr == LCR_CONF_BF) {
            val = s->xoff1;
        } else {
            val = MSR_CTS | MSR_DSR | MSR_DCD;
        }
        break;

    case REG_SPR:
        if (s->lcr == LCR_CONF_BF) {
            val = s->xoff2;
        } else {
            val = s->spr;
        }
        break;

    case REG_MDR1:
        val = s->mdr1;
        break;

    case REG_SCR:
        val = s->scr;
        break;

    case REG_SSR:
        val = s->ssr & ~SSR_TX_FIFO_FULL;
        if (s->wakeup_pending && (s->scr & 0x10)) {
            val |= 0x02;                       /* RX_CTS_WAKE_UP_STS */
        }
        break;

    default:
        break;
    }

    return val;
}

static void calypso_uart_write(void *opaque, hwaddr offset,
                               uint64_t value, unsigned size)
{
    CalypsoUARTState *s = CALYPSO_UART(opaque);

    switch (offset) {
    case REG_RBR_THR:
        if (s->lcr & LCR_DLAB) {
            s->dll = value;
        } else {
            uint8_t ch = (uint8_t)value;
  ×3
            (void)qemu_chr_fe_write(&s->chr, &ch, 1);

            if (s->label && !strcmp(s->label, "modem")) {
                calypso_l1_do_uart_tx_byte(ch);
            }

            s->lsr |= LSR_THRE | LSR_TEMT;
            s->thr_empty_pending = true;
            s->tx_empty_reads = 0;
            calypso_uart_update_irq(s);
        }
        break;

    case REG_IER:
        if (s->lcr & LCR_DLAB) {
            s->dlh = value;
        } else {
            uint8_t old = s->ier;
            if (uart_debug()) {
                fprintf(stderr, "[uart %s] IER <- 0x%02x (scr=0x%02x fifo=%d)\n", s->label ? s->label : "?",
                        (unsigned)value, s->scr, s->rx_count);
            }
            s->ier = value & 0x0F;


            if (!(old & IER_TX_EMPTY) &&
                (s->ier & IER_TX_EMPTY) &&
                (s->lsr & LSR_THRE)) {
                s->thr_empty_pending = true;
            }

            calypso_uart_update_irq(s);
        }
        break;

    case REG_IIR_FCR:
        if (s->lcr == LCR_CONF_BF) {
            s->efr = value;
        } else {
            s->fcr = value;

            if (value & FCR_RX_RESET) {
                fifo_reset(s);
                s->lsr &= ~LSR_DR;
            }

            if (value & FCR_TX_RESET) {
                s->thr_empty_pending = false;
                s->lsr |= LSR_THRE | LSR_TEMT;
            }

            calypso_uart_update_irq(s);
        }
        break;

    case REG_LCR:
        s->lcr = value;
        break;

    case REG_MCR:
        if (s->lcr == LCR_CONF_BF) {
            s->xon1 = value;
        } else {
            s->mcr = value;
        }
        break;

    case REG_LSR:
        if (s->lcr == LCR_CONF_BF) {
            s->xon2 = value;
        }
        break;

    case REG_MSR:
        if (s->lcr == LCR_CONF_BF) {
            s->xoff1 = value;
        }
        break;

    case REG_SPR:
        if (s->lcr == LCR_CONF_BF) {
            s->xoff2 = value;
        } else {
            s->spr = value;
        }
        break;

    case REG_MDR1:
        s->mdr1 = value;
        break;

    case REG_SCR:
        if (uart_debug()) {
            fprintf(stderr, "[uart %s] SCR <- 0x%02x (ier=0x%02x fifo=%d)\n", s->label ? s->label : "?",
                    (unsigned)value, s->ier, s->rx_count);
        }
        s->scr = value;
        if (!(value & 0x10)) {
            s->wakeup_pending = false;         /* DISABLE_WAKE_UP_INTERRUPT */
        }
        calypso_uart_update_irq(s);
        break;

    case REG_SSR:
        s->ssr = value;
        break;

    default:
        break;
    }
}

static const MemoryRegionOps calypso_uart_ops = {
    .read = calypso_uart_read,
    .write = calypso_uart_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
    .impl = { .min_access_size = 1, .max_access_size = 1 },
    .valid = { .min_access_size = 1, .max_access_size = 1 },
};

static void calypso_uart_realize(DeviceState *dev, Error **errp)
{
    CalypsoUARTState *s = CALYPSO_UART(dev);
    bool connected;

    memory_region_init_io(&s->iomem, OBJECT(dev), &calypso_uart_ops, s,
                          "calypso-uart", 0x100);
    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->iomem);
    sysbus_init_irq(SYS_BUS_DEVICE(dev), &s->irq);

    connected = qemu_chr_fe_backend_connected(&s->chr);


    if (connected) {
        qemu_chr_fe_set_handlers(&s->chr,
                                 calypso_uart_can_receive,
                                 calypso_uart_receive,
                                 NULL, NULL,
                                 s,
                                 NULL, true);

        s->rx_poll_timer = timer_new_ms(QEMU_CLOCK_REALTIME,
                                        calypso_uart_rx_poll, s);
        timer_mod(s->rx_poll_timer,
                  qemu_clock_get_ms(QEMU_CLOCK_REALTIME) + 10);
    }

}

static void calypso_uart_reset_state(DeviceState *dev)
{
    CalypsoUARTState *s = CALYPSO_UART(dev);

    s->ier = 0;
    s->iir = IIR_NO_INT;
    s->fcr = 0;
    s->lcr = 0;
    s->mcr = 0;
    s->lsr = LSR_THRE | LSR_TEMT;
    s->msr = MSR_CTS | MSR_DSR | MSR_DCD;
    s->spr = 0;
    s->dll = 0;
    s->dlh = 0;
    s->mdr1 = 0;

    s->efr = 0;
    s->xon1 = 0;
    s->xon2 = 0;
    s->xoff1 = 0;
    s->xoff2 = 0;
    s->scr = 0;
    s->ssr = 0;

    s->thr_empty_pending = false;

    fifo_reset(s);
}

static Property calypso_uart_properties[] = {
    DEFINE_PROP_CHR("chardev", CalypsoUARTState, chr),
    DEFINE_PROP_STRING("label", CalypsoUARTState, label),
    DEFINE_PROP_END_OF_LIST(),
};

static void calypso_uart_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);

    dc->realize = calypso_uart_realize;
    device_class_set_legacy_reset(dc, calypso_uart_reset_state);
    dc->desc = "Calypso UART";
    device_class_set_props(dc, calypso_uart_properties);
}

static const TypeInfo calypso_uart_info = {
    .name          = TYPE_CALYPSO_UART,
    .parent        = TYPE_SYS_BUS_DEVICE,
    .instance_size = sizeof(CalypsoUARTState),
    .class_init    = calypso_uart_class_init,
};

static void calypso_uart_register_types(void)
{
    type_register_static(&calypso_uart_info);
}

type_init(calypso_uart_register_types)

4.83 https://github.com/bbaranoff/qosmo/blob/main/hw/intc/calypso_inth.c

8597 octets, 287 lignes → 287 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */

#include "qemu/osdep.h"
#include "hw/irq.h"
#include "hw/sysbus.h"
#include "hw/arm/calypso/calypso_inth.h"

static void calypso_inth_update(CalypsoINTHState *s)
{
    uint32_t active = s->levels & ~s->mask;
    int best_irq = -1, best_irq_prio = 0x7F;
    int best_fiq = -1, best_fiq_prio = 0x7F;

    for (int j = 0; j < CALYPSO_INTH_NUM_IRQS; j++) {
        int i = (s->rr_start + j) % CALYPSO_INTH_NUM_IRQS;
        if (!(active & (1u << i))) continue;
        int prio = s->ilr[i] & 0x1F;
        int is_fiq = (s->ilr[i] >> 8) & 1;
        if (is_fiq) {
            if (prio < best_fiq_prio) { best_fiq_prio = prio; best_fiq = i; }
        } else {
            if (prio < best_irq_prio) { best_irq_prio = prio; best_irq = i; }
        }
    }

    if (best_irq >= 0) {
        s->ith_v = best_irq;
        {
            static int dbg = -1, nirq;
            if (dbg < 0) { const char *e = getenv("CALYPSO_INTH_DEBUG"); dbg = (e && *e == '1'); }
            if (dbg && (best_irq == 8 || nirq < 20) && (nirq++ < 2000 || nirq % 500 == 0)) {
                fprintf(stderr, "[inth] IRQ %d levee (prio %d, mask 0x%08x, levels 0x%08x, n=%d)\n",
                        best_irq, best_irq_prio, s->mask, s->levels, nirq);
            }
        }
        qemu_irq_raise(s->parent_irq);
    } else {
        if (best_fiq < 0) s->ith_v = 0;
        qemu_irq_lower(s->parent_irq);
    }

    if (best_fiq >= 0) {
        s->fiq_v = best_fiq;
        qemu_irq_raise(s->parent_fiq);
    } else {
        qemu_irq_lower(s->parent_fiq);
    }
}

static void calypso_inth_set_irq(void *opaque, int irq, int level)
{
    CalypsoINTHState *s = CALYPSO_INTH(opaque);

    if (level) {
        s->levels |= (1u << irq);
    } else if ((s->ilr[irq] & 0x2) && (irq == 4 || irq == 5 || irq == 15)) {
        /* EDGE-CONFIGURED SOURCE (ILR bit 1, irq_config(..., edge=1)): the
         * edge is LATCHED in IT_REG until IRQ_NUM is read (below, for 4/5/15)
         * or until the software clears it. The line falling must not clear the
         * bit: the TPU frame IRQ (IRQ 4) is a 1 ms pulse (calypso_trx.c
         * FRAME_IRQ_PULSE_NS) and, if the ARM is still inside a long handler
         * (UART/L1CTL during speech), the IRQ would be LOST. The firmware does
         * not detect a lost TDMA frame: its frame counter would slip by one for
         * the rest of the channel (a_a5fn = fn-1: speech deciphered with the
         * wrong stream, wrong FACCH, lost SACCH, LOS ~15 s later). */
    } else {
        s->levels &= ~(1u << irq);
    }

    calypso_inth_update(s);
}

static uint64_t calypso_inth_read(void *opaque, hwaddr offset, unsigned size)
{
    CalypsoINTHState *s = CALYPSO_INTH(opaque);

    switch (offset) {
    case 0x00:
        return s->levels & 0xFFFF;
    case 0x02:
        return (s->levels >> 16) & 0xFFFF;
    case 0x08:
        return s->mask & 0xFFFF;
    case 0x0a:
        return (s->mask >> 16) & 0xFFFF;
    case 0x10:
    case 0x80:
    {
        uint16_t num = s->ith_v;
        s->last_num = num;

        /* An EDGE-configured source (ILR bit 1: the jiffies TIMER2, which never lowers its
         * line) is acknowledged by the IRQ_NUM read, like 4/5/15; otherwise it stays pending
         * and the ARM re-enters the IRQ in a loop (C123 keypad: keypad_poll never reached). */
        if (num == 4 || num == 5 || num == 15 || (s->ilr[num] & 0x2)) {
            s->levels &= ~(1u << num);
        }

        calypso_inth_update(s);
        return num;
    }
    case 0x12:
    case 0x82:
    {

        uint16_t num = s->fiq_v;
        if (num == 4 || num == 5 || num == 15 || (s->ilr[num] & 0x2)) {
            s->levels &= ~(1u << num);
        }
        calypso_inth_update(s);
        return num;
    }
    case 0x14:
    case 0x84:
        return 0;
    default:
        if (offset >= 0x20 && offset < 0x60) {
            int idx = (offset - 0x20) / 2;
            return s->ilr[idx];
        }
        return 0;
    }
}

static void calypso_inth_write(void *opaque, hwaddr offset, uint64_t value,
                                unsigned size)
{
    CalypsoINTHState *s = CALYPSO_INTH(opaque);

    switch (offset) {

    case 0x00:
    {
        s->levels &= (0xFFFF0000u | (uint32_t)(value & 0xFFFF));
        calypso_inth_update(s);
        break;
    }
    case 0x02:
    {
        s->levels &= (0x0000FFFFu | ((uint32_t)(value & 0xFFFF) << 16));
        calypso_inth_update(s);
        break;
    }
    case 0x08:
    {
        s->mask = (s->mask & 0xFFFF0000) | (value & 0xFFFF);
        calypso_inth_update(s);
        break;
    }
    case 0x0a:
    {
        s->mask = (s->mask & 0x0000FFFF) | ((value & 0xFFFF) << 16);
        calypso_inth_update(s);
        break;
    }
    case 0x14:
    case 0x84:
    {

        uint16_t svc = s->ith_v;
        if (svc > 0 || (s->levels & 1)) {

            s->rr_start = (svc + 1) % CALYPSO_INTH_NUM_IRQS;
        }
        if ((value & 1) && s->last_num == 4) {
            s->frame_eoi++;          /* l1_sync() of the firmware is over */
        }
        calypso_inth_update(s);
        break;
    }
    default:
        if (offset >= 0x20 && offset < 0x60) {
            int idx = (offset - 0x20) / 2;
            /* ILR = 7 bits (FIQ/nIRQ, SENSE_EDGE, PRIORITY[4:0]). The firmware writes 0xfffe
             * for TIMER2 (default_irq_prio 0xff read as int8_t -1, irq.c irq_config): with a
             * 0x1FFF mask bit 8 would be taken for the FIQ flag (calypso_inth_update) and the
             * jiffies timer would go to FIQ, never acknowledged (ARM stuck in fiq()). */
            s->ilr[idx] = value & 0x7F;

            if (idx == 7) {
                s->ilr[7] = (s->ilr[7] & ~0x1F) | (s->ilr[4] & 0x1F);
            }

            if (idx == 18) {
                s->ilr[18] = (s->ilr[18] & ~0x1F) | (s->ilr[4] & 0x1F);
            }
        }
        break;
    }
}

static const MemoryRegionOps calypso_inth_ops = {
    .read = calypso_inth_read,
    .write = calypso_inth_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
    .valid = { .min_access_size = 1, .max_access_size = 2 },
    .impl  = { .min_access_size = 1, .max_access_size = 2 },
};

/* Single instance, for calypso_inth_arm_ack(): the emulated DSP needs this
 * acknowledgement. */
static CalypsoINTHState *g_inth;

/* Interrupt acknowledgement issued by the DSP, not by the ARM: end of service
 * of the current source, the queue moves on to the next one. No nesting, no
 * irq_in_service blocking. */
void calypso_inth_arm_ack(void)
{
    CalypsoINTHState *s = g_inth;
    if (!s) {
        return;
    }

    uint16_t svc = s->ith_v;
    if (svc > 0 || (s->levels & 1)) {
        s->levels &= ~(1u << svc);      /* end of service: the source drops */
        s->rr_start = (svc + 1) % CALYPSO_INTH_NUM_IRQS;
    }
}

uint64_t calypso_inth_frame_eoi(void)
{
    return g_inth ? g_inth->frame_eoi : 0;
}

bool calypso_inth_irq_masked(int irq)
{
    return !g_inth || (g_inth->mask & (1u << irq));
}

/* IRQ still pending in IT_REG (raised, not served yet). */
bool calypso_inth_irq_pending(int irq)
{
    return g_inth && (g_inth->levels & (1u << irq));
}

static void calypso_inth_realize(DeviceState *dev, Error **errp)
{
    g_inth = CALYPSO_INTH(dev);
    CalypsoINTHState *s = CALYPSO_INTH(dev);

    memory_region_init_io(&s->iomem, OBJECT(dev), &calypso_inth_ops, s,
                          "calypso-inth", 0x100);
    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->iomem);

    sysbus_init_irq(SYS_BUS_DEVICE(dev), &s->parent_irq);
    sysbus_init_irq(SYS_BUS_DEVICE(dev), &s->parent_fiq);

    qdev_init_gpio_in(dev, calypso_inth_set_irq, CALYPSO_INTH_NUM_IRQS);
}

static void calypso_inth_reset(DeviceState *dev)
{
    CalypsoINTHState *s = CALYPSO_INTH(dev);

    s->levels = 0;
    s->mask = 0x00000000;
    s->ith_v = 0;
    s->fiq_v = 0;
    s->rr_start = 0;
    s->last_num = 0;
    s->frame_eoi = 0;
    memset(s->ilr, 0, sizeof(s->ilr));
}

static void calypso_inth_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);

    dc->realize = calypso_inth_realize;
    device_class_set_legacy_reset(dc, calypso_inth_reset);
    dc->desc = "Calypso INTH interrupt controller";
}

static const TypeInfo calypso_inth_info = {
    .name          = TYPE_CALYPSO_INTH,
    .parent        = TYPE_SYS_BUS_DEVICE,
    .instance_size = sizeof(CalypsoINTHState),
    .class_init    = calypso_inth_class_init,
};

static void calypso_inth_register_types(void)
{
    type_register_static(&calypso_inth_info);
}

type_init(calypso_inth_register_types)

4.84 https://github.com/bbaranoff/qosmo/blob/main/hw/ssi/calypso_i2c.c

1644 octets, 66 lignes → 66 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */

#include "qemu/osdep.h"
#include "hw/sysbus.h"
#include "qemu/log.h"

#define TYPE_CALYPSO_I2C "calypso-i2c"
OBJECT_DECLARE_SIMPLE_TYPE(CalypsoI2CState, CALYPSO_I2C)

struct CalypsoI2CState {
    SysBusDevice parent_obj;
    MemoryRegion iomem;
};

static uint64_t calypso_i2c_read(void *opaque, hwaddr offset, unsigned size)
{
    switch (offset) {
    case 0x04:
        return 0x04;
    default:
        return 0;
    }
}

static void calypso_i2c_write(void *opaque, hwaddr offset, uint64_t value,
                              unsigned size)
{

}

static const MemoryRegionOps calypso_i2c_ops = {
    .read = calypso_i2c_read,
    .write = calypso_i2c_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
    .impl = { .min_access_size = 2, .max_access_size = 2 },
};

static void calypso_i2c_realize(DeviceState *dev, Error **errp)
{
    CalypsoI2CState *s = CALYPSO_I2C(dev);

    memory_region_init_io(&s->iomem, OBJECT(dev), &calypso_i2c_ops, s,
                          "calypso-i2c", 0x100);
    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->iomem);
}

static void calypso_i2c_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    dc->realize = calypso_i2c_realize;
    dc->desc = "Calypso I2C stub";
}

static const TypeInfo calypso_i2c_info = {
    .name          = TYPE_CALYPSO_I2C,
    .parent        = TYPE_SYS_BUS_DEVICE,
    .instance_size = sizeof(CalypsoI2CState),
    .class_init    = calypso_i2c_class_init,
};

static void calypso_i2c_register_types(void)
{
    type_register_static(&calypso_i2c_info);
}

type_init(calypso_i2c_register_types)

4.85 https://github.com/bbaranoff/qosmo/blob/main/hw/ssi/calypso_spi.c

5821 octets, 191 lignes → 191 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */

#include "qemu/osdep.h"
#include "hw/sysbus.h"
#include "hw/irq.h"
#include "qemu/log.h"
#include "hw/arm/calypso/calypso_spi.h"

#define SPI_REG_SET1     0x00
#define SPI_REG_SET2     0x02
#define SPI_REG_CTRL     0x04
#define SPI_REG_STATUS   0x06
#define SPI_REG_TX_LSB   0x08
#define SPI_REG_TX_MSB   0x0A
#define SPI_REG_RX_LSB   0x0C
#define SPI_REG_RX_MSB   0x0E

/* The bits as the drivers write them (TI spi_drv.h and osmocom-bb
 * calypso/spi.c): CTRL bit 0 = RDWR_ACTIV (read), bit 1 = WR (write);
 * STATUS bit 0 = RE_ST, bit 1 = WE_ST. ABB_ReadRegister() of TI (FreeCalypso
 * Magnetite) waits for RE_ST. */
#define SPI_CTRL_RDWR    (1 << 0)
#define SPI_CTRL_WR      (1 << 1)
#define SPI_STATUS_RE_ST (1 << 0)
#define SPI_STATUS_WE_ST (1 << 1)

#define ABB_PAGEREG      0x01
#define ABB_ONBSTS       0x01      /* VRPCSTS: powered on by the ON button */
#define ABB_ONREFLT      0x10      /* VRPCSTS: ON button still pressed */

/* 16-bit TWL3025 frame, the same for TI (abb_inline.h) and osmocom-bb
 * (twl3025.c): bits 15..6 = data (10 bits), 5..1 = register, 0 = read.
 * The answer comes back in RX_LSB with the data in 15..6. The pages (PAGEREG
 * = 0x0001 page 0, 0x0002 page 1) index abb_regs by page << 5 | reg. */
static uint16_t twl3025_spi_xfer(CalypsoSPIState *s, uint16_t tx)
{
    int rd = tx & 1;
    int reg = (tx >> 1) & 0x1f;
    int data = (tx >> 6) & 0x3ff;
    int idx = (s->page << 5) | reg;

    if (rd) {
        return (s->abb_regs[idx] & 0x3ff) << 6;
    }
    if (reg == ABB_PAGEREG) {
        s->page = (data & 0x2) ? 1 : 0;
        return 0;
    }
    s->abb_regs[idx] = data;
    /* VRPCSIM (page 1, reg 23): the TI SIM driver (sim.c SIM_StartVolt, Iota)
     * writes the SIM regulator mode then expects RSIMRSU (bit 2, "SIM
     * regulator running") on read-back, 5 tries, otherwise
     * SIM_ERR_HARDWARE_FAIL = no card. */
    if (idx == ((1 << 5) | 23)) {
        s->abb_regs[idx] = data | 0x04;
    }
    if (idx == ABB_VRPCDEV) {
        s->abb_regs[ABB_VRPCSTS] = ABB_ONBSTS;
    }
    return 0;
}

static uint64_t calypso_spi_read(void *opaque, hwaddr offset, unsigned size)
{
    CalypsoSPIState *s = CALYPSO_SPI(opaque);

    switch (offset) {
    case SPI_REG_SET1:
        return s->set1;
    case SPI_REG_SET2:
        return s->set2;
    case SPI_REG_CTRL:
        return s->ctrl;
    case SPI_REG_STATUS:
        /* instantaneous transfer: read and write always complete */
        return SPI_STATUS_RE_ST | SPI_STATUS_WE_ST;
    case SPI_REG_TX_LSB:
        return s->tx_lsb;
    case SPI_REG_TX_MSB:
        return s->tx_data;
    case SPI_REG_RX_LSB:
        /* 16-bit frame: the whole answer is in RX_LSB (TI reads
         * (RX_LSB >> 6) & 0x3ff, osmocom likewise) */
        return s->rx_data;
    case SPI_REG_RX_MSB:
        return 0;
    default:
        qemu_log_mask(LOG_UNIMP, "calypso-spi: read at 0x%02x\n",
                       (unsigned)offset);
        return 0;
    }
}

static void calypso_spi_write(void *opaque, hwaddr offset, uint64_t value,
                               unsigned size)
{
    CalypsoSPIState *s = CALYPSO_SPI(opaque);

    switch (offset) {
    case SPI_REG_SET1:
        s->set1 = value & 0xFFFF;
        break;
    case SPI_REG_SET2:
        s->set2 = value & 0xFFFF;
        break;
    case SPI_REG_CTRL:
        s->ctrl = value & 0xFFFF;
        if (value & (SPI_CTRL_RDWR | SPI_CTRL_WR)) {
            s->rx_data = twl3025_spi_xfer(s, s->tx_data);
            qemu_irq_pulse(s->irq);
        }
        break;
    case SPI_REG_STATUS:
        break;
    case SPI_REG_TX_LSB:
        s->tx_lsb = value & 0xFFFF;
        break;
    case SPI_REG_TX_MSB:
        /* a 16-bit frame fits entirely in TX_MSB (TI SPI_WRITE_TX_MSB,
         * osmocom spi_xfer left-aligned) */
        s->tx_data = value & 0xFFFF;
        break;
    case SPI_REG_RX_LSB:
    case SPI_REG_RX_MSB:
        break;
    default:
        qemu_log_mask(LOG_UNIMP, "calypso-spi: write 0x%04x at 0x%02x\n",
                       (unsigned)value, (unsigned)offset);
        break;
    }
}

static const MemoryRegionOps calypso_spi_ops = {
    .read = calypso_spi_read,
    .write = calypso_spi_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
    .impl = { .min_access_size = 2, .max_access_size = 2 },
};

static void calypso_spi_realize(DeviceState *dev, Error **errp)
{
    CalypsoSPIState *s = CALYPSO_SPI(dev);

    memory_region_init_io(&s->iomem, OBJECT(dev), &calypso_spi_ops, s,
                          "calypso-spi", 0x100);
    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->iomem);
    sysbus_init_irq(SYS_BUS_DEVICE(dev), &s->irq);
}

static void calypso_spi_reset(DeviceState *dev)
{
    CalypsoSPIState *s = CALYPSO_SPI(dev);

    s->set1 = 0;
    s->set2 = 0;
    s->ctrl = 0;
    s->status = SPI_STATUS_RE_ST | SPI_STATUS_WE_ST;
    s->tx_data = 0;
    s->tx_lsb = 0;
    s->rx_data = 0;
    s->page = 0;
    memset(s->abb_regs, 0, sizeof(s->abb_regs));

    /* powered on by the ON button, button released (no ONREFLT: TI loops in
     * ABB_Read_Status waiting for its release) */
    s->abb_regs[ABB_VRPCSTS] = ABB_ONBSTS;
    s->abb_regs[ABB_ITSTATREG] = 0x00;
}

static void calypso_spi_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);

    dc->realize = calypso_spi_realize;
    device_class_set_legacy_reset(dc, calypso_spi_reset);
    dc->desc = "Calypso SPI controller + TWL3025 ABB";
}

static const TypeInfo calypso_spi_info = {
    .name          = TYPE_CALYPSO_SPI,
    .parent        = TYPE_SYS_BUS_DEVICE,
    .instance_size = sizeof(CalypsoSPIState),
    .class_init    = calypso_spi_class_init,
};

static void calypso_spi_register_types(void)
{
    type_register_static(&calypso_spi_info);
}

type_init(calypso_spi_register_types)

4.86 https://github.com/bbaranoff/qosmo/blob/main/hw/timer/calypso_timer.c

6439 octets, 226 lignes → 226 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */

#include "qemu/osdep.h"
#include "hw/sysbus.h"
#include "hw/irq.h"
#include "qemu/timer.h"
#include "hw/arm/calypso/calypso_timer.h"

#define TIMER_CTRL_START         (1 << 0)
#define TIMER_CTRL_RELOAD        (1 << 1)
#define TIMER_CTRL_PRESCALER_SH  2
#define TIMER_CTRL_PRESCALER_MSK (0x7 << 2)
#define TIMER_CTRL_CLOCK_ENABLE  (1 << 5)

#define CALYPSO_BASE_CLK   13000000LL

static QEMUClockType calypso_timer_clock(void)
{
    return QEMU_CLOCK_REALTIME;
}

static bool calypso_timer_should_run(CalypsoTimerState *s)
{
    return (s->ctrl & TIMER_CTRL_START) && (s->ctrl & TIMER_CTRL_CLOCK_ENABLE);
}

static void calypso_timer_recompute_tick(CalypsoTimerState *s)
{
    int prescaler = (s->ctrl & TIMER_CTRL_PRESCALER_MSK) >> TIMER_CTRL_PRESCALER_SH;

    int64_t divider = 32LL << prescaler;
    int64_t freq = CALYPSO_BASE_CLK / divider;
    if (freq <= 0) freq = 1;
    s->tick_ns = NANOSECONDS_PER_SECOND / freq;
}

static uint16_t calypso_timer_current_count(CalypsoTimerState *s)
{
    if (!s->running) return s->count;
    int64_t now = qemu_clock_get_ns(calypso_timer_clock());
    int64_t elapsed = now - s->epoch_ns;
    if (elapsed < 0) elapsed = 0;
    int64_t ticks = elapsed / s->tick_ns;
    int64_t period = (int64_t)s->load + 1;
    if (s->ctrl & TIMER_CTRL_RELOAD) {
        ticks %= period;
    } else if (ticks > s->load) {
        return 0;
    }
    return (uint16_t)(s->load - ticks);
}

static void calypso_timer_schedule_wrap(CalypsoTimerState *s)
{

    int64_t now = qemu_clock_get_ns(calypso_timer_clock());
    uint16_t cur = calypso_timer_current_count(s);
    int64_t ns_to_wrap = (int64_t)(cur + 1) * s->tick_ns;
    timer_mod(s->timer, now + ns_to_wrap);
}

static void calypso_timer_tick(void *opaque)
{
    CalypsoTimerState *s = CALYPSO_TIMER(opaque);

    if (!s->running) return;

    qemu_irq_raise(s->irq);

    if (s->ctrl & TIMER_CTRL_RELOAD) {

        s->epoch_ns = qemu_clock_get_ns(calypso_timer_clock());
        s->count = s->load;
        timer_mod(s->timer, s->epoch_ns + (int64_t)(s->load + 1) * s->tick_ns);
    } else {
        s->running = false;
        s->count = 0;
    }
}

static void calypso_timer_start(CalypsoTimerState *s)
{
    if (s->load == 0) return;
    calypso_timer_recompute_tick(s);
    s->count = s->load;
    s->running = true;
    s->epoch_ns = qemu_clock_get_ns(calypso_timer_clock());
    timer_mod(s->timer, s->epoch_ns + (int64_t)(s->load + 1) * s->tick_ns);
}

#define LOST_TICKS_PER_TDMA 1875

static CalypsoTimerState *g_lost_timer;

void calypso_timer_register_lost(DeviceState *d)
{
    g_lost_timer = CALYPSO_TIMER(d);
}

void calypso_timer_lost_frame_tick(uint32_t fn)
{
    CalypsoTimerState *s = g_lost_timer;
    if (!s || !s->running) {
        return;
    }
    uint32_t period = (uint32_t)s->load + 1;
    uint32_t step = ((uint64_t)fn * LOST_TICKS_PER_TDMA) % period;
    s->lost_latch_count = (uint16_t)(s->load - step);
    s->lost_latch_active = true;
}

static uint64_t calypso_timer_read(void *opaque, hwaddr offset, unsigned size)
{
    CalypsoTimerState *s = CALYPSO_TIMER(opaque);


    switch (offset) {
    case 0x00: return s->ctrl;
    case 0x02: return s->load;
    case 0x04:
        if (s->lost_latch_active) {
            if (s == g_lost_timer) {

                uint32_t period = (uint32_t)s->load + 1;
                s->lost_read_phase = (s->lost_read_phase + LOST_TICKS_PER_TDMA) % period;
                return (uint16_t)(s->load - s->lost_read_phase);
            }
            return s->lost_latch_count;
        }
        return calypso_timer_current_count(s);
    default:   return 0;
    }
}

static void calypso_timer_write(void *opaque, hwaddr offset, uint64_t value,
                                 unsigned size)
{
    CalypsoTimerState *s = CALYPSO_TIMER(opaque);

    switch (offset) {
    case 0x00: {
        bool was_running = s->running;
        uint16_t old_ctrl = s->ctrl;
        s->ctrl = value & 0xFF;
        if (calypso_timer_should_run(s)) {
            if (!was_running) {
                calypso_timer_start(s);
            } else if ((old_ctrl & TIMER_CTRL_PRESCALER_MSK) !=
                       (s->ctrl & TIMER_CTRL_PRESCALER_MSK)) {

                s->count = calypso_timer_current_count(s);
                calypso_timer_recompute_tick(s);
                s->epoch_ns = qemu_clock_get_ns(calypso_timer_clock()) -
                              (int64_t)(s->load - s->count) * s->tick_ns;
                calypso_timer_schedule_wrap(s);
            }
        } else {
            s->count = calypso_timer_current_count(s);
            s->running = false;
            timer_del(s->timer);
        }
        break;
    }
    case 0x02:
        s->load = value;
        break;
    }
}

static const MemoryRegionOps calypso_timer_ops = {
    .read = calypso_timer_read,
    .write = calypso_timer_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
    .valid = { .min_access_size = 1, .max_access_size = 2 },
    .impl  = { .min_access_size = 1, .max_access_size = 2 },
};

static void calypso_timer_realize(DeviceState *dev, Error **errp)
{
    CalypsoTimerState *s = CALYPSO_TIMER(dev);

    memory_region_init_io(&s->iomem, OBJECT(dev), &calypso_timer_ops, s,
                          "calypso-timer", 0x100);
    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->iomem);
    sysbus_init_irq(SYS_BUS_DEVICE(dev), &s->irq);

    s->timer = timer_new_ns(calypso_timer_clock(), calypso_timer_tick, s);
}

static void calypso_timer_reset(DeviceState *dev)
{
    CalypsoTimerState *s = CALYPSO_TIMER(dev);

    s->load = 0;
    s->count = 0;
    s->ctrl = 0;
    s->prescaler = 0;
    s->running = false;
    s->lost_latch_active = false;
    s->lost_latch_count = 0;
    s->lost_read_phase = 0;
    timer_del(s->timer);
}

static void calypso_timer_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);

    dc->realize = calypso_timer_realize;
    device_class_set_legacy_reset(dc, calypso_timer_reset);
    dc->desc = "Calypso GP/Watchdog timer";
}

static const TypeInfo calypso_timer_info = {
    .name          = TYPE_CALYPSO_TIMER,
    .parent        = TYPE_SYS_BUS_DEVICE,
    .instance_size = sizeof(CalypsoTimerState),
    .class_init    = calypso_timer_class_init,
};

static void calypso_timer_register_types(void)
{
    type_register_static(&calypso_timer_info);
}

type_init(calypso_timer_register_types)

4.87 https://github.com/bbaranoff/qosmo/blob/main/include/exec/translation-block.h

6090 octets, 159 lignes → 159 lignes

/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
 * Definition of TranslationBlock.
 *  Copyright (c) 2003 Fabrice Bellard
 */

#ifndef EXEC_TRANSLATION_BLOCK_H
#define EXEC_TRANSLATION_BLOCK_H

#include "qemu/thread.h"
#include "exec/cpu-common.h"
#ifdef CONFIG_USER_ONLY
#include "qemu/interval-tree.h"
#endif

/*
 * Page tracking code uses ram addresses in system mode, and virtual
 * addresses in userspace mode.  Define tb_page_addr_t to be an
 * appropriate type.
 */
#if defined(CONFIG_USER_ONLY)
typedef vaddr tb_page_addr_t;
#define TB_PAGE_ADDR_FMT "%" VADDR_PRIx
#else
typedef ram_addr_t tb_page_addr_t;
#define TB_PAGE_ADDR_FMT RAM_ADDR_FMT
#endif

/*
 * Translation Cache-related fields of a TB.
 * This struct exists just for convenience; we keep track of TB's in a binary
 * search tree, and the only fields needed to compare TB's in the tree are
 * @ptr and @size.
 * Note: the address of search data can be obtained by adding @size to @ptr.
 */
struct tb_tc {
    const void *ptr;    /* pointer to the translated code */
    size_t size;
};

struct TranslationBlock {
    /*
     * Guest PC corresponding to this block.  This must be the true
     * virtual address.  Therefore e.g. x86 stores EIP + CS_BASE, and
     * targets like Arm, MIPS, HP-PA, which reuse low bits for ISA or
     * privilege, must store those bits elsewhere.
     *
     * If CF_PCREL, the opcodes for the TranslationBlock are written
     * such that the TB is associated only with the physical page and
     * may be run in any virtual address context.  In this case, PC
     * must always be taken from ENV in a target-specific manner.
     * Unwind information is taken as offsets from the page, to be
     * deposited into the "current" PC.
     */
    vaddr pc;

    /*
     * Target-specific data associated with the TranslationBlock, e.g.:
     * x86: the original user, the Code Segment virtual base,
     * arm: an extension of tb->flags,
     * s390x: instruction data for EXECUTE,
     * sparc: the next pc of the instruction queue (for delay slots).
     */
    uint64_t cs_base;

    uint32_t flags; /* flags defining in which context the code was generated */
    uint32_t cflags;    /* compile flags */

/* Note that TCG_MAX_INSNS is 512; we validate this match elsewhere. */
#define CF_COUNT_MASK    0x000001ff
#define CF_NO_GOTO_TB    0x00000200 /* Do not chain with goto_tb */
#define CF_NO_GOTO_PTR   0x00000400 /* Do not chain with goto_ptr */
#define CF_SINGLE_STEP   0x00000800 /* gdbstub single-step in effect */
#define CF_MEMI_ONLY     0x00001000 /* Only instrument memory ops */
#define CF_USE_ICOUNT    0x00002000
#define CF_INVALID       0x00004000 /* TB is stale. Set with @jmp_lock held */
#define CF_PARALLEL      0x00008000 /* Generate code for a parallel context */
#define CF_NOIRQ         0x00010000 /* Generate an uninterruptible TB */
#define CF_PCREL         0x00020000 /* Opcodes in TB are PC-relative */
#define CF_BP_PAGE       0x00040000 /* Breakpoint present in code page */
#define CF_CLUSTER_MASK  0xff000000 /* Top 8 bits are cluster ID */
#define CF_CLUSTER_SHIFT 24

    /*
     * Above fields used for comparing
     */

    /* size of target code for this block (1 <= size <= TARGET_PAGE_SIZE) */
    uint16_t size;
    uint16_t icount;

    struct tb_tc tc;

    /*
     * Track tb_page_addr_t intervals that intersect this TB.
     * For user-only, the virtual addresses are always contiguous,
     * and we use a unified interval tree.  For system, we use a
     * linked list headed in each PageDesc.  Within the list, the lsb
     * of the previous pointer tells the index of page_next[], and the
     * list is protected by the PageDesc lock(s).
     */
#ifdef CONFIG_USER_ONLY
    IntervalTreeNode itree;
#else
    uintptr_t page_next[2];
    tb_page_addr_t page_addr[2];
#endif

    /* jmp_lock placed here to fill a 4-byte hole. Its documentation is below */
    QemuSpin jmp_lock;

    /* The following data are used to directly call another TB from
     * the code of this one. This can be done either by emitting direct or
     * indirect native jump instructions. These jumps are reset so that the TB
     * just continues its execution. The TB can be linked to another one by
     * setting one of the jump targets (or patching the jump instruction). Only
     * two of such jumps are supported.
     */
#define TB_JMP_OFFSET_INVALID 0xffff /* indicates no jump generated */
    uint16_t jmp_reset_offset[2]; /* offset of original jump target */
    uint16_t jmp_insn_offset[2];  /* offset of direct jump insn */
    uintptr_t jmp_target_addr[2]; /* target address */

    /*
     * Each TB has a NULL-terminated list (jmp_list_head) of incoming jumps.
     * Each TB can have two outgoing jumps, and therefore can participate
     * in two lists. The list entries are kept in jmp_list_next[2]. The least
     * significant bit (LSB) of the pointers in these lists is used to encode
     * which of the two list entries is to be used in the pointed TB.
     *
     * List traversals are protected by jmp_lock. The destination TB of each
     * outgoing jump is kept in jmp_dest[] so that the appropriate jmp_lock
     * can be acquired from any origin TB.
     *
     * jmp_dest[] are tagged pointers as well. The LSB is set when the TB is
     * being invalidated, so that no further outgoing jumps from it can be set.
     *
     * jmp_lock also protects the CF_INVALID cflag; a jump must not be chained
     * to a destination TB that has CF_INVALID set.
     */
    uintptr_t jmp_list_head;
    uintptr_t jmp_list_next[2];
    uintptr_t jmp_dest[2];

    /* -d exec: lookup_symbol(pc) resolved once at translation time instead
     * of a bsearch in the ELF table for every executed block. */
    const char *log_sym;
};

/* The alignment given to TranslationBlock during allocation. */
#define CODE_GEN_ALIGN  16

/* Hide the qatomic_read to make code a little easier on the eyes */
static inline uint32_t tb_cflags(const TranslationBlock *tb)
{
    return qatomic_read(&tb->cflags);
}

#endif /* EXEC_TRANSLATION_BLOCK_H */

4.88 https://github.com/bbaranoff/qosmo/blob/main/include/hw/arm/calypso/calypso_api.h

3032 octets, 96 lignes → 96 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef HW_ARM_CALYPSO_API_H
#define HW_ARM_CALYPSO_API_H

#include <stdint.h>

#define CALYPSO_API_BASE        0xFFD00000u
#define CALYPSO_API_SIZE        (64 * 1024)
#define CALYPSO_API_WORDS       (CALYPSO_API_SIZE / 2)

#define API_W_PAGE(p)           ((p) ? 0x0028u : 0x0000u)
#define API_R_PAGE(p)           ((p) ? 0x0078u : 0x0050u)
#define API_NDB                 0x01A8u

#define WP_D_TASK_D             0x00u
#define WP_D_BURST_D            0x02u
#define WP_D_TASK_U             0x04u
#define WP_D_BURST_U            0x06u
#define WP_D_TASK_MD            0x08u
#define WP_D_TASK_RA            0x0Eu
#define WP_D_FN                 0x10u

#define RP_D_TASK_D             0x00u
#define RP_D_BURST_D            0x02u
#define RP_D_TASK_MD            0x08u
#define RP_A_SERV_DEMOD         0x10u
#define RP_A_PM                 0x18u
#define RP_A_SCH                0x1Eu

#define NDB_D_DSP_PAGE          0x000u
#define NDB_D_FB_DET            0x048u
#define NDB_A_SYNC_DEMOD        0x04Cu
#define NDB_A_DD_1              0x108u
#define NDB_A_DU_1              0x134u
#define NDB_D_A5MODE            0x1CEu
#define NDB_A_CD                0x1FCu
#define NDB_A_FD                0x21Au
#define NDB_A_DD_0              0x238u
#define NDB_A_CU                0x264u
#define NDB_A_FU                0x282u
#define NDB_D_RACH              0x2CCu
#define NDB_A_KC                0x2CEu

#define API_BL_STATUS           0x0FFEu
#define API_VERSION             0x01B4u
#define API_VERSION2            0x01B6u
#define BL_STATUS_BOOT          0x0001u
#define BL_STATUS_READY         0x0002u
#define API_VERSION_VALUE       0x3606u

#define B_GSM_PAGE              (1u << 0)
#define B_GSM_TASK              (1u << 1)
#define B_BLUD                  (1u << 15)

#define D_TOA                   0
#define D_PM                    1
#define D_ANGLE                 2
#define D_SNR                   3

#define PM_DSP_TASK             1
#define FB_DSP_TASK             5
#define SB_DSP_TASK             6
#define RACH_DSP_TASK           10   /* firmware include/calypso/l1_environment.h:49 */
#define AUL_DSP_TASK            11
#define DUL_DSP_TASK            12
#define TCHT_DSP_TASK           13
#define TCHA_DSP_TASK           14
#define ALLC_DSP_TASK           24
#define TCHD_DSP_TASK           28

#define GSM_HYPERFRAME          2715648u
#define GSM_TDMA_NS             4615384

uint16_t *calypso_api_ram(void);
const char *calypso_firmware_elf(void);
uint32_t calypso_firmware_symbol(const char *name);
uint32_t calypso_trx_get_fn(void);
void calypso_trx_autosync_fn(uint32_t sch_fn);
void calypso_trx_force_tick(uint32_t fn);

static inline uint16_t *api_wp(uint8_t page, unsigned off)
{
    return &calypso_api_ram()[(API_W_PAGE(page) + off) / 2];
}

static inline uint16_t *api_rp(uint8_t page, unsigned off)
{
    return &calypso_api_ram()[(API_R_PAGE(page) + off) / 2];
}

static inline uint16_t *api_ndb(unsigned off)
{
    return &calypso_api_ram()[(API_NDB + off) / 2];
}

#endif

4.89 https://github.com/bbaranoff/qosmo/blob/main/include/hw/arm/calypso/calypso_asm4532.h

1892 octets, 45 lignes → 45 lignes

/*
 * calypso_asm4532.h - model of the ASM4532 antenna switch.
 *
 * Position in the chain (osmocom wiki TypicalCalypsoModemDesign):
 *   antenna <-> ASM4532 <-> [SAW Rx -> TRF6151]  or  [RF3166 -> TX]
 * It connects the antenna EITHER to one of the GSM/DCS/PCS Rx paths,
 * OR to the PA output for the duration of the burst.
 *
 * Control: TSPACT lines, not the serial TSP. Mapping taken as is from
 * osmocom-bb board/compal/rffe_dualband.c (the emulated C123 board):
 *   TRENA    = TSPACT(6)  Transmit Enable (Antenna Switch)  -- ACTIVE LOW
 *   GSM_TXEN = TSPACT(8)  GSM (as opposed to DCS)           -- ACTIVE LOW
 * and the rffe_mode() sequence:
 *   tspact |= TRENA | GSM_TXEN;          (idle: both deasserted)
 *   if TX: tspact &= ~TRENA;             (assertion)
 *          if GSM900: tspact &= ~GSM_TXEN;
 *
 * SCOPE: this block is on the TX / band selection side. It has NO effect
 *    on FB/SB acquisition: with a synthetic downlink, the Rx path is already
 *    assumed connected. Modelling it (a) makes the TX sequence readable,
 *    (b) detects a transmission requested while the antenna is not switched.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#ifndef HW_ARM_CALYPSO_ASM4532_H
#define HW_ARM_CALYPSO_ASM4532_H

#include <stdint.h>
#include <stdbool.h>

/* Called from calypso_tsp.c on every tsp_act_update() (TSP_ACT_L/U write),
 * with the COMPLETE state of the 16 TSPACT lines. */
void calypso_asm4532_tspact_update(uint16_t tspact, uint32_t fn);

/* true = the antenna is switched to the PA output (TRENA asserted). */
bool calypso_asm4532_tx_connected(void);

/* true = GSM900 band selected for transmission (GSM_TXEN asserted),
 * false = DCS1800. Only meaningful if tx_connected(). */
bool calypso_asm4532_band_gsm(void);

/* Counters for the barriers and diagnostics. */
uint32_t calypso_asm4532_tx_windows(void);

#endif /* HW_ARM_CALYPSO_ASM4532_H */

4.90 https://github.com/bbaranoff/qosmo/blob/main/include/hw/arm/calypso/calypso_debug.h

5013 octets, 118 lignes → 118 lignes

/*
 * calypso_debug.h - env-gated probe output infrastructure
 *
 * All diagnostic probes are SILENT by default. They are enabled through a
 * single env var:
 *
 *   CALYPSO_DEBUG="probe1,probe2,probe3,..."    - enables the listed probes
 *   CALYPSO_DEBUG=ALL                          - enables everything
 *   CALYPSO_DEBUG="ALL,..."                     - same
 *
 * The probe name is normalised to upper case, with '-' / ' ' / '.' / '/'
 * replaced by '_'. Matching is case-insensitive: probe "IMR-W" matches the
 * entry "imr-w" as well as "IMR_W".
 *
 * Cache: the list is parsed ONCE on the first call and stored.
 * Lookup = O(N) over the list (~50 ns for the typical 10-20 probes).
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#ifndef HW_ARM_CALYPSO_DEBUG_H
#define HW_ARM_CALYPSO_DEBUG_H

#include <stdbool.h>
#include <stdio.h>

/* Master gate: cached, parsed once. When CALYPSO_DEBUG is empty,
 * calypso_debug_master == 0 and calypso_debug_enabled() returns false
 * inline, WITHOUT any function call (no per-instruction overhead at the
 * call sites in the DSP hot loop). Identical behaviour otherwise. */
extern int  calypso_debug_master;
void        calypso_debug_master_init(void);
bool        calypso_debug_enabled_(const char *probe_name);   /* real impl */

static inline bool calypso_debug_enabled(const char *probe_name)
{
    if (__builtin_expect(calypso_debug_master < 0, 0)) {
        calypso_debug_master_init();
    }
    if (__builtin_expect(calypso_debug_master == 0, 1)) {
        return false;
    }
    return calypso_debug_enabled_(probe_name);
}

/* CALYPSO_DBG: gated fprintf. probe_name is a compile-time string literal.
 * fmt includes the prefix (e.g. "[c54x] IMR-W ...") and the final \n. */
#define CALYPSO_DBG(probe_name, fmt, ...) \
    do { \
        if (calypso_debug_enabled(probe_name)) { \
            fprintf(stderr, fmt, ##__VA_ARGS__); \
        } \
    } while (0)

/* Per-component wrappers: automatic prefix + \n + gate on the probe name
 * passed by the caller. */
#define C54_DBG(probe, fmt, ...) \
    CALYPSO_DBG(probe, "[c54x] " fmt "\n", ##__VA_ARGS__)
#define TRX_DBG(probe, fmt, ...) \
    CALYPSO_DBG(probe, "[calypso-trx] " fmt "\n", ##__VA_ARGS__)
#define BSP_DBG(probe, fmt, ...) \
    CALYPSO_DBG(probe, "[BSP] " fmt "\n", ##__VA_ARGS__)
#define IOTA_DBG(probe, fmt, ...) \
    CALYPSO_DBG(probe, "[iota] " fmt "\n", ##__VA_ARGS__)
#define PCB_DBG(probe, fmt, ...) \
    CALYPSO_DBG(probe, "[pcb] " fmt "\n", ##__VA_ARGS__)
#define INTH_DBG(probe, fmt, ...) \
    CALYPSO_DBG(probe, "[INTH] " fmt "\n", ##__VA_ARGS__)
#define UART_DBG(probe, fmt, ...) \
    CALYPSO_DBG(probe, fmt "\n", ##__VA_ARGS__)
#define TIMER_DBG(probe, fmt, ...) \
    CALYPSO_DBG(probe, "[timer] " fmt "\n", ##__VA_ARGS__)
#define TWL3025_DBG(probe, fmt, ...) \
    CALYPSO_DBG(probe, "[twl3025] " fmt "\n", ##__VA_ARGS__)

/* cdbg_env: compatibility shim for sites that test
 * getenv("CALYPSO_X")=="1". Returns "1" if the token is active in
 * CALYPSO_DEBUG, NULL otherwise. A getenv gate becomes a token by changing
 * only the call getenv -> cdbg_env. */
static inline const char *cdbg_env(const char *token)
{
    return calypso_debug_enabled(token) ? "1" : NULL;
}

#endif /* HW_ARM_CALYPSO_DEBUG_H */

/* =============================================================================
 *  calypso_gate() - ONE semantics for every environment-driven switch
 * =============================================================================
 *
 *  Every "is this option active?" question goes through this helper, so
 *  that `X=0` and `X=` get the same answer everywhere (an idiom such as
 *  `getenv("X") ? 1 : 0` would treat a variable declared EMPTY in an env
 *  file as ACTIVE).
 *
 *  SINGLE SEMANTICS:
 *
 *      variable absent                       -> `defaut`
 *      "0" · "" · "no" · "off" · "false"     -> 0     (case-insensitive)
 *      everything else                       -> 1
 *
 *  So `X=0` ALWAYS disables, and `X=1` always enables. `defaut` makes
 *  explicit, at the call site, what absence means.
 *
 *  WHAT THIS HELPER IS NOT. It answers yes or no. Variables that carry a
 *  VALUE (address, length, rate, path, list) keep their `getenv` +
 *  `strtoul`/`atoi`.
 * ---------------------------------------------------------------------------- */
int calypso_gate(const char *nom, int defaut);

/* getenv() memoised by name. The C54x run loop and its probes
 * consult the environment on hot paths (c54x_run is entered ~1200 times per
 * TDMA frame by the benches, c54x_exec_one 17k times), and glibc's getenv()
 * is a linear scan of the environment with a strncmp per entry: measured 40 %
 * of the replay's wall time under gdb sampling, 6.7 ms per 4.615 ms frame.
 * The environment never changes once the core runs (c54x_exe sets its one
 * variable before c54x_init()), so the first answer is kept. NULL is cached
 * too. Returns the same pointer glibc would (the environ string). */
const char *calypso_getenv(const char *name);

4.91 https://github.com/bbaranoff/qosmo/blob/main/include/hw/arm/calypso/calypso_dsp_pont.h

4892 octets, 93 lignes → 93 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * calypso_dsp_pont.h - the bridge between the ARM under QEMU and an
 * out-of-process C54x DSP (c54x_exe --arm).
 *
 * Why a bridge: c54x_exe runs the TI mask ROM on its own, in milliseconds;
 * the osmocom-bb layer1 is ARM code and only runs under QEMU. To couple them
 * without compiling the DSP into QEMU, both share the API RAM (the
 * 0xFFD00000 window on the ARM side, DARAM 0x0800 on the DSP side) through
 * a /dev/shm segment, and synchronise frame by frame over a UNIX
 * SOCK_SEQPACKET socket.
 *
 * The protocol reproduces EXACTLY what the in-process calypso_trx.c does on
 * every tdma_tick: DMA tick, DSP boot up to the first IDLE, TPU-frame
 * interrupt if the IMR arms it, a c54x_run budget, and the "busy -> IDLE"
 * edge that raises the API IRQ (IRQ 15) on the ARM side. Nothing more,
 * nothing less: any divergence between the bridge and the in-process path
 * is a bridge bug.
 *
 * Frame lock-step: QEMU sends TICK and WAITS for DONE before raising the
 * TPU-frame IRQ. The DSP therefore never free-runs; the ARM/DSP protocol is
 * synchronous (write/read pages swapped on the frame IRQ).
 *
 * This file is included on both sides (QEMU and c54x_exe): it depends on
 * nothing but <stdint.h>.
 */
#ifndef CALYPSO_DSP_PONT_H
#define CALYPSO_DSP_PONT_H

#include <stdint.h>

/* Shared segment: shm_open() of this name = /dev/shm/calypso_api_ram.
 * Size = CALYPSO_API_SIZE (64 KiB, 32 K words); created by c54x_exe, opened
 * read/write by QEMU. */
#define CALYPSO_PONT_SHM        "/calypso_api_ram"
/* Segment size: the API window as seen by the DSP, C54X_API_SIZE = 0x2000
 * words. The segment is NOT a copy: on the c54x_exe side it is mmap'ed
 * MAP_FIXED OVER data[0x0800..0x27FF] of the C54xState (data[] is page
 * aligned for that), because the core writes its API window into data[]
 * and only copies to api_ram on some paths; api_ram then aliases
 * &data[0x0800]. On the QEMU side it covers the first 16 KiB of the 64 KiB
 * ARM window. */
#define CALYPSO_PONT_SHM_BYTES  0x4000
/* Synchronisation socket, c54x_exe side = server. */
#define CALYPSO_PONT_SOCK       "/tmp/calypso_dsp.sock"
#define CALYPSO_PONT_MAGIC      0x43353458u   /* 'C54X' */

enum CalypsoPontType {
    PONT_HELLO    = 1,  /* QEMU -> DSP: a = API RAM words expected, b = magic   */
    PONT_HELLO_OK = 2,  /* DSP -> QEMU: a = API RAM words served,   b = magic   */
    PONT_RESET    = 3,  /* QEMU -> DSP: the ARM wrote DL_STATUS (a = value)     */
    PONT_TICK     = 4,  /* QEMU -> DSP: one TDMA frame, a = fn, b = d_dsp_page  */
    PONT_DONE     = 5,  /* DSP -> QEMU: frame played, a = flags, b = insns      */
    PONT_GO       = 6,  /* QEMU -> DSP: ARM l1_sync done, deliver the burst (2 phases) */
    /* Dedicated channel: a = TN, b = kind (0 SDCCH/4, 1 SDCCH/8, 0xFF
     * released), c = subchannel. QEMU learns it from the firmware's L1CTL
     * stream (calypso_dcch_tap.c) and the DSP needs it to know WHICH timeslot
     * to deliver: the bridge sends it all eight, but it only plays one per
     * tick. Sent just before a TICK, never while waiting for a GO. */
    PONT_DCCH     = 7,
    /* Distinct from GO: a GO arriving after the 2 s timeout of pont.c
     * ("PONT_GO attendu") falls back into the main loop of c54x_exe, which
     * must not read it as BYE. */
    PONT_BYE      = 8,  /* either side: clean end                               */
};

/* PONT_DONE.a flags */
#define PONT_DONE_API_IRQ   (1u << 0)   /* busy -> IDLE edge: raise IRQ 15     */
#define PONT_DONE_IDLE      (1u << 1)   /* the DSP is in IDLE after the frame  */
#define PONT_DONE_INIT      (1u << 2)   /* boot finished (first IDLE reached)  */
#define PONT_DONE_RUNNING   (1u << 3)   /* dsp->running                         */
/* TICK.b: bit 0 = W page announced by the ARM (d_dsp_page & 1), bit 16 = the
 * ARM armed the DSP frame interrupt (TPU_CTRL_DSP_EN, one-shot) since the
 * previous tick. Without this bit the DSP gets no frame interrupt. */
#define CALYPSO_PONT_TICK_IRQ_TRAME (1u << 16)
/* TWO-PHASE TICK (bit 17). The ROM demodulates the burst of frame N and
 * writes the R page in its frame N+1 ISR, THEN reads the W page; the ARM
 * reads that R page in l1_sync(N+1) and posts the W page of N+2. The silicon
 * order within a frame is therefore: DSP ISR (results + dispatch) ->
 * ARM l1_sync -> burst reception. With this bit the DSP plays the ISR up to
 * the arming of the RX window, answers DONE|PONT_DONE_PHASE_A, and waits for
 * PONT_GO (sent when l1_sync is finished) to deliver the burst and finish. */
#define CALYPSO_PONT_TICK_DEUX_PHASES (1u << 17)
#define PONT_DONE_PHASE_A   (1u << 4)   /* ISR played, burst not delivered: waits for GO */

typedef struct {
    uint32_t type;
    uint32_t a;
    uint32_t b;
    uint32_t c;   /* TICK: tpu_offset (qbits) to align the DSP RX window */
} CalypsoPontMsg;

#endif

4.92 https://github.com/bbaranoff/qosmo/blob/main/include/hw/arm/calypso/calypso_inth.h

1449 octets, 47 lignes → 47 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */

#ifndef HW_INTC_CALYPSO_INTH_H
#define HW_INTC_CALYPSO_INTH_H

#include "hw/sysbus.h"
#include "qom/object.h"

#define TYPE_CALYPSO_INTH "calypso-inth"
OBJECT_DECLARE_SIMPLE_TYPE(CalypsoINTHState, CALYPSO_INTH)

#define CALYPSO_INTH_NUM_IRQS  32

struct CalypsoINTHState {

    SysBusDevice parent_obj;

    MemoryRegion iomem;

    qemu_irq parent_irq;
    qemu_irq parent_fiq;

    uint16_t ilr[CALYPSO_INTH_NUM_IRQS];

    uint16_t ith_v;
    uint16_t fiq_v;
    uint32_t levels;
    uint32_t mask;
    int rr_start;
    uint16_t last_num;          /* last IRQ number handed to the ARM (IRQ_NUM read) */
    uint64_t frame_eoi;         /* IRQ_CTRL end-of-service writes for the TPU frame IRQ */
};

/* Acknowledge emitted by the emulated DSP (calypso_c54x.c). Without a C54x
 * layer 1, nobody calls it. */
void calypso_inth_arm_ack(void);

/* [2026-09-21] End-of-service count of the TPU frame interrupt (IRQ 4): the
 * firmware's irq() reads IRQ_NUM, runs l1_sync(), then writes IRQ_CTRL bit 0
 * ("new IRQ agreement"). One more count = one l1_sync() finished. The DSP
 * lock-step (calypso_trx.c) waits on it before handing the frame to the DSP,
 * so the ARM reads the R page before the DSP overwrites it, as on silicon. */
uint64_t calypso_inth_frame_eoi(void);
bool calypso_inth_irq_masked(int irq);
bool calypso_inth_irq_pending(int irq);   /* interrupt raised, not yet serviced */

#endif

4.93 https://github.com/bbaranoff/qosmo/blob/main/include/hw/arm/calypso/calypso_invariants.h

405 octets, 9 lignes → 9 lignes

#ifndef CALYPSO_INVARIANTS_H
#define CALYPSO_INVARIANTS_H
#include <stdbool.h>
/* Manifest: dump the active CALYPSO_* once. Gate CALYPSO_INVARIANTS. */
void calypso_manifest_once(void);
/* Invariant: if ok=false, [INVARIANT-FAIL] <tag> once. Gate CALYPSO_INVARIANTS (default off). */
bool calypso_invariant(const char *tag, bool ok, const char *fmt, ...)
    __attribute__((format(printf, 3, 4)));
#endif

4.94 https://github.com/bbaranoff/qosmo/blob/main/include/hw/arm/calypso/calypso_iota.h

1806 octets, 48 lignes → 48 lignes

/*
 * TWL3025 / IOTA — analog baseband chip model.
 *
 * Sits on the Calypso TSP serial bus (dev_idx 0). The L1 firmware programs
 * BDLON / BDLENA / BULON / BULENA via single-byte TSP writes; this model
 * tracks the resulting downlink/uplink window state so other QEMU
 * components (notably calypso_bsp) can gate sample DMA the way the real
 * BSP serial link is gated by IOTA's BDLENA pin.
 *
 * Reference: osmocom-bb src/target/firmware/include/abb/twl3025.h
 *            (BDLON/BDLENA/BULON/BULENA bit values).
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#ifndef HW_ARM_CALYPSO_IOTA_H
#define HW_ARM_CALYPSO_IOTA_H

#include <stdbool.h>
#include <stdint.h>

/* Bit values written by twl3025_tsp_write() — straight from twl3025.h */
#define IOTA_TSP_BULON      0x80
#define IOTA_TSP_BULENA     0x40
#define IOTA_TSP_BULCAL     0x20
#define IOTA_TSP_BDLON      0x10
#define IOTA_TSP_BDLCAL     0x08
#define IOTA_TSP_BDLENA     0x04

void calypso_iota_init(void);

/* Process one TSP write (TPU sequencer feeds us). `data` is the 7-bit byte
 * the firmware passed to twl3025_tsp_write(). `expected_tn` is the GSM
 * timeslot the L1 has armed this BDLENA window for, derived by the TPU
 * sequencer from the AT instruction immediately preceding this MOVE. */
void calypso_iota_tsp_write(uint8_t data, uint8_t expected_tn);

/* True while the firmware has BDLENA asserted on IOTA. */
bool calypso_iota_bdl_ena(void);

/* Number of times BDLENA has been asserted (rising edge counter). */
uint32_t calypso_iota_bdl_ena_pulses(void);

/* Atomically take one bdl_ena rising-edge credit if it matches the burst's
 * timeslot. Returns true if a credit existed for this `tn` and was consumed,
 * false otherwise. */
bool calypso_iota_take_bdl_pulse(uint8_t tn);

#endif /* HW_ARM_CALYPSO_IOTA_H */

4.95 https://github.com/bbaranoff/qosmo/blob/main/include/hw/arm/calypso/calypso_l1_ops.h

6424 octets, 125 lignes → 125 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef HW_ARM_CALYPSO_L1_OPS_H
#define HW_ARM_CALYPSO_L1_OPS_H

#include <stdint.h>
#include <stdbool.h>

/* ═════════════════════════════════════════════════════════════════════════
 * THE SEAM BETWEEN THE PLATFORM AND LAYER 1
 * ═════════════════════════════════════════════════════════════════════════
 *
 * The platform (qosmo) models the Calypso and its board: ARM, SoC, TPU, TSP,
 * SIM, ULPD, UART, timers, SPI/I²C, INTH, TRF6151, IOTA, RF3166, ASM4532,
 * XIO, plus the TDMA clock and the API RAM window. It carries NO layer 1 and
 * builds and boots on its own: the osmocom-bb firmware starts and never sees
 * anything on the air. A layer 1 is a separate implementation plugged into
 * this vtable:
 *
 *     l1-grgsm      calypso_l1_grgsm.c   (demodulation by gr-gsm, on the host)
 *     l1-dsp        calypso_c54x.c ...   (real TI mask ROM on an emulated C54x)
 *
 * The layer 1 is otherwise reached only through direct calls into the
 * symbols the platform exports; `api_read`/`api_write` on the 0xFFD00000
 * window are the two places where layer 1 behaviour hides behind a memory
 * access, hence the hooks below.
 *
 * WHAT GOES THROUGH HERE, AND WHAT DOES NOT
 *
 * This vtable only carries the platform -> L1 direction: the 9 places where
 * the hardware has to tell layer 1 that something happened. The opposite
 * direction (the L1 reading the API RAM or the current frame, resynchronising)
 * stays as direct calls into the symbols the platform exports
 * (`calypso_api_ram()`, `calypso_trx_get_fn()`, `calypso_trx_autosync_fn()`,
 * `calypso_tsp_move()`, `calypso_tpu_run_scenario()`). This is deliberate:
 * those symbols are hardware, they have a single implementation by
 * construction, and indirecting them would buy nothing.
 *
 * The `dcch_set`, `dcch_active`, `dcch_is_tch` and `rach_conf` entries are
 * NOT in the vtable either: their only callers are in calypso_l1ctl_tap.c,
 * i.e. inside the grgsm L1. They are internal to that layer 1, not part of
 * the seam; exposing them here would force the l1-dsp layer to implement
 * functions nothing on its execution path calls.
 *
 * IF NO L1 IS REGISTERED
 *
 * Everything is silently inert (see calypso_l1_dispatch.c) and
 * `api_read_override` returns false, so the API RAM behaves as bare RAM.
 * This is the nominal state of the platform alone, not an error: it is what
 * makes `qosmo` testable without embedding either layer 1.
 * ═════════════════════════════════════════════════════════════════════════ */

typedef struct CalypsoL1Ops {
    /* Short name of the layer 1, for traces: "grgsm", "c54x", ... */
    const char *name;

    /* End of calypso_machine_init(). `firmware_elf` is the -kernel of the
     * command line (osmocom-bb's layer1.highram.elf), NULL if absent. */
    void (*init)(const char *firmware_elf);

    /* On every lowering of the TPU frame IRQ, i.e. once per TDMA frame, from
     * frame_irq_lower(). This is the heartbeat of the L1. */
    void (*frame_tick)(void);

    /* 16-bit read in the API RAM window, BEFORE the value goes to the ARM.
     * Return true and fill `*out` to impose a value; returning false lets
     * the real RAM content through.
     * `off` is the offset in the window, not an absolute address. */
    bool (*api_read_override)(uint32_t off, uint16_t *out);

    /* Does the L1 have a block to serve right now? Consulted when the
     * firmware reads d_task_d and finds 0: if true, the platform answers
     * ALLC_DSP_TASK instead of "nothing to do". */
    bool (*si_valid)(void);

    /* Frame number as seen by the L1 (l1s). Used by the platform to detect
     * a continuity break in the burst ring. */
    uint32_t (*l1s_fn)(void);

    /* The firmware just wrote d_burst_d / d_rach / d_dsp_page. These are the
     * three offsets the gr-gsm L1 needs. */
    void (*burst_written)(uint16_t d_burst_d);
    void (*rach_written)(uint16_t d_rach, uint32_t fn);
    void (*page_written)(uint16_t d_dsp_page);

    /* EVERY 16-bit write in the API RAM window, after the platform has stored
     * it. The C54x L1 decodes the whole window (its calypso_dsp_write()),
     * whereas gr-gsm only cares about three named offsets.
     *
     * The three hooks above ARE redundant with this one and stay anyway: an
     * L1 that only wants d_burst_d does not have to filter the whole window
     * to get it. An L1 may fill either or both; the order is
     * api_write_observed() FIRST, then the named hook if there is one. */
    void (*api_write_observed)(uint32_t off, uint16_t val, unsigned size);

    /* A byte went out on the modem UART (sercomm). Optional: this is the hook
     * point of the grgsm L1CTL tap. NULL if the layer 1 does not want it, in
     * which case the UART behaves as a UART. */
    void (*uart_tx_byte)(uint8_t ch);
} CalypsoL1Ops;

/* To be called by the layer 1 from a constructor or its own type_init, hence
 * BEFORE calypso_machine_init(). A second registration overwrites the first
 * and reports it on stderr: two layer 1s in the same binary is a meson.build
 * error, not an operating mode. */
void calypso_l1_register(const CalypsoL1Ops *ops);

/* Name of the active L1, or "aucune" (none). For banners and traces. */
const char *calypso_l1_name(void);

/* The accessors the platform uses. They wrap the "is an L1 registered?" test
 * so that the call sites stay readable. */
void calypso_l1_do_init(const char *firmware_elf);
void calypso_l1_do_frame_tick(void);
bool calypso_l1_do_api_read_override(uint32_t off, uint16_t *out);
bool calypso_l1_do_si_valid(void);
uint32_t calypso_l1_do_l1s_fn(void);
void calypso_l1_do_burst_written(uint16_t d_burst_d);
void calypso_l1_do_rach_written(uint16_t d_rach, uint32_t fn);
void calypso_l1_do_page_written(uint16_t d_dsp_page);
void calypso_l1_do_api_write_observed(uint32_t off, uint16_t val, unsigned size);
void calypso_l1_do_uart_tx_byte(uint8_t ch);

void calypso_l1_disable(const char *why);

#endif

4.96 https://github.com/bbaranoff/qosmo/blob/main/include/hw/arm/calypso/calypso_rf3166.h

2083 octets, 50 lignes → 50 lignes

/*
 * calypso_rf3166.h - model of the RF3166 power amplifier.
 *
 * Position in the chain (doc/CHAINE_RF_MATERIELLE.md §3):
 *   TRF6151 --RF--> RF3166 --RF--> ASM4532 --> antenna
 * The wiki says: "amplifies the signal according to the analog level of the
 * APC"; the APC (Automatic Power Correction) is the power envelope produced
 * by the ABB (TWL3025), in ANALOG form. The PA thus has only two inputs of
 * interest: its digital enable, and that analog level.
 *
 * Enable: TSPACT line, taken from osmocom-bb board/compal/rffe_dualband.c:
 *   PA_ENABLE = TSPACT(1)   -- ACTIVE HIGH
 *
 * SCOPE: purely a TX block. It is not in the Rx path at all, so it cannot
 *    explain anything about FB/SB acquisition. What it provides: making the
 *    transmit sequence checkable, and above all the CROSS-CHECK with the
 *    antenna switch: a PA active while the antenna is not switched to it is
 *    a fault.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#ifndef HW_ARM_CALYPSO_RF3166_H
#define HW_ARM_CALYPSO_RF3166_H

#include <stdint.h>
#include <stdbool.h>

/* Called from calypso_tsp.c on every tsp_act_update(), with the COMPLETE
 * state of the 16 TSPACT lines. */
void calypso_rf3166_tspact_update(uint16_t tspact, uint32_t fn);

/* APC level, to be called by the ABB model once it produces it.
 * Unit: that of the ABB APC DAC (0..255), 0 = no transmission.
 * As long as nobody calls it, the level stays UNKNOWN and out_dbm() says so. */
void calypso_rf3166_set_apc(uint8_t apc_level);

/* true = PA_ENABLE asserted. */
bool calypso_rf3166_enabled(void);

/* Estimated output power, in dBm. Returns INT32_MIN if the APC has never
 * been set (= unknown) or if the PA is off. A MODEL, not a measurement:
 * linear ramp 0..255 -> 0..33 dBm, to be replaced by a real calibration
 * table once the UL is measured. */
int32_t calypso_rf3166_out_dbm(void);

/* Number of times the PA was enabled while the antenna was NOT switched to
 * it (cross-check with calypso_asm4532). Must stay at 0. */
uint32_t calypso_rf3166_faults(void);

#endif /* HW_ARM_CALYPSO_RF3166_H */

4.97 https://github.com/bbaranoff/qosmo/blob/main/include/hw/arm/calypso/calypso_rf_arfcn.h

1842 octets, 40 lignes → 40 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
/* Fake air: the ARFCN the phone is tuned to, from QEMU to c54x_exe.
 *
 * The synthesizer (TRF6151, PLL/PWR registers) is programmed by the TPU, hence in
 * QEMU (calypso_tsp.c), while the bursts enter the DSP in c54x_exe (calypso_bsp.c).
 * QEMU writes every programming (frame, position in the frame in qbits, ARFCN) into
 * a ring in shared memory; c54x_exe looks up the ARFCN of the window it is filling
 * and delivers noise away from the cell frequency (CALYPSO_FAUX_AIR=<arfcn>).
 * One file per instance: each instance has its own /dev/shm. */
#ifndef CALYPSO_RF_ARFCN_H
#define CALYPSO_RF_ARFCN_H

#include <stdint.h>

#define CALYPSO_RF_ARFCN_SHM    "/dev/shm/calypso_rf_arfcn"
#define CALYPSO_RF_ARFCN_MAGIC  0x52464132u   /* "RFA2": layout with RX gain */
#define CALYPSO_RF_ARFCN_N      64u

typedef struct {
    uint32_t fn;        /* QEMU frame (the PONT_TICK one) */
    int16_t  qbit;      /* position of the MOVE in the frame (TPU sequencer) */
    int16_t  arfcn;     /* calypso_trf6151_arfcn(), -1 if unknown */
    /* Total programmed RX gain (calypso_trf6151_total_gain_db): c54x_exe does not
     * have the TRF6151 registers (only QEMU receives the TSP), so its a_pm model
     * needs the gain the TI L1 uses to convert the idle-mode PM (l1ctl_pgc,
     * used_il/used_lna); with the reset gain the cell reads at the floor. */
    int16_t  gain_db;
    /* calypso_trf6151_rf_flags(): bit 15 RX mode, bit 14 TX mode, bit 13 UPLINK
     * frequency (ARFCN in bits 0..10). 0: field unused (c54x_exe falls back to
     * `arfcn` alone). */
    int16_t  reserve;
} CalypsoRfProg;

typedef struct {
    uint32_t magic;
    volatile uint32_t widx;                    /* total number of entries written */
    CalypsoRfProg e[CALYPSO_RF_ARFCN_N];
} CalypsoRfArfcnShm;

#endif

4.98 https://github.com/bbaranoff/qosmo/blob/main/include/hw/arm/calypso/calypso_sim.h

1630 octets, 48 lignes → 48 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef HW_ARM_CALYPSO_SIM_H
#define HW_ARM_CALYPSO_SIM_H

#include "qemu/osdep.h"
#include "exec/hwaddr.h"
#include "hw/irq.h"

#define CALYPSO_SIM_REG_CMD      0x00
#define CALYPSO_SIM_REG_STAT     0x02
#define CALYPSO_SIM_REG_CONF1    0x04
#define CALYPSO_SIM_REG_CONF2    0x06
#define CALYPSO_SIM_REG_IT       0x08
#define CALYPSO_SIM_REG_DRX      0x0A
#define CALYPSO_SIM_REG_DTX      0x0C
#define CALYPSO_SIM_REG_MASKIT   0x0E
#define CALYPSO_SIM_REG_IT_CD    0x10

#define CALYPSO_SIM_CMD_CARDRST       (1 << 0)
#define CALYPSO_SIM_CMD_IFRST         (1 << 1)
#define CALYPSO_SIM_CMD_STOP          (1 << 2)
#define CALYPSO_SIM_CMD_START         (1 << 3)
#define CALYPSO_SIM_CMD_MODULE_CLK_EN (1 << 4)

#define CALYPSO_SIM_STAT_NOCARD       (1 << 0)
#define CALYPSO_SIM_STAT_TXPAR        (1 << 1)
#define CALYPSO_SIM_STAT_FIFOFULL     (1 << 2)
#define CALYPSO_SIM_STAT_FIFOEMPTY    (1 << 3)

#define CALYPSO_SIM_IT_NATR           (1 << 0)
#define CALYPSO_SIM_IT_WT             (1 << 1)
#define CALYPSO_SIM_IT_OV             (1 << 2)
#define CALYPSO_SIM_IT_TX             (1 << 3)
#define CALYPSO_SIM_IT_RX             (1 << 4)

#define CALYPSO_SIM_CONF1_SCLKEN      (1 << 3)      /* card clock (TI sim.h) */
#define CALYPSO_SIM_CONF1_BYPASS      (1 << 8)
#define CALYPSO_SIM_CONF1_SVCCLEV     (1 << 9)
#define CALYPSO_SIM_CONF1_SRSTLEV     (1 << 10)

typedef struct CalypsoSim CalypsoSim;

CalypsoSim *calypso_sim_new(qemu_irq sim_irq);

uint16_t calypso_sim_reg_read(CalypsoSim *s, hwaddr off);
void     calypso_sim_reg_write(CalypsoSim *s, hwaddr off, uint16_t val);

#endif

4.99 https://github.com/bbaranoff/qosmo/blob/main/include/hw/arm/calypso/calypso_soc.h

1069 octets, 45 lignes → 45 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */

#ifndef HW_ARM_CALYPSO_SOC_H
#define HW_ARM_CALYPSO_SOC_H

#include "hw/sysbus.h"
#include "qom/object.h"
#include "hw/arm/calypso/calypso_inth.h"
#include "hw/arm/calypso/calypso_timer.h"
#include "hw/arm/calypso/calypso_uart.h"
#include "hw/arm/calypso/calypso_spi.h"

#define TYPE_CALYPSO_SOC "calypso-soc"
OBJECT_DECLARE_SIMPLE_TYPE(CalypsoSoCState, CALYPSO_SOC)

#define CALYPSO_SOC_NUM_IRQS  2

struct CalypsoSoCState {

    SysBusDevice parent_obj;

    MemoryRegion iram;

    CalypsoINTHState  inth;
    CalypsoTimerState timer1;
    CalypsoTimerState timer2;
    CalypsoUARTState  uart_modem;
    CalypsoUARTState  uart_irda;
    CalypsoSPIState   spi;

    void *trx;

    qemu_irq cpu_irq;
    qemu_irq cpu_fiq;

    MemoryRegion iram_alias;
    bool iram_at_zero;
    MemoryRegion cntl_iomem;
    uint16_t extra_conf;
    bool flash_boot;            /* real firmware in flash (nIBOOT, C1xx): never IRAM at 0 */
    uint16_t cntl_rst;          /* CLKM CNTL_RST (0xFFFFFD04) : bit1 = RESET_DSP */

};

#endif

4.100 https://github.com/bbaranoff/qosmo/blob/main/include/hw/arm/calypso/calypso_spi.h

960 octets, 45 lignes → 45 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */

#ifndef HW_SSI_CALYPSO_SPI_H
#define HW_SSI_CALYPSO_SPI_H

#include "hw/sysbus.h"
#include "qom/object.h"

#define TYPE_CALYPSO_SPI "calypso-spi"
OBJECT_DECLARE_SIMPLE_TYPE(CalypsoSPIState, CALYPSO_SPI)

struct CalypsoSPIState {

    SysBusDevice parent_obj;

    MemoryRegion iomem;
    qemu_irq     irq;

    uint16_t set1;
    uint16_t set2;
    uint16_t ctrl;
    uint16_t status;
    uint16_t tx_data;
    uint16_t tx_lsb;
    int      page;              /* page TWL3025 courante (PAGEREG) */
    uint16_t rx_data;

    uint16_t abb_regs[256];
};

#define ABB_VRPCDEV    0x01
#define ABB_VRPCSTS    0x02
#define ABB_VBUCTRL    0x03
#define ABB_VBDR1      0x04
#define ABB_TOGBR1     0x09
#define ABB_TOGBR2     0x0A
#define ABB_AUXLED     0x17
#define ABB_ITSTATREG  0x1B

#define SPI_STATUS_RE        (1 << 1)

#define SPI_STATUS_TX_READY  SPI_STATUS_RE
#define SPI_STATUS_RX_READY  SPI_STATUS_RE

#endif

4.101 https://github.com/bbaranoff/qosmo/blob/main/include/hw/arm/calypso/calypso_timer.h

778 octets, 38 lignes → 38 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */

#ifndef HW_TIMER_CALYPSO_TIMER_H
#define HW_TIMER_CALYPSO_TIMER_H

#include "hw/sysbus.h"
#include "qom/object.h"
#include "qemu/timer.h"

#define TYPE_CALYPSO_TIMER "calypso-timer"
OBJECT_DECLARE_SIMPLE_TYPE(CalypsoTimerState, CALYPSO_TIMER)

struct CalypsoTimerState {

    SysBusDevice parent_obj;

    MemoryRegion iomem;
    QEMUTimer    *timer;
    qemu_irq     irq;

    uint16_t load;
    uint16_t count;
    uint16_t ctrl;
    uint16_t prescaler;
    int64_t  tick_ns;
    int64_t  epoch_ns;
    bool     running;

    bool     lost_latch_active;
    uint16_t lost_latch_count;
    uint32_t lost_read_phase;
};

void calypso_timer_register_lost(DeviceState *d);

void calypso_timer_lost_frame_tick(uint32_t fn);

#endif

4.102 https://github.com/bbaranoff/qosmo/blob/main/include/hw/arm/calypso/calypso_trx.h

2884 octets, 79 lignes → 79 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef CALYPSO_TRX_H
#define CALYPSO_TRX_H

#include "hw/irq.h"
#include "exec/memory.h"

#define CALYPSO_IRQ_WATCHDOG       0
#define CALYPSO_IRQ_TIMER1         1
#define CALYPSO_IRQ_TIMER2         2
#define CALYPSO_IRQ_TSP_RX         3
#define CALYPSO_IRQ_TPU_FRAME      4
#define CALYPSO_IRQ_TPU_PAGE       5
#define CALYPSO_IRQ_SIM            6
#define CALYPSO_IRQ_UART_MODEM     7
#define CALYPSO_IRQ_KEYPAD_GPIO    8
#define CALYPSO_IRQ_RTC_TIMER      9
#define CALYPSO_IRQ_RTC_ALARM      10
#define CALYPSO_IRQ_ULPD_GAUGING   11
#define CALYPSO_IRQ_EXTERNAL       12
#define CALYPSO_IRQ_SPI            13
#define CALYPSO_IRQ_DMA            14
#define CALYPSO_IRQ_API            15
#define CALYPSO_IRQ_SIM_DETECT     16
#define CALYPSO_IRQ_EXTERNAL_FIQ   17
#define CALYPSO_IRQ_UART_IRDA      18
#define CALYPSO_IRQ_ULPD_GSM_TIMER 19
#define CALYPSO_IRQ_GEA            20
#define CALYPSO_NUM_IRQS           32

#define CALYPSO_TPU_BASE      0xFFFF1000
#define CALYPSO_TPU_SIZE      0x0100
#define CALYPSO_TPU_RAM_BASE  0xFFFF9000
#define CALYPSO_TPU_RAM_SIZE  0x0800
#define CALYPSO_TSP_BASE      0xFFFE0800
#define CALYPSO_TSP_SIZE      0x0100
#define CALYPSO_SIM_BASE      0xFFFE0000
#define CALYPSO_SIM_SIZE      0x0100
#define CALYPSO_ULPD_BASE     0xFFFE2800
#define CALYPSO_ULPD_SIZE     0x0100

#define TPU_CTRL              0x0000
#define TPU_INT_CTRL          0x0002
#define TPU_INT_STAT          0x0004
#define TPU_OFFSET            0x000C
#define TPU_SYNCHRO           0x000E
#define TPU_IT_DSP_PG         0x0020

#define TPU_CTRL_EN           (1 << 2)
#define TPU_CTRL_DSP_EN       (1 << 4)
#define TPU_CTRL_IDLE         (1 << 8)

#define ICTRL_MCU_FRAME       (1 << 0)
#define ICTRL_DSP_FRAME       (1 << 2)

#define TSP_RX_REG            0x08

#define ULPD_SETUP_CLK13      0x00
#define ULPD_COUNTER_HI       0x1C
#define ULPD_COUNTER_LO       0x1E
#define ULPD_GAUGING_CTRL     0x24
#define ULPD_GSM_TIMER        0x28

void calypso_trx_init(MemoryRegion *sysmem, qemu_irq *irqs);
/* RESET_DSP line of the CNTL_RST register (calypso_soc.c): with an external
 * DSP, the asserted -> released edge restarts the C54x boot ROM (PONT_RESET). */
void calypso_trx_dsp_reset_line(bool active);
/* L1CTL_RACH_CONF seen by the sercomm tap: the frame the firmware reports to
 * layer 2/3 for the oldest unconfirmed access attempt. This is THE value
 * gsm48_rr stores in cr_hist and later compares with the request reference
 * of the IMMEDIATE ASSIGNMENT. */
void calypso_trx_rach_conf(uint32_t fn);
/* Dedicated channel learnt from the L1CTL stream: kind 0 = SDCCH/4,
 * 1 = SDCCH/8, 0xFF = released. Relayed to the DSP by PONT_DCCH. */
void calypso_trx_dcch(int genre, int ss, int tn);
void calypso_tpu_run_scenario(uint16_t *tpu_ram, uint32_t fn, uint16_t *tpu_regs);
void calypso_tpu_sequencer_tick(uint32_t fn);

#endif

4.103 https://github.com/bbaranoff/qosmo/blob/main/include/hw/arm/calypso/calypso_tsp.h

733 octets, 29 lignes → 27 lignes (1 groupes compactés)

/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef CALYPSO_TSP_H
#define CALYPSO_TSP_H

#include <stdint.h>

#define TPUI_TSP_CTRL1   0x00
#define TPUI_TSP_CTRL2   0x01
#define TPUI_TX_⟨1⟩        0x⟨2⟩  ×4
    ⟨⟩ = (3,02) (2,03) (1,04) (4,05)
#define TPUI_TSP_ACT_L   0x06
#define TPUI_TSP_ACT_U   0x07
#define TPUI_TSP_SET1    0x09
#define TPUI_TSP_SET2    0x0a
#define TPUI_TSP_SET3    0x0b

#define TPUI_CTRL2_RD    (1 << 0)
#define TPUI_CTRL2_WR    (1 << 1)

bool calypso_tsp_owns_addr(uint8_t addr);

void calypso_tsp_move(uint8_t addr, uint8_t data, uint32_t fn);

/* position of the TPU sequencer in the frame, in qbits (calypso_tpu.c) */
int calypso_tpu_qbit(void);

#endif

4.104 https://github.com/bbaranoff/qosmo/blob/main/include/hw/arm/calypso/calypso_uart.h

1637 octets, 73 lignes → 73 lignes

/* SPDX-License-Identifier: GPL-2.0-or-later */

#ifndef HW_CHAR_CALYPSO_UART_H
#define HW_CHAR_CALYPSO_UART_H

#include "hw/sysbus.h"
#include "chardev/char-fe.h"
#include "qom/object.h"

#define TYPE_CALYPSO_UART "calypso-uart"
OBJECT_DECLARE_SIMPLE_TYPE(CalypsoUARTState, CALYPSO_UART)

#define CALYPSO_UART_RX_FIFO_SIZE 8192

typedef struct CalypsoUARTState {
    SysBusDevice parent_obj;

    MemoryRegion iomem;

    CharBackend chr;
    qemu_irq irq;

    char *label;

    uint8_t ier;
    uint8_t iir;
    uint8_t fcr;
    uint8_t lcr;
    uint8_t mcr;
    uint8_t lsr;
    uint8_t msr;
    uint8_t spr;
    uint8_t dll;
    uint8_t dlh;
    uint8_t mdr1;

    uint8_t efr;
    uint8_t xon1;
    uint8_t xon2;
    uint8_t xoff1;
    uint8_t xoff2;
    uint8_t scr;
    uint8_t ssr;
    bool    wakeup_pending;   /* [2026-10-21] IT de reveil RX (SCR bit 4, SSR bit 1), driver UART TI */

    uint8_t rx_fifo[CALYPSO_UART_RX_FIFO_SIZE];
    uint16_t rx_head;
    uint16_t rx_tail;
    uint16_t rx_count;

    bool thr_empty_pending;

    uint8_t tx_empty_reads;

    QEMUTimer *rx_poll_timer;

} CalypsoUARTState;

int calypso_uart_can_receive(void *opaque);
void calypso_uart_receive(void *opaque, const uint8_t *buf, int size);

void calypso_uart_inject_raw(CalypsoUARTState *s, const uint8_t *buf, int size);

void calypso_uart_kick_rx(CalypsoUARTState *s);

void calypso_uart_poll_backend(CalypsoUARTState *s);

void calypso_uart_kick_tx(CalypsoUARTState *s);
void calypso_uart_force_init(CalypsoUARTState *s);
void calypso_uart_romload_passthrough(void);   /* [2026-10-21] firmware en flash : pas de stub romload */


#endif

4.105 https://github.com/bbaranoff/qosmo/blob/main/include/hw/arm/calypso/fw_console.h

109 octets, 6 lignes → 6 lignes

#ifndef HW_ARM_CALYPSO_FW_CONSOLE_H
#define HW_ARM_CALYPSO_FW_CONSOLE_H

void fw_console_init(void);

#endif

4.106 https://github.com/bbaranoff/qosmo/blob/main/include/hw/arm/calypso/sercomm_gate.h

1025 octets, 28 lignes → 28 lignes

/*
 * sercomm_gate.h — Sercomm DLCI router (HDLC demux)
 *
 * Emulates the hardware separation between:
 *   - BSP path (bursts) : DLCI 4 → calypso_trx_rx_burst → c54x BSP
 *   - UART path (L1CTL) : all other DLCIs → re-wrap → FIFO → firmware
 *
 * On real Calypso, bursts come from the ABB via BSP hardware, not UART.
 * In QEMU, the bridge sends them as sercomm DLCI 4 on the PTY, so the
 * gate intercepts them and routes to the BSP emulation layer.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#ifndef SERCOMM_GATE_H
#define SERCOMM_GATE_H

#include "hw/arm/calypso/calypso_uart.h"

/* Feed raw bytes from PTY into the sercomm HDLC parser.
 * Complete frames are routed by DLCI:
 *   4 (bursts) → calypso_trx_rx_burst (BSP emulation)
 *   * (L1CTL, debug, console) → re-wrap → UART FIFO (firmware) */
void sercomm_gate_feed(CalypsoUARTState *s, const uint8_t *buf, int size);

/* Bind UDP CLK listener starting at base_port. */
void sercomm_gate_init(int base_port);

#endif /* SERCOMM_GATE_H */

4.107 https://github.com/bbaranoff/qosmo/blob/main/include/qemu/log.h

3193 octets, 99 lignes → 99 lignes

#ifndef QEMU_LOG_H
#define QEMU_LOG_H

/* A small part of this API is split into its own header */
#include "qemu/log-for-trace.h"

/* 
 * The new API:
 */

/* Returns true if qemu_log() will really write somewhere. */
bool qemu_log_enabled(void);

/* Returns true if qemu_log() will write somewhere other than stderr. */
bool qemu_log_separate(void);

#define CPU_LOG_TB_OUT_ASM (1 << 0)
#define CPU_LOG_TB_IN_ASM  (1 << 1)
#define CPU_LOG_TB_OP      (1 << 2)
#define CPU_LOG_TB_OP_OPT  (1 << 3)
#define CPU_LOG_INT        (1 << 4)
#define CPU_LOG_EXEC       (1 << 5)
#define CPU_LOG_PCALL      (1 << 6)
#define CPU_LOG_TB_CPU     (1 << 8)
#define CPU_LOG_RESET      (1 << 9)
#define LOG_UNIMP          (1 << 10)
#define LOG_GUEST_ERROR    (1 << 11)
#define CPU_LOG_MMU        (1 << 12)
#define CPU_LOG_TB_NOCHAIN (1 << 13)
#define CPU_LOG_PAGE       (1 << 14)
/* LOG_TRACE (1 << 15) is defined in log-for-trace.h */
#define CPU_LOG_TB_OP_IND  (1 << 16)
#define CPU_LOG_TB_FPU     (1 << 17)
#define CPU_LOG_PLUGIN     (1 << 18)
/* LOG_STRACE is used for user-mode strace logging. */
#define LOG_STRACE         (1 << 19)
#define LOG_PER_THREAD     (1 << 20)
#define CPU_LOG_TB_VPU     (1 << 21)
#define LOG_TB_OP_PLUGIN   (1 << 22)

/* Lock/unlock output. */

FILE *qemu_log_trylock(void) G_GNUC_WARN_UNUSED_RESULT;
void qemu_log_unlock(FILE *fd);
void qemu_log_flush(void);

/* Logging functions: */

/* log only if a bit is set on the current loglevel mask:
 * @mask: bit to check in the mask
 * @fmt: printf-style format string
 * @args: optional arguments for format string
 */
#define qemu_log_mask(MASK, FMT, ...)                   \
    do {                                                \
        if (unlikely(qemu_loglevel_mask(MASK))) {       \
            qemu_log(FMT, ## __VA_ARGS__);              \
        }                                               \
    } while (0)

/* log only if a bit is set on the current loglevel mask
 * and we are in the address range we care about:
 * @mask: bit to check in the mask
 * @addr: address to check in dfilter
 * @fmt: printf-style format string
 * @args: optional arguments for format string
 */
#define qemu_log_mask_and_addr(MASK, ADDR, FMT, ...)    \
    do {                                                \
        if (unlikely(qemu_loglevel_mask(MASK)) &&       \
                     qemu_log_in_addr_range(ADDR)) {    \
            qemu_log(FMT, ## __VA_ARGS__);              \
        }                                               \
    } while (0)

/* Maintenance: */

/* define log items */
typedef struct QEMULogItem {
    int mask;
    const char *name;
    const char *help;
} QEMULogItem;

extern const QEMULogItem qemu_log_items[];

bool qemu_set_log(int log_flags, Error **errp);
bool qemu_set_log_filename(const char *filename, Error **errp);
bool qemu_set_log_filename_flags(const char *name, int flags, Error **errp);
void qemu_set_dfilter_ranges(const char *ranges, Error **errp);
bool qemu_log_in_addr_range(uint64_t addr);
int qemu_str_to_log_mask(const char *str);

/* Print a usage message listing all the valid logging categories
 * to the specified FILE*.
 */
void qemu_print_log_usage(FILE *f);

#endif

4.108 https://github.com/bbaranoff/qosmo/blob/main/target/arm/tcg/hflags.c

16223 octets, 503 lignes → 503 lignes

/*
 * ARM hflags
 *
 * This code is licensed under the GNU GPL v2 or later.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#include "qemu/osdep.h"
#include "cpu.h"
#include "internals.h"
#include "cpu-features.h"
#include "exec/helper-proto.h"
#include "cpregs.h"

static inline bool fgt_svc(CPUARMState *env, int el)
{
    /*
     * Assuming fine-grained-traps are active, return true if we
     * should be trapping on SVC instructions. Only AArch64 can
     * trap on an SVC at EL1, but we don't need to special-case this
     * because if this is AArch32 EL1 then arm_fgt_active() is false.
     * We also know el is 0 or 1.
     */
    return el == 0 ?
        FIELD_EX64(env->cp15.fgt_exec[FGTREG_HFGITR], HFGITR_EL2, SVC_EL0) :
        FIELD_EX64(env->cp15.fgt_exec[FGTREG_HFGITR], HFGITR_EL2, SVC_EL1);
}

/* Return true if memory alignment should be enforced. */
static bool aprofile_require_alignment(CPUARMState *env, int el, uint64_t sctlr)
{
#ifdef CONFIG_USER_ONLY
    return false;
#else
    /* Check the alignment enable bit. */
    if (sctlr & SCTLR_A) {
        return true;
    }

    /*
     * With PMSA, when the MPU is disabled, all memory types in the
     * default map are Normal, so don't need aligment enforcing.
     */
    if (arm_feature(env, ARM_FEATURE_PMSA)) {
        return false;
    }

    /*
     * [2026-10-21 qosmo] ARMv4/v4T cores (ARM7TDMI of the TI Calypso, ti925t
     * here) never raise alignment faults with SCTLR.A clear: a halfword store
     * to an odd address silently goes to the aligned address. TI's TCS211
     * flash driver relies on it (0x55 -> 0x5555 of the AMD unlock sequence).
     */
    if (!arm_feature(env, ARM_FEATURE_V5)) {
        return false;
    }

    /*
     * With VMSA, if translation is disabled, then the default memory type
     * is Device(-nGnRnE) instead of Normal, which requires that alignment
     * be enforced.  Since this affects all ram, it is most efficient
     * to handle this during translation.
     */
    if (sctlr & SCTLR_M) {
        /* Translation enabled: memory type in PTE via MAIR_ELx. */
        return false;
    }
    if (el < 2 && (arm_hcr_el2_eff(env) & (HCR_DC | HCR_VM))) {
        /* Stage 2 translation enabled: memory type in PTE. */
        return false;
    }
    return true;
#endif
}

static CPUARMTBFlags rebuild_hflags_common(CPUARMState *env, int fp_el,
                                           ARMMMUIdx mmu_idx,
                                           CPUARMTBFlags flags)
{
    DP_TBFLAG_ANY(flags, FPEXC_EL, fp_el);
    DP_TBFLAG_ANY(flags, MMUIDX, arm_to_core_mmu_idx(mmu_idx));

    if (arm_singlestep_active(env)) {
        DP_TBFLAG_ANY(flags, SS_ACTIVE, 1);
    }

    return flags;
}

static CPUARMTBFlags rebuild_hflags_common_32(CPUARMState *env, int fp_el,
                                              ARMMMUIdx mmu_idx,
                                              CPUARMTBFlags flags)
{
    bool sctlr_b = arm_sctlr_b(env);

    if (sctlr_b) {
        DP_TBFLAG_A32(flags, SCTLR__B, 1);
    }
    if (arm_cpu_data_is_big_endian_a32(env, sctlr_b)) {
        DP_TBFLAG_ANY(flags, BE_DATA, 1);
    }
    DP_TBFLAG_A32(flags, NS, !access_secure_reg(env));

    return rebuild_hflags_common(env, fp_el, mmu_idx, flags);
}

static CPUARMTBFlags rebuild_hflags_m32(CPUARMState *env, int fp_el,
                                        ARMMMUIdx mmu_idx)
{
    CPUARMTBFlags flags = {};
    uint32_t ccr = env->v7m.ccr[env->v7m.secure];

    /* Without HaveMainExt, CCR.UNALIGN_TRP is RES1. */
    if (ccr & R_V7M_CCR_UNALIGN_TRP_MASK) {
        DP_TBFLAG_ANY(flags, ALIGN_MEM, 1);
    }

    if (arm_v7m_is_handler_mode(env)) {
        DP_TBFLAG_M32(flags, HANDLER, 1);
    }

    /*
     * v8M always applies stack limit checks unless CCR.STKOFHFNMIGN
     * is suppressing them because the requested execution priority
     * is less than 0.
     */
    if (arm_feature(env, ARM_FEATURE_V8) &&
        !((mmu_idx & ARM_MMU_IDX_M_NEGPRI) &&
          (ccr & R_V7M_CCR_STKOFHFNMIGN_MASK))) {
        DP_TBFLAG_M32(flags, STACKCHECK, 1);
    }

    if (arm_feature(env, ARM_FEATURE_M_SECURITY) && env->v7m.secure) {
        DP_TBFLAG_M32(flags, SECURE, 1);
    }

    return rebuild_hflags_common_32(env, fp_el, mmu_idx, flags);
}

/* This corresponds to the ARM pseudocode function IsFullA64Enabled(). */
static bool sme_fa64(CPUARMState *env, int el)
{
    if (!cpu_isar_feature(aa64_sme_fa64, env_archcpu(env))) {
        return false;
    }

    if (el <= 1 && !el_is_in_host(env, el)) {
        if (!FIELD_EX64(env->vfp.smcr_el[1], SMCR, FA64)) {
            return false;
        }
    }
    if (el <= 2 && arm_is_el2_enabled(env)) {
        if (!FIELD_EX64(env->vfp.smcr_el[2], SMCR, FA64)) {
            return false;
        }
    }
    if (arm_feature(env, ARM_FEATURE_EL3)) {
        if (!FIELD_EX64(env->vfp.smcr_el[3], SMCR, FA64)) {
            return false;
        }
    }

    return true;
}

static CPUARMTBFlags rebuild_hflags_a32(CPUARMState *env, int fp_el,
                                        ARMMMUIdx mmu_idx)
{
    CPUARMTBFlags flags = {};
    int el = arm_current_el(env);
    uint64_t sctlr = arm_sctlr(env, el);

    if (aprofile_require_alignment(env, el, sctlr)) {
        DP_TBFLAG_ANY(flags, ALIGN_MEM, 1);
    }

    if (arm_el_is_aa64(env, 1)) {
        DP_TBFLAG_A32(flags, VFPEN, 1);
    }

    if (el < 2 && env->cp15.hstr_el2 && arm_is_el2_enabled(env) &&
        (arm_hcr_el2_eff(env) & (HCR_E2H | HCR_TGE)) != (HCR_E2H | HCR_TGE)) {
        DP_TBFLAG_A32(flags, HSTR_ACTIVE, 1);
    }

    if (arm_fgt_active(env, el)) {
        DP_TBFLAG_ANY(flags, FGT_ACTIVE, 1);
        if (fgt_svc(env, el)) {
            DP_TBFLAG_ANY(flags, FGT_SVC, 1);
        }
    }

    if (env->uncached_cpsr & CPSR_IL) {
        DP_TBFLAG_ANY(flags, PSTATE__IL, 1);
    }

    /*
     * The SME exception we are testing for is raised via
     * AArch64.CheckFPAdvSIMDEnabled(), as called from
     * AArch32.CheckAdvSIMDOrFPEnabled().
     */
    if (el == 0
        && FIELD_EX64(env->svcr, SVCR, SM)
        && (!arm_is_el2_enabled(env)
            || (arm_el_is_aa64(env, 2) && !(env->cp15.hcr_el2 & HCR_TGE)))
        && arm_el_is_aa64(env, 1)
        && !sme_fa64(env, el)) {
        DP_TBFLAG_A32(flags, SME_TRAP_NONSTREAMING, 1);
    }

    return rebuild_hflags_common_32(env, fp_el, mmu_idx, flags);
}

static CPUARMTBFlags rebuild_hflags_a64(CPUARMState *env, int el, int fp_el,
                                        ARMMMUIdx mmu_idx)
{
    CPUARMTBFlags flags = {};
    ARMMMUIdx stage1 = stage_1_mmu_idx(mmu_idx);
    uint64_t tcr = regime_tcr(env, mmu_idx);
    uint64_t hcr = arm_hcr_el2_eff(env);
    uint64_t sctlr;
    int tbii, tbid;

    DP_TBFLAG_ANY(flags, AARCH64_STATE, 1);

    /* Get control bits for tagged addresses.  */
    tbid = aa64_va_parameter_tbi(tcr, mmu_idx);
    tbii = tbid & ~aa64_va_parameter_tbid(tcr, mmu_idx);

    DP_TBFLAG_A64(flags, TBII, tbii);
    DP_TBFLAG_A64(flags, TBID, tbid);

    if (cpu_isar_feature(aa64_sve, env_archcpu(env))) {
        int sve_el = sve_exception_el(env, el);

        /*
         * If either FP or SVE are disabled, translator does not need len.
         * If SVE EL > FP EL, FP exception has precedence, and translator
         * does not need SVE EL.  Save potential re-translations by forcing
         * the unneeded data to zero.
         */
        if (fp_el != 0) {
            if (sve_el > fp_el) {
                sve_el = 0;
            }
        } else if (sve_el == 0) {
            DP_TBFLAG_A64(flags, VL, sve_vqm1_for_el(env, el));
        }
        DP_TBFLAG_A64(flags, SVEEXC_EL, sve_el);
    }
    if (cpu_isar_feature(aa64_sme, env_archcpu(env))) {
        int sme_el = sme_exception_el(env, el);
        bool sm = FIELD_EX64(env->svcr, SVCR, SM);

        DP_TBFLAG_A64(flags, SMEEXC_EL, sme_el);
        if (sme_el == 0) {
            /* Similarly, do not compute SVL if SME is disabled. */
            int svl = sve_vqm1_for_el_sm(env, el, true);
            DP_TBFLAG_A64(flags, SVL, svl);
            if (sm) {
                /* If SVE is disabled, we will not have set VL above. */
                DP_TBFLAG_A64(flags, VL, svl);
            }
        }
        if (sm) {
            DP_TBFLAG_A64(flags, PSTATE_SM, 1);
            DP_TBFLAG_A64(flags, SME_TRAP_NONSTREAMING, !sme_fa64(env, el));
        }
        DP_TBFLAG_A64(flags, PSTATE_ZA, FIELD_EX64(env->svcr, SVCR, ZA));
    }

    sctlr = regime_sctlr(env, stage1);

    if (aprofile_require_alignment(env, el, sctlr)) {
        DP_TBFLAG_ANY(flags, ALIGN_MEM, 1);
    }

    if (arm_cpu_data_is_big_endian_a64(el, sctlr)) {
        DP_TBFLAG_ANY(flags, BE_DATA, 1);
    }

    if (cpu_isar_feature(aa64_pauth, env_archcpu(env))) {
        /*
         * In order to save space in flags, we record only whether
         * pauth is "inactive", meaning all insns are implemented as
         * a nop, or "active" when some action must be performed.
         * The decision of which action to take is left to a helper.
         */
        if (sctlr & (SCTLR_EnIA | SCTLR_EnIB | SCTLR_EnDA | SCTLR_EnDB)) {
            DP_TBFLAG_A64(flags, PAUTH_ACTIVE, 1);
        }
    }

    if (cpu_isar_feature(aa64_bti, env_archcpu(env))) {
        /* Note that SCTLR_EL[23].BT == SCTLR_BT1.  */
        if (sctlr & (el == 0 ? SCTLR_BT0 : SCTLR_BT1)) {
            DP_TBFLAG_A64(flags, BT, 1);
        }
    }

    if (cpu_isar_feature(aa64_lse2, env_archcpu(env))) {
        if (sctlr & SCTLR_nAA) {
            DP_TBFLAG_A64(flags, NAA, 1);
        }
    }

    /* Compute the condition for using AccType_UNPRIV for LDTR et al. */
    if (!(env->pstate & PSTATE_UAO)) {
        switch (mmu_idx) {
        case ARMMMUIdx_E10_1:
        case ARMMMUIdx_E10_1_PAN:
            /* FEAT_NV: NV,NV1 == 1,1 means we don't do UNPRIV accesses */
            if ((hcr & (HCR_NV | HCR_NV1)) != (HCR_NV | HCR_NV1)) {
                DP_TBFLAG_A64(flags, UNPRIV, 1);
            }
            break;
        case ARMMMUIdx_E20_2:
        case ARMMMUIdx_E20_2_PAN:
            /*
             * Note that EL20_2 is gated by HCR_EL2.E2H == 1, but EL20_0 is
             * gated by HCR_EL2.<E2H,TGE> == '11', and so is LDTR.
             */
            if (env->cp15.hcr_el2 & HCR_TGE) {
                DP_TBFLAG_A64(flags, UNPRIV, 1);
            }
            break;
        default:
            break;
        }
    }

    if (env->pstate & PSTATE_IL) {
        DP_TBFLAG_ANY(flags, PSTATE__IL, 1);
    }

    if (arm_fgt_active(env, el)) {
        DP_TBFLAG_ANY(flags, FGT_ACTIVE, 1);
        if (FIELD_EX64(env->cp15.fgt_exec[FGTREG_HFGITR], HFGITR_EL2, ERET)) {
            DP_TBFLAG_A64(flags, TRAP_ERET, 1);
        }
        if (fgt_svc(env, el)) {
            DP_TBFLAG_ANY(flags, FGT_SVC, 1);
        }
    }

    /*
     * ERET can also be trapped for FEAT_NV. arm_hcr_el2_eff() takes care
     * of "is EL2 enabled" and the NV bit can only be set if FEAT_NV is present.
     */
    if (el == 1 && (hcr & HCR_NV)) {
        DP_TBFLAG_A64(flags, TRAP_ERET, 1);
        DP_TBFLAG_A64(flags, NV, 1);
        if (hcr & HCR_NV1) {
            DP_TBFLAG_A64(flags, NV1, 1);
        }
        if (hcr & HCR_NV2) {
            DP_TBFLAG_A64(flags, NV2, 1);
            if (hcr & HCR_E2H) {
                DP_TBFLAG_A64(flags, NV2_MEM_E20, 1);
            }
            if (env->cp15.sctlr_el[2] & SCTLR_EE) {
                DP_TBFLAG_A64(flags, NV2_MEM_BE, 1);
            }
        }
    }

    if (cpu_isar_feature(aa64_mte, env_archcpu(env))) {
        /*
         * Set MTE_ACTIVE if any access may be Checked, and leave clear
         * if all accesses must be Unchecked:
         * 1) If no TBI, then there are no tags in the address to check,
         * 2) If Tag Check Override, then all accesses are Unchecked,
         * 3) If Tag Check Fail == 0, then Checked access have no effect,
         * 4) If no Allocation Tag Access, then all accesses are Unchecked.
         */
        if (allocation_tag_access_enabled(env, el, sctlr)) {
            DP_TBFLAG_A64(flags, ATA, 1);
            if (tbid
                && !(env->pstate & PSTATE_TCO)
                && (sctlr & (el == 0 ? SCTLR_TCF0 : SCTLR_TCF))) {
                DP_TBFLAG_A64(flags, MTE_ACTIVE, 1);
                if (!EX_TBFLAG_A64(flags, UNPRIV)) {
                    /*
                     * In non-unpriv contexts (eg EL0), unpriv load/stores
                     * act like normal ones; duplicate the MTE info to
                     * avoid translate-a64.c having to check UNPRIV to see
                     * whether it is OK to index into MTE_ACTIVE[].
                     */
                    DP_TBFLAG_A64(flags, MTE0_ACTIVE, 1);
                }
            }
        }
        /* And again for unprivileged accesses, if required.  */
        if (EX_TBFLAG_A64(flags, UNPRIV)
            && tbid
            && !(env->pstate & PSTATE_TCO)
            && (sctlr & SCTLR_TCF0)
            && allocation_tag_access_enabled(env, 0, sctlr)) {
            DP_TBFLAG_A64(flags, MTE0_ACTIVE, 1);
        }
        /*
         * For unpriv tag-setting accesses we also need ATA0. Again, in
         * contexts where unpriv and normal insns are the same we
         * duplicate the ATA bit to save effort for translate-a64.c.
         */
        if (EX_TBFLAG_A64(flags, UNPRIV)) {
            if (allocation_tag_access_enabled(env, 0, sctlr)) {
                DP_TBFLAG_A64(flags, ATA0, 1);
            }
        } else {
            DP_TBFLAG_A64(flags, ATA0, EX_TBFLAG_A64(flags, ATA));
        }
        /* Cache TCMA as well as TBI. */
        DP_TBFLAG_A64(flags, TCMA, aa64_va_parameter_tcma(tcr, mmu_idx));
    }

    return rebuild_hflags_common(env, fp_el, mmu_idx, flags);
}

static CPUARMTBFlags rebuild_hflags_internal(CPUARMState *env)
{
    int el = arm_current_el(env);
    int fp_el = fp_exception_el(env, el);
    ARMMMUIdx mmu_idx = arm_mmu_idx_el(env, el);

    if (is_a64(env)) {
        return rebuild_hflags_a64(env, el, fp_el, mmu_idx);
    } else if (arm_feature(env, ARM_FEATURE_M)) {
        return rebuild_hflags_m32(env, fp_el, mmu_idx);
    } else {
        return rebuild_hflags_a32(env, fp_el, mmu_idx);
    }
}

void arm_rebuild_hflags(CPUARMState *env)
{
    env->hflags = rebuild_hflags_internal(env);
}

/*
 * If we have triggered a EL state change we can't rely on the
 * translator having passed it to us, we need to recompute.
 */
void HELPER(rebuild_hflags_m32_newel)(CPUARMState *env)
{
    int el = arm_current_el(env);
    int fp_el = fp_exception_el(env, el);
    ARMMMUIdx mmu_idx = arm_mmu_idx_el(env, el);

    env->hflags = rebuild_hflags_m32(env, fp_el, mmu_idx);
}

void HELPER(rebuild_hflags_m32)(CPUARMState *env, int el)
{
    int fp_el = fp_exception_el(env, el);
    ARMMMUIdx mmu_idx = arm_mmu_idx_el(env, el);

    env->hflags = rebuild_hflags_m32(env, fp_el, mmu_idx);
}

/*
 * If we have triggered a EL state change we can't rely on the
 * translator having passed it to us, we need to recompute.
 */
void HELPER(rebuild_hflags_a32_newel)(CPUARMState *env)
{
    int el = arm_current_el(env);
    int fp_el = fp_exception_el(env, el);
    ARMMMUIdx mmu_idx = arm_mmu_idx_el(env, el);
    env->hflags = rebuild_hflags_a32(env, fp_el, mmu_idx);
}

void HELPER(rebuild_hflags_a32)(CPUARMState *env, int el)
{
    int fp_el = fp_exception_el(env, el);
    ARMMMUIdx mmu_idx = arm_mmu_idx_el(env, el);

    env->hflags = rebuild_hflags_a32(env, fp_el, mmu_idx);
}

void HELPER(rebuild_hflags_a64)(CPUARMState *env, int el)
{
    int fp_el = fp_exception_el(env, el);
    ARMMMUIdx mmu_idx = arm_mmu_idx_el(env, el);

    env->hflags = rebuild_hflags_a64(env, el, fp_el, mmu_idx);
}

void assert_hflags_rebuild_correctly(CPUARMState *env)
{
#ifdef CONFIG_DEBUG_TCG
    CPUARMTBFlags c = env->hflags;
    CPUARMTBFlags r = rebuild_hflags_internal(env);

    if (unlikely(c.flags != r.flags || c.flags2 != r.flags2)) {
        fprintf(stderr, "TCG hflags mismatch "
                        "(current:(0x%08x,0x" TARGET_FMT_lx ")"
                        " rebuilt:(0x%08x,0x" TARGET_FMT_lx ")\n",
                c.flags, c.flags2, r.flags, r.flags2);
        abort();
    }
#endif
}

4.109 https://github.com/bbaranoff/qosmo/blob/main/target/arm/tcg/translate.c

243185 octets, 8170 lignes → 8166 lignes (2 groupes compactés)

/*
 *  ARM translation
 *
 *  Copyright (c) 2003 Fabrice Bellard
 *  Copyright (c) 2005-2007 CodeSourcery
 *  Copyright (c) 2007 OpenedHand, Ltd.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see <http://www.gnu.org/licenses/>.
 */
#include "qemu/osdep.h"

#include "translate.h"
#include "translate-a32.h"
#include "qemu/log.h"
#include "arm_ldst.h"
#include "semihosting/semihost.h"
#include "cpregs.h"
#include "exec/helper-proto.h"

#define HELPER_H "helper.h"
#include "exec/helper-info.c.inc"
#undef  HELPER_H

#define ENABLE_ARCH_4T    arm_dc_feature(s, ARM_FEATURE_V4T)
#define ENABLE_ARCH_5     arm_dc_feature(s, ARM_FEATURE_V5)
/* currently all emulated v5 cores are also v5TE, so don't bother */
#define ENABLE_ARCH_5TE   arm_dc_feature(s, ARM_FEATURE_V5)
#define ENABLE_ARCH_5J    dc_isar_feature(aa32_jazelle, s)
#define ENABLE_ARCH_6     arm_dc_feature(s, ARM_FEATURE_V6)
#define ENABLE_ARCH_6K    arm_dc_feature(s, ARM_FEATURE_V6K)
#define ENABLE_ARCH_6T2   arm_dc_feature(s, ARM_FEATURE_THUMB2)
#define ENABLE_ARCH_7     arm_dc_feature(s, ARM_FEATURE_V7)
#define ENABLE_ARCH_8     arm_dc_feature(s, ARM_FEATURE_V8)

/* These are TCG temporaries used only by the legacy iwMMXt decoder */
static TCGv_i64 cpu_V0, cpu_V1, cpu_M0;
/* These are TCG globals which alias CPUARMState fields */
static TCGv_i32 cpu_R[16];
TCGv_i32 cpu_CF, cpu_NF, cpu_VF, cpu_ZF;
TCGv_i64 cpu_exclusive_addr;
TCGv_i64 cpu_exclusive_val;

static const char * const regnames[] =
    { "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7",
      "r8", "r9", "r10", "r11", "r12", "r13", "r14", "pc" };


/* initialize TCG globals.  */
void arm_translate_init(void)
{
    int i;

    for (i = 0; i < 16; i++) {
        cpu_R[i] = tcg_global_mem_new_i32(tcg_env,
                                          offsetof(CPUARMState, regs[i]),
                                          regnames[i]);
    }
    cpu_CF = tcg_global_mem_new_i32(tcg_env, offsetof(CPUARMState, CF), "CF");
    cpu_NF = tcg_global_mem_new_i32(tcg_env, offsetof(CPUARMState, NF), "NF");
    cpu_VF = tcg_global_mem_new_i32(tcg_env, offsetof(CPUARMState, VF), "VF");
    cpu_ZF = tcg_global_mem_new_i32(tcg_env, offsetof(CPUARMState, ZF), "ZF");

    cpu_exclusive_addr = tcg_global_mem_new_i64(tcg_env,
        offsetof(CPUARMState, exclusive_addr), "exclusive_addr");
    cpu_exclusive_val = tcg_global_mem_new_i64(tcg_env,
        offsetof(CPUARMState, exclusive_val), "exclusive_val");

    a64_translate_init();
}

uint64_t asimd_imm_const(uint32_t imm, int cmode, int op)
{
    /* Expand the encoded constant as per AdvSIMDExpandImm pseudocode */
    switch (cmode) {
    case 0: case 1:
        /* no-op */
        break;
    case 2: case 3:
        imm <<= 8;
        break;
    case 4: case 5:
        imm <<= 16;
        break;
    case 6: case 7:
        imm <<= 24;
        break;
    case 8: case 9:
        imm |= imm << 16;
        break;
    case 10: case 11:
        imm = (imm << 8) | (imm << 24);
        break;
    case 12:
        imm = (imm << 8) | 0xff;
        break;
    case 13:
        imm = (imm << 16) | 0xffff;
        break;
    case 14:
        if (op) {
            /*
             * This and cmode == 15 op == 1 are the only cases where
             * the top and bottom 32 bits of the encoded constant differ.
             */
            uint64_t imm64 = 0;
            int n;

            for (n = 0; n < 8; n++) {
                if (imm & (1 << n)) {
                    imm64 |= (0xffULL << (n * 8));
                }
            }
            return imm64;
        }
        imm |= (imm << 8) | (imm << 16) | (imm << 24);
        break;
    case 15:
        if (op) {
            /* Reserved encoding for AArch32; valid for AArch64 */
            uint64_t imm64 = (uint64_t)(imm & 0x3f) << 48;
            if (imm & 0x80) {
                imm64 |= 0x8000000000000000ULL;
            }
            if (imm & 0x40) {
                imm64 |= 0x3fc0000000000000ULL;
            } else {
                imm64 |= 0x4000000000000000ULL;
            }
            return imm64;
        }
        imm = ((imm & 0x80) << 24) | ((imm & 0x3f) << 19)
            | ((imm & 0x40) ? (0x1f << 25) : (1 << 30));
        break;
    }
    if (op) {
        imm = ~imm;
    }
    return dup_const(MO_32, imm);
}

/* Generate a label used for skipping this instruction */
void arm_gen_condlabel(DisasContext *s)
{
    if (!s->condjmp) {
        s->condlabel = gen_disas_label(s);
        s->condjmp = 1;
    }
}

/* Flags for the disas_set_da_iss info argument:
 * lower bits hold the Rt register number, higher bits are flags.
 */
typedef enum ISSInfo {
    ISSNone = 0,
    ISSRegMask = 0x1f,
    ISSInvalid = (1 << 5),
    ISSIsAcqRel = (1 << 6),
    ISSIsWrite = (1 << 7),
    ISSIs16Bit = (1 << 8),
} ISSInfo;

/*
 * Store var into env + offset to a member with size bytes.
 * Free var after use.
 */
void store_cpu_offset(TCGv_i32 var, int offset, int size)
{
    switch (size) {
    case 1:
        tcg_gen_st8_i32(var, tcg_env, offset);
        break;
    case 4:
        tcg_gen_st_i32(var, tcg_env, offset);
        break;
    default:
        g_assert_not_reached();
    }
}

/* Save the syndrome information for a Data Abort */
static void disas_set_da_iss(DisasContext *s, MemOp memop, ISSInfo issinfo)
{
    uint32_t syn;
    int sas = memop & MO_SIZE;
    bool sse = memop & MO_SIGN;
    bool is_acqrel = issinfo & ISSIsAcqRel;
    bool is_write = issinfo & ISSIsWrite;
    bool is_16bit = issinfo & ISSIs16Bit;
    int srt = issinfo & ISSRegMask;

    if (issinfo & ISSInvalid) {
        /* Some callsites want to conditionally provide ISS info,
         * eg "only if this was not a writeback"
         */
        return;
    }

    if (srt == 15) {
        /* For AArch32, insns where the src/dest is R15 never generate
         * ISS information. Catching that here saves checking at all
         * the call sites.
         */
        return;
    }

    syn = syn_data_abort_with_iss(0, sas, sse, srt, 0, is_acqrel,
                                  0, 0, 0, is_write, 0, is_16bit);
    disas_set_insn_syndrome(s, syn);
}

static inline int get_a32_user_mem_index(DisasContext *s)
{
    /* Return the core mmu_idx to use for A32/T32 "unprivileged load/store"
     * insns:
     *  if PL2, UNPREDICTABLE (we choose to implement as if PL0)
     *  otherwise, access as if at PL0.
     */
    switch (s->mmu_idx) {
    case ARMMMUIdx_E3:
    case ARMMMUIdx_E30_0:
    case ARMMMUIdx_E30_3_PAN:
        return arm_to_core_mmu_idx(ARMMMUIdx_E30_0);
    case ARMMMUIdx_E2:        /* this one is UNPREDICTABLE */
    case ARMMMUIdx_E10_0:
    case ARMMMUIdx_E10_1:
    case ARMMMUIdx_E10_1_PAN:
        return arm_to_core_mmu_idx(ARMMMUIdx_E10_0);
    case ARMMMUIdx_MUser:
    case ARMMMUIdx_MPriv:
        return arm_to_core_mmu_idx(ARMMMUIdx_MUser);
    case ARMMMUIdx_MUserNegPri:
    case ARMMMUIdx_MPrivNegPri:
        return arm_to_core_mmu_idx(ARMMMUIdx_MUserNegPri);
    case ARMMMUIdx_MSUser:
    case ARMMMUIdx_MSPriv:
        return arm_to_core_mmu_idx(ARMMMUIdx_MSUser);
    case ARMMMUIdx_MSUserNegPri:
    case ARMMMUIdx_MSPrivNegPri:
        return arm_to_core_mmu_idx(ARMMMUIdx_MSUserNegPri);
    default:
        g_assert_not_reached();
    }
}

/* The pc_curr difference for an architectural jump. */
static target_long jmp_diff(DisasContext *s, target_long diff)
{
    return diff + (s->thumb ? 4 : 8);
}

static void gen_pc_plus_diff(DisasContext *s, TCGv_i32 var, target_long diff)
{
    assert(s->pc_save != -1);
    if (tb_cflags(s->base.tb) & CF_PCREL) {
        tcg_gen_addi_i32(var, cpu_R[15], (s->pc_curr - s->pc_save) + diff);
    } else {
        tcg_gen_movi_i32(var, s->pc_curr + diff);
    }
}

/* Set a variable to the value of a CPU register.  */
void load_reg_var(DisasContext *s, TCGv_i32 var, int reg)
{
    if (reg == 15) {
        gen_pc_plus_diff(s, var, jmp_diff(s, 0));
    } else {
        tcg_gen_mov_i32(var, cpu_R[reg]);
    }
}

/*
 * Create a new temp, REG + OFS, except PC is ALIGN(PC, 4).
 * This is used for load/store for which use of PC implies (literal),
 * or ADD that implies ADR.
 */
TCGv_i32 add_reg_for_lit(DisasContext *s, int reg, int ofs)
{
    TCGv_i32 tmp = tcg_temp_new_i32();

    if (reg == 15) {
        /*
         * This address is computed from an aligned PC:
         * subtract off the low bits.
         */
        gen_pc_plus_diff(s, tmp, jmp_diff(s, ofs - (s->pc_curr & 3)));
    } else {
        tcg_gen_addi_i32(tmp, cpu_R[reg], ofs);
    }
    return tmp;
}

/* Set a CPU register.  The source must be a temporary and will be
   marked as dead.  */
void store_reg(DisasContext *s, int reg, TCGv_i32 var)
{
    if (reg == 15) {
        /* In Thumb mode, we must ignore bit 0.
         * In ARM mode, for ARMv4 and ARMv5, it is UNPREDICTABLE if bits [1:0]
         * are not 0b00, but for ARMv6 and above, we must ignore bits [1:0].
         * We choose to ignore [1:0] in ARM mode for all architecture versions.
         */
        tcg_gen_andi_i32(var, var, s->thumb ? ~1 : ~3);
        s->base.is_jmp = DISAS_JUMP;
        s->pc_save = -1;
    } else if (reg == 13 && arm_dc_feature(s, ARM_FEATURE_M)) {
        /* For M-profile SP bits [1:0] are always zero */
        tcg_gen_andi_i32(var, var, ~3);
    }
    tcg_gen_mov_i32(cpu_R[reg], var);
}

/*
 * Variant of store_reg which applies v8M stack-limit checks before updating
 * SP. If the check fails this will result in an exception being taken.
 * We disable the stack checks for CONFIG_USER_ONLY because we have
 * no idea what the stack limits should be in that case.
 * If stack checking is not being done this just acts like store_reg().
 */
static void store_sp_checked(DisasContext *s, TCGv_i32 var)
{
#ifndef CONFIG_USER_ONLY
    if (s->v8m_stackcheck) {
        gen_helper_v8m_stackcheck(tcg_env, var);
    }
#endif
    store_reg(s, 13, var);
}

/* Value extensions.  */
#define gen_uxtb(var) tcg_gen_ext8u_i32(var, var)
#define gen_uxth(var) tcg_gen_ext16u_i32(var, var)
#define gen_sxtb(var) tcg_gen_ext8s_i32(var, var)
#define gen_sxth(var) tcg_gen_ext16s_i32(var, var)

#define gen_sxtb16(var) gen_helper_sxtb16(var, var)
#define gen_uxtb16(var) gen_helper_uxtb16(var, var)

void gen_set_cpsr(TCGv_i32 var, uint32_t mask)
{
    gen_helper_cpsr_write(tcg_env, var, tcg_constant_i32(mask));
}

static void gen_rebuild_hflags(DisasContext *s, bool new_el)
{
    bool m_profile = arm_dc_feature(s, ARM_FEATURE_M);

    if (new_el) {
        if (m_profile) {
            gen_helper_rebuild_hflags_m32_newel(tcg_env);
        } else {
            gen_helper_rebuild_hflags_a32_newel(tcg_env);
        }
    } else {
        TCGv_i32 tcg_el = tcg_constant_i32(s->current_el);
        if (m_profile) {
            gen_helper_rebuild_hflags_m32(tcg_env, tcg_el);
        } else {
            gen_helper_rebuild_hflags_a32(tcg_env, tcg_el);
        }
    }
}

static void gen_exception_internal(int excp)
{
    assert(excp_is_internal(excp));
    gen_helper_exception_internal(tcg_env, tcg_constant_i32(excp));
}

static void gen_singlestep_exception(DisasContext *s)
{
    /* We just completed step of an insn. Move from Active-not-pending
     * to Active-pending, and then also take the swstep exception.
     * This corresponds to making the (IMPDEF) choice to prioritize
     * swstep exceptions over asynchronous exceptions taken to an exception
     * level where debug is disabled. This choice has the advantage that
     * we do not need to maintain internal state corresponding to the
     * ISV/EX syndrome bits between completion of the step and generation
     * of the exception, and our syndrome information is always correct.
     */
    gen_ss_advance(s);
    gen_swstep_exception(s, 1, s->is_ldex);
    s->base.is_jmp = DISAS_NORETURN;
}

void clear_eci_state(DisasContext *s)
{
    /*
     * Clear any ECI/ICI state: used when a load multiple/store
     * multiple insn executes.
     */
    if (s->eci) {
        store_cpu_field_constant(0, condexec_bits);
        s->eci = 0;
    }
}

static void gen_smul_dual(TCGv_i32 a, TCGv_i32 b)
{
    TCGv_i32 tmp1 = tcg_temp_new_i32();
    TCGv_i32 tmp2 = tcg_temp_new_i32();
    tcg_gen_ext16s_i32(tmp1, a);
    tcg_gen_ext16s_i32(tmp2, b);
    tcg_gen_mul_i32(tmp1, tmp1, tmp2);
    tcg_gen_sari_i32(a, a, 16);
    tcg_gen_sari_i32(b, b, 16);
    tcg_gen_mul_i32(b, b, a);
    tcg_gen_mov_i32(a, tmp1);
}

/* Byteswap each halfword.  */
void gen_rev16(TCGv_i32 dest, TCGv_i32 var)
{
    TCGv_i32 tmp = tcg_temp_new_i32();
    TCGv_i32 mask = tcg_constant_i32(0x00ff00ff);
    tcg_gen_shri_i32(tmp, var, 8);
    tcg_gen_and_i32(tmp, tmp, mask);
    tcg_gen_and_i32(var, var, mask);
    tcg_gen_shli_i32(var, var, 8);
    tcg_gen_or_i32(dest, var, tmp);
}

/* Byteswap low halfword and sign extend.  */
static void gen_revsh(TCGv_i32 dest, TCGv_i32 var)
{
    tcg_gen_bswap16_i32(var, var, TCG_BSWAP_OS);
}

/* Dual 16-bit add.  Result placed in t0 and t1 is marked as dead.
    tmp = (t0 ^ t1) & 0x8000;
    t0 &= ~0x8000;
    t1 &= ~0x8000;
    t0 = (t0 + t1) ^ tmp;
 */

static void gen_add16(TCGv_i32 dest, TCGv_i32 t0, TCGv_i32 t1)
{
    TCGv_i32 tmp = tcg_temp_new_i32();
    tcg_gen_xor_i32(tmp, t0, t1);
    tcg_gen_andi_i32(tmp, tmp, 0x8000);
    tcg_gen_andi_i32(t0, t0, ~0x8000);
    tcg_gen_andi_i32(t1, t1, ~0x8000);
    tcg_gen_add_i32(t0, t0, t1);
    tcg_gen_xor_i32(dest, t0, tmp);
}

/* Set N and Z flags from var.  */
static inline void gen_logic_CC(TCGv_i32 var)
{
    tcg_gen_mov_i32(cpu_NF, var);
    tcg_gen_mov_i32(cpu_ZF, var);
}

/* dest = T0 + T1 + CF. */
static void gen_add_carry(TCGv_i32 dest, TCGv_i32 t0, TCGv_i32 t1)
{
    tcg_gen_add_i32(dest, t0, t1);
    tcg_gen_add_i32(dest, dest, cpu_CF);
}

/* dest = T0 - T1 + CF - 1.  */
static void gen_sub_carry(TCGv_i32 dest, TCGv_i32 t0, TCGv_i32 t1)
{
    tcg_gen_sub_i32(dest, t0, t1);
    tcg_gen_add_i32(dest, dest, cpu_CF);
    tcg_gen_subi_i32(dest, dest, 1);
}

/* dest = T0 + T1. Compute C, N, V and Z flags */
static void gen_add_CC(TCGv_i32 dest, TCGv_i32 t0, TCGv_i32 t1)
{
    TCGv_i32 tmp = tcg_temp_new_i32();
    tcg_gen_movi_i32(tmp, 0);
    tcg_gen_add2_i32(cpu_NF, cpu_CF, t0, tmp, t1, tmp);
    tcg_gen_mov_i32(cpu_ZF, cpu_NF);
    tcg_gen_xor_i32(cpu_VF, cpu_NF, t0);
    tcg_gen_xor_i32(tmp, t0, t1);
    tcg_gen_andc_i32(cpu_VF, cpu_VF, tmp);
    tcg_gen_mov_i32(dest, cpu_NF);
}

/* dest = T0 + T1 + CF.  Compute C, N, V and Z flags */
static void gen_adc_CC(TCGv_i32 dest, TCGv_i32 t0, TCGv_i32 t1)
{
    TCGv_i32 tmp = tcg_temp_new_i32();
    if (TCG_TARGET_HAS_add2_i32) {
        tcg_gen_movi_i32(tmp, 0);
        tcg_gen_add2_i32(cpu_NF, cpu_CF, t0, tmp, cpu_CF, tmp);
        tcg_gen_add2_i32(cpu_NF, cpu_CF, cpu_NF, cpu_CF, t1, tmp);
    } else {
        TCGv_i64 q0 = tcg_temp_new_i64();
        TCGv_i64 q1 = tcg_temp_new_i64();
        tcg_gen_extu_i32_i64(q0, t0);
        tcg_gen_extu_i32_i64(q1, t1);
        tcg_gen_add_i64(q0, q0, q1);
        tcg_gen_extu_i32_i64(q1, cpu_CF);
        tcg_gen_add_i64(q0, q0, q1);
        tcg_gen_extr_i64_i32(cpu_NF, cpu_CF, q0);
    }
    tcg_gen_mov_i32(cpu_ZF, cpu_NF);
    tcg_gen_xor_i32(cpu_VF, cpu_NF, t0);
    tcg_gen_xor_i32(tmp, t0, t1);
    tcg_gen_andc_i32(cpu_VF, cpu_VF, tmp);
    tcg_gen_mov_i32(dest, cpu_NF);
}

/* dest = T0 - T1. Compute C, N, V and Z flags */
static void gen_sub_CC(TCGv_i32 dest, TCGv_i32 t0, TCGv_i32 t1)
{
    TCGv_i32 tmp;
    tcg_gen_sub_i32(cpu_NF, t0, t1);
    tcg_gen_mov_i32(cpu_ZF, cpu_NF);
    tcg_gen_setcond_i32(TCG_COND_GEU, cpu_CF, t0, t1);
    tcg_gen_xor_i32(cpu_VF, cpu_NF, t0);
    tmp = tcg_temp_new_i32();
    tcg_gen_xor_i32(tmp, t0, t1);
    tcg_gen_and_i32(cpu_VF, cpu_VF, tmp);
    tcg_gen_mov_i32(dest, cpu_NF);
}

/* dest = T0 + ~T1 + CF.  Compute C, N, V and Z flags */
static void gen_sbc_CC(TCGv_i32 dest, TCGv_i32 t0, TCGv_i32 t1)
{
    TCGv_i32 tmp = tcg_temp_new_i32();
    tcg_gen_not_i32(tmp, t1);
    gen_adc_CC(dest, t0, tmp);
}

#define GEN_SHIFT(name)                                               \
static void gen_##name(TCGv_i32 dest, TCGv_i32 t0, TCGv_i32 t1)       \
{                                                                     \
    TCGv_i32 tmpd = tcg_temp_new_i32();                               \
    TCGv_i32 tmp1 = tcg_temp_new_i32();                               \
    TCGv_i32 zero = tcg_constant_i32(0);                              \
    tcg_gen_andi_i32(tmp1, t1, 0x1f);                                 \
    tcg_gen_##name##_i32(tmpd, t0, tmp1);                             \
    tcg_gen_andi_i32(tmp1, t1, 0xe0);                                 \
    tcg_gen_movcond_i32(TCG_COND_NE, dest, tmp1, zero, zero, tmpd);   \
}
GEN_SHIFT(shl)
GEN_SHIFT(shr)
#undef GEN_SHIFT

static void gen_sar(TCGv_i32 dest, TCGv_i32 t0, TCGv_i32 t1)
{
    TCGv_i32 tmp1 = tcg_temp_new_i32();

    tcg_gen_andi_i32(tmp1, t1, 0xff);
    tcg_gen_umin_i32(tmp1, tmp1, tcg_constant_i32(31));
    tcg_gen_sar_i32(dest, t0, tmp1);
}

static void shifter_out_im(TCGv_i32 var, int shift)
{
    tcg_gen_extract_i32(cpu_CF, var, shift, 1);
}

/* Shift by immediate.  Includes special handling for shift == 0.  */
static inline void gen_arm_shift_im(TCGv_i32 var, int shiftop,
                                    int shift, int flags)
{
    switch (shiftop) {
    case 0: /* LSL */
        if (shift != 0) {
            if (flags)
                shifter_out_im(var, 32 - shift);
            tcg_gen_shli_i32(var, var, shift);
        }
        break;
    case 1: /* LSR */
        if (shift == 0) {
            if (flags) {
                tcg_gen_shri_i32(cpu_CF, var, 31);
            }
            tcg_gen_movi_i32(var, 0);
        } else {
            if (flags)
                shifter_out_im(var, shift - 1);
            tcg_gen_shri_i32(var, var, shift);
        }
        break;
    case 2: /* ASR */
        if (shift == 0)
            shift = 32;
        if (flags)
            shifter_out_im(var, shift - 1);
        if (shift == 32)
          shift = 31;
        tcg_gen_sari_i32(var, var, shift);
        break;
    case 3: /* ROR/RRX */
        if (shift != 0) {
            if (flags)
                shifter_out_im(var, shift - 1);
            tcg_gen_rotri_i32(var, var, shift); break;
        } else {
            TCGv_i32 tmp = tcg_temp_new_i32();
            tcg_gen_shli_i32(tmp, cpu_CF, 31);
            if (flags)
                shifter_out_im(var, 0);
            tcg_gen_shri_i32(var, var, 1);
            tcg_gen_or_i32(var, var, tmp);
        }
    }
};

static inline void gen_arm_shift_reg(TCGv_i32 var, int shiftop,
                                     TCGv_i32 shift, int flags)
{
    if (flags) {
        switch (shiftop) {
        case 0: gen_helper_shl_cc(var, tcg_env, var, shift); break;
        case 1: gen_helper_shr_cc(var, tcg_env, var, shift); break;
        case 2: gen_helper_sar_cc(var, tcg_env, var, shift); break;
        case 3: gen_helper_ror_cc(var, tcg_env, var, shift); break;
        }
    } else {
        switch (shiftop) {
        case 0:
            gen_shl(var, var, shift);
            break;
        case 1:
            gen_shr(var, var, shift);
            break;
        case 2:
            gen_sar(var, var, shift);
            break;
        case 3: tcg_gen_andi_i32(shift, shift, 0x1f);
                tcg_gen_rotr_i32(var, var, shift); break;
        }
    }
}

/*
 * Generate a conditional based on ARM condition code cc.
 * This is common between ARM and Aarch64 targets.
 */
void arm_test_cc(DisasCompare *cmp, int cc)
{
    TCGv_i32 value;
    TCGCond cond;

    switch (cc) {
    case 0: /* eq: Z */
    case 1: /* ne: !Z */
        cond = TCG_COND_EQ;
        value = cpu_ZF;
        break;

    case 2: /* cs: C */
    case 3: /* cc: !C */
        cond = TCG_COND_NE;
        value = cpu_CF;
        break;

    case 4: /* mi: N */
    case 5: /* pl: !N */
        cond = TCG_COND_LT;
        value = cpu_NF;
        break;

    case 6: /* vs: V */
    case 7: /* vc: !V */
        cond = TCG_COND_LT;
        value = cpu_VF;
        break;

    case 8: /* hi: C && !Z */
    case 9: /* ls: !C || Z -> !(C && !Z) */
        cond = TCG_COND_NE;
        value = tcg_temp_new_i32();
        /* CF is 1 for C, so -CF is an all-bits-set mask for C;
           ZF is non-zero for !Z; so AND the two subexpressions.  */
        tcg_gen_neg_i32(value, cpu_CF);
        tcg_gen_and_i32(value, value, cpu_ZF);
        break;

    case 10: /* ge: N == V -> N ^ V == 0 */
    case 11: /* lt: N != V -> N ^ V != 0 */
        /* Since we're only interested in the sign bit, == 0 is >= 0.  */
        cond = TCG_COND_GE;
        value = tcg_temp_new_i32();
        tcg_gen_xor_i32(value, cpu_VF, cpu_NF);
        break;

    case 12: /* gt: !Z && N == V */
    case 13: /* le: Z || N != V */
        cond = TCG_COND_NE;
        value = tcg_temp_new_i32();
        /* (N == V) is equal to the sign bit of ~(NF ^ VF).  Propagate
         * the sign bit then AND with ZF to yield the result.  */
        tcg_gen_xor_i32(value, cpu_VF, cpu_NF);
        tcg_gen_sari_i32(value, value, 31);
        tcg_gen_andc_i32(value, cpu_ZF, value);
        break;

    case 14: /* always */
    case 15: /* always */
        /* Use the ALWAYS condition, which will fold early.
         * It doesn't matter what we use for the value.  */
        cond = TCG_COND_ALWAYS;
        value = cpu_ZF;
        goto no_invert;

    default:
        fprintf(stderr, "Bad condition code 0x%x\n", cc);
        abort();
    }

    if (cc & 1) {
        cond = tcg_invert_cond(cond);
    }

 no_invert:
    cmp->cond = cond;
    cmp->value = value;
}

void arm_jump_cc(DisasCompare *cmp, TCGLabel *label)
{
    tcg_gen_brcondi_i32(cmp->cond, cmp->value, 0, label);
}

void arm_gen_test_cc(int cc, TCGLabel *label)
{
    DisasCompare cmp;
    arm_test_cc(&cmp, cc);
    arm_jump_cc(&cmp, label);
}

void gen_set_condexec(DisasContext *s)
{
    if (s->condexec_mask) {
        uint32_t val = (s->condexec_cond << 4) | (s->condexec_mask >> 1);

        store_cpu_field_constant(val, condexec_bits);
    }
}

void gen_update_pc(DisasContext *s, target_long diff)
{
    gen_pc_plus_diff(s, cpu_R[15], diff);
    s->pc_save = s->pc_curr + diff;
}

/* Set PC and Thumb state from var.  var is marked as dead.  */
static inline void gen_bx(DisasContext *s, TCGv_i32 var)
{
    s->base.is_jmp = DISAS_JUMP;
    if (!arm_dc_feature(s, ARM_FEATURE_V5)) {
        /*
         * ARMv4T (the Calypso ARM7TDMI): a BX into ARM state ignores bit 1 of
         * the address (the ARM PC has no bits [1:0]). The TI firmware relies
         * on it: int.s does "bx pc" from Thumb at an address 2 mod 4 to switch
         * to ARM, expecting the target (pc+4) & ~3.
         */
        TCGv_i32 mask = tcg_temp_new_i32();
        tcg_gen_andi_i32(mask, var, 1);          /* 1 if Thumb */
        tcg_gen_shli_i32(mask, mask, 1);         /* 2 if Thumb, 0 if ARM */
        tcg_gen_ori_i32(mask, mask, ~3);         /* ~1 if Thumb, ~3 if ARM */
        tcg_gen_and_i32(cpu_R[15], var, mask);
    } else {
        tcg_gen_andi_i32(cpu_R[15], var, ~1);
    }
    tcg_gen_andi_i32(var, var, 1);
    store_cpu_field(var, thumb);
    s->pc_save = -1;
}

/*
 * Set PC and Thumb state from var. var is marked as dead.
 * For M-profile CPUs, include logic to detect exception-return
 * branches and handle them. This is needed for Thumb POP/LDM to PC, LDR to PC,
 * and BX reg, and no others, and happens only for code in Handler mode.
 * The Security Extension also requires us to check for the FNC_RETURN
 * which signals a function return from non-secure state; this can happen
 * in both Handler and Thread mode.
 * To avoid having to do multiple comparisons in inline generated code,
 * we make the check we do here loose, so it will match for EXC_RETURN
 * in Thread mode. For system emulation do_v7m_exception_exit() checks
 * for these spurious cases and returns without doing anything (giving
 * the same behaviour as for a branch to a non-magic address).
 *
 * In linux-user mode it is unclear what the right behaviour for an
 * attempted FNC_RETURN should be, because in real hardware this will go
 * directly to Secure code (ie not the Linux kernel) which will then treat
 * the error in any way it chooses. For QEMU we opt to make the FNC_RETURN
 * attempt behave the way it would on a CPU without the security extension,
 * which is to say "like a normal branch". That means we can simply treat
 * all branches as normal with no magic address behaviour.
 */
static inline void gen_bx_excret(DisasContext *s, TCGv_i32 var)
{
    /* Generate the same code here as for a simple bx, but flag via
     * s->base.is_jmp that we need to do the rest of the work later.
     */
    gen_bx(s, var);
#ifndef CONFIG_USER_ONLY
    if (arm_dc_feature(s, ARM_FEATURE_M_SECURITY) ||
        (s->v7m_handler_mode && arm_dc_feature(s, ARM_FEATURE_M))) {
        s->base.is_jmp = DISAS_BX_EXCRET;
    }
#endif
}

static inline void gen_bx_excret_final_code(DisasContext *s)
{
    /* Generate the code to finish possible exception return and end the TB */
    DisasLabel excret_label = gen_disas_label(s);
    uint32_t min_magic;

    if (arm_dc_feature(s, ARM_FEATURE_M_SECURITY)) {
        /* Covers FNC_RETURN and EXC_RETURN magic */
        min_magic = FNC_RETURN_MIN_MAGIC;
    } else {
        /* EXC_RETURN magic only */
        min_magic = EXC_RETURN_MIN_MAGIC;
    }

    /* Is the new PC value in the magic range indicating exception return? */
    tcg_gen_brcondi_i32(TCG_COND_GEU, cpu_R[15], min_magic, excret_label.label);
    /* No: end the TB as we would for a DISAS_JMP */
    if (s->ss_active) {
        gen_singlestep_exception(s);
    } else {
        tcg_gen_exit_tb(NULL, 0);
    }
    set_disas_label(s, excret_label);
    /* Yes: this is an exception return.
     * At this point in runtime env->regs[15] and env->thumb will hold
     * the exception-return magic number, which do_v7m_exception_exit()
     * will read. Nothing else will be able to see those values because
     * the cpu-exec main loop guarantees that we will always go straight
     * from raising the exception to the exception-handling code.
     *
     * gen_ss_advance(s) does nothing on M profile currently but
     * calling it is conceptually the right thing as we have executed
     * this instruction (compare SWI, HVC, SMC handling).
     */
    gen_ss_advance(s);
    gen_exception_internal(EXCP_EXCEPTION_EXIT);
}

static inline void gen_bxns(DisasContext *s, int rm)
{
    TCGv_i32 var = load_reg(s, rm);

    /* The bxns helper may raise an EXCEPTION_EXIT exception, so in theory
     * we need to sync state before calling it, but:
     *  - we don't need to do gen_update_pc() because the bxns helper will
     *    always set the PC itself
     *  - we don't need to do gen_set_condexec() because BXNS is UNPREDICTABLE
     *    unless it's outside an IT block or the last insn in an IT block,
     *    so we know that condexec == 0 (already set at the top of the TB)
     *    is correct in the non-UNPREDICTABLE cases, and we can choose
     *    "zeroes the IT bits" as our UNPREDICTABLE behaviour otherwise.
     */
    gen_helper_v7m_bxns(tcg_env, var);
    s->base.is_jmp = DISAS_EXIT;
}

static inline void gen_blxns(DisasContext *s, int rm)
{
    TCGv_i32 var = load_reg(s, rm);

    /* We don't need to sync condexec state, for the same reason as bxns.
     * We do however need to set the PC, because the blxns helper reads it.
     * The blxns helper may throw an exception.
     */
    gen_update_pc(s, curr_insn_len(s));
    gen_helper_v7m_blxns(tcg_env, var);
    s->base.is_jmp = DISAS_EXIT;
}

/* Variant of store_reg which uses branch&exchange logic when storing
   to r15 in ARM architecture v7 and above. The source must be a temporary
   and will be marked as dead. */
static inline void store_reg_bx(DisasContext *s, int reg, TCGv_i32 var)
{
    if (reg == 15 && ENABLE_ARCH_7) {
        gen_bx(s, var);
    } else {
        store_reg(s, reg, var);
    }
}

/* Variant of store_reg which uses branch&exchange logic when storing
 * to r15 in ARM architecture v5T and above. This is used for storing
 * the results of a LDR/LDM/POP into r15, and corresponds to the cases
 * in the ARM ARM which use the LoadWritePC() pseudocode function. */
static inline void store_reg_from_load(DisasContext *s, int reg, TCGv_i32 var)
{
    if (reg == 15 && ENABLE_ARCH_5) {
        gen_bx_excret(s, var);
    } else {
        store_reg(s, reg, var);
    }
}

#ifdef CONFIG_USER_ONLY
#define IS_USER_ONLY 1
#else
#define IS_USER_ONLY 0
#endif

MemOp pow2_align(unsigned i)
{
    static const MemOp mop_align[] = {
        0, MO_ALIGN_2, MO_ALIGN_4, MO_ALIGN_8, MO_ALIGN_16, MO_ALIGN_32
    };
    g_assert(i < ARRAY_SIZE(mop_align));
    return mop_align[i];
}

/*
 * Abstractions of "generate code to do a guest load/store for
 * AArch32", where a vaddr is always 32 bits (and is zero
 * extended if we're a 64 bit core) and  data is also
 * 32 bits unless specifically doing a 64 bit access.
 * These functions work like tcg_gen_qemu_{ld,st}* except
 * that the address argument is TCGv_i32 rather than TCGv.
 */

static TCGv gen_aa32_addr(DisasContext *s, TCGv_i32 a32, MemOp op)
{
    TCGv addr = tcg_temp_new();
    tcg_gen_extu_i32_tl(addr, a32);

    /* Not needed for user-mode BE32, where we use MO_BE instead.  */
    if (!IS_USER_ONLY && s->sctlr_b && (op & MO_SIZE) < MO_32) {
        tcg_gen_xori_tl(addr, addr, 4 - (1 << (op & MO_SIZE)));
    }
    return addr;
}

/*
 * Internal routines are used for NEON cases where the endianness
 * and/or alignment has already been taken into account and manipulated.
 */
void gen_aa32_ld_internal_i32(DisasContext *s, TCGv_i32 val,
                              TCGv_i32 a32, int index, MemOp opc)
{
    TCGv addr = gen_aa32_addr(s, a32, opc);
    tcg_gen_qemu_ld_i32(val, addr, index, opc);
}

void gen_aa32_st_internal_i32(DisasContext *s, TCGv_i32 val,
                              TCGv_i32 a32, int index, MemOp opc)
{
    TCGv addr = gen_aa32_addr(s, a32, opc);
    tcg_gen_qemu_st_i32(val, addr, index, opc);
}

void gen_aa32_ld_internal_i64(DisasContext *s, TCGv_i64 val,
                              TCGv_i32 a32, int index, MemOp opc)
{
    TCGv addr = gen_aa32_addr(s, a32, opc);

    tcg_gen_qemu_ld_i64(val, addr, index, opc);

    /* Not needed for user-mode BE32, where we use MO_BE instead.  */
    if (!IS_USER_ONLY && s->sctlr_b && (opc & MO_SIZE) == MO_64) {
        tcg_gen_rotri_i64(val, val, 32);
    }
}

void gen_aa32_st_internal_i64(DisasContext *s, TCGv_i64 val,
                              TCGv_i32 a32, int index, MemOp opc)
{
    TCGv addr = gen_aa32_addr(s, a32, opc);

    /* Not needed for user-mode BE32, where we use MO_BE instead.  */
    if (!IS_USER_ONLY && s->sctlr_b && (opc & MO_SIZE) == MO_64) {
        TCGv_i64 tmp = tcg_temp_new_i64();
        tcg_gen_rotri_i64(tmp, val, 32);
        tcg_gen_qemu_st_i64(tmp, addr, index, opc);
    } else {
        tcg_gen_qemu_st_i64(val, addr, index, opc);
    }
}

void gen_aa32_ld_i32(DisasContext *s, TCGv_i32 val, TCGv_i32 a32,
                     int index, MemOp opc)
{
    gen_aa32_ld_internal_i32(s, val, a32, index, finalize_memop(s, opc));
}

void gen_aa32_st_i32(DisasContext *s, TCGv_i32 val, TCGv_i32 a32,
                     int index, MemOp opc)
{
    gen_aa32_st_internal_i32(s, val, a32, index, finalize_memop(s, opc));
}

void gen_aa32_ld_i64(DisasContext *s, TCGv_i64 val, TCGv_i32 a32,
                     int index, MemOp opc)
{
    gen_aa32_ld_internal_i64(s, val, a32, index, finalize_memop(s, opc));
}

void gen_aa32_st_i64(DisasContext *s, TCGv_i64 val, TCGv_i32 a32,
                     int index, MemOp opc)
{
    gen_aa32_st_internal_i64(s, val, a32, index, finalize_memop(s, opc));
}

#define DO_GEN_LD(SUFF, OPC)                                            \
    static inline void gen_aa32_ld##SUFF(DisasContext *s, TCGv_i32 val, \
                                         TCGv_i32 a32, int index)       \
    {                                                                   \
        gen_aa32_ld_i32(s, val, a32, index, OPC);                       \
    }

#define DO_GEN_ST(SUFF, OPC)                                            \
    static inline void gen_aa32_st##SUFF(DisasContext *s, TCGv_i32 val, \
                                         TCGv_i32 a32, int index)       \
    {                                                                   \
        gen_aa32_st_i32(s, val, a32, index, OPC);                       \
    }

static inline void gen_hvc(DisasContext *s, int imm16)
{
    /* The pre HVC helper handles cases when HVC gets trapped
     * as an undefined insn by runtime configuration (ie before
     * the insn really executes).
     */
    gen_update_pc(s, 0);
    gen_helper_pre_hvc(tcg_env);
    /* Otherwise we will treat this as a real exception which
     * happens after execution of the insn. (The distinction matters
     * for the PC value reported to the exception handler and also
     * for single stepping.)
     */
    s->svc_imm = imm16;
    gen_update_pc(s, curr_insn_len(s));
    s->base.is_jmp = DISAS_HVC;
}

static inline void gen_smc(DisasContext *s)
{
    /* As with HVC, we may take an exception either before or after
     * the insn executes.
     */
    gen_update_pc(s, 0);
    gen_helper_pre_smc(tcg_env, tcg_constant_i32(syn_aa32_smc()));
    gen_update_pc(s, curr_insn_len(s));
    s->base.is_jmp = DISAS_SMC;
}

static void gen_exception_internal_insn(DisasContext *s, int excp)
{
    gen_set_condexec(s);
    gen_update_pc(s, 0);
    gen_exception_internal(excp);
    s->base.is_jmp = DISAS_NORETURN;
}

static void gen_exception_el_v(int excp, uint32_t syndrome, TCGv_i32 tcg_el)
{
    gen_helper_exception_with_syndrome_el(tcg_env, tcg_constant_i32(excp),
                                          tcg_constant_i32(syndrome), tcg_el);
}

static void gen_exception_el(int excp, uint32_t syndrome, uint32_t target_el)
{
    gen_exception_el_v(excp, syndrome, tcg_constant_i32(target_el));
}

static void gen_exception(int excp, uint32_t syndrome)
{
    gen_helper_exception_with_syndrome(tcg_env, tcg_constant_i32(excp),
                                       tcg_constant_i32(syndrome));
}

static void gen_exception_insn_el_v(DisasContext *s, target_long pc_diff,
                                    int excp, uint32_t syn, TCGv_i32 tcg_el)
{
    if (s->aarch64) {
        gen_a64_update_pc(s, pc_diff);
    } else {
        gen_set_condexec(s);
        gen_update_pc(s, pc_diff);
    }
    gen_exception_el_v(excp, syn, tcg_el);
    s->base.is_jmp = DISAS_NORETURN;
}

void gen_exception_insn_el(DisasContext *s, target_long pc_diff, int excp,
                           uint32_t syn, uint32_t target_el)
{
    gen_exception_insn_el_v(s, pc_diff, excp, syn,
                            tcg_constant_i32(target_el));
}

void gen_exception_insn(DisasContext *s, target_long pc_diff,
                        int excp, uint32_t syn)
{
    if (s->aarch64) {
        gen_a64_update_pc(s, pc_diff);
    } else {
        gen_set_condexec(s);
        gen_update_pc(s, pc_diff);
    }
    gen_exception(excp, syn);
    s->base.is_jmp = DISAS_NORETURN;
}

static void gen_exception_bkpt_insn(DisasContext *s, uint32_t syn)
{
    gen_set_condexec(s);
    gen_update_pc(s, 0);
    gen_helper_exception_bkpt_insn(tcg_env, tcg_constant_i32(syn));
    s->base.is_jmp = DISAS_NORETURN;
}

void unallocated_encoding(DisasContext *s)
{
    /* Unallocated and reserved encodings are uncategorized */
    gen_exception_insn(s, 0, EXCP_UDEF, syn_uncategorized());
}

/* Force a TB lookup after an instruction that changes the CPU state.  */
void gen_lookup_tb(DisasContext *s)
{
    gen_pc_plus_diff(s, cpu_R[15], curr_insn_len(s));
    s->base.is_jmp = DISAS_EXIT;
}

static inline void gen_hlt(DisasContext *s, int imm)
{
    /* HLT. This has two purposes.
     * Architecturally, it is an external halting debug instruction.
     * Since QEMU doesn't implement external debug, we treat this as
     * it is required for halting debug disabled: it will UNDEF.
     * Secondly, "HLT 0x3C" is a T32 semihosting trap instruction,
     * and "HLT 0xF000" is an A32 semihosting syscall. These traps
     * must trigger semihosting even for ARMv7 and earlier, where
     * HLT was an undefined encoding.
     * In system mode, we don't allow userspace access to
     * semihosting, to provide some semblance of security
     * (and for consistency with our 32-bit semihosting).
     */
    if (semihosting_enabled(s->current_el == 0) &&
        (imm == (s->thumb ? 0x3c : 0xf000))) {
        gen_exception_internal_insn(s, EXCP_SEMIHOST);
        return;
    }

    unallocated_encoding(s);
}

/*
 * Return the offset of a "full" NEON Dreg.
 */
long neon_full_reg_offset(unsigned reg)
{
    return offsetof(CPUARMState, vfp.zregs[reg >> 1].d[reg & 1]);
}

/*
 * Return the offset of a 2**SIZE piece of a NEON register, at index ELE,
 * where 0 is the least significant end of the register.
 */
long neon_element_offset(int reg, int element, MemOp memop)
{
    int element_size = 1 << (memop & MO_SIZE);
    int ofs = element * element_size;
#if HOST_BIG_ENDIAN
    /*
     * Calculate the offset assuming fully little-endian,
     * then XOR to account for the order of the 8-byte units.
     */
    if (element_size < 8) {
        ofs ^= 8 - element_size;
    }
#endif
    return neon_full_reg_offset(reg) + ofs;
}

/* Return the offset of a VFP Dreg (dp = true) or VFP Sreg (dp = false). */
long vfp_reg_offset(bool dp, unsigned reg)
{
    if (dp) {
        return neon_element_offset(reg, 0, MO_64);
    } else {
        return neon_element_offset(reg >> 1, reg & 1, MO_32);
    }
}

void read_neon_element32(TCGv_i32 dest, int reg, int ele, MemOp memop)
{
    long off = neon_element_offset(reg, ele, memop);

    switch (memop) {
    case MO_SB:
        tcg_gen_ld8s_i32(dest, tcg_env, off);
        break;
    case MO_UB:
        tcg_gen_ld8u_i32(dest, tcg_env, off);
        break;
    case MO_SW:
        tcg_gen_ld16s_i32(dest, tcg_env, off);
        break;
    case MO_UW:
        tcg_gen_ld16u_i32(dest, tcg_env, off);
        break;
    case MO_UL:
    case MO_SL:
        tcg_gen_ld_i32(dest, tcg_env, off);
        break;
    default:
        g_assert_not_reached();
    }
}

void read_neon_element64(TCGv_i64 dest, int reg, int ele, MemOp memop)
{
    long off = neon_element_offset(reg, ele, memop);

    switch (memop) {
    case MO_SL:
        tcg_gen_ld32s_i64(dest, tcg_env, off);
        break;
    case MO_UL:
        tcg_gen_ld32u_i64(dest, tcg_env, off);
        break;
    case MO_UQ:
        tcg_gen_ld_i64(dest, tcg_env, off);
        break;
    default:
        g_assert_not_reached();
    }
}

void write_neon_element32(TCGv_i32 src, int reg, int ele, MemOp memop)
{
    long off = neon_element_offset(reg, ele, memop);

    switch (memop) {
    case MO_8:
        tcg_gen_st8_i32(src, tcg_env, off);
        break;
    case MO_16:
        tcg_gen_st16_i32(src, tcg_env, off);
        break;
    case MO_32:
        tcg_gen_st_i32(src, tcg_env, off);
        break;
    default:
        g_assert_not_reached();
    }
}

void write_neon_element64(TCGv_i64 src, int reg, int ele, MemOp memop)
{
    long off = neon_element_offset(reg, ele, memop);

    switch (memop) {
    case MO_32:
        tcg_gen_st32_i64(src, tcg_env, off);
        break;
    case MO_64:
        tcg_gen_st_i64(src, tcg_env, off);
        break;
    default:
        g_assert_not_reached();
    }
}

#define ARM_CP_RW_BIT   (1 << 20)

static inline void iwmmxt_load_reg(TCGv_i64 var, int reg)
{
    tcg_gen_ld_i64(var, tcg_env, offsetof(CPUARMState, iwmmxt.regs[reg]));
}

static inline void iwmmxt_store_reg(TCGv_i64 var, int reg)
{
    tcg_gen_st_i64(var, tcg_env, offsetof(CPUARMState, iwmmxt.regs[reg]));
}

static inline TCGv_i32 iwmmxt_load_creg(int reg)
{
    TCGv_i32 var = tcg_temp_new_i32();
    tcg_gen_ld_i32(var, tcg_env, offsetof(CPUARMState, iwmmxt.cregs[reg]));
    return var;
}

static inline void iwmmxt_store_creg(int reg, TCGv_i32 var)
{
    tcg_gen_st_i32(var, tcg_env, offsetof(CPUARMState, iwmmxt.cregs[reg]));
}

static inline void gen_op_iwmmxt_movq_wRn_M0(int rn)
{
    iwmmxt_store_reg(cpu_M0, rn);
}

static inline void gen_op_iwmmxt_movq_M0_wRn(int rn)
{
    iwmmxt_load_reg(cpu_M0, rn);
}

static inline void gen_op_iwmmxt_orq_M0_wRn(int rn)
{
    iwmmxt_load_reg(cpu_V1, rn);
    tcg_gen_or_i64(cpu_M0, cpu_M0, cpu_V1);
}

static inline void gen_op_iwmmxt_andq_M0_wRn(int rn)
{
    iwmmxt_load_reg(cpu_V1, rn);
    tcg_gen_and_i64(cpu_M0, cpu_M0, cpu_V1);
}

static inline void gen_op_iwmmxt_xorq_M0_wRn(int rn)
{
    iwmmxt_load_reg(cpu_V1, rn);
    tcg_gen_xor_i64(cpu_M0, cpu_M0, cpu_V1);
}

#define IWMMXT_OP(name) \
static inline void gen_op_iwmmxt_##name##_M0_wRn(int rn) \
{ \
    iwmmxt_load_reg(cpu_V1, rn); \
    gen_helper_iwmmxt_##name(cpu_M0, cpu_M0, cpu_V1); \
}

#define IWMMXT_OP_ENV(name) \
static inline void gen_op_iwmmxt_##name##_M0_wRn(int rn) \
{ \
    iwmmxt_load_reg(cpu_V1, rn); \
    gen_helper_iwmmxt_##name(cpu_M0, tcg_env, cpu_M0, cpu_V1); \
}

#define IWMMXT_OP_ENV_SIZE(name) \
IWMMXT_OP_ENV(name##b) \
IWMMXT_OP_ENV(name##w) \
IWMMXT_OP_ENV(name##l)

#define IWMMXT_OP_ENV1(name) \
static inline void gen_op_iwmmxt_##name##_M0(void) \
{ \
    gen_helper_iwmmxt_##name(cpu_M0, tcg_env, cpu_M0); \
}

IWMMXT_OP(maddsq)
IWMMXT_OP(madduq)
IWMMXT_OP(sadb)
IWMMXT_OP(sadw)
IWMMXT_OP(mulslw)
IWMMXT_OP(mulshw)
IWMMXT_OP(mululw)
IWMMXT_OP(muluhw)
IWMMXT_OP(macsw)
IWMMXT_OP(macuw)

IWMMXT_OP_ENV_SIZE(unpackl)
IWMMXT_OP_ENV_SIZE(unpackh)

IWMMXT_OP_ENV1(unpacklub)
IWMMXT_OP_ENV1(unpackluw)
IWMMXT_OP_ENV1(unpacklul)
IWMMXT_OP_ENV1(unpackhub)
IWMMXT_OP_ENV1(unpackhuw)
IWMMXT_OP_ENV1(unpackhul)
IWMMXT_OP_ENV1(unpacklsb)
IWMMXT_OP_ENV1(unpacklsw)
IWMMXT_OP_ENV1(unpacklsl)
IWMMXT_OP_ENV1(unpackhsb)
IWMMXT_OP_ENV1(unpackhsw)
IWMMXT_OP_ENV1(unpackhsl)

IWMMXT_OP_ENV_SIZE(cmpeq)
IWMMXT_OP_ENV_SIZE(cmpgtu)
IWMMXT_OP_ENV_SIZE(cmpgts)

IWMMXT_OP_ENV_SIZE(mins)
IWMMXT_OP_ENV_SIZE(minu)
IWMMXT_OP_ENV_SIZE(maxs)
IWMMXT_OP_ENV_SIZE(maxu)

IWMMXT_OP_ENV_SIZE(subn)
IWMMXT_OP_ENV_SIZE(addn)
IWMMXT_OP_ENV_SIZE(subu)
IWMMXT_OP_ENV_SIZE(addu)
IWMMXT_OP_ENV_SIZE(subs)
IWMMXT_OP_ENV_SIZE(adds)

IWMMXT_OP_ENV(avgb0)
IWMMXT_OP_ENV(avgb1)
IWMMXT_OP_ENV(avgw0)
IWMMXT_OP_ENV(avgw1)

IWMMXT_OP_ENV(packuw)
IWMMXT_OP_ENV(packul)
IWMMXT_OP_ENV(packuq)
IWMMXT_OP_ENV(packsw)
IWMMXT_OP_ENV(packsl)
IWMMXT_OP_ENV(packsq)

static void gen_op_iwmmxt_set_mup(void)
{
    TCGv_i32 tmp;
    tmp = load_cpu_field(iwmmxt.cregs[ARM_IWMMXT_wCon]);
    tcg_gen_ori_i32(tmp, tmp, 2);
    store_cpu_field(tmp, iwmmxt.cregs[ARM_IWMMXT_wCon]);
}

static void gen_op_iwmmxt_set_cup(void)
{
    TCGv_i32 tmp;
    tmp = load_cpu_field(iwmmxt.cregs[ARM_IWMMXT_wCon]);
    tcg_gen_ori_i32(tmp, tmp, 1);
    store_cpu_field(tmp, iwmmxt.cregs[ARM_IWMMXT_wCon]);
}

static void gen_op_iwmmxt_setpsr_nz(void)
{
    TCGv_i32 tmp = tcg_temp_new_i32();
    gen_helper_iwmmxt_setpsr_nz(tmp, cpu_M0);
    store_cpu_field(tmp, iwmmxt.cregs[ARM_IWMMXT_wCASF]);
}

static inline void gen_op_iwmmxt_addl_M0_wRn(int rn)
{
    iwmmxt_load_reg(cpu_V1, rn);
    tcg_gen_ext32u_i64(cpu_V1, cpu_V1);
    tcg_gen_add_i64(cpu_M0, cpu_M0, cpu_V1);
}

static inline int gen_iwmmxt_address(DisasContext *s, uint32_t insn,
                                     TCGv_i32 dest)
{
    int rd;
    uint32_t offset;
    TCGv_i32 tmp;

    rd = (insn >> 16) & 0xf;
    tmp = load_reg(s, rd);

    offset = (insn & 0xff) << ((insn >> 7) & 2);
    if (insn & (1 << 24)) {
        /* Pre indexed */
        if (insn & (1 << 23))
            tcg_gen_addi_i32(tmp, tmp, offset);
        else
            tcg_gen_addi_i32(tmp, tmp, -offset);
        tcg_gen_mov_i32(dest, tmp);
        if (insn & (1 << 21)) {
            store_reg(s, rd, tmp);
        }
    } else if (insn & (1 << 21)) {
        /* Post indexed */
        tcg_gen_mov_i32(dest, tmp);
        if (insn & (1 << 23))
            tcg_gen_addi_i32(tmp, tmp, offset);
        else
            tcg_gen_addi_i32(tmp, tmp, -offset);
        store_reg(s, rd, tmp);
    } else if (!(insn & (1 << 23)))
        return 1;
    return 0;
}

static inline int gen_iwmmxt_shift(uint32_t insn, uint32_t mask, TCGv_i32 dest)
{
    int rd = (insn >> 0) & 0xf;
    TCGv_i32 tmp;

    if (insn & (1 << 8)) {
        if (rd < ARM_IWMMXT_wCGR0 || rd > ARM_IWMMXT_wCGR3) {
            return 1;
        } else {
            tmp = iwmmxt_load_creg(rd);
        }
    } else {
        tmp = tcg_temp_new_i32();
        iwmmxt_load_reg(cpu_V0, rd);
        tcg_gen_extrl_i64_i32(tmp, cpu_V0);
    }
    tcg_gen_andi_i32(tmp, tmp, mask);
    tcg_gen_mov_i32(dest, tmp);
    return 0;
}

/* Disassemble an iwMMXt instruction.  Returns nonzero if an error occurred
   (ie. an undefined instruction).  */
static int disas_iwmmxt_insn(DisasContext *s, uint32_t insn)
{
    int rd, wrd;
    int rdhi, rdlo, rd0, rd1, i;
    TCGv_i32 addr;
    TCGv_i32 tmp, tmp2, tmp3;

    if ((insn & 0x0e000e00) == 0x0c000000) {
        if ((insn & 0x0fe00ff0) == 0x0c400000) {
            wrd = insn & 0xf;
            rdlo = (insn >> 12) & 0xf;
            rdhi = (insn >> 16) & 0xf;
            if (insn & ARM_CP_RW_BIT) {                         /* TMRRC */
                iwmmxt_load_reg(cpu_V0, wrd);
                tcg_gen_extrl_i64_i32(cpu_R[rdlo], cpu_V0);
                tcg_gen_extrh_i64_i32(cpu_R[rdhi], cpu_V0);
            } else {                                    /* TMCRR */
                tcg_gen_concat_i32_i64(cpu_V0, cpu_R[rdlo], cpu_R[rdhi]);
                iwmmxt_store_reg(cpu_V0, wrd);
                gen_op_iwmmxt_set_mup();
            }
            return 0;
        }

        wrd = (insn >> 12) & 0xf;
        addr = tcg_temp_new_i32();
        if (gen_iwmmxt_address(s, insn, addr)) {
            return 1;
        }
        if (insn & ARM_CP_RW_BIT) {
            if ((insn >> 28) == 0xf) {                  /* WLDRW wCx */
                tmp = tcg_temp_new_i32();
                gen_aa32_ld32u(s, tmp, addr, get_mem_index(s));
                iwmmxt_store_creg(wrd, tmp);
            } else {
                i = 1;
                if (insn & (1 << 8)) {
                    if (insn & (1 << 22)) {             /* WLDRD */
                        gen_aa32_ld64(s, cpu_M0, addr, get_mem_index(s));
                        i = 0;
                    } else {                            /* WLDRW wRd */
                        tmp = tcg_temp_new_i32();
                        gen_aa32_ld32u(s, tmp, addr, get_mem_index(s));
                    }
                } else {
                    tmp = tcg_temp_new_i32();
                    if (insn & (1 << 22)) {             /* WLDRH */
                        gen_aa32_ld16u(s, tmp, addr, get_mem_index(s));
                    } else {                            /* WLDRB */
                        gen_aa32_ld8u(s, tmp, addr, get_mem_index(s));
                    }
                }
                if (i) {
                    tcg_gen_extu_i32_i64(cpu_M0, tmp);
                }
                gen_op_iwmmxt_movq_wRn_M0(wrd);
            }
        } else {
            if ((insn >> 28) == 0xf) {                  /* WSTRW wCx */
                tmp = iwmmxt_load_creg(wrd);
                gen_aa32_st32(s, tmp, addr, get_mem_index(s));
            } else {
                gen_op_iwmmxt_movq_M0_wRn(wrd);
                tmp = tcg_temp_new_i32();
                if (insn & (1 << 8)) {
                    if (insn & (1 << 22)) {             /* WSTRD */
                        gen_aa32_st64(s, cpu_M0, addr, get_mem_index(s));
                    } else {                            /* WSTRW wRd */
                        tcg_gen_extrl_i64_i32(tmp, cpu_M0);
                        gen_aa32_st32(s, tmp, addr, get_mem_index(s));
                    }
                } else {
                    if (insn & (1 << 22)) {             /* WSTRH */
                        tcg_gen_extrl_i64_i32(tmp, cpu_M0);
                        gen_aa32_st16(s, tmp, addr, get_mem_index(s));
                    } else {                            /* WSTRB */
                        tcg_gen_extrl_i64_i32(tmp, cpu_M0);
                        gen_aa32_st8(s, tmp, addr, get_mem_index(s));
                    }
                }
            }
        }
        return 0;
    }

    if ((insn & 0x0f000000) != 0x0e000000)
        return 1;

    switch (((insn >> 12) & 0xf00) | ((insn >> 4) & 0xff)) {
    case 0x000:                                                 /* WOR */
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 0) & 0xf;
        rd1 = (insn >> 16) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        gen_op_iwmmxt_orq_M0_wRn(rd1);
        gen_op_iwmmxt_setpsr_nz();
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        gen_op_iwmmxt_set_cup();
        break;
    case 0x011:                                                 /* TMCR */
        if (insn & 0xf)
            return 1;
        rd = (insn >> 12) & 0xf;
        wrd = (insn >> 16) & 0xf;
        switch (wrd) {
        case ARM_IWMMXT_wCID:
        case ARM_IWMMXT_wCASF:
            break;
        case ARM_IWMMXT_wCon:
            gen_op_iwmmxt_set_cup();
            /* Fall through.  */
        case ARM_IWMMXT_wCSSF:
            tmp = iwmmxt_load_creg(wrd);
            tmp2 = load_reg(s, rd);
            tcg_gen_andc_i32(tmp, tmp, tmp2);
            iwmmxt_store_creg(wrd, tmp);
            break;
        case ARM_IWMMXT_wCGR⟨1⟩:  ×4
    ⟨⟩ = (0) (1) (2) (3)
            gen_op_iwmmxt_set_cup();
            tmp = load_reg(s, rd);
            iwmmxt_store_creg(wrd, tmp);
            break;
        default:
            return 1;
        }
        break;
    case 0x100:                                                 /* WXOR */
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 0) & 0xf;
        rd1 = (insn >> 16) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        gen_op_iwmmxt_xorq_M0_wRn(rd1);
        gen_op_iwmmxt_setpsr_nz();
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        gen_op_iwmmxt_set_cup();
        break;
    case 0x111:                                                 /* TMRC */
        if (insn & 0xf)
            return 1;
        rd = (insn >> 12) & 0xf;
        wrd = (insn >> 16) & 0xf;
        tmp = iwmmxt_load_creg(wrd);
        store_reg(s, rd, tmp);
        break;
    case 0x300:                                                 /* WANDN */
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 0) & 0xf;
        rd1 = (insn >> 16) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        tcg_gen_neg_i64(cpu_M0, cpu_M0);
        gen_op_iwmmxt_andq_M0_wRn(rd1);
        gen_op_iwmmxt_setpsr_nz();
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        gen_op_iwmmxt_set_cup();
        break;
    case 0x200:                                                 /* WAND */
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 0) & 0xf;
        rd1 = (insn >> 16) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        gen_op_iwmmxt_andq_M0_wRn(rd1);
        gen_op_iwmmxt_setpsr_nz();
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        gen_op_iwmmxt_set_cup();
        break;
    case 0x810: case 0xa10:                             /* WMADD */
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 0) & 0xf;
        rd1 = (insn >> 16) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        if (insn & (1 << 21))
            gen_op_iwmmxt_maddsq_M0_wRn(rd1);
        else
            gen_op_iwmmxt_madduq_M0_wRn(rd1);
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        break;
    case 0x10e: case 0x50e: case 0x90e: case 0xd0e:     /* WUNPCKIL */
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        rd1 = (insn >> 0) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        switch ((insn >> 22) & 3) {
        case 0:
            gen_op_iwmmxt_unpacklb_M0_wRn(rd1);
            break;
        case 1:
            gen_op_iwmmxt_unpacklw_M0_wRn(rd1);
            break;
        case 2:
            gen_op_iwmmxt_unpackll_M0_wRn(rd1);
            break;
        case 3:
            return 1;
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        gen_op_iwmmxt_set_cup();
        break;
    case 0x10c: case 0x50c: case 0x90c: case 0xd0c:     /* WUNPCKIH */
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        rd1 = (insn >> 0) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        switch ((insn >> 22) & 3) {
        case 0:
            gen_op_iwmmxt_unpackhb_M0_wRn(rd1);
            break;
        case 1:
            gen_op_iwmmxt_unpackhw_M0_wRn(rd1);
            break;
        case 2:
            gen_op_iwmmxt_unpackhl_M0_wRn(rd1);
            break;
        case 3:
            return 1;
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        gen_op_iwmmxt_set_cup();
        break;
    case 0x012: case 0x112: case 0x412: case 0x512:     /* WSAD */
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        rd1 = (insn >> 0) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        if (insn & (1 << 22))
            gen_op_iwmmxt_sadw_M0_wRn(rd1);
        else
            gen_op_iwmmxt_sadb_M0_wRn(rd1);
        if (!(insn & (1 << 20)))
            gen_op_iwmmxt_addl_M0_wRn(wrd);
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        break;
    case 0x010: case 0x110: case 0x210: case 0x310:     /* WMUL */
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        rd1 = (insn >> 0) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        if (insn & (1 << 21)) {
            if (insn & (1 << 20))
                gen_op_iwmmxt_mulshw_M0_wRn(rd1);
            else
                gen_op_iwmmxt_mulslw_M0_wRn(rd1);
        } else {
            if (insn & (1 << 20))
                gen_op_iwmmxt_muluhw_M0_wRn(rd1);
            else
                gen_op_iwmmxt_mululw_M0_wRn(rd1);
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        break;
    case 0x410: case 0x510: case 0x610: case 0x710:     /* WMAC */
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        rd1 = (insn >> 0) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        if (insn & (1 << 21))
            gen_op_iwmmxt_macsw_M0_wRn(rd1);
        else
            gen_op_iwmmxt_macuw_M0_wRn(rd1);
        if (!(insn & (1 << 20))) {
            iwmmxt_load_reg(cpu_V1, wrd);
            tcg_gen_add_i64(cpu_M0, cpu_M0, cpu_V1);
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        break;
    case 0x006: case 0x406: case 0x806: case 0xc06:     /* WCMPEQ */
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        rd1 = (insn >> 0) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        switch ((insn >> 22) & 3) {
        case 0:
            gen_op_iwmmxt_cmpeqb_M0_wRn(rd1);
            break;
        case 1:
            gen_op_iwmmxt_cmpeqw_M0_wRn(rd1);
            break;
        case 2:
            gen_op_iwmmxt_cmpeql_M0_wRn(rd1);
            break;
        case 3:
            return 1;
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        gen_op_iwmmxt_set_cup();
        break;
    case 0x800: case 0x900: case 0xc00: case 0xd00:     /* WAVG2 */
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        rd1 = (insn >> 0) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        if (insn & (1 << 22)) {
            if (insn & (1 << 20))
                gen_op_iwmmxt_avgw1_M0_wRn(rd1);
            else
                gen_op_iwmmxt_avgw0_M0_wRn(rd1);
        } else {
            if (insn & (1 << 20))
                gen_op_iwmmxt_avgb1_M0_wRn(rd1);
            else
                gen_op_iwmmxt_avgb0_M0_wRn(rd1);
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        gen_op_iwmmxt_set_cup();
        break;
    case 0x802: case 0x902: case 0xa02: case 0xb02:     /* WALIGNR */
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        rd1 = (insn >> 0) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        tmp = iwmmxt_load_creg(ARM_IWMMXT_wCGR0 + ((insn >> 20) & 3));
        tcg_gen_andi_i32(tmp, tmp, 7);
        iwmmxt_load_reg(cpu_V1, rd1);
        gen_helper_iwmmxt_align(cpu_M0, cpu_M0, cpu_V1, tmp);
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        break;
    case 0x601: case 0x605: case 0x609: case 0x60d:     /* TINSR */
        if (((insn >> 6) & 3) == 3)
            return 1;
        rd = (insn >> 12) & 0xf;
        wrd = (insn >> 16) & 0xf;
        tmp = load_reg(s, rd);
        gen_op_iwmmxt_movq_M0_wRn(wrd);
        switch ((insn >> 6) & 3) {
        case 0:
            tmp2 = tcg_constant_i32(0xff);
            tmp3 = tcg_constant_i32((insn & 7) << 3);
            break;
        case 1:
            tmp2 = tcg_constant_i32(0xffff);
            tmp3 = tcg_constant_i32((insn & 3) << 4);
            break;
        case 2:
            tmp2 = tcg_constant_i32(0xffffffff);
            tmp3 = tcg_constant_i32((insn & 1) << 5);
            break;
        default:
            g_assert_not_reached();
        }
        gen_helper_iwmmxt_insr(cpu_M0, cpu_M0, tmp, tmp2, tmp3);
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        break;
    case 0x107: case 0x507: case 0x907: case 0xd07:     /* TEXTRM */
        rd = (insn >> 12) & 0xf;
        wrd = (insn >> 16) & 0xf;
        if (rd == 15 || ((insn >> 22) & 3) == 3)
            return 1;
        gen_op_iwmmxt_movq_M0_wRn(wrd);
        tmp = tcg_temp_new_i32();
        switch ((insn >> 22) & 3) {
        case 0:
            tcg_gen_shri_i64(cpu_M0, cpu_M0, (insn & 7) << 3);
            tcg_gen_extrl_i64_i32(tmp, cpu_M0);
            if (insn & 8) {
                tcg_gen_ext8s_i32(tmp, tmp);
            } else {
                tcg_gen_andi_i32(tmp, tmp, 0xff);
            }
            break;
        case 1:
            tcg_gen_shri_i64(cpu_M0, cpu_M0, (insn & 3) << 4);
            tcg_gen_extrl_i64_i32(tmp, cpu_M0);
            if (insn & 8) {
                tcg_gen_ext16s_i32(tmp, tmp);
            } else {
                tcg_gen_andi_i32(tmp, tmp, 0xffff);
            }
            break;
        case 2:
            tcg_gen_shri_i64(cpu_M0, cpu_M0, (insn & 1) << 5);
            tcg_gen_extrl_i64_i32(tmp, cpu_M0);
            break;
        }
        store_reg(s, rd, tmp);
        break;
    case 0x117: case 0x517: case 0x917: case 0xd17:     /* TEXTRC */
        if ((insn & 0x000ff008) != 0x0003f000 || ((insn >> 22) & 3) == 3)
            return 1;
        tmp = iwmmxt_load_creg(ARM_IWMMXT_wCASF);
        switch ((insn >> 22) & 3) {
        case 0:
            tcg_gen_shri_i32(tmp, tmp, ((insn & 7) << 2) + 0);
            break;
        case 1:
            tcg_gen_shri_i32(tmp, tmp, ((insn & 3) << 3) + 4);
            break;
        case 2:
            tcg_gen_shri_i32(tmp, tmp, ((insn & 1) << 4) + 12);
            break;
        }
        tcg_gen_shli_i32(tmp, tmp, 28);
        gen_set_nzcv(tmp);
        break;
    case 0x401: case 0x405: case 0x409: case 0x40d:     /* TBCST */
        if (((insn >> 6) & 3) == 3)
            return 1;
        rd = (insn >> 12) & 0xf;
        wrd = (insn >> 16) & 0xf;
        tmp = load_reg(s, rd);
        switch ((insn >> 6) & 3) {
        case 0:
            gen_helper_iwmmxt_bcstb(cpu_M0, tmp);
            break;
        case 1:
            gen_helper_iwmmxt_bcstw(cpu_M0, tmp);
            break;
        case 2:
            gen_helper_iwmmxt_bcstl(cpu_M0, tmp);
            break;
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        break;
    case 0x113: case 0x513: case 0x913: case 0xd13:     /* TANDC */
        if ((insn & 0x000ff00f) != 0x0003f000 || ((insn >> 22) & 3) == 3)
            return 1;
        tmp = iwmmxt_load_creg(ARM_IWMMXT_wCASF);
        tmp2 = tcg_temp_new_i32();
        tcg_gen_mov_i32(tmp2, tmp);
        switch ((insn >> 22) & 3) {
        case 0:
            for (i = 0; i < 7; i ++) {
                tcg_gen_shli_i32(tmp2, tmp2, 4);
                tcg_gen_and_i32(tmp, tmp, tmp2);
            }
            break;
        case 1:
            for (i = 0; i < 3; i ++) {
                tcg_gen_shli_i32(tmp2, tmp2, 8);
                tcg_gen_and_i32(tmp, tmp, tmp2);
            }
            break;
        case 2:
            tcg_gen_shli_i32(tmp2, tmp2, 16);
            tcg_gen_and_i32(tmp, tmp, tmp2);
            break;
        }
        gen_set_nzcv(tmp);
        break;
    case 0x01c: case 0x41c: case 0x81c: case 0xc1c:     /* WACC */
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        switch ((insn >> 22) & 3) {
        case 0:
            gen_helper_iwmmxt_addcb(cpu_M0, cpu_M0);
            break;
        case 1:
            gen_helper_iwmmxt_addcw(cpu_M0, cpu_M0);
            break;
        case 2:
            gen_helper_iwmmxt_addcl(cpu_M0, cpu_M0);
            break;
        case 3:
            return 1;
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        break;
    case 0x115: case 0x515: case 0x915: case 0xd15:     /* TORC */
        if ((insn & 0x000ff00f) != 0x0003f000 || ((insn >> 22) & 3) == 3)
            return 1;
        tmp = iwmmxt_load_creg(ARM_IWMMXT_wCASF);
        tmp2 = tcg_temp_new_i32();
        tcg_gen_mov_i32(tmp2, tmp);
        switch ((insn >> 22) & 3) {
        case 0:
            for (i = 0; i < 7; i ++) {
                tcg_gen_shli_i32(tmp2, tmp2, 4);
                tcg_gen_or_i32(tmp, tmp, tmp2);
            }
            break;
        case 1:
            for (i = 0; i < 3; i ++) {
                tcg_gen_shli_i32(tmp2, tmp2, 8);
                tcg_gen_or_i32(tmp, tmp, tmp2);
            }
            break;
        case 2:
            tcg_gen_shli_i32(tmp2, tmp2, 16);
            tcg_gen_or_i32(tmp, tmp, tmp2);
            break;
        }
        gen_set_nzcv(tmp);
        break;
    case 0x103: case 0x503: case 0x903: case 0xd03:     /* TMOVMSK */
        rd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        if ((insn & 0xf) != 0 || ((insn >> 22) & 3) == 3)
            return 1;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        tmp = tcg_temp_new_i32();
        switch ((insn >> 22) & 3) {
        case 0:
            gen_helper_iwmmxt_msbb(tmp, cpu_M0);
            break;
        case 1:
            gen_helper_iwmmxt_msbw(tmp, cpu_M0);
            break;
        case 2:
            gen_helper_iwmmxt_msbl(tmp, cpu_M0);
            break;
        }
        store_reg(s, rd, tmp);
        break;
    case 0x106: case 0x306: case 0x506: case 0x706:     /* WCMPGT */
    case 0x906: case 0xb06: case 0xd06: case 0xf06:
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        rd1 = (insn >> 0) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        switch ((insn >> 22) & 3) {
        case 0:
            if (insn & (1 << 21))
                gen_op_iwmmxt_cmpgtsb_M0_wRn(rd1);
            else
                gen_op_iwmmxt_cmpgtub_M0_wRn(rd1);
            break;
        case 1:
            if (insn & (1 << 21))
                gen_op_iwmmxt_cmpgtsw_M0_wRn(rd1);
            else
                gen_op_iwmmxt_cmpgtuw_M0_wRn(rd1);
            break;
        case 2:
            if (insn & (1 << 21))
                gen_op_iwmmxt_cmpgtsl_M0_wRn(rd1);
            else
                gen_op_iwmmxt_cmpgtul_M0_wRn(rd1);
            break;
        case 3:
            return 1;
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        gen_op_iwmmxt_set_cup();
        break;
    case 0x00e: case 0x20e: case 0x40e: case 0x60e:     /* WUNPCKEL */
    case 0x80e: case 0xa0e: case 0xc0e: case 0xe0e:
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        switch ((insn >> 22) & 3) {
        case 0:
            if (insn & (1 << 21))
                gen_op_iwmmxt_unpacklsb_M0();
            else
                gen_op_iwmmxt_unpacklub_M0();
            break;
        case 1:
            if (insn & (1 << 21))
                gen_op_iwmmxt_unpacklsw_M0();
            else
                gen_op_iwmmxt_unpackluw_M0();
            break;
        case 2:
            if (insn & (1 << 21))
                gen_op_iwmmxt_unpacklsl_M0();
            else
                gen_op_iwmmxt_unpacklul_M0();
            break;
        case 3:
            return 1;
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        gen_op_iwmmxt_set_cup();
        break;
    case 0x00c: case 0x20c: case 0x40c: case 0x60c:     /* WUNPCKEH */
    case 0x80c: case 0xa0c: case 0xc0c: case 0xe0c:
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        switch ((insn >> 22) & 3) {
        case 0:
            if (insn & (1 << 21))
                gen_op_iwmmxt_unpackhsb_M0();
            else
                gen_op_iwmmxt_unpackhub_M0();
            break;
        case 1:
            if (insn & (1 << 21))
                gen_op_iwmmxt_unpackhsw_M0();
            else
                gen_op_iwmmxt_unpackhuw_M0();
            break;
        case 2:
            if (insn & (1 << 21))
                gen_op_iwmmxt_unpackhsl_M0();
            else
                gen_op_iwmmxt_unpackhul_M0();
            break;
        case 3:
            return 1;
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        gen_op_iwmmxt_set_cup();
        break;
    case 0x204: case 0x604: case 0xa04: case 0xe04:     /* WSRL */
    case 0x214: case 0x614: case 0xa14: case 0xe14:
        if (((insn >> 22) & 3) == 0)
            return 1;
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        tmp = tcg_temp_new_i32();
        if (gen_iwmmxt_shift(insn, 0xff, tmp)) {
            return 1;
        }
        switch ((insn >> 22) & 3) {
        case 1:
            gen_helper_iwmmxt_srlw(cpu_M0, tcg_env, cpu_M0, tmp);
            break;
        case 2:
            gen_helper_iwmmxt_srll(cpu_M0, tcg_env, cpu_M0, tmp);
            break;
        case 3:
            gen_helper_iwmmxt_srlq(cpu_M0, tcg_env, cpu_M0, tmp);
            break;
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        gen_op_iwmmxt_set_cup();
        break;
    case 0x004: case 0x404: case 0x804: case 0xc04:     /* WSRA */
    case 0x014: case 0x414: case 0x814: case 0xc14:
        if (((insn >> 22) & 3) == 0)
            return 1;
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        tmp = tcg_temp_new_i32();
        if (gen_iwmmxt_shift(insn, 0xff, tmp)) {
            return 1;
        }
        switch ((insn >> 22) & 3) {
        case 1:
            gen_helper_iwmmxt_sraw(cpu_M0, tcg_env, cpu_M0, tmp);
            break;
        case 2:
            gen_helper_iwmmxt_sral(cpu_M0, tcg_env, cpu_M0, tmp);
            break;
        case 3:
            gen_helper_iwmmxt_sraq(cpu_M0, tcg_env, cpu_M0, tmp);
            break;
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        gen_op_iwmmxt_set_cup();
        break;
    case 0x104: case 0x504: case 0x904: case 0xd04:     /* WSLL */
    case 0x114: case 0x514: case 0x914: case 0xd14:
        if (((insn >> 22) & 3) == 0)
            return 1;
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        tmp = tcg_temp_new_i32();
        if (gen_iwmmxt_shift(insn, 0xff, tmp)) {
            return 1;
        }
        switch ((insn >> 22) & 3) {
        case 1:
            gen_helper_iwmmxt_sllw(cpu_M0, tcg_env, cpu_M0, tmp);
            break;
        case 2:
            gen_helper_iwmmxt_slll(cpu_M0, tcg_env, cpu_M0, tmp);
            break;
        case 3:
            gen_helper_iwmmxt_sllq(cpu_M0, tcg_env, cpu_M0, tmp);
            break;
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        gen_op_iwmmxt_set_cup();
        break;
    case 0x304: case 0x704: case 0xb04: case 0xf04:     /* WROR */
    case 0x314: case 0x714: case 0xb14: case 0xf14:
        if (((insn >> 22) & 3) == 0)
            return 1;
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        tmp = tcg_temp_new_i32();
        switch ((insn >> 22) & 3) {
        case 1:
            if (gen_iwmmxt_shift(insn, 0xf, tmp)) {
                return 1;
            }
            gen_helper_iwmmxt_rorw(cpu_M0, tcg_env, cpu_M0, tmp);
            break;
        case 2:
            if (gen_iwmmxt_shift(insn, 0x1f, tmp)) {
                return 1;
            }
            gen_helper_iwmmxt_rorl(cpu_M0, tcg_env, cpu_M0, tmp);
            break;
        case 3:
            if (gen_iwmmxt_shift(insn, 0x3f, tmp)) {
                return 1;
            }
            gen_helper_iwmmxt_rorq(cpu_M0, tcg_env, cpu_M0, tmp);
            break;
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        gen_op_iwmmxt_set_cup();
        break;
    case 0x116: case 0x316: case 0x516: case 0x716:     /* WMIN */
    case 0x916: case 0xb16: case 0xd16: case 0xf16:
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        rd1 = (insn >> 0) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        switch ((insn >> 22) & 3) {
        case 0:
            if (insn & (1 << 21))
                gen_op_iwmmxt_minsb_M0_wRn(rd1);
            else
                gen_op_iwmmxt_minub_M0_wRn(rd1);
            break;
        case 1:
            if (insn & (1 << 21))
                gen_op_iwmmxt_minsw_M0_wRn(rd1);
            else
                gen_op_iwmmxt_minuw_M0_wRn(rd1);
            break;
        case 2:
            if (insn & (1 << 21))
                gen_op_iwmmxt_minsl_M0_wRn(rd1);
            else
                gen_op_iwmmxt_minul_M0_wRn(rd1);
            break;
        case 3:
            return 1;
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        break;
    case 0x016: case 0x216: case 0x416: case 0x616:     /* WMAX */
    case 0x816: case 0xa16: case 0xc16: case 0xe16:
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        rd1 = (insn >> 0) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        switch ((insn >> 22) & 3) {
        case 0:
            if (insn & (1 << 21))
                gen_op_iwmmxt_maxsb_M0_wRn(rd1);
            else
                gen_op_iwmmxt_maxub_M0_wRn(rd1);
            break;
        case 1:
            if (insn & (1 << 21))
                gen_op_iwmmxt_maxsw_M0_wRn(rd1);
            else
                gen_op_iwmmxt_maxuw_M0_wRn(rd1);
            break;
        case 2:
            if (insn & (1 << 21))
                gen_op_iwmmxt_maxsl_M0_wRn(rd1);
            else
                gen_op_iwmmxt_maxul_M0_wRn(rd1);
            break;
        case 3:
            return 1;
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        break;
    case 0x002: case 0x102: case 0x202: case 0x302:     /* WALIGNI */
    case 0x402: case 0x502: case 0x602: case 0x702:
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        rd1 = (insn >> 0) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        iwmmxt_load_reg(cpu_V1, rd1);
        gen_helper_iwmmxt_align(cpu_M0, cpu_M0, cpu_V1,
                                tcg_constant_i32((insn >> 20) & 3));
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        break;
    case 0x01a: case 0x11a: case 0x21a: case 0x31a:     /* WSUB */
    case 0x41a: case 0x51a: case 0x61a: case 0x71a:
    case 0x81a: case 0x91a: case 0xa1a: case 0xb1a:
    case 0xc1a: case 0xd1a: case 0xe1a: case 0xf1a:
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        rd1 = (insn >> 0) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        switch ((insn >> 20) & 0xf) {
        case 0x0:
            gen_op_iwmmxt_subnb_M0_wRn(rd1);
            break;
        case 0x1:
            gen_op_iwmmxt_subub_M0_wRn(rd1);
            break;
        case 0x3:
            gen_op_iwmmxt_subsb_M0_wRn(rd1);
            break;
        case 0x4:
            gen_op_iwmmxt_subnw_M0_wRn(rd1);
            break;
        case 0x5:
            gen_op_iwmmxt_subuw_M0_wRn(rd1);
            break;
        case 0x7:
            gen_op_iwmmxt_subsw_M0_wRn(rd1);
            break;
        case 0x8:
            gen_op_iwmmxt_subnl_M0_wRn(rd1);
            break;
        case 0x9:
            gen_op_iwmmxt_subul_M0_wRn(rd1);
            break;
        case 0xb:
            gen_op_iwmmxt_subsl_M0_wRn(rd1);
            break;
        default:
            return 1;
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        gen_op_iwmmxt_set_cup();
        break;
    case 0x01e: case 0x11e: case 0x21e: case 0x31e:     /* WSHUFH */
    case 0x41e: case 0x51e: case 0x61e: case 0x71e:
    case 0x81e: case 0x91e: case 0xa1e: case 0xb1e:
    case 0xc1e: case 0xd1e: case 0xe1e: case 0xf1e:
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        tmp = tcg_constant_i32(((insn >> 16) & 0xf0) | (insn & 0x0f));
        gen_helper_iwmmxt_shufh(cpu_M0, tcg_env, cpu_M0, tmp);
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        gen_op_iwmmxt_set_cup();
        break;
    case 0x018: case 0x118: case 0x218: case 0x318:     /* WADD */
    case 0x418: case 0x518: case 0x618: case 0x718:
    case 0x818: case 0x918: case 0xa18: case 0xb18:
    case 0xc18: case 0xd18: case 0xe18: case 0xf18:
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        rd1 = (insn >> 0) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        switch ((insn >> 20) & 0xf) {
        case 0x0:
            gen_op_iwmmxt_addnb_M0_wRn(rd1);
            break;
        case 0x1:
            gen_op_iwmmxt_addub_M0_wRn(rd1);
            break;
        case 0x3:
            gen_op_iwmmxt_addsb_M0_wRn(rd1);
            break;
        case 0x4:
            gen_op_iwmmxt_addnw_M0_wRn(rd1);
            break;
        case 0x5:
            gen_op_iwmmxt_adduw_M0_wRn(rd1);
            break;
        case 0x7:
            gen_op_iwmmxt_addsw_M0_wRn(rd1);
            break;
        case 0x8:
            gen_op_iwmmxt_addnl_M0_wRn(rd1);
            break;
        case 0x9:
            gen_op_iwmmxt_addul_M0_wRn(rd1);
            break;
        case 0xb:
            gen_op_iwmmxt_addsl_M0_wRn(rd1);
            break;
        default:
            return 1;
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        gen_op_iwmmxt_set_cup();
        break;
    case 0x008: case 0x108: case 0x208: case 0x308:     /* WPACK */
    case 0x408: case 0x508: case 0x608: case 0x708:
    case 0x808: case 0x908: case 0xa08: case 0xb08:
    case 0xc08: case 0xd08: case 0xe08: case 0xf08:
        if (!(insn & (1 << 20)) || ((insn >> 22) & 3) == 0)
            return 1;
        wrd = (insn >> 12) & 0xf;
        rd0 = (insn >> 16) & 0xf;
        rd1 = (insn >> 0) & 0xf;
        gen_op_iwmmxt_movq_M0_wRn(rd0);
        switch ((insn >> 22) & 3) {
        case 1:
            if (insn & (1 << 21))
                gen_op_iwmmxt_packsw_M0_wRn(rd1);
            else
                gen_op_iwmmxt_packuw_M0_wRn(rd1);
            break;
        case 2:
            if (insn & (1 << 21))
                gen_op_iwmmxt_packsl_M0_wRn(rd1);
            else
                gen_op_iwmmxt_packul_M0_wRn(rd1);
            break;
        case 3:
            if (insn & (1 << 21))
                gen_op_iwmmxt_packsq_M0_wRn(rd1);
            else
                gen_op_iwmmxt_packuq_M0_wRn(rd1);
            break;
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        gen_op_iwmmxt_set_cup();
        break;
    case 0x201: case 0x203: case 0x205: case 0x207:
    case 0x209: case 0x20b: case 0x20d: case 0x20f:
    case 0x211: case 0x213: case 0x215: case 0x217:
    case 0x219: case 0x21b: case 0x21d: case 0x21f:
        wrd = (insn >> 5) & 0xf;
        rd0 = (insn >> 12) & 0xf;
        rd1 = (insn >> 0) & 0xf;
        if (rd0 == 0xf || rd1 == 0xf)
            return 1;
        gen_op_iwmmxt_movq_M0_wRn(wrd);
        tmp = load_reg(s, rd0);
        tmp2 = load_reg(s, rd1);
        switch ((insn >> 16) & 0xf) {
        case 0x0:                                       /* TMIA */
            gen_helper_iwmmxt_muladdsl(cpu_M0, cpu_M0, tmp, tmp2);
            break;
        case 0x8:                                       /* TMIAPH */
            gen_helper_iwmmxt_muladdsw(cpu_M0, cpu_M0, tmp, tmp2);
            break;
        case 0xc: case 0xd: case 0xe: case 0xf:                 /* TMIAxy */
            if (insn & (1 << 16))
                tcg_gen_shri_i32(tmp, tmp, 16);
            if (insn & (1 << 17))
                tcg_gen_shri_i32(tmp2, tmp2, 16);
            gen_helper_iwmmxt_muladdswl(cpu_M0, cpu_M0, tmp, tmp2);
            break;
        default:
            return 1;
        }
        gen_op_iwmmxt_movq_wRn_M0(wrd);
        gen_op_iwmmxt_set_mup();
        break;
    default:
        return 1;
    }

    return 0;
}

/* Disassemble an XScale DSP instruction.  Returns nonzero if an error occurred
   (ie. an undefined instruction).  */
static int disas_dsp_insn(DisasContext *s, uint32_t insn)
{
    int acc, rd0, rd1, rdhi, rdlo;
    TCGv_i32 tmp, tmp2;

    if ((insn & 0x0ff00f10) == 0x0e200010) {
        /* Multiply with Internal Accumulate Format */
        rd0 = (insn >> 12) & 0xf;
        rd1 = insn & 0xf;
        acc = (insn >> 5) & 7;

        if (acc != 0)
            return 1;

        tmp = load_reg(s, rd0);
        tmp2 = load_reg(s, rd1);
        switch ((insn >> 16) & 0xf) {
        case 0x0:                                       /* MIA */
            gen_helper_iwmmxt_muladdsl(cpu_M0, cpu_M0, tmp, tmp2);
            break;
        case 0x8:                                       /* MIAPH */
            gen_helper_iwmmxt_muladdsw(cpu_M0, cpu_M0, tmp, tmp2);
            break;
        case 0xc:                                       /* MIABB */
        case 0xd:                                       /* MIABT */
        case 0xe:                                       /* MIATB */
        case 0xf:                                       /* MIATT */
            if (insn & (1 << 16))
                tcg_gen_shri_i32(tmp, tmp, 16);
            if (insn & (1 << 17))
                tcg_gen_shri_i32(tmp2, tmp2, 16);
            gen_helper_iwmmxt_muladdswl(cpu_M0, cpu_M0, tmp, tmp2);
            break;
        default:
            return 1;
        }

        gen_op_iwmmxt_movq_wRn_M0(acc);
        return 0;
    }

    if ((insn & 0x0fe00ff8) == 0x0c400000) {
        /* Internal Accumulator Access Format */
        rdhi = (insn >> 16) & 0xf;
        rdlo = (insn >> 12) & 0xf;
        acc = insn & 7;

        if (acc != 0)
            return 1;

        if (insn & ARM_CP_RW_BIT) {                     /* MRA */
            iwmmxt_load_reg(cpu_V0, acc);
            tcg_gen_extrl_i64_i32(cpu_R[rdlo], cpu_V0);
            tcg_gen_extrh_i64_i32(cpu_R[rdhi], cpu_V0);
            tcg_gen_andi_i32(cpu_R[rdhi], cpu_R[rdhi], (1 << (40 - 32)) - 1);
        } else {                                        /* MAR */
            tcg_gen_concat_i32_i64(cpu_V0, cpu_R[rdlo], cpu_R[rdhi]);
            iwmmxt_store_reg(cpu_V0, acc);
        }
        return 0;
    }

    return 1;
}

static void gen_goto_ptr(void)
{
    tcg_gen_lookup_and_goto_ptr();
}

/* This will end the TB but doesn't guarantee we'll return to
 * cpu_loop_exec. Any live exit_requests will be processed as we
 * enter the next TB.
 */
static void gen_goto_tb(DisasContext *s, int n, target_long diff)
{
    if (translator_use_goto_tb(&s->base, s->pc_curr + diff)) {
        /*
         * For pcrel, the pc must always be up-to-date on entry to
         * the linked TB, so that it can use simple additions for all
         * further adjustments.  For !pcrel, the linked TB is compiled
         * to know its full virtual address, so we can delay the
         * update to pc to the unlinked path.  A long chain of links
         * can thus avoid many updates to the PC.
         */
        if (tb_cflags(s->base.tb) & CF_PCREL) {
            gen_update_pc(s, diff);
            tcg_gen_goto_tb(n);
        } else {
            tcg_gen_goto_tb(n);
            gen_update_pc(s, diff);
        }
        tcg_gen_exit_tb(s->base.tb, n);
    } else {
        gen_update_pc(s, diff);
        gen_goto_ptr();
    }
    s->base.is_jmp = DISAS_NORETURN;
}

/* Jump, specifying which TB number to use if we gen_goto_tb() */
static void gen_jmp_tb(DisasContext *s, target_long diff, int tbno)
{
    if (unlikely(s->ss_active)) {
        /* An indirect jump so that we still trigger the debug exception.  */
        gen_update_pc(s, diff);
        s->base.is_jmp = DISAS_JUMP;
        return;
    }
    switch (s->base.is_jmp) {
    case DISAS_NEXT:
    case DISAS_TOO_MANY:
    case DISAS_NORETURN:
        /*
         * The normal case: just go to the destination TB.
         * NB: NORETURN happens if we generate code like
         *    gen_brcondi(l);
         *    gen_jmp();
         *    gen_set_label(l);
         *    gen_jmp();
         * on the second call to gen_jmp().
         */
        gen_goto_tb(s, tbno, diff);
        break;
    case DISAS_UPDATE_NOCHAIN:
    case DISAS_UPDATE_EXIT:
        /*
         * We already decided we're leaving the TB for some other reason.
         * Avoid using goto_tb so we really do exit back to the main loop
         * and don't chain to another TB.
         */
        gen_update_pc(s, diff);
        gen_goto_ptr();
        s->base.is_jmp = DISAS_NORETURN;
        break;
    default:
        /*
         * We shouldn't be emitting code for a jump and also have
         * is_jmp set to one of the special cases like DISAS_SWI.
         */
        g_assert_not_reached();
    }
}

static inline void gen_jmp(DisasContext *s, target_long diff)
{
    gen_jmp_tb(s, diff, 0);
}

static inline void gen_mulxy(TCGv_i32 t0, TCGv_i32 t1, int x, int y)
{
    if (x)
        tcg_gen_sari_i32(t0, t0, 16);
    else
        gen_sxth(t0);
    if (y)
        tcg_gen_sari_i32(t1, t1, 16);
    else
        gen_sxth(t1);
    tcg_gen_mul_i32(t0, t0, t1);
}

/* Return the mask of PSR bits set by a MSR instruction.  */
static uint32_t msr_mask(DisasContext *s, int flags, int spsr)
{
    uint32_t mask = 0;

    if (flags & (1 << 0)) {
        mask |= 0xff;
    }
    if (flags & (1 << 1)) {
        mask |= 0xff00;
    }
    if (flags & (1 << 2)) {
        mask |= 0xff0000;
    }
    if (flags & (1 << 3)) {
        mask |= 0xff000000;
    }

    /* Mask out undefined and reserved bits.  */
    mask &= aarch32_cpsr_valid_mask(s->features, s->isar);

    /* Mask out execution state.  */
    if (!spsr) {
        mask &= ~CPSR_EXEC;
    }

    /* Mask out privileged bits.  */
    if (IS_USER(s)) {
        mask &= CPSR_USER;
    }
    return mask;
}

/* Returns nonzero if access to the PSR is not permitted. Marks t0 as dead. */
static int gen_set_psr(DisasContext *s, uint32_t mask, int spsr, TCGv_i32 t0)
{
    TCGv_i32 tmp;
    if (spsr) {
        /* ??? This is also undefined in system mode.  */
        if (IS_USER(s))
            return 1;

        tmp = load_cpu_field(spsr);
        tcg_gen_andi_i32(tmp, tmp, ~mask);
        tcg_gen_andi_i32(t0, t0, mask);
        tcg_gen_or_i32(tmp, tmp, t0);
        store_cpu_field(tmp, spsr);
    } else {
        gen_set_cpsr(t0, mask);
    }
    gen_lookup_tb(s);
    return 0;
}

/* Returns nonzero if access to the PSR is not permitted.  */
static int gen_set_psr_im(DisasContext *s, uint32_t mask, int spsr, uint32_t val)
{
    TCGv_i32 tmp;
    tmp = tcg_temp_new_i32();
    tcg_gen_movi_i32(tmp, val);
    return gen_set_psr(s, mask, spsr, tmp);
}

static bool msr_banked_access_decode(DisasContext *s, int r, int sysm, int rn,
                                     int *tgtmode, int *regno)
{
    /* Decode the r and sysm fields of MSR/MRS banked accesses into
     * the target mode and register number, and identify the various
     * unpredictable cases.
     * MSR (banked) and MRS (banked) are CONSTRAINED UNPREDICTABLE if:
     *  + executed in user mode
     *  + using R15 as the src/dest register
     *  + accessing an unimplemented register
     *  + accessing a register that's inaccessible at current PL/security state*
     *  + accessing a register that you could access with a different insn
     * We choose to UNDEF in all these cases.
     * Since we don't know which of the various AArch32 modes we are in
     * we have to defer some checks to runtime.
     * Accesses to Monitor mode registers from Secure EL1 (which implies
     * that EL3 is AArch64) must trap to EL3.
     *
     * If the access checks fail this function will emit code to take
     * an exception and return false. Otherwise it will return true,
     * and set *tgtmode and *regno appropriately.
     */
    /* These instructions are present only in ARMv8, or in ARMv7 with the
     * Virtualization Extensions.
     */
    if (!arm_dc_feature(s, ARM_FEATURE_V8) &&
        !arm_dc_feature(s, ARM_FEATURE_EL2)) {
        goto undef;
    }

    if (IS_USER(s) || rn == 15) {
        goto undef;
    }

    /* The table in the v8 ARM ARM section F5.2.3 describes the encoding
     * of registers into (r, sysm).
     */
    if (r) {
        /* SPSRs for other modes */
        switch (sysm) {
        case 0xe: /* SPSR_fiq */
            *tgtmode = ARM_CPU_MODE_FIQ;
            break;
        case 0x10: /* SPSR_irq */
            *tgtmode = ARM_CPU_MODE_IRQ;
            break;
        case 0x12: /* SPSR_svc */
            *tgtmode = ARM_CPU_MODE_SVC;
            break;
        case 0x14: /* SPSR_abt */
            *tgtmode = ARM_CPU_MODE_ABT;
            break;
        case 0x16: /* SPSR_und */
            *tgtmode = ARM_CPU_MODE_UND;
            break;
        case 0x1c: /* SPSR_mon */
            *tgtmode = ARM_CPU_MODE_MON;
            break;
        case 0x1e: /* SPSR_hyp */
            *tgtmode = ARM_CPU_MODE_HYP;
            break;
        default: /* unallocated */
            goto undef;
        }
        /* We arbitrarily assign SPSR a register number of 16. */
        *regno = 16;
    } else {
        /* general purpose registers for other modes */
        switch (sysm) {
        case 0x0 ... 0x6:   /* 0b00xxx : r8_usr ... r14_usr */
            *tgtmode = ARM_CPU_MODE_USR;
            *regno = sysm + 8;
            break;
        case 0x8 ... 0xe:   /* 0b01xxx : r8_fiq ... r14_fiq */
            *tgtmode = ARM_CPU_MODE_FIQ;
            *regno = sysm;
            break;
        case 0x10 ... 0x11: /* 0b1000x : r14_irq, r13_irq */
            *tgtmode = ARM_CPU_MODE_IRQ;
            *regno = sysm & 1 ? 13 : 14;
            break;
        case 0x12 ... 0x13: /* 0b1001x : r14_svc, r13_svc */
            *tgtmode = ARM_CPU_MODE_SVC;
            *regno = sysm & 1 ? 13 : 14;
            break;
        case 0x14 ... 0x15: /* 0b1010x : r14_abt, r13_abt */
            *tgtmode = ARM_CPU_MODE_ABT;
            *regno = sysm & 1 ? 13 : 14;
            break;
        case 0x16 ... 0x17: /* 0b1011x : r14_und, r13_und */
            *tgtmode = ARM_CPU_MODE_UND;
            *regno = sysm & 1 ? 13 : 14;
            break;
        case 0x1c ... 0x1d: /* 0b1110x : r14_mon, r13_mon */
            *tgtmode = ARM_CPU_MODE_MON;
            *regno = sysm & 1 ? 13 : 14;
            break;
        case 0x1e ... 0x1f: /* 0b1111x : elr_hyp, r13_hyp */
            *tgtmode = ARM_CPU_MODE_HYP;
            /* Arbitrarily pick 17 for ELR_Hyp (which is not a banked LR!) */
            *regno = sysm & 1 ? 13 : 17;
            break;
        default: /* unallocated */
            goto undef;
        }
    }

    /* Catch the 'accessing inaccessible register' cases we can detect
     * at translate time.
     */
    switch (*tgtmode) {
    case ARM_CPU_MODE_MON:
        if (!arm_dc_feature(s, ARM_FEATURE_EL3) || s->ns) {
            goto undef;
        }
        if (s->current_el == 1) {
            /* If we're in Secure EL1 (which implies that EL3 is AArch64)
             * then accesses to Mon registers trap to Secure EL2, if it exists,
             * otherwise EL3.
             */
            TCGv_i32 tcg_el;

            if (arm_dc_feature(s, ARM_FEATURE_AARCH64) &&
                dc_isar_feature(aa64_sel2, s)) {
                /* Target EL is EL<3 minus SCR_EL3.EEL2> */
                tcg_el = load_cpu_field_low32(cp15.scr_el3);
                tcg_gen_sextract_i32(tcg_el, tcg_el, ctz32(SCR_EEL2), 1);
                tcg_gen_addi_i32(tcg_el, tcg_el, 3);
            } else {
                tcg_el = tcg_constant_i32(3);
            }

            gen_exception_insn_el_v(s, 0, EXCP_UDEF,
                                    syn_uncategorized(), tcg_el);
            return false;
        }
        break;
    case ARM_CPU_MODE_HYP:
        /*
         * r13_hyp can only be accessed from Monitor mode, and so we
         * can forbid accesses from EL2 or below.
         * elr_hyp can be accessed also from Hyp mode, so forbid
         * accesses from EL0 or EL1.
         * SPSR_hyp is supposed to be in the same category as r13_hyp
         * and UNPREDICTABLE if accessed from anything except Monitor
         * mode. However there is some real-world code that will do
         * it because at least some hardware happens to permit the
         * access. (Notably a standard Cortex-R52 startup code fragment
         * does this.) So we permit SPSR_hyp from Hyp mode also, to allow
         * this (incorrect) guest code to run.
         */
        if (!arm_dc_feature(s, ARM_FEATURE_EL2) || s->current_el < 2
            || (s->current_el < 3 && *regno != 16 && *regno != 17)) {
            goto undef;
        }
        break;
    default:
        break;
    }

    return true;

undef:
    /* If we get here then some access check did not pass */
    gen_exception_insn(s, 0, EXCP_UDEF, syn_uncategorized());
    return false;
}

static void gen_msr_banked(DisasContext *s, int r, int sysm, int rn)
{
    TCGv_i32 tcg_reg;
    int tgtmode = 0, regno = 0;

    if (!msr_banked_access_decode(s, r, sysm, rn, &tgtmode, &regno)) {
        return;
    }

    /* Sync state because msr_banked() can raise exceptions */
    gen_set_condexec(s);
    gen_update_pc(s, 0);
    tcg_reg = load_reg(s, rn);
    gen_helper_msr_banked(tcg_env, tcg_reg,
                          tcg_constant_i32(tgtmode),
                          tcg_constant_i32(regno));
    s->base.is_jmp = DISAS_UPDATE_EXIT;
}

static void gen_mrs_banked(DisasContext *s, int r, int sysm, int rn)
{
    TCGv_i32 tcg_reg;
    int tgtmode = 0, regno = 0;

    if (!msr_banked_access_decode(s, r, sysm, rn, &tgtmode, &regno)) {
        return;
    }

    /* Sync state because mrs_banked() can raise exceptions */
    gen_set_condexec(s);
    gen_update_pc(s, 0);
    tcg_reg = tcg_temp_new_i32();
    gen_helper_mrs_banked(tcg_reg, tcg_env,
                          tcg_constant_i32(tgtmode),
                          tcg_constant_i32(regno));
    store_reg(s, rn, tcg_reg);
    s->base.is_jmp = DISAS_UPDATE_EXIT;
}

/* Store value to PC as for an exception return (ie don't
 * mask bits). The subsequent call to gen_helper_cpsr_write_eret()
 * will do the masking based on the new value of the Thumb bit.
 */
static void store_pc_exc_ret(DisasContext *s, TCGv_i32 pc)
{
    tcg_gen_mov_i32(cpu_R[15], pc);
}

/* Generate a v6 exception return.  Marks both values as dead.  */
static void gen_rfe(DisasContext *s, TCGv_i32 pc, TCGv_i32 cpsr)
{
    store_pc_exc_ret(s, pc);
    /* The cpsr_write_eret helper will mask the low bits of PC
     * appropriately depending on the new Thumb bit, so it must
     * be called after storing the new PC.
     */
    translator_io_start(&s->base);
    gen_helper_cpsr_write_eret(tcg_env, cpsr);
    /* Must exit loop to check un-masked IRQs */
    s->base.is_jmp = DISAS_EXIT;
}

/* Generate an old-style exception return. Marks pc as dead. */
static void gen_exception_return(DisasContext *s, TCGv_i32 pc)
{
    gen_rfe(s, pc, load_cpu_field(spsr));
}

static bool aa32_cpreg_encoding_in_impdef_space(uint8_t crn, uint8_t crm)
{
    static const uint16_t mask[3] = {
        0b0000000111100111,  /* crn ==  9, crm == {c0-c2, c5-c8}   */
        0b0000000100010011,  /* crn == 10, crm == {c0, c1, c4, c8} */
        0b1000000111111111,  /* crn == 11, crm == {c0-c8, c15}     */
    };

    if (crn >= 9 && crn <= 11) {
        return (mask[crn - 9] >> crm) & 1;
    }
    return false;
}

static void do_coproc_insn(DisasContext *s, int cpnum, int is64,
                           int opc1, int crn, int crm, int opc2,
                           bool isread, int rt, int rt2)
{
    uint32_t key = ENCODE_CP_REG(cpnum, is64, s->ns, crn, crm, opc1, opc2);
    const ARMCPRegInfo *ri = get_arm_cp_reginfo(s->cp_regs, key);
    TCGv_ptr tcg_ri = NULL;
    bool need_exit_tb = false;
    uint32_t syndrome;

    /*
     * Note that since we are an implementation which takes an
     * exception on a trapped conditional instruction only if the
     * instruction passes its condition code check, we can take
     * advantage of the clause in the ARM ARM that allows us to set
     * the COND field in the instruction to 0xE in all cases.
     * We could fish the actual condition out of the insn (ARM)
     * or the condexec bits (Thumb) but it isn't necessary.
     */
    switch (cpnum) {
    case 14:
        if (is64) {
            syndrome = syn_cp14_rrt_trap(1, 0xe, opc1, crm, rt, rt2,
                                         isread, false);
        } else {
            syndrome = syn_cp14_rt_trap(1, 0xe, opc1, opc2, crn, crm,
                                        rt, isread, false);
        }
        break;
    case 15:
        if (is64) {
            syndrome = syn_cp15_rrt_trap(1, 0xe, opc1, crm, rt, rt2,
                                         isread, false);
        } else {
            syndrome = syn_cp15_rt_trap(1, 0xe, opc1, opc2, crn, crm,
                                        rt, isread, false);
        }
        break;
    default:
        /*
         * ARMv8 defines that only coprocessors 14 and 15 exist,
         * so this can only happen if this is an ARMv7 or earlier CPU,
         * in which case the syndrome information won't actually be
         * guest visible.
         */
        assert(!arm_dc_feature(s, ARM_FEATURE_V8));
        syndrome = syn_uncategorized();
        break;
    }

    if (s->hstr_active && cpnum == 15 && s->current_el == 1) {
        /*
         * At EL1, check for a HSTR_EL2 trap, which must take precedence
         * over the UNDEF for "no such register" or the UNDEF for "access
         * permissions forbid this EL1 access". HSTR_EL2 traps from EL0
         * only happen if the cpreg doesn't UNDEF at EL0, so we do those in
         * access_check_cp_reg(), after the checks for whether the access
         * configurably trapped to EL1.
         */
        uint32_t maskbit = is64 ? crm : crn;

        if (maskbit != 4 && maskbit != 14) {
            /* T4 and T14 are RES0 so never cause traps */
            TCGv_i32 t;
            DisasLabel over = gen_disas_label(s);

            t = load_cpu_offset(offsetoflow32(CPUARMState, cp15.hstr_el2));
            tcg_gen_andi_i32(t, t, 1u << maskbit);
            tcg_gen_brcondi_i32(TCG_COND_EQ, t, 0, over.label);

            gen_exception_insn_el(s, 0, EXCP_UDEF, syndrome, 2);
            /*
             * gen_exception_insn() will set is_jmp to DISAS_NORETURN,
             * but since we're conditionally branching over it, we want
             * to assume continue-to-next-instruction.
             */
            s->base.is_jmp = DISAS_NEXT;
            set_disas_label(s, over);
        }
    }

    if (cpnum == 15 && aa32_cpreg_encoding_in_impdef_space(crn, crm)) {
        /*
         * Check for TIDCP trap, which must take precedence over the UNDEF
         * for "no such register" etc.  It shares precedence with HSTR,
         * but raises the same exception, so order doesn't matter.
         */
        switch (s->current_el) {
        case 0:
            if (arm_dc_feature(s, ARM_FEATURE_AARCH64)
                && dc_isar_feature(aa64_tidcp1, s)) {
                gen_helper_tidcp_el0(tcg_env, tcg_constant_i32(syndrome));
            }
            break;
        case 1:
            gen_helper_tidcp_el1(tcg_env, tcg_constant_i32(syndrome));
            break;
        }
    }

    if (!ri) {
        /*
         * Unknown register; this might be a guest error or a QEMU
         * unimplemented feature.
         */
        if (is64) {
            qemu_log_mask(LOG_UNIMP, "%s access to unsupported AArch32 "
                          "64 bit system register cp:%d opc1: %d crm:%d "
                          "(%s)\n",
                          isread ? "read" : "write", cpnum, opc1, crm,
                          s->ns ? "non-secure" : "secure");
        } else {
            qemu_log_mask(LOG_UNIMP, "%s access to unsupported AArch32 "
                          "system register cp:%d opc1:%d crn:%d crm:%d "
                          "opc2:%d (%s)\n",
                          isread ? "read" : "write", cpnum, opc1, crn,
                          crm, opc2, s->ns ? "non-secure" : "secure");
        }
        unallocated_encoding(s);
        return;
    }

    /* Check access permissions */
    if (!cp_access_ok(s->current_el, ri, isread)) {
        unallocated_encoding(s);
        return;
    }

    if ((s->hstr_active && s->current_el == 0) || ri->accessfn ||
        (ri->fgt && s->fgt_active) ||
        (arm_dc_feature(s, ARM_FEATURE_XSCALE) && cpnum < 14)) {
        /*
         * Emit code to perform further access permissions checks at
         * runtime; this may result in an exception.
         * Note that on XScale all cp0..c13 registers do an access check
         * call in order to handle c15_cpar.
         */
        gen_set_condexec(s);
        gen_update_pc(s, 0);
        tcg_ri = tcg_temp_new_ptr();
        gen_helper_access_check_cp_reg(tcg_ri, tcg_env,
                                       tcg_constant_i32(key),
                                       tcg_constant_i32(syndrome),
                                       tcg_constant_i32(isread));
    } else if (ri->type & ARM_CP_RAISES_EXC) {
        /*
         * The readfn or writefn might raise an exception;
         * synchronize the CPU state in case it does.
         */
        gen_set_condexec(s);
        gen_update_pc(s, 0);
    }

    /* Handle special cases first */
    switch (ri->type & ARM_CP_SPECIAL_MASK) {
    case 0:
        break;
    case ARM_CP_NOP:
        return;
    case ARM_CP_WFI:
        if (isread) {
            unallocated_encoding(s);
        } else {
            gen_update_pc(s, curr_insn_len(s));
            s->base.is_jmp = DISAS_WFI;
        }
        return;
    default:
        g_assert_not_reached();
    }

    if (ri->type & ARM_CP_IO) {
        /* I/O operations must end the TB here (whether read or write) */
        need_exit_tb = translator_io_start(&s->base);
    }

    if (isread) {
        /* Read */
        if (is64) {
            TCGv_i64 tmp64;
            TCGv_i32 tmp;
            if (ri->type & ARM_CP_CONST) {
                tmp64 = tcg_constant_i64(ri->resetvalue);
            } else if (ri->readfn) {
                if (!tcg_ri) {
                    tcg_ri = gen_lookup_cp_reg(key);
                }
                tmp64 = tcg_temp_new_i64();
                gen_helper_get_cp_reg64(tmp64, tcg_env, tcg_ri);
            } else {
                tmp64 = tcg_temp_new_i64();
                tcg_gen_ld_i64(tmp64, tcg_env, ri->fieldoffset);
            }
            tmp = tcg_temp_new_i32();
            tcg_gen_extrl_i64_i32(tmp, tmp64);
            store_reg(s, rt, tmp);
            tmp = tcg_temp_new_i32();
            tcg_gen_extrh_i64_i32(tmp, tmp64);
            store_reg(s, rt2, tmp);
        } else {
            TCGv_i32 tmp;
            if (ri->type & ARM_CP_CONST) {
                tmp = tcg_constant_i32(ri->resetvalue);
            } else if (ri->readfn) {
                if (!tcg_ri) {
                    tcg_ri = gen_lookup_cp_reg(key);
                }
                tmp = tcg_temp_new_i32();
                gen_helper_get_cp_reg(tmp, tcg_env, tcg_ri);
            } else {
                tmp = load_cpu_offset(ri->fieldoffset);
            }
            if (rt == 15) {
                /* Destination register of r15 for 32 bit loads sets
                 * the condition codes from the high 4 bits of the value
                 */
                gen_set_nzcv(tmp);
            } else {
                store_reg(s, rt, tmp);
            }
        }
    } else {
        /* Write */
        if (ri->type & ARM_CP_CONST) {
            /* If not forbidden by access permissions, treat as WI */
            return;
        }

        if (is64) {
            TCGv_i32 tmplo, tmphi;
            TCGv_i64 tmp64 = tcg_temp_new_i64();
            tmplo = load_reg(s, rt);
            tmphi = load_reg(s, rt2);
            tcg_gen_concat_i32_i64(tmp64, tmplo, tmphi);
            if (ri->writefn) {
                if (!tcg_ri) {
                    tcg_ri = gen_lookup_cp_reg(key);
                }
                gen_helper_set_cp_reg64(tcg_env, tcg_ri, tmp64);
            } else {
                tcg_gen_st_i64(tmp64, tcg_env, ri->fieldoffset);
            }
        } else {
            TCGv_i32 tmp = load_reg(s, rt);
            if (ri->writefn) {
                if (!tcg_ri) {
                    tcg_ri = gen_lookup_cp_reg(key);
                }
                gen_helper_set_cp_reg(tcg_env, tcg_ri, tmp);
            } else {
                store_cpu_offset(tmp, ri->fieldoffset, 4);
            }
        }
    }

    if (!isread && !(ri->type & ARM_CP_SUPPRESS_TB_END)) {
        /*
         * A write to any coprocessor register that ends a TB
         * must rebuild the hflags for the next TB.
         */
        gen_rebuild_hflags(s, ri->type & ARM_CP_NEWEL);
        /*
         * We default to ending the TB on a coprocessor register write,
         * but allow this to be suppressed by the register definition
         * (usually only necessary to work around guest bugs).
         */
        need_exit_tb = true;
    }
    if (need_exit_tb) {
        gen_lookup_tb(s);
    }
}

/* Decode XScale DSP or iWMMXt insn (in the copro space, cp=0 or 1) */
static void disas_xscale_insn(DisasContext *s, uint32_t insn)
{
    int cpnum = (insn >> 8) & 0xf;

    if (extract32(s->c15_cpar, cpnum, 1) == 0) {
        unallocated_encoding(s);
    } else if (arm_dc_feature(s, ARM_FEATURE_IWMMXT)) {
        if (disas_iwmmxt_insn(s, insn)) {
            unallocated_encoding(s);
        }
    } else if (arm_dc_feature(s, ARM_FEATURE_XSCALE)) {
        if (disas_dsp_insn(s, insn)) {
            unallocated_encoding(s);
        }
    }
}

/* Store a 64-bit value to a register pair.  Clobbers val.  */
static void gen_storeq_reg(DisasContext *s, int rlow, int rhigh, TCGv_i64 val)
{
    TCGv_i32 tmp;
    tmp = tcg_temp_new_i32();
    tcg_gen_extrl_i64_i32(tmp, val);
    store_reg(s, rlow, tmp);
    tmp = tcg_temp_new_i32();
    tcg_gen_extrh_i64_i32(tmp, val);
    store_reg(s, rhigh, tmp);
}

/* load and add a 64-bit value from a register pair.  */
static void gen_addq(DisasContext *s, TCGv_i64 val, int rlow, int rhigh)
{
    TCGv_i64 tmp;
    TCGv_i32 tmpl;
    TCGv_i32 tmph;

    /* Load 64-bit value rd:rn.  */
    tmpl = load_reg(s, rlow);
    tmph = load_reg(s, rhigh);
    tmp = tcg_temp_new_i64();
    tcg_gen_concat_i32_i64(tmp, tmpl, tmph);
    tcg_gen_add_i64(val, val, tmp);
}

/* Set N and Z flags from hi|lo.  */
static void gen_logicq_cc(TCGv_i32 lo, TCGv_i32 hi)
{
    tcg_gen_mov_i32(cpu_NF, hi);
    tcg_gen_or_i32(cpu_ZF, lo, hi);
}

/* Load/Store exclusive instructions are implemented by remembering
   the value/address loaded, and seeing if these are the same
   when the store is performed.  This should be sufficient to implement
   the architecturally mandated semantics, and avoids having to monitor
   regular stores.  The compare vs the remembered value is done during
   the cmpxchg operation, but we must compare the addresses manually.  */
static void gen_load_exclusive(DisasContext *s, int rt, int rt2,
                               TCGv_i32 addr, int size)
{
    TCGv_i32 tmp = tcg_temp_new_i32();
    MemOp opc = size | MO_ALIGN | s->be_data;

    s->is_ldex = true;

    if (size == 3) {
        TCGv_i32 tmp2 = tcg_temp_new_i32();
        TCGv_i64 t64 = tcg_temp_new_i64();

        /*
         * For AArch32, architecturally the 32-bit word at the lowest
         * address is always Rt and the one at addr+4 is Rt2, even if
         * the CPU is big-endian. That means we don't want to do a
         * gen_aa32_ld_i64(), which checks SCTLR_B as if for an
         * architecturally 64-bit access, but instead do a 64-bit access
         * using MO_BE if appropriate and then split the two halves.
         */
        TCGv taddr = gen_aa32_addr(s, addr, opc);

        tcg_gen_qemu_ld_i64(t64, taddr, get_mem_index(s), opc);
        tcg_gen_mov_i64(cpu_exclusive_val, t64);
        if (s->be_data == MO_BE) {
            tcg_gen_extr_i64_i32(tmp2, tmp, t64);
        } else {
            tcg_gen_extr_i64_i32(tmp, tmp2, t64);
        }
        store_reg(s, rt2, tmp2);
    } else {
        gen_aa32_ld_i32(s, tmp, addr, get_mem_index(s), opc);
        tcg_gen_extu_i32_i64(cpu_exclusive_val, tmp);
    }

    store_reg(s, rt, tmp);
    tcg_gen_extu_i32_i64(cpu_exclusive_addr, addr);
}

static void gen_clrex(DisasContext *s)
{
    tcg_gen_movi_i64(cpu_exclusive_addr, -1);
}

static void gen_store_exclusive(DisasContext *s, int rd, int rt, int rt2,
                                TCGv_i32 addr, int size)
{
    TCGv_i32 t0, t1, t2;
    TCGv_i64 extaddr;
    TCGv taddr;
    TCGLabel *done_label;
    TCGLabel *fail_label;
    MemOp opc = size | MO_ALIGN | s->be_data;

    /* if (env->exclusive_addr == addr && env->exclusive_val == [addr]) {
         [addr] = {Rt};
         {Rd} = 0;
       } else {
         {Rd} = 1;
       } */
    fail_label = gen_new_label();
    done_label = gen_new_label();
    extaddr = tcg_temp_new_i64();
    tcg_gen_extu_i32_i64(extaddr, addr);
    tcg_gen_brcond_i64(TCG_COND_NE, extaddr, cpu_exclusive_addr, fail_label);

    taddr = gen_aa32_addr(s, addr, opc);
    t0 = tcg_temp_new_i32();
    t1 = load_reg(s, rt);
    if (size == 3) {
        TCGv_i64 o64 = tcg_temp_new_i64();
        TCGv_i64 n64 = tcg_temp_new_i64();

        t2 = load_reg(s, rt2);

        /*
         * For AArch32, architecturally the 32-bit word at the lowest
         * address is always Rt and the one at addr+4 is Rt2, even if
         * the CPU is big-endian. Since we're going to treat this as a
         * single 64-bit BE store, we need to put the two halves in the
         * opposite order for BE to LE, so that they end up in the right
         * places.  We don't want gen_aa32_st_i64, because that checks
         * SCTLR_B as if for an architectural 64-bit access.
         */
        if (s->be_data == MO_BE) {
            tcg_gen_concat_i32_i64(n64, t2, t1);
        } else {
            tcg_gen_concat_i32_i64(n64, t1, t2);
        }

        tcg_gen_atomic_cmpxchg_i64(o64, taddr, cpu_exclusive_val, n64,
                                   get_mem_index(s), opc);

        tcg_gen_setcond_i64(TCG_COND_NE, o64, o64, cpu_exclusive_val);
        tcg_gen_extrl_i64_i32(t0, o64);
    } else {
        t2 = tcg_temp_new_i32();
        tcg_gen_extrl_i64_i32(t2, cpu_exclusive_val);
        tcg_gen_atomic_cmpxchg_i32(t0, taddr, t2, t1, get_mem_index(s), opc);
        tcg_gen_setcond_i32(TCG_COND_NE, t0, t0, t2);
    }
    tcg_gen_mov_i32(cpu_R[rd], t0);
    tcg_gen_br(done_label);

    gen_set_label(fail_label);
    tcg_gen_movi_i32(cpu_R[rd], 1);
    gen_set_label(done_label);
    tcg_gen_movi_i64(cpu_exclusive_addr, -1);
}

/* gen_srs:
 * @env: CPUARMState
 * @s: DisasContext
 * @mode: mode field from insn (which stack to store to)
 * @amode: addressing mode (DA/IA/DB/IB), encoded as per P,U bits in ARM insn
 * @writeback: true if writeback bit set
 *
 * Generate code for the SRS (Store Return State) insn.
 */
static void gen_srs(DisasContext *s,
                    uint32_t mode, uint32_t amode, bool writeback)
{
    int32_t offset;
    TCGv_i32 addr, tmp;
    bool undef = false;

    /* SRS is:
     * - trapped to EL3 if EL3 is AArch64 and we are at Secure EL1
     *   and specified mode is monitor mode
     * - UNDEFINED in Hyp mode
     * - UNPREDICTABLE in User or System mode
     * - UNPREDICTABLE if the specified mode is:
     * -- not implemented
     * -- not a valid mode number
     * -- a mode that's at a higher exception level
     * -- Monitor, if we are Non-secure
     * For the UNPREDICTABLE cases we choose to UNDEF.
     */
    if (s->current_el == 1 && !s->ns && mode == ARM_CPU_MODE_MON) {
        gen_exception_insn_el(s, 0, EXCP_UDEF, syn_uncategorized(), 3);
        return;
    }

    if (s->current_el == 0 || s->current_el == 2) {
        undef = true;
    }

    switch (mode) {
    case ARM_CPU_MODE_USR:
    case ARM_CPU_MODE_FIQ:
    case ARM_CPU_MODE_IRQ:
    case ARM_CPU_MODE_SVC:
    case ARM_CPU_MODE_ABT:
    case ARM_CPU_MODE_UND:
    case ARM_CPU_MODE_SYS:
        break;
    case ARM_CPU_MODE_HYP:
        if (s->current_el == 1 || !arm_dc_feature(s, ARM_FEATURE_EL2)) {
            undef = true;
        }
        break;
    case ARM_CPU_MODE_MON:
        /* No need to check specifically for "are we non-secure" because
         * we've already made EL0 UNDEF and handled the trap for S-EL1;
         * so if this isn't EL3 then we must be non-secure.
         */
        if (s->current_el != 3) {
            undef = true;
        }
        break;
    default:
        undef = true;
    }

    if (undef) {
        unallocated_encoding(s);
        return;
    }

    addr = tcg_temp_new_i32();
    /* get_r13_banked() will raise an exception if called from System mode */
    gen_set_condexec(s);
    gen_update_pc(s, 0);
    gen_helper_get_r13_banked(addr, tcg_env, tcg_constant_i32(mode));
    switch (amode) {
    case 0: /* DA */
        offset = -4;
        break;
    case 1: /* IA */
        offset = 0;
        break;
    case 2: /* DB */
        offset = -8;
        break;
    case 3: /* IB */
        offset = 4;
        break;
    default:
        g_assert_not_reached();
    }
    tcg_gen_addi_i32(addr, addr, offset);
    tmp = load_reg(s, 14);
    gen_aa32_st_i32(s, tmp, addr, get_mem_index(s), MO_UL | MO_ALIGN);
    tmp = load_cpu_field(spsr);
    tcg_gen_addi_i32(addr, addr, 4);
    gen_aa32_st_i32(s, tmp, addr, get_mem_index(s), MO_UL | MO_ALIGN);
    if (writeback) {
        switch (amode) {
        case 0:
            offset = -8;
            break;
        case 1:
            offset = 4;
            break;
        case 2:
            offset = -4;
            break;
        case 3:
            offset = 0;
            break;
        default:
            g_assert_not_reached();
        }
        tcg_gen_addi_i32(addr, addr, offset);
        gen_helper_set_r13_banked(tcg_env, tcg_constant_i32(mode), addr);
    }
    s->base.is_jmp = DISAS_UPDATE_EXIT;
}

/* Skip this instruction if the ARM condition is false */
static void arm_skip_unless(DisasContext *s, uint32_t cond)
{
    arm_gen_condlabel(s);
    arm_gen_test_cc(cond ^ 1, s->condlabel.label);
}


/*
 * Constant expanders used by T16/T32 decode
 */

/* Return only the rotation part of T32ExpandImm.  */
static int t32_expandimm_rot(DisasContext *s, int x)
{
    return x & 0xc00 ? extract32(x, 7, 5) : 0;
}

/* Return the unrotated immediate from T32ExpandImm.  */
static int t32_expandimm_imm(DisasContext *s, int x)
{
    int imm = extract32(x, 0, 8);

    switch (extract32(x, 8, 4)) {
    case 0: /* XY */
        /* Nothing to do.  */
        break;
    case 1: /* 00XY00XY */
        imm *= 0x00010001;
        break;
    case 2: /* XY00XY00 */
        imm *= 0x01000100;
        break;
    case 3: /* XYXYXYXY */
        imm *= 0x01010101;
        break;
    default:
        /* Rotated constant.  */
        imm |= 0x80;
        break;
    }
    return imm;
}

static int t32_branch24(DisasContext *s, int x)
{
    /* Convert J1:J2 at x[22:21] to I2:I1, which involves I=J^~S.  */
    x ^= !(x < 0) * (3 << 21);
    /* Append the final zero.  */
    return x << 1;
}

static int t16_setflags(DisasContext *s)
{
    return s->condexec_mask == 0;
}

static int t16_push_list(DisasContext *s, int x)
{
    return (x & 0xff) | (x & 0x100) << (14 - 8);
}

static int t16_pop_list(DisasContext *s, int x)
{
    return (x & 0xff) | (x & 0x100) << (15 - 8);
}

/*
 * Include the generated decoders.
 */

#include "decode-a32.c.inc"
#include "decode-a32-uncond.c.inc"
#include "decode-t32.c.inc"
#include "decode-t16.c.inc"

static bool valid_cp(DisasContext *s, int cp)
{
    /*
     * Return true if this coprocessor field indicates something
     * that's really a possible coprocessor.
     * For v7 and earlier, coprocessors 8..15 were reserved for Arm use,
     * and of those only cp14 and cp15 were used for registers.
     * cp10 and cp11 were used for VFP and Neon, whose decode is
     * dealt with elsewhere. With the advent of fp16, cp9 is also
     * now part of VFP.
     * For v8A and later, the encoding has been tightened so that
     * only cp14 and cp15 are valid, and other values aren't considered
     * to be in the coprocessor-instruction space at all. v8M still
     * permits coprocessors 0..7.
     * For XScale, we must not decode the XScale cp0, cp1 space as
     * a standard coprocessor insn, because we want to fall through to
     * the legacy disas_xscale_insn() decoder after decodetree is done.
     */
    if (arm_dc_feature(s, ARM_FEATURE_XSCALE) && (cp == 0 || cp == 1)) {
        return false;
    }

    if (arm_dc_feature(s, ARM_FEATURE_V8) &&
        !arm_dc_feature(s, ARM_FEATURE_M)) {
        return cp >= 14;
    }
    return cp < 8 || cp >= 14;
}

static bool trans_MCR(DisasContext *s, arg_MCR *a)
{
    if (!valid_cp(s, a->cp)) {
        return false;
    }
    do_coproc_insn(s, a->cp, false, a->opc1, a->crn, a->crm, a->opc2,
                   false, a->rt, 0);
    return true;
}

static bool trans_MRC(DisasContext *s, arg_MRC *a)
{
    if (!valid_cp(s, a->cp)) {
        return false;
    }
    do_coproc_insn(s, a->cp, false, a->opc1, a->crn, a->crm, a->opc2,
                   true, a->rt, 0);
    return true;
}

static bool trans_MCRR(DisasContext *s, arg_MCRR *a)
{
    if (!valid_cp(s, a->cp)) {
        return false;
    }
    do_coproc_insn(s, a->cp, true, a->opc1, 0, a->crm, 0,
                   false, a->rt, a->rt2);
    return true;
}

static bool trans_MRRC(DisasContext *s, arg_MRRC *a)
{
    if (!valid_cp(s, a->cp)) {
        return false;
    }
    do_coproc_insn(s, a->cp, true, a->opc1, 0, a->crm, 0,
                   true, a->rt, a->rt2);
    return true;
}

/* Helpers to swap operands for reverse-subtract.  */
static void gen_rsb(TCGv_i32 dst, TCGv_i32 a, TCGv_i32 b)
{
    tcg_gen_sub_i32(dst, b, a);
}

static void gen_rsb_CC(TCGv_i32 dst, TCGv_i32 a, TCGv_i32 b)
{
    gen_sub_CC(dst, b, a);
}

static void gen_rsc(TCGv_i32 dest, TCGv_i32 a, TCGv_i32 b)
{
    gen_sub_carry(dest, b, a);
}

static void gen_rsc_CC(TCGv_i32 dest, TCGv_i32 a, TCGv_i32 b)
{
    gen_sbc_CC(dest, b, a);
}

/*
 * Helpers for the data processing routines.
 *
 * After the computation store the results back.
 * This may be suppressed altogether (STREG_NONE), require a runtime
 * check against the stack limits (STREG_SP_CHECK), or generate an
 * exception return.  Oh, or store into a register.
 *
 * Always return true, indicating success for a trans_* function.
 */
typedef enum {
   STREG_NONE,
   STREG_NORMAL,
   STREG_SP_CHECK,
   STREG_EXC_RET,
} StoreRegKind;

static bool store_reg_kind(DisasContext *s, int rd,
                            TCGv_i32 val, StoreRegKind kind)
{
    switch (kind) {
    case STREG_NONE:
        return true;
    case STREG_NORMAL:
        /* See ALUWritePC: Interworking only from a32 mode. */
        if (s->thumb) {
            store_reg(s, rd, val);
        } else {
            store_reg_bx(s, rd, val);
        }
        return true;
    case STREG_SP_CHECK:
        store_sp_checked(s, val);
        return true;
    case STREG_EXC_RET:
        gen_exception_return(s, val);
        return true;
    }
    g_assert_not_reached();
}

/*
 * Data Processing (register)
 *
 * Operate, with set flags, one register source,
 * one immediate shifted register source, and a destination.
 */
static bool op_s_rrr_shi(DisasContext *s, arg_s_rrr_shi *a,
                         void (*gen)(TCGv_i32, TCGv_i32, TCGv_i32),
                         int logic_cc, StoreRegKind kind)
{
    TCGv_i32 tmp1, tmp2;

    tmp2 = load_reg(s, a->rm);
    gen_arm_shift_im(tmp2, a->shty, a->shim, logic_cc);
    tmp1 = load_reg(s, a->rn);

    gen(tmp1, tmp1, tmp2);

    if (logic_cc) {
        gen_logic_CC(tmp1);
    }
    return store_reg_kind(s, a->rd, tmp1, kind);
}

static bool op_s_rxr_shi(DisasContext *s, arg_s_rrr_shi *a,
                         void (*gen)(TCGv_i32, TCGv_i32),
                         int logic_cc, StoreRegKind kind)
{
    TCGv_i32 tmp;

    tmp = load_reg(s, a->rm);
    gen_arm_shift_im(tmp, a->shty, a->shim, logic_cc);

    gen(tmp, tmp);
    if (logic_cc) {
        gen_logic_CC(tmp);
    }
    return store_reg_kind(s, a->rd, tmp, kind);
}

/*
 * Data-processing (register-shifted register)
 *
 * Operate, with set flags, one register source,
 * one register shifted register source, and a destination.
 */
static bool op_s_rrr_shr(DisasContext *s, arg_s_rrr_shr *a,
                         void (*gen)(TCGv_i32, TCGv_i32, TCGv_i32),
                         int logic_cc, StoreRegKind kind)
{
    TCGv_i32 tmp1, tmp2;

    tmp1 = load_reg(s, a->rs);
    tmp2 = load_reg(s, a->rm);
    gen_arm_shift_reg(tmp2, a->shty, tmp1, logic_cc);
    tmp1 = load_reg(s, a->rn);

    gen(tmp1, tmp1, tmp2);

    if (logic_cc) {
        gen_logic_CC(tmp1);
    }
    return store_reg_kind(s, a->rd, tmp1, kind);
}

static bool op_s_rxr_shr(DisasContext *s, arg_s_rrr_shr *a,
                         void (*gen)(TCGv_i32, TCGv_i32),
                         int logic_cc, StoreRegKind kind)
{
    TCGv_i32 tmp1, tmp2;

    tmp1 = load_reg(s, a->rs);
    tmp2 = load_reg(s, a->rm);
    gen_arm_shift_reg(tmp2, a->shty, tmp1, logic_cc);

    gen(tmp2, tmp2);
    if (logic_cc) {
        gen_logic_CC(tmp2);
    }
    return store_reg_kind(s, a->rd, tmp2, kind);
}

/*
 * Data-processing (immediate)
 *
 * Operate, with set flags, one register source,
 * one rotated immediate, and a destination.
 *
 * Note that logic_cc && a->rot setting CF based on the msb of the
 * immediate is the reason why we must pass in the unrotated form
 * of the immediate.
 */
static bool op_s_rri_rot(DisasContext *s, arg_s_rri_rot *a,
                         void (*gen)(TCGv_i32, TCGv_i32, TCGv_i32),
                         int logic_cc, StoreRegKind kind)
{
    TCGv_i32 tmp1;
    uint32_t imm;

    imm = ror32(a->imm, a->rot);
    if (logic_cc && a->rot) {
        tcg_gen_movi_i32(cpu_CF, imm >> 31);
    }
    tmp1 = load_reg(s, a->rn);

    gen(tmp1, tmp1, tcg_constant_i32(imm));

    if (logic_cc) {
        gen_logic_CC(tmp1);
    }
    return store_reg_kind(s, a->rd, tmp1, kind);
}

static bool op_s_rxi_rot(DisasContext *s, arg_s_rri_rot *a,
                         void (*gen)(TCGv_i32, TCGv_i32),
                         int logic_cc, StoreRegKind kind)
{
    TCGv_i32 tmp;
    uint32_t imm;

    imm = ror32(a->imm, a->rot);
    if (logic_cc && a->rot) {
        tcg_gen_movi_i32(cpu_CF, imm >> 31);
    }

    tmp = tcg_temp_new_i32();
    gen(tmp, tcg_constant_i32(imm));

    if (logic_cc) {
        gen_logic_CC(tmp);
    }
    return store_reg_kind(s, a->rd, tmp, kind);
}

#define DO_ANY3(NAME, OP, L, K)                                         \
    static bool trans_##NAME##_rrri(DisasContext *s, arg_s_rrr_shi *a)  \
    { StoreRegKind k = (K); return op_s_rrr_shi(s, a, OP, L, k); }      \
    static bool trans_##NAME##_rrrr(DisasContext *s, arg_s_rrr_shr *a)  \
    { StoreRegKind k = (K); return op_s_rrr_shr(s, a, OP, L, k); }      \
    static bool trans_##NAME##_rri(DisasContext *s, arg_s_rri_rot *a)   \
    { StoreRegKind k = (K); return op_s_rri_rot(s, a, OP, L, k); }

#define DO_ANY2(NAME, OP, L, K)                                         \
    static bool trans_##NAME##_rxri(DisasContext *s, arg_s_rrr_shi *a)  \
    { StoreRegKind k = (K); return op_s_rxr_shi(s, a, OP, L, k); }      \
    static bool trans_##NAME##_rxrr(DisasContext *s, arg_s_rrr_shr *a)  \
    { StoreRegKind k = (K); return op_s_rxr_shr(s, a, OP, L, k); }      \
    static bool trans_##NAME##_rxi(DisasContext *s, arg_s_rri_rot *a)   \
    { StoreRegKind k = (K); return op_s_rxi_rot(s, a, OP, L, k); }

#define DO_CMP2(NAME, OP, L)                                            \
    static bool trans_##NAME##_xrri(DisasContext *s, arg_s_rrr_shi *a)  \
    { return op_s_rrr_shi(s, a, OP, L, STREG_NONE); }                   \
    static bool trans_##NAME##_xrrr(DisasContext *s, arg_s_rrr_shr *a)  \
    { return op_s_rrr_shr(s, a, OP, L, STREG_NONE); }                   \
    static bool trans_##NAME##_xri(DisasContext *s, arg_s_rri_rot *a)   \
    { return op_s_rri_rot(s, a, OP, L, STREG_NONE); }

DO_ANY3(AND, tcg_gen_and_i32, a->s, STREG_NORMAL)
DO_ANY3(EOR, tcg_gen_xor_i32, a->s, STREG_NORMAL)
DO_ANY3(ORR, tcg_gen_or_i32, a->s, STREG_NORMAL)
DO_ANY3(BIC, tcg_gen_andc_i32, a->s, STREG_NORMAL)

DO_ANY3(RSB, a->s ? gen_rsb_CC : gen_rsb, false, STREG_NORMAL)
DO_ANY3(ADC, a->s ? gen_adc_CC : gen_add_carry, false, STREG_NORMAL)
DO_ANY3(SBC, a->s ? gen_sbc_CC : gen_sub_carry, false, STREG_NORMAL)
DO_ANY3(RSC, a->s ? gen_rsc_CC : gen_rsc, false, STREG_NORMAL)

DO_CMP2(TST, tcg_gen_and_i32, true)
DO_CMP2(TEQ, tcg_gen_xor_i32, true)
DO_CMP2(CMN, gen_add_CC, false)
DO_CMP2(CMP, gen_sub_CC, false)

DO_ANY3(ADD, a->s ? gen_add_CC : tcg_gen_add_i32, false,
        a->rd == 13 && a->rn == 13 ? STREG_SP_CHECK : STREG_NORMAL)

/*
 * Note for the computation of StoreRegKind we return out of the
 * middle of the functions that are expanded by DO_ANY3, and that
 * we modify a->s via that parameter before it is used by OP.
 */
DO_ANY3(SUB, a->s ? gen_sub_CC : tcg_gen_sub_i32, false,
        ({
            StoreRegKind ret = STREG_NORMAL;
            if (a->rd == 15 && a->s) {
                /*
                 * See ALUExceptionReturn:
                 * In User mode, UNPREDICTABLE; we choose UNDEF.
                 * In Hyp mode, UNDEFINED.
                 */
                if (IS_USER(s) || s->current_el == 2) {
                    unallocated_encoding(s);
                    return true;
                }
                /* There is no writeback of nzcv to PSTATE.  */
                a->s = 0;
                ret = STREG_EXC_RET;
            } else if (a->rd == 13 && a->rn == 13) {
                ret = STREG_SP_CHECK;
            }
            ret;
        }))

DO_ANY2(MOV, tcg_gen_mov_i32, a->s,
        ({
            StoreRegKind ret = STREG_NORMAL;
            if (a->rd == 15 && a->s) {
                /*
                 * See ALUExceptionReturn:
                 * In User mode, UNPREDICTABLE; we choose UNDEF.
                 * In Hyp mode, UNDEFINED.
                 */
                if (IS_USER(s) || s->current_el == 2) {
                    unallocated_encoding(s);
                    return true;
                }
                /* There is no writeback of nzcv to PSTATE.  */
                a->s = 0;
                ret = STREG_EXC_RET;
            } else if (a->rd == 13) {
                ret = STREG_SP_CHECK;
            }
            ret;
        }))

DO_ANY2(MVN, tcg_gen_not_i32, a->s, STREG_NORMAL)

/*
 * ORN is only available with T32, so there is no register-shifted-register
 * form of the insn.  Using the DO_ANY3 macro would create an unused function.
 */
static bool trans_ORN_rrri(DisasContext *s, arg_s_rrr_shi *a)
{
    return op_s_rrr_shi(s, a, tcg_gen_orc_i32, a->s, STREG_NORMAL);
}

static bool trans_ORN_rri(DisasContext *s, arg_s_rri_rot *a)
{
    return op_s_rri_rot(s, a, tcg_gen_orc_i32, a->s, STREG_NORMAL);
}

#undef DO_ANY3
#undef DO_ANY2
#undef DO_CMP2

static bool trans_ADR(DisasContext *s, arg_ri *a)
{
    store_reg_bx(s, a->rd, add_reg_for_lit(s, 15, a->imm));
    return true;
}

static bool trans_MOVW(DisasContext *s, arg_MOVW *a)
{
    if (!ENABLE_ARCH_6T2) {
        return false;
    }

    store_reg(s, a->rd, tcg_constant_i32(a->imm));
    return true;
}

static bool trans_MOVT(DisasContext *s, arg_MOVW *a)
{
    TCGv_i32 tmp;

    if (!ENABLE_ARCH_6T2) {
        return false;
    }

    tmp = load_reg(s, a->rd);
    tcg_gen_ext16u_i32(tmp, tmp);
    tcg_gen_ori_i32(tmp, tmp, a->imm << 16);
    store_reg(s, a->rd, tmp);
    return true;
}

/*
 * v8.1M MVE wide-shifts
 */
static bool do_mve_shl_ri(DisasContext *s, arg_mve_shl_ri *a,
                          WideShiftImmFn *fn)
{
    TCGv_i64 rda;
    TCGv_i32 rdalo, rdahi;

    if (!arm_dc_feature(s, ARM_FEATURE_V8_1M)) {
        /* Decode falls through to ORR/MOV UNPREDICTABLE handling */
        return false;
    }
    if (a->rdahi == 15) {
        /* These are a different encoding (SQSHL/SRSHR/UQSHL/URSHR) */
        return false;
    }
    if (!dc_isar_feature(aa32_mve, s) ||
        !arm_dc_feature(s, ARM_FEATURE_M_MAIN) ||
        a->rdahi == 13) {
        /* RdaHi == 13 is UNPREDICTABLE; we choose to UNDEF */
        unallocated_encoding(s);
        return true;
    }

    if (a->shim == 0) {
        a->shim = 32;
    }

    rda = tcg_temp_new_i64();
    rdalo = load_reg(s, a->rdalo);
    rdahi = load_reg(s, a->rdahi);
    tcg_gen_concat_i32_i64(rda, rdalo, rdahi);

    fn(rda, rda, a->shim);

    tcg_gen_extrl_i64_i32(rdalo, rda);
    tcg_gen_extrh_i64_i32(rdahi, rda);
    store_reg(s, a->rdalo, rdalo);
    store_reg(s, a->rdahi, rdahi);

    return true;
}

static bool trans_ASRL_ri(DisasContext *s, arg_mve_shl_ri *a)
{
    return do_mve_shl_ri(s, a, tcg_gen_sari_i64);
}

static bool trans_LSLL_ri(DisasContext *s, arg_mve_shl_ri *a)
{
    return do_mve_shl_ri(s, a, tcg_gen_shli_i64);
}

static bool trans_LSRL_ri(DisasContext *s, arg_mve_shl_ri *a)
{
    return do_mve_shl_ri(s, a, tcg_gen_shri_i64);
}

static void gen_mve_sqshll(TCGv_i64 r, TCGv_i64 n, int64_t shift)
{
    gen_helper_mve_sqshll(r, tcg_env, n, tcg_constant_i32(shift));
}

static bool trans_SQSHLL_ri(DisasContext *s, arg_mve_shl_ri *a)
{
    return do_mve_shl_ri(s, a, gen_mve_sqshll);
}

static void gen_mve_uqshll(TCGv_i64 r, TCGv_i64 n, int64_t shift)
{
    gen_helper_mve_uqshll(r, tcg_env, n, tcg_constant_i32(shift));
}

static bool trans_UQSHLL_ri(DisasContext *s, arg_mve_shl_ri *a)
{
    return do_mve_shl_ri(s, a, gen_mve_uqshll);
}

static bool trans_SRSHRL_ri(DisasContext *s, arg_mve_shl_ri *a)
{
    return do_mve_shl_ri(s, a, gen_srshr64_i64);
}

static bool trans_URSHRL_ri(DisasContext *s, arg_mve_shl_ri *a)
{
    return do_mve_shl_ri(s, a, gen_urshr64_i64);
}

static bool do_mve_shl_rr(DisasContext *s, arg_mve_shl_rr *a, WideShiftFn *fn)
{
    TCGv_i64 rda;
    TCGv_i32 rdalo, rdahi;

    if (!arm_dc_feature(s, ARM_FEATURE_V8_1M)) {
        /* Decode falls through to ORR/MOV UNPREDICTABLE handling */
        return false;
    }
    if (a->rdahi == 15) {
        /* These are a different encoding (SQSHL/SRSHR/UQSHL/URSHR) */
        return false;
    }
    if (!dc_isar_feature(aa32_mve, s) ||
        !arm_dc_feature(s, ARM_FEATURE_M_MAIN) ||
        a->rdahi == 13 || a->rm == 13 || a->rm == 15 ||
        a->rm == a->rdahi || a->rm == a->rdalo) {
        /* These rdahi/rdalo/rm cases are UNPREDICTABLE; we choose to UNDEF */
        unallocated_encoding(s);
        return true;
    }

    rda = tcg_temp_new_i64();
    rdalo = load_reg(s, a->rdalo);
    rdahi = load_reg(s, a->rdahi);
    tcg_gen_concat_i32_i64(rda, rdalo, rdahi);

    /* The helper takes care of the sign-extension of the low 8 bits of Rm */
    fn(rda, tcg_env, rda, cpu_R[a->rm]);

    tcg_gen_extrl_i64_i32(rdalo, rda);
    tcg_gen_extrh_i64_i32(rdahi, rda);
    store_reg(s, a->rdalo, rdalo);
    store_reg(s, a->rdahi, rdahi);

    return true;
}

static bool trans_LSLL_rr(DisasContext *s, arg_mve_shl_rr *a)
{
    return do_mve_shl_rr(s, a, gen_helper_mve_ushll);
}

static bool trans_ASRL_rr(DisasContext *s, arg_mve_shl_rr *a)
{
    return do_mve_shl_rr(s, a, gen_helper_mve_sshrl);
}

static bool trans_UQRSHLL64_rr(DisasContext *s, arg_mve_shl_rr *a)
{
    return do_mve_shl_rr(s, a, gen_helper_mve_uqrshll);
}

static bool trans_SQRSHRL64_rr(DisasContext *s, arg_mve_shl_rr *a)
{
    return do_mve_shl_rr(s, a, gen_helper_mve_sqrshrl);
}

static bool trans_UQRSHLL48_rr(DisasContext *s, arg_mve_shl_rr *a)
{
    return do_mve_shl_rr(s, a, gen_helper_mve_uqrshll48);
}

static bool trans_SQRSHRL48_rr(DisasContext *s, arg_mve_shl_rr *a)
{
    return do_mve_shl_rr(s, a, gen_helper_mve_sqrshrl48);
}

static bool do_mve_sh_ri(DisasContext *s, arg_mve_sh_ri *a, ShiftImmFn *fn)
{
    if (!arm_dc_feature(s, ARM_FEATURE_V8_1M)) {
        /* Decode falls through to ORR/MOV UNPREDICTABLE handling */
        return false;
    }
    if (!dc_isar_feature(aa32_mve, s) ||
        !arm_dc_feature(s, ARM_FEATURE_M_MAIN) ||
        a->rda == 13 || a->rda == 15) {
        /* These rda cases are UNPREDICTABLE; we choose to UNDEF */
        unallocated_encoding(s);
        return true;
    }

    if (a->shim == 0) {
        a->shim = 32;
    }
    fn(cpu_R[a->rda], cpu_R[a->rda], a->shim);

    return true;
}

static bool trans_URSHR_ri(DisasContext *s, arg_mve_sh_ri *a)
{
    return do_mve_sh_ri(s, a, gen_urshr32_i32);
}

static bool trans_SRSHR_ri(DisasContext *s, arg_mve_sh_ri *a)
{
    return do_mve_sh_ri(s, a, gen_srshr32_i32);
}

static void gen_mve_sqshl(TCGv_i32 r, TCGv_i32 n, int32_t shift)
{
    gen_helper_mve_sqshl(r, tcg_env, n, tcg_constant_i32(shift));
}

static bool trans_SQSHL_ri(DisasContext *s, arg_mve_sh_ri *a)
{
    return do_mve_sh_ri(s, a, gen_mve_sqshl);
}

static void gen_mve_uqshl(TCGv_i32 r, TCGv_i32 n, int32_t shift)
{
    gen_helper_mve_uqshl(r, tcg_env, n, tcg_constant_i32(shift));
}

static bool trans_UQSHL_ri(DisasContext *s, arg_mve_sh_ri *a)
{
    return do_mve_sh_ri(s, a, gen_mve_uqshl);
}

static bool do_mve_sh_rr(DisasContext *s, arg_mve_sh_rr *a, ShiftFn *fn)
{
    if (!arm_dc_feature(s, ARM_FEATURE_V8_1M)) {
        /* Decode falls through to ORR/MOV UNPREDICTABLE handling */
        return false;
    }
    if (!dc_isar_feature(aa32_mve, s) ||
        !arm_dc_feature(s, ARM_FEATURE_M_MAIN) ||
        a->rda == 13 || a->rda == 15 || a->rm == 13 || a->rm == 15 ||
        a->rm == a->rda) {
        /* These rda/rm cases are UNPREDICTABLE; we choose to UNDEF */
        unallocated_encoding(s);
        return true;
    }

    /* The helper takes care of the sign-extension of the low 8 bits of Rm */
    fn(cpu_R[a->rda], tcg_env, cpu_R[a->rda], cpu_R[a->rm]);
    return true;
}

static bool trans_SQRSHR_rr(DisasContext *s, arg_mve_sh_rr *a)
{
    return do_mve_sh_rr(s, a, gen_helper_mve_sqrshr);
}

static bool trans_UQRSHL_rr(DisasContext *s, arg_mve_sh_rr *a)
{
    return do_mve_sh_rr(s, a, gen_helper_mve_uqrshl);
}

/*
 * Multiply and multiply accumulate
 */

static bool op_mla(DisasContext *s, arg_s_rrrr *a, bool add)
{
    TCGv_i32 t1, t2;

    t1 = load_reg(s, a->rn);
    t2 = load_reg(s, a->rm);
    tcg_gen_mul_i32(t1, t1, t2);
    if (add) {
        t2 = load_reg(s, a->ra);
        tcg_gen_add_i32(t1, t1, t2);
    }
    if (a->s) {
        gen_logic_CC(t1);
    }
    store_reg(s, a->rd, t1);
    return true;
}

static bool trans_MUL(DisasContext *s, arg_MUL *a)
{
    return op_mla(s, a, false);
}

static bool trans_MLA(DisasContext *s, arg_MLA *a)
{
    return op_mla(s, a, true);
}

static bool trans_MLS(DisasContext *s, arg_MLS *a)
{
    TCGv_i32 t1, t2;

    if (!ENABLE_ARCH_6T2) {
        return false;
    }
    t1 = load_reg(s, a->rn);
    t2 = load_reg(s, a->rm);
    tcg_gen_mul_i32(t1, t1, t2);
    t2 = load_reg(s, a->ra);
    tcg_gen_sub_i32(t1, t2, t1);
    store_reg(s, a->rd, t1);
    return true;
}

static bool op_mlal(DisasContext *s, arg_s_rrrr *a, bool uns, bool add)
{
    TCGv_i32 t0, t1, t2, t3;

    t0 = load_reg(s, a->rm);
    t1 = load_reg(s, a->rn);
    if (uns) {
        tcg_gen_mulu2_i32(t0, t1, t0, t1);
    } else {
        tcg_gen_muls2_i32(t0, t1, t0, t1);
    }
    if (add) {
        t2 = load_reg(s, a->ra);
        t3 = load_reg(s, a->rd);
        tcg_gen_add2_i32(t0, t1, t0, t1, t2, t3);
    }
    if (a->s) {
        gen_logicq_cc(t0, t1);
    }
    store_reg(s, a->ra, t0);
    store_reg(s, a->rd, t1);
    return true;
}

static bool trans_UMULL(DisasContext *s, arg_UMULL *a)
{
    return op_mlal(s, a, true, false);
}

static bool trans_SMULL(DisasContext *s, arg_SMULL *a)
{
    return op_mlal(s, a, false, false);
}

static bool trans_UMLAL(DisasContext *s, arg_UMLAL *a)
{
    return op_mlal(s, a, true, true);
}

static bool trans_SMLAL(DisasContext *s, arg_SMLAL *a)
{
    return op_mlal(s, a, false, true);
}

static bool trans_UMAAL(DisasContext *s, arg_UMAAL *a)
{
    TCGv_i32 t0, t1, t2, zero;

    if (s->thumb
        ? !arm_dc_feature(s, ARM_FEATURE_THUMB_DSP)
        : !ENABLE_ARCH_6) {
        return false;
    }

    t0 = load_reg(s, a->rm);
    t1 = load_reg(s, a->rn);
    tcg_gen_mulu2_i32(t0, t1, t0, t1);
    zero = tcg_constant_i32(0);
    t2 = load_reg(s, a->ra);
    tcg_gen_add2_i32(t0, t1, t0, t1, t2, zero);
    t2 = load_reg(s, a->rd);
    tcg_gen_add2_i32(t0, t1, t0, t1, t2, zero);
    store_reg(s, a->ra, t0);
    store_reg(s, a->rd, t1);
    return true;
}

/*
 * Saturating addition and subtraction
 */

static bool op_qaddsub(DisasContext *s, arg_rrr *a, bool add, bool doub)
{
    TCGv_i32 t0, t1;

    if (s->thumb
        ? !arm_dc_feature(s, ARM_FEATURE_THUMB_DSP)
        : !ENABLE_ARCH_5TE) {
        return false;
    }

    t0 = load_reg(s, a->rm);
    t1 = load_reg(s, a->rn);
    if (doub) {
        gen_helper_add_saturate(t1, tcg_env, t1, t1);
    }
    if (add) {
        gen_helper_add_saturate(t0, tcg_env, t0, t1);
    } else {
        gen_helper_sub_saturate(t0, tcg_env, t0, t1);
    }
    store_reg(s, a->rd, t0);
    return true;
}

#define DO_QADDSUB(NAME, ADD, DOUB) \
static bool trans_##NAME(DisasContext *s, arg_rrr *a)    \
{                                                        \
    return op_qaddsub(s, a, ADD, DOUB);                  \
}

DO_QADDSUB(QADD, true, false)
DO_QADDSUB(QSUB, false, false)
DO_QADDSUB(QDADD, true, true)
DO_QADDSUB(QDSUB, false, true)

#undef DO_QADDSUB

/*
 * Halfword multiply and multiply accumulate
 */

static bool op_smlaxxx(DisasContext *s, arg_rrrr *a,
                       int add_long, bool nt, bool mt)
{
    TCGv_i32 t0, t1, tl, th;

    if (s->thumb
        ? !arm_dc_feature(s, ARM_FEATURE_THUMB_DSP)
        : !ENABLE_ARCH_5TE) {
        return false;
    }

    t0 = load_reg(s, a->rn);
    t1 = load_reg(s, a->rm);
    gen_mulxy(t0, t1, nt, mt);

    switch (add_long) {
    case 0:
        store_reg(s, a->rd, t0);
        break;
    case 1:
        t1 = load_reg(s, a->ra);
        gen_helper_add_setq(t0, tcg_env, t0, t1);
        store_reg(s, a->rd, t0);
        break;
    case 2:
        tl = load_reg(s, a->ra);
        th = load_reg(s, a->rd);
        /* Sign-extend the 32-bit product to 64 bits.  */
        t1 = tcg_temp_new_i32();
        tcg_gen_sari_i32(t1, t0, 31);
        tcg_gen_add2_i32(tl, th, tl, th, t0, t1);
        store_reg(s, a->ra, tl);
        store_reg(s, a->rd, th);
        break;
    default:
        g_assert_not_reached();
    }
    return true;
}

#define DO_SMLAX(NAME, add, nt, mt) \
static bool trans_##NAME(DisasContext *s, arg_rrrr *a)     \
{                                                          \
    return op_smlaxxx(s, a, add, nt, mt);                  \
}

DO_SMLAX(SMULBB, 0, 0, 0)
DO_SMLAX(SMULBT, 0, 0, 1)
DO_SMLAX(SMULTB, 0, 1, 0)
DO_SMLAX(SMULTT, 0, 1, 1)

DO_SMLAX(SMLABB, 1, 0, 0)
DO_SMLAX(SMLABT, 1, 0, 1)
DO_SMLAX(SMLATB, 1, 1, 0)
DO_SMLAX(SMLATT, 1, 1, 1)

DO_SMLAX(SMLALBB, 2, 0, 0)
DO_SMLAX(SMLALBT, 2, 0, 1)
DO_SMLAX(SMLALTB, 2, 1, 0)
DO_SMLAX(SMLALTT, 2, 1, 1)

#undef DO_SMLAX

static bool op_smlawx(DisasContext *s, arg_rrrr *a, bool add, bool mt)
{
    TCGv_i32 t0, t1;

    if (!ENABLE_ARCH_5TE) {
        return false;
    }

    t0 = load_reg(s, a->rn);
    t1 = load_reg(s, a->rm);
    /*
     * Since the nominal result is product<47:16>, shift the 16-bit
     * input up by 16 bits, so that the result is at product<63:32>.
     */
    if (mt) {
        tcg_gen_andi_i32(t1, t1, 0xffff0000);
    } else {
        tcg_gen_shli_i32(t1, t1, 16);
    }
    tcg_gen_muls2_i32(t0, t1, t0, t1);
    if (add) {
        t0 = load_reg(s, a->ra);
        gen_helper_add_setq(t1, tcg_env, t1, t0);
    }
    store_reg(s, a->rd, t1);
    return true;
}

#define DO_SMLAWX(NAME, add, mt) \
static bool trans_##NAME(DisasContext *s, arg_rrrr *a)     \
{                                                          \
    return op_smlawx(s, a, add, mt);                       \
}

DO_SMLAWX(SMULWB, 0, 0)
DO_SMLAWX(SMULWT, 0, 1)
DO_SMLAWX(SMLAWB, 1, 0)
DO_SMLAWX(SMLAWT, 1, 1)

#undef DO_SMLAWX

/*
 * MSR (immediate) and hints
 */

static bool trans_YIELD(DisasContext *s, arg_YIELD *a)
{
    /*
     * When running single-threaded TCG code, use the helper to ensure that
     * the next round-robin scheduled vCPU gets a crack.  When running in
     * MTTCG we don't generate jumps to the helper as it won't affect the
     * scheduling of other vCPUs.
     */
    if (!(tb_cflags(s->base.tb) & CF_PARALLEL)) {
        gen_update_pc(s, curr_insn_len(s));
        s->base.is_jmp = DISAS_YIELD;
    }
    return true;
}

static bool trans_WFE(DisasContext *s, arg_WFE *a)
{
    /*
     * When running single-threaded TCG code, use the helper to ensure that
     * the next round-robin scheduled vCPU gets a crack.  In MTTCG mode we
     * just skip this instruction.  Currently the SEV/SEVL instructions,
     * which are *one* of many ways to wake the CPU from WFE, are not
     * implemented so we can't sleep like WFI does.
     */
    if (!(tb_cflags(s->base.tb) & CF_PARALLEL)) {
        gen_update_pc(s, curr_insn_len(s));
        s->base.is_jmp = DISAS_WFE;
    }
    return true;
}

static bool trans_WFI(DisasContext *s, arg_WFI *a)
{
    /* For WFI, halt the vCPU until an IRQ. */
    gen_update_pc(s, curr_insn_len(s));
    s->base.is_jmp = DISAS_WFI;
    return true;
}

static bool trans_ESB(DisasContext *s, arg_ESB *a)
{
    /*
     * For M-profile, minimal-RAS ESB can be a NOP.
     * Without RAS, we must implement this as NOP.
     */
    if (!arm_dc_feature(s, ARM_FEATURE_M) && dc_isar_feature(aa32_ras, s)) {
        /*
         * QEMU does not have a source of physical SErrors,
         * so we are only concerned with virtual SErrors.
         * The pseudocode in the ARM for this case is
         *   if PSTATE.EL IN {EL0, EL1} && EL2Enabled() then
         *      AArch32.vESBOperation();
         * Most of the condition can be evaluated at translation time.
         * Test for EL2 present, and defer test for SEL2 to runtime.
         */
        if (s->current_el <= 1 && arm_dc_feature(s, ARM_FEATURE_EL2)) {
            gen_helper_vesb(tcg_env);
        }
    }
    return true;
}

static bool trans_NOP(DisasContext *s, arg_NOP *a)
{
    return true;
}

static bool trans_MSR_imm(DisasContext *s, arg_MSR_imm *a)
{
    uint32_t val = ror32(a->imm, a->rot * 2);
    uint32_t mask = msr_mask(s, a->mask, a->r);

    if (gen_set_psr_im(s, mask, a->r, val)) {
        unallocated_encoding(s);
    }
    return true;
}

/*
 * Cyclic Redundancy Check
 */

static bool op_crc32(DisasContext *s, arg_rrr *a, bool c, MemOp sz)
{
    TCGv_i32 t1, t2, t3;

    if (!dc_isar_feature(aa32_crc32, s)) {
        return false;
    }

    t1 = load_reg(s, a->rn);
    t2 = load_reg(s, a->rm);
    switch (sz) {
    case MO_8:
        gen_uxtb(t2);
        break;
    case MO_16:
        gen_uxth(t2);
        break;
    case MO_32:
        break;
    default:
        g_assert_not_reached();
    }
    t3 = tcg_constant_i32(1 << sz);
    if (c) {
        gen_helper_crc32c(t1, t1, t2, t3);
    } else {
        gen_helper_crc32(t1, t1, t2, t3);
    }
    store_reg(s, a->rd, t1);
    return true;
}

#define DO_CRC32(NAME, c, sz) \
static bool trans_##NAME(DisasContext *s, arg_rrr *a)  \
    { return op_crc32(s, a, c, sz); }

DO_CRC32(CRC32B, false, MO_8)
DO_CRC32(CRC32H, false, MO_16)
DO_CRC32(CRC32W, false, MO_32)
DO_CRC32(CRC32CB, true, MO_8)
DO_CRC32(CRC32CH, true, MO_16)
DO_CRC32(CRC32CW, true, MO_32)

#undef DO_CRC32

/*
 * Miscellaneous instructions
 */

static bool trans_MRS_bank(DisasContext *s, arg_MRS_bank *a)
{
    if (arm_dc_feature(s, ARM_FEATURE_M)) {
        return false;
    }
    gen_mrs_banked(s, a->r, a->sysm, a->rd);
    return true;
}

static bool trans_MSR_bank(DisasContext *s, arg_MSR_bank *a)
{
    if (arm_dc_feature(s, ARM_FEATURE_M)) {
        return false;
    }
    gen_msr_banked(s, a->r, a->sysm, a->rn);
    return true;
}

static bool trans_MRS_reg(DisasContext *s, arg_MRS_reg *a)
{
    TCGv_i32 tmp;

    if (arm_dc_feature(s, ARM_FEATURE_M)) {
        return false;
    }
    if (a->r) {
        if (IS_USER(s)) {
            unallocated_encoding(s);
            return true;
        }
        tmp = load_cpu_field(spsr);
    } else {
        tmp = tcg_temp_new_i32();
        gen_helper_cpsr_read(tmp, tcg_env);
    }
    store_reg(s, a->rd, tmp);
    return true;
}

static bool trans_MSR_reg(DisasContext *s, arg_MSR_reg *a)
{
    TCGv_i32 tmp;
    uint32_t mask = msr_mask(s, a->mask, a->r);

    if (arm_dc_feature(s, ARM_FEATURE_M)) {
        return false;
    }
    tmp = load_reg(s, a->rn);
    if (gen_set_psr(s, mask, a->r, tmp)) {
        unallocated_encoding(s);
    }
    return true;
}

static bool trans_MRS_v7m(DisasContext *s, arg_MRS_v7m *a)
{
    TCGv_i32 tmp;

    if (!arm_dc_feature(s, ARM_FEATURE_M)) {
        return false;
    }
    tmp = tcg_temp_new_i32();
    gen_helper_v7m_mrs(tmp, tcg_env, tcg_constant_i32(a->sysm));
    store_reg(s, a->rd, tmp);
    return true;
}

static bool trans_MSR_v7m(DisasContext *s, arg_MSR_v7m *a)
{
    TCGv_i32 addr, reg;

    if (!arm_dc_feature(s, ARM_FEATURE_M)) {
        return false;
    }
    addr = tcg_constant_i32((a->mask << 10) | a->sysm);
    reg = load_reg(s, a->rn);
    gen_helper_v7m_msr(tcg_env, addr, reg);
    /* If we wrote to CONTROL, the EL might have changed */
    gen_rebuild_hflags(s, true);
    gen_lookup_tb(s);
    return true;
}

static bool trans_BX(DisasContext *s, arg_BX *a)
{
    if (!ENABLE_ARCH_4T) {
        return false;
    }
    gen_bx_excret(s, load_reg(s, a->rm));
    return true;
}

static bool trans_BXJ(DisasContext *s, arg_BXJ *a)
{
    if (!ENABLE_ARCH_5J || arm_dc_feature(s, ARM_FEATURE_M)) {
        return false;
    }
    /*
     * v7A allows BXJ to be trapped via HSTR.TJDBX. We don't waste a
     * TBFLAGS bit on a basically-never-happens case, so call a helper
     * function to check for the trap and raise the exception if needed
     * (passing it the register number for the syndrome value).
     * v8A doesn't have this HSTR bit.
     */
    if (!arm_dc_feature(s, ARM_FEATURE_V8) &&
        arm_dc_feature(s, ARM_FEATURE_EL2) &&
        s->current_el < 2 && s->ns) {
        gen_helper_check_bxj_trap(tcg_env, tcg_constant_i32(a->rm));
    }
    /* Trivial implementation equivalent to bx.  */
    gen_bx(s, load_reg(s, a->rm));
    return true;
}

static bool trans_BLX_r(DisasContext *s, arg_BLX_r *a)
{
    TCGv_i32 tmp;

    if (!ENABLE_ARCH_5) {
        return false;
    }
    tmp = load_reg(s, a->rm);
    gen_pc_plus_diff(s, cpu_R[14], curr_insn_len(s) | s->thumb);
    gen_bx(s, tmp);
    return true;
}

/*
 * BXNS/BLXNS: only exist for v8M with the security extensions,
 * and always UNDEF if NonSecure.  We don't implement these in
 * the user-only mode either (in theory you can use them from
 * Secure User mode but they are too tied in to system emulation).
 */
static bool trans_BXNS(DisasContext *s, arg_BXNS *a)
{
    if (!s->v8m_secure || IS_USER_ONLY) {
        unallocated_encoding(s);
    } else {
        gen_bxns(s, a->rm);
    }
    return true;
}

static bool trans_BLXNS(DisasContext *s, arg_BLXNS *a)
{
    if (!s->v8m_secure || IS_USER_ONLY) {
        unallocated_encoding(s);
    } else {
        gen_blxns(s, a->rm);
    }
    return true;
}

static bool trans_CLZ(DisasContext *s, arg_CLZ *a)
{
    TCGv_i32 tmp;

    if (!ENABLE_ARCH_5) {
        return false;
    }
    tmp = load_reg(s, a->rm);
    tcg_gen_clzi_i32(tmp, tmp, 32);
    store_reg(s, a->rd, tmp);
    return true;
}

static bool trans_ERET(DisasContext *s, arg_ERET *a)
{
    TCGv_i32 tmp;

    if (!arm_dc_feature(s, ARM_FEATURE_V7VE)) {
        return false;
    }
    if (IS_USER(s)) {
        unallocated_encoding(s);
        return true;
    }
    if (s->current_el == 2) {
        /* ERET from Hyp uses ELR_Hyp, not LR */
        tmp = load_cpu_field_low32(elr_el[2]);
    } else {
        tmp = load_reg(s, 14);
    }
    gen_exception_return(s, tmp);
    return true;
}

static bool trans_HLT(DisasContext *s, arg_HLT *a)
{
    gen_hlt(s, a->imm);
    return true;
}

static bool trans_BKPT(DisasContext *s, arg_BKPT *a)
{
    if (!ENABLE_ARCH_5) {
        return false;
    }
    /* BKPT is OK with ECI set and leaves it untouched */
    s->eci_handled = true;
    if (arm_dc_feature(s, ARM_FEATURE_M) &&
        semihosting_enabled(s->current_el == 0) &&
        (a->imm == 0xab)) {
        gen_exception_internal_insn(s, EXCP_SEMIHOST);
    } else {
        gen_exception_bkpt_insn(s, syn_aa32_bkpt(a->imm, false));
    }
    return true;
}

static bool trans_HVC(DisasContext *s, arg_HVC *a)
{
    if (!ENABLE_ARCH_7 || arm_dc_feature(s, ARM_FEATURE_M)) {
        return false;
    }
    if (IS_USER(s)) {
        unallocated_encoding(s);
    } else {
        gen_hvc(s, a->imm);
    }
    return true;
}

static bool trans_SMC(DisasContext *s, arg_SMC *a)
{
    if (!ENABLE_ARCH_6K || arm_dc_feature(s, ARM_FEATURE_M)) {
        return false;
    }
    if (IS_USER(s)) {
        unallocated_encoding(s);
    } else {
        gen_smc(s);
    }
    return true;
}

static bool trans_SG(DisasContext *s, arg_SG *a)
{
    if (!arm_dc_feature(s, ARM_FEATURE_M) ||
        !arm_dc_feature(s, ARM_FEATURE_V8)) {
        return false;
    }
    /*
     * SG (v8M only)
     * The bulk of the behaviour for this instruction is implemented
     * in v7m_handle_execute_nsc(), which deals with the insn when
     * it is executed by a CPU in non-secure state from memory
     * which is Secure & NonSecure-Callable.
     * Here we only need to handle the remaining cases:
     *  * in NS memory (including the "security extension not
     *    implemented" case) : NOP
     *  * in S memory but CPU already secure (clear IT bits)
     * We know that the attribute for the memory this insn is
     * in must match the current CPU state, because otherwise
     * get_phys_addr_pmsav8 would have generated an exception.
     */
    if (s->v8m_secure) {
        /* Like the IT insn, we don't need to generate any code */
        s->condexec_cond = 0;
        s->condexec_mask = 0;
    }
    return true;
}

static bool trans_TT(DisasContext *s, arg_TT *a)
{
    TCGv_i32 addr, tmp;

    if (!arm_dc_feature(s, ARM_FEATURE_M) ||
        !arm_dc_feature(s, ARM_FEATURE_V8)) {
        return false;
    }
    if (a->rd == 13 || a->rd == 15 || a->rn == 15) {
        /* We UNDEF for these UNPREDICTABLE cases */
        unallocated_encoding(s);
        return true;
    }
    if (a->A && !s->v8m_secure) {
        /* This case is UNDEFINED.  */
        unallocated_encoding(s);
        return true;
    }

    addr = load_reg(s, a->rn);
    tmp = tcg_temp_new_i32();
    gen_helper_v7m_tt(tmp, tcg_env, addr, tcg_constant_i32((a->A << 1) | a->T));
    store_reg(s, a->rd, tmp);
    return true;
}

/*
 * Load/store register index
 */

static ISSInfo make_issinfo(DisasContext *s, int rd, bool p, bool w)
{
    ISSInfo ret;

    /* ISS not valid if writeback */
    if (p && !w) {
        ret = rd;
        if (curr_insn_len(s) == 2) {
            ret |= ISSIs16Bit;
        }
    } else {
        ret = ISSInvalid;
    }
    return ret;
}

static TCGv_i32 op_addr_rr_pre(DisasContext *s, arg_ldst_rr *a)
{
    TCGv_i32 addr = load_reg(s, a->rn);

    if (s->v8m_stackcheck && a->rn == 13 && a->w) {
        gen_helper_v8m_stackcheck(tcg_env, addr);
    }

    if (a->p) {
        TCGv_i32 ofs = load_reg(s, a->rm);
        gen_arm_shift_im(ofs, a->shtype, a->shimm, 0);
        if (a->u) {
            tcg_gen_add_i32(addr, addr, ofs);
        } else {
            tcg_gen_sub_i32(addr, addr, ofs);
        }
    }
    return addr;
}

static void op_addr_rr_post(DisasContext *s, arg_ldst_rr *a,
                            TCGv_i32 addr, int address_offset)
{
    if (!a->p) {
        TCGv_i32 ofs = load_reg(s, a->rm);
        gen_arm_shift_im(ofs, a->shtype, a->shimm, 0);
        if (a->u) {
            tcg_gen_add_i32(addr, addr, ofs);
        } else {
            tcg_gen_sub_i32(addr, addr, ofs);
        }
    } else if (!a->w) {
        return;
    }
    tcg_gen_addi_i32(addr, addr, address_offset);
    store_reg(s, a->rn, addr);
}

static bool op_load_rr(DisasContext *s, arg_ldst_rr *a,
                       MemOp mop, int mem_idx)
{
    ISSInfo issinfo = make_issinfo(s, a->rt, a->p, a->w);
    TCGv_i32 addr, tmp;

    addr = op_addr_rr_pre(s, a);

    tmp = tcg_temp_new_i32();
    gen_aa32_ld_i32(s, tmp, addr, mem_idx, mop);
    disas_set_da_iss(s, mop, issinfo);

    /*
     * Perform base writeback before the loaded value to
     * ensure correct behavior with overlapping index registers.
     */
    op_addr_rr_post(s, a, addr, 0);
    store_reg_from_load(s, a->rt, tmp);
    return true;
}

static bool op_store_rr(DisasContext *s, arg_ldst_rr *a,
                        MemOp mop, int mem_idx)
{
    ISSInfo issinfo = make_issinfo(s, a->rt, a->p, a->w) | ISSIsWrite;
    TCGv_i32 addr, tmp;

    /*
     * In Thumb encodings of stores Rn=1111 is UNDEF; for Arm it
     * is either UNPREDICTABLE or has defined behaviour
     */
    if (s->thumb && a->rn == 15) {
        return false;
    }

    addr = op_addr_rr_pre(s, a);

    tmp = load_reg(s, a->rt);
    gen_aa32_st_i32(s, tmp, addr, mem_idx, mop);
    disas_set_da_iss(s, mop, issinfo);

    op_addr_rr_post(s, a, addr, 0);
    return true;
}

static void do_ldrd_load(DisasContext *s, TCGv_i32 addr, int rt, int rt2)
{
    /*
     * LDRD is required to be an atomic 64-bit access if the
     * address is 8-aligned, two atomic 32-bit accesses if
     * it's only 4-aligned, and to give an alignment fault
     * if it's not 4-aligned. This is MO_ALIGN_4 | MO_ATOM_SUBALIGN.
     * Rt is always the word from the lower address, and Rt2 the
     * data from the higher address, regardless of endianness.
     * So (like gen_load_exclusive) we avoid gen_aa32_ld_i64()
     * so we don't get its SCTLR_B check, and instead do a 64-bit access
     * using MO_BE if appropriate and then split the two halves.
     *
     * For M-profile, and for A-profile before LPAE, the 64-bit
     * atomicity is not required. We could model that using
     * the looser MO_ATOM_IFALIGN_PAIR, but providing a higher
     * level of atomicity than required is harmless (we would not
     * currently generate better code for IFALIGN_PAIR here).
     *
     * This also gives us the correct behaviour of not updating
     * rt if the load of rt2 faults; this is required for cases
     * like "ldrd r2, r3, [r2]" where rt is also the base register.
     */
    int mem_idx = get_mem_index(s);
    MemOp opc = MO_64 | MO_ALIGN_4 | MO_ATOM_SUBALIGN | s->be_data;
    TCGv taddr = gen_aa32_addr(s, addr, opc);
    TCGv_i64 t64 = tcg_temp_new_i64();
    TCGv_i32 tmp = tcg_temp_new_i32();
    TCGv_i32 tmp2 = tcg_temp_new_i32();

    tcg_gen_qemu_ld_i64(t64, taddr, mem_idx, opc);
    if (s->be_data == MO_BE) {
        tcg_gen_extr_i64_i32(tmp2, tmp, t64);
    } else {
        tcg_gen_extr_i64_i32(tmp, tmp2, t64);
    }
    store_reg(s, rt, tmp);
    store_reg(s, rt2, tmp2);
}

static bool trans_LDRD_rr(DisasContext *s, arg_ldst_rr *a)
{
    TCGv_i32 addr;

    if (!ENABLE_ARCH_5TE) {
        return false;
    }
    if (a->rt & 1) {
        unallocated_encoding(s);
        return true;
    }
    addr = op_addr_rr_pre(s, a);

    do_ldrd_load(s, addr, a->rt, a->rt + 1);

    /* LDRD w/ base writeback is undefined if the registers overlap.  */
    op_addr_rr_post(s, a, addr, 0);
    return true;
}

static void do_strd_store(DisasContext *s, TCGv_i32 addr, int rt, int rt2)
{
    /*
     * STRD is required to be an atomic 64-bit access if the
     * address is 8-aligned, two atomic 32-bit accesses if
     * it's only 4-aligned, and to give an alignment fault
     * if it's not 4-aligned.
     * Rt is always the word from the lower address, and Rt2 the
     * data from the higher address, regardless of endianness.
     * So (like gen_store_exclusive) we avoid gen_aa32_ld_i64()
     * so we don't get its SCTLR_B check, and instead do a 64-bit access
     * using MO_BE if appropriate, using a value constructed
     * by putting the two halves together in the right order.
     *
     * As with LDRD, the 64-bit atomicity is not required for
     * M-profile, or for A-profile before LPAE, and we provide
     * the higher guarantee always for simplicity.
     */
    int mem_idx = get_mem_index(s);
    MemOp opc = MO_64 | MO_ALIGN_4 | MO_ATOM_SUBALIGN | s->be_data;
    TCGv taddr = gen_aa32_addr(s, addr, opc);
    TCGv_i32 t1 = load_reg(s, rt);
    TCGv_i32 t2 = load_reg(s, rt2);
    TCGv_i64 t64 = tcg_temp_new_i64();

    if (s->be_data == MO_BE) {
        tcg_gen_concat_i32_i64(t64, t2, t1);
    } else {
        tcg_gen_concat_i32_i64(t64, t1, t2);
    }
    tcg_gen_qemu_st_i64(t64, taddr, mem_idx, opc);
}

static bool trans_STRD_rr(DisasContext *s, arg_ldst_rr *a)
{
    TCGv_i32 addr;

    if (!ENABLE_ARCH_5TE) {
        return false;
    }
    if (a->rt & 1) {
        unallocated_encoding(s);
        return true;
    }
    addr = op_addr_rr_pre(s, a);

    do_strd_store(s, addr, a->rt, a->rt + 1);

    op_addr_rr_post(s, a, addr, 0);
    return true;
}

/*
 * Load/store immediate index
 */

static TCGv_i32 op_addr_ri_pre(DisasContext *s, arg_ldst_ri *a)
{
    int ofs = a->imm;

    if (!a->u) {
        ofs = -ofs;
    }

    if (s->v8m_stackcheck && a->rn == 13 && a->w) {
        /*
         * Stackcheck. Here we know 'addr' is the current SP;
         * U is set if we're moving SP up, else down. It is
         * UNKNOWN whether the limit check triggers when SP starts
         * below the limit and ends up above it; we chose to do so.
         */
        if (!a->u) {
            TCGv_i32 newsp = tcg_temp_new_i32();
            tcg_gen_addi_i32(newsp, cpu_R[13], ofs);
            gen_helper_v8m_stackcheck(tcg_env, newsp);
        } else {
            gen_helper_v8m_stackcheck(tcg_env, cpu_R[13]);
        }
    }

    return add_reg_for_lit(s, a->rn, a->p ? ofs : 0);
}

static void op_addr_ri_post(DisasContext *s, arg_ldst_ri *a,
                            TCGv_i32 addr, int address_offset)
{
    if (!a->p) {
        if (a->u) {
            address_offset += a->imm;
        } else {
            address_offset -= a->imm;
        }
    } else if (!a->w) {
        return;
    }
    tcg_gen_addi_i32(addr, addr, address_offset);
    store_reg(s, a->rn, addr);
}

static bool op_load_ri(DisasContext *s, arg_ldst_ri *a,
                       MemOp mop, int mem_idx)
{
    ISSInfo issinfo = make_issinfo(s, a->rt, a->p, a->w);
    TCGv_i32 addr, tmp;

    addr = op_addr_ri_pre(s, a);

    tmp = tcg_temp_new_i32();
    gen_aa32_ld_i32(s, tmp, addr, mem_idx, mop);
    disas_set_da_iss(s, mop, issinfo);

    /*
     * Perform base writeback before the loaded value to
     * ensure correct behavior with overlapping index registers.
     */
    op_addr_ri_post(s, a, addr, 0);
    store_reg_from_load(s, a->rt, tmp);
    return true;
}

static bool op_store_ri(DisasContext *s, arg_ldst_ri *a,
                        MemOp mop, int mem_idx)
{
    ISSInfo issinfo = make_issinfo(s, a->rt, a->p, a->w) | ISSIsWrite;
    TCGv_i32 addr, tmp;

    /*
     * In Thumb encodings of stores Rn=1111 is UNDEF; for Arm it
     * is either UNPREDICTABLE or has defined behaviour
     */
    if (s->thumb && a->rn == 15) {
        return false;
    }

    addr = op_addr_ri_pre(s, a);

    tmp = load_reg(s, a->rt);
    gen_aa32_st_i32(s, tmp, addr, mem_idx, mop);
    disas_set_da_iss(s, mop, issinfo);

    op_addr_ri_post(s, a, addr, 0);
    return true;
}

static bool op_ldrd_ri(DisasContext *s, arg_ldst_ri *a, int rt2)
{
    TCGv_i32 addr;

    addr = op_addr_ri_pre(s, a);

    do_ldrd_load(s, addr, a->rt, rt2);

    /* LDRD w/ base writeback is undefined if the registers overlap.  */
    op_addr_ri_post(s, a, addr, 0);
    return true;
}

static bool trans_LDRD_ri_a32(DisasContext *s, arg_ldst_ri *a)
{
    if (!ENABLE_ARCH_5TE || (a->rt & 1)) {
        return false;
    }
    return op_ldrd_ri(s, a, a->rt + 1);
}

static bool trans_LDRD_ri_t32(DisasContext *s, arg_ldst_ri2 *a)
{
    arg_ldst_ri b = {
        .u = a->u, .w = a->w, .p = a->p,
        .rn = a->rn, .rt = a->rt, .imm = a->imm
    };
    return op_ldrd_ri(s, &b, a->rt2);
}

static bool op_strd_ri(DisasContext *s, arg_ldst_ri *a, int rt2)
{
    TCGv_i32 addr;

    addr = op_addr_ri_pre(s, a);

    do_strd_store(s, addr, a->rt, rt2);

    op_addr_ri_post(s, a, addr, 0);
    return true;
}

static bool trans_STRD_ri_a32(DisasContext *s, arg_ldst_ri *a)
{
    if (!ENABLE_ARCH_5TE || (a->rt & 1)) {
        return false;
    }
    return op_strd_ri(s, a, a->rt + 1);
}

static bool trans_STRD_ri_t32(DisasContext *s, arg_ldst_ri2 *a)
{
    arg_ldst_ri b = {
        .u = a->u, .w = a->w, .p = a->p,
        .rn = a->rn, .rt = a->rt, .imm = a->imm
    };
    return op_strd_ri(s, &b, a->rt2);
}

#define DO_LDST(NAME, WHICH, MEMOP) \
static bool trans_##NAME##_ri(DisasContext *s, arg_ldst_ri *a)        \
{                                                                     \
    return op_##WHICH##_ri(s, a, MEMOP, get_mem_index(s));            \
}                                                                     \
static bool trans_##NAME##T_ri(DisasContext *s, arg_ldst_ri *a)       \
{                                                                     \
    return op_##WHICH##_ri(s, a, MEMOP, get_a32_user_mem_index(s));   \
}                                                                     \
static bool trans_##NAME##_rr(DisasContext *s, arg_ldst_rr *a)        \
{                                                                     \
    return op_##WHICH##_rr(s, a, MEMOP, get_mem_index(s));            \
}                                                                     \
static bool trans_##NAME##T_rr(DisasContext *s, arg_ldst_rr *a)       \
{                                                                     \
    return op_##WHICH##_rr(s, a, MEMOP, get_a32_user_mem_index(s));   \
}

DO_LDST(LDR, load, MO_UL)
DO_LDST(LDRB, load, MO_UB)
DO_LDST(LDRH, load, MO_UW)
DO_LDST(LDRSB, load, MO_SB)
DO_LDST(LDRSH, load, MO_SW)

DO_LDST(STR, store, MO_UL)
DO_LDST(STRB, store, MO_UB)
DO_LDST(STRH, store, MO_UW)

#undef DO_LDST

/*
 * Synchronization primitives
 */

static bool op_swp(DisasContext *s, arg_SWP *a, MemOp opc)
{
    TCGv_i32 addr, tmp;
    TCGv taddr;

    opc |= s->be_data;
    addr = load_reg(s, a->rn);
    taddr = gen_aa32_addr(s, addr, opc);

    tmp = load_reg(s, a->rt2);
    tcg_gen_atomic_xchg_i32(tmp, taddr, tmp, get_mem_index(s), opc);

    store_reg(s, a->rt, tmp);
    return true;
}

static bool trans_SWP(DisasContext *s, arg_SWP *a)
{
    return op_swp(s, a, MO_UL | MO_ALIGN);
}

static bool trans_SWPB(DisasContext *s, arg_SWP *a)
{
    return op_swp(s, a, MO_UB);
}

/*
 * Load/Store Exclusive and Load-Acquire/Store-Release
 */

static bool op_strex(DisasContext *s, arg_STREX *a, MemOp mop, bool rel)
{
    TCGv_i32 addr;
    /* Some cases stopped being UNPREDICTABLE in v8A (but not v8M) */
    bool v8a = ENABLE_ARCH_8 && !arm_dc_feature(s, ARM_FEATURE_M);

    /* We UNDEF for these UNPREDICTABLE cases.  */
    if (a->rd == 15 || a->rn == 15 || a->rt == 15
        || a->rd == a->rn || a->rd == a->rt
        || (!v8a && s->thumb && (a->rd == 13 || a->rt == 13))
        || (mop == MO_64
            && (a->rt2 == 15
                || a->rd == a->rt2
                || (!v8a && s->thumb && a->rt2 == 13)))) {
        unallocated_encoding(s);
        return true;
    }

    if (rel) {
        tcg_gen_mb(TCG_MO_ALL | TCG_BAR_STRL);
    }

    addr = tcg_temp_new_i32();
    load_reg_var(s, addr, a->rn);
    tcg_gen_addi_i32(addr, addr, a->imm);

    gen_store_exclusive(s, a->rd, a->rt, a->rt2, addr, mop);
    return true;
}

static bool trans_STREX(DisasContext *s, arg_STREX *a)
{
    if (!ENABLE_ARCH_6) {
        return false;
    }
    return op_strex(s, a, MO_32, false);
}

static bool trans_STREXD_a32(DisasContext *s, arg_STREX *a)
{
    if (!ENABLE_ARCH_6K) {
        return false;
    }
    /* We UNDEF for these UNPREDICTABLE cases.  */
    if (a->rt & 1) {
        unallocated_encoding(s);
        return true;
    }
    a->rt2 = a->rt + 1;
    return op_strex(s, a, MO_64, false);
}

static bool trans_STREXD_t32(DisasContext *s, arg_STREX *a)
{
    return op_strex(s, a, MO_64, false);
}

static bool trans_STREXB(DisasContext *s, arg_STREX *a)
{
    if (s->thumb ? !ENABLE_ARCH_7 : !ENABLE_ARCH_6K) {
        return false;
    }
    return op_strex(s, a, MO_8, false);
}

static bool trans_STREXH(DisasContext *s, arg_STREX *a)
{
    if (s->thumb ? !ENABLE_ARCH_7 : !ENABLE_ARCH_6K) {
        return false;
    }
    return op_strex(s, a, MO_16, false);
}

static bool trans_STLEX(DisasContext *s, arg_STREX *a)
{
    if (!ENABLE_ARCH_8) {
        return false;
    }
    return op_strex(s, a, MO_32, true);
}

static bool trans_STLEXD_a32(DisasContext *s, arg_STREX *a)
{
    if (!ENABLE_ARCH_8) {
        return false;
    }
    /* We UNDEF for these UNPREDICTABLE cases.  */
    if (a->rt & 1) {
        unallocated_encoding(s);
        return true;
    }
    a->rt2 = a->rt + 1;
    return op_strex(s, a, MO_64, true);
}

static bool trans_STLEXD_t32(DisasContext *s, arg_STREX *a)
{
    if (!ENABLE_ARCH_8) {
        return false;
    }
    return op_strex(s, a, MO_64, true);
}

static bool trans_STLEXB(DisasContext *s, arg_STREX *a)
{
    if (!ENABLE_ARCH_8) {
        return false;
    }
    return op_strex(s, a, MO_8, true);
}

static bool trans_STLEXH(DisasContext *s, arg_STREX *a)
{
    if (!ENABLE_ARCH_8) {
        return false;
    }
    return op_strex(s, a, MO_16, true);
}

static bool op_stl(DisasContext *s, arg_STL *a, MemOp mop)
{
    TCGv_i32 addr, tmp;

    if (!ENABLE_ARCH_8) {
        return false;
    }
    /* We UNDEF for these UNPREDICTABLE cases.  */
    if (a->rn == 15 || a->rt == 15) {
        unallocated_encoding(s);
        return true;
    }

    addr = load_reg(s, a->rn);
    tmp = load_reg(s, a->rt);
    tcg_gen_mb(TCG_MO_ALL | TCG_BAR_STRL);
    gen_aa32_st_i32(s, tmp, addr, get_mem_index(s), mop | MO_ALIGN);
    disas_set_da_iss(s, mop, a->rt | ISSIsAcqRel | ISSIsWrite);

    return true;
}

static bool trans_STL(DisasContext *s, arg_STL *a)
{
    return op_stl(s, a, MO_UL);
}

static bool trans_STLB(DisasContext *s, arg_STL *a)
{
    return op_stl(s, a, MO_UB);
}

static bool trans_STLH(DisasContext *s, arg_STL *a)
{
    return op_stl(s, a, MO_UW);
}

static bool op_ldrex(DisasContext *s, arg_LDREX *a, MemOp mop, bool acq)
{
    TCGv_i32 addr;
    /* Some cases stopped being UNPREDICTABLE in v8A (but not v8M) */
    bool v8a = ENABLE_ARCH_8 && !arm_dc_feature(s, ARM_FEATURE_M);

    /* We UNDEF for these UNPREDICTABLE cases.  */
    if (a->rn == 15 || a->rt == 15
        || (!v8a && s->thumb && a->rt == 13)
        || (mop == MO_64
            && (a->rt2 == 15 || a->rt == a->rt2
                || (!v8a && s->thumb && a->rt2 == 13)))) {
        unallocated_encoding(s);
        return true;
    }

    addr = tcg_temp_new_i32();
    load_reg_var(s, addr, a->rn);
    tcg_gen_addi_i32(addr, addr, a->imm);

    gen_load_exclusive(s, a->rt, a->rt2, addr, mop);

    if (acq) {
        tcg_gen_mb(TCG_MO_ALL | TCG_BAR_LDAQ);
    }
    return true;
}

static bool trans_LDREX(DisasContext *s, arg_LDREX *a)
{
    if (!ENABLE_ARCH_6) {
        return false;
    }
    return op_ldrex(s, a, MO_32, false);
}

static bool trans_LDREXD_a32(DisasContext *s, arg_LDREX *a)
{
    if (!ENABLE_ARCH_6K) {
        return false;
    }
    /* We UNDEF for these UNPREDICTABLE cases.  */
    if (a->rt & 1) {
        unallocated_encoding(s);
        return true;
    }
    a->rt2 = a->rt + 1;
    return op_ldrex(s, a, MO_64, false);
}

static bool trans_LDREXD_t32(DisasContext *s, arg_LDREX *a)
{
    return op_ldrex(s, a, MO_64, false);
}

static bool trans_LDREXB(DisasContext *s, arg_LDREX *a)
{
    if (s->thumb ? !ENABLE_ARCH_7 : !ENABLE_ARCH_6K) {
        return false;
    }
    return op_ldrex(s, a, MO_8, false);
}

static bool trans_LDREXH(DisasContext *s, arg_LDREX *a)
{
    if (s->thumb ? !ENABLE_ARCH_7 : !ENABLE_ARCH_6K) {
        return false;
    }
    return op_ldrex(s, a, MO_16, false);
}

static bool trans_LDAEX(DisasContext *s, arg_LDREX *a)
{
    if (!ENABLE_ARCH_8) {
        return false;
    }
    return op_ldrex(s, a, MO_32, true);
}

static bool trans_LDAEXD_a32(DisasContext *s, arg_LDREX *a)
{
    if (!ENABLE_ARCH_8) {
        return false;
    }
    /* We UNDEF for these UNPREDICTABLE cases.  */
    if (a->rt & 1) {
        unallocated_encoding(s);
        return true;
    }
    a->rt2 = a->rt + 1;
    return op_ldrex(s, a, MO_64, true);
}

static bool trans_LDAEXD_t32(DisasContext *s, arg_LDREX *a)
{
    if (!ENABLE_ARCH_8) {
        return false;
    }
    return op_ldrex(s, a, MO_64, true);
}

static bool trans_LDAEXB(DisasContext *s, arg_LDREX *a)
{
    if (!ENABLE_ARCH_8) {
        return false;
    }
    return op_ldrex(s, a, MO_8, true);
}

static bool trans_LDAEXH(DisasContext *s, arg_LDREX *a)
{
    if (!ENABLE_ARCH_8) {
        return false;
    }
    return op_ldrex(s, a, MO_16, true);
}

static bool op_lda(DisasContext *s, arg_LDA *a, MemOp mop)
{
    TCGv_i32 addr, tmp;

    if (!ENABLE_ARCH_8) {
        return false;
    }
    /* We UNDEF for these UNPREDICTABLE cases.  */
    if (a->rn == 15 || a->rt == 15) {
        unallocated_encoding(s);
        return true;
    }

    addr = load_reg(s, a->rn);
    tmp = tcg_temp_new_i32();
    gen_aa32_ld_i32(s, tmp, addr, get_mem_index(s), mop | MO_ALIGN);
    disas_set_da_iss(s, mop, a->rt | ISSIsAcqRel);

    store_reg(s, a->rt, tmp);
    tcg_gen_mb(TCG_MO_ALL | TCG_BAR_STRL);
    return true;
}

static bool trans_LDA(DisasContext *s, arg_LDA *a)
{
    return op_lda(s, a, MO_UL);
}

static bool trans_LDAB(DisasContext *s, arg_LDA *a)
{
    return op_lda(s, a, MO_UB);
}

static bool trans_LDAH(DisasContext *s, arg_LDA *a)
{
    return op_lda(s, a, MO_UW);
}

/*
 * Media instructions
 */

static bool trans_USADA8(DisasContext *s, arg_USADA8 *a)
{
    TCGv_i32 t1, t2;

    if (!ENABLE_ARCH_6) {
        return false;
    }

    t1 = load_reg(s, a->rn);
    t2 = load_reg(s, a->rm);
    gen_helper_usad8(t1, t1, t2);
    if (a->ra != 15) {
        t2 = load_reg(s, a->ra);
        tcg_gen_add_i32(t1, t1, t2);
    }
    store_reg(s, a->rd, t1);
    return true;
}

static bool op_bfx(DisasContext *s, arg_UBFX *a, bool u)
{
    TCGv_i32 tmp;
    int width = a->widthm1 + 1;
    int shift = a->lsb;

    if (!ENABLE_ARCH_6T2) {
        return false;
    }
    if (shift + width > 32) {
        /* UNPREDICTABLE; we choose to UNDEF */
        unallocated_encoding(s);
        return true;
    }

    tmp = load_reg(s, a->rn);
    if (u) {
        tcg_gen_extract_i32(tmp, tmp, shift, width);
    } else {
        tcg_gen_sextract_i32(tmp, tmp, shift, width);
    }
    store_reg(s, a->rd, tmp);
    return true;
}

static bool trans_SBFX(DisasContext *s, arg_SBFX *a)
{
    return op_bfx(s, a, false);
}

static bool trans_UBFX(DisasContext *s, arg_UBFX *a)
{
    return op_bfx(s, a, true);
}

static bool trans_BFCI(DisasContext *s, arg_BFCI *a)
{
    int msb = a->msb, lsb = a->lsb;
    TCGv_i32 t_in, t_rd;
    int width;

    if (!ENABLE_ARCH_6T2) {
        return false;
    }
    if (msb < lsb) {
        /* UNPREDICTABLE; we choose to UNDEF */
        unallocated_encoding(s);
        return true;
    }

    width = msb + 1 - lsb;
    if (a->rn == 15) {
        /* BFC */
        t_in = tcg_constant_i32(0);
    } else {
        /* BFI */
        t_in = load_reg(s, a->rn);
    }
    t_rd = load_reg(s, a->rd);
    tcg_gen_deposit_i32(t_rd, t_rd, t_in, lsb, width);
    store_reg(s, a->rd, t_rd);
    return true;
}

static bool trans_UDF(DisasContext *s, arg_UDF *a)
{
    unallocated_encoding(s);
    return true;
}

/*
 * Parallel addition and subtraction
 */

static bool op_par_addsub(DisasContext *s, arg_rrr *a,
                          void (*gen)(TCGv_i32, TCGv_i32, TCGv_i32))
{
    TCGv_i32 t0, t1;

    if (s->thumb
        ? !arm_dc_feature(s, ARM_FEATURE_THUMB_DSP)
        : !ENABLE_ARCH_6) {
        return false;
    }

    t0 = load_reg(s, a->rn);
    t1 = load_reg(s, a->rm);

    gen(t0, t0, t1);

    store_reg(s, a->rd, t0);
    return true;
}

static bool op_par_addsub_ge(DisasContext *s, arg_rrr *a,
                             void (*gen)(TCGv_i32, TCGv_i32,
                                         TCGv_i32, TCGv_ptr))
{
    TCGv_i32 t0, t1;
    TCGv_ptr ge;

    if (s->thumb
        ? !arm_dc_feature(s, ARM_FEATURE_THUMB_DSP)
        : !ENABLE_ARCH_6) {
        return false;
    }

    t0 = load_reg(s, a->rn);
    t1 = load_reg(s, a->rm);

    ge = tcg_temp_new_ptr();
    tcg_gen_addi_ptr(ge, tcg_env, offsetof(CPUARMState, GE));
    gen(t0, t0, t1, ge);

    store_reg(s, a->rd, t0);
    return true;
}

#define DO_PAR_ADDSUB(NAME, helper) \
static bool trans_##NAME(DisasContext *s, arg_rrr *a)   \
{                                                       \
    return op_par_addsub(s, a, helper);                 \
}

#define DO_PAR_ADDSUB_GE(NAME, helper) \
static bool trans_##NAME(DisasContext *s, arg_rrr *a)   \
{                                                       \
    return op_par_addsub_ge(s, a, helper);              \
}

DO_PAR_ADDSUB_GE(SADD16, gen_helper_sadd16)
DO_PAR_ADDSUB_GE(SASX, gen_helper_saddsubx)
DO_PAR_ADDSUB_GE(SSAX, gen_helper_ssubaddx)
DO_PAR_ADDSUB_GE(SSUB16, gen_helper_ssub16)
DO_PAR_ADDSUB_GE(SADD8, gen_helper_sadd8)
DO_PAR_ADDSUB_GE(SSUB8, gen_helper_ssub8)

DO_PAR_ADDSUB_GE(UADD16, gen_helper_uadd16)
DO_PAR_ADDSUB_GE(UASX, gen_helper_uaddsubx)
DO_PAR_ADDSUB_GE(USAX, gen_helper_usubaddx)
DO_PAR_ADDSUB_GE(USUB16, gen_helper_usub16)
DO_PAR_ADDSUB_GE(UADD8, gen_helper_uadd8)
DO_PAR_ADDSUB_GE(USUB8, gen_helper_usub8)

DO_PAR_ADDSUB(QADD16, gen_helper_qadd16)
DO_PAR_ADDSUB(QASX, gen_helper_qaddsubx)
DO_PAR_ADDSUB(QSAX, gen_helper_qsubaddx)
DO_PAR_ADDSUB(QSUB16, gen_helper_qsub16)
DO_PAR_ADDSUB(QADD8, gen_helper_qadd8)
DO_PAR_ADDSUB(QSUB8, gen_helper_qsub8)

DO_PAR_ADDSUB(UQADD16, gen_helper_uqadd16)
DO_PAR_ADDSUB(UQASX, gen_helper_uqaddsubx)
DO_PAR_ADDSUB(UQSAX, gen_helper_uqsubaddx)
DO_PAR_ADDSUB(UQSUB16, gen_helper_uqsub16)
DO_PAR_ADDSUB(UQADD8, gen_helper_uqadd8)
DO_PAR_ADDSUB(UQSUB8, gen_helper_uqsub8)

DO_PAR_ADDSUB(SHADD16, gen_helper_shadd16)
DO_PAR_ADDSUB(SHASX, gen_helper_shaddsubx)
DO_PAR_ADDSUB(SHSAX, gen_helper_shsubaddx)
DO_PAR_ADDSUB(SHSUB16, gen_helper_shsub16)
DO_PAR_ADDSUB(SHADD8, gen_helper_shadd8)
DO_PAR_ADDSUB(SHSUB8, gen_helper_shsub8)

DO_PAR_ADDSUB(UHADD16, gen_helper_uhadd16)
DO_PAR_ADDSUB(UHASX, gen_helper_uhaddsubx)
DO_PAR_ADDSUB(UHSAX, gen_helper_uhsubaddx)
DO_PAR_ADDSUB(UHSUB16, gen_helper_uhsub16)
DO_PAR_ADDSUB(UHADD8, gen_helper_uhadd8)
DO_PAR_ADDSUB(UHSUB8, gen_helper_uhsub8)

#undef DO_PAR_ADDSUB
#undef DO_PAR_ADDSUB_GE

/*
 * Packing, unpacking, saturation, and reversal
 */

static bool trans_PKH(DisasContext *s, arg_PKH *a)
{
    TCGv_i32 tn, tm;
    int shift = a->imm;

    if (s->thumb
        ? !arm_dc_feature(s, ARM_FEATURE_THUMB_DSP)
        : !ENABLE_ARCH_6) {
        return false;
    }

    tn = load_reg(s, a->rn);
    tm = load_reg(s, a->rm);
    if (a->tb) {
        /* PKHTB */
        if (shift == 0) {
            shift = 31;
        }
        tcg_gen_sari_i32(tm, tm, shift);
        tcg_gen_deposit_i32(tn, tn, tm, 0, 16);
    } else {
        /* PKHBT */
        tcg_gen_shli_i32(tm, tm, shift);
        tcg_gen_deposit_i32(tn, tm, tn, 0, 16);
    }
    store_reg(s, a->rd, tn);
    return true;
}

static bool op_sat(DisasContext *s, arg_sat *a,
                   void (*gen)(TCGv_i32, TCGv_env, TCGv_i32, TCGv_i32))
{
    TCGv_i32 tmp;
    int shift = a->imm;

    if (!ENABLE_ARCH_6) {
        return false;
    }

    tmp = load_reg(s, a->rn);
    if (a->sh) {
        tcg_gen_sari_i32(tmp, tmp, shift ? shift : 31);
    } else {
        tcg_gen_shli_i32(tmp, tmp, shift);
    }

    gen(tmp, tcg_env, tmp, tcg_constant_i32(a->satimm));

    store_reg(s, a->rd, tmp);
    return true;
}

static bool trans_SSAT(DisasContext *s, arg_sat *a)
{
    return op_sat(s, a, gen_helper_ssat);
}

static bool trans_USAT(DisasContext *s, arg_sat *a)
{
    return op_sat(s, a, gen_helper_usat);
}

static bool trans_SSAT16(DisasContext *s, arg_sat *a)
{
    if (s->thumb && !arm_dc_feature(s, ARM_FEATURE_THUMB_DSP)) {
        return false;
    }
    return op_sat(s, a, gen_helper_ssat16);
}

static bool trans_USAT16(DisasContext *s, arg_sat *a)
{
    if (s->thumb && !arm_dc_feature(s, ARM_FEATURE_THUMB_DSP)) {
        return false;
    }
    return op_sat(s, a, gen_helper_usat16);
}

static bool op_xta(DisasContext *s, arg_rrr_rot *a,
                   void (*gen_extract)(TCGv_i32, TCGv_i32),
                   void (*gen_add)(TCGv_i32, TCGv_i32, TCGv_i32))
{
    TCGv_i32 tmp;

    if (!ENABLE_ARCH_6) {
        return false;
    }

    tmp = load_reg(s, a->rm);
    /*
     * TODO: In many cases we could do a shift instead of a rotate.
     * Combined with a simple extend, that becomes an extract.
     */
    tcg_gen_rotri_i32(tmp, tmp, a->rot * 8);
    gen_extract(tmp, tmp);

    if (a->rn != 15) {
        TCGv_i32 tmp2 = load_reg(s, a->rn);
        gen_add(tmp, tmp, tmp2);
    }
    store_reg(s, a->rd, tmp);
    return true;
}

static bool trans_SXTAB(DisasContext *s, arg_rrr_rot *a)
{
    return op_xta(s, a, tcg_gen_ext8s_i32, tcg_gen_add_i32);
}

static bool trans_SXTAH(DisasContext *s, arg_rrr_rot *a)
{
    return op_xta(s, a, tcg_gen_ext16s_i32, tcg_gen_add_i32);
}

static bool trans_SXTAB16(DisasContext *s, arg_rrr_rot *a)
{
    if (s->thumb && !arm_dc_feature(s, ARM_FEATURE_THUMB_DSP)) {
        return false;
    }
    return op_xta(s, a, gen_helper_sxtb16, gen_add16);
}

static bool trans_UXTAB(DisasContext *s, arg_rrr_rot *a)
{
    return op_xta(s, a, tcg_gen_ext8u_i32, tcg_gen_add_i32);
}

static bool trans_UXTAH(DisasContext *s, arg_rrr_rot *a)
{
    return op_xta(s, a, tcg_gen_ext16u_i32, tcg_gen_add_i32);
}

static bool trans_UXTAB16(DisasContext *s, arg_rrr_rot *a)
{
    if (s->thumb && !arm_dc_feature(s, ARM_FEATURE_THUMB_DSP)) {
        return false;
    }
    return op_xta(s, a, gen_helper_uxtb16, gen_add16);
}

static bool trans_SEL(DisasContext *s, arg_rrr *a)
{
    TCGv_i32 t1, t2, t3;

    if (s->thumb
        ? !arm_dc_feature(s, ARM_FEATURE_THUMB_DSP)
        : !ENABLE_ARCH_6) {
        return false;
    }

    t1 = load_reg(s, a->rn);
    t2 = load_reg(s, a->rm);
    t3 = tcg_temp_new_i32();
    tcg_gen_ld_i32(t3, tcg_env, offsetof(CPUARMState, GE));
    gen_helper_sel_flags(t1, t3, t1, t2);
    store_reg(s, a->rd, t1);
    return true;
}

static bool op_rr(DisasContext *s, arg_rr *a,
                  void (*gen)(TCGv_i32, TCGv_i32))
{
    TCGv_i32 tmp;

    tmp = load_reg(s, a->rm);
    gen(tmp, tmp);
    store_reg(s, a->rd, tmp);
    return true;
}

static bool trans_REV(DisasContext *s, arg_rr *a)
{
    if (!ENABLE_ARCH_6) {
        return false;
    }
    return op_rr(s, a, tcg_gen_bswap32_i32);
}

static bool trans_REV16(DisasContext *s, arg_rr *a)
{
    if (!ENABLE_ARCH_6) {
        return false;
    }
    return op_rr(s, a, gen_rev16);
}

static bool trans_REVSH(DisasContext *s, arg_rr *a)
{
    if (!ENABLE_ARCH_6) {
        return false;
    }
    return op_rr(s, a, gen_revsh);
}

static bool trans_RBIT(DisasContext *s, arg_rr *a)
{
    if (!ENABLE_ARCH_6T2) {
        return false;
    }
    return op_rr(s, a, gen_helper_rbit);
}

/*
 * Signed multiply, signed and unsigned divide
 */

static bool op_smlad(DisasContext *s, arg_rrrr *a, bool m_swap, bool sub)
{
    TCGv_i32 t1, t2;

    if (!ENABLE_ARCH_6) {
        return false;
    }

    t1 = load_reg(s, a->rn);
    t2 = load_reg(s, a->rm);
    if (m_swap) {
        gen_swap_half(t2, t2);
    }
    gen_smul_dual(t1, t2);

    if (sub) {
        /*
         * This subtraction cannot overflow, so we can do a simple
         * 32-bit subtraction and then a possible 32-bit saturating
         * addition of Ra.
         */
        tcg_gen_sub_i32(t1, t1, t2);

        if (a->ra != 15) {
            t2 = load_reg(s, a->ra);
            gen_helper_add_setq(t1, tcg_env, t1, t2);
        }
    } else if (a->ra == 15) {
        /* Single saturation-checking addition */
        gen_helper_add_setq(t1, tcg_env, t1, t2);
    } else {
        /*
         * We need to add the products and Ra together and then
         * determine whether the final result overflowed. Doing
         * this as two separate add-and-check-overflow steps incorrectly
         * sets Q for cases like (-32768 * -32768) + (-32768 * -32768) + -1.
         * Do all the arithmetic at 64-bits and then check for overflow.
         */
        TCGv_i64 p64, q64;
        TCGv_i32 t3, qf, one;

        p64 = tcg_temp_new_i64();
        q64 = tcg_temp_new_i64();
        tcg_gen_ext_i32_i64(p64, t1);
        tcg_gen_ext_i32_i64(q64, t2);
        tcg_gen_add_i64(p64, p64, q64);
        load_reg_var(s, t2, a->ra);
        tcg_gen_ext_i32_i64(q64, t2);
        tcg_gen_add_i64(p64, p64, q64);

        tcg_gen_extr_i64_i32(t1, t2, p64);
        /*
         * t1 is the low half of the result which goes into Rd.
         * We have overflow and must set Q if the high half (t2)
         * is different from the sign-extension of t1.
         */
        t3 = tcg_temp_new_i32();
        tcg_gen_sari_i32(t3, t1, 31);
        qf = load_cpu_field(QF);
        one = tcg_constant_i32(1);
        tcg_gen_movcond_i32(TCG_COND_NE, qf, t2, t3, one, qf);
        store_cpu_field(qf, QF);
    }
    store_reg(s, a->rd, t1);
    return true;
}

static bool trans_SMLAD(DisasContext *s, arg_rrrr *a)
{
    return op_smlad(s, a, false, false);
}

static bool trans_SMLADX(DisasContext *s, arg_rrrr *a)
{
    return op_smlad(s, a, true, false);
}

static bool trans_SMLSD(DisasContext *s, arg_rrrr *a)
{
    return op_smlad(s, a, false, true);
}

static bool trans_SMLSDX(DisasContext *s, arg_rrrr *a)
{
    return op_smlad(s, a, true, true);
}

static bool op_smlald(DisasContext *s, arg_rrrr *a, bool m_swap, bool sub)
{
    TCGv_i32 t1, t2;
    TCGv_i64 l1, l2;

    if (!ENABLE_ARCH_6) {
        return false;
    }

    t1 = load_reg(s, a->rn);
    t2 = load_reg(s, a->rm);
    if (m_swap) {
        gen_swap_half(t2, t2);
    }
    gen_smul_dual(t1, t2);

    l1 = tcg_temp_new_i64();
    l2 = tcg_temp_new_i64();
    tcg_gen_ext_i32_i64(l1, t1);
    tcg_gen_ext_i32_i64(l2, t2);

    if (sub) {
        tcg_gen_sub_i64(l1, l1, l2);
    } else {
        tcg_gen_add_i64(l1, l1, l2);
    }

    gen_addq(s, l1, a->ra, a->rd);
    gen_storeq_reg(s, a->ra, a->rd, l1);
    return true;
}

static bool trans_SMLALD(DisasContext *s, arg_rrrr *a)
{
    return op_smlald(s, a, false, false);
}

static bool trans_SMLALDX(DisasContext *s, arg_rrrr *a)
{
    return op_smlald(s, a, true, false);
}

static bool trans_SMLSLD(DisasContext *s, arg_rrrr *a)
{
    return op_smlald(s, a, false, true);
}

static bool trans_SMLSLDX(DisasContext *s, arg_rrrr *a)
{
    return op_smlald(s, a, true, true);
}

static bool op_smmla(DisasContext *s, arg_rrrr *a, bool round, bool sub)
{
    TCGv_i32 t1, t2;

    if (s->thumb
        ? !arm_dc_feature(s, ARM_FEATURE_THUMB_DSP)
        : !ENABLE_ARCH_6) {
        return false;
    }

    t1 = load_reg(s, a->rn);
    t2 = load_reg(s, a->rm);
    tcg_gen_muls2_i32(t2, t1, t1, t2);

    if (a->ra != 15) {
        TCGv_i32 t3 = load_reg(s, a->ra);
        if (sub) {
            /*
             * For SMMLS, we need a 64-bit subtract.  Borrow caused by
             * a non-zero multiplicand lowpart, and the correct result
             * lowpart for rounding.
             */
            tcg_gen_sub2_i32(t2, t1, tcg_constant_i32(0), t3, t2, t1);
        } else {
            tcg_gen_add_i32(t1, t1, t3);
        }
    }
    if (round) {
        /*
         * Adding 0x80000000 to the 64-bit quantity means that we have
         * carry in to the high word when the low word has the msb set.
         */
        tcg_gen_shri_i32(t2, t2, 31);
        tcg_gen_add_i32(t1, t1, t2);
    }
    store_reg(s, a->rd, t1);
    return true;
}

static bool trans_SMMLA(DisasContext *s, arg_rrrr *a)
{
    return op_smmla(s, a, false, false);
}

static bool trans_SMMLAR(DisasContext *s, arg_rrrr *a)
{
    return op_smmla(s, a, true, false);
}

static bool trans_SMMLS(DisasContext *s, arg_rrrr *a)
{
    return op_smmla(s, a, false, true);
}

static bool trans_SMMLSR(DisasContext *s, arg_rrrr *a)
{
    return op_smmla(s, a, true, true);
}

static bool op_div(DisasContext *s, arg_rrr *a, bool u)
{
    TCGv_i32 t1, t2;

    if (s->thumb
        ? !dc_isar_feature(aa32_thumb_div, s)
        : !dc_isar_feature(aa32_arm_div, s)) {
        return false;
    }

    t1 = load_reg(s, a->rn);
    t2 = load_reg(s, a->rm);
    if (u) {
        gen_helper_udiv(t1, tcg_env, t1, t2);
    } else {
        gen_helper_sdiv(t1, tcg_env, t1, t2);
    }
    store_reg(s, a->rd, t1);
    return true;
}

static bool trans_SDIV(DisasContext *s, arg_rrr *a)
{
    return op_div(s, a, false);
}

static bool trans_UDIV(DisasContext *s, arg_rrr *a)
{
    return op_div(s, a, true);
}

/*
 * Block data transfer
 */

static TCGv_i32 op_addr_block_pre(DisasContext *s, arg_ldst_block *a, int n)
{
    TCGv_i32 addr = load_reg(s, a->rn);

    if (a->b) {
        if (a->i) {
            /* pre increment */
            tcg_gen_addi_i32(addr, addr, 4);
        } else {
            /* pre decrement */
            tcg_gen_addi_i32(addr, addr, -(n * 4));
        }
    } else if (!a->i && n != 1) {
        /* post decrement */
        tcg_gen_addi_i32(addr, addr, -((n - 1) * 4));
    }

    if (s->v8m_stackcheck && a->rn == 13 && a->w) {
        /*
         * If the writeback is incrementing SP rather than
         * decrementing it, and the initial SP is below the
         * stack limit but the final written-back SP would
         * be above, then we must not perform any memory
         * accesses, but it is IMPDEF whether we generate
         * an exception. We choose to do so in this case.
         * At this point 'addr' is the lowest address, so
         * either the original SP (if incrementing) or our
         * final SP (if decrementing), so that's what we check.
         */
        gen_helper_v8m_stackcheck(tcg_env, addr);
    }

    return addr;
}

static void op_addr_block_post(DisasContext *s, arg_ldst_block *a,
                               TCGv_i32 addr, int n)
{
    if (a->w) {
        /* write back */
        if (!a->b) {
            if (a->i) {
                /* post increment */
                tcg_gen_addi_i32(addr, addr, 4);
            } else {
                /* post decrement */
                tcg_gen_addi_i32(addr, addr, -(n * 4));
            }
        } else if (!a->i && n != 1) {
            /* pre decrement */
            tcg_gen_addi_i32(addr, addr, -((n - 1) * 4));
        }
        store_reg(s, a->rn, addr);
    }
}

static bool op_stm(DisasContext *s, arg_ldst_block *a)
{
    int i, j, n, list, mem_idx;
    bool user = a->u;
    TCGv_i32 addr, tmp;

    if (user) {
        /* STM (user) */
        if (IS_USER(s)) {
            /* Only usable in supervisor mode.  */
            unallocated_encoding(s);
            return true;
        }
    }

    list = a->list;
    n = ctpop16(list);
    /*
     * This is UNPREDICTABLE for n < 1 in all encodings, and we choose
     * to UNDEF. In the T32 STM encoding n == 1 is also UNPREDICTABLE,
     * but hardware treats it like the A32 version and implements the
     * single-register-store, and some in-the-wild (buggy) software
     * assumes that, so we don't UNDEF on that case.
     */
    if (n < 1 || a->rn == 15) {
        unallocated_encoding(s);
        return true;
    }

    s->eci_handled = true;

    addr = op_addr_block_pre(s, a, n);
    mem_idx = get_mem_index(s);

    for (i = j = 0; i < 16; i++) {
        if (!(list & (1 << i))) {
            continue;
        }

        if (user && i != 15) {
            tmp = tcg_temp_new_i32();
            gen_helper_get_user_reg(tmp, tcg_env, tcg_constant_i32(i));
        } else {
            tmp = load_reg(s, i);
        }
        gen_aa32_st_i32(s, tmp, addr, mem_idx, MO_UL | MO_ALIGN);

        /* No need to add after the last transfer.  */
        if (++j != n) {
            tcg_gen_addi_i32(addr, addr, 4);
        }
    }

    op_addr_block_post(s, a, addr, n);
    clear_eci_state(s);
    return true;
}

static bool trans_STM(DisasContext *s, arg_ldst_block *a)
{
    return op_stm(s, a);
}

static bool trans_STM_t32(DisasContext *s, arg_ldst_block *a)
{
    /* Writeback register in register list is UNPREDICTABLE for T32.  */
    if (a->w && (a->list & (1 << a->rn))) {
        unallocated_encoding(s);
        return true;
    }
    return op_stm(s, a);
}

static bool do_ldm(DisasContext *s, arg_ldst_block *a)
{
    int i, j, n, list, mem_idx;
    bool loaded_base;
    bool user = a->u;
    bool exc_return = false;
    TCGv_i32 addr, tmp, loaded_var;

    if (user) {
        /* LDM (user), LDM (exception return) */
        if (IS_USER(s)) {
            /* Only usable in supervisor mode.  */
            unallocated_encoding(s);
            return true;
        }
        if (extract32(a->list, 15, 1)) {
            exc_return = true;
            user = false;
        } else {
            /* LDM (user) does not allow writeback.  */
            if (a->w) {
                unallocated_encoding(s);
                return true;
            }
        }
    }

    list = a->list;
    n = ctpop16(list);
    /*
     * This is UNPREDICTABLE for n < 1 in all encodings, and we choose
     * to UNDEF. In the T32 LDM encoding n == 1 is also UNPREDICTABLE,
     * but hardware treats it like the A32 version and implements the
     * single-register-load, and some in-the-wild (buggy) software
     * assumes that, so we don't UNDEF on that case.
     */
    if (n < 1 || a->rn == 15) {
        unallocated_encoding(s);
        return true;
    }

    s->eci_handled = true;

    addr = op_addr_block_pre(s, a, n);
    mem_idx = get_mem_index(s);
    loaded_base = false;
    loaded_var = NULL;

    for (i = j = 0; i < 16; i++) {
        if (!(list & (1 << i))) {
            continue;
        }

        tmp = tcg_temp_new_i32();
        gen_aa32_ld_i32(s, tmp, addr, mem_idx, MO_UL | MO_ALIGN);
        if (user) {
            gen_helper_set_user_reg(tcg_env, tcg_constant_i32(i), tmp);
        } else if (i == a->rn) {
            loaded_var = tmp;
            loaded_base = true;
        } else if (i == 15 && exc_return) {
            store_pc_exc_ret(s, tmp);
        } else {
            store_reg_from_load(s, i, tmp);
        }

        /* No need to add after the last transfer.  */
        if (++j != n) {
            tcg_gen_addi_i32(addr, addr, 4);
        }
    }

    op_addr_block_post(s, a, addr, n);

    if (loaded_base) {
        /* Note that we reject base == pc above.  */
        store_reg(s, a->rn, loaded_var);
    }

    if (exc_return) {
        /* Restore CPSR from SPSR.  */
        tmp = load_cpu_field(spsr);
        translator_io_start(&s->base);
        gen_helper_cpsr_write_eret(tcg_env, tmp);
        /* Must exit loop to check un-masked IRQs */
        s->base.is_jmp = DISAS_EXIT;
    }
    clear_eci_state(s);
    return true;
}

static bool trans_LDM_a32(DisasContext *s, arg_ldst_block *a)
{
    /*
     * Writeback register in register list is UNPREDICTABLE
     * for ArchVersion() >= 7.  Prior to v7, A32 would write
     * an UNKNOWN value to the base register.
     */
    if (ENABLE_ARCH_7 && a->w && (a->list & (1 << a->rn))) {
        unallocated_encoding(s);
        return true;
    }
    return do_ldm(s, a);
}

static bool trans_LDM_t32(DisasContext *s, arg_ldst_block *a)
{
    /* Writeback register in register list is UNPREDICTABLE for T32. */
    if (a->w && (a->list & (1 << a->rn))) {
        unallocated_encoding(s);
        return true;
    }
    return do_ldm(s, a);
}

static bool trans_LDM_t16(DisasContext *s, arg_ldst_block *a)
{
    /* Writeback is conditional on the base register not being loaded.  */
    a->w = !(a->list & (1 << a->rn));
    return do_ldm(s, a);
}

static bool trans_CLRM(DisasContext *s, arg_CLRM *a)
{
    int i;
    TCGv_i32 zero;

    if (!dc_isar_feature(aa32_m_sec_state, s)) {
        return false;
    }

    if (extract32(a->list, 13, 1)) {
        return false;
    }

    if (!a->list) {
        /* UNPREDICTABLE; we choose to UNDEF */
        return false;
    }

    s->eci_handled = true;

    zero = tcg_constant_i32(0);
    for (i = 0; i < 15; i++) {
        if (extract32(a->list, i, 1)) {
            /* Clear R[i] */
            tcg_gen_mov_i32(cpu_R[i], zero);
        }
    }
    if (extract32(a->list, 15, 1)) {
        /*
         * Clear APSR (by calling the MSR helper with the same argument
         * as for "MSR APSR_nzcvqg, Rn": mask = 0b1100, SYSM=0)
         */
        gen_helper_v7m_msr(tcg_env, tcg_constant_i32(0xc00), zero);
    }
    clear_eci_state(s);
    return true;
}

/*
 * Branch, branch with link
 */

static bool trans_B(DisasContext *s, arg_i *a)
{
    gen_jmp(s, jmp_diff(s, a->imm));
    return true;
}

static bool trans_B_cond_thumb(DisasContext *s, arg_ci *a)
{
    /* This has cond from encoding, required to be outside IT block.  */
    if (a->cond >= 0xe) {
        return false;
    }
    if (s->condexec_mask) {
        unallocated_encoding(s);
        return true;
    }
    arm_skip_unless(s, a->cond);
    gen_jmp(s, jmp_diff(s, a->imm));
    return true;
}

static bool trans_BL(DisasContext *s, arg_i *a)
{
    gen_pc_plus_diff(s, cpu_R[14], curr_insn_len(s) | s->thumb);
    gen_jmp(s, jmp_diff(s, a->imm));
    return true;
}

static bool trans_BLX_i(DisasContext *s, arg_BLX_i *a)
{
    /*
     * BLX <imm> would be useless on M-profile; the encoding space
     * is used for other insns from v8.1M onward, and UNDEFs before that.
     */
    if (arm_dc_feature(s, ARM_FEATURE_M)) {
        return false;
    }

    /* For A32, ARM_FEATURE_V5 is checked near the start of the uncond block. */
    if (s->thumb && (a->imm & 2)) {
        return false;
    }
    gen_pc_plus_diff(s, cpu_R[14], curr_insn_len(s) | s->thumb);
    store_cpu_field_constant(!s->thumb, thumb);
    /* This jump is computed from an aligned PC: subtract off the low bits. */
    gen_jmp(s, jmp_diff(s, a->imm - (s->pc_curr & 3)));
    return true;
}

static bool trans_BL_BLX_prefix(DisasContext *s, arg_BL_BLX_prefix *a)
{
    assert(!arm_dc_feature(s, ARM_FEATURE_THUMB2));
    gen_pc_plus_diff(s, cpu_R[14], jmp_diff(s, a->imm << 12));
    return true;
}

static bool trans_BL_suffix(DisasContext *s, arg_BL_suffix *a)
{
    TCGv_i32 tmp = tcg_temp_new_i32();

    assert(!arm_dc_feature(s, ARM_FEATURE_THUMB2));
    tcg_gen_addi_i32(tmp, cpu_R[14], (a->imm << 1) | 1);
    gen_pc_plus_diff(s, cpu_R[14], curr_insn_len(s) | 1);
    gen_bx(s, tmp);
    return true;
}

static bool trans_BLX_suffix(DisasContext *s, arg_BLX_suffix *a)
{
    TCGv_i32 tmp;

    assert(!arm_dc_feature(s, ARM_FEATURE_THUMB2));
    if (!ENABLE_ARCH_5) {
        return false;
    }
    tmp = tcg_temp_new_i32();
    tcg_gen_addi_i32(tmp, cpu_R[14], a->imm << 1);
    tcg_gen_andi_i32(tmp, tmp, 0xfffffffc);
    gen_pc_plus_diff(s, cpu_R[14], curr_insn_len(s) | 1);
    gen_bx(s, tmp);
    return true;
}

static bool trans_BF(DisasContext *s, arg_BF *a)
{
    /*
     * M-profile branch future insns. The architecture permits an
     * implementation to implement these as NOPs (equivalent to
     * discarding the LO_BRANCH_INFO cache immediately), and we
     * take that IMPDEF option because for QEMU a "real" implementation
     * would be complicated and wouldn't execute any faster.
     */
    if (!dc_isar_feature(aa32_lob, s)) {
        return false;
    }
    if (a->boff == 0) {
        /* SEE "Related encodings" (loop insns) */
        return false;
    }
    /* Handle as NOP */
    return true;
}

static bool trans_DLS(DisasContext *s, arg_DLS *a)
{
    /* M-profile low-overhead loop start */
    TCGv_i32 tmp;

    if (!dc_isar_feature(aa32_lob, s)) {
        return false;
    }
    if (a->rn == 13 || a->rn == 15) {
        /*
         * For DLSTP rn == 15 is a related encoding (LCTP); the
         * other cases caught by this condition are all
         * CONSTRAINED UNPREDICTABLE: we choose to UNDEF
         */
        return false;
    }

    if (a->size != 4) {
        /* DLSTP */
        if (!dc_isar_feature(aa32_mve, s)) {
            return false;
        }
        if (!vfp_access_check(s)) {
            return true;
        }
    }

    /* Not a while loop: set LR to the count, and set LTPSIZE for DLSTP */
    tmp = load_reg(s, a->rn);
    store_reg(s, 14, tmp);
    if (a->size != 4) {
        /* DLSTP: set FPSCR.LTPSIZE */
        store_cpu_field(tcg_constant_i32(a->size), v7m.ltpsize);
        s->base.is_jmp = DISAS_UPDATE_NOCHAIN;
    }
    return true;
}

static bool trans_WLS(DisasContext *s, arg_WLS *a)
{
    /* M-profile low-overhead while-loop start */
    TCGv_i32 tmp;
    DisasLabel nextlabel;

    if (!dc_isar_feature(aa32_lob, s)) {
        return false;
    }
    if (a->rn == 13 || a->rn == 15) {
        /*
         * For WLSTP rn == 15 is a related encoding (LE); the
         * other cases caught by this condition are all
         * CONSTRAINED UNPREDICTABLE: we choose to UNDEF
         */
        return false;
    }
    if (s->condexec_mask) {
        /*
         * WLS in an IT block is CONSTRAINED UNPREDICTABLE;
         * we choose to UNDEF, because otherwise our use of
         * gen_goto_tb(1) would clash with the use of TB exit 1
         * in the dc->condjmp condition-failed codepath in
         * arm_tr_tb_stop() and we'd get an assertion.
         */
        return false;
    }
    if (a->size != 4) {
        /* WLSTP */
        if (!dc_isar_feature(aa32_mve, s)) {
            return false;
        }
        /*
         * We need to check that the FPU is enabled here, but mustn't
         * call vfp_access_check() to do that because we don't want to
         * do the lazy state preservation in the "loop count is zero" case.
         * Do the check-and-raise-exception by hand.
         */
        if (s->fp_excp_el) {
            gen_exception_insn_el(s, 0, EXCP_NOCP,
                                  syn_uncategorized(), s->fp_excp_el);
            return true;
        }
    }

    nextlabel = gen_disas_label(s);
    tcg_gen_brcondi_i32(TCG_COND_EQ, cpu_R[a->rn], 0, nextlabel.label);
    tmp = load_reg(s, a->rn);
    store_reg(s, 14, tmp);
    if (a->size != 4) {
        /*
         * WLSTP: set FPSCR.LTPSIZE. This requires that we do the
         * lazy state preservation, new FP context creation, etc,
         * that vfp_access_check() does. We know that the actual
         * access check will succeed (ie it won't generate code that
         * throws an exception) because we did that check by hand earlier.
         */
        bool ok = vfp_access_check(s);
        assert(ok);
        store_cpu_field(tcg_constant_i32(a->size), v7m.ltpsize);
        /*
         * LTPSIZE updated, but MVE_NO_PRED will always be the same thing (0)
         * when we take this upcoming exit from this TB, so gen_jmp_tb() is OK.
         */
    }
    gen_jmp_tb(s, curr_insn_len(s), 1);

    set_disas_label(s, nextlabel);
    gen_jmp(s, jmp_diff(s, a->imm));
    return true;
}

static bool trans_LE(DisasContext *s, arg_LE *a)
{
    /*
     * M-profile low-overhead loop end. The architecture permits an
     * implementation to discard the LO_BRANCH_INFO cache at any time,
     * and we take the IMPDEF option to never set it in the first place
     * (equivalent to always discarding it immediately), because for QEMU
     * a "real" implementation would be complicated and wouldn't execute
     * any faster.
     */
    TCGv_i32 tmp;
    DisasLabel loopend;
    bool fpu_active;

    if (!dc_isar_feature(aa32_lob, s)) {
        return false;
    }
    if (a->f && a->tp) {
        return false;
    }
    if (s->condexec_mask) {
        /*
         * LE in an IT block is CONSTRAINED UNPREDICTABLE;
         * we choose to UNDEF, because otherwise our use of
         * gen_goto_tb(1) would clash with the use of TB exit 1
         * in the dc->condjmp condition-failed codepath in
         * arm_tr_tb_stop() and we'd get an assertion.
         */
        return false;
    }
    if (a->tp) {
        /* LETP */
        if (!dc_isar_feature(aa32_mve, s)) {
            return false;
        }
        if (!vfp_access_check(s)) {
            s->eci_handled = true;
            return true;
        }
    }

    /* LE/LETP is OK with ECI set and leaves it untouched */
    s->eci_handled = true;

    /*
     * With MVE, LTPSIZE might not be 4, and we must emit an INVSTATE
     * UsageFault exception for the LE insn in that case. Note that we
     * are not directly checking FPSCR.LTPSIZE but instead check the
     * pseudocode LTPSIZE() function, which returns 4 if the FPU is
     * not currently active (ie ActiveFPState() returns false). We
     * can identify not-active purely from our TB state flags, as the
     * FPU is active only if:
     *  the FPU is enabled
     *  AND lazy state preservation is not active
     *  AND we do not need a new fp context (this is the ASPEN/FPCA check)
     *
     * Usually we don't need to care about this distinction between
     * LTPSIZE and FPSCR.LTPSIZE, because the code in vfp_access_check()
     * will either take an exception or clear the conditions that make
     * the FPU not active. But LE is an unusual case of a non-FP insn
     * that looks at LTPSIZE.
     */
    fpu_active = !s->fp_excp_el && !s->v7m_lspact && !s->v7m_new_fp_ctxt_needed;

    if (!a->tp && dc_isar_feature(aa32_mve, s) && fpu_active) {
        /* Need to do a runtime check for LTPSIZE != 4 */
        DisasLabel skipexc = gen_disas_label(s);
        tmp = load_cpu_field(v7m.ltpsize);
        tcg_gen_brcondi_i32(TCG_COND_EQ, tmp, 4, skipexc.label);
        gen_exception_insn(s, 0, EXCP_INVSTATE, syn_uncategorized());
        set_disas_label(s, skipexc);
    }

    if (a->f) {
        /* Loop-forever: just jump back to the loop start */
        gen_jmp(s, jmp_diff(s, -a->imm));
        return true;
    }

    /*
     * Not loop-forever. If LR <= loop-decrement-value this is the last loop.
     * For LE, we know at this point that LTPSIZE must be 4 and the
     * loop decrement value is 1. For LETP we need to calculate the decrement
     * value from LTPSIZE.
     */
    loopend = gen_disas_label(s);
    if (!a->tp) {
        tcg_gen_brcondi_i32(TCG_COND_LEU, cpu_R[14], 1, loopend.label);
        tcg_gen_addi_i32(cpu_R[14], cpu_R[14], -1);
    } else {
        /*
         * Decrement by 1 << (4 - LTPSIZE). We need to use a TCG local
         * so that decr stays live after the brcondi.
         */
        TCGv_i32 decr = tcg_temp_new_i32();
        TCGv_i32 ltpsize = load_cpu_field(v7m.ltpsize);
        tcg_gen_sub_i32(decr, tcg_constant_i32(4), ltpsize);
        tcg_gen_shl_i32(decr, tcg_constant_i32(1), decr);

        tcg_gen_brcond_i32(TCG_COND_LEU, cpu_R[14], decr, loopend.label);

        tcg_gen_sub_i32(cpu_R[14], cpu_R[14], decr);
    }
    /* Jump back to the loop start */
    gen_jmp(s, jmp_diff(s, -a->imm));

    set_disas_label(s, loopend);
    if (a->tp) {
        /* Exits from tail-pred loops must reset LTPSIZE to 4 */
        store_cpu_field(tcg_constant_i32(4), v7m.ltpsize);
    }
    /* End TB, continuing to following insn */
    gen_jmp_tb(s, curr_insn_len(s), 1);
    return true;
}

static bool trans_LCTP(DisasContext *s, arg_LCTP *a)
{
    /*
     * M-profile Loop Clear with Tail Predication. Since our implementation
     * doesn't cache branch information, all we need to do is reset
     * FPSCR.LTPSIZE to 4.
     */

    if (!dc_isar_feature(aa32_lob, s) ||
        !dc_isar_feature(aa32_mve, s)) {
        return false;
    }

    if (!vfp_access_check(s)) {
        return true;
    }

    store_cpu_field_constant(4, v7m.ltpsize);
    return true;
}

static bool trans_VCTP(DisasContext *s, arg_VCTP *a)
{
    /*
     * M-profile Create Vector Tail Predicate. This insn is itself
     * predicated and is subject to beatwise execution.
     */
    TCGv_i32 rn_shifted, masklen;

    if (!dc_isar_feature(aa32_mve, s) || a->rn == 13 || a->rn == 15) {
        return false;
    }

    if (!mve_eci_check(s) || !vfp_access_check(s)) {
        return true;
    }

    /*
     * We pre-calculate the mask length here to avoid having
     * to have multiple helpers specialized for size.
     * We pass the helper "rn <= (1 << (4 - size)) ? (rn << size) : 16".
     */
    rn_shifted = tcg_temp_new_i32();
    masklen = load_reg(s, a->rn);
    tcg_gen_shli_i32(rn_shifted, masklen, a->size);
    tcg_gen_movcond_i32(TCG_COND_LEU, masklen,
                        masklen, tcg_constant_i32(1 << (4 - a->size)),
                        rn_shifted, tcg_constant_i32(16));
    gen_helper_mve_vctp(tcg_env, masklen);
    /* This insn updates predication bits */
    s->base.is_jmp = DISAS_UPDATE_NOCHAIN;
    mve_update_eci(s);
    return true;
}

static bool op_tbranch(DisasContext *s, arg_tbranch *a, bool half)
{
    TCGv_i32 addr, tmp;

    tmp = load_reg(s, a->rm);
    if (half) {
        tcg_gen_add_i32(tmp, tmp, tmp);
    }
    addr = load_reg(s, a->rn);
    tcg_gen_add_i32(addr, addr, tmp);

    gen_aa32_ld_i32(s, tmp, addr, get_mem_index(s), half ? MO_UW : MO_UB);

    tcg_gen_add_i32(tmp, tmp, tmp);
    gen_pc_plus_diff(s, addr, jmp_diff(s, 0));
    tcg_gen_add_i32(tmp, tmp, addr);
    store_reg(s, 15, tmp);
    return true;
}

static bool trans_TBB(DisasContext *s, arg_tbranch *a)
{
    return op_tbranch(s, a, false);
}

static bool trans_TBH(DisasContext *s, arg_tbranch *a)
{
    return op_tbranch(s, a, true);
}

static bool trans_CBZ(DisasContext *s, arg_CBZ *a)
{
    TCGv_i32 tmp = load_reg(s, a->rn);

    arm_gen_condlabel(s);
    tcg_gen_brcondi_i32(a->nz ? TCG_COND_EQ : TCG_COND_NE,
                        tmp, 0, s->condlabel.label);
    gen_jmp(s, jmp_diff(s, a->imm));
    return true;
}

/*
 * Supervisor call - both T32 & A32 come here so we need to check
 * which mode we are in when checking for semihosting.
 */

static bool trans_SVC(DisasContext *s, arg_SVC *a)
{
    const uint32_t semihost_imm = s->thumb ? 0xab : 0x123456;

    if (!arm_dc_feature(s, ARM_FEATURE_M) &&
        semihosting_enabled(s->current_el == 0) &&
        (a->imm == semihost_imm)) {
        gen_exception_internal_insn(s, EXCP_SEMIHOST);
    } else {
        if (s->fgt_svc) {
            uint32_t syndrome = syn_aa32_svc(a->imm, s->thumb);
            gen_exception_insn_el(s, 0, EXCP_UDEF, syndrome, 2);
        } else {
            gen_update_pc(s, curr_insn_len(s));
            s->svc_imm = a->imm;
            s->base.is_jmp = DISAS_SWI;
        }
    }
    return true;
}

/*
 * Unconditional system instructions
 */

static bool trans_RFE(DisasContext *s, arg_RFE *a)
{
    static const int8_t pre_offset[4] = {
        /* DA */ -4, /* IA */ 0, /* DB */ -8, /* IB */ 4
    };
    static const int8_t post_offset[4] = {
        /* DA */ -8, /* IA */ 4, /* DB */ -4, /* IB */ 0
    };
    TCGv_i32 addr, t1, t2;

    if (!ENABLE_ARCH_6 || arm_dc_feature(s, ARM_FEATURE_M)) {
        return false;
    }
    if (IS_USER(s)) {
        unallocated_encoding(s);
        return true;
    }

    addr = load_reg(s, a->rn);
    tcg_gen_addi_i32(addr, addr, pre_offset[a->pu]);

    /* Load PC into tmp and CPSR into tmp2.  */
    t1 = tcg_temp_new_i32();
    gen_aa32_ld_i32(s, t1, addr, get_mem_index(s), MO_UL | MO_ALIGN);
    tcg_gen_addi_i32(addr, addr, 4);
    t2 = tcg_temp_new_i32();
    gen_aa32_ld_i32(s, t2, addr, get_mem_index(s), MO_UL | MO_ALIGN);

    if (a->w) {
        /* Base writeback.  */
        tcg_gen_addi_i32(addr, addr, post_offset[a->pu]);
        store_reg(s, a->rn, addr);
    }
    gen_rfe(s, t1, t2);
    return true;
}

static bool trans_SRS(DisasContext *s, arg_SRS *a)
{
    if (!ENABLE_ARCH_6 || arm_dc_feature(s, ARM_FEATURE_M)) {
        return false;
    }
    gen_srs(s, a->mode, a->pu, a->w);
    return true;
}

static bool trans_CPS(DisasContext *s, arg_CPS *a)
{
    uint32_t mask, val;

    if (!ENABLE_ARCH_6 || arm_dc_feature(s, ARM_FEATURE_M)) {
        return false;
    }
    if (IS_USER(s)) {
        /* Implemented as NOP in user mode.  */
        return true;
    }
    /* TODO: There are quite a lot of UNPREDICTABLE argument combinations. */

    mask = val = 0;
    if (a->imod & 2) {
        if (a->A) {
            mask |= CPSR_A;
        }
        if (a->I) {
            mask |= CPSR_I;
        }
        if (a->F) {
            mask |= CPSR_F;
        }
        if (a->imod & 1) {
            val |= mask;
        }
    }
    if (a->M) {
        mask |= CPSR_M;
        val |= a->mode;
    }
    if (mask) {
        gen_set_psr_im(s, mask, 0, val);
    }
    return true;
}

static bool trans_CPS_v7m(DisasContext *s, arg_CPS_v7m *a)
{
    TCGv_i32 tmp, addr;

    if (!arm_dc_feature(s, ARM_FEATURE_M)) {
        return false;
    }
    if (IS_USER(s)) {
        /* Implemented as NOP in user mode.  */
        return true;
    }

    tmp = tcg_constant_i32(a->im);
    /* FAULTMASK */
    if (a->F) {
        addr = tcg_constant_i32(19);
        gen_helper_v7m_msr(tcg_env, addr, tmp);
    }
    /* PRIMASK */
    if (a->I) {
        addr = tcg_constant_i32(16);
        gen_helper_v7m_msr(tcg_env, addr, tmp);
    }
    gen_rebuild_hflags(s, false);
    gen_lookup_tb(s);
    return true;
}

/*
 * Clear-Exclusive, Barriers
 */

static bool trans_CLREX(DisasContext *s, arg_CLREX *a)
{
    if (s->thumb
        ? !ENABLE_ARCH_7 && !arm_dc_feature(s, ARM_FEATURE_M)
        : !ENABLE_ARCH_6K) {
        return false;
    }
    gen_clrex(s);
    return true;
}

static bool trans_DSB(DisasContext *s, arg_DSB *a)
{
    if (!ENABLE_ARCH_7 && !arm_dc_feature(s, ARM_FEATURE_M)) {
        return false;
    }
    tcg_gen_mb(TCG_MO_ALL | TCG_BAR_SC);
    return true;
}

static bool trans_DMB(DisasContext *s, arg_DMB *a)
{
    return trans_DSB(s, NULL);
}

static bool trans_ISB(DisasContext *s, arg_ISB *a)
{
    if (!ENABLE_ARCH_7 && !arm_dc_feature(s, ARM_FEATURE_M)) {
        return false;
    }
    /*
     * We need to break the TB after this insn to execute
     * self-modifying code correctly and also to take
     * any pending interrupts immediately.
     */
    s->base.is_jmp = DISAS_TOO_MANY;
    return true;
}

static bool trans_SB(DisasContext *s, arg_SB *a)
{
    if (!dc_isar_feature(aa32_sb, s)) {
        return false;
    }
    /*
     * TODO: There is no speculation barrier opcode
     * for TCG; MB and end the TB instead.
     */
    tcg_gen_mb(TCG_MO_ALL | TCG_BAR_SC);
    s->base.is_jmp = DISAS_TOO_MANY;
    return true;
}

static bool trans_SETEND(DisasContext *s, arg_SETEND *a)
{
    if (!ENABLE_ARCH_6) {
        return false;
    }
    if (a->E != (s->be_data == MO_BE)) {
        gen_helper_setend(tcg_env);
        s->base.is_jmp = DISAS_UPDATE_EXIT;
    }
    return true;
}

/*
 * Preload instructions
 * All are nops, contingent on the appropriate arch level.
 */

static bool trans_PLD(DisasContext *s, arg_PLD *a)
{
    return ENABLE_ARCH_5TE;
}

static bool trans_PLDW(DisasContext *s, arg_PLDW *a)
{
    return arm_dc_feature(s, ARM_FEATURE_V7MP);
}

static bool trans_PLI(DisasContext *s, arg_PLI *a)
{
    return ENABLE_ARCH_7;
}

/*
 * If-then
 */

static bool trans_IT(DisasContext *s, arg_IT *a)
{
    int cond_mask = a->cond_mask;

    /*
     * No actual code generated for this insn, just setup state.
     *
     * Combinations of firstcond and mask which set up an 0b1111
     * condition are UNPREDICTABLE; we take the CONSTRAINED
     * UNPREDICTABLE choice to treat 0b1111 the same as 0b1110,
     * i.e. both meaning "execute always".
     */
    s->condexec_cond = (cond_mask >> 4) & 0xe;
    s->condexec_mask = cond_mask & 0x1f;
    return true;
}

/* v8.1M CSEL/CSINC/CSNEG/CSINV */
static bool trans_CSEL(DisasContext *s, arg_CSEL *a)
{
    TCGv_i32 rn, rm;
    DisasCompare c;

    if (!arm_dc_feature(s, ARM_FEATURE_V8_1M)) {
        return false;
    }

    if (a->rm == 13) {
        /* SEE "Related encodings" (MVE shifts) */
        return false;
    }

    if (a->rd == 13 || a->rd == 15 || a->rn == 13 || a->fcond >= 14) {
        /* CONSTRAINED UNPREDICTABLE: we choose to UNDEF */
        return false;
    }

    /* In this insn input reg fields of 0b1111 mean "zero", not "PC" */
    rn = tcg_temp_new_i32();
    rm = tcg_temp_new_i32();
    if (a->rn == 15) {
        tcg_gen_movi_i32(rn, 0);
    } else {
        load_reg_var(s, rn, a->rn);
    }
    if (a->rm == 15) {
        tcg_gen_movi_i32(rm, 0);
    } else {
        load_reg_var(s, rm, a->rm);
    }

    switch (a->op) {
    case 0: /* CSEL */
        break;
    case 1: /* CSINC */
        tcg_gen_addi_i32(rm, rm, 1);
        break;
    case 2: /* CSINV */
        tcg_gen_not_i32(rm, rm);
        break;
    case 3: /* CSNEG */
        tcg_gen_neg_i32(rm, rm);
        break;
    default:
        g_assert_not_reached();
    }

    arm_test_cc(&c, a->fcond);
    tcg_gen_movcond_i32(c.cond, rn, c.value, tcg_constant_i32(0), rn, rm);

    store_reg(s, a->rd, rn);
    return true;
}

/*
 * Legacy decoder.
 */

static void disas_arm_insn(DisasContext *s, unsigned int insn)
{
    unsigned int cond = insn >> 28;

    /* M variants do not implement ARM mode; this must raise the INVSTATE
     * UsageFault exception.
     */
    if (arm_dc_feature(s, ARM_FEATURE_M)) {
        gen_exception_insn(s, 0, EXCP_INVSTATE, syn_uncategorized());
        return;
    }

    if (s->pstate_il) {
        /*
         * Illegal execution state. This has priority over BTI
         * exceptions, but comes after instruction abort exceptions.
         */
        gen_exception_insn(s, 0, EXCP_UDEF, syn_illegalstate());
        return;
    }

    if (cond == 0xf) {
        /* In ARMv3 and v4 the NV condition is UNPREDICTABLE; we
         * choose to UNDEF. In ARMv5 and above the space is used
         * for miscellaneous unconditional instructions.
         */
        if (!arm_dc_feature(s, ARM_FEATURE_V5)) {
            unallocated_encoding(s);
            return;
        }

        /* Unconditional instructions.  */
        /* TODO: Perhaps merge these into one decodetree output file.  */
        if (disas_a32_uncond(s, insn) ||
            disas_vfp_uncond(s, insn) ||
            disas_neon_dp(s, insn) ||
            disas_neon_ls(s, insn) ||
            disas_neon_shared(s, insn)) {
            return;
        }
        /* fall back to legacy decoder */

        if ((insn & 0x0e000f00) == 0x0c000100) {
            if (arm_dc_feature(s, ARM_FEATURE_IWMMXT)) {
                /* iWMMXt register transfer.  */
                if (extract32(s->c15_cpar, 1, 1)) {
                    if (!disas_iwmmxt_insn(s, insn)) {
                        return;
                    }
                }
            }
        }
        goto illegal_op;
    }
    if (cond != 0xe) {
        /* if not always execute, we generate a conditional jump to
           next instruction */
        arm_skip_unless(s, cond);
    }

    /* TODO: Perhaps merge these into one decodetree output file.  */
    if (disas_a32(s, insn) ||
        disas_vfp(s, insn)) {
        return;
    }
    /* fall back to legacy decoder */
    /* TODO: convert xscale/iwmmxt decoder to decodetree ?? */
    if (arm_dc_feature(s, ARM_FEATURE_XSCALE)) {
        if (((insn & 0x0c000e00) == 0x0c000000)
            && ((insn & 0x03000000) != 0x03000000)) {
            /* Coprocessor insn, coprocessor 0 or 1 */
            disas_xscale_insn(s, insn);
            return;
        }
    }

illegal_op:
    unallocated_encoding(s);
}

static bool thumb_insn_is_16bit(DisasContext *s, uint32_t pc, uint32_t insn)
{
    /*
     * Return true if this is a 16 bit instruction. We must be precise
     * about this (matching the decode).
     */
    if ((insn >> 11) < 0x1d) {
        /* Definitely a 16-bit instruction */
        return true;
    }

    /* Top five bits 0b11101 / 0b11110 / 0b11111 : this is the
     * first half of a 32-bit Thumb insn. Thumb-1 cores might
     * end up actually treating this as two 16-bit insns, though,
     * if it's half of a bl/blx pair that might span a page boundary.
     */
    if (arm_dc_feature(s, ARM_FEATURE_THUMB2) ||
        arm_dc_feature(s, ARM_FEATURE_M)) {
        /* Thumb2 cores (including all M profile ones) always treat
         * 32-bit insns as 32-bit.
         */
        return false;
    }

    if ((insn >> 11) == 0x1e && pc - s->page_start < TARGET_PAGE_SIZE - 3) {
        /* 0b1111_0xxx_xxxx_xxxx : BL/BLX prefix, and the suffix
         * is not on the next page; we merge this into a 32-bit
         * insn.
         */
        return false;
    }
    /* 0b1110_1xxx_xxxx_xxxx : BLX suffix (or UNDEF);
     * 0b1111_1xxx_xxxx_xxxx : BL suffix;
     * 0b1111_0xxx_xxxx_xxxx : BL/BLX prefix on the end of a page
     *  -- handle as single 16 bit insn
     */
    return true;
}

/* Translate a 32-bit thumb instruction. */
static void disas_thumb2_insn(DisasContext *s, uint32_t insn)
{
    /*
     * ARMv6-M supports a limited subset of Thumb2 instructions.
     * Other Thumb1 architectures allow only 32-bit
     * combined BL/BLX prefix and suffix.
     */
    if (arm_dc_feature(s, ARM_FEATURE_M) &&
        !arm_dc_feature(s, ARM_FEATURE_V7)) {
        int i;
        bool found = false;
        static const uint32_t armv6m_insn[] = {0xf3808000 /* msr */,
                                               0xf3b08040 /* dsb */,
                                               0xf3b08050 /* dmb */,
                                               0xf3b08060 /* isb */,
                                               0xf3e08000 /* mrs */,
                                               0xf000d000 /* bl */};
        static const uint32_t armv6m_mask[] = {0xffe0d000,
                                               0xfff0d0f0,  ×3
                                               0xffe0d000,
                                               0xf800d000};

        for (i = 0; i < ARRAY_SIZE(armv6m_insn); i++) {
            if ((insn & armv6m_mask[i]) == armv6m_insn[i]) {
                found = true;
                break;
            }
        }
        if (!found) {
            goto illegal_op;
        }
    } else if ((insn & 0xf800e800) != 0xf000e800)  {
        if (!arm_dc_feature(s, ARM_FEATURE_THUMB2)) {
            unallocated_encoding(s);
            return;
        }
    }

    if (arm_dc_feature(s, ARM_FEATURE_M)) {
        /*
         * NOCP takes precedence over any UNDEF for (almost) the
         * entire wide range of coprocessor-space encodings, so check
         * for it first before proceeding to actually decode eg VFP
         * insns. This decode also handles the few insns which are
         * in copro space but do not have NOCP checks (eg VLLDM, VLSTM).
         */
        if (disas_m_nocp(s, insn)) {
            return;
        }
    }

    if ((insn & 0xef000000) == 0xef000000) {
        /*
         * T32 encodings 0b111p_1111_qqqq_qqqq_qqqq_qqqq_qqqq_qqqq
         * transform into
         * A32 encodings 0b1111_001p_qqqq_qqqq_qqqq_qqqq_qqqq_qqqq
         */
        uint32_t a32_insn = (insn & 0xe2ffffff) |
            ((insn & (1 << 28)) >> 4) | (1 << 28);

        if (disas_neon_dp(s, a32_insn)) {
            return;
        }
    }

    if ((insn & 0xff100000) == 0xf9000000) {
        /*
         * T32 encodings 0b1111_1001_ppp0_qqqq_qqqq_qqqq_qqqq_qqqq
         * transform into
         * A32 encodings 0b1111_0100_ppp0_qqqq_qqqq_qqqq_qqqq_qqqq
         */
        uint32_t a32_insn = (insn & 0x00ffffff) | 0xf4000000;

        if (disas_neon_ls(s, a32_insn)) {
            return;
        }
    }

    /*
     * TODO: Perhaps merge these into one decodetree output file.
     * Note disas_vfp is written for a32 with cond field in the
     * top nibble.  The t32 encoding requires 0xe in the top nibble.
     */
    if (disas_t32(s, insn) ||
        disas_vfp_uncond(s, insn) ||
        disas_neon_shared(s, insn) ||
        disas_mve(s, insn) ||
        ((insn >> 28) == 0xe && disas_vfp(s, insn))) {
        return;
    }

illegal_op:
    unallocated_encoding(s);
}

static void disas_thumb_insn(DisasContext *s, uint32_t insn)
{
    if (!disas_t16(s, insn)) {
        unallocated_encoding(s);
    }
}

static bool insn_crosses_page(CPUARMState *env, DisasContext *s)
{
    /* Return true if the insn at dc->base.pc_next might cross a page boundary.
     * (False positives are OK, false negatives are not.)
     * We know this is a Thumb insn, and our caller ensures we are
     * only called if dc->base.pc_next is less than 4 bytes from the page
     * boundary, so we cross the page if the first 16 bits indicate
     * that this is a 32 bit insn.
     */
    uint16_t insn = arm_lduw_code(env, &s->base, s->base.pc_next, s->sctlr_b);

    return !thumb_insn_is_16bit(s, s->base.pc_next, insn);
}

static void arm_tr_init_disas_context(DisasContextBase *dcbase, CPUState *cs)
{
    DisasContext *dc = container_of(dcbase, DisasContext, base);
    CPUARMState *env = cpu_env(cs);
    ARMCPU *cpu = env_archcpu(env);
    CPUARMTBFlags tb_flags = arm_tbflags_from_tb(dc->base.tb);
    uint32_t condexec, core_mmu_idx;

    dc->isar = &cpu->isar;
    dc->condjmp = 0;
    dc->pc_save = dc->base.pc_first;
    dc->aarch64 = false;
    dc->thumb = EX_TBFLAG_AM32(tb_flags, THUMB);
    dc->be_data = EX_TBFLAG_ANY(tb_flags, BE_DATA) ? MO_BE : MO_LE;
    condexec = EX_TBFLAG_AM32(tb_flags, CONDEXEC);
    /*
     * the CONDEXEC TB flags are CPSR bits [15:10][26:25]. On A-profile this
     * is always the IT bits. On M-profile, some of the reserved encodings
     * of IT are used instead to indicate either ICI or ECI, which
     * indicate partial progress of a restartable insn that was interrupted
     * partway through by an exception:
     *  * if CONDEXEC[3:0] != 0b0000 : CONDEXEC is IT bits
     *  * if CONDEXEC[3:0] == 0b0000 : CONDEXEC is ICI or ECI bits
     * In all cases CONDEXEC == 0 means "not in IT block or restartable
     * insn, behave normally".
     */
    dc->eci = dc->condexec_mask = dc->condexec_cond = 0;
    dc->eci_handled = false;
    if (condexec & 0xf) {
        dc->condexec_mask = (condexec & 0xf) << 1;
        dc->condexec_cond = condexec >> 4;
    } else {
        if (arm_feature(env, ARM_FEATURE_M)) {
            dc->eci = condexec >> 4;
        }
    }

    core_mmu_idx = EX_TBFLAG_ANY(tb_flags, MMUIDX);
    dc->mmu_idx = core_to_arm_mmu_idx(env, core_mmu_idx);
    dc->current_el = arm_mmu_idx_to_el(dc->mmu_idx);
#if !defined(CONFIG_USER_ONLY)
    dc->user = (dc->current_el == 0);
#endif
    dc->fp_excp_el = EX_TBFLAG_ANY(tb_flags, FPEXC_EL);
    dc->align_mem = EX_TBFLAG_ANY(tb_flags, ALIGN_MEM);
    dc->pstate_il = EX_TBFLAG_ANY(tb_flags, PSTATE__IL);
    dc->fgt_active = EX_TBFLAG_ANY(tb_flags, FGT_ACTIVE);
    dc->fgt_svc = EX_TBFLAG_ANY(tb_flags, FGT_SVC);

    if (arm_feature(env, ARM_FEATURE_M)) {
        dc->vfp_enabled = 1;
        dc->be_data = MO_TE;
        dc->v7m_handler_mode = EX_TBFLAG_M32(tb_flags, HANDLER);
        dc->v8m_secure = EX_TBFLAG_M32(tb_flags, SECURE);
        dc->v8m_stackcheck = EX_TBFLAG_M32(tb_flags, STACKCHECK);
        dc->v8m_fpccr_s_wrong = EX_TBFLAG_M32(tb_flags, FPCCR_S_WRONG);
        dc->v7m_new_fp_ctxt_needed =
            EX_TBFLAG_M32(tb_flags, NEW_FP_CTXT_NEEDED);
        dc->v7m_lspact = EX_TBFLAG_M32(tb_flags, LSPACT);
        dc->mve_no_pred = EX_TBFLAG_M32(tb_flags, MVE_NO_PRED);
    } else {
        dc->sctlr_b = EX_TBFLAG_A32(tb_flags, SCTLR__B);
        dc->hstr_active = EX_TBFLAG_A32(tb_flags, HSTR_ACTIVE);
        dc->ns = EX_TBFLAG_A32(tb_flags, NS);
        dc->vfp_enabled = EX_TBFLAG_A32(tb_flags, VFPEN);
        if (arm_feature(env, ARM_FEATURE_XSCALE)) {
            dc->c15_cpar = EX_TBFLAG_A32(tb_flags, XSCALE_CPAR);
        } else {
            dc->vec_len = EX_TBFLAG_A32(tb_flags, VECLEN);
            dc->vec_stride = EX_TBFLAG_A32(tb_flags, VECSTRIDE);
        }
        dc->sme_trap_nonstreaming =
            EX_TBFLAG_A32(tb_flags, SME_TRAP_NONSTREAMING);
    }
    dc->lse2 = false; /* applies only to aarch64 */
    dc->cp_regs = cpu->cp_regs;
    dc->features = env->features;

    /* Single step state. The code-generation logic here is:
     *  SS_ACTIVE == 0:
     *   generate code with no special handling for single-stepping (except
     *   that anything that can make us go to SS_ACTIVE == 1 must end the TB;
     *   this happens anyway because those changes are all system register or
     *   PSTATE writes).
     *  SS_ACTIVE == 1, PSTATE.SS == 1: (active-not-pending)
     *   emit code for one insn
     *   emit code to clear PSTATE.SS
     *   emit code to generate software step exception for completed step
     *   end TB (as usual for having generated an exception)
     *  SS_ACTIVE == 1, PSTATE.SS == 0: (active-pending)
     *   emit code to generate a software step exception
     *   end the TB
     */
    dc->ss_active = EX_TBFLAG_ANY(tb_flags, SS_ACTIVE);
    dc->pstate_ss = EX_TBFLAG_ANY(tb_flags, PSTATE__SS);
    dc->is_ldex = false;

    dc->page_start = dc->base.pc_first & TARGET_PAGE_MASK;

    /* If architectural single step active, limit to 1.  */
    if (dc->ss_active) {
        dc->base.max_insns = 1;
    }

    /* ARM is a fixed-length ISA.  Bound the number of insns to execute
       to those left on the page.  */
    if (!dc->thumb) {
        int bound = -(dc->base.pc_first | TARGET_PAGE_MASK) / 4;
        dc->base.max_insns = MIN(dc->base.max_insns, bound);
    }

    cpu_V0 = tcg_temp_new_i64();
    cpu_V1 = tcg_temp_new_i64();
    cpu_M0 = tcg_temp_new_i64();
}

static void arm_tr_tb_start(DisasContextBase *dcbase, CPUState *cpu)
{
    DisasContext *dc = container_of(dcbase, DisasContext, base);

    /* A note on handling of the condexec (IT) bits:
     *
     * We want to avoid the overhead of having to write the updated condexec
     * bits back to the CPUARMState for every instruction in an IT block. So:
     * (1) if the condexec bits are not already zero then we write
     * zero back into the CPUARMState now. This avoids complications trying
     * to do it at the end of the block. (For example if we don't do this
     * it's hard to identify whether we can safely skip writing condexec
     * at the end of the TB, which we definitely want to do for the case
     * where a TB doesn't do anything with the IT state at all.)
     * (2) if we are going to leave the TB then we call gen_set_condexec()
     * which will write the correct value into CPUARMState if zero is wrong.
     * This is done both for leaving the TB at the end, and for leaving
     * it because of an exception we know will happen, which is done in
     * gen_exception_insn(). The latter is necessary because we need to
     * leave the TB with the PC/IT state just prior to execution of the
     * instruction which caused the exception.
     * (3) if we leave the TB unexpectedly (eg a data abort on a load)
     * then the CPUARMState will be wrong and we need to reset it.
     * This is handled in the same way as restoration of the
     * PC in these situations; we save the value of the condexec bits
     * for each PC via tcg_gen_insn_start(), and restore_state_to_opc()
     * then uses this to restore them after an exception.
     *
     * Note that there are no instructions which can read the condexec
     * bits, and none which can write non-static values to them, so
     * we don't need to care about whether CPUARMState is correct in the
     * middle of a TB.
     */

    /* Reset the conditional execution bits immediately. This avoids
       complications trying to do it at the end of the block.  */
    if (dc->condexec_mask || dc->condexec_cond) {
        store_cpu_field_constant(0, condexec_bits);
    }
}

static void arm_tr_insn_start(DisasContextBase *dcbase, CPUState *cpu)
{
    DisasContext *dc = container_of(dcbase, DisasContext, base);
    /*
     * The ECI/ICI bits share PSR bits with the IT bits, so we
     * need to reconstitute the bits from the split-out DisasContext
     * fields here.
     */
    uint32_t condexec_bits;
    target_ulong pc_arg = dc->base.pc_next;

    if (tb_cflags(dcbase->tb) & CF_PCREL) {
        pc_arg &= ~TARGET_PAGE_MASK;
    }
    if (dc->eci) {
        condexec_bits = dc->eci << 4;
    } else {
        condexec_bits = (dc->condexec_cond << 4) | (dc->condexec_mask >> 1);
    }
    tcg_gen_insn_start(pc_arg, condexec_bits, 0);
    dc->insn_start_updated = false;
}

static bool arm_check_kernelpage(DisasContext *dc)
{
#ifdef CONFIG_USER_ONLY
    /* Intercept jump to the magic kernel page.  */
    if (dc->base.pc_next >= 0xffff0000) {
        /* We always get here via a jump, so know we are not in a
           conditional execution block.  */
        gen_exception_internal(EXCP_KERNEL_TRAP);
        dc->base.is_jmp = DISAS_NORETURN;
        return true;
    }
#endif
    return false;
}

static bool arm_check_ss_active(DisasContext *dc)
{
    if (dc->ss_active && !dc->pstate_ss) {
        /* Singlestep state is Active-pending.
         * If we're in this state at the start of a TB then either
         *  a) we just took an exception to an EL which is being debugged
         *     and this is the first insn in the exception handler
         *  b) debug exceptions were masked and we just unmasked them
         *     without changing EL (eg by clearing PSTATE.D)
         * In either case we're going to take a swstep exception in the
         * "did not step an insn" case, and so the syndrome ISV and EX
         * bits should be zero.
         */
        assert(dc->base.num_insns == 1);
        gen_swstep_exception(dc, 0, 0);
        dc->base.is_jmp = DISAS_NORETURN;
        return true;
    }

    return false;
}

static void arm_post_translate_insn(DisasContext *dc)
{
    if (dc->condjmp &&
        (dc->base.is_jmp == DISAS_NEXT || dc->base.is_jmp == DISAS_TOO_MANY)) {
        if (dc->pc_save != dc->condlabel.pc_save) {
            gen_update_pc(dc, dc->condlabel.pc_save - dc->pc_save);
        }
        gen_set_label(dc->condlabel.label);
        dc->condjmp = 0;
    }
}

static void arm_tr_translate_insn(DisasContextBase *dcbase, CPUState *cpu)
{
    DisasContext *dc = container_of(dcbase, DisasContext, base);
    CPUARMState *env = cpu_env(cpu);
    uint32_t pc = dc->base.pc_next;
    unsigned int insn;

    /* Singlestep exceptions have the highest priority. */
    if (arm_check_ss_active(dc)) {
        dc->base.pc_next = pc + 4;
        return;
    }

    if (pc & 3) {
        /*
         * PC alignment fault.  This has priority over the instruction abort
         * that we would receive from a translation fault via arm_ldl_code
         * (or the execution of the kernelpage entrypoint). This should only
         * be possible after an indirect branch, at the start of the TB.
         */
        assert(dc->base.num_insns == 1);
        gen_helper_exception_pc_alignment(tcg_env, tcg_constant_tl(pc));
        dc->base.is_jmp = DISAS_NORETURN;
        dc->base.pc_next = QEMU_ALIGN_UP(pc, 4);
        return;
    }

    if (arm_check_kernelpage(dc)) {
        dc->base.pc_next = pc + 4;
        return;
    }

    dc->pc_curr = pc;
    insn = arm_ldl_code(env, &dc->base, pc, dc->sctlr_b);
    dc->insn = insn;
    dc->base.pc_next = pc + 4;
    disas_arm_insn(dc, insn);

    arm_post_translate_insn(dc);

    /* ARM is a fixed-length ISA.  We performed the cross-page check
       in init_disas_context by adjusting max_insns.  */
}

static bool thumb_insn_is_unconditional(DisasContext *s, uint32_t insn)
{
    /* Return true if this Thumb insn is always unconditional,
     * even inside an IT block. This is true of only a very few
     * instructions: BKPT, HLT, and SG.
     *
     * A larger class of instructions are UNPREDICTABLE if used
     * inside an IT block; we do not need to detect those here, because
     * what we do by default (perform the cc check and update the IT
     * bits state machine) is a permitted CONSTRAINED UNPREDICTABLE
     * choice for those situations.
     *
     * insn is either a 16-bit or a 32-bit instruction; the two are
     * distinguishable because for the 16-bit case the top 16 bits
     * are zeroes, and that isn't a valid 32-bit encoding.
     */
    if ((insn & 0xffffff00) == 0xbe00) {
        /* BKPT */
        return true;
    }

    if ((insn & 0xffffffc0) == 0xba80 && arm_dc_feature(s, ARM_FEATURE_V8) &&
        !arm_dc_feature(s, ARM_FEATURE_M)) {
        /* HLT: v8A only. This is unconditional even when it is going to
         * UNDEF; see the v8A ARM ARM DDI0487B.a H3.3.
         * For v7 cores this was a plain old undefined encoding and so
         * honours its cc check. (We might be using the encoding as
         * a semihosting trap, but we don't change the cc check behaviour
         * on that account, because a debugger connected to a real v7A
         * core and emulating semihosting traps by catching the UNDEF
         * exception would also only see cases where the cc check passed.
         * No guest code should be trying to do a HLT semihosting trap
         * in an IT block anyway.
         */
        return true;
    }

    if (insn == 0xe97fe97f && arm_dc_feature(s, ARM_FEATURE_V8) &&
        arm_dc_feature(s, ARM_FEATURE_M)) {
        /* SG: v8M only */
        return true;
    }

    return false;
}

static void thumb_tr_translate_insn(DisasContextBase *dcbase, CPUState *cpu)
{
    DisasContext *dc = container_of(dcbase, DisasContext, base);
    CPUARMState *env = cpu_env(cpu);
    uint32_t pc = dc->base.pc_next;
    uint32_t insn;
    bool is_16bit;
    /* TCG op to rewind to if this turns out to be an invalid ECI state */
    TCGOp *insn_eci_rewind = NULL;
    target_ulong insn_eci_pc_save = -1;

    /* Misaligned thumb PC is architecturally impossible. */
    assert((dc->base.pc_next & 1) == 0);

    if (arm_check_ss_active(dc) || arm_check_kernelpage(dc)) {
        dc->base.pc_next = pc + 2;
        return;
    }

    dc->pc_curr = pc;
    insn = arm_lduw_code(env, &dc->base, pc, dc->sctlr_b);
    is_16bit = thumb_insn_is_16bit(dc, dc->base.pc_next, insn);
    pc += 2;
    if (!is_16bit) {
        uint32_t insn2 = arm_lduw_code(env, &dc->base, pc, dc->sctlr_b);
        insn = insn << 16 | insn2;
        pc += 2;
    }
    dc->base.pc_next = pc;
    dc->insn = insn;

    if (dc->pstate_il) {
        /*
         * Illegal execution state. This has priority over BTI
         * exceptions, but comes after instruction abort exceptions.
         */
        gen_exception_insn(dc, 0, EXCP_UDEF, syn_illegalstate());
        return;
    }

    if (dc->eci) {
        /*
         * For M-profile continuable instructions, ECI/ICI handling
         * falls into these cases:
         *  - interrupt-continuable instructions
         *     These are the various load/store multiple insns (both
         *     integer and fp). The ICI bits indicate the register
         *     where the load/store can resume. We make the IMPDEF
         *     choice to always do "instruction restart", ie ignore
         *     the ICI value and always execute the ldm/stm from the
         *     start. So all we need to do is zero PSR.ICI if the
         *     insn executes.
         *  - MVE instructions subject to beat-wise execution
         *     Here the ECI bits indicate which beats have already been
         *     executed, and we must honour this. Each insn of this
         *     type will handle it correctly. We will update PSR.ECI
         *     in the helper function for the insn (some ECI values
         *     mean that the following insn also has been partially
         *     executed).
         *  - Special cases which don't advance ECI
         *     The insns LE, LETP and BKPT leave the ECI/ICI state
         *     bits untouched.
         *  - all other insns (the common case)
         *     Non-zero ECI/ICI means an INVSTATE UsageFault.
         *     We place a rewind-marker here. Insns in the previous
         *     three categories will set a flag in the DisasContext.
         *     If the flag isn't set after we call disas_thumb_insn()
         *     or disas_thumb2_insn() then we know we have a "some other
         *     insn" case. We will rewind to the marker (ie throwing away
         *     all the generated code) and instead emit "take exception".
         */
        insn_eci_rewind = tcg_last_op();
        insn_eci_pc_save = dc->pc_save;
    }

    if (dc->condexec_mask && !thumb_insn_is_unconditional(dc, insn)) {
        uint32_t cond = dc->condexec_cond;

        /*
         * Conditionally skip the insn. Note that both 0xe and 0xf mean
         * "always"; 0xf is not "never".
         */
        if (cond < 0x0e) {
            arm_skip_unless(dc, cond);
        }
    }

    if (is_16bit) {
        disas_thumb_insn(dc, insn);
    } else {
        disas_thumb2_insn(dc, insn);
    }

    /* Advance the Thumb condexec condition.  */
    if (dc->condexec_mask) {
        dc->condexec_cond = ((dc->condexec_cond & 0xe) |
                             ((dc->condexec_mask >> 4) & 1));
        dc->condexec_mask = (dc->condexec_mask << 1) & 0x1f;
        if (dc->condexec_mask == 0) {
            dc->condexec_cond = 0;
        }
    }

    if (dc->eci && !dc->eci_handled) {
        /*
         * Insn wasn't valid for ECI/ICI at all: undo what we
         * just generated and instead emit an exception
         */
        tcg_remove_ops_after(insn_eci_rewind);
        dc->pc_save = insn_eci_pc_save;
        dc->condjmp = 0;
        gen_exception_insn(dc, 0, EXCP_INVSTATE, syn_uncategorized());
    }

    arm_post_translate_insn(dc);

    /* Thumb is a variable-length ISA.  Stop translation when the next insn
     * will touch a new page.  This ensures that prefetch aborts occur at
     * the right place.
     *
     * We want to stop the TB if the next insn starts in a new page,
     * or if it spans between this page and the next. This means that
     * if we're looking at the last halfword in the page we need to
     * see if it's a 16-bit Thumb insn (which will fit in this TB)
     * or a 32-bit Thumb insn (which won't).
     * This is to avoid generating a silly TB with a single 16-bit insn
     * in it at the end of this page (which would execute correctly
     * but isn't very efficient).
     */
    if (dc->base.is_jmp == DISAS_NEXT
        && (dc->base.pc_next - dc->page_start >= TARGET_PAGE_SIZE
            || (dc->base.pc_next - dc->page_start >= TARGET_PAGE_SIZE - 3
                && insn_crosses_page(env, dc)))) {
        dc->base.is_jmp = DISAS_TOO_MANY;
    }
}

static void arm_tr_tb_stop(DisasContextBase *dcbase, CPUState *cpu)
{
    DisasContext *dc = container_of(dcbase, DisasContext, base);

    /* At this stage dc->condjmp will only be set when the skipped
       instruction was a conditional branch or trap, and the PC has
       already been written.  */
    gen_set_condexec(dc);
    if (dc->base.is_jmp == DISAS_BX_EXCRET) {
        /* Exception return branches need some special case code at the
         * end of the TB, which is complex enough that it has to
         * handle the single-step vs not and the condition-failed
         * insn codepath itself.
         */
        gen_bx_excret_final_code(dc);
    } else if (unlikely(dc->ss_active)) {
        /* Unconditional and "condition passed" instruction codepath. */
        switch (dc->base.is_jmp) {
        case DISAS_SWI:
            gen_ss_advance(dc);
            gen_exception(EXCP_SWI, syn_aa32_svc(dc->svc_imm, dc->thumb));
            break;
        case DISAS_HVC:
            gen_ss_advance(dc);
            gen_exception_el(EXCP_HVC, syn_aa32_hvc(dc->svc_imm), 2);
            break;
        case DISAS_SMC:
            gen_ss_advance(dc);
            gen_exception_el(EXCP_SMC, syn_aa32_smc(), 3);
            break;
        case DISAS_NEXT:
        case DISAS_TOO_MANY:
        case DISAS_UPDATE_EXIT:
        case DISAS_UPDATE_NOCHAIN:
            gen_update_pc(dc, curr_insn_len(dc));
            /* fall through */
        default:
            /* FIXME: Single stepping a WFI insn will not halt the CPU. */
            gen_singlestep_exception(dc);
            break;
        case DISAS_NORETURN:
            break;
        }
    } else {
        /* While branches must always occur at the end of an IT block,
           there are a few other things that can cause us to terminate
           the TB in the middle of an IT block:
            - Exception generating instructions (bkpt, swi, undefined).
            - Page boundaries.
            - Hardware watchpoints.
           Hardware breakpoints have already been handled and skip this code.
         */
        switch (dc->base.is_jmp) {
        case DISAS_NEXT:
        case DISAS_TOO_MANY:
            gen_goto_tb(dc, 1, curr_insn_len(dc));
            break;
        case DISAS_UPDATE_NOCHAIN:
            gen_update_pc(dc, curr_insn_len(dc));
            /* fall through */
        case DISAS_JUMP:
            gen_goto_ptr();
            break;
        case DISAS_UPDATE_EXIT:
            gen_update_pc(dc, curr_insn_len(dc));
            /* fall through */
        default:
            /* indicate that the hash table must be used to find the next TB */
            tcg_gen_exit_tb(NULL, 0);
            break;
        case DISAS_NORETURN:
            /* nothing more to generate */
            break;
        case DISAS_WFI:
            gen_helper_wfi(tcg_env, tcg_constant_i32(curr_insn_len(dc)));
            /*
             * The helper doesn't necessarily throw an exception, but we
             * must go back to the main loop to check for interrupts anyway.
             */
            tcg_gen_exit_tb(NULL, 0);
            break;
        case DISAS_WFE:
            gen_helper_wfe(tcg_env);
            break;
        case DISAS_YIELD:
            gen_helper_yield(tcg_env);
            break;
        case DISAS_SWI:
            gen_exception(EXCP_SWI, syn_aa32_svc(dc->svc_imm, dc->thumb));
            break;
        case DISAS_HVC:
            gen_exception_el(EXCP_HVC, syn_aa32_hvc(dc->svc_imm), 2);
            break;
        case DISAS_SMC:
            gen_exception_el(EXCP_SMC, syn_aa32_smc(), 3);
            break;
        }
    }

    if (dc->condjmp) {
        /* "Condition failed" instruction codepath for the branch/trap insn */
        set_disas_label(dc, dc->condlabel);
        gen_set_condexec(dc);
        if (unlikely(dc->ss_active)) {
            gen_update_pc(dc, curr_insn_len(dc));
            gen_singlestep_exception(dc);
        } else {
            gen_goto_tb(dc, 1, curr_insn_len(dc));
        }
    }
}

static const TranslatorOps arm_translator_ops = {
    .init_disas_context = arm_tr_init_disas_context,
    .tb_start           = arm_tr_tb_start,
    .insn_start         = arm_tr_insn_start,
    .translate_insn     = arm_tr_translate_insn,
    .tb_stop            = arm_tr_tb_stop,
};

static const TranslatorOps thumb_translator_ops = {
    .init_disas_context = arm_tr_init_disas_context,
    .tb_start           = arm_tr_tb_start,
    .insn_start         = arm_tr_insn_start,
    .translate_insn     = thumb_tr_translate_insn,
    .tb_stop            = arm_tr_tb_stop,
};

/* generate intermediate code for basic block 'tb'.  */
void gen_intermediate_code(CPUState *cpu, TranslationBlock *tb, int *max_insns,
                           vaddr pc, void *host_pc)
{
    DisasContext dc = { };
    const TranslatorOps *ops = &arm_translator_ops;
    CPUARMTBFlags tb_flags = arm_tbflags_from_tb(tb);

    if (EX_TBFLAG_AM32(tb_flags, THUMB)) {
        ops = &thumb_translator_ops;
    }
#ifdef TARGET_AARCH64
    if (EX_TBFLAG_ANY(tb_flags, AARCH64_STATE)) {
        ops = &aarch64_translator_ops;
    }
#endif

    translator_loop(cpu, tb, max_insns, pc, host_pc, ops, &dc.base);
}

4.110 https://github.com/bbaranoff/qosmo/blob/main/tools/qosmo/qosmo.c

42605 octets, 949 lignes → 948 lignes (1 groupes compactés)

/*
 * qosmo.c - C launcher for qemu-system-arm on the Calypso machine.
 *
 * QOSMO_DSP is fixed at build time from the l1_dsp configure option, so the
 * launcher always knows which layer 1 the qemu it starts embeds.
 *
 * What the launcher does, and nothing more:
 *   1. resolve the firmware (-k ELF | directory | .bin) and its siblings (.bin
 *      for osmocon, .map); read the `l1s` and `last_rach` symbols from the ARM
 *      ELF32 and export them as run_modules/16-fwsyms.sh does with nm
 *      (CALYPSO_L1S_FN_ADDR / CALYPSO_LAST_RACH_FN_ADDR);
 *   2. (DSP build) resolve the 7 C54x ROMs from a directory or a prefix (-dsp)
 *      and pass them as machine properties dsp-prom0=...;
 *   3. expose the sockets and the network with the defaults that already work
 *      (those of run.sh / 40-qemu.sh): unix monitor, ARM gdbstub tcp::1234,
 *      L1CTL /tmp/osmocom_l2, TRXDv0 udp 0.0.0.0:6702, I/Q tee 127.0.0.1:6703;
 *      each one selectable by an option (--monitor, --gdb, --l1ctl, --bind,
 *      --trx-port, --iq-tee);
 *   4. start QEMU with `-serial pty -serial pty`, relay its stderr verbatim so
 *      the "redirected" greps keep working, and publish STABLE links to both
 *      ptys: <rundir>/modem.pty and <rundir>/irda.pty, which is what osmocon
 *      is pointed at;
 *   5. on request (-o) start osmocon itself (-m romload) as soon as the modem
 *      pty exists; forward SIGINT/SIGTERM to QEMU; exit with its status.
 *
 * Any unknown argument starting with `-` (and its value, if that does not
 * start with `-`) is forwarded to QEMU. After `--`, everything goes to QEMU.
 */
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <arpa/inet.h>
#include <ctype.h>
#include <dirent.h>
#include <elf.h>
#include <errno.h>
#include <fcntl.h>
#include <ifaddrs.h>
#include <libgen.h>
#include <limits.h>
#include <net/if.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#ifndef QOSMO_ALIAS
#define QOSMO_ALIAS "qosmo-dsp"
#endif
#ifndef QOSMO_TREE
#define QOSMO_TREE "/opt/GSM/qosmo-dsp"
#endif
#ifndef QOSMO_DSP
#define QOSMO_DSP 1
#endif
#ifndef QOSMO_GSM_ROOT
#define QOSMO_GSM_ROOT "/opt/GSM"
#endif
#ifndef QOSMO_VERSION
#define QOSMO_VERSION "1.0"
#endif

/* ---- defaults = what already works (run.sh / paths.env / 40-qemu.sh) ---- */
#define DEF_QEMU        QOSMO_TREE "/build/qemu-system-arm"
#define DEF_FIRMWARE    QOSMO_GSM_ROOT "/firmware/board/compal_e88/layer1.highram.elf"
#define DEF_DSP_DIR     QOSMO_GSM_ROOT
#define DEF_OSMOCON     QOSMO_GSM_ROOT "/osmocom-bb/src/host/osmocon/osmocon"
#define DEF_CPU         "arm946"
#define DEF_GDB         "1234"
/* The calypso machine has no screen, so default to --display none.
 * Without -display, QEMU picks its graphical default (SDL is built into this
 * tree) and wires its remaining default devices onto virtual consoles. Both
 * serial ports are already taken (-serial pty twice) and so is the monitor,
 * which leaves the parallel port: the result is an empty SDL window titled
 * "parallel0" that grabs the keyboard while the boot happens on the ptys.
 * With no DISPLAY at all, as under osmo-banc.service at startup, it is worse:
 * QEMU cannot open SDL and EXITS. `--display sdl`, or -display/-nographic
 * after --, restores the old behaviour. */
#define DEF_DISPLAY     "none"
#define DEF_L1CTL       "/tmp/osmocom_l2"
#define DEF_BIND        "0.0.0.0"
#define DEF_TRX_PORT    "6702"
#define DEF_IQ_TEE_HOST "127.0.0.1"
#define DEF_IQ_TEE_PORT "6703"
#define DEF_OSMOCON_MODEL "romload"
#define DEF_OSMOCON_DELAY "100"
#define DEF_OSMOCON_DEBUG "tr"
#define GRGSM_GSMTAP_PORT "4730"   /* hard-coded in calypso_l1_grgsm.c */

static const char *g_alias = QOSMO_ALIAS;

/* ---------------------------------------------------------------- utils */
static void say(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
static void say(const char *fmt, ...)
{
    va_list ap;
    fprintf(stderr, "[%s] ", g_alias);
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
    fputc('\n', stderr);
}

static void die(const char *fmt, ...) __attribute__((format(printf, 1, 2), noreturn));
static void die(const char *fmt, ...)
{
    va_list ap;
    fprintf(stderr, "[%s] ERREUR : ", g_alias);
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
    fputc('\n', stderr);
    exit(2);
}

static char *xstrdup(const char *s)
{
    char *d = strdup(s ? s : "");
    if (!d) die("mémoire épuisée");
    return d;
}

static char *xasprintf(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
static char *xasprintf(const char *fmt, ...)
{
    char *p = NULL;
    va_list ap;
    va_start(ap, fmt);
    if (vasprintf(&p, fmt, ap) < 0) die("mémoire épuisée");
    va_end(ap);
    return p;
}

static bool is_dir(const char *p)
{
    struct stat st;
    return p && stat(p, &st) == 0 && S_ISDIR(st.st_mode);
}

static bool is_readable_file(const char *p)
{
    struct stat st;
    return p && stat(p, &st) == 0 && S_ISREG(st.st_mode) && access(p, R_OK) == 0;
}

static bool is_exec(const char *p)
{
    struct stat st;
    return p && stat(p, &st) == 0 && S_ISREG(st.st_mode) && access(p, X_OK) == 0;
}

static bool ends_with(const char *s, const char *suf)
{
    size_t ls = strlen(s), lf = strlen(suf);
    return ls >= lf && strcmp(s + ls - lf, suf) == 0;
}

static const char *env_nonempty(const char *n)
{
    const char *v = getenv(n);
    return (v && *v) ? v : NULL;
}

/* Set a variable ONLY if it is absent or empty, so the inherited environment
 * (run.sh, operator) keeps precedence unless an explicit option overrides. */
static void env_default(const char *n, const char *v)
{
    if (!env_nonempty(n) && v && *v) setenv(n, v, 1);
}

static void env_force(const char *n, const char *v)
{
    if (v && *v) setenv(n, v, 1);
}

static int mkdir_p(const char *path)
{
    char tmp[PATH_MAX];
    snprintf(tmp, sizeof tmp, "%s", path);
    for (char *p = tmp + 1; *p; p++) {
        if (*p == '/') {
            *p = 0;
            if (mkdir(tmp, 0755) < 0 && errno != EEXIST) return -1;
            *p = '/';
        }
    }
    if (mkdir(tmp, 0755) < 0 && errno != EEXIST) return -1;
    return 0;
}

static char *abspath(const char *p)
{
    char buf[PATH_MAX];
    if (realpath(p, buf)) return xstrdup(buf);
    return xstrdup(p);
}

/* --------------------------------------------------------- dynamic argv */
typedef struct { char **v; int n, cap; } argv_t;

static void argv_push(argv_t *a, const char *s)
{
    if (a->n + 2 > a->cap) {
        a->cap = a->cap ? a->cap * 2 : 32;
        a->v = realloc(a->v, sizeof(char *) * a->cap);
        if (!a->v) die("mémoire épuisée");
    }
    a->v[a->n++] = xstrdup(s);
    a->v[a->n] = NULL;
}

/* --------------------------------------------------------- ELF32 symbols */
/* Look up names[i] in the symbol table of a little-endian ELF32 and return its
 * value in addrs[i] (found[i]=true). Equivalent to the
 * `nm ELF | awk '$3==name'` of 16-fwsyms.sh, without depending on binutils. */
static int elf32_lookup(const char *path, const char **names, int n,
                        uint32_t *addrs, bool *found)
{
    int fd = open(path, O_RDONLY);
    if (fd < 0) return -1;
    struct stat st;
    if (fstat(fd, &st) < 0 || st.st_size < (off_t)sizeof(Elf32_Ehdr)) { close(fd); return -1; }
    uint8_t *m = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    close(fd);
    if (m == MAP_FAILED) return -1;
    int rc = -1;
    Elf32_Ehdr *eh = (Elf32_Ehdr *)m;
    if (memcmp(eh->e_ident, ELFMAG, SELFMAG) || eh->e_ident[EI_CLASS] != ELFCLASS32 ||
        eh->e_ident[EI_DATA] != ELFDATA2LSB)
        goto out;
    if (eh->e_shoff == 0 || eh->e_shentsize != sizeof(Elf32_Shdr) ||
        (off_t)eh->e_shoff + (off_t)((size_t)eh->e_shnum * sizeof(Elf32_Shdr)) > st.st_size)
        goto out;
    Elf32_Shdr *sh = (Elf32_Shdr *)(m + eh->e_shoff);
    for (int i = 0; i < n; i++) found[i] = false;
    rc = 0;
    for (int s = 0; s < eh->e_shnum; s++) {
        if (sh[s].sh_type != SHT_SYMTAB) continue;
        if (sh[s].sh_link >= eh->e_shnum) continue;
        Elf32_Shdr *strs = &sh[sh[s].sh_link];
        if ((off_t)sh[s].sh_offset + sh[s].sh_size > st.st_size ||
            (off_t)strs->sh_offset + strs->sh_size > st.st_size)
            continue;
        Elf32_Sym *sym = (Elf32_Sym *)(m + sh[s].sh_offset);
        size_t nsym = sh[s].sh_size / sizeof(Elf32_Sym);
        const char *strtab = (const char *)(m + strs->sh_offset);
        for (size_t k = 0; k < nsym; k++) {
            if (sym[k].st_name >= strs->sh_size) continue;
            const char *nm = strtab + sym[k].st_name;
            for (int i = 0; i < n; i++) {
                if (!found[i] && strcmp(nm, names[i]) == 0) {
                    addrs[i] = sym[k].st_value;
                    found[i] = true;
                }
            }
        }
    }
out:
    munmap(m, st.st_size);
    return rc;
}

/* ----------------------------------------------------------- DSP ROMs */
static const char *rom_tokens[] = { "PROM0", "PROM1", "PROM2", "PROM3", "DROM", "PDROM", "Registers" };
static const char *rom_props[]  = { "dsp-prom0", "dsp-prom1", "dsp-prom2", "dsp-prom3", "dsp-drom", "dsp-pdrom", "dsp-registers" };
static const char *rom_envs[]   = { "DSP_PROM0", "DSP_PROM1", "DSP_PROM2", "DSP_PROM3", "DSP_DROM", "DSP_PDROM", "DSP_REGISTERS" };
#define N_ROMS 7

static int strcasestr_has(const char *hay, const char *needle)
{
    return strcasestr(hay, needle) != NULL;
}

/* Find ROM `tok` in `dir`: known names first, then a scan for any file whose
 * name contains "<tok>.bin", case-insensitively. */
static char *find_rom_in_dir(const char *dir, const char *tok)
{
    const char *pat[] = { "%s/calypso_dsp.%s.bin", "%s/%s.bin", "%s/.%s.bin",
                          "%s/calypso_dsp_%s.bin", "%s/dsp.%s.bin", "%s/dsp_%s.bin" };
    for (size_t i = 0; i < sizeof pat / sizeof *pat; i++) {
        char *p = xasprintf(pat[i], dir, tok);
        if (is_readable_file(p)) return p;
        free(p);
    }
    DIR *d = opendir(dir);
    if (!d) return NULL;
    struct dirent *e;
    char *best = NULL;
    char *needle = xasprintf("%s.bin", tok);
    while ((e = readdir(d))) {
        if (!strcasestr_has(e->d_name, needle)) continue;
        /* do not match PDROM when looking for DROM */
        if (strcmp(tok, "DROM") == 0 && strcasestr_has(e->d_name, "PDROM")) continue;
        char *p = xasprintf("%s/%s", dir, e->d_name);
        if (is_readable_file(p)) { best = p; break; }
        free(p);
    }
    free(needle);
    closedir(d);
    return best;
}

/* `spec` = a directory, or a ROM prefix/file (e.g. /x/calypso_dsp.PROM0.bin or
 * /x/calypso_dsp.). Fills roms[]. Returns how many were found. */
static int resolve_roms(const char *spec, char **roms)
{
    int n = 0;
    if (is_dir(spec)) {
        for (int i = 0; i < N_ROMS; i++) {
            roms[i] = find_rom_in_dir(spec, rom_tokens[i]);
            if (roms[i]) n++;
        }
        return n;
    }
    /* prefix: truncate at the first known token if one is present */
    char *prefix = xstrdup(spec);
    for (int i = 0; i < N_ROMS; i++) {
        char *hit = strcasestr(prefix, rom_tokens[i]);
        if (hit) { *hit = 0; break; }
    }
    for (int i = 0; i < N_ROMS; i++) {
        char *p = xasprintf("%s%s.bin", prefix, rom_tokens[i]);
        if (is_readable_file(p)) { roms[i] = p; n++; }
        else { free(p); roms[i] = NULL; }
    }
    if (n == 0) {
        /* possibly a file in a directory with unusual names: fall back to
         * scanning the directory */
        char *dup = xstrdup(spec);
        char *dir = dirname(dup);
        for (int i = 0; i < N_ROMS; i++) {
            roms[i] = find_rom_in_dir(dir, rom_tokens[i]);
            if (roms[i]) n++;
        }
        free(dup);
    }
    free(prefix);
    return n;
}

/* -------------------------------------------------------------- network */
static bool is_ipv4_literal(const char *s)
{
    struct in_addr a;
    return inet_aton(s, &a) != 0;
}

/* `spec` = an IPv4 literal, or an interface name (eth0, lo, ...) -> IPv4 */
static char *resolve_bind(const char *spec)
{
    if (is_ipv4_literal(spec)) return xstrdup(spec);
    struct ifaddrs *ifa = NULL, *p;
    if (getifaddrs(&ifa) < 0) die("--bind %s : getifaddrs : %s", spec, strerror(errno));
    char *res = NULL;
    for (p = ifa; p; p = p->ifa_next) {
        if (!p->ifa_addr || p->ifa_addr->sa_family != AF_INET) continue;
        if (strcmp(p->ifa_name, spec) != 0) continue;
        char buf[INET_ADDRSTRLEN];
        struct sockaddr_in *sin = (struct sockaddr_in *)p->ifa_addr;
        if (inet_ntop(AF_INET, &sin->sin_addr, buf, sizeof buf)) { res = xstrdup(buf); break; }
    }
    freeifaddrs(ifa);
    if (!res) die("--bind %s : ni adresse IPv4 ni interface avec une IPv4", spec);
    return res;
}

/* `spec` -> argument for `-gdb`: off | PORT | ADDR:PORT | IFACE:PORT | raw
 * (already contains "tcp:" or "unix:"). Returns NULL for off. */
static char *gdb_spec(const char *spec)
{
    if (!spec || !*spec || !strcmp(spec, "off") || !strcmp(spec, "none") || !strcmp(spec, "0"))
        return NULL;
    if (strstr(spec, "tcp:") || strstr(spec, "unix:")) return xstrdup(spec);
    const char *colon = strrchr(spec, ':');
    if (!colon) {
        char *e; long p = strtol(spec, &e, 10);
        if (*e || p <= 0 || p > 65535) die("--gdb %s : port invalide", spec);
        return xasprintf("tcp::%ld", p);
    }
    char *host = strndup(spec, colon - spec);
    char *hres = resolve_bind(host);
    free(host);
    char *r = xasprintf("tcp:%s:%s", hres, colon + 1);
    free(hres);
    return r;
}

/* -------------------------------------------------------------- options */
typedef struct {
    const char *qemu, *firmware, *fw_bin, *cpu, *rundir, *monitor, *gdb, *display;
    const char *l1ctl, *bind, *trx_port, *iq_tee;
    const char *pty_link, *irda_link;
    const char *dsp_spec;
    const char *osmocon_bin, *osmocon_model, *osmocon_delay, *osmocon_debug, *osmocon_log;
    bool osmocon, dry_run, quiet, no_pty_link, no_monitor;
    argv_t extra;   /* extra QEMU arguments */
    argv_t envs;    /* -e VAR=VAL */
} opts_t;

static void usage(FILE *f)
{
    fprintf(f,
"Usage : %s [-k FIRMWARE.elf]%s [options] [-- args QEMU]\n"
"\n"
"Lance qemu-system-arm (machine calypso) du fork %s avec les défauts qui\n"
"marchent déjà, publie les sockets/pty, et permet osmocon directement dessus.\n"
"\n"
"Firmware :\n"
"  -k, -kernel FICHIER     ELF layer1 (défaut : $FIRMWARE_ELF ou\n"
"                          %s)\n"
"                          Un DOSSIER => layer1.highram.elf dedans ; un .bin => l'.elf voisin.\n"
"                          Les symboles l1s/last_rach sont lus dans l'ELF, le .bin voisin\n"
"                          sert à osmocon.\n"
"      --bin FICHIER       image .bin pour osmocon (défaut : <elf sans .elf>.bin)\n"
"%s"
"\n"
"Sockets :\n"
"  -r, --rundir DIR        dossier des sockets/liens/pid (défaut : $RUN_DIR ou /tmp/%s)\n"
"  -M, --monitor CHEMIN    socket moniteur QEMU (défaut : <rundir>/qemu-monitor.sock ; none = aucun)\n"
"  -s, --l1ctl CHEMIN      socket L1CTL (défaut : $L1CTL_SOCK ou %s) — exportée L1CTL_SOCK,\n"
"                          passée à osmocon -s\n"
"  -p, --pty CHEMIN        lien stable vers le pty modem/serial0 (défaut : <rundir>/modem.pty)\n"
"      --irda CHEMIN       lien stable vers le pty IrDA/serial1 (défaut : <rundir>/irda.pty)\n"
"      --no-pty-link       ne crée aucun lien\n"
"\n"
"Réseau :\n"
"  -g, --gdb SPEC          gdbstub ARM : PORT | ADDR:PORT | IFACE:PORT | off (défaut : %s)\n"
"      --bind ADDR|IFACE   adresse/interface d'écoute du modèle (TRXDv0) (défaut : %s)%s\n"
"      --trx-port N        port UDP TRXDv0 d'entrée des bursts (défaut : %s)%s\n"
"      --iq-tee HOST:PORT  destination du tee I/Q (défaut : %s:%s)%s\n"
"\n"
"osmocon :\n"
"  -o, --osmocon           lance osmocon (-m %s -i %s) dès que le pty modem existe\n"
"      --osmocon-bin P     binaire (défaut : $OSMOCON ou %s)\n"
"      --osmocon-model M   (défaut %s)   --osmocon-delay N (défaut %s)   --osmocon-debug F (défaut %s)\n"
"      --osmocon-log F     journal d'osmocon (défaut : <rundir>/osmocon.log ; - = hérite du terminal)\n"
"\n"
"Divers :\n"
"      --qemu CHEMIN       binaire qemu-system-arm (défaut : $QEMU_BIN ou %s)\n"
"      --cpu MODÈLE        (défaut %s)\n"
"      --display BACKEND   affichage QEMU : none | sdl | … ($QOSMO_DISPLAY, défaut %s).\n"
"                          La machine calypso n'a pas d'écran : `none` évite la fenêtre\n"
"                          vide « parallel0 » et permet de tourner sans DISPLAY.\n"
"      --nographic         équivalent de --display none\n"
"  -e, --env VAR=VAL       variable d'environnement pour le modèle (CALYPSO_*, …)\n"
"  -n, --dry-run           affiche l'environnement et la commande, ne lance rien\n"
"  -q, --quiet             pas de bannière\n"
"  -h, --help  -V, --version\n"
"\n"
"Tout argument inconnu commençant par `-` est transmis à QEMU (avec sa valeur si\n"
"elle ne commence pas par `-`). Après `--`, tout va à QEMU.\n"
"\n"
"Exemples :\n"
"  %s -k /opt/GSM/firmware/board/compal_e88/layer1.highram.elf%s -o\n"
"  osmocon -m romload -i 100 -p /tmp/%s/modem.pty -s %s layer1.highram.bin\n",
        g_alias, QOSMO_DSP ? " [-dsp DOSSIER|PRÉFIXE]" : "", QOSMO_TREE, DEF_FIRMWARE,
        QOSMO_DSP ?
"\n"
"DSP (TMS320C54x) :\n"
"  -dsp, --dsp DOSSIER|PRÉFIXE\n"
"                          ROMs PROM0..3, DROM, PDROM, Registers. Dossier : calypso_dsp.X.bin,\n"
"                          X.bin, .X.bin ou tout *X.bin ; préfixe : /chemin/calypso_dsp.\n"
"                          Défaut : $DSP_PROM0..$DSP_REGISTERS si posés, sinon $DSP_ROM_DIR\n"
"                          ou " DEF_DSP_DIR ".\n" : "",
        g_alias, DEF_L1CTL, DEF_GDB,
        DEF_BIND, QOSMO_DSP ? "" : "  [sans effet sur ce fork]",
        DEF_TRX_PORT, QOSMO_DSP ? "" : "  [sans effet sur ce fork]",
        DEF_IQ_TEE_HOST, DEF_IQ_TEE_PORT, QOSMO_DSP ? "" : "  [sans effet sur ce fork]",
        DEF_OSMOCON_MODEL, DEF_OSMOCON_DELAY, DEF_OSMOCON, DEF_OSMOCON_MODEL, DEF_OSMOCON_DELAY, DEF_OSMOCON_DEBUG,
        DEF_QEMU, DEF_CPU, DEF_DISPLAY,
        g_alias, QOSMO_DSP ? " -dsp /opt/GSM" : "", g_alias, DEF_L1CTL);
}

/* --opt VAL | --opt=VAL (but not -oVAL); returns the value, advances *i */
static const char *optval(int argc, char **argv, int *i, const char *eq)
{
    if (eq) return eq + 1;
    if (*i + 1 >= argc) die("option %s : valeur manquante", argv[*i]);
    return argv[++(*i)];
}

static bool optis(const char *a, const char *eq, const char *s1, const char *s2, const char *s3)
{
    size_t l = eq ? (size_t)(eq - a) : strlen(a);
    if (s⟨1⟩ && strlen(s⟨2⟩) == l && !strncmp(a, s⟨3⟩, l)) return true;  ×3
    ⟨⟩ = (1,1,1) (2,2,2) (3,3,3)
    return false;
}

static void parse_args(int argc, char **argv, opts_t *o)
{
    for (int i = 1; i < argc; i++) {
        const char *a = argv[i];
        if (!strcmp(a, "--")) { for (i++; i < argc; i++) argv_push(&o->extra, argv[i]); break; }
        const char *eq = (a[0] == '-' && a[1] == '-') ? strchr(a, '=') : NULL;
#define IS(...) optis(a, eq, __VA_ARGS__)
#define VAL()   optval(argc, argv, &i, eq)
        if (IS("-h", "--help", NULL))        { usage(stdout); exit(0); }
        else if (IS("-V", "--version", NULL)) { printf("%s %s (%s)\n", g_alias, QOSMO_VERSION, QOSMO_TREE); exit(0); }
        else if (IS("-k", "-kernel", "--kernel")) o->firmware = VAL();
        else if (IS("--bin", NULL, NULL))     o->fw_bin = VAL();
        else if (IS("-dsp", "--dsp", "--dsp-roms")) {
            if (!QOSMO_DSP) die("%s : pas de DSP émulé sur ce fork (utilisez qosmo-dsp)", a);
            o->dsp_spec = VAL();
        }
        else if (IS("-r", "--rundir", NULL))  o->rundir = VAL();
        else if (IS("-M", "--monitor", NULL)) o->monitor = VAL();
        else if (IS("--no-monitor", NULL, NULL)) o->no_monitor = true;
        else if (IS("-s", "--l1ctl", NULL))   o->l1ctl = VAL();
        else if (IS("-p", "--pty", "--pty-link")) o->pty_link = VAL();
        else if (IS("--irda", "--irda-link", NULL)) o->irda_link = VAL();
        else if (IS("--no-pty-link", NULL, NULL)) o->no_pty_link = true;
        else if (IS("-g", "--gdb", NULL))     o->gdb = VAL();
        else if (IS("--no-gdb", NULL, NULL))  o->gdb = "off";
        else if (IS("--bind", "--iface", "--interface")) o->bind = VAL();
        else if (IS("--trx-port", "--bsp-port", NULL)) o->trx_port = VAL();
        else if (IS("--iq-tee", NULL, NULL))  o->iq_tee = VAL();
        else if (IS("-o", "--osmocon", NULL)) o->osmocon = true;
        else if (IS("--no-osmocon", NULL, NULL)) o->osmocon = false;
        else if (IS("--osmocon-bin", NULL, NULL))   o->osmocon_bin = VAL();
        else if (IS("--osmocon-model", NULL, NULL)) o->osmocon_model = VAL();
        else if (IS("--osmocon-delay", NULL, NULL)) o->osmocon_delay = VAL();
        else if (IS("--osmocon-debug", NULL, NULL)) o->osmocon_debug = VAL();
        else if (IS("--osmocon-log", NULL, NULL))   o->osmocon_log = VAL();
        else if (IS("--qemu", "--qemu-bin", NULL)) o->qemu = VAL();
        else if (IS("--cpu", "-cpu", NULL))   o->cpu = VAL();
        else if (IS("--display", "-display", NULL)) o->display = VAL();
        else if (IS("--nographic", "-nographic", NULL)) o->display = "none";
        else if (IS("-e", "--env", NULL))     argv_push(&o->envs, VAL());
        else if (IS("-n", "--dry-run", NULL)) o->dry_run = true;
        else if (IS("-q", "--quiet", NULL))   o->quiet = true;
        else if (a[0] == '-' && a[1]) {
            /* unknown -> forward to QEMU, with its value if it has one */
            argv_push(&o->extra, a);
            if (i + 1 < argc && argv[i + 1][0] != '-') argv_push(&o->extra, argv[++i]);
        }
        else if (!o->firmware && (ends_with(a, ".elf") || ends_with(a, ".bin") || is_dir(a)))
            o->firmware = a;
        else die("argument inattendu : %s (voir --help)", a);
#undef IS
#undef VAL
    }
}

/* ------------------------------------------------------- pty / osmocon */
static volatile sig_atomic_t g_sig = 0;
static void on_sig(int s) { g_sig = s; }

static pid_t g_qemu = 0, g_osmocon = 0;

static void make_link(const char *target, const char *link)
{
    if (!link || !*link) return;
    unlink(link);
    if (symlink(target, link) < 0)
        say("lien %s -> %s impossible : %s", link, target, strerror(errno));
}

static void write_pid(const char *path, pid_t pid)
{
    FILE *f = fopen(path, "w");
    if (!f) return;
    fprintf(f, "%d\n", (int)pid);
    fclose(f);
}

static pid_t spawn_osmocon(const opts_t *o, const char *pty)
{
    if (!is_exec(o->osmocon_bin)) {
        say("osmocon introuvable ou non exécutable : %s (--osmocon-bin)", o->osmocon_bin);
        return 0;
    }
    if (!is_readable_file(o->fw_bin)) {
        say("image .bin pour osmocon illisible : %s (--bin)", o->fw_bin ? o->fw_bin : "(aucune)");
        return 0;
    }
    /* as in 50-osmocon.sh: a socket left by a previous run would refuse bind */
    unlink(o->l1ctl);
    pid_t p = fork();
    if (p < 0) { say("fork osmocon : %s", strerror(errno)); return 0; }
    if (p == 0) {
        if (o->osmocon_log && strcmp(o->osmocon_log, "-") != 0) {
            int fd = open(o->osmocon_log, O_WRONLY | O_CREAT | O_APPEND, 0644);
            if (fd >= 0) { dup2(fd, 1); dup2(fd, 2); close(fd); }
        }
        execl(o->osmocon_bin, o->osmocon_bin,
              "-m", o->osmocon_model, "-i", o->osmocon_delay,
              "-p", pty, "-s", o->l1ctl, o->fw_bin,
              "-d", o->osmocon_debug, (char *)NULL);
        _exit(127);
    }
    return p;
}

/* ----------------------------------------------------------------- main */
int main(int argc, char **argv)
{
    opts_t o = { 0 };
    parse_args(argc, argv, &o);

    /* -- environment set by -e, BEFORE the defaults, so it wins -- */
    for (int i = 0; i < o.envs.n; i++) {
        char *kv = xstrdup(o.envs.v[i]);
        char *e = strchr(kv, '=');
        if (!e) die("-e %s : attendu VAR=VAL", kv);
        *e = 0;
        setenv(kv, e + 1, 1);
        free(kv);
    }

    /* -- QEMU -- */
    if (!o.qemu) o.qemu = env_nonempty("QEMU_BIN") ? env_nonempty("QEMU_BIN") : DEF_QEMU;
    if (!is_exec(o.qemu)) die("qemu-system-arm introuvable : %s (--qemu ; compilez : ninja -C %s/build qemu-system-arm)", o.qemu, QOSMO_TREE);
    if (!o.cpu) o.cpu = DEF_CPU;
    if (!o.display) o.display = env_nonempty("QOSMO_DISPLAY") ? env_nonempty("QOSMO_DISPLAY") : DEF_DISPLAY;

    /* -- firmware -- */
    if (!o.firmware) o.firmware = env_nonempty("FIRMWARE_ELF") ? env_nonempty("FIRMWARE_ELF") : DEF_FIRMWARE;
    char *fw = xstrdup(o.firmware);
    if (is_dir(fw)) { char *t = xasprintf("%s/layer1.highram.elf", fw); free(fw); fw = t; }
    if (ends_with(fw, ".bin")) {
        char *elf = xstrdup(fw); strcpy(elf + strlen(elf) - 4, ".elf");
        if (!is_readable_file(elf)) die("firmware %s : .elf voisin introuvable (%s)", fw, elf);
        if (!o.fw_bin) o.fw_bin = xstrdup(fw);
        free(fw); fw = elf;
    }
    if (!is_readable_file(fw)) die("firmware ELF illisible : %s (-k)", fw);
    fw = abspath(fw);
    char *stem = xstrdup(fw);
    if (ends_with(stem, ".elf")) stem[strlen(stem) - 4] = 0;
    if (!o.fw_bin) {
        char *b = xasprintf("%s.bin", stem);
        if (!is_readable_file(b) && env_nonempty("FIRMWARE_BIN")) { free(b); b = xstrdup(env_nonempty("FIRMWARE_BIN")); }
        o.fw_bin = b;
    }
    /* Guard: under QEMU the romload stub in hw/char/calypso_uart.c IGNORES the
     * .bin payload (the firmware that runs is the -kernel ELF), the .bin only
     * serves the osmocon handshake. But run.sh sets FIRMWARE_BIN independently
     * of FIRMWARE_ELF, so a .bin from another build goes unnoticed here, while
     * on a REAL phone it is the one that would run. */
    bool fw_mismatch = false;
    {
        char *sib = xasprintf("%s.bin", stem);
        char *a = abspath(o.fw_bin), *b = abspath(sib);
        if (is_readable_file(sib) && strcmp(a, b) != 0) fw_mismatch = true;
        free(a); free(b); free(sib);
    }
    char *fw_map = xasprintf("%s.map", stem);
    char *fw_dir = xstrdup(fw); fw_dir = dirname(fw_dir);

    const char *symnames[2] = { "l1s", "last_rach" };
    uint32_t symaddr[2] = { 0, 0 }; bool symok[2] = { false, false };
    if (elf32_lookup(fw, symnames, 2, symaddr, symok) < 0)
        say("avertissement : %s n'est pas un ELF32 lisible, symboles non lus", fw);

    env_default("FIRMWARE_ELF", fw);
    env_default("FIRMWARE_BIN", o.fw_bin);
    env_default("FIRMWARE_DIR", fw_dir);
    env_default("CALYPSO_FIRMWARE_ELF", fw);
    env_default("CALYPSO_GDB_ELF", fw);
    if (symok[0]) { char *v = xasprintf("0x%08x", symaddr[0]); env_default("CALYPSO_L1S_FN_ADDR", v); free(v); }
    if (symok[1]) { char *v = xasprintf("0x%08x", symaddr[1]); env_default("CALYPSO_LAST_RACH_FN_ADDR", v); free(v); }

    /* -- rundir / sockets -- */
    if (!o.rundir) o.rundir = env_nonempty("RUN_DIR") ? env_nonempty("RUN_DIR") : xasprintf("/tmp/%s", g_alias);
    if (!o.dry_run && mkdir_p(o.rundir) < 0) die("rundir %s : %s", o.rundir, strerror(errno));
    if (o.no_monitor || (o.monitor && (!strcmp(o.monitor, "none") || !strcmp(o.monitor, "off")))) o.monitor = NULL, o.no_monitor = true;
    else if (!o.monitor) o.monitor = xasprintf("%s/qemu-monitor.sock", o.rundir);
    if (!o.l1ctl) o.l1ctl = env_nonempty("L1CTL_SOCK") ? env_nonempty("L1CTL_SOCK") : DEF_L1CTL;
    env_force("L1CTL_SOCK", o.l1ctl);           /* read by l1ctl_sock.c */
    env_default("CALYPSO_L1CTL_SOCK", o.l1ctl);
    if (!o.no_pty_link) {
        if (!o.pty_link)  o.pty_link  = xasprintf("%s/modem.pty", o.rundir);
        if (!o.irda_link) o.irda_link = xasprintf("%s/irda.pty", o.rundir);
    }

    /* -- network -- */
    char *gdb = gdb_spec(o.gdb ? o.gdb : (env_nonempty("CALYPSO_GDB_PORT") ? env_nonempty("CALYPSO_GDB_PORT") : DEF_GDB));
    if (gdb) { const char *p = strrchr(gdb, ':'); if (p) env_force("CALYPSO_GDB_PORT", p + 1); }
    char *bind_addr = NULL;
    if (QOSMO_DSP) {
        if (o.bind) { bind_addr = resolve_bind(o.bind); env_force("CALYPSO_BSP_BIND_ADDR", bind_addr); }
        else { env_default("CALYPSO_BSP_BIND_ADDR", DEF_BIND); bind_addr = xstrdup(getenv("CALYPSO_BSP_BIND_ADDR")); }
        if (o.trx_port) env_force("CALYPSO_BSP_PORT", o.trx_port);
        else env_default("CALYPSO_BSP_PORT", DEF_TRX_PORT);
        if (o.iq_tee) {
            char *d = xstrdup(o.iq_tee); char *c = strrchr(d, ':');
            if (!c) die("--iq-tee %s : attendu HOST:PORT", o.iq_tee);
            *c = 0; char *h = resolve_bind(d);
            env_force("CALYPSO_IQ_TEE_HOST", h); env_force("CALYPSO_IQ_TEE_PORT", c + 1);
            free(h); free(d);
        } else { env_default("CALYPSO_IQ_TEE_HOST", DEF_IQ_TEE_HOST); env_default("CALYPSO_IQ_TEE_PORT", DEF_IQ_TEE_PORT); }
    } else if (o.bind || o.trx_port || o.iq_tee) {
        say("avertissement : --bind/--trx-port/--iq-tee sont sans effet sur le fork gr-gsm (aucune écoute configurable dans son modèle)");
    }

    /* -- DSP ROMs -- */
    char *roms[N_ROMS] = { 0 };
    char *mach = xstrdup("calypso");
    if (QOSMO_DSP) {
        int n = 0;
        if (o.dsp_spec) {
            n = resolve_roms(o.dsp_spec, roms);
        } else {
            bool any_env = false;
            for (int i = 0; i < N_ROMS; i++) if (env_nonempty(rom_envs[i])) any_env = true;
            if (any_env) {
                for (int i = 0; i < N_ROMS; i++) {
                    const char *v = env_nonempty(rom_envs[i]);
                    if (v && is_readable_file(v)) { roms[i] = xstrdup(v); n++; }
                }
            }
            if (n == 0) {
                const char *d = env_nonempty("DSP_ROM_DIR") ? env_nonempty("DSP_ROM_DIR") : DEF_DSP_DIR;
                n = resolve_roms(d, roms);
                o.dsp_spec = d;
            } else o.dsp_spec = "$DSP_PROM0..$DSP_REGISTERS";
        }
        for (int i = 0; i < 6; i++)
            if (!roms[i]) die("ROM DSP %s introuvable (recherche : %s). Utilisez -dsp DOSSIER ou -dsp /chemin/calypso_dsp.",
                              rom_tokens[i], o.dsp_spec);
        if (!roms[6]) say("avertissement : Registers.bin absent — le DSP partira sans instantané des MMR");
        for (int i = 0; i < N_ROMS; i++) {
            if (!roms[i]) continue;
            roms[i] = abspath(roms[i]);
            char *t = xasprintf("%s,%s=%s", mach, rom_props[i], roms[i]); free(mach); mach = t;
            env_force(rom_envs[i], roms[i]);
            char *ce = xasprintf("CALYPSO_%s", rom_envs[i]); env_force(ce, roms[i]); free(ce);
        }
        {
            char *dd = xstrdup(roms[0]); env_default("DSP_ROM_DIR", dirname(dd)); free(dd);
        }
    }

    /* -- osmocon -- */
    if (!o.osmocon_bin)   o.osmocon_bin   = env_nonempty("OSMOCON") ? env_nonempty("OSMOCON") : DEF_OSMOCON;
    if (!o.osmocon_model) o.osmocon_model = env_nonempty("OSMOCON_MODEL") ? env_nonempty("OSMOCON_MODEL") : DEF_OSMOCON_MODEL;
    if (!o.osmocon_delay) o.osmocon_delay = env_nonempty("OSMOCON_INTER_BYTE_DELAY") ? env_nonempty("OSMOCON_INTER_BYTE_DELAY") : DEF_OSMOCON_DELAY;
    if (!o.osmocon_debug) o.osmocon_debug = env_nonempty("OSMOCON_DEBUG") ? env_nonempty("OSMOCON_DEBUG") : DEF_OSMOCON_DEBUG;
    if (!o.osmocon_log)   o.osmocon_log   = xasprintf("%s/osmocon.log", o.rundir);

    /* -- QEMU command line -- */
    argv_t qa = { 0 };
    argv_push(&qa, o.qemu);
    bool user_M = false, user_cpu = false, user_serial = false, user_kernel = false;
    bool user_display = false, user_parallel = false;
    for (int i = 0; i < o.extra.n; i++) {
        if (!strcmp(o.extra.v[i], "-M") || !strcmp(o.extra.v[i], "-machine")) user_M = true;
        if (!strcmp(o.extra.v[i], "-cpu")) user_cpu = true;
        if (!strcmp(o.extra.v[i], "-serial")) user_serial = true;
        if (!strcmp(o.extra.v[i], "-kernel")) user_kernel = true;
        if (!strcmp(o.extra.v[i], "-display") || !strcmp(o.extra.v[i], "-nographic")) user_display = true;
        if (!strcmp(o.extra.v[i], "-parallel")) user_parallel = true;
    }
    if (!user_M)   { argv_push(&qa, "-M"); argv_push(&qa, mach); }
    if (!user_cpu) { argv_push(&qa, "-cpu"); argv_push(&qa, o.cpu); }
    /* -display none removes the window; -parallel none is still needed to
     * remove the "parallel0" chardev itself, which `info chardev` still
     * reported as `vc` despite -display none. The calypso machine has no
     * parallel port to model. */
    if (!user_display)  { argv_push(&qa, "-display"); argv_push(&qa, o.display); }
    if (!user_parallel && !strcmp(o.display, "none")) { argv_push(&qa, "-parallel"); argv_push(&qa, "none"); }
    if (gdb)       { argv_push(&qa, "-gdb"); argv_push(&qa, gdb); }
    if (!user_serial) { argv_push(&qa, "-serial"); argv_push(&qa, "pty"); argv_push(&qa, "-serial"); argv_push(&qa, "pty"); }
    if (!o.no_monitor) { argv_push(&qa, "-monitor"); char *m = xasprintf("unix:%s,server,nowait", o.monitor); argv_push(&qa, m); free(m); }
    if (!user_kernel) { argv_push(&qa, "-kernel"); argv_push(&qa, fw); }
    for (int i = 0; i < o.extra.n; i++) argv_push(&qa, o.extra.v[i]);

    /* -- banner -- */
    if (!o.quiet || o.dry_run) {
        say("fork      : %s  (%s)", QOSMO_TREE, QOSMO_DSP ? "DSP C54x émulé sur la mask-ROM TI" : "L1 gr-gsm, DSP non émulé");
        say("qemu      : %s", o.qemu);
        say("firmware  : %s", fw);
        say("  .bin    : %s%s", o.fw_bin, is_readable_file(o.fw_bin) ? "" : "  (ABSENT)");
        if (fw_mismatch)
            say("  ATTENTION: ce .bin n'est pas le voisin de l'ELF (%s.bin existe). Sous QEMU le stub\n"
                "[%s]             romload ignore son contenu (c'est l'ELF qui tourne, symboles corrects), mais\n"
                "[%s]             FIRMWARE_BIN et FIRMWARE_ELF divergent : passez --bin %s.bin", stem, g_alias, g_alias, stem);
        say("  .map    : %s", is_readable_file(fw_map) ? fw_map : "(absent)");
        if (symok[0] || symok[1])
            say("  symboles: l1s=%s%08x last_rach=%s%08x  (CALYPSO_L1S_FN_ADDR / CALYPSO_LAST_RACH_FN_ADDR)",
                symok[0] ? "0x" : "?", symaddr[0], symok[1] ? "0x" : "?", symaddr[1]);
        else say("  symboles: l1s / last_rach ABSENTS de l'ELF (AGCH en péril, cf. 16-fwsyms.sh)");
        if (QOSMO_DSP) {
            say("ROMs DSP  : %s", o.dsp_spec);
            for (int i = 0; i < N_ROMS; i++) if (roms[i]) say("  %-9s: %s", rom_tokens[i], roms[i]);
        }
        say("rundir    : %s", o.rundir);
        say("moniteur  : %s", o.no_monitor ? "(aucun)" : o.monitor);
        say("affichage : %s", user_display ? "(passé à QEMU tel quel)" : o.display);
        say("gdb ARM   : %s", gdb ? gdb : "(off)");
        say("L1CTL     : %s", o.l1ctl);
        if (QOSMO_DSP)
            say("réseau    : TRXDv0 udp %s:%s (entrée bursts)  |  IQ tee -> udp %s:%s",
                getenv("CALYPSO_BSP_BIND_ADDR"), getenv("CALYPSO_BSP_PORT"),
                getenv("CALYPSO_IQ_TEE_HOST"), getenv("CALYPSO_IQ_TEE_PORT"));
        else
            say("réseau    : GSMTAP udp/%s (fixe dans calypso_l1_grgsm.c)", GRGSM_GSMTAP_PORT);
        say("pty liens : modem=%s  irda=%s", o.pty_link ? o.pty_link : "(aucun)", o.irda_link ? o.irda_link : "(aucun)");
        say("osmocon   : %s%s", o.osmocon ? "lancé automatiquement -> " : "à la main -> ",
            o.osmocon ? o.osmocon_log : "");
        say("  %s -m %s -i %s -p %s -s %s %s -d %s", o.osmocon_bin, o.osmocon_model, o.osmocon_delay,
            o.pty_link ? o.pty_link : "<pty serial0>", o.l1ctl, o.fw_bin, o.osmocon_debug);
    }

    if (o.dry_run) {
        const char *shown[] = { "FIRMWARE_ELF", "FIRMWARE_BIN", "CALYPSO_FIRMWARE_ELF", "CALYPSO_GDB_ELF",
            "CALYPSO_L1S_FN_ADDR", "CALYPSO_LAST_RACH_FN_ADDR", "L1CTL_SOCK", "CALYPSO_GDB_PORT",
            "CALYPSO_BSP_BIND_ADDR", "CALYPSO_BSP_PORT", "CALYPSO_IQ_TEE_HOST", "CALYPSO_IQ_TEE_PORT",
            "DSP_ROM_DIR", "DSP_PROM0", "DSP_PROM1", "DSP_PROM2", "DSP_PROM3", "DSP_DROM", "DSP_PDROM", "DSP_REGISTERS" };
        printf("# environnement\n");
        for (size_t i = 0; i < sizeof shown / sizeof *shown; i++)
            if (env_nonempty(shown[i])) printf("export %s='%s'\n", shown[i], getenv(shown[i]));
        printf("# commande\n");
        for (int i = 0; i < qa.n; i++) printf("%s%s", i ? " " : "", qa.v[i]);
        printf("\n");
        return 0;
    }

    /* -- launch: QEMU stdout+stderr into a pipe, relayed and scanned -- */
    int pfd[2];
    if (pipe(pfd) < 0) die("pipe : %s", strerror(errno));
    struct sigaction sa = { 0 };
    sa.sa_handler = on_sig;           /* no SA_RESTART: read() must return EINTR */
    sigaction(SIGINT, &sa, NULL); sigaction(SIGTERM, &sa, NULL); sigaction(SIGHUP, &sa, NULL);
    struct sigaction sc = { 0 };
    sc.sa_handler = on_sig; sc.sa_flags = SA_NOCLDSTOP;
    sigaction(SIGCHLD, &sc, NULL);

    g_qemu = fork();
    if (g_qemu < 0) die("fork : %s", strerror(errno));
    if (g_qemu == 0) {
        close(pfd[0]);
        /* QEMU announces its ptys on STDOUT ("char device redirected to ...")
         * and its probes on STDERR: capture both, like the `>>qemu.log 2>&1`
         * of 40-qemu.sh. */
        dup2(pfd[1], 1);
        dup2(pfd[1], 2);
        close(pfd[1]);
        signal(SIGINT, SIG_DFL); signal(SIGTERM, SIG_DFL); signal(SIGHUP, SIG_DFL); signal(SIGCHLD, SIG_DFL);
        execv(qa.v[0], qa.v);
        fprintf(stderr, "[%s] exec %s : %s\n", g_alias, qa.v[0], strerror(errno));
        _exit(127);
    }
    close(pfd[1]);
    {
        char *p = xasprintf("%s/%s.pid", o.rundir, g_alias); write_pid(p, getpid()); free(p);
        p = xasprintf("%s/%s-qemu.pid", o.rundir, g_alias); write_pid(p, g_qemu); free(p);
    }
    if (!o.quiet) say("QEMU pid %d", (int)g_qemu);

    char buf[65536];
    char line[4096]; size_t ll = 0;
    bool modem_done = false, irda_done = false;
    int term_sent = 0;
    int qemu_status = -1;
    for (;;) {
        if (g_sig) {
            int s = g_sig; g_sig = 0;
            if (s == SIGCHLD) {
                int st; pid_t p;
                while ((p = waitpid(-1, &st, WNOHANG)) > 0) {
                    if (p == g_qemu) { qemu_status = st; g_qemu = 0; }
                    else if (p == g_osmocon) { if (!o.quiet) say("osmocon terminé (%d)", WIFEXITED(st) ? WEXITSTATUS(st) : -1); g_osmocon = 0; }
                }
            } else {
                if (g_qemu > 0) kill(g_qemu, term_sent++ ? SIGKILL : SIGTERM);
                if (g_osmocon > 0) kill(g_osmocon, SIGTERM);
            }
        }
        ssize_t n = read(pfd[0], buf, sizeof buf);
        if (n < 0) { if (errno == EINTR) continue; break; }
        if (n == 0) break;                          /* QEMU closed stderr */
        (void)!write(2, buf, n);                    /* relay verbatim */
        for (ssize_t i = 0; i < n; i++) {
            char c = buf[i];
            if (c == '\n') {
                line[ll] = 0;
                /* "char device redirected to /dev/pts/N (label serial0)" */
                char *r = strstr(line, "redirected to ");
                if (r) {
                    char *dev = r + strlen("redirected to ");
                    char *sp = strchr(dev, ' ');
                    if (sp) *sp = 0;
                    const char *lab = sp ? strstr(sp + 1, "label ") : NULL;
                    if (lab && !strncmp(lab + 6, "serial0", 7) && !modem_done) {
                        modem_done = true;
                        if (o.pty_link) make_link(dev, o.pty_link);
                        {
                            char *p = xasprintf("%s/modem.pty.path", o.rundir);
                            FILE *f = fopen(p, "w"); if (f) { fprintf(f, "%s\n", dev); fclose(f); } free(p);
                        }
                        if (!o.quiet) say("pty modem : %s%s%s", dev, o.pty_link ? "  ->  " : "", o.pty_link ? o.pty_link : "");
                        if (o.osmocon) {
                            g_osmocon = spawn_osmocon(&o, o.pty_link ? o.pty_link : dev);
                            if (g_osmocon > 0) {
                                char *p = xasprintf("%s/%s-osmocon.pid", o.rundir, g_alias); write_pid(p, g_osmocon); free(p);
                                if (!o.quiet) say("osmocon pid %d  (journal %s)", (int)g_osmocon, o.osmocon_log);
                            }
                        } else if (!o.quiet) {
                            say("osmocon   : %s -m %s -i %s -p %s -s %s %s -d %s", o.osmocon_bin, o.osmocon_model,
                                o.osmocon_delay, o.pty_link ? o.pty_link : dev, o.l1ctl, o.fw_bin, o.osmocon_debug);
                        }
                    } else if (lab && !strncmp(lab + 6, "serial1", 7) && !irda_done) {
                        irda_done = true;
                        if (o.irda_link) make_link(dev, o.irda_link);
                        if (!o.quiet) say("pty irda  : %s%s%s", dev, o.irda_link ? "  ->  " : "", o.irda_link ? o.irda_link : "");
                    }
                }
                ll = 0;
            } else if (ll + 1 < sizeof line) {
                line[ll++] = c;
            }
        }
    }
    close(pfd[0]);
    if (g_qemu > 0) {
        int st;
        while (waitpid(g_qemu, &st, 0) < 0 && errno == EINTR) {}
        qemu_status = st; g_qemu = 0;
    }
    if (g_osmocon > 0) { kill(g_osmocon, SIGTERM); waitpid(g_osmocon, NULL, 0); g_osmocon = 0; }

    /* cleanup: links and pid files; the L1CTL socket is left to osmocon/mobile */
    if (o.pty_link) unlink(o.pty_link);
    if (o.irda_link) unlink(o.irda_link);
    { char *p = xasprintf("%s/%s.pid", o.rundir, g_alias); unlink(p); free(p);
      p = xasprintf("%s/%s-qemu.pid", o.rundir, g_alias); unlink(p); free(p);
      p = xasprintf("%s/%s-osmocon.pid", o.rundir, g_alias); unlink(p); free(p);
      p = xasprintf("%s/modem.pty.path", o.rundir); unlink(p); free(p); }

    int rc = 1;
    if (qemu_status >= 0) {
        if (WIFEXITED(qemu_status)) rc = WEXITSTATUS(qemu_status);
        else if (WIFSIGNALED(qemu_status)) rc = 128 + WTERMSIG(qemu_status);
    }
    if (!o.quiet) say("QEMU terminé (code %d)", rc);
    return rc;
}

4.111 https://github.com/bbaranoff/qosmo/blob/main/tools/qosmo-launch/qosmo-launch.c

44332 octets, 971 lignes → 970 lignes (1 groupes compactés)

/*
 * qosmo-launch.c — C launcher of qemu-system-arm for the Calypso machine.
 *
 * Built TWICE, in the directory of each fork:
 *   /opt/GSM/qosmo-dsp/tools/qosmo-launch    -> qosmo-dsp    (-DQOSMO_DSP=1)
 *   /opt/GSM/qosmo-grgsm/tools/qosmo-launch  -> qosmo-grgsm  (-DQOSMO_DSP=0)
 * The source is IDENTICAL in both directories; the Makefile sets the alias,
 * the QEMU tree and the flavour. Installed in /usr/local/bin by `make install`.
 *
 * What the launcher does, and nothing more:
 *   1. resolves the firmware (-k ELF | directory | .bin) and its neighbours
 *      (.bin for osmocon, .map); reads the `l1s` and `last_rach` symbols in the
 *      ELF (ELF32 ARM) and exports them as run_modules/16-fwsyms.sh does with
 *      nm (CALYPSO_L1S_FN_ADDR / CALYPSO_LAST_RACH_FN_ADDR);
 *   2. (dsp flavour) resolves the 7 C54x ROMs from a directory or a prefix
 *      (-dsp) and passes them as machine properties dsp-prom0=…;
 *   3. exposes the sockets and the network WITH THE WORKING DEFAULTS (those of
 *      run.sh / 40-qemu.sh): unix monitor, ARM gdbstub tcp::1234,
 *      L1CTL /tmp/osmocom_l2, TRXDv0 udp 0.0.0.0:6702, IQ tee 127.0.0.1:6703;
 *      each is selected by an option (--monitor, --gdb, --l1ctl, --bind,
 *      --trx-port, --iq-tee);
 *   4. starts QEMU with `-serial pty -serial pty`, relays its stderr as is
 *      (the 41-pty modules / grep "redirected" keep working), and publishes
 *      STABLE links to the two ptys: <rundir>/modem.pty and <rundir>/irda.pty
 *      — osmocon is run directly on those;
 *   5. on request (-o) starts osmocon itself (-m romload) as soon as the modem
 *      pty exists; forwards SIGINT/SIGTERM to QEMU; exits with its code.
 *
 * Any unknown argument starting with `-` (and its value if it does not start
 * with `-`) is passed to QEMU. After `--`, everything goes to QEMU.
 */
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <ctype.h>
#include <dirent.h>
#include <elf.h>
#include <errno.h>
#include <fcntl.h>
#include <ifaddrs.h>
#include <libgen.h>
#include <limits.h>
#include <net/if.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#ifndef QOSMO_ALIAS
#define QOSMO_ALIAS "qosmo-dsp"
#endif
#ifndef QOSMO_TREE
#define QOSMO_TREE "/opt/GSM/qosmo-dsp"
#endif
#ifndef QOSMO_DSP
#define QOSMO_DSP 1
#endif
#ifndef QOSMO_GSM_ROOT
#define QOSMO_GSM_ROOT "/opt/GSM"
#endif
#ifndef QOSMO_VERSION
#define QOSMO_VERSION "1.0"
#endif

/* ---- defaults = the working values (run.sh / paths.env / 40-qemu.sh) ---- */
#define DEF_QEMU        QOSMO_TREE "/build/qemu-system-arm"
#define DEF_FIRMWARE    QOSMO_GSM_ROOT "/firmware/board/compal_e88/layer1.highram.elf"
#define DEF_DSP_DIR     QOSMO_GSM_ROOT
#define DEF_OSMOCON     QOSMO_GSM_ROOT "/osmocom-bb/src/host/osmocon/osmocon"
#define DEF_CPU         "arm946"
#define DEF_GDB         "1234"
/* THE calypso MACHINE HAS NO SCREEN. Without -display, QEMU takes its
 * graphical default (SDL is built into this fork: `qemu-system-arm -display
 * help` lists it) and, with a display active, wires its default devices to
 * virtual consoles. Both serial ports are already taken (-serial pty x2) and
 * the monitor too (-monitor unix:): only the parallel port remains, which
 * would give an empty SDL window named "parallel0" that grabs the keyboard,
 * while the boot happens on the ptys. Without DISPLAY (osmo-banc.service at
 * boot) QEMU cannot open SDL and EXITS. The model has nothing to display:
 * --display none by default. `--display sdl` (or -display/-nographic after
 * --) gives the SDL behaviour. */
#define DEF_DISPLAY     "none"
#define DEF_L1CTL       "/tmp/osmocom_l2"
#define DEF_BIND        "0.0.0.0"
#define DEF_TRX_PORT    "6702"
#define DEF_IQ_TEE_HOST "127.0.0.1"
#define DEF_IQ_TEE_PORT "6703"
#define DEF_OSMOCON_MODEL "romload"
#define DEF_OSMOCON_DELAY "100"
#define DEF_OSMOCON_DEBUG "tr"
#define GRGSM_GSMTAP_PORT "4730"   /* fixed in calypso_l1_grgsm.c */

static const char *g_alias = QOSMO_ALIAS;

/* ------------------------------------------------------------------ util */
static void say(const char *fmt, ...)
{
    va_list ap;
    fprintf(stderr, "[%s] ", g_alias);
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
    fputc('\n', stderr);
}

static void die(const char *fmt, ...)
{
    va_list ap;
    fprintf(stderr, "[%s] ERREUR : ", g_alias);
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
    fputc('\n', stderr);
    exit(2);
}

static char *xstrdup(const char *s)
{
    char *d = strdup(s ? s : "");
    if (!d) die("mémoire épuisée");
    return d;
}

static char *xasprintf(const char *fmt, ...)
{
    char *p = NULL;
    va_list ap;
    va_start(ap, fmt);
    if (vasprintf(&p, fmt, ap) < 0) die("mémoire épuisée");
    va_end(ap);
    return p;
}

static bool is_dir(const char *p)
{
    struct stat st;
    return p && stat(p, &st) == 0 && S_ISDIR(st.st_mode);
}

static bool is_readable_file(const char *p)
{
    struct stat st;
    return p && stat(p, &st) == 0 && S_ISREG(st.st_mode) && access(p, R_OK) == 0;
}

static bool is_exec(const char *p)
{
    struct stat st;
    return p && stat(p, &st) == 0 && S_ISREG(st.st_mode) && access(p, X_OK) == 0;
}

static bool ends_with(const char *s, const char *suf)
{
    size_t ls = strlen(s), lf = strlen(suf);
    return ls >= lf && strcmp(s + ls - lf, suf) == 0;
}

static const char *env_nonempty(const char *n)
{
    const char *v = getenv(n);
    return (v && *v) ? v : NULL;
}

/* Sets a variable ONLY if it is absent or empty: the inherited environment
 * (run.sh, operator) keeps precedence, unless an explicit option is given. */
static void env_default(const char *n, const char *v)
{
    if (!env_nonempty(n) && v && *v) setenv(n, v, 1);
}

static void env_force(const char *n, const char *v)
{
    if (v && *v) setenv(n, v, 1);
}

static int mkdir_p(const char *path)
{
    char tmp[PATH_MAX];
    snprintf(tmp, sizeof tmp, "%s", path);
    for (char *p = tmp + 1; *p; p++) {
        if (*p == '/') {
            *p = 0;
            if (mkdir(tmp, 0755) < 0 && errno != EEXIST) return -1;
            *p = '/';
        }
    }
    if (mkdir(tmp, 0755) < 0 && errno != EEXIST) return -1;
    return 0;
}

static char *abspath(const char *p)
{
    char buf[PATH_MAX];
    if (realpath(p, buf)) return xstrdup(buf);
    return xstrdup(p);
}

/* ------------------------------------------------------------ argv dyn. */
typedef struct { char **v; int n, cap; } argv_t;

static void argv_push(argv_t *a, const char *s)
{
    if (a->n + 2 > a->cap) {
        a->cap = a->cap ? a->cap * 2 : 32;
        a->v = realloc(a->v, sizeof(char *) * a->cap);
        if (!a->v) die("mémoire épuisée");
    }
    a->v[a->n++] = xstrdup(s);
    a->v[a->n] = NULL;
}

/* ------------------------------------------------------- ELF32 symbols */
/* Looks up `names[i]` in the symbol table of a little-endian ELF32 and
 * returns its value in addrs[i] (found[i]=true). Equivalent of
 * `nm ELF | awk '$3==name'` of 16-fwsyms.sh, without depending on binutils. */
static int elf32_lookup(const char *path, const char **names, int n,
                        uint32_t *addrs, bool *found)
{
    int fd = open(path, O_RDONLY);
    if (fd < 0) return -1;
    struct stat st;
    if (fstat(fd, &st) < 0 || st.st_size < (off_t)sizeof(Elf32_Ehdr)) { close(fd); return -1; }
    uint8_t *m = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    close(fd);
    if (m == MAP_FAILED) return -1;
    int rc = -1;
    Elf32_Ehdr *eh = (Elf32_Ehdr *)m;
    if (memcmp(eh->e_ident, ELFMAG, SELFMAG) || eh->e_ident[EI_CLASS] != ELFCLASS32 ||
        eh->e_ident[EI_DATA] != ELFDATA2LSB)
        goto out;
    if (eh->e_shoff == 0 || eh->e_shentsize != sizeof(Elf32_Shdr) ||
        (off_t)eh->e_shoff + (off_t)((size_t)eh->e_shnum * sizeof(Elf32_Shdr)) > st.st_size)
        goto out;
    Elf32_Shdr *sh = (Elf32_Shdr *)(m + eh->e_shoff);
    for (int i = 0; i < n; i++) found[i] = false;
    rc = 0;
    for (int s = 0; s < eh->e_shnum; s++) {
        if (sh[s].sh_type != SHT_SYMTAB) continue;
        if (sh[s].sh_link >= eh->e_shnum) continue;
        Elf32_Shdr *strs = &sh[sh[s].sh_link];
        if ((off_t)sh[s].sh_offset + sh[s].sh_size > st.st_size ||
            (off_t)strs->sh_offset + strs->sh_size > st.st_size)
            continue;
        Elf32_Sym *sym = (Elf32_Sym *)(m + sh[s].sh_offset);
        size_t nsym = sh[s].sh_size / sizeof(Elf32_Sym);
        const char *strtab = (const char *)(m + strs->sh_offset);
        for (size_t k = 0; k < nsym; k++) {
            if (sym[k].st_name >= strs->sh_size) continue;
            const char *nm = strtab + sym[k].st_name;
            for (int i = 0; i < n; i++) {
                if (!found[i] && strcmp(nm, names[i]) == 0) {
                    addrs[i] = sym[k].st_value;
                    found[i] = true;
                }
            }
        }
    }
out:
    munmap(m, st.st_size);
    return rc;
}

/* ------------------------------------------------------------ DSP ROMs */
static const char *rom_tokens[] = { "PROM0", "PROM1", "PROM2", "PROM3", "DROM", "PDROM", "Registers" };
static const char *rom_props[]  = { "dsp-prom0", "dsp-prom1", "dsp-prom2", "dsp-prom3", "dsp-drom", "dsp-pdrom", "dsp-registers" };
static const char *rom_envs[]   = { "DSP_PROM0", "DSP_PROM1", "DSP_PROM2", "DSP_PROM3", "DSP_DROM", "DSP_PDROM", "DSP_REGISTERS" };
#define N_ROMS 7

static int strcasestr_has(const char *hay, const char *needle)
{
    return strcasestr(hay, needle) != NULL;
}

/* Looks for the ROM `tok` in `dir`: known names first, then a scan (any file
 * whose name contains "<tok>.bin", case-insensitive). */
static char *find_rom_in_dir(const char *dir, const char *tok)
{
    const char *pat[] = { "%s/calypso_dsp.%s.bin", "%s/%s.bin", "%s/.%s.bin",
                          "%s/calypso_dsp_%s.bin", "%s/dsp.%s.bin", "%s/dsp_%s.bin" };
    for (size_t i = 0; i < sizeof pat / sizeof *pat; i++) {
        char *p = xasprintf(pat[i], dir, tok);
        if (is_readable_file(p)) return p;
        free(p);
    }
    DIR *d = opendir(dir);
    if (!d) return NULL;
    struct dirent *e;
    char *best = NULL;
    char *needle = xasprintf("%s.bin", tok);
    while ((e = readdir(d))) {
        if (!strcasestr_has(e->d_name, needle)) continue;
        /* avoids PDROM when looking for DROM */
        if (strcmp(tok, "DROM") == 0 && strcasestr_has(e->d_name, "PDROM")) continue;
        char *p = xasprintf("%s/%s", dir, e->d_name);
        if (is_readable_file(p)) { best = p; break; }
        free(p);
    }
    free(needle);
    closedir(d);
    return best;
}

/* `spec` = directory, or prefix/file of a ROM (e.g. /x/calypso_dsp.PROM0.bin
 * or /x/calypso_dsp.). Fills roms[]. Returns the number found. */
static int resolve_roms(const char *spec, char **roms)
{
    int n = 0;
    if (is_dir(spec)) {
        for (int i = 0; i < N_ROMS; i++) {
            roms[i] = find_rom_in_dir(spec, rom_tokens[i]);
            if (roms[i]) n++;
        }
        return n;
    }
    /* prefix: cut at the first known token if present */
    char *prefix = xstrdup(spec);
    for (int i = 0; i < N_ROMS; i++) {
        char *hit = strcasestr(prefix, rom_tokens[i]);
        if (hit) { *hit = 0; break; }
    }
    for (int i = 0; i < N_ROMS; i++) {
        char *p = xasprintf("%s%s.bin", prefix, rom_tokens[i]);
        if (is_readable_file(p)) { roms[i] = p; n++; }
        else { free(p); roms[i] = NULL; }
    }
    if (n == 0) {
        /* maybe a file in a directory with exotic names: fall back to the
         * directory */
        char *dup = xstrdup(spec);
        char *dir = dirname(dup);
        for (int i = 0; i < N_ROMS; i++) {
            roms[i] = find_rom_in_dir(dir, rom_tokens[i]);
            if (roms[i]) n++;
        }
        free(dup);
    }
    free(prefix);
    return n;
}

/* -------------------------------------------------------------- network */
static bool is_ipv4_literal(const char *s)
{
    struct in_addr a;
    return inet_aton(s, &a) != 0;
}

/* `spec` = literal IPv4, or interface name (eth0, lo, …) -> IPv4 */
static char *resolve_bind(const char *spec)
{
    if (is_ipv4_literal(spec)) return xstrdup(spec);
    struct ifaddrs *ifa = NULL, *p;
    if (getifaddrs(&ifa) < 0) die("--bind %s : getifaddrs : %s", spec, strerror(errno));
    char *res = NULL;
    for (p = ifa; p; p = p->ifa_next) {
        if (!p->ifa_addr || p->ifa_addr->sa_family != AF_INET) continue;
        if (strcmp(p->ifa_name, spec) != 0) continue;
        char buf[INET_ADDRSTRLEN];
        struct sockaddr_in *sin = (struct sockaddr_in *)p->ifa_addr;
        if (inet_ntop(AF_INET, &sin->sin_addr, buf, sizeof buf)) { res = xstrdup(buf); break; }
    }
    freeifaddrs(ifa);
    if (!res) die("--bind %s : ni adresse IPv4 ni interface avec une IPv4", spec);
    return res;
}

/* `spec` -> argument of `-gdb`: off | PORT | ADDR:PORT | IFACE:PORT | raw
 * (already contains "tcp:" or "unix:"). Returns NULL for off. */
static char *gdb_spec(const char *spec)
{
    if (!spec || !*spec || !strcmp(spec, "off") || !strcmp(spec, "none") || !strcmp(spec, "0"))
        return NULL;
    if (strstr(spec, "tcp:") || strstr(spec, "unix:")) return xstrdup(spec);
    const char *colon = strrchr(spec, ':');
    if (!colon) {
        char *e; long p = strtol(spec, &e, 10);
        if (*e || p <= 0 || p > 65535) die("--gdb %s : port invalide", spec);
        return xasprintf("tcp::%ld", p);
    }
    char *host = strndup(spec, colon - spec);
    char *hres = resolve_bind(host);
    free(host);
    char *r = xasprintf("tcp:%s:%s", hres, colon + 1);
    free(hres);
    return r;
}

/* ------------------------------------------------------------- options */
typedef struct {
    const char *qemu, *firmware, *fw_bin, *cpu, *rundir, *monitor, *gdb, *display;
    const char *icount, *asm_log;   /* -icount SPEC ; -d in_asm -D FILE */
    const char *l1ctl, *bind, *trx_port, *iq_tee;
    const char *pty_link, *irda_link;
    const char *dsp_spec;
    const char *osmocon_bin, *osmocon_model, *osmocon_delay, *osmocon_debug, *osmocon_log;
    bool osmocon, dry_run, quiet, no_pty_link, no_monitor;
    argv_t extra;   /* extra QEMU args */
    argv_t envs;    /* -e VAR=VAL */
} opts_t;

static void usage(FILE *f)
{
    fprintf(f,
"Usage : %s [-k FIRMWARE.elf]%s [options] [-- args QEMU]\n"
"\n"
"Lance qemu-system-arm (machine calypso) du fork %s avec les défauts qui\n"
"marchent déjà, publie les sockets/pty, et permet osmocon directement dessus.\n"
"\n"
"Firmware :\n"
"  -k, -kernel FICHIER     ELF layer1 (défaut : $FIRMWARE_ELF ou\n"
"                          %s)\n"
"                          Un DOSSIER => layer1.highram.elf dedans ; un .bin => l'.elf voisin.\n"
"                          Les symboles l1s/last_rach sont lus dans l'ELF, le .bin voisin\n"
"                          sert à osmocon.\n"
"      --bin FICHIER       image .bin pour osmocon (défaut : <elf sans .elf>.bin)\n"
"%s"
"\n"
"Sockets :\n"
"  -r, --rundir DIR        dossier des sockets/liens/pid (défaut : $RUN_DIR ou /tmp/%s)\n"
"  -M, --monitor CHEMIN    socket moniteur QEMU (défaut : <rundir>/qemu-monitor.sock ; none = aucun)\n"
"  -s, --l1ctl CHEMIN      socket L1CTL (défaut : $L1CTL_SOCK ou %s) — exportée L1CTL_SOCK,\n"
"                          passée à osmocon -s\n"
"  -p, --pty CHEMIN        lien stable vers le pty modem/serial0 (défaut : <rundir>/modem.pty)\n"
"      --irda CHEMIN       lien stable vers le pty IrDA/serial1 (défaut : <rundir>/irda.pty)\n"
"      --no-pty-link       ne crée aucun lien\n"
"\n"
"Réseau :\n"
"  -g, --gdb SPEC          gdbstub ARM : PORT | ADDR:PORT | IFACE:PORT | off (défaut : %s)\n"
"      --bind ADDR|IFACE   adresse/interface d'écoute du modèle (TRXDv0) (défaut : %s)%s\n"
"      --trx-port N        port UDP TRXDv0 d'entrée des bursts (défaut : %s)%s\n"
"      --iq-tee HOST:PORT  destination du tee I/Q (défaut : %s:%s)%s\n"
"\n"
"osmocon :\n"
"  -o, --osmocon           lance osmocon (-m %s -i %s) dès que le pty modem existe\n"
"      --osmocon-bin P     binaire (défaut : $OSMOCON ou %s)\n"
"      --osmocon-model M   (défaut %s)   --osmocon-delay N (défaut %s)   --osmocon-debug F (défaut %s)\n"
"      --osmocon-log F     journal d'osmocon (défaut : <rundir>/osmocon.log ; - = hérite du terminal)\n"
"\n"
"Divers :\n"
"      --qemu CHEMIN       binaire qemu-system-arm (défaut : $QEMU_BIN ou %s)\n"
"      --cpu MODÈLE        (défaut %s)\n"
"      --display BACKEND   affichage QEMU : none | sdl | … ($QOSMO_DISPLAY, défaut %s).\n"
"                          La machine calypso n'a pas d'écran : `none` évite la fenêtre\n"
"                          vide « parallel0 » et permet de tourner sans DISPLAY.\n"
"      --nographic         équivalent de --display none\n"
"      --icount[=SPEC]     -icount SPEC ($QOSMO_ICOUNT ; SPEC défaut : auto)\n"
"      --asm[=FICHIER]     -d in_asm -D FICHIER ($QOSMO_ASM ; défaut : <rundir>/qemu-asm.log)\n"
"  -e, --env VAR=VAL       variable d'environnement pour le modèle (CALYPSO_*, …)\n"
"  -n, --dry-run           affiche l'environnement et la commande, ne lance rien\n"
"  -q, --quiet             pas de bannière\n"
"  -h, --help  -V, --version\n"
"\n"
"Tout argument inconnu commençant par `-` est transmis à QEMU (avec sa valeur si\n"
"elle ne commence pas par `-`). Après `--`, tout va à QEMU.\n"
"\n"
"Exemples :\n"
"  %s -k /opt/GSM/firmware/board/compal_e88/layer1.highram.elf%s -o\n"
"  osmocon -m romload -i 100 -p /tmp/%s/modem.pty -s %s layer1.highram.bin\n",
        g_alias, QOSMO_DSP ? " [-dsp DOSSIER|PRÉFIXE]" : "", QOSMO_TREE, DEF_FIRMWARE,
        QOSMO_DSP ?
"\n"
"DSP (TMS320C54x) :\n"
"  -dsp, --dsp DOSSIER|PRÉFIXE\n"
"                          ROMs PROM0..3, DROM, PDROM, Registers. Dossier : calypso_dsp.X.bin,\n"
"                          X.bin, .X.bin ou tout *X.bin ; préfixe : /chemin/calypso_dsp.\n"
"                          Défaut : $DSP_PROM0..$DSP_REGISTERS si posés, sinon $DSP_ROM_DIR\n"
"                          ou " DEF_DSP_DIR ".\n" : "",
        g_alias, DEF_L1CTL, DEF_GDB,
        DEF_BIND, QOSMO_DSP ? "" : "  [sans effet sur ce fork]",
        DEF_TRX_PORT, QOSMO_DSP ? "" : "  [sans effet sur ce fork]",
        DEF_IQ_TEE_HOST, DEF_IQ_TEE_PORT, QOSMO_DSP ? "" : "  [sans effet sur ce fork]",
        DEF_OSMOCON_MODEL, DEF_OSMOCON_DELAY, DEF_OSMOCON, DEF_OSMOCON_MODEL, DEF_OSMOCON_DELAY, DEF_OSMOCON_DEBUG,
        DEF_QEMU, DEF_CPU, DEF_DISPLAY,
        g_alias, QOSMO_DSP ? " -dsp /opt/GSM" : "", g_alias, DEF_L1CTL);
}

/* --opt VAL | --opt=VAL | -oVAL(no); returns the value, advances *i */
static const char *optval(int argc, char **argv, int *i, const char *eq)
{
    if (eq) return eq + 1;
    if (*i + 1 >= argc) die("option %s : valeur manquante", argv[*i]);
    return argv[++(*i)];
}

static bool optis(const char *a, const char *eq, const char *s1, const char *s2, const char *s3)
{
    size_t l = eq ? (size_t)(eq - a) : strlen(a);
    if (s⟨1⟩ && strlen(s⟨2⟩) == l && !strncmp(a, s⟨3⟩, l)) return true;  ×3
    ⟨⟩ = (1,1,1) (2,2,2) (3,3,3)
    return false;
}

static void parse_args(int argc, char **argv, opts_t *o)
{
    for (int i = 1; i < argc; i++) {
        const char *a = argv[i];
        if (!strcmp(a, "--")) { for (i++; i < argc; i++) argv_push(&o->extra, argv[i]); break; }
        const char *eq = (a[0] == '-' && a[1] == '-') ? strchr(a, '=') : NULL;
#define IS(...) optis(a, eq, __VA_ARGS__)
#define VAL()   optval(argc, argv, &i, eq)
        if (IS("-h", "--help", NULL))        { usage(stdout); exit(0); }
        else if (IS("-V", "--version", NULL)) { printf("%s %s (%s)\n", g_alias, QOSMO_VERSION, QOSMO_TREE); exit(0); }
        else if (IS("-k", "-kernel", "--kernel")) o->firmware = VAL();
        else if (IS("--bin", NULL, NULL))     o->fw_bin = VAL();
        else if (IS("-dsp", "--dsp", "--dsp-roms")) {
            if (!QOSMO_DSP) die("%s : pas de DSP émulé sur ce fork (utilisez qosmo-dsp)", a);
            o->dsp_spec = VAL();
        }
        else if (IS("-r", "--rundir", NULL))  o->rundir = VAL();
        else if (IS("-M", "--monitor", NULL)) o->monitor = VAL();
        else if (IS("--no-monitor", NULL, NULL)) o->no_monitor = true;
        else if (IS("-s", "--l1ctl", NULL))   o->l1ctl = VAL();
        else if (IS("-p", "--pty", "--pty-link")) o->pty_link = VAL();
        else if (IS("--irda", "--irda-link", NULL)) o->irda_link = VAL();
        else if (IS("--no-pty-link", NULL, NULL)) o->no_pty_link = true;
        else if (IS("-g", "--gdb", NULL))     o->gdb = VAL();
        else if (IS("--no-gdb", NULL, NULL))  o->gdb = "off";
        else if (IS("--bind", "--iface", "--interface")) o->bind = VAL();
        else if (IS("--trx-port", "--bsp-port", NULL)) o->trx_port = VAL();
        else if (IS("--iq-tee", NULL, NULL))  o->iq_tee = VAL();
        else if (IS("-o", "--osmocon", NULL)) o->osmocon = true;
        else if (IS("--no-osmocon", NULL, NULL)) o->osmocon = false;
        else if (IS("--osmocon-bin", NULL, NULL))   o->osmocon_bin = VAL();
        else if (IS("--osmocon-model", NULL, NULL)) o->osmocon_model = VAL();
        else if (IS("--osmocon-delay", NULL, NULL)) o->osmocon_delay = VAL();
        else if (IS("--osmocon-debug", NULL, NULL)) o->osmocon_debug = VAL();
        else if (IS("--osmocon-log", NULL, NULL))   o->osmocon_log = VAL();
        else if (IS("--qemu", "--qemu-bin", NULL)) o->qemu = VAL();
        else if (IS("--cpu", "-cpu", NULL))   o->cpu = VAL();
        else if (IS("--display", "-display", NULL)) o->display = VAL();
        else if (IS("--nographic", "-nographic", NULL)) o->display = "none";
        else if (IS("--icount", NULL, NULL))  o->icount = eq ? eq + 1 : "auto";
        else if (IS("--no-icount", NULL, NULL)) o->icount = "off";
        else if (IS("--asm", "--in-asm", NULL)) o->asm_log = eq ? eq + 1 : "";
        else if (IS("--no-asm", NULL, NULL))  o->asm_log = "off";
        else if (IS("-e", "--env", NULL))     argv_push(&o->envs, VAL());
        else if (IS("-n", "--dry-run", NULL)) o->dry_run = true;
        else if (IS("-q", "--quiet", NULL))   o->quiet = true;
        else if (a[0] == '-' && a[1]) {
            /* unknown -> QEMU, with its value if any */
            argv_push(&o->extra, a);
            if (i + 1 < argc && argv[i + 1][0] != '-') argv_push(&o->extra, argv[++i]);
        }
        else if (!o->firmware && (ends_with(a, ".elf") || ends_with(a, ".bin") || is_dir(a)))
            o->firmware = a;
        else die("argument inattendu : %s (voir --help)", a);
#undef IS
#undef VAL
    }
}

/* ------------------------------------------------------ pty / osmocon */
static volatile sig_atomic_t g_sig = 0;
static void on_sig(int s) { g_sig = s; }

static pid_t g_qemu = 0, g_osmocon = 0;

static void make_link(const char *target, const char *link)
{
    if (!link || !*link) return;
    unlink(link);
    if (symlink(target, link) < 0)
        say("lien %s -> %s impossible : %s", link, target, strerror(errno));
}

static void write_pid(const char *path, pid_t pid)
{
    FILE *f = fopen(path, "w");
    if (!f) return;
    fprintf(f, "%d\n", (int)pid);
    fclose(f);
}

static pid_t spawn_osmocon(const opts_t *o, const char *pty)
{
    if (!is_exec(o->osmocon_bin)) {
        say("osmocon introuvable ou non exécutable : %s (--osmocon-bin)", o->osmocon_bin);
        return 0;
    }
    if (!is_readable_file(o->fw_bin)) {
        say("image .bin pour osmocon illisible : %s (--bin)", o->fw_bin ? o->fw_bin : "(aucune)");
        return 0;
    }
    /* like 50-osmocon.sh: the socket of a previous run would make the bind fail */
    unlink(o->l1ctl);
    pid_t p = fork();
    if (p < 0) { say("fork osmocon : %s", strerror(errno)); return 0; }
    if (p == 0) {
        if (o->osmocon_log && strcmp(o->osmocon_log, "-") != 0) {
            int fd = open(o->osmocon_log, O_WRONLY | O_CREAT | O_APPEND, 0644);
            if (fd >= 0) { dup2(fd, 1); dup2(fd, 2); close(fd); }
        }
        execl(o->osmocon_bin, o->osmocon_bin,
              "-m", o->osmocon_model, "-i", o->osmocon_delay,
              "-p", pty, "-s", o->l1ctl, o->fw_bin,
              "-d", o->osmocon_debug, (char *)NULL);
        _exit(127);
    }
    return p;
}

/* ------------------------------------------------------------------ main */
int main(int argc, char **argv)
{
    opts_t o = { 0 };
    parse_args(argc, argv, &o);

    /* -- environment set by -e, BEFORE the defaults (it wins) -- */
    for (int i = 0; i < o.envs.n; i++) {
        char *kv = xstrdup(o.envs.v[i]);
        char *e = strchr(kv, '=');
        if (!e) die("-e %s : attendu VAR=VAL", kv);
        *e = 0;
        setenv(kv, e + 1, 1);
        free(kv);
    }

    /* -- QEMU -- */
    if (!o.qemu) o.qemu = env_nonempty("QEMU_BIN") ? env_nonempty("QEMU_BIN") : DEF_QEMU;
    if (!is_exec(o.qemu)) die("qemu-system-arm introuvable : %s (--qemu ; compilez : ninja -C %s/build qemu-system-arm)", o.qemu, QOSMO_TREE);
    if (!o.cpu) o.cpu = DEF_CPU;
    if (!o.display) o.display = env_nonempty("QOSMO_DISPLAY") ? env_nonempty("QOSMO_DISPLAY") : DEF_DISPLAY;

    /* -- firmware -- */
    if (!o.firmware) o.firmware = env_nonempty("FIRMWARE_ELF") ? env_nonempty("FIRMWARE_ELF") : DEF_FIRMWARE;
    char *fw = xstrdup(o.firmware);
    if (is_dir(fw)) { char *t = xasprintf("%s/layer1.highram.elf", fw); free(fw); fw = t; }
    if (ends_with(fw, ".bin")) {
        char *elf = xstrdup(fw); strcpy(elf + strlen(elf) - 4, ".elf");
        if (!is_readable_file(elf)) die("firmware %s : .elf voisin introuvable (%s)", fw, elf);
        if (!o.fw_bin) o.fw_bin = xstrdup(fw);
        free(fw); fw = elf;
    }
    if (!is_readable_file(fw)) die("firmware ELF illisible : %s (-k)", fw);
    fw = abspath(fw);
    char *stem = xstrdup(fw);
    if (ends_with(stem, ".elf")) stem[strlen(stem) - 4] = 0;
    if (!o.fw_bin) {
        char *b = xasprintf("%s.bin", stem);
        if (!is_readable_file(b) && env_nonempty("FIRMWARE_BIN")) { free(b); b = xstrdup(env_nonempty("FIRMWARE_BIN")); }
        o.fw_bin = b;
    }
    /* Safeguard: under QEMU the romload stub of hw/char/calypso_uart.c IGNORES
     * the .bin payload (the running firmware is the ELF of -kernel), the .bin
     * only serves the osmocon handshake. But FIRMWARE_BIN (run.sh) is set
     * independently of FIRMWARE_ELF: a .bin from another build would go
     * unnoticed, and on a REAL phone that is the one that would run. */
    bool fw_mismatch = false;
    {
        char *sib = xasprintf("%s.bin", stem);
        char *a = abspath(o.fw_bin), *b = abspath(sib);
        if (is_readable_file(sib) && strcmp(a, b) != 0) fw_mismatch = true;
        free(a); free(b); free(sib);
    }
    char *fw_map = xasprintf("%s.map", stem);
    char *fw_dir = xstrdup(fw); fw_dir = dirname(fw_dir);

    const char *symnames[2] = { "l1s", "last_rach" };
    uint32_t symaddr[2] = { 0, 0 }; bool symok[2] = { false, false };
    if (elf32_lookup(fw, symnames, 2, symaddr, symok) < 0)
        say("avertissement : %s n'est pas un ELF32 lisible, symboles non lus", fw);

    env_default("FIRMWARE_ELF", fw);
    env_default("FIRMWARE_BIN", o.fw_bin);
    env_default("FIRMWARE_DIR", fw_dir);
    env_default("CALYPSO_FIRMWARE_ELF", fw);
    env_default("CALYPSO_GDB_ELF", fw);
    if (symok[0]) { char *v = xasprintf("0x%08x", symaddr[0]); env_default("CALYPSO_L1S_FN_ADDR", v); free(v); }
    if (symok[1]) { char *v = xasprintf("0x%08x", symaddr[1]); env_default("CALYPSO_LAST_RACH_FN_ADDR", v); free(v); }

    /* -- rundir / sockets -- */
    if (!o.rundir) o.rundir = env_nonempty("RUN_DIR") ? env_nonempty("RUN_DIR") : xasprintf("/tmp/%s", g_alias);
    if (!o.dry_run && mkdir_p(o.rundir) < 0) die("rundir %s : %s", o.rundir, strerror(errno));
    if (o.no_monitor || (o.monitor && (!strcmp(o.monitor, "none") || !strcmp(o.monitor, "off")))) o.monitor = NULL, o.no_monitor = true;
    else if (!o.monitor) o.monitor = xasprintf("%s/qemu-monitor.sock", o.rundir);
    if (!o.l1ctl) o.l1ctl = env_nonempty("L1CTL_SOCK") ? env_nonempty("L1CTL_SOCK") : DEF_L1CTL;
    env_force("L1CTL_SOCK", o.l1ctl);           /* read by calypso_soc.c (dsp fork) */
    env_default("CALYPSO_L1CTL_SOCK", o.l1ctl);
    if (!o.no_pty_link) {
        if (!o.pty_link)  o.pty_link  = xasprintf("%s/modem.pty", o.rundir);
        if (!o.irda_link) o.irda_link = xasprintf("%s/irda.pty", o.rundir);
    }

    /* -- network -- */
    char *gdb = gdb_spec(o.gdb ? o.gdb : (env_nonempty("CALYPSO_GDB_PORT") ? env_nonempty("CALYPSO_GDB_PORT") : DEF_GDB));
    if (gdb) { const char *p = strrchr(gdb, ':'); if (p) env_force("CALYPSO_GDB_PORT", p + 1); }
    char *bind_addr = NULL;
    if (QOSMO_DSP) {
        if (o.bind) { bind_addr = resolve_bind(o.bind); env_force("CALYPSO_BSP_BIND_ADDR", bind_addr); }
        else { env_default("CALYPSO_BSP_BIND_ADDR", DEF_BIND); bind_addr = xstrdup(getenv("CALYPSO_BSP_BIND_ADDR")); }
        if (o.trx_port) env_force("CALYPSO_BSP_PORT", o.trx_port);
        else env_default("CALYPSO_BSP_PORT", DEF_TRX_PORT);
        if (o.iq_tee) {
            char *d = xstrdup(o.iq_tee); char *c = strrchr(d, ':');
            if (!c) die("--iq-tee %s : attendu HOST:PORT", o.iq_tee);
            *c = 0; char *h = resolve_bind(d);
            env_force("CALYPSO_IQ_TEE_HOST", h); env_force("CALYPSO_IQ_TEE_PORT", c + 1);
            free(h); free(d);
        } else { env_default("CALYPSO_IQ_TEE_HOST", DEF_IQ_TEE_HOST); env_default("CALYPSO_IQ_TEE_PORT", DEF_IQ_TEE_PORT); }
    } else if (o.bind || o.trx_port || o.iq_tee) {
        say("avertissement : --bind/--trx-port/--iq-tee sont sans effet sur le fork gr-gsm (aucune écoute configurable dans son modèle)");
    }

    /* -- DSP ROMs -- */
    char *roms[N_ROMS] = { 0 };
    char *mach = xstrdup("calypso");
    if (QOSMO_DSP) {
        int n = 0;
        if (o.dsp_spec) {
            n = resolve_roms(o.dsp_spec, roms);
        } else {
            bool any_env = false;
            for (int i = 0; i < N_ROMS; i++) if (env_nonempty(rom_envs[i])) any_env = true;
            if (any_env) {
                for (int i = 0; i < N_ROMS; i++) {
                    const char *v = env_nonempty(rom_envs[i]);
                    if (v && is_readable_file(v)) { roms[i] = xstrdup(v); n++; }
                }
            }
            if (n == 0) {
                const char *d = env_nonempty("DSP_ROM_DIR") ? env_nonempty("DSP_ROM_DIR") : DEF_DSP_DIR;
                n = resolve_roms(d, roms);
                o.dsp_spec = d;
            } else o.dsp_spec = "$DSP_PROM0..$DSP_REGISTERS";
        }
        for (int i = 0; i < 6; i++)
            if (!roms[i]) die("ROM DSP %s introuvable (recherche : %s). Utilisez -dsp DOSSIER ou -dsp /chemin/calypso_dsp.",
                              rom_tokens[i], o.dsp_spec);
        if (!roms[6]) say("avertissement : Registers.bin absent — le DSP partira sans instantané des MMR");
        for (int i = 0; i < N_ROMS; i++) {
            if (!roms[i]) continue;
            roms[i] = abspath(roms[i]);
            char *t = xasprintf("%s,%s=%s", mach, rom_props[i], roms[i]); free(mach); mach = t;
            env_force(rom_envs[i], roms[i]);
            char *ce = xasprintf("CALYPSO_%s", rom_envs[i]); env_force(ce, roms[i]); free(ce);
        }
        {
            char *dd = xstrdup(roms[0]); env_default("DSP_ROM_DIR", dirname(dd)); free(dd);
        }
    }

    /* -- osmocon -- */
    if (!o.osmocon_bin)   o.osmocon_bin   = env_nonempty("OSMOCON") ? env_nonempty("OSMOCON") : DEF_OSMOCON;
    if (!o.osmocon_model) o.osmocon_model = env_nonempty("OSMOCON_MODEL") ? env_nonempty("OSMOCON_MODEL") : DEF_OSMOCON_MODEL;
    if (!o.osmocon_delay) o.osmocon_delay = env_nonempty("OSMOCON_INTER_BYTE_DELAY") ? env_nonempty("OSMOCON_INTER_BYTE_DELAY") : DEF_OSMOCON_DELAY;
    if (!o.osmocon_debug) o.osmocon_debug = env_nonempty("OSMOCON_DEBUG") ? env_nonempty("OSMOCON_DEBUG") : DEF_OSMOCON_DEBUG;
    if (!o.osmocon_log)   o.osmocon_log   = xasprintf("%s/osmocon.log", o.rundir);

    /* -- icount / in_asm trace (disabled by default) --
     * $QOSMO_ICOUNT=auto|shift=N,… ; $QOSMO_ASM=1 (default file) or a path.
     * "off" (option or variable) disables. */
    if (!o.icount)  o.icount  = env_nonempty("QOSMO_ICOUNT");
    if (!o.asm_log) o.asm_log = env_nonempty("QOSMO_ASM");
    if (o.icount && (!*o.icount || !strcmp(o.icount, "off") || !strcmp(o.icount, "0"))) o.icount = NULL;
    if (o.asm_log && (!strcmp(o.asm_log, "off") || !strcmp(o.asm_log, "0"))) o.asm_log = NULL;
    if (o.asm_log && (!*o.asm_log || !strcmp(o.asm_log, "1")))
        o.asm_log = xasprintf("%s/qemu-asm.log", o.rundir);

    /* -- QEMU command line -- */
    argv_t qa = { 0 };
    argv_push(&qa, o.qemu);
    bool user_M = false, user_cpu = false, user_serial = false, user_kernel = false;
    bool user_display = false, user_parallel = false, user_icount = false, user_d = false;
    for (int i = 0; i < o.extra.n; i++) {
        if (!strcmp(o.extra.v[i], "-M") || !strcmp(o.extra.v[i], "-machine")) user_M = true;
        if (!strcmp(o.extra.v[i], "-cpu")) user_cpu = true;
        if (!strcmp(o.extra.v[i], "-serial")) user_serial = true;
        if (!strcmp(o.extra.v[i], "-kernel")) user_kernel = true;
        if (!strcmp(o.extra.v[i], "-display") || !strcmp(o.extra.v[i], "-nographic")) user_display = true;
        if (!strcmp(o.extra.v[i], "-parallel")) user_parallel = true;
        if (!strcmp(o.extra.v[i], "-icount")) user_icount = true;
        if (!strcmp(o.extra.v[i], "-d") || !strcmp(o.extra.v[i], "-D")) user_d = true;
    }
    if (!user_M)   { argv_push(&qa, "-M"); argv_push(&qa, mach); }
    if (!user_cpu) { argv_push(&qa, "-cpu"); argv_push(&qa, o.cpu); }
    /* -display none: no window. -parallel none: no "parallel0" chardev at all
     * (`info chardev` would still show it as `vc` despite -display none). The
     * calypso machine has no parallel port to model: that default device only
     * exists to carry the virtual console removed above. */
    if (!user_display)  { argv_push(&qa, "-display"); argv_push(&qa, o.display); }
    if (!user_parallel && !strcmp(o.display, "none")) { argv_push(&qa, "-parallel"); argv_push(&qa, "none"); }
    if (gdb)       { argv_push(&qa, "-gdb"); argv_push(&qa, gdb); }
    if (!user_serial) { argv_push(&qa, "-serial"); argv_push(&qa, "pty"); argv_push(&qa, "-serial"); argv_push(&qa, "pty"); }
    if (!o.no_monitor) { argv_push(&qa, "-monitor"); char *m = xasprintf("unix:%s,server,nowait", o.monitor); argv_push(&qa, m); free(m); }
    if (!user_kernel) { argv_push(&qa, "-kernel"); argv_push(&qa, fw); }
    if (o.icount && !user_icount) { argv_push(&qa, "-icount"); argv_push(&qa, o.icount); }
    if (o.asm_log && !user_d) { argv_push(&qa, "-d"); argv_push(&qa, "in_asm"); argv_push(&qa, "-D"); argv_push(&qa, o.asm_log); }
    for (int i = 0; i < o.extra.n; i++) argv_push(&qa, o.extra.v[i]);

    /* -- banner -- */
    if (!o.quiet || o.dry_run) {
        say("fork      : %s  (%s)", QOSMO_TREE, QOSMO_DSP ? "DSP C54x émulé sur la mask-ROM TI" : "L1 gr-gsm, DSP non émulé");
        say("qemu      : %s", o.qemu);
        say("firmware  : %s", fw);
        say("  .bin    : %s%s", o.fw_bin, is_readable_file(o.fw_bin) ? "" : "  (ABSENT)");
        if (fw_mismatch)
            say("  ATTENTION: ce .bin n'est pas le voisin de l'ELF (%s.bin existe). Sous QEMU le stub\n"
                "[%s]             romload ignore son contenu (c'est l'ELF qui tourne, symboles corrects), mais\n"
                "[%s]             FIRMWARE_BIN et FIRMWARE_ELF divergent : passez --bin %s.bin", stem, g_alias, g_alias, stem);
        say("  .map    : %s", is_readable_file(fw_map) ? fw_map : "(absent)");
        if (symok[0] || symok[1])
            say("  symboles: l1s=%s%08x last_rach=%s%08x  (CALYPSO_L1S_FN_ADDR / CALYPSO_LAST_RACH_FN_ADDR)",
                symok[0] ? "0x" : "?", symaddr[0], symok[1] ? "0x" : "?", symaddr[1]);
        else say("  symboles: l1s / last_rach ABSENTS de l'ELF (AGCH en péril, cf. 16-fwsyms.sh)");
        if (QOSMO_DSP) {
            say("ROMs DSP  : %s", o.dsp_spec);
            for (int i = 0; i < N_ROMS; i++) if (roms[i]) say("  %-9s: %s", rom_tokens[i], roms[i]);
        }
        say("rundir    : %s", o.rundir);
        if (o.icount)  say("icount    : %s", o.icount);
        if (o.asm_log) say("in_asm    : %s", o.asm_log);
        say("moniteur  : %s", o.no_monitor ? "(aucun)" : o.monitor);
        say("affichage : %s", user_display ? "(passé à QEMU tel quel)" : o.display);
        say("gdb ARM   : %s", gdb ? gdb : "(off)");
        say("L1CTL     : %s", o.l1ctl);
        if (QOSMO_DSP)
            say("réseau    : TRXDv0 udp %s:%s (entrée bursts)  |  IQ tee -> udp %s:%s",
                getenv("CALYPSO_BSP_BIND_ADDR"), getenv("CALYPSO_BSP_PORT"),
                getenv("CALYPSO_IQ_TEE_HOST"), getenv("CALYPSO_IQ_TEE_PORT"));
        else
            say("réseau    : GSMTAP udp/%s (fixe dans calypso_l1_grgsm.c)", GRGSM_GSMTAP_PORT);
        say("pty liens : modem=%s  irda=%s", o.pty_link ? o.pty_link : "(aucun)", o.irda_link ? o.irda_link : "(aucun)");
        say("osmocon   : %s%s", o.osmocon ? "lancé automatiquement -> " : "à la main -> ",
            o.osmocon ? o.osmocon_log : "");
        say("  %s -m %s -i %s -p %s -s %s %s -d %s", o.osmocon_bin, o.osmocon_model, o.osmocon_delay,
            o.pty_link ? o.pty_link : "<pty serial0>", o.l1ctl, o.fw_bin, o.osmocon_debug);
    }

    if (o.dry_run) {
        const char *shown[] = { "FIRMWARE_ELF", "FIRMWARE_BIN", "CALYPSO_FIRMWARE_ELF", "CALYPSO_GDB_ELF",
            "CALYPSO_L1S_FN_ADDR", "CALYPSO_LAST_RACH_FN_ADDR", "L1CTL_SOCK", "CALYPSO_GDB_PORT",
            "CALYPSO_BSP_BIND_ADDR", "CALYPSO_BSP_PORT", "CALYPSO_IQ_TEE_HOST", "CALYPSO_IQ_TEE_PORT",
            "DSP_ROM_DIR", "DSP_PROM0", "DSP_PROM1", "DSP_PROM2", "DSP_PROM3", "DSP_DROM", "DSP_PDROM", "DSP_REGISTERS" };
        printf("# environnement\n");
        for (size_t i = 0; i < sizeof shown / sizeof *shown; i++)
            if (env_nonempty(shown[i])) printf("export %s='%s'\n", shown[i], getenv(shown[i]));
        printf("# commande\n");
        for (int i = 0; i < qa.n; i++) printf("%s%s", i ? " " : "", qa.v[i]);
        printf("\n");
        return 0;
    }

    /* -- launch: QEMU's stdout+stderr in a pipe, relayed and scanned -- */
    int pfd[2];
    if (pipe(pfd) < 0) die("pipe : %s", strerror(errno));
    struct sigaction sa = { 0 };
    sa.sa_handler = on_sig;           /* no SA_RESTART: read() returns EINTR */
    sigaction(SIGINT, &sa, NULL); sigaction(SIGTERM, &sa, NULL); sigaction(SIGHUP, &sa, NULL);
    struct sigaction sc = { 0 };
    sc.sa_handler = on_sig; sc.sa_flags = SA_NOCLDSTOP;
    sigaction(SIGCHLD, &sc, NULL);

    g_qemu = fork();
    if (g_qemu < 0) die("fork : %s", strerror(errno));
    if (g_qemu == 0) {
        close(pfd[0]);
        /* QEMU announces its ptys on STDOUT ("char device redirected to …")
         * and its probes on STDERR: both are captured, like the
         * `>>qemu.log 2>&1` of 40-qemu.sh. */
        dup2(pfd[1], 1);
        dup2(pfd[1], 2);
        close(pfd[1]);
        signal(SIGINT, SIG_DFL); signal(SIGTERM, SIG_DFL); signal(SIGHUP, SIG_DFL); signal(SIGCHLD, SIG_DFL);
        execv(qa.v[0], qa.v);
        fprintf(stderr, "[%s] exec %s : %s\n", g_alias, qa.v[0], strerror(errno));
        _exit(127);
    }
    close(pfd[1]);
    {
        char *p = xasprintf("%s/%s.pid", o.rundir, g_alias); write_pid(p, getpid()); free(p);
        p = xasprintf("%s/%s-qemu.pid", o.rundir, g_alias); write_pid(p, g_qemu); free(p);
    }
    if (!o.quiet) say("QEMU pid %d", (int)g_qemu);

    char buf[65536];
    char line[4096]; size_t ll = 0;
    bool modem_done = false, irda_done = false;
    int term_sent = 0;
    int qemu_status = -1;
    for (;;) {
        if (g_sig) {
            int s = g_sig; g_sig = 0;
            if (s == SIGCHLD) {
                int st; pid_t p;
                while ((p = waitpid(-1, &st, WNOHANG)) > 0) {
                    if (p == g_qemu) { qemu_status = st; g_qemu = 0; }
                    else if (p == g_osmocon) { if (!o.quiet) say("osmocon terminé (%d)", WIFEXITED(st) ? WEXITSTATUS(st) : -1); g_osmocon = 0; }
                }
            } else {
                if (g_qemu > 0) kill(g_qemu, term_sent++ ? SIGKILL : SIGTERM);
                if (g_osmocon > 0) kill(g_osmocon, SIGTERM);
            }
        }
        ssize_t n = read(pfd[0], buf, sizeof buf);
        if (n < 0) { if (errno == EINTR) continue; break; }
        if (n == 0) break;                          /* QEMU closed stderr */
        (void)!write(2, buf, n);                    /* relayed as is */
        for (ssize_t i = 0; i < n; i++) {
            char c = buf[i];
            if (c == '\n') {
                line[ll] = 0;
                /* "char device redirected to /dev/pts/N (label serial0)" */
                char *r = strstr(line, "redirected to ");
                if (r) {
                    char *dev = r + strlen("redirected to ");
                    char *sp = strchr(dev, ' ');
                    if (sp) *sp = 0;
                    const char *lab = sp ? strstr(sp + 1, "label ") : NULL;
                    if (lab && !strncmp(lab + 6, "serial0", 7) && !modem_done) {
                        modem_done = true;
                        if (o.pty_link) make_link(dev, o.pty_link);
                        {
                            char *p = xasprintf("%s/modem.pty.path", o.rundir);
                            FILE *f = fopen(p, "w"); if (f) { fprintf(f, "%s\n", dev); fclose(f); } free(p);
                        }
                        if (!o.quiet) say("pty modem : %s%s%s", dev, o.pty_link ? "  ->  " : "", o.pty_link ? o.pty_link : "");
                        if (o.osmocon) {
                            g_osmocon = spawn_osmocon(&o, o.pty_link ? o.pty_link : dev);
                            if (g_osmocon > 0) {
                                char *p = xasprintf("%s/%s-osmocon.pid", o.rundir, g_alias); write_pid(p, g_osmocon); free(p);
                                if (!o.quiet) say("osmocon pid %d  (journal %s)", (int)g_osmocon, o.osmocon_log);
                            }
                        } else if (!o.quiet) {
                            say("osmocon   : %s -m %s -i %s -p %s -s %s %s -d %s", o.osmocon_bin, o.osmocon_model,
                                o.osmocon_delay, o.pty_link ? o.pty_link : dev, o.l1ctl, o.fw_bin, o.osmocon_debug);
                        }
                    } else if (lab && !strncmp(lab + 6, "serial1", 7) && !irda_done) {
                        irda_done = true;
                        if (o.irda_link) make_link(dev, o.irda_link);
                        if (!o.quiet) say("pty irda  : %s%s%s", dev, o.irda_link ? "  ->  " : "", o.irda_link ? o.irda_link : "");
                    }
                }
                ll = 0;
            } else if (ll + 1 < sizeof line) {
                line[ll++] = c;
            }
        }
    }
    close(pfd[0]);
    if (g_qemu > 0) {
        int st;
        while (waitpid(g_qemu, &st, 0) < 0 && errno == EINTR) {}
        qemu_status = st; g_qemu = 0;
    }
    if (g_osmocon > 0) { kill(g_osmocon, SIGTERM); waitpid(g_osmocon, NULL, 0); g_osmocon = 0; }

    /* cleanup: links and pid; the L1CTL socket is left (osmocon/mobile manage it) */
    if (o.pty_link) unlink(o.pty_link);
    if (o.irda_link) unlink(o.irda_link);
    { char *p = xasprintf("%s/%s.pid", o.rundir, g_alias); unlink(p); free(p);
      p = xasprintf("%s/%s-qemu.pid", o.rundir, g_alias); unlink(p); free(p);
      p = xasprintf("%s/%s-osmocon.pid", o.rundir, g_alias); unlink(p); free(p);
      p = xasprintf("%s/modem.pty.path", o.rundir); unlink(p); free(p); }

    int rc = 1;
    if (qemu_status >= 0) {
        if (WIFEXITED(qemu_status)) rc = WEXITSTATUS(qemu_status);
        else if (WIFSIGNALED(qemu_status)) rc = 128 + WTERMSIG(qemu_status);
    }
    if (!o.quiet) say("QEMU terminé (code %d)", rc);
    return rc;
}

4.112 https://github.com/bbaranoff/qosmo/blob/main/tools/qosmo-tbring/tbring.c

4986 octets, 117 lignes → 117 lignes

/*
 * tbring.c - TCG plugin: ring of the executed ARM blocks, in shared memory.
 *
 * Why. "-d in_asm,exec,nochain" writes one text line per executed block,
 * under a lock, with one fflush() per line (util/log.c) and one
 * lookup_symbol() per block: on the Calypso bench that is 2.8 GB in 3 min,
 * the ARM 5 times slower and the DSP bridge losing sync. Here nothing is
 * formatted in the loop: every execution of a block is an 8-byte write (PC,
 * counter) into a ring mmapped on /dev/shm. The plugin is called even for
 * chained blocks, so no "nochain": the TCG runs as in production. The text is
 * produced afterwards (tbring-dump.py), with -d in_asm (one line per block,
 * at translation) as the lookup table.
 *
 * Loading:  -plugin libtbring.so[,anneau=/dev/shm/qemu-tbring][,entries=N]
 *   anneau  : the file (NOT "file=": that is QEMU's keyword for the plugin
 *             path, it would load the ring as a second plugin).
 *   entries : power of 2 (default 1 Mi = 8 MiB), the last N blocks.
 *
 * File:  64-byte header  { magic "TBRING01", entries, count, shift }
 *        then entries x { uint32 pc ; uint32 tour }  (tour = count >> shift at
 *        write time: the reader knows whether the entry belongs to the current
 *        window or to a previous lap; valid up to 2^32 laps).
 * On disk (ASSEMBLY_LOGS_RING_FILE, option anneau=) the file is sparse: 64 GiB
 * (2^33 entries) only cost what has been written, 8 bytes per block, i.e.
 * ~1.4 MB/s at 180 000 blocks/s -- against 150 MB/s for the text of -d exec.
 * The global counter advances on every block: tbring-dump reads the file live,
 * at any time (no SIGUSR1 needed, it is shared memory).
 */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <inttypes.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <glib.h>
#include <qemu-plugin.h>

QEMU_PLUGIN_EXPORT int qemu_plugin_version = QEMU_PLUGIN_VERSION;

typedef struct {
    char     magic[8];      /* "TBRING01" */
    uint64_t entries;       /* ring size, power of 2 */
    uint64_t count;         /* blocks executed since start (write index) */
    uint64_t shift;         /* log2(entries) */
    uint64_t reserve[4];
} TbRingHdr;                /* 64 bytes */

typedef struct {
    uint32_t pc;
    uint32_t tour;
} TbRingEnt;

static TbRingHdr *g_hdr;
static TbRingEnt *g_ring;
static uint64_t   g_mask;
static unsigned   g_shift;

static void tb_exec(unsigned int vcpu_index, void *udata)
{
    uint64_t i = __atomic_fetch_add(&g_hdr->count, 1, __ATOMIC_RELAXED);
    TbRingEnt *e = &g_ring[i & g_mask];
    e->pc   = (uint32_t)(uintptr_t)udata;
    e->tour = (uint32_t)(i >> g_shift);
}

static void tb_trans(qemu_plugin_id_t id, struct qemu_plugin_tb *tb)
{
    uint64_t pc = qemu_plugin_tb_vaddr(tb);
    qemu_plugin_register_vcpu_tb_exec_cb(tb, tb_exec, QEMU_PLUGIN_CB_NO_REGS,
                                         (void *)(uintptr_t)pc);
}

static void at_exit(qemu_plugin_id_t id, void *p)
{
    g_autoptr(GString) s = g_string_new(NULL);
    g_string_printf(s, "tbring: %" PRIu64 " blocs executes, anneau de %" PRIu64 "\n",
                    g_hdr->count, g_hdr->entries);
    qemu_plugin_outs(s->str);
}

QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
                                           const qemu_info_t *info,
                                           int argc, char **argv)
{
    const char *file = "/dev/shm/qemu-tbring";
    uint64_t entries = 1u << 20;
    for (int i = 0; i < argc; i++) {
        g_auto(GStrv) tok = g_strsplit(argv[i], "=", 2);
        if (!tok[0] || !tok[1]) { fprintf(stderr, "tbring: option invalide %s\n", argv[i]); return -1; }
        if (!strcmp(tok[0], "anneau") || !strcmp(tok[0], "ring")) file = g_strdup(tok[1]);
        else if (!strcmp(tok[0], "entries")) entries = g_ascii_strtoull(tok[1], NULL, 0);
        else { fprintf(stderr, "tbring: option inconnue %s\n", tok[0]); return -1; }
    }
    if (entries < 1024 || (entries & (entries - 1))) {
        fprintf(stderr, "tbring: entries doit etre une puissance de 2 >= 1024\n"); return -1;
    }
    size_t sz = sizeof(TbRingHdr) + entries * sizeof(TbRingEnt);
    int fd = open(file, O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd < 0 || ftruncate(fd, sz) < 0) { perror("tbring: open/ftruncate"); return -1; }
    void *m = mmap(NULL, sz, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);
    if (m == MAP_FAILED) { perror("tbring: mmap"); return -1; }
    g_hdr = m;
    memcpy(g_hdr->magic, "TBRING01", 8);
    g_hdr->entries = entries;
    g_hdr->count = 0;
    g_ring = (TbRingEnt *)((char *)m + sizeof(TbRingHdr));
    g_mask = entries - 1;
    g_shift = __builtin_ctzll(entries);
    g_hdr->shift = g_shift;
    qemu_plugin_register_vcpu_tb_trans_cb(id, tb_trans);
    qemu_plugin_register_atexit_cb(id, at_exit, NULL);
    fprintf(stderr, "tbring: anneau de %" PRIu64 " blocs dans %s (%zu octets)\n", entries, file, sz);
    return 0;
}

4.113 https://github.com/bbaranoff/qosmo/blob/main/util/log.c

17147 octets, 601 lignes → 601 lignes

/*
 * Logging support
 *
 *  Copyright (c) 2003 Fabrice Bellard
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see <http://www.gnu.org/licenses/>.
 */

#include "qemu/osdep.h"
#include "qemu/log.h"
#include "qemu/range.h"
#include "qemu/error-report.h"
#include "qapi/error.h"
#include "qemu/cutils.h"
#include "trace/control.h"
#include "qemu/thread.h"
#include "qemu/lockable.h"
#include "qemu/rcu.h"
#ifdef CONFIG_LINUX
#include <sys/syscall.h>
#endif


typedef struct RCUCloseFILE {
    struct rcu_head rcu;
    FILE *fd;
} RCUCloseFILE;

/* Mutex covering the other global_* variables. */
static QemuMutex global_mutex;
static char *global_filename;
static FILE *global_file;
static __thread FILE *thread_file;
static __thread Notifier qemu_log_thread_cleanup_notifier;

int qemu_loglevel;
static bool log_per_thread;
static GArray *debug_regions;

/* Returns true if qemu_log() will really write somewhere. */
bool qemu_log_enabled(void)
{
    return log_per_thread || qatomic_read(&global_file) != NULL;
}

/* Returns true if qemu_log() will write somewhere other than stderr. */
bool qemu_log_separate(void)
{
    if (log_per_thread) {
        return true;
    } else {
        FILE *logfile = qatomic_read(&global_file);
        return logfile && logfile != stderr;
    }
}

static int log_thread_id(void)
{
#ifdef CONFIG_GETTID
    return gettid();
#elif defined(SYS_gettid)
    return syscall(SYS_gettid);
#else
    static int counter;
    return qatomic_fetch_inc(&counter);
#endif
}

static void qemu_log_thread_cleanup(Notifier *n, void *unused)
{
    if (thread_file != stderr) {
        fclose(thread_file);
        thread_file = NULL;
    }
}

/* Lock/unlock output. */

static FILE *qemu_log_trylock_with_err(Error **errp)
{
    FILE *logfile;

    logfile = thread_file;
    if (!logfile) {
        if (log_per_thread) {
            g_autofree char *filename
                = g_strdup_printf(global_filename, log_thread_id());
            logfile = fopen(filename, "w");
            if (!logfile) {
                error_setg_errno(errp, errno,
                                 "Error opening logfile %s for thread %d",
                                 filename, log_thread_id());
                return NULL;
            }
            thread_file = logfile;
            qemu_log_thread_cleanup_notifier.notify = qemu_log_thread_cleanup;
            qemu_thread_atexit_add(&qemu_log_thread_cleanup_notifier);
        } else {
            rcu_read_lock();
            /*
             * FIXME: typeof_strip_qual, as used by qatomic_rcu_read,
             * does not work with pointers to undefined structures,
             * such as we have with struct _IO_FILE and musl libc.
             * Since all we want is a read of a pointer, cast to void**,
             * which does work with typeof_strip_qual.
             */
            logfile = qatomic_rcu_read((void **)&global_file);
            if (!logfile) {
                rcu_read_unlock();
                return NULL;
            }
        }
    }

    qemu_flockfile(logfile);
    return logfile;
}

FILE *qemu_log_trylock(void)
{
    return qemu_log_trylock_with_err(NULL);
}

/* QEMU_LOG_BUFFERED=<bytes>: the -D file is fully buffered (setvbuf) and
 * qemu_log_unlock() does not fflush() per line. With -d exec every executed
 * block otherwise costs one write(2). Content reaches the disk in chunks, so
 * the last lines may be missing after a crash; unset keeps the default. */
static size_t qemu_log_buffered(void)
{
    static ssize_t sz = -1;
    if (sz < 0) {
        const char *e = getenv("QEMU_LOG_BUFFERED");
        sz = (e && *e) ? (ssize_t)strtoll(e, NULL, 0) : 0;
        if (sz < 0) {
            sz = 0;
        }
    }
    return (size_t)sz;
}

static void qemu_log_setvbuf(FILE *f)
{
    size_t sz = qemu_log_buffered();
    if (f && sz >= 4096) {
        setvbuf(f, NULL, _IOFBF, sz);
    }
}

/* Explicit flush of the QEMU_LOG_BUFFERED buffer: called by the calypso
 * machine on every VM stop (gdb Ctrl-C, monitor stop) so the last lines are
 * in the file when they are read. */
void qemu_log_flush(void)
{
    FILE *f = qemu_log_trylock();
    if (f) {
        fflush(f);
        qemu_funlockfile(f);
        if (!log_per_thread) {
            rcu_read_unlock();
        }
    }
}

void qemu_log_unlock(FILE *logfile)
{
    if (logfile) {
        if (!qemu_log_buffered()) {
            fflush(logfile);
        }
        qemu_funlockfile(logfile);
        if (!log_per_thread) {
            rcu_read_unlock();
        }
    }
}

void qemu_log(const char *fmt, ...)
{
    FILE *f = qemu_log_trylock();
    if (f) {
        va_list ap;

        va_start(ap, fmt);
        vfprintf(f, fmt, ap);
        va_end(ap);
        qemu_log_unlock(f);
    }
}

static void __attribute__((__constructor__)) startup(void)
{
    qemu_mutex_init(&global_mutex);
}

static void rcu_close_file(RCUCloseFILE *r)
{
    fclose(r->fd);
    g_free(r);
}

/**
 * valid_filename_template:
 *
 * Validate the filename template.  Require %d if per_thread, allow it
 * otherwise; require no other % within the template.
 */

typedef enum {
    vft_error,
    vft_stderr,
    vft_strdup,
    vft_pid_printf,
} ValidFilenameTemplateResult;

static ValidFilenameTemplateResult
valid_filename_template(const char *filename, bool per_thread, Error **errp)
{
    if (filename) {
        char *pidstr = strstr(filename, "%");

        if (pidstr) {
            /* We only accept one %d, no other format strings */
            if (pidstr[1] != 'd' || strchr(pidstr + 2, '%')) {
                error_setg(errp, "Bad logfile template: %s", filename);
                return 0;
            }
            return per_thread ? vft_strdup : vft_pid_printf;
        }
    }
    if (per_thread) {
        error_setg(errp, "Filename template with '%%d' required for 'tid'");
        return vft_error;
    }
    return filename ? vft_strdup : vft_stderr;
}

/* enable or disable low levels log */
static bool qemu_set_log_internal(const char *filename, bool changed_name,
                                  int log_flags, Error **errp)
{
    bool need_to_open_file;
    bool daemonized;
    bool per_thread;
    FILE *logfile;

    QEMU_LOCK_GUARD(&global_mutex);
    logfile = global_file;

    /* The per-thread flag is immutable. */
    if (log_per_thread) {
        log_flags |= LOG_PER_THREAD;
    } else {
        if (global_filename) {
            log_flags &= ~LOG_PER_THREAD;
        }
    }

    per_thread = log_flags & LOG_PER_THREAD;

    if (changed_name) {
        char *newname = NULL;

        /*
         * Once threads start opening their own log files, we have no
         * easy mechanism to tell them all to close and re-open.
         * There seems little cause to do so either -- this option
         * will most often be used at user-only startup.
         */
        if (log_per_thread) {
            error_setg(errp, "Cannot change log filename after setting 'tid'");
            return false;
        }

        switch (valid_filename_template(filename, per_thread, errp)) {
        case vft_error:
            return false;
        case vft_stderr:
            break;
        case vft_strdup:
            newname = g_strdup(filename);
            break;
        case vft_pid_printf:
            newname = g_strdup_printf(filename, getpid());
            break;
        }

        g_free(global_filename);
        global_filename = newname;
        filename = newname;
    } else {
        filename = global_filename;
        if (per_thread &&
            valid_filename_template(filename, true, errp) == vft_error) {
            return false;
        }
    }

    /* Once the per-thread flag is set, it cannot be unset. */
    if (per_thread) {
        log_per_thread = true;
    }
    /* The flag itself is not relevant for need_to_open_file. */
    log_flags &= ~LOG_PER_THREAD;
#ifdef CONFIG_TRACE_LOG
    log_flags |= LOG_TRACE;
#endif
    qemu_loglevel = log_flags;

    daemonized = is_daemonized();
    need_to_open_file = false;
    if (!daemonized) {
        /*
         * If not daemonized we only log if qemu_loglevel is set, either to
         * stderr or to a file (if there is a filename).
         * If per-thread, open the file for each thread in qemu_log_trylock().
         */
        need_to_open_file = qemu_loglevel && !log_per_thread;
    } else {
        /*
         * If we are daemonized, we will only log if there is a filename.
         */
        need_to_open_file = filename != NULL;
    }

    if (logfile) {
        fflush(logfile);
        if (changed_name && logfile != stderr) {
            RCUCloseFILE *r = g_new0(RCUCloseFILE, 1);
            r->fd = logfile;
            qatomic_rcu_set(&global_file, NULL);
            call_rcu(r, rcu_close_file, rcu);
        }
        if (changed_name) {
            logfile = NULL;
        }
    }

    if (log_per_thread && daemonized) {
        logfile = thread_file;
    }

    if (!logfile && need_to_open_file) {
        if (filename) {
            if (log_per_thread) {
                logfile = qemu_log_trylock_with_err(errp);
                if (!logfile) {
                    return false;
                }
                qemu_log_unlock(logfile);
            } else {
                logfile = fopen(filename, "w");
                if (!logfile) {
                    error_setg_errno(errp, errno, "Error opening logfile %s",
                                     filename);
                    return false;
                }
                qemu_log_setvbuf(logfile);
            }
            /* In case we are a daemon redirect stderr to logfile */
            if (daemonized) {
                dup2(fileno(logfile), STDERR_FILENO);
                fclose(logfile);
                /*
                 * This will skip closing logfile in rcu_close_file()
                 * or qemu_log_thread_cleanup().
                 */
                logfile = stderr;
            }
        } else {
            /* Default to stderr if no log file specified */
            assert(!daemonized);
            logfile = stderr;
        }

        if (log_per_thread && daemonized) {
            thread_file = logfile;
        } else {
            qatomic_rcu_set(&global_file, logfile);
        }
    }
    return true;
}

bool qemu_set_log(int log_flags, Error **errp)
{
    return qemu_set_log_internal(NULL, false, log_flags, errp);
}

bool qemu_set_log_filename(const char *filename, Error **errp)
{
    return qemu_set_log_internal(filename, true, qemu_loglevel, errp);
}

bool qemu_set_log_filename_flags(const char *name, int flags, Error **errp)
{
    return qemu_set_log_internal(name, true, flags, errp);
}

/* Returns true if addr is in our debug filter or no filter defined
 */
bool qemu_log_in_addr_range(uint64_t addr)
{
    if (debug_regions) {
        int i = 0;
        for (i = 0; i < debug_regions->len; i++) {
            Range *range = &g_array_index(debug_regions, Range, i);
            if (range_contains(range, addr)) {
                return true;
            }
        }
        return false;
    } else {
        return true;
    }
}


void qemu_set_dfilter_ranges(const char *filter_spec, Error **errp)
{
    gchar **ranges = g_strsplit(filter_spec, ",", 0);
    int i;

    if (debug_regions) {
        g_array_unref(debug_regions);
        debug_regions = NULL;
    }

    debug_regions = g_array_sized_new(FALSE, FALSE,
                                      sizeof(Range), g_strv_length(ranges));
    for (i = 0; ranges[i]; i++) {
        const char *r = ranges[i];
        const char *range_op, *r2, *e;
        uint64_t r1val, r2val, lob, upb;
        struct Range range;

        range_op = strstr(r, "-");
        r2 = range_op ? range_op + 1 : NULL;
        if (!range_op) {
            range_op = strstr(r, "+");
            r2 = range_op ? range_op + 1 : NULL;
        }
        if (!range_op) {
            range_op = strstr(r, "..");
            r2 = range_op ? range_op + 2 : NULL;
        }
        if (!range_op) {
            error_setg(errp, "Bad range specifier");
            goto out;
        }

        if (qemu_strtou64(r, &e, 0, &r1val)
            || e != range_op) {
            error_setg(errp, "Invalid number to the left of %.*s",
                       (int)(r2 - range_op), range_op);
            goto out;
        }
        if (qemu_strtou64(r2, NULL, 0, &r2val)) {
            error_setg(errp, "Invalid number to the right of %.*s",
                       (int)(r2 - range_op), range_op);
            goto out;
        }

        switch (*range_op) {
        case '+':
            lob = r1val;
            upb = r1val + r2val - 1;
            break;
        case '-':
            upb = r1val;
            lob = r1val - (r2val - 1);
            break;
        case '.':
            lob = r1val;
            upb = r2val;
            break;
        default:
            g_assert_not_reached();
        }
        if (lob > upb) {
            error_setg(errp, "Invalid range");
            goto out;
        }
        range_set_bounds(&range, lob, upb);
        g_array_append_val(debug_regions, range);
    }
out:
    g_strfreev(ranges);
}

const QEMULogItem qemu_log_items[] = {
    { CPU_LOG_TB_OUT_ASM, "out_asm",
      "show generated host assembly code for each compiled TB" },
    { CPU_LOG_TB_IN_ASM, "in_asm",
      "show target assembly code for each compiled TB" },
    { CPU_LOG_TB_OP, "op",
      "show micro ops for each compiled TB" },
    { CPU_LOG_TB_OP_OPT, "op_opt",
      "show micro ops after optimization" },
    { CPU_LOG_TB_OP_IND, "op_ind",
      "show micro ops before indirect lowering" },
#ifdef CONFIG_PLUGIN
    { LOG_TB_OP_PLUGIN, "op_plugin",
      "show micro ops before plugin injection" },
#endif
    { CPU_LOG_INT, "int",
      "show interrupts/exceptions in short format" },
    { CPU_LOG_EXEC, "exec",
      "show trace before each executed TB (lots of logs)" },
    { CPU_LOG_TB_CPU, "cpu",
      "show CPU registers before entering a TB (lots of logs)" },
    { CPU_LOG_TB_FPU, "fpu",
      "include FPU registers in the 'cpu' logging" },
    { CPU_LOG_MMU, "mmu",
      "log MMU-related activities" },
    { CPU_LOG_PCALL, "pcall",
      "x86 only: show protected mode far calls/returns/exceptions" },
    { CPU_LOG_RESET, "cpu_reset",
      "show CPU state before CPU resets" },
    { LOG_UNIMP, "unimp",
      "log unimplemented functionality" },
    { LOG_GUEST_ERROR, "guest_errors",
      "log when the guest OS does something invalid (eg accessing a\n"
      "non-existent register)" },
    { CPU_LOG_PAGE, "page",
      "dump pages at beginning of user mode emulation" },
    { CPU_LOG_TB_NOCHAIN, "nochain",
      "do not chain compiled TBs so that \"exec\" and \"cpu\" show\n"
      "complete traces" },
#ifdef CONFIG_PLUGIN
    { CPU_LOG_PLUGIN, "plugin", "output from TCG plugins"},
#endif
    { LOG_STRACE, "strace",
      "log every user-mode syscall, its input, and its result" },
    { LOG_PER_THREAD, "tid",
      "open a separate log file per thread; filename must contain '%d'" },
    { CPU_LOG_TB_VPU, "vpu",
      "include VPU registers in the 'cpu' logging" },
    { 0, NULL, NULL },
};

/* takes a comma separated list of log masks. Return 0 if error. */
int qemu_str_to_log_mask(const char *str)
{
    const QEMULogItem *item;
    int mask = 0;
    char **parts = g_strsplit(str, ",", 0);
    char **tmp;

    for (tmp = parts; tmp && *tmp; tmp++) {
        if (g_str_equal(*tmp, "all")) {
            for (item = qemu_log_items; item->mask != 0; item++) {
                mask |= item->mask;
            }
#ifdef CONFIG_TRACE_LOG
        } else if (g_str_has_prefix(*tmp, "trace:") && (*tmp)[6] != '\0') {
            trace_enable_events((*tmp) + 6);
            mask |= LOG_TRACE;
#endif
        } else {
            for (item = qemu_log_items; item->mask != 0; item++) {
                if (g_str_equal(*tmp, item->name)) {
                    goto found;
                }
            }
            goto error;
        found:
            mask |= item->mask;
        }
    }

    g_strfreev(parts);
    return mask;

 error:
    g_strfreev(parts);
    return 0;
}

void qemu_print_log_usage(FILE *f)
{
    const QEMULogItem *item;
    fprintf(f, "Log items (comma separated):\n");
    for (item = qemu_log_items; item->mask != 0; item++) {
        fprintf(f, "%-15s %s\n", item->name, item->help);
    }
#ifdef CONFIG_TRACE_LOG
    fprintf(f, "trace:PATTERN   enable trace events\n");
    fprintf(f, "\nUse \"-d trace:help\" to get a list of trace events.\n\n");
#endif
}