Total sources files : 42
================================================================================
FILE: diag/source_8xxx_handlers.c SIZE: 15685 bytes, 346 lines
================================================================================
/* SACCD src, Xmem, cond — Conditional accumulator store * Encoding:
1001 11SD XXXX COND per SPRU172C p.4-152 / if ((op & 0xFC00) ==
0x9C00) { int src_s = (op >> 9) & 1; int64_t acc = src_s ?
s->b : s->a; int xar_s = (op >> 4) & 0x07; uint16_t
xaddr = s->ar[xar_s]; int cond = op & 0x0F; / Evaluate
condition / int take = 0; switch (cond) { case 0x0: take = (acc ==
0); break; / EQ / case 0x1: take = (acc != 0); break; /
NEQ / case 0x2: take = (acc > 0); break; / GT / case
0x3: take = (acc < 0); break; / LT / case 0x4: take = (acc
>= 0); break; / GEQ / case 0x5: take = (acc == 0); break;
/ AEQ / case 0x6: take = (acc > 0); break; / AGT /
case 0x7: take = (acc <= 0); break; / LEQ/ALEQ / default:
take = 0; break; } int asm_val = asm_shift(s); if (take) { / Store
shifted accumulator high part / int64_t shifted = acc <<
(asm_val > 0 ? asm_val : 0); if (asm_val < 0) shifted = acc
>> (-asm_val); uint16_t val = (uint16_t)((shifted >> 16)
& 0xFFFF); data_write(s, xaddr, val); } else { / Read and write
back (no change) / uint16_t val = data_read(s, xaddr); data_write(s,
xaddr, val); } / Xmem post-modify / if ((op >> 7) &
1) s->ar[xar_s]–; else s->ar[xar_s]++; 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 : value at SP popped to MMR, SP++. Bug fix
2026-05-08 : 0x8Axx était précédemment mal décodé en * MVDK Smem,dmad
(qui est en réalité 0x7100 mask 0xFF00). Le * pattern PSHM/POPM
symétrique du firmware (e.g. PROM0 0x7013-0x7023 * sauve/restaure 6 MMRs
autour d’un CALA) ne fonctionnait jamais * post-CALA → ST1 jamais
restauré → INTM=1 dwell perpétuel * → IRQ vectoring bloqué → DSP wait
stuck → L1 mort. * Le case MVDK ci-dessous devient dead code mais est
laissé pour * référence historique. / if ((op & 0xFF00) ==
0x8A00) { uint16_t mmr = op & 0x7F; uint16_t val = data_read(s,
s->sp); s->sp = (s->sp + 1) & 0xFFFF; data_write(s, mmr,
val); return consumed + s->lk_used; } / OBSOLETE — superseded by
POPM above. The 0x8Axx range belongs to * POPM per tic54x-opc.c, not
MVDK (which is 0x7100 mask 0xFF00). * Kept commented for one revision so
any caller depending on the * old (incorrect) behaviour is forced to be
re-examined. / if (0 && hi8 == 0x8A) { / MVDK Smem,
dmad — INCORRECT for 0x8Axx, see POPM above / 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;
} / 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): * if decoded as 2-word MVDM the emulator
eats the next opcode * (0xb42e = 0xf84c, a BC), then jumps into 0xb431
(MACR family) * with an uninitialised T register, producing A=0x10 —
which * the immediately-following BACC A at 0xb430 then uses as the *
jump target, dropping 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 = op & 0x7F; 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; } if (hi8 == 0x80) { / STUB-NOP : tic54x
dit 0x80 = STL src,Smem (1-word). * Ancienne classification qemu = MVDD
2-word (incorrect). * Voir doc/opcodes/tic54x_hi8_map.md. Neutralisé
pour éviter * les écritures mémoire fantômes en attendant impl correcte.
/ return 1; } if (hi8 == 0x8C) { / MVPD pmad, Smem (prog→data)
/ addr = resolve_smem(s, op, &ind); op2 = prog_fetch(s, s->pc
+ 1); consumed = 2; uint16_t mvpd_val = prog_read(s, op2); data_write(s,
addr, mvpd_val); { static unsigned mvpd_log = 0; static unsigned
mvpd_total; static uint16_t src_min = 0xFFFF, src_max; static uint16_t
dst_min = 0xFFFF, dst_max; static unsigned hits_a040; mvpd_total++; if
(op2 < src_min) src_min = op2; if (op2 > src_max) src_max = op2;
if (addr < dst_min) dst_min = addr; if (addr > dst_max) dst_max =
addr; if (addr >= 0xa040 && addr <= 0xa080) hits_a040++;
if (mvpd_log++ < 500 || (addr >= 0xa040 && addr <=
0xa080) || (mvpd_total % 1000) == 0) C54_LOG(“MVPD#%u:
prog[0x%04x]=0x%04x → data[0x%04x] PC=0x%04x insn=%u%s”, mvpd_total,
op2, mvpd_val, addr, s->pc, s->insn_count, (addr >= 0xa040
&& addr <= 0xa080) ? ” A040” : ““); if ((mvpd_total
% 500) == 0) C54_LOG(”MVPD-SUMMARY total=%u src=[0x%04x..0x%04x]
dst=[0x%04x..0x%04x] hits_a040=%u”, mvpd_total, src_min, src_max,
dst_min, dst_max, hits_a040); } return consumed + s->lk_used; } if
(hi8 == 0x8E) { / MVDP Smem, pmad (data→prog) / addr =
resolve_smem(s, op, &ind); op2 = prog_fetch(s, s->pc + 1);
consumed = 2; prog_write(s, op2, data_read(s, addr)); return consumed +
s->lk_used; } if (hi8 == 0x8F) { / PORTR PA, Smem — read I/O
port / addr = resolve_smem(s, op, &ind); op2 = prog_fetch(s,
s->pc + 1); consumed = 2; / BSP RX data register — return next
burst sample. * The DSP firmware uses PORTR PA=0xF430 (64 sites in
PROM0, * verified from ROM dump). We also accept 0x0034 for legacy *
compatibility with earlier QEMU experiments. / uint16_t portr_val;
bool is_bsp_pa = (op2 == 0xF430 || op2 == 0x0034); if (is_bsp_pa
&& s->bsp_pos < s->bsp_len) { portr_val =
s->bsp_buf[s->bsp_pos++]; data_write(s, addr, portr_val); } else {
portr_val = 0; data_write(s, addr, 0); } / Per-PA counters so we
can see which I/O ports the DSP polls * and how often. */ { static
uint64_t portr_total[16]; static uint64_t portr_since_summary; int
pa_bucket = (op2 >> 4) & 0xF; portr_total[pa_bucket]++;
portr_since_summary++;
static int portr_log = 0;
if (portr_log < 50) {
C54_LOG("PORTR PA=0x%04x → [0x%04x] val=0x%04x "
"bsp_pos=%u/%u PC=0x%04x",
op2, addr, portr_val,
(unsigned)s->bsp_pos, (unsigned)s->bsp_len,
s->pc);
portr_log++;
}
if ((portr_since_summary % 10000) == 0) {
C54_LOG("PORTR summary (last 10000): "
"PA0x=%llu 1x=%llu 2x=%llu 3x=%llu 4x=%llu "
"5x=%llu 6x=%llu 7x=%llu",
(unsigned long long)portr_total[0],
(unsigned long long)portr_total[1],
(unsigned long long)portr_total[2],
(unsigned long long)portr_total[3],
(unsigned long long)portr_total[4],
(unsigned long long)portr_total[5],
(unsigned long long)portr_total[6],
(unsigned long long)portr_total[7]);
}
}
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;
}
/* 86xx: MVDM dmad, MMR */
if (hi8 == 0x86) {
op2 = prog_fetch(s, s->pc + 1);
consumed = 2;
uint16_t mmr = op & 0x7F;
data_write(s, mmr, data_read(s, op2));
return consumed + s->lk_used;
}
/* 87xx: MVMD MMR, dmad */
if (hi8 == 0x87) {
op2 = prog_fetch(s, s->pc + 1);
consumed = 2;
uint16_t mmr = op & 0x7F;
data_write(s, op2, data_read(s, mmr));
return consumed + s->lk_used;
}
/* 81xx: STL src, ASM, Smem (store with shift) */
if (hi8 == 0x81) {
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;
}
/* 82xx: STH src, ASM, Smem */
if (hi8 == 0x82) {
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 >> 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;
}
/* 8Bxx: MVDK with long address */
if (hi8 == 0x8B) {
/* STUB-NOP : tic54x dit 0x8B = POPD Smem (1-word).
* Ancienne classification qemu = MVDK long-addr 2-word (incorrect).
* Voir doc/opcodes/tic54x_hi8_map.md. Neutralisé. */
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;
}
/* 83xx: WRITA Smem (write A to prog), 84xx: READA Smem */
if (hi8 == 0x83) {
addr = resolve_smem(s, op, &ind);
prog_write(s, (uint16_t)(s->a & 0xFFFF), data_read(s, addr));
return consumed + s->lk_used;
}
if (hi8 == 0x84) {
addr = resolve_smem(s, op, &ind);
data_write(s, addr, prog_read(s, (uint16_t)(s->a & 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) {
int xar_d = (op >> 4) & 0x07;
int yar_d = op & 0x07;
uint16_t xval_d = data_read(s, s->ar[xar_d]);
uint16_t yval_d = data_read(s, s->ar[yar_d]);
/* Post-modify */
if ((op >> 7) & 1) s->ar[xar_d]--; else s->ar[xar_d]++;
if ((op & 0x08) == 0) s->ar[yar_d]++; else s->ar[yar_d]--;
/* Multiply T * Xmem */
int64_t prod = (int64_t)(int16_t)s->t * (int64_t)(int16_t)xval_d;
if (s->st1 & ST1_FRCT) prod <<= 1;
/* Round if R bit set (odd hi8) */
if (hi8 & 0x01) prod += 0x8000;
/* Determine dest and operation */
int is_sub = (hi8 >= 0xB4 && hi8 <= 0xB7);
int dst_b;
if (hi8 >= 0xA4 && hi8 <= 0xA7) dst_b = (hi8 >= 0xA6);
else if (hi8 >= 0xB4 && hi8 <= 0xB7) dst_b = (hi8 >= 0xB6);
else dst_b = (hi8 & 0x02) ? 1 : 0; /* 0xB0/B1→A, 0xB2/B3→B */
if (dst_b) {
if (is_sub) s->b = sext40(s->b - prod);
else s->b = sext40(s->b + prod);
} else {
if (is_sub) s->a = sext40(s->a - prod);
else s->a = sext40(s->a + prod);
}
/* T = Ymem */
s->t = yval_d;
return consumed + s->lk_used;
}
================================================================================
FILE: diag/source_F2_to_F7_handlers.c SIZE: 22812 bytes, 462 lines
================================================================================
if ((hi8 == 0xF2 || hi8 == 0xF3) && op != 0xF272 && op
!= 0xF273 && op != 0xF274) { int xar_l = (op >> 4) &
0x07; int yar_l = op & 0x07; uint16_t xval_l = data_read(s,
s->ar[xar_l]); uint16_t yval_l = data_read(s, s->ar[yar_l]); /*
MAC: dst += T * Xmem / int64_t prod_l = (int64_t)(int16_t)s->t
(int64_t)(int16_t)xval_l; if (s->st1 & ST1_FRCT) prod_l
<<= 1; int dst_l = hi8 & 1; if (dst_l) s->b =
sext40(s->b + prod_l); else s->a = sext40(s->a + prod_l); /*
LMS coefficient update: Ymem += rnd(AH * T) / int16_t ah_l =
(int16_t)((s->a >> 16) & 0xFFFF); int32_t update =
(int32_t)ah_l (int32_t)(int16_t)s->t; if (s->st1 &
ST1_FRCT) update <<= 1; update += 0x8000; /* round / int16_t
new_ym = (int16_t)yval_l + (int16_t)(update >> 16); data_write(s,
s->ar[yar_l], (uint16_t)new_ym); / T = Xmem / s->t =
xval_l; / Post-modify / if ((op >> 7) & 1)
s->ar[xar_l]–; else s->ar[xar_l]++; if ((op & 0x08) == 0)
s->ar[yar_l]++; else s->ar[yar_l]–; return consumed +
s->lk_used; } / 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. / 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; / For
now: cond=0x20 → branch if A != 0; cond=0x30 → A == 0. * These are
heuristics until we confirm the exact cond * mapping from SPRU172C.
Tweak based on observed dispatcher * behaviour. / if (sub == 0x2)
take = (acc_signed != 0); else / sub==0x3 / take = (acc_signed
== 0); if (take) { s->pc = op2; return 0; } return consumed +
s->lk_used; } if (sub == 0x2) { / Unreachable now — kept for
clarity in case we revert. / 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 (sub == 0x3) { / Unreachable now. / op2 =
prog_fetch(s, s->pc + 1); s->rpt_count = op2; s->rpt_active =
true; s->pc += 2; return 0; } / 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. Earlier this range was treated as
plain B pmad — a bug that * kept XPC=0 forever (DSP never reached 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: BANZ
with condition / CALL variants / if (sub == 0x4 || sub == 0x5) { op2
= prog_fetch(s, s->pc + 1); / BANZ ARn, pmad / int ar_idx =
op & 0x07; if (s->ar[ar_idx] != 0) { s->ar[ar_idx]–; if (hi8
== 0xF3) { / F300-F31F: INTR k (preserve existing behavior) /
if ((op & 0xFFE0) == 0xF300) { int vec = op & 0x1F; s->sp–;
data_write(s, s->sp, (uint16_t)(s->pc + 1)); s->st1 |=
ST1_INTM; uint16_t iptr = (s->pmst >> PMST_IPTR_SHIFT) &
0x1FF; s->pc = (iptr 0x80) + vec * 4; return 0; }
/* 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: result = src + ((int64_t)(int16_t)op2 << 16); break;
case 0x1: result = src - ((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: { /* 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;
}
/* F330-F35F: 2-word with mask FCF0 (#lk + 4-bit shift).
* AND (sub=3), OR (sub=4), XOR (sub=5).
* Note: ADD (sub=0) and SUB (sub=1) at F30x/F31x are caught
* by INTR handler above (those ranges are INTR semantically). */
if ((op & 0xFCF0) == 0xF030 || /* AND #lk, SHIFT, src, [dst] */
(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 & 0x8) ? (shift_raw - 16) : shift_raw;
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 = (int16_t)op2;
int64_t shifted = (shift >= 0) ? (lk_signed << shift)
: (lk_signed >> (-shift));
int64_t result = src;
switch (subop) {
case 0x3: result = src & shifted; break; /* AND */
case 0x4: result = src | shifted; break; /* OR */
case 0x5: result = src ^ shifted; 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 = (shift >= 0) ? (dst_in << shift) : (dst_in >> (-shift));
result = src & sh;
break;
}
case 0x5: { /* OR */
int64_t dst_in = dst_b ? s->b : s->a;
int64_t sh = (shift >= 0) ? (dst_in << shift) : (dst_in >> (-shift));
result = src | sh;
break;
}
case 0x6: { /* XOR */
int64_t dst_in = dst_b ? s->b : s->a;
int64_t sh = (shift >= 0) ? (dst_in << shift) : (dst_in >> (-shift));
result = src ^ sh;
break;
}
case 0x7: { /* SFTL src,SHIFT,DST: DST = SRC << shift (logical) */
uint64_t usrc = (uint64_t)src & 0xFFFFFFFFFFULL;
result = (int64_t)((shift >= 0) ? (usrc << shift) : (usrc >> (-shift)));
break;
}
}
if (dst_b) s->b = sext40(result); else s->a = sext40(result);
return consumed + s->lk_used;
}
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;
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 — RETED in particular leaves
* INTM=1 forever, blocking every subsequent INT3 and stalling
* the firmware↔DSP frame loop (the original CLAUDE.md root bug).
*
* 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;
{
static uint64_t reted_count;
reted_count++;
if (reted_count <= 20 || (reted_count % 100) == 0)
C54_LOG("RETED #%llu PC=0x%04x -> ra=0x%04x SP=0x%04x INTM=0",
(unsigned long long)reted_count,
s->pc, ra, s->sp);
}
return consumed + s->lk_used;
}
if (op == 0xF69B) {
/* RETFD — fast return, delayed (no INTM change). */
uint16_t ra = data_read(s, s->sp); s->sp++;
s->delayed_pc = ra;
s->delay_slots = 2;
return consumed + s->lk_used;
}
if (op == 0xF6E2 || op == 0xF6E3) {
/* 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.
* Pop XPC + PC unconditionally (FL_FAR). FRETED also clears INTM.
* 2026-04-28 — fixed: was APTS-gated (= AVIS, 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;
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) → tgt. XPC update is immediate
* (mirrors FRETED at line ~1639). FCALAD pushes ret PC+3,
* and (when APTS) pushes XPC first (so RETF/FRETD pops in
* order). 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): push XPC + return PC unconditionally (FL_FAR).
* 2026-04-28 — fixed: was APTS-gated (= AVIS, no stack semantics). */
s->sp = (s->sp - 1) & 0xFFFF;
data_write(s, s->sp, s->xpc);
uint16_t ret_pc = (uint16_t)(s->pc + 3);
s->sp = (s->sp - 1) & 0xFFFF;
data_write(s, s->sp, ret_pc);
}
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) {
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;
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 0x3: s->ar[2] = k; break;
case 0x4: s->ar[3] = k; break;
case 0x5: s->ar[4] = k; break;
case 0x6: s->ar[5] = k; break;
case 0x7: s->ar[6] = k; break;
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); 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: s->bk = k; break;
case 0xD: s->sp = k; break;
case 0xE: /* F7Ex: LD #k8, BRC */
s->brc = k; break;
case 0xF: /* F7Fx: LD #k8, ... */
break;
}
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 : push XPC + return PC unconditionally (FL_FAR).
================================================================================
FILE: diag/source_data_write.c SIZE: 7161 bytes, 149 lines
================================================================================
static void data_write(C54xState s, uint16_t addr, uint16_t val) {
/ 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] : “??”;
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”, 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-WRITE on the same mailbox slots tracked in data_read.
* Whoever writes them — DSP or ARM via api_ram alias — gets logged
* so we can attribute the source of the value the firmware polls. */
/* WATCH-WRITE 0x3dd2 — la cellule sur laquelle 0x75db poll en boucle
* (37M reads/15s). Identifier qui écrit (et qui ne le fait pas).
* Cas 1 : zéro write → un bloc compute ne fire jamais.
* Cas 2 : write boot only → init OK mais set steady-state manquant.
* Cas 3 : writes périodiques avec valeur jamais matchée par le test
* à 0x75db → bug dans le compute en amont. */
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));
}
}
if (addr == 0x0ffe || addr == 0x0fff || addr == 0x01F0) {
static unsigned wcount;
if (wcount++ < 30) {
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) {
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) {
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) {
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]. FORCE-DARAM62=1 (env) proves that
* setting data[0x62]=1 makes the DSP escape and reach 0x770c, so
* this range gates the runtime task pipeline. ARM-side writes to
* the API page mirror at +0x0800 (calypso_trx.c calypso_dsp_write)
* but never to DARAM 0x40..0x90 — so any value here must come from
* DSP-self stores (ST/STH/STM/...) or stay zero forever. Capture
* EVERY write with PC+INTM+insn so we can attribute the source.
* INTM annotation lets us tell ISR-context writes from main code. */
if (addr >= 0x0040 && addr <= 0x0090) {
static unsigned daram_disp_w;
if (daram_disp_w++ < 1000) {
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) {
fprintf(stderr,
"[c54x] DISP-FLAG-W log capped at 1000 — pattern visible above\n");
}
}
}
/* 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; }
================================================================================
FILE: diag/source_resolve_smem.c SIZE: 4749 bytes, 117 lines
================================================================================
if ((s->pmst & PMST_OVLY) && addr16 >= 0x80 &&
addr16 < 0x2800) return s->data[addr16]; /* For addresses >=
0x8000: use XPC to select extended page. * prog_read is used for
data/operand reads (MVPD, FIRS coeff, etc.) * which need XPC banking —
unlike prog_fetch which is PC-only. */ if (addr16 >= 0x8000) {
uint32_t ext = ((uint32_t)s->xpc << 16) | addr16; ext &=
(C54X_PROG_SIZE - 1); return s->prog[ext]; } return
s->prog[addr16]; }
static void attribute((unused)) 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 >= 0x80 && 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 *
================================================================ */
/* Resolve Smem operand: direct or indirect addressing. * Returns the
data memory address. / static 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 for * THIS instruction. ARP (in ST0) is then updated
to ARF for the * NEXT direct-Smem reference. Earlier this code used
arp(s) for * cur_arp, which made every indirect insn operate on the *
PREVIOUS insn’s ARF — off-by-one. Symptoms: BANZD AR1- after
STL AR2+ would decrement AR2 instead of AR1 (BANZD test
against AR2 stayed non-zero forever, AR1 frozen). Diagnosed * via
5×500M-insn STATE-DUMP showing AR1=0x1c / AR2=0x2b0c * frozen across 2B
insns at PC=0xa2c2..0xa2ca. / indirect = true; int mod =
(opcode >> 3) & 0x0F; int nar = opcode & 0x07; int cur_arp
= nar; uint16_t addr = s->ar[cur_arp];
/* 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;
case 0x4: /* *ARn-0 */
s->ar[cur_arp] -= s->ar[0];
break;
case 0x5: /* *ARn+0 */
s->ar[cur_arp] += s->ar[0];
break;
case 0x6: /* *ARn-0B (bit-reversed) */
/* Simplified: just subtract */
s->ar[cur_arp] -= s->ar[0];
break;
case 0x7: /* *ARn+0B (bit-reversed) */
s->ar[cur_arp] += s->ar[0];
break;
case 0x8: /* *ARn-% (circular) */
if (s->bk == 0) s->ar[cur_arp]--;
else {
uint16_t base = s->ar[cur_arp] - (s->ar[cur_arp] % s->bk);
s->ar[cur_arp]--;
if (s->ar[cur_arp] < base) s->ar[cur_arp] = base + s->bk - 1;
}
break;
case 0x9: /* *ARn+% (circular) */
if (s->bk == 0) s->ar[cur_arp]++;
else {
uint16_t base = s->ar[cur_arp] - (s->ar[cur_arp] % s->bk);
s->ar[cur_arp]++;
if (s->ar[cur_arp] >= base + s->bk) s->ar[cur_arp] = base;
}
break;
case 0xA: /* *ARn-0% */
s->ar[cur_arp] -= s->ar[0];
break;
case 0xB: /* *ARn+0% */
s->ar[cur_arp] += s->ar[0];
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)
*
* The bootloader at PROM0 0xb429 uses MOD=15 (`LDU *(0x0ffe), A`)
* to read BL_ADDR_LO. Misdecoding 15 as "AR + lk circular"
* produced AR0+0x0ffe instead of 0x0ffe — one of the multiple
* subtle off-by-AR bugs that left A=0 after the load. */
case 0xC: /* *AR(x)(lk) */
addr = s->ar[cur_arp] + prog_fetch(s, s->pc + 1);
s->lk_used = true;
break;
================================================================================
FILE: hw/arm/calypso/calypso_arm2dsp.c SIZE: 17194 bytes, 322 lines
================================================================================
/ calypso_arm2dsp — ARM->DSP task-post bridge (faithful
data wire). On real Calypso the ARM (osmocom L1) orchestrates
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) that polls * the task-ready word
data[0x0fff]: when bit1 (0x0002) is set, it dequeues and * runs the
posted task (0xb42a) via its own ROM code. Earlier this
module FORCED go-live by redirecting the DSP PC into a setter * and
poking IMR/INTM/SP. That approach was proven a dead-end (2026-07-02 *
STATUS addendum 8): forcing the frame IT out of a clean idle context
pushes a * bogus return PC and self-loops PC=0x0000. It is removed.
The faithful wire only posts DATA the DSP already polls: on the
ARM * B_GSM_TASK write we set the DSP task-ready flag (data[0x0fff]
bit1, mirrored * into api_ram). No PC redirect, no IMR/INTM/SP poke, no
vector poke — the DSP * dispatches and goes live entirely through its
own ROM. 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) Fix A (2026-07-24) — faithful ARM
background-enable handshake: * 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 enable value posted into both cells *
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_dsp_shunt.h” #include “calypso_arm2dsp.h”
#include <stdlib.h> #include <stdio.h>
/* ARM byte offset of d_dsp_page (DSP word 0x08E2). B_GSM_TASK =
bit1. */ #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 */
/* —- Fix A (2026-07-24): faithful ARM background-enable handshake
———— * VERIFIED on the live full-grgsm run (PHASE-SM trace, not the code
comments): * PHASE-SM pc=0xdddb d[3f70]=0000 d[098a]=0000 d[098c]=0000
d[0fff]=0002 * PHASE-SM pc=0xddeb d[3f70]=0001 d[098a]=0000 … * PHASE-SM
pc=0xde8b A=0x0000 d[3f70]=0001 d[098a]=0000 … <- reset branch * The
DSP go-live phase-SM (0xdddb -> 0xddeb -> 0xde8b) polls d[0x098a]
* (d_background_enable) / 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 would raise d[0x3f70] bit1 — the flag the
go-live wait-loop test * at 0xa4d4 reads to leave the loop. Consequence
(all measured on the live log): * 0xde9c = 0 hits, F70-SETBIT1 = 0, the
wait-loop 0xa4ca/0xa4d0 spins 300k+ * times toggling INTM (“blocs de 10
en 10”), and the FB correlator [0x8d00.. * 0x9000] is never entered (0
hits). The ARM never writes near 0x098a in the * live run. On real
Calypso the ARM posts these cells as part of the go-live * handshake;
here we post them — DATA the DSP already polls, no PC / IMR / INTM * /
SP poke — exactly ONCE, gated on the ARM having commanded the task (the
* dispatcher bit d[0x0fff] & 0x0002 is set). The DSP then reaches
0xde9c, sets * d[0x3f70] bit1 itself and leaves the wait-loop natively:
a SINGLE go-live * transition instead of the per-frame re-fire. Off by
default (reversible). / 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) / 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 = getenv(name); if (!e || !*e) { return def; } return
(uint16_t)strtoul(e, NULL, 0); }
/* @BEQUILLE — ARM2DSP (+ _TASKWORD /
_TASKBIT) (CALYPSO_ARM2DSP, atoi>0, defaut 0) * masque : la
propagation ARM->DSP du bit dispatcher data[0x0fff] bit1 que *
l’ecriture ARM de d_dsp_page (B_GSM_TASK) devrait produire via l’API *
RAM partagee. * retirer : quand l’ecriture ARM de d_dsp_page est
reellement routee vers ce * module (ou quand le dispatcher ROM lit la
cellule que l’ARM ecrit). * NB : calypso_arm2dsp_on_arm_write() n’a
AUCUN appelant -> sans * CALYPSO_ARM2DSP_CONT, ARM2DSP=1 ne poste
rien. / static void a2d_resolve(void) { if (a2d_on >= 0) {
return; } const char e = 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);
/* Fix A: faithful background-enable handshake (independent of a2d_on). */
/* @BEQUILLE — ARM2DSP_BGEN (+ _A / _C / _VAL / _POLLPC / _ONESHOT)
* (CALYPSO_ARM2DSP_BGEN, atoi>0 ; :=1 en calypso.env, native,
* native_helped, wire ; INDEPENDANT de CALYPSO_ARM2DSP)
* masque : le handshake go-live ou l'ARM pose d_background_enable (0x098a) et
* d_background_state (0x098c). Sans lui la phase-SM 0xdddb->0xddeb
* prend la branche reset, 0xde9c n'est jamais atteint, d[0x3f70] bit1
* reste 0 et la wait-loop 0xa4ca/0xa4d0 spinne indefiniment.
* retirer : quand le firmware ARM emule ecrit lui-meme 0x098a/0x098c dans
* l'API RAM (portage du handshake cote ARM).
*/
const char *eb = 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_pollpc = a2d_env_u16("CALYPSO_ARM2DSP_BGEN_POLLPC", 0xdddb);
const char *eo = 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 sous WIRE, :=0 en NATIVE et
* NATIVE_HELPED)
* masque : l'ecriture ARM de d_ctrl_system (data[0x0810] bit15) faite par
* l1s_reset() sur le vrai Calypso, que le pont API emule ne propage
* pas. Sans elle le gate 0xa53c (BITF #0x8000) court-circuite en 0xa575.
* retirer : quand le firmware ARM emule ecrit 0x0810 via le chemin API normal.
* ATTENTION : ecriture DIRECTE dans s->data[] -> invisible de data_write, donc
* de CALYPSO_WATCH_0810. Forcee, elle declenche B_TASK_ABORT et casse
* le retour FB (d'ou le =0 des profils natifs).
*/
const char *ec = 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]=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_c, a2d_bgen_val, 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; } / [2026-07-27] Ctrl-C mobile / L1CTL_RESET_REQ FULL : le
firmware fait * l1s_reset_hw() -> ecrit d_dsp_page = 0 (sync.c:168).
Signal UNIQUE de reset * L1 (jamais 0 en operation = B_GSM_TASK|w_page).
On re-arme le go-live BGEN * -> le DSP re-produit FBSB/SI apres la
relance mobile (sinon rien ne revient). / if (offset ==
A2D_DSP_PAGE_OFF && value == 0) { a2d_bgen_done = 0;
calypso_dsp_shunt_l1_reset(); / clear IMM-ASSIGN/SDCCH latches
(SMS) -> SI revient */ } }
/* 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 each pass (persists — nothing clears it, but
* this is robust 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);
}
}
/* ---- Fix A: faithful 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. */
/* [2026-07-27] RE-ARM go-live sur reset L1 : un L1CTL_RESET_REQ FULL
* (re-sync post-dedie SMS, OU relance du process mobile) fait re-clear
* d[0x098a]/d[0x098c] par le firmware -> l'oneshot bloquait le re-post ->
* DSP L1S stale -> pas de FBSB completion -> sync timeout. On re-arme des
* que la cellule enable est revue a 0 au poll (= go-live re-demande). */
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;
if (a2d_bgen_a >= A2D_API_BASE && s->api_ram) {
s->api_ram[a2d_bgen_a - A2D_API_BASE] = a2d_bgen_val;
}
if (a2d_bgen_c >= A2D_API_BASE && s->api_ram) {
s->api_ram[a2d_bgen_c - A2D_API_BASE] = a2d_bgen_val;
}
a2d_bgen_done = 1;
a2d_bgen_posts++;
if (a2d_bgen_posts <= 8) {
fprintf(stderr,
"[arm2dsp] BGEN post #%u: data[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_c, a2d_bgen_val,
exec_pc, s->data[0x3f70], s->insn_count);
}
}
}
if (!a2d_on) {
return;
}
/* @BEQUILLE — ARM2DSP_CONT (CALYPSO_ARM2DSP_CONT, idiome EXISTS -> "=0" l'ACTIVE,
* seul unset la coupe)
* masque : l'absence de re-post par trame. Le dispatcher ROM efface le bit tache
* entre deux passages ; faute d'un chemin ARM vivant, CONT relit
* d_dsp_page en API RAM a chaque pas DSP et repose le bit. C'est le
* SEUL chemin par lequel ARM2DSP=1 produit un effet.
* retirer : des que on_arm_write() est appele (a2d_pending redevient le
* declencheur) ; verifier au passage l'offset 0x08E2, conteste
* (d_dsp_page pourrait etre 0x08D4).
*/
if (a2d_cont < 0) a2d_cont = getenv("CALYPSO_ARM2DSP_CONT") ? 1 : 0;
/* CONT mode : re-post every step while the ARM's B_GSM_TASK is asserted in
* DSP memory (d_dsp_page word 0x08E2 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[0x08E2 - A2D_API_BASE]
: s->data[0x08E2];
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);
}
}
================================================================================
FILE: hw/arm/calypso/calypso_bsp.c SIZE: 96543 bytes, 1964 lines
================================================================================
/ 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
“qemu/timer.h” #include “hw/arm/calypso/calypso_bsp.h” #include
“hw/arm/calypso/calypso_c54x.h” #include “hw/arm/calypso/calypso_iota.h”
#include “hw/arm/calypso/calypso_invariants.h” #include
“hw/arm/calypso/calypso_twl3025.h” #include
“hw/arm/calypso/calypso_trx.h” #include “calypso_tint0.h” /
GSM_HYPERFRAME / #include “calypso_full_pcb.h” / DARAM lock
helpers — voir pcb.h gap #3 */ #include “calypso_dsp_shunt.h”
int calypso_rxfb_fired = 0; /* [probe golive] 1 des que RX-FBFLAGS
pose 3fad bit15 */
/* calypso_trx_get_fn now provided by calypso_trx.h (included above).
*/
/* Forward decls for env-gated helpers used in calypso_bsp_init
pre-warm. */ static uint32_t d_rach_word_offset(void); static int
rach_force_bsic(void);
#include “hw/arm/calypso/calypso_debug.h”
/* [2026-07-27] DARAM-FNSTAMP : publiees pour le dump c54x (diag). */
unsigned calypso_daram_last_fn; unsigned calypso_daram_wr_count; #define
BSP_LOG(fmt, …)
do { if (calypso_debug_enabled(“BSP”))
fprintf(stderr, “[BSP]” fmt “”, ##VA_ARGS); } while
(0)
#define BSP_TRXD_PORT 6702 /* bridge forwards DL bursts here (5702 is
bridge’s own) */
/* 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 / #define
BSP_QUEUE_LEN 128 / lookahead depth per TN / / Match
window: real BSP captures samples around BDLENA; exact FN match * is a
QEMU artefact. ±4 frames tolerates bridge/BTS CLK IND jitter and * is
narrow enough not to swap adjacent FCCH with non-FCCH in the 51- *
multiframe pattern (FCCH appears every 10 frames on the BCCH slot).
/ / Was 4: too tight for BTS scheduler lookahead (observed
delta=1..139 with * mean ~50). 99 % of bursts went stale before the QEMU
virtual FN caught up. * 64 covers the typical lookahead and lets the
queue drain fast enough that * BDLENA pulses actually consume bursts.
(Bumped to 1024 in diag 2026-04-26 * — confirmed not the bottleneck.
Restored to 64.) */ #define BSP_FN_MATCH_WINDOW 64
/* === DARAM write-by-range instrumentation (2026-05-14) ===
Plages observées dans le rapport 05-14 + finding 100% match PROM0
: * low : DARAM zone lue par AR3 stride +19 dans le correlator FB-det *
target : zone cible CALYPSO_BSP_DARAM_ADDR par défaut (0x3FB0..) * wrap
: zone wrap circulaire AR2/AR7 BK=176 stride -19 * other : ailleurs
(incluant débord daram_len=296 vers [0x4000..0x40D7]) Une
stat tranche 3 hypothèses sur la priorité A : * low=0 ET target=0 ET
wrap=0 → BSP DMA jamais armée (bug amont TPU/INTH) * target>>0 ET
low=0 → BSP écrit mais env var ignorée * low>>0 → BSP écrit où il
faut, mismatch est en contenu/timing Exposé via log line
[BSP] DARAM-WR-STATS ... toutes les 1000 writes, *
parseable par le harnais pytest (test_bsp_daram_write_distribution). */
#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
typedef struct { int16_t iq[296]; /* 148 I/Q pairs max / 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 avec 0xCAFE pour
identifier buffer cible via read trace / uint8_t bypass_bdlena;
/ CALYPSO_BSP_BYPASS_BDLENA=1 : delivre tous les bursts sans
attendre la fenetre BDLENA — debug-only pour sonder l’adresse DARAM
cible. ATTENTION HACK env-gated. / 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 (cf. BSP_BUCKET_* + 2026-05-14 plan). */
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;
/* Virtual-clock drain timer (revised 2026-05-24 PM): decouple BSP→DSP
* DMA delivery from tdma_tick (which can be slow under icount=auto),
* but stay on QEMU_CLOCK_VIRTUAL so BSP and ARM cur_fn share the same
* time domain. Previously on REALTIME → drift ~1300 fr in 6 s wall
* vs ARM (BTS livré au rythme wall, ARM compté au rythme icount). */
QEMUTimer *drain_timer;
} bsp;
#define BSP_DRAIN_PERIOD_MS 5
/* === Deterministic replay (2026-05-28) ============================
* Test discriminant : si CALYPSO_BSP_REPLAY_FILE est set, le BSP charge
* un dump de bursts (format identique à BSP_DUMP_RX_FILE) et les injecte
* sur QEMU_CLOCK_VIRTUAL à cadence fixe, AU LIEU d’écouter le socket
UDP. * Source devient totalement déterministe. Workflow : *
1. Run normal avec BSP_DUMP_RX_FILE=/tmp/bsp_rx.dump → capture * 2.
Re-run avec CALYPSO_BSP_REPLAY_FILE=/tmp/bsp_rx.dump → replay * 3.
Comparer signature d_fb_det 2-3 runs replay → si identique entre * runs,
déterminisme restauré, course feed = root cause confirmée
Cadence : 1 burst toutes les 576us virtuels = 1 burst par TN slot GSM *
(8 slots × 217 frames/sec ≈ 1736 bursts/sec). Approximation suffisante *
pour reproduire le rythme TDMA. */ typedef struct ReplayBurst { uint32_t
fn; uint8_t tn; uint16_t n; int16_t iq[296]; } 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 / / 2026-05-24 fix drift BTS↔︎L1 :
BSP drain timer passe REALTIME → VIRTUAL pour * tourner sur la même
horloge qu’ARM fn (via TINT0) et tdma_tick. Avec * icount=auto, REALTIME
avance ~9% plus vite que VIRTUAL → drift cumulatif * (~1300 fr / 6 sec
wall observé, “1 seconde d’écart BTS↔︎L1”). NS variant pour * appairage
avec QEMU_CLOCK_VIRTUAL (timer_new_ns / qemu_clock_get_ns). /
#define BSP_DRAIN_PERIOD_NS (BSP_DRAIN_PERIOD_MS 1000000ULL)
/* Incrémente le bucket selon addr puis émet une ligne
stats périodiquement. * Appelé à chaque write DARAM côté BSP (rx_burst
direct + deliver_buffered). / static inline void
bsp_daram_wr_bucket(uint16_t addr) { bsp.wr_total++; / target zone
suit le runtime daram_addr (= ce que BSP écrit pour de vrai). *
Anciennes bornes hardcodées 0x3FB0..0x3FFF étaient avant le canary fix *
2026-05-28 qui a changé le default à 0x2a00. Si daram_addr=0 (=
discovery * mode), pas de target zone — tous les writes sont “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++;
}
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 (2026-07-24) : suit le burst le PLUS PROCHE,
fenêtre ignorée, pour * exposer l’offset/dérive burst_fn vs
dispatcher_fn même quand tout est 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 (gated CALYPSO_BSP_FN_PROBE) : la FN portée par le burst le plus
* proche (posée à bsp_enqueue) vs la FN que le dispatcher teste ici
* (current_fn = calypso_trx_get_fn), côte à côte. delta CONSTANT = offset
* (fix = une ligne) ; delta qui DÉRIVE = problème d'horloge. Cap 300 + 1/500. */
{
static int fp = -1;
if (fp < 0) fp = getenv("CALYPSO_BSP_FN_PROBE") ? 1 : 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 = getenv(name); if (!v || !v) return def; /
Auto-detect hex even sans préfixe 0x : si la chaîne contient * un digit
hex non-décimal (a-f / A-F), force base 16. Évite le * piège strtoul
base=0 qui parse “2a00” comme décimal → 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; }
uint16_t calypso_bsp_get_daram_len(void) { return bsp.daram_len; }
uint8_t calypso_bsp_get_last_att(void) { return bsp.last_att; }
/* —- UDP TRXDv0 DL receive callback —- */
static void bsp_trxd_readable(void opaque) { /
BRIDGE_BSP_IQ=1 envoie 8 hdr + 4148 IQ = 600 bytes. Le buffer 512
historique TRONQUAIT silencieusement → BSP recevait soft-bits non
* convertis → IQ_PASSTHROUGH if-branch jamais prise → hard cos_tab *
fallback → AFC rotation BSP totalement ineffective. / uint8_t
buf[4096]; / [2026-07-22] 2376 > 2048 : evite la troncature du
burst 592 I/Q */ 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;
/* Tee I/Q vers le bridge de démod (gr-gsm py) sous shunt : le BSP gate
* la livraison DARAM de toute façon (calypso_bsp.c ~990), donc on forwarde
* le burst brut (8 hdr + I/Q cs16) vers CALYPSO_IQ_TEE_PORT (défaut 6703).
* Le bridge démode → GSMTAP → 4730 → shunt feed_si → a_cd. ZÉRO hack :
* c'est le VRAI signal du BTS qui transite, juste copié hors-bande.
* Sert aussi au FFT live (lit le tee 6703). Gaté shunt only (diag). */
if (calypso_dsp_shunt_active()) {
static int tee_fd = -1;
static struct sockaddr_in tee_dst;
if (tee_fd == -1) {
tee_fd = socket(AF_INET, SOCK_DGRAM, 0);
const char *p = getenv("CALYPSO_IQ_TEE_PORT");
int port = (p && *p) ? atoi(p) : 6703;
/* Dest configurable : 127.0.0.1 (bridge in-container) par défaut,
* ou CALYPSO_IQ_TEE_HOST=172.20.0.1 (gateway gsm-inter) pour viser
* l'hôte → FFT live pop-up côté hôte (X natif, pas de X dans docker). */
const char *h = getenv("CALYPSO_IQ_TEE_HOST");
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 = (h && *h) ? inet_addr(h)
: htonl(INADDR_LOOPBACK);
BSP_LOG("IQ-TEE -> %s:%d (bridge/FFT)", (h && *h) ? h : "127.0.0.1", port);
}
if (tee_fd >= 0)
sendto(tee_fd, buf, n, MSG_DONTWAIT,
(struct sockaddr *)&tee_dst, sizeof(tee_dst));
/* Buffer shm (pas UDP) : publie l'I/Q d'entree du DSP shunte pour
* gr-gsm. buf[8..] = int16 I/Q entrelaces (cs16, mode passthrough),
* comme le tee. gr-gsm lit ce buffer cote shm. */
if (n > 8) {
uint32_t _fn = ((uint32_t)buf[1] << 24) | ((uint32_t)buf[2] << 16) |
((uint32_t)buf[3] << 8) | (uint32_t)buf[4];
calypso_dsp_shunt_feed_iq(_fn, (const int16_t *)(buf + 8),
(int)((n - 8) / 2));
}
}
/* Diag : log first 10 recv sizes pour vérifier que bridge envoie bien
* 600 bytes (= IQ mode) et que BSP buffer ne tronque pas. */
{
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));
}
/* "le shunt dsp ne doit pas shunt l'ipc" : shunt actif = le vrai DSP ne
* consomme JAMAIS la DARAM (gr-gsm decode via le shm feed_iq). On a deja
* draine l'UDP (recvfrom) + publie l'I/Q (feed_iq) + appris le peer UL.
* On NE remplit donc PAS la queue DARAM (bsp_enqueue) -> sinon elle sature
* (bursts_dropped_queue_full) -> backpressure qui remonte et TUE l'IPC/BTS.
* On rend la main : l'IPC continue de couler, le shm est nourri. */
{
/* Frontiere B (2026-07-20) : en mode revive c54x (DSP=c54x + RUN_C54X=1)
* le correlateur NATIF lit la DARAM 0x2a00 -> on NE return PAS, on remplit
* AUSSI la DARAM pour lui fournir de IQ (le DSP la consomme -> pas la
* saturation du shunt-only). Hors revive : comportement inchange. */
/* @BEQUILLE — BSP_DARAM_FORCE (CALYPSO_BSP_DARAM_FORCE, idiome EXISTS, defaut OFF ; calypso_wire.env:=1)
* masque : calypso_dsp_shunt_route_c54x_active() ne devient jamais vrai sous
* DSP_SHUNT=1, donc ce gate shunt fermerait la DARAM au correlateur
* natif. Le forcage remplace la route DSP reelle, non modelisee.
* retirer : quand route_c54x_active() reflete l'etat reel du c54x, ou quand
* DSP_SHUNT et DSP_RUN_C54X cessent d'etre simultanement a 1.
* NB : EXISTS -> "=0" NE COUPE PAS, seul unset coupe. Sites solidaires :
* rx_burst (rb_revive) et deliver_buffered (rxw).
*/
static int bsp_revive = -1;
if (bsp_revive < 0) {
const char *e = getenv("CALYPSO_DSP_RUN_C54X");
/* [2026-07-25] read-path diag : sous shunt le buffer I/Q DARAM 0x2a00
* n'etait ecrit que 2x (route_c54x_active=faux) -> correlateur affame.
* CALYPSO_BSP_DARAM_FORCE=1 force le revive (ecrit 0x2a00) quand le vrai
* c54x tourne (RUN_C54X=1), independamment de route_c54x_active. */
bsp_revive = (e && *e == '1'
&& (calypso_dsp_shunt_route_c54x_active()
|| getenv("CALYPSO_BSP_DARAM_FORCE"))) ? 1 : 0;
}
if (calypso_dsp_shunt_active() && !bsp_revive)
return;
}
/* TRXDv0 DL: tn(1) fn(4) rssi(1) toa(2) bits(148) = 156 bytes.
* (Confirmed empirically 2026-05-07 — earlier "asymmetric 6-byte
* header" hypothesis was wrong : RX header IS 8 bytes like TX.
* Even when BTS emits 154-byte packets, the n-8 skip + 148-bit
* clamp keeps DSP demod aligned. n-6 broke mobile L1 sync —
* mobile stayed in cell-selection loop and never reached 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];
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;
/* 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 (2026-05-24) : si le payload UDP fait >= 296 octets
* et que CALYPSO_BSP_IQ_PASSTHROUGH=1, on interprète buf[8..] comme
* int16 IQ pairs LE (calypso-ipc-device CALYPSO_BSP_IQ_PASSTHROUGH=1 envoie ce format,
* GMSK-modulé scipy BT=0.3 réaliste vs notre ±π/2 hard-modulation).
* Sinon : modulation interne historique (148 hard-bits → 296 int16). */
int16_t iq[296]; /* 148 I/Q pairs = 296 values */
int iq_count = 0;
static int iq_pt_mode = -1;
if (iq_pt_mode < 0) {
const char *e = getenv("CALYPSO_BSP_IQ_PASSTHROUGH");
/* [2026-07-25] passthrough par DEFAUT (coherence feed : 0x2a00 = MEME
* I/Q que gr-gsm via feed_iq). Synthese cos/sin = tone incoherent ->
* opt-out explicite CALYPSO_BSP_IQ_PASSTHROUGH=0 seulement. */
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 */
/* Bridge envoie 2 int16 par bit (I,Q interleaved). 4 bytes/bit.
* Pour 146-148 bits = 584-592 octets payload.
* Auto-détection : si bytes >= 4*146 → IQ mode (BTS-source 146 bits OK).
* Sinon → soft bits 1 byte/bit. */
int iq_min_bits = 146;
if (iq_pt_mode && iq_bytes >= 4 * iq_min_bits) {
/* [2026-07-22] STEP 3 : le device envoie 592 I/Q @4SPS (OSR=4). Le
* correlateur DSP veut 148 samples @1SPS (FCCH = +pi/2/samp). On DECIME
* par CALYPSO_BSP_IQ_DECIM (defaut 4) : 1 sample sur decim -> le tone
* +0.393/samp @4SPS devient +0.393*decim = +pi/2. decim=1 = ancien
* comportement (148 premiers @4SPS = 37 symb, jamais correle). */
static int decim = -1;
if (decim < 0) {
const char *d = 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; /* jusqu'a 592 (buf[4096]) */
iq_count = 0;
for (int k = 0; k * decim < total_cplx && iq_count <= 294; k++) {
iq[iq_count++] = isrc[2 * (k * decim)]; /* I */
iq[iq_count++] = isrc[2 * (k * decim) + 1]; /* Q */
}
nbits = iq_count / 2;
/* Apply AFC rotation : TWL3025 VCXO offset propagation. No-op si
* CALYPSO_TWL3025_AFC != 1. Convergence AFC chain dépend de ça :
* firmware applique AFC delta → DSP TSP → TWL3025 DAC → samples
* rotated → DSP correlator voit la convergence. */
/* ⚠️ TESTING 2026-05-29 : apply_phase déplacé décode -> delivery
* (l'AFC doit s'appliquer quand le DSP voit les samples = dac courant,
* pas au décode où le dac est stale de ~lookahead frames). */
/* 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. */
static const int16_t cos_tab[4] = { 0x7FFE, 0, -0x7FFE, 0 };
static const int16_t sin_tab[4] = { 0, 0x7FFE, 0, -0x7FFE };
int phase_idx = 0;
for (int i = 0; i < nbits; i++) {
/* Anomaly A fix (2026-05-08) : émettre AVANT advance, donc le premier
* sample est à phase=0 au lieu de phase=π/2. Le code original
* advance-then-emit décalait tout le burst de 90°, faisant que la
* corrélation cohérente du DSP correlator tombait dans la partie
* quadrature au lieu d'in-phase → d_fb_det principalement négatif
* (pattern observé : +23k, +20k occasionnel puis 4× -5k consécutifs).
* À valider sur le prochain run. */
iq[iq_count++] = cos_tab[phase_idx]; /* I — phase_idx avant advance */
iq[iq_count++] = sin_tab[phase_idx]; /* Q */
phase_idx = (phase_idx + (bits[i] ? 3 : 1)) & 3;
}
}
/* Enqueue the burst FN-indexed for this TN. With BTS lookahead of up
* to ~92 frames, several bursts are in flight at once; each must be
* delivered at the exact QEMU virtual FN it was scheduled for, or
* the DSP correlator runs against incoherent samples. */
/* [2026-07-22] Option 2 GATED (CALYPSO_BSP_DIRECT_FEED=1) : restaure le wire
* mort. En full, bsp_enqueue->deliver_buffered ne livre JAMAIS (device_fn
* temps-reel >> cur_fn virtuel qui traine sous icount=auto -> match FN +/-64
* echoue -> DARAM 0x2a00 jamais ecrite -> correlateur affame, fb0_ret=0).
* Gate ON : feed DARAM 0x2a00 DIRECTEMENT via calypso_bsp_rx_burst (write
* immediat + c54x_bsp_load + INT3, SANS match FN). Gate OFF : inchange. */
{
/* @BEQUILLE — BSP_DIRECT_FEED (CALYPSO_BSP_DIRECT_FEED, EQ1, calypso.env:=1 -> ACTIF)
* masque : le match FN de bsp_take_for_fn (+/-BSP_FN_MATCH_WINDOW) echoue
* systematiquement parce que la FN du device (temps reel) et la FN
* virtuelle QEMU divergent -> DARAM jamais ecrite. On livre sans
* aucune correspondance temporelle : le burst arrive "maintenant".
* retirer : quand la FN virtuelle et la FN device sont alignees (FN-PROBE
* delta ~0 stable) ; alors bsp_enqueue -> deliver_buffered suffit.
* NB : tant que ce gate vaut 1, TOUT calypso_bsp_deliver_buffered() est
* du code mort (file toujours vide).
*/
static int direct_feed = -1;
if (direct_feed < 0) {
const char *e = getenv("CALYPSO_BSP_DIRECT_FEED");
direct_feed = (e && *e == '1') ? 1 : 0;
BSP_LOG("BSP_DIRECT_FEED=%d (option2 wire)", direct_feed);
}
if (direct_feed)
calypso_bsp_rx_burst(tn, fn, iq, iq_count);
else
bsp_enqueue(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 (2026-05-29) : pulls BSP UDP queue into
DSP DMA * à la cadence wall-clock 5ms (= 200/sec). Monotonic anti-drift
rearm sur * last_target + period pour éviter accumulation
de jitter dispatcher. Historique : pre-2026-05-24 c’était
REALTIME → drift vs VIRTUAL sous * icount=auto. Switch vers VIRTUAL
fixait ce drift. 2026-05-29 : maintenant * que tdma_tick est REALTIME
monotonic + clk_master pthread, virtual et * wall sont alignés. On peut
repasser drain en REALTIME — la cadence wall * matche la cadence ARM
frame_irq/tdma. Et surtout : sous load DSP heavy, * VIRTUAL tournait
moins vite que wall → drain trop lent → BSP queue * overflow → 95% des
bursts droppés. / static void bsp_drain_cb(void opaque) {
static int64_t last_target = 0; /* Drain la socket UDP DL ICI (timer
REALTIME fiable, fix 2026-05-30). * Sous icount=auto le DSP (c54x_run)
monopolise le thread mainloop → * l’iohandler bsp_trxd_readable n’est
jamais servi → les paquets device * s’accumulent non-lus (Recv-Q monte)
→ BSP-DELIVER=0, D_BURST_D vide, * snr=0. On vide la socket à chaque
tick drain (recvfrom MSG_DONTWAIT), * indépendant de la mainloop
affamée. 64 = marge (≈1-2 bursts/5ms). / { / Test décisif :
PEEK direct sur bsp.trxd_fd — la data est-elle sur CE * fd ?
(errno=EAGAIN/11 = rien ici ; >0 = data présente). / 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) /* FIX 2026-06-02 : reporte le
VRAI compteur de livraison * bursts_written (incr. dans
deliver_buffered ligne 1107 = burst * réellement écrit en DARAM
dsp->data[a]) au lieu du bursts_seen *
MORT. bursts_seen vit dans calypso_bsp_rx_burst, que le refactor *
2026-05-29 a bypassé (deliver écrit inline) → seen=0 à vie = sonde *
menteuse qui a coûté des heures de fausse piste “feed mort”. *
delivered>0 et qui monte = signal réellement livré au DSP. */
fprintf(stderr, “[BSP] DRAIN-CB #%llu fd=%d PEEK=%zd errno=%d”
“delivered=%llu enq_drops(stale=%llu,full=%llu) seen_DEAD=%llu”,
(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 1 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. / 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 >
296) { 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 forcages actifs (gate
CALYPSO_INVARIANTS, defaut off) / / 2026-05-28 : ancien
commentaire “DSP reads I/Q at 0x3fb3-0x3fbe” * obsolete. Discovery par
CALYPSO_BSP_INJECT_CANARY a confirme que * le vrai buffer cote DSP est
0x2a00 (PC=0x93a5 consumer, AR3 post-inc * sur 0x2a00..0x2a13). Nouveau
default ci-dessous. / / DARAM target where BSP DMAs DL samples.
Default 0x2a00, identifie via * methode 3 (CALYPSO_BSP_INJECT_CANARY
2026-05-28) : * 1. Static scan PROM0 : 0x2a00 = top STM #imm,ARx init
(50 sites, * AR1..AR6 ; companion BK=0x015e=350 = burst size GSM) * 2.
Runtime canary injection : CALYPSO_BSP_INJECT_CANARY=1 → * DSP READS
0xCAFE at addr=0x2a00..0x2a13 via PC=0x93a5 (= real * consumer routine),
AR3=0x2a00 post-incrementing. E2E proven. * Voir
doc/BOOT_TO_FBSB_SEQUENCE.md. Override via env si besoin. /
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; / ATTENTION HACK HACK HACK
!!!!!!!! * CALYPSO_BSP_BYPASS_BDLENA=1 : bypass de la fenetre IOTA
BDLENA. * Sur silicon, le BSP ne delivre les samples au DSP que pendant
la * fenetre BDLENA assertee par IOTA. Sur emu, on a parfois besoin de *
sonder quelle adresse DARAM le DSP correlator lit reellement *
(CALYPSO_BSP_DARAM_ADDR mismatch suspecte) — ce flag desactive le * gate
pour livrer TOUS les bursts. Default OFF. * Critere de retrait : DARAM
target identifiee + a_pm/a_sync_demod * publies nonzero par DSP. Voir
doc/TODO.md. / 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) : if
CALYPSO_BSP_INJECT_CANARY=1, BSP * overwrites all samples with a
recognizable marker (0xCAFE) before * DARAM write. Combined with
data_read_locked canary watch in * calypso_c54x.c, this directly
identifies WHERE the DSP reads from * the BSP buffer at runtime — no
brute-force. * Disable in normal runs. Voir doc/TODO.md. /
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 = 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 = getenv("CALYPSO_BSP_BIND_ADDR");
const char *bind_lo_env = 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 : permet d'insérer un proxy Python (iq_proxy.py)
* entre source et QEMU. Source unchanged (envoie sur 6702), QEMU
* listen sur CALYPSO_BSP_PORT=6712 (par ex), proxy fait Doppler. */
const char *port_env = 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 doesn’t pay the * cost of strtoul/getenv mid-run. Reportedly the
static-cache pattern * had correlated runtime variability with LU
success rate. */ (void)d_rach_word_offset();
(void)rach_force_bsic();
/* Arm REALTIME drain timer — wall-paced 5ms, monotonic anti-drift dans
* bsp_drain_cb. Aligné sur le même CLOCK_MONOTONIC que le pthread
* clk_master (calypso_trx.c). 2026-05-29 : sortie de VIRTUAL parce
* qu'on droppait 95% des bursts sous load DSP (virtual lag → drain
* trop lent). */
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 —- */
void calypso_bsp_rx_burst(uint8_t tn, uint32_t fn, const int16_t
iq, int n_int16) { bsp.bursts_seen++; / ⚠️ TESTING 2026-05-29 :
marqueur — si ça fire, rx_burst EST vivant * (et il faudra y appliquer
l’AFC aussi). Sinon = code mort. */ { 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”, rxb_n, (unsigned)fn, (unsigned)tn, n_int16); rxb_n++; }
/* GATE DSP_SHUNT : si le shunt est actif, le c54x ne tourne pas et
* le mock écrit directement NDB/read-page. Toute écriture BSP vers
* DARAM écraserait les valeurs canned du mock. */
/* [2026-07-22] Exception revive : en c54x REEL (route_c54x + RUN_C54X=1) le
* vrai DSP LIT la DARAM 0x2a00 -> on DOIT ecrire (aucun mock canned a clobber).
* Meme condition que bsp_trxd_readable:416. Drop UNIQUEMENT en shunt-mock pur. */
{
/* @BEQUILLE — BSP_DARAM_FORCE (site rx_burst) (CALYPSO_BSP_DARAM_FORCE, EXISTS, defaut OFF)
* masque : idem site bsp_revive — la route c54x reelle n'est pas modelisee,
* on force l'ecriture DARAM sous shunt quand RUN_C54X=1.
* retirer : avec le site bsp_revive (meme condition).
*/
static int rb_revive = -1;
if (rb_revive < 0) {
const char *e = getenv("CALYPSO_DSP_RUN_C54X");
/* [2026-07-25] idem bsp_revive : CALYPSO_BSP_DARAM_FORCE force l'ecriture
* DARAM 0x2a00 (buffer correlateur) sous shunt quand RUN_C54X=1. */
rb_revive = (e && *e == '1'
&& (calypso_dsp_shunt_route_c54x_active()
|| getenv("CALYPSO_BSP_DARAM_FORCE"))) ? 1 : 0;
}
if (calypso_dsp_shunt_active() && !rb_revive) {
if (bsp.bursts_seen <= 3)
BSP_LOG("rx_burst: DSP_SHUNT active (no revive), dropping fn=%u tn=%u", fn, tn);
return;
}
}
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;
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;
}
/* 2026-05-29 : remplacement du gate BDLENA. Anciennement on droppait
* le burst si pas de pulse IOTA matching ; maintenant on délivre
* inconditionnellement. AUCUNE écriture de d_dsp_page ici — c'est
* firmware qui pilote le page flip via dsp_end_scenario (MMIO WR
* sur 0x01A8). On signale juste l'arrivée samples au DSP via INT3
* (= silicon BDLENA→BSP→DSP arm_done equivalent).
*
* Probe read-only sur d_dsp_page : on log la valeur vue par DSP
* au moment du burst (= ce que firmware a écrit). Sans modifier. */
/* [2026-07-22] Sonde FCCH (gated CALYPSO_IQDUMP_FCCH=1) : coherence + dphi du
* burst DECIME ecrit en DARAM. Vrai FCCH decime -> dphi ~ +1.571 (pi/2), coh~1.
* Verifie la couche contenu du feed. fprintf inconditionnel (pas BSP_LOG). */
if (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;
uint16_t cur = bsp.dsp->api_ram[0x08E2 - 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);
}
}
/* Gate INT3 fire : skip si IFR.bit3 déjà set = DSP pas encore servi
* le précédent. Évite stacking d'IRQs quand DSP traite plus lentement
* que BSP delivery rate. */
/* [2026-07-23] FIX namespace bit3/bit12 (diag horloges) : natif remappe vec19/bit3
* -> vec28/bit12 ; l anti-stack doit tester le bit REEL sinon gate tjrs ouvert -> flood. */
{ static int _nat = -1; if (_nat < 0) _nat = (getenv("CALYPSO_FRAME_IT_NATIVE") || getenv("CALYPSO_DSP_FRAME_VEC28")) ? 1 : 0; int _fb = _nat ? 12 : 3;
if (bsp.dsp && bsp.dsp->running && !(bsp.dsp->ifr & (1 << _fb))) {
c54x_interrupt_ex(bsp.dsp, 19, 3);
if (bsp.dsp->idle) bsp.dsp->idle = false;
} }
/* [2026-07-25] WIRE BSP->DSP BRINT0 (direct-feed) : le chemin direct-feed
* (CALYPSO_BSP_DIRECT_FEED=1) livre l'I/Q en DARAM 0x2a00 mais ne levait QUE
* INT3 (vec19/bit3, frame). Le chemin buffered, lui, leve BRINT0 (vec21/bit5)
* = l'IT "buffer recu" qui reveille le handler FB-det correlateur (ISR
* PROM1[0xFFD4]->CALL 0xf310). Sans BRINT0 le correlateur 0x8d00 n'est JAMAIS
* dispatche (0 hit confirme). On le leve comme deliver_buffered (meme
* anti-stack gate IFR bit5). Gate CALYPSO_BSP_DIRECT_BRINT0 (defaut OFF pour
* tester une variable a la fois). */
/* @BEQUILLE — BSP_DIRECT_BRINT0 (CALYPSO_BSP_DIRECT_BRINT0, EXISTS, defaut OFF ; calypso_wire.env:=1)
* masque : sur silicium, la fin de DMA BSP (fenetre BDLENA) leve BRINT0
* vec21/bit5. Le chemin direct-feed ne leve qu'INT3 ; la chaine
* TPU->TSP->IOTA->BSP qui produirait le pulse n'est pas cablee.
* retirer : des que calypso_iota_take_bdl_pulse() est alimente par la fenetre
* RX du TPU et consomme sur le chemin vivant (cf TPU_RX_WIRE).
*/
{ static int _db = -1; if (_db < 0) _db = getenv("CALYPSO_BSP_DIRECT_BRINT0") ? 1 : 0;
/* MISSION-GATE : ne lever BRINT0 que si le DSP est reellement sur la
* mission FB/SB (d_task_md), pas hors-mission -> le reveil correlateur
* arrive au bon moment, comme le vrai "buffer recu" du silicium.
* FB=5 SB=6 TCH_FB=8 TCH_SB=9 (osmo l1_environment.h). */
uint16_t _md = calypso_dsp_shunt_get_task_md();
int _fbsb = (_md == 5 || _md == 6 || _md == 8 || _md == 9);
if (_db && _fbsb && bsp.dsp && bsp.dsp->running && !(bsp.dsp->ifr & (1 << 5))) {
c54x_interrupt_ex(bsp.dsp, 21, 5);
if (bsp.dsp->idle) bsp.dsp->idle = false;
} }
/* [2026-07-26 WF golive-handshake] RX-FBFLAGS sur le chemin VIVANT (rx_burst /
* DIRECT_FEED). Modelise l'ISR BRINT0 0xf310 (jamais prise, INTM=1) qui OR les
* bits de handshake FB-det que le handler-dispatcher 0x8d00 poll. Le handler
* 0x8d00->0xa076 est POLLING PUR (BITF/BC sur RAM) : pas besoin d'IT/INTM.
* MASTER = data[0x3fad] bit15 : gate CC 0xa0a0 @0x8754 -> kernel 0xa076
* (le bloc homonyme de deliver_buffered est MORT sous shunt ET omet 3fad bit15
* -> ne peut pas deboucher le noyau). Gate CALYPSO_RX_FBFLAGS, mission FB/SB. */
/* @BEQUILLE — RX_FBFLAGS (chemin vivant rx_burst) (CALYPSO_RX_FBFLAGS, EXISTS, defaut OFF)
* masque : l'ISR BRINT0 (PROM1[0xFFD4] -> CALL 0xf310) n'est jamais prise,
* donc les bits de handshake FB-det qu'elle devrait poser ne le sont
* pas : data[0x3fad] bit15 (verrou-maitre du kernel @0x8754),
* 0x3faa bit2+bit8, 0x3fab bit8, 0x3fae bit8. On les pose depuis la
* livraison du burst.
* retirer : quand BRINT0 est reellement servie et que son ISR ecrit ces bits ;
* le bloc jumeau de deliver_buffered est deja mort sous
* BSP_DIRECT_FEED=1 et omet 0x3fad -> a supprimer en premier.
* NB : conteneur de POKE_TASK_MD et POKE_DISPATCH ci-dessous.
*/
{ static int _fbf = -1; if (_fbf < 0) _fbf = getenv("CALYPSO_RX_FBFLAGS") ? 1 : 0;
uint16_t _mdf = calypso_dsp_shunt_get_task_md();
int _fbsbf = (_mdf == 5 || _mdf == 6 || _mdf == 8 || _mdf == 9);
if (_fbf && _fbsbf && bsp.dsp) {
bsp.dsp->data[0x3fad] |= 0x8000; /* MASTER kernel gate @0x8754 */
calypso_rxfb_fired = 1; /* [probe] arme la sonde 0x8753 cote c54x */
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 */
/* [2026-07-26 POKE TACHE DSP] le CALA entre le correlateur avec
* task_md(0x0804/0x0818)=0 = AUCUNE mission -> il spinne sur du vide.
* On pose le descripteur de tache (la mission FB/SB) dans les 2 pages
* API-RAM que le dispatcher DSP lit. d_dsp_page(0x08e2) a deja bit1
* (task-ready) set -> seul task_md manquait. */
/* @BEQUILLE — POKE_TASK_MD (+ POKE_DISPATCH ci-dessous) (CALYPSO_POKE_TASK_MD,
* atoi>0 mais DEFAUT 1 SI LA VARIABLE EST ABSENTE)
* masque : le descripteur de tache (d_task_md, pages API-RAM 0x0804/0x0818)
* n'est jamais publie vers le DSP : la DMA page-ecriture ARM->DARAM
* 0x0586 (calypso_trx.c) est fermee sous shunt. Le correlateur entre
* donc sans mission et tourne dans le vide ; on lui pose la mission a
* la main. POKE_DISPATCH va plus loin et replique dsp_end_scenario
* (d_dsp_page = B_GSM_TASK|w_page en alternance).
* retirer : quand la DMA de la write-page atteint le DSP (task_md lu depuis
* 0x0586+DB_W_D_TASK_MD) ; alors ces deux pokes deviennent nuls.
* NB : defaut ON, mais imbrique dans le bloc RX_FBFLAGS -> sans
* CALYPSO_RX_FBFLAGS, jamais atteint.
*/
{ static int _pt = -1; if (_pt < 0) { const char *_pe = getenv("CALYPSO_POKE_TASK_MD"); _pt = _pe ? (atoi(_pe) > 0) : 1; } /* defaut ON, =0 pour desactiver */
if (_pt) { bsp.dsp->data[0x0804] = _mdf; /* task_md page0 = mission (5=FB 6=SB) */
bsp.dsp->data[0x0818] = _mdf; } /* task_md page1 */ }
/* [2026-07-26 POKE DISPATCH osmocom] replique dsp_end_scenario (dsp.c:480):
* d_task_md sur la WRITE-page courante + d_dsp_page = B_GSM_TASK(0x0002)|w_page
* qui ALTERNE 2<->3 (le natif fige a 2 = w_page jamais flippe). Gate. */
{ static int _pd = -1; static uint16_t _wp = 0;
if (_pd < 0) { const char *_de = getenv("CALYPSO_POKE_DISPATCH"); _pd = _de ? (atoi(_de) > 0) : 0; }
if (_pd) {
bsp.dsp->data[_wp ? 0x0818 : 0x0804] = _mdf; /* d_task_md sur write-page */
bsp.dsp->data[0x08e2] = (uint16_t)(0x0002 | _wp); /* d_dsp_page = B_GSM_TASK|w_page */
_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);
} }
/* [2026-07-25] TEST RANK3 (WF1 boot->correlator) : le handler FB-det 0x8d00
* n'est JAMAIS installe dans un slot de dispatch -> le BACC/CALA calcule
* resout toujours vers le stub 0xab38 (le go-live arm 0xa4c7 sinon), et la
* LUT native 0x8341 qui l'installerait est inatteignable (0x7234 deraille
* vers l'overlay 0x013b via d_dsp_page garbage). Ici on INSTALLE 0x8d00
* dans les slots de dispatch POUR LA TRAME du burst FB/SB (mission-gate,
* dynamique par burst -- PAS un pin statique) + on leve IMR bit9 (route
* scheduler 0x7234->0x8341). Au prochain BACC(0xb40f)/CALA(0xb01e) le DSP
* tombe dans le correlateur. Gate CALYPSO_BSP_DISPATCH_FB (defaut OFF). */
/* @BEQUILLE — BSP_DISPATCH_FB (+ _TGT, _NOIMR, _ONESHOT) (CALYPSO_BSP_DISPATCH_FB,
* EXISTS, defaut OFF ; calypso_wire.env:=1)
* masque : la LUT native 0x8341 qui installe le handler FB-det 0x8d00 dans
* les slots de dispatch n'est jamais atteinte (0x7234 deraille vers
* l'overlay 0x013b). On ecrit les slots 0x43c0/0x4387/0x43d8 a la
* main et on ouvre IMR bit9 a la place du scheduler.
* retirer : quand 0x7234 atteint 0x8341 et peuple ces slots tout seul ; le
* demasquage IMR est 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 = getenv("CALYPSO_BSP_DISPATCH_FB") ? 1 : 0;
const char *_t = getenv("CALYPSO_BSP_DISPATCH_FB_TGT");
_tgt = (_t && *_t) ? (uint16_t)strtoul(_t, NULL, 0) : 0x8d00;
_os = getenv("CALYPSO_BSP_DISPATCH_ONESHOT") ? 1 : 0; } /* cible 0x8d00.
* ONESHOT (diag user "pulse") : n'installe+leve BRINT0 qu'UNE fois, au lieu
* de re-dispatcher chaque trame (= la congestion : correlateur re-entre en
* boucle sans finir). Le pulse laisse le correlateur derouler une passe. */
uint16_t _md2 = calypso_dsp_shunt_get_task_md();
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; /* slot terminal BACC 0xb40f (etait 0xa4c7 go-live) */
bsp.dsp->data[0x4387] = _tgt; /* slot idle/CALA 0xb01e (etait stub 0xab38) */
bsp.dsp->data[0x43d8] = _tgt; /* slot reseed (etait stub 0xab38) */
{ /* [2026-07-27] NOIMR : le demasquage IMR est separable de l install
* du handler — il preempte la routine FB 6 instructions apres son
* entree (IT vers vec21/0x00d4). CALYPSO_BSP_DISPATCH_NOIMR=1 pour
* installer le handler SANS toucher l IMR. */
static int _noimr = -1;
if (_noimr < 0) _noimr = getenv("CALYPSO_BSP_DISPATCH_NOIMR") ? 1 : 0;
if (!_noimr) bsp.dsp->imr |= 0x0200; /* bit9 : route 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);
} }
int n = n_int16 < (int)bsp.daram_len ? n_int16 : (int)bsp.daram_len;
/* Load samples into BSP serial port buffer (PORTR PA=0x0034).
* The DSP reads one sample per PORTR instruction from this buffer.
* ⚠️ NON-DÉFINITIF / TESTING 2026-05-29 (hypothèse, à valider/débugger).
* FIX 2026-05-29 : livrer le burst COMPLET (iq[] = I/Q interleaved,
* 2*nbits int16, jusqu'à 296), PAS tronqué à 148. Tronquer à 148 int16
* ne donnait au corrélateur que 74 symboles complexes = la MOITIÉ du
* burst → la tonalité FCCH (FB) ne pouvait jamais corréler → FBSB_CONF
* jamais émis. On borne sur n_int16 (taille réelle du burst), pas n
* (qui était clampé à daram_len pour l'écriture DARAM). */
{
uint16_t samples[296];
int ns = n_int16 > 296 ? 296 : n_int16;
for (int i = 0; i < ns; i++)
samples[i] = (uint16_t)iq[i];
c54x_bsp_load(bsp.dsp, samples, ns);
}
/* Also write to DARAM for code that reads samples directly.
* Wrap the whole burst write + post-write log in a single DARAM lock
* section — sans ça, DSP thread (Phase 2 PCB) racerait avec ce write
* et lirait des samples partiellement écrits. Cost = 1 mutex op pour
* ~157 itérations ≈ négligeable. */
/* ⚠️ NON-DÉFINITIF / TESTING 2026-05-29 (hypothèse, à valider/débugger).
* FIX 2026-05-29 : woff LOCAL (était static) — chaque burst écrit aligné
* à daram_addr[0..n-1]. Le static faisait rouler l'offset cross-burst :
* le burst FB d'une frame atterrissait à un offset que le DSP ne lit pas
* (fragmenté sur le wrap) → corrélateur sur données désalignées. */
unsigned woff = 0;
/* [2026-07-26 golive-mac] ROOT-CAUSE d_fb_det=0 : ce writer rx_burst ecrit
* son iq[] (burst DC degenere = 0x12ed constant) en DARAM 0x2a00, CLOBBANT
* les vrais samples FCCH que feed_iq (calypso_dsp_shunt.c, coh=0.999) y a
* poses. Quand CALYPSO_FB_IQ_DARAM=1, feed_iq DETIENT la DARAM 0x2a00 : on
* SAUTE la boucle d'ecriture concurrente (mais on GARDE acquire/release :
* le lock enveloppe aussi le post-write log jusqu'a 0x2a00 release plus bas).
* Indep. de DIRECT_FEED/DARAM_FORCE. */
/* @BEQUILLE — FB_IQ_OWNS (CALYPSO_FB_IQ_OWNS, atoi>0, calypso.env:=0)
* masque : deux producteurs concurrents ecrivent le meme buffer d'entree du
* correlateur (rx_burst cote BSP et feed_iq cote shunt). Le silicium
* n'a qu'un seul chemin : le BSP. Ce flag arbitre a la main qui
* gagne, faute d'un unique writer.
* retirer : quand feed_iq disparait au profit du seul chemin BSP (ou
* inversement) — il ne doit rester qu'un writer de bsp.daram_addr.
*/
static int _fbiq_owns = -1;
if (_fbiq_owns < 0) {
/* [2026-07-27] DECOUPLE : le SKIP rx_burst ne se declenche QUE sur opt-in
* explicite CALYPSO_FB_IQ_OWNS (defaut OFF). Avant gate sur FB_IQ_DARAM ->
* quand feed_iq n'ecrit pas 0x2a00 (marker=0), le buffer restait affame ->
* kernel correle du vide -> SHADOW-DADST PERDU. Par defaut rx_burst nourrit
* toujours 0x2a00 (kernel vivant). Mettre FB_IQ_OWNS=1 seulement quand le
* feed_iq->0x2a00 est prouve fonctionnel. */
const char *e = getenv("CALYPSO_FB_IQ_OWNS");
_fbiq_owns = (e && atoi(e) > 0) ? 1 : 0;
}
calypso_pcb_daram_lock_acquire();
if (_fbiq_owns) {
static unsigned _sk = 0;
if (_sk++ < 8)
fprintf(stderr, "[BSP] FB-IQ-DARAM owns 0x2a00 : rx_burst DARAM write SKIP "
"(fn=%u tn=%u) -> feed_iq authoritative\n", (unsigned)fn, (unsigned)tn);
} else {
/* [2026-07-27] CALYPSO_BSP_IQ_SHIFT : voir en-tete du patch (instrument). */
static int _iqsh = -1;
if (_iqsh < 0) {
const char *e = getenv("CALYPSO_BSP_IQ_SHIFT");
_iqsh = (e && *e) ? atoi(e) : 0;
if (_iqsh < 0) _iqsh = 0;
if (_iqsh > 12) _iqsh = 12;
if (_iqsh) BSP_LOG("IQ_SHIFT=%d (echantillons >>%d avant DARAM : test saturation)", _iqsh, _iqsh);
}
for (int i = 0; i < n; i++) {
uint16_t a = (uint16_t)(bsp.daram_addr + woff);
bsp.dsp->data[a] = (uint16_t)(int16_t)(_iqsh ? (iq[i] >> _iqsh) : iq[i]);
bsp_daram_wr_bucket(a);
woff++;
if (woff >= bsp.daram_len) woff = 0;
}
/* [2026-07-27] DARAM-FNSTAMP : publie le fn et le nombre d'ecritures
* pour que le dump c54x estampille CE QU'IL LIT (voir en-tete patch). */
calypso_daram_last_fn = (unsigned)fn;
calypso_daram_wr_count++;
}
bsp.bursts_written++;
/* PROBE 2026-05-31 fork-1 : dump des bursts I/Q pour FFT offline (cherche
* le pic FCCH à +67.7 kHz = 1625/24). Gated CALYPSO_IQDUMP. Dump bursts
* 5..28 en raw int16 (un fichier par burst → l'un d'eux = FCCH). À RETIRER. */
/* [2026-07-22] Dumps diag : capture les bursts COHERENTS (= FCCH), pas les
* 24 premiers (startup non-FCCH). coh = meme math que FCCH-PROBE. Sorties :
* /tmp/iq_rx_*.bin (CALYPSO_IQDUMP, raw int16) + bursts.cfile (BSP_DUMP_RX_FILE,
* IQ16 : hdr 12o [magic|fn LE|tn|n_int16 LE|pad] + int16). */
if (getenv("CALYPSO_IQDUMP") || 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) { /* burst coherent = FCCH */
if (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 = 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+1]:0,
n>2?(int16_t)bsp.dsp->data[bsp.daram_addr+2]:0,
n>3?(int16_t)bsp.dsp->data[bsp.daram_addr+3]:0,
n>4?(int16_t)bsp.dsp->data[bsp.daram_addr+4]:0,
n>5?(int16_t)bsp.dsp->data[bsp.daram_addr+5]:0,
n>6?(int16_t)bsp.dsp->data[bsp.daram_addr+6]:0,
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 — gated by BDLENA from the TPU/TSP/IOTA chain.
* The firmware opens the RX window via TPU scenario → TSP write → IOTA BDLENA.
* calypso_iota_take_bdl_pulse() consumed the window above.
* BRINT0 fires once per window, rate-limited by IFR bit. */
if (bsp.dsp && !(bsp.dsp->ifr & (1 << 5))) {
c54x_interrupt_ex(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) { / GATE
DSP_SHUNT (cf calypso_bsp_rx_burst). Idem ici : si le shunt * est actif,
on ne livre aucun sample bufferisé — le mock owns la * DARAM API region
pour ses canned responses. * HYBRIDE (RANK2, CALYPSO_TPU_RX_WIRE=1) : on
lève ce gate pour laisser la * chaîne RX native remplir DARAM 0x2a00 (+
d[3f92]) même sous shunt, afin * d’alimenter le vrai corrélateur DSP.
Réversible : sans l’env, inchangé. / { / @BEQUILLE — TPU_RX_WIRE (gate de livraison)
(CALYPSO_TPU_RX_WIRE, EXISTS, * defaut OFF ; calypso_wire.env:=1) *
masque : la fenetre RX du TPU (scenario TPU -> MOVE TSP -> IOTA
BDLENA) * n’est raccordee a rien, donc la livraison bufferisee reste
fermee * sous shunt et le correlateur natif reste affame. * retirer :
quand le sequenceur TPU produit le pulse BDLENA et que le BSP le *
consomme sans gate. * NB : ce gate se leve AUSSI via (DSP_RUN_C54X=1
&& BSP_DARAM_FORCE) — * mesure du 2026-07-28 : deliver ouvert
alors que le site du pulse * et la DMA de tache (calypso_trx.c) restent
fermes = wire a moitie * leve (d[0x3f92] non pose). / static int rxw
= -1; if (rxw < 0) { / [2026-07-28] coherence avec les gates
:474 et :997, qui se levent * deja avec CALYPSO_BSP_DARAM_FORCE. Sans
ca, DARAM_FORCE=1 ouvrait * 2 verrous sur 3 et la livraison restait
bloquee ici -> entree du * correlateur (0x4c00) gelee (mesure corr_iq
: 3 lectures identiques). * Defaut inchange : sans env, comportement
strictement identique. / const char rc =
getenv(“CALYPSO_DSP_RUN_C54X”); rxw = (getenv(“CALYPSO_TPU_RX_WIRE”) ||
(rc && *rc == ‘1’ && getenv(“CALYPSO_BSP_DARAM_FORCE”)))
? 1 : 0; if (rxw) fprintf(stderr, “[bsp] deliver: gate shunt LEVE
(rxw=1) —” “livraison DARAM active”); } if (calypso_dsp_shunt_active()
&& !rxw) return; }
if (!bsp.dsp || bsp.daram_addr == 0) return;
for (int tn = 0; tn < BSP_NUM_TN; tn++) {
/* Drain ALL matchable bursts per call (2026-05-29 fix anti-stale).
* Avant : 1 burst/appel → sous contention BQL le drain rate effectif
* tombe sous le rate d'arrivée IPC → queue fills → bursts > 64 FN
* derriere cur_fn marqués stale (= 87% drop observé).
* Maintenant : drain catch-up jusqu'à plus aucun match. Bornage
* via la fenêtre BSP_FN_MATCH_WINDOW dans bsp_take_for_fn — pas de
* runaway. */
/* TPU-RX-WIRE (RANK2, gated CALYPSO_TPU_RX_WIRE=1) : consume the BDLENA
* pulse the TPU RX window opened (TPU scenario -> TSP MOVE -> IOTA). The
* native consumer calypso_iota_take_bdl_pulse() had ZERO callers, so the
* RX-window -> BSP transfer was never wired : DARAM 0x2a00 stayed empty
* and the FB correlator ran on garbage. On a pulse for this TN :
* (a) queue the FB task in the DSP scheduler word : d[0x3f92] |= 0x0800
* — this is "wire d[3f92] via the TPU" : the RX window hands the FB
* task to the DSP scheduler (the native ORM at 0xa539 is skipped
* because d[5a00]==0x88, so nothing else ever sets it) ;
* (b) deliver the NEAREST buffered burst NOW (bsp_take_nearest), bypassing
* the FN-match window that never lands in full mode.
* The loop body still does the DARAM write, INT3 and BRINT0 assert. */
int rxwin = 0;
{
/* @BEQUILLE — TPU_RX_WIRE (consommation du pulse BDLENA) (CALYPSO_TPU_RX_WIRE,
* EXISTS, defaut OFF)
* masque : calypso_iota_take_bdl_pulse() n'a qu'un seul appelant, celui-ci.
* Le wire (a) consomme le pulse, (b) pose la tache FB dans le mot
* scheduler d[0x3f92] bit11 a la place de l'ORM natif 0xa539 jamais
* execute, (c) livre le burst le PLUS PROCHE en contournant la
* fenetre de match FN.
* retirer : quand d[0x3f92] est pose par l'ORM natif et que le match FN
* aboutit sans contournement.
*/
static int en = -1;
if (en < 0) en = getenv("CALYPSO_TPU_RX_WIRE") ? 1 : 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) {
/* 2026-05-29 : pas d'écriture d_dsp_page, juste INT3 (arm_done).
* Probe read-only voir commentaire dans calypso_bsp_rx_burst. */
if (bsp.dsp && bsp.dsp->api_ram) {
static uint32_t obs_n = 0;
uint16_t cur = bsp.dsp->api_ram[0x08E2 - 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);
}
}
/* Gate INT3 : skip si IFR.bit3 déjà set (cf rx_burst). */
{ static int _nat = -1; if (_nat < 0) _nat = (getenv("CALYPSO_FRAME_IT_NATIVE") || getenv("CALYPSO_DSP_FRAME_VEC28")) ? 1 : 0; int _fb = _nat ? 12 : 3;
if (bsp.dsp && bsp.dsp->running && !(bsp.dsp->ifr & (1 << _fb))) {
c54x_interrupt_ex(bsp.dsp, 19, 3);
if (bsp.dsp->idle) bsp.dsp->idle = false;
} }
int n = sl->n < (int)bsp.daram_len ? sl->n : (int)bsp.daram_len;
/* === SB-INPUT discriminator (phase-based, 2026-05-28 v2) ===
* GMSK = constant envelope → magnitude(I,Q) constant pour FCCH ET
* SCH. Le seul discriminant qui sépare est la trajectoire de phase :
* FCCH = tone pur → Δphase constant → cross[k]=I[k]*Q[k-1]-Q[k]*I[k-1]
* a même signe à tous les k (rotation monotone)
* SCH/NB = GMSK data → Δphase varie ±90°/sample → cross alterne
* Compteur de cross-product de même signe que cross[0] sur 10 paires :
* ≥9 same-sign → TONAL_FB
* ≤8 → MODULATED
* nmax conservé en plus pour détecter SILENT. Cap 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];
/* Use int64 to avoid overflow : I*Q up to 1G, diff up to 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);
}
}
/* ⚠️ TESTING 2026-05-29 : marqueur (cette fonction boucle-t-elle ?)
* + apply_phase ICI (delivery, dac courant) — théorie : le chemin
* vivant n'appliquait pas l'AFC sur les samples livrés au corrélateur. */
{
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++;
}
calypso_twl3025_apply_phase(sl->iq, sl->n / 2, sl->fn, (uint8_t)tn);
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);
/* ⚠️ TESTING : woff LOCAL (était static rolling cross-burst). */
unsigned woff = 0;
calypso_pcb_daram_lock_acquire();
for (int i = 0; i < n; i++) {
uint16_t a = (uint16_t)(bsp.daram_addr + woff);
/* HACK CALYPSO_BSP_INJECT_CANARY : overwrite avec marker 0xCAFE
* pour identifier le vrai buffer cible cote DSP via le hook
* canary-read en c54x. Voir doc/TODO.md. */
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++;
/* PROBE 2026-05-31 fork-1 : dump I/Q (chemin deliver_buffered, le VIVANT
* = samples post-AFC livrés au corrélateur). Gated CALYPSO_IQDUMP,
* compteur indépendant, préfixe iq_dlv. À RETIRER. */
if (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 (2026-05-28) =====================================
* Fire BRINT0 IRQ (vec 21, IMR bit 5) after DARAM write. Sur silicon,
* BSP DMA-complete declenche cette IRQ qui reveille le DSP et execute
* l'ISR a PROM1[0xFFD4 → CALL 0xf310]. Sans cet assert, le DSP ne sait
* jamais qu'un burst est disponible et reste dans son dispatcher loop
* polling data[0x3fab] eternellement (= 59M reads observed).
* Confirme par chain analysis 2026-05-28 :
* 1. Canary 0xCAFE prouve E2E BSP→DSP read at 0x2a00 (PC=0x93a5)
* 2. DSP polls *(0x3fab) bits via dispatcher table at data[0x16b3]
* 3. *(0x3fab) bits sont OR'ed par ISR triggered par BRINT0
* 4. Sans BRINT0 → pas d'ISR → pas de bit set → loop infini */
/* Gate : skip si BRINT0 précédent pas encore servi par DSP — évite
* iota pending queue overflow quand DSP traite ISR plus lentement
* que BSP rate (= GSM 217 Hz wall vs DSP-processed BRINT0). */
if (bsp.dsp && !(bsp.dsp->ifr & (1 << 5))) {
c54x_interrupt_ex(bsp.dsp, 21, 5);
}
/* RX-FBFLAGS (gated CALYPSO_RX_FBFLAGS) — GATE DEPUIS LE RX (remplace le
* poke c54x CALYPSO_FORCE_3FAE). Sur silicon, l'ISR BRINT0 (0xf310) OR les
* bits de handshake FB-det que le handler correlateur poll en boucle :
* data[0x3faa] bit2 (0x0004) + bit8 (0x0100) @0x886b/0x8885/0x8898
* data[0x3fab] bit8 (0x0100) @0x888d
* data[0x3fae] bit8 (0x0100) @0x90c8/0x90ed/0x9128
* L'ISR emulee ne les pose pas -> le handler boucle 0x90b0-0x9130 sans
* jamais atteindre le kernel. On les pose ICI, a la livraison du burst
* DARAM 0x2a00 (= "burst pret"), pour que le correlateur deroule. Le
* traceur CORR-FLOW dira si un gate SUIVANT apparait. */
{
/* @BEQUILLE — RX_FBFLAGS (chemin buffered) (CALYPSO_RX_FBFLAGS, EXISTS, defaut OFF)
* masque : les memes bits de handshake FB-det que l'ISR BRINT0 devrait poser,
* mais SANS data[0x3fad] bit15 -> ne peut pas deboucher le kernel.
* retirer : EN PREMIER — ce bloc est deja du code mort tant que
* CALYPSO_BSP_DIRECT_FEED=1 (la file bufferisee reste vide).
*/
static int _fbf = -1;
if (_fbf < 0) _fbf = getenv("CALYPSO_RX_FBFLAGS") ? 1 : 0;
if (_fbf && bsp.dsp) {
bsp.dsp->data[0x3faa] |= 0x0104; /* bit2 + bit8 */
bsp.dsp->data[0x3fab] |= 0x0100; /* bit8 (cible FBEN) */
bsp.dsp->data[0x3fae] |= 0x0100; /* bit8 (gate confirme 2026-07-25) */
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 : si BSP_DUMP_RX_FILE est set, append le burst brut
* (n int16_t LE I/Q interleaved) au fichier. Header 12B par burst :
* magic 'IQ16' (4B) | fn (4B LE) | tn (1B) | n_int16 (2B LE) | _pad (1B)
* Permet ensuite python3 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 = 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 + 0],
bsp.dsp->data[bsp.daram_addr + 1],
bsp.dsp->data[bsp.daram_addr + 2],
bsp.dsp->data[bsp.daram_addr + 3],
bsp.dsp->data[bsp.daram_addr + 4],
bsp.dsp->data[bsp.daram_addr + 5],
bsp.dsp->data[bsp.daram_addr + 6],
bsp.dsp->data[bsp.daram_addr + 7]);
calypso_pcb_daram_lock_release();
}
/* Fire BRINT0 */
if (bsp.dsp && !(bsp.dsp->ifr & (1 << 5))) {
c54x_interrupt_ex(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's sent — symmetric with the calypso-ipc-device
* UL print, so we can correlate L1 → bridge → BTS at the byte level
* when chasing TRXD framing or RACH parity issues. Cap at 200 to keep
* 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.
Cached via env var for ABI predictability — the old static-init+branch *
pattern was reportedly correlated with worse LU success rate vs explicit
* env set, so we now read env once and stash in bsp.* state at init.
/ #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 =
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, VALEUR, defaut unset = inerte)
* masque : l’incertitude sur l’offset NDB de d_rach : plutot que de lire
le * BSIC ecrit par le firmware, on impose celui du BSC pour prouver *
la chaine d’encodage RACH independamment de l’offset. * retirer : quand
CALYPSO_NDB_D_RACH_OFFSET est confirme (IMM_ASS_CMD recu * avec le BSIC
lu depuis d_rach, sans forcage). / static int rach_force_bsic(void)
{ static int cached = -2; if (cached != -2) return cached; const char
e = 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;
}
/* [2026-07-26 PORT LU] Emet un access-burst RACH UL depuis un
ra/bsic EXPLICITE * (bypass le read DARAM d_rach). Appele par le hook
write-d_rach de calypso_trx.c * sous SHUNT_LEGIT : la tache DSP
d_task_ra est avalee par le shunt et * calypso_bsp_tx_rach_burst ne tire
jamais. 1 appel = 1 vraie tentative RACH. / 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 -> pont g_bsp_fd */ return true; }
================================================================================
FILE: hw/arm/calypso/calypso_c54x.c SIZE: 934874 bytes, 17307 lines
================================================================================
/ calypso_c54x.c — TMS320C54x DSP emulator for Calypso
Minimal C54x core: enough to run the Calypso DSP ROM for GSM *
signal processing (Viterbi, deinterleaving, burst decode).
SPDX-License-Identifier: GPL-2.0-or-later */
#include “calypso_c54x.h” #include “calypso_arm2dsp.h” #include
“hw/arm/calypso/calypso_invariants.h” #include
“hw/arm/calypso/calypso_dsp_shunt.h” #include
“hw/arm/calypso/calypso_trf6151.h” #include
“hw/arm/calypso/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 du buffer corr / / [2026-07-27] DARAM-FNSTAMP :
publiees par calypso_bsp.c. */ extern unsigned calypso_daram_last_fn;
extern unsigned calypso_daram_wr_count;
extern int calypso_rxfb_fired; /* [probe golive] defini dans
calypso_bsp.c */
static int g_boot_trace = 0; /* VEC28-STACK-TRACE (2026-07-03, gated
CALYPSO_TRACE_VEC28_STACK, READ-ONLY diagnostic, * addendum 23). Traces
the software stack from vec28 interrupt entry (2-word PC+XPC push * by
c54x_interrupt_ex) through every RET-family instruction until SP returns
to the * pre-interrupt level, to confirm whether a 1-word RET/RCD/RETED
pop consumes the orphaned * XPC word (expected 0x0000) as if it were a
return PC – the suspected mechanism of the * PC=0x0000 derail (Addendum
22/23). No DSP state is modified, only observed. */ 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 par CALYPSO_DEBUG containing “C54X” or
“ALL”. * Pour gating fin par probe, utiliser C54_DBG(“PROBE_NAME”, fmt,
…). */ #define C54_LOG(fmt, …)
do { if (calypso_debug_enabled(“C54X”))
fprintf(stderr, “[c54x]” fmt “”, ##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; }
/* ================================================================ *
Memory access *
================================================================ */
/* Forward decl: used by data_write() VECDUMP at MMR_PMST. /
static uint16_t prog_read(C54xState s, uint32_t addr); static
uint16_t prog_fetch(C54xState *s, uint16_t pc);
/* Propagated by D_BURST_D probe, consumed by A_CD-BY-BURST
correlation. */ static uint16_t g_last_d_burst_d;
/* PROBE 2026-05-31 (review c web) : dernier PC/op ayant écrit chaque
slot de * pile (zone 0x1000-0x1FFF). Nomme le PSHM qui a posé l’orphelin
0x80fd lu par * POPM ST0 @0x94f3 →
matching-frame vs frame étranger. À RETIRER. */
/* === Generic watch-write zone helper (2026-05-15 matin) ===
Factorisation du pattern COEFFS-WR / A_CD-WR / … : pour chaque
zone * mémoire surveillée, maintient per-PC counter + log throttled +
summary * périodique. La 4e+ instrumentation devient triviale au call
site. Cost : 512 KB de statique par zone (per_pc[0x10000] ×
uint64_t). Acceptable * pour debug. */ 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;
static 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”, 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; / Format <NAME>-WR-SUMMARY
(avec -WR- infix) pour cohérence avec * les per-hit lines
<NAME>-WR #N et backward-compat 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, “”); } return true; }
/* === FB-det timing/content stats (2026-05-14 night) ===
Captures sur les ~928 fires de 0x8f51 (au lieu du cap 50 actuel) : * -
AR4 dans/hors zone [0x2bc0..0x2bff] → addressing vs timing * - delta
insn depuis dernier write par cluster (compute/clear/pattern) * -
histogramme val[AR4] : zero / 0xfffe sentinel / other Verdict
: * ar4_in_zone < 100% → bug d’addressing (AR4 pointe hors zone) *
delta_clear < delta_compute systématique → timing race (clear gagne)
* delta_compute >> 1M → compute jamais dans la fenêtre du fire *
other > 0 → certains sweeps voient des données — creuser ces fires
/ static struct { / Timing trackers (mis à jour dans data_write
côté 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 au
moment du fire 0x8f51 / 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 : un sweep = 50 fires 0x8f51
consécutifs avec AR3 * progressant 0..0x3A3 stride+19. Wrap (ar3 <
last_ar3) = nouveau sweep. */ uint16_t last_ar3_at_fire; uint64_t
sweep_id; uint64_t sweep_nonzero_count; } g_fb_det_timing;
/* === Generic ARn write tracer with provenance (2026-05-25 v3
unified) === * Tracer paramétré pour AR0..AR7. Remplace les ad-hoc
ar2/ar4. Env mask * CALYPSO_AR_TRACE (hex, default 0) : *
=0xFF → trace tous les AR0..AR7 * =0x14 → trace AR2 + AR4 seulement
(bits 2 et 4 set) * =0x04 → AR2 only * =0 → désactivé (zéro coût)
Hook : case MMR_AR0..AR7 dans data_write_locked. Skip
auto-modify * noise (Δ ∈ [-3, 3]). Classification opcode via decode +
flag ZERO * automatique (= suspect clobber MMR via STL A,AR-).
* Question résolue par cette sonde : qui pose ARn = mauvaise valeur ? *
STM-#lk → immediate hardcoded ROM (silicon-intentional, le fix * est
ailleurs : étape ultérieure qui ré-set manque) * MVDM-mem → load depuis
mem (slot uninit divergence QEMU vs silicon) * MVMM → copie d’un autre
AR (remonter le tracer sur source) * STM Smem → load mem indirect (idem
MVDM) * STLM-A → from accumulator A (vérifier d’où A vient) */ #define
AR_HIST_MAX 64 typedef struct { uint16_t pc; uint16_t op_last; uint16_t
val_last; uint32_t count; } ArEntry; static ArEntry
g_ar_hist[8][AR_HIST_MAX]; static unsigned g_ar_used[8] = {0}; static
unsigned g_ar_total[8] = {0}; static unsigned g_ar_mask = 0; static int
g_ar_enabled = -1; static unsigned g_ar_log_cap = 50;
static void ar_write_track(C54xState s, unsigned idx, uint16_t
new_val) { / AR3-PRELOAD (revival dsp 2026-06-22, read-only,
toujours actif, cape 120) : * capture les LOADS d’AR3 dans/autour du
buffer I/Q [0x2a00..0x2c00). Le * correlateur PC=0xee38 lit AR3=0x2b97
(HORS buffer, fin=0x2b28). ar_write_track * n’est appele QUE sur load
MMR (STM/STLM/MVDM), PAS sur auto-increment. * VERDICT : * - AR3 loade a
~0x2a00 (debut buffer) et AUCUN load >=0x2b28 ici -> le 0x2b97 *
vient d’INCREMENTS = boucle trop longue / buffer trop court = FIX B
(BSP). * - AR3 loade directement >=0x2b28 (flag OUT-OF-BUF) ->
instruction mal emulee * = FIX A (decodage c54x), avec le PC/op
coupable. / 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”,
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 = 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”, 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);
}
/* Distinction sémantique critique (cf review Claude web 2026-05-25 v2) :
* - STM-#lk = deliberate AR update via immediate hardcoded ROM
* (silicon-intentional, l'AR change par design firmware)
* - LD-#k = idem (small immediate)
* - STL-A / autres = side-effect d'un MMR write where AR happens to
* self-alias (= AR pointing at its own MMR slot).
* NOT an explicit AR update — coincidence pointer.
* Label clairement pour ne pas confondre les 2 dans le hunt. */
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 (2026-05-25 v3) === * Capture
le LAST WRITER de A via chokepoint top-of-loop : compare A *
iter-à-iter, si change → mémorise PC + op du writer. Quand un trigger *
PC fire (default = 0x9ac0 = STL A,AR2- clobber IMR), dump A + last
writer. Réponse à la question Claude web : A=0 délibéré (=
mask-all * design firmware) ou A=0 divergence (= A devait porter mask
valide) ? Env : CALYPSO_A_TRACE_PC=0x9ac0 (hex PC trigger,
default 0xFFFF=off) * Zéro coût si env non set. */ static int64_t
g_a_last_value = 0; static uint16_t g_a_last_writer_pc = 0; static
uint16_t g_a_last_writer_op = 0; static unsigned g_a_last_writer_insn =
0; static int g_a_trace_enabled = -1; static uint16_t g_a_trace_pc =
0xFFFF; static unsigned g_a_trace_hits = 0; static unsigned
g_a_trace_log_cap = 50;
static void a_track_init_lazy(void) { if (g_a_trace_enabled >= 0)
return; const char e = 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”,
g_a_trace_pc, g_a_trace_log_cap); } else { g_a_trace_enabled = 0; }
}
static void a_track_iter(C54xState s, uint16_t prev_pc, uint16_t
prev_op) { if (g_a_trace_enabled <= 0) return; / Detect A change
→ mémorise dernier 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 if (a) parmi les N premiers (contexte) OR (b)
A_low=0 (= cas * suspect STL clobber zone). Évite cap log silencieux qui
masque * les events critiques tardifs (cf cas insn=253328 IMR clobber).
*/ 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”, 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 trigger PC (2026-05-25 v4) === *
Capture AR6 + B + source provenance à chaque fire d’un PC trigger, *
fenêtré sur [insn_lo, insn_hi] pour éviter explosion log (le PC 0x821a *
fire 10M+ fois). Réponse à la question Claude web : aux fires qui *
clobber IMR à PC=0x821a, AR6 vaut 0 (= base divergence) ou 0x16 * (=
self-alias feedback) ? Tracking AR6’s last writer (= what set
AR6 to its current value) * via top-of-loop comparison (même pattern que
A tracer). Env : * CALYPSO_AR6_AT_PC=0x821a PC trigger *
CALYPSO_AR6_WIN_LO=3619500 insn window start *
CALYPSO_AR6_WIN_HI=3619810 insn window end (one outer-loop iter) *
CALYPSO_AR6_AT_LOG_CAP=200 max log lines (default 200) */ static
uint16_t g_ar6_last_value = 0; static uint16_t g_ar6_last_writer_pc = 0;
static uint16_t g_ar6_last_writer_op = 0; static unsigned
g_ar6_last_writer_insn = 0; static int g_ar6_at_enabled = -1; static
uint16_t g_ar6_at_pc = 0xFFFF; static unsigned g_ar6_at_win_lo = 0;
static unsigned g_ar6_at_win_hi = 0; static unsigned g_ar6_at_hits = 0;
static unsigned g_ar6_at_log_cap = 200;
static void ar6_at_init_lazy(void) { if (g_ar6_at_enabled >= 0)
return; const char e = 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 = getenv(“CALYPSO_AR6_WIN_LO”); const char hi =
getenv(“CALYPSO_AR6_WIN_HI”); const char cap =
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”, 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; }
}
static 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 within 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”, 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 hits counter (cheap probe, candidat 1 du doc §7). */
static uint64_t g_rsbx_intm_hits = 0; static int g_rsbx_intm_enabled =
-1;
/* DISP-ENTRY discriminateur (c web 2026-05-29) : capture du contexte
de * préemption d’IT, pour trancher “dispatcher atteint via vecteur IT
avec DP * foreground sale” (b) vs “DP clobbé” (a). C54x n’empile PAS
ST0/DP à l’IT → * l’ISR hérite du DP du code interrompu. Si les entrées
dispatcher KO * (DP≠0x124) corrèlent avec une IT récente → préemption
confirmée. / static uint64_t g_last_intr_insn = 0; / insn_count
de la dernière IT servie / static int g_last_intr_vec = -1; /
vecteur de la dernière IT / static uint16_t g_last_intr_fg_pc = 0;
/ PC foreground préempté / static uint16_t g_last_intr_fg_dp =
0; / DP foreground préempté / / dernier LDP (qui a posé
DP) + PC prédécesseur (comment on arrive à 0x8341) / static uint16_t
g_last_ldp_pc = 0; / PC de l’instruction qui a posé DP / static
uint16_t g_last_ldp_val = 0; / valeur DP posée / static int
g_last_ldp_kind = 0; / 1=LDP#k(5902) 2=LDP#k9(6262) 3=LD
Smem,DP(7049) / static uint16_t g_prev_pc = 0; static uint16_t
g_prev_op = 0; / opcode insn precedente - bc TC-fiable apres
cmpm/bitf (2026-06-23) / / PC de l’instruction exécutée juste
avant / static uint16_t g_last_st0w_pc = 0; / PC du dernier
write ST0 entier (POPM ST0/STLM) / static uint16_t g_last_st0w_val =
0; / valeur ST0 restaurée / static uint16_t g_last_st0w_op = 0;
/ opcode de l’instruction qui écrit ST0 / static uint16_t
g_last_st0w_xpc = 0; / XPC au moment du write (0xf48b dépend de
XPC) / static uint16_t g_last_st0w_prev = 0; / PC prédécesseur
du write (comment on y arrive) */
/* === ST0 push/pop ring (C-sweep 2026-05-30, gated DISP-ENTRY)
============ * Capture PSHM ST0 (push) + POPM/STLM ST0 (write) dans un
ring. Dumpé au * dispatcher BAD (lut != 0xff72) pour discriminer le DP
périmé en 3 branches : * - dernier PUSH val=0x3124 mais POP=0x3125 →
CLOBBE pile entre push/pop * (famille SP/circulaire) * - dernier PUSH
val=0x3125 → DP déjà faux au push (LDP sauté en amont) * - pas de PUSH
ST0 apparié au POP → désalignement SP (pop lit autre slot) / typedef
struct { uint16_t pc, op, val, sp; char kind; } St0Ev; / kind
P=push p=pop L=ldp / #define ST0_RING_N 24 static St0Ev
g_st0_ring[ST0_RING_N]; static unsigned g_st0_ring_idx = 0; static int
g_st0_ring_on = -1; static 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++; } / SURGICAL
2026-05-30 : slot LUT lu à l’entrée dispatcher 0x834d (capture *
silencieuse, pour épingler le DP coupable du self-CALA 0x70c3 sans le *
spam de DISP-TRACE qui décale le timing et masque le bug). / static
uint16_t g_disp_lut_ea = 0; static uint16_t g_disp_lut_val = 0; /
SURGICAL 2026-05-30 : ring des évènements SP (push/pop) au chokepoint *
unique de la boucle run. Sur tout changement de SP on enregistre * {pc,
op, delta}. Dumpé par BLACKHOLE-CALA → nomme la source récurrente * de
drain (push jamais dépoppé). Écritures array only = ~zéro coût. */
struct sp_evt { uint16_t pc; uint16_t op; int16_t delta; uint16_t sp; };
static struct sp_evt g_spring[64]; static uint32_t g_spring_idx = 0;
/* === SHADOW STACK (pairing push/pop, 2026-05-30, c-web) === *
Miroir logique de la pile DSP : à chaque PUSH (CALL/CALLD/PSHM/IRQ →
SP-) * on empile {pc,op,kind} ; à chaque POP
(RET/RETD/RETE/FRET/RETED/POPM → SP+) * on dépile et on VÉRIFIE
l’appariement. Un POP sur shadow VIDE = return SANS * call apparié = LA
source de l’over-pop (lit la pile vierge au-dessus de SP_base). * On
nomme ce return orphelin (PC/op/SP), ce que les 15 victimes 0xc8be ne
disent * pas. Gated CALYPSO_DEBUG=ORPHAN. kind: ‘C’=call ‘P’=pshm/pshd
‘I’=irq ‘R’=reti. * Array-only quand off → ~zéro coût. / struct
shadow_ent { uint16_t pc; uint16_t op; uint16_t sp; char kind; };
#define SHADOW_N 512 static struct shadow_ent g_shadow[SHADOW_N]; static
int g_shadow_depth = 0; / nb de mots actuellement empilés (logique)
/ static int g_shadow_on = -1; / -1 = pas encore résolu le gate
/ static uint64_t g_orphan_hits = 0; / nb de POP-orphelins
détectés / static uint64_t g_mismatch_hits = 0;/ nb de POP avec
kind mismatché */
/* Tracker stores directs zone pile [0x1100..0x1140] (AU-DESSUS de
SP_base) : * ces slots ne sont JAMAIS écrits par un push (la pile
descend SOUS 0x1100), * donc uniquement par un ST direct. Discrimine
vecteur-init LÉGIT (slot écrit * par le firmware) vs slot VIERGE (jamais
écrit = vrai over-pop garbage). */ #define STKSLOT_LO 0x1100 #define
STKSLOT_HI 0x1140 #define STKSLOT_N (STKSLOT_HI - STKSLOT_LO + 1) static
uint16_t g_stkslot_wpc[STKSLOT_N]; static uint16_t
g_stkslot_wop[STKSLOT_N]; static uint8_t
g_stkslot_written[STKSLOT_N];
static 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)”); } } 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 !”, (unsigned long
long)g_rsbx_intm_hits, s->insn_count, s->pc, s->st1); } } }
/* === AR4 write tracer with provenance (2026-05-25) === * Hooke
chaque write vers MMR_AR4 (= 0x14) via data_write_locked. * Logge (insn,
PC, opcode courant, val écrite, ancien AR4) + tente une * classification
de provenance via decode opcode : * STM #lk, ARn (0x77yx) → immediate
value depuis next prog word * STLM src,ARn (0x84yx) → from accumulator
A/B low * MVDM dmad,ARn (0x86yx) → from data memory absolute * MVDD/MVMM
(autres) → from another register * Flag SUSPECT si nouvelle valeur AR4 ∈
[0x2b80..0x2c00] (observed bug * zone) ou [0x3fb0..0x3fbf] (BSP buffer
area). Env CALYPSO_AR4_TRACE=1. Question critique (cf Claude
web) : provenance = const/mem/register ? * Si AR4 vient d’un mem load
(LDM/MVDM), le corrupter remonte au TCB * en mémoire — pas l’instruction
qui charge AR4, mais le TCB lui-même * (potentiellement uninitialized
faute de re-init firmware sautée). / / === SP absolute-write
tracer (2026-05-25 — nohack hunt) === * Logge chaque write SP via
STL/STM/STLM absolute, FRAME #imm, MVMM * register transfer —
c’est-à-dire les sites où SP est téléporté à une * valeur
arbitraire, par opposition aux PUSH/POP/CALL/RET qui sont des * inc/dec
de 1. Si un site téléporte SP=0x3fbe, on tient le corrupter * exact du
bootstub-entry observé à insn=3995013. Hooké aux 3 sites
identifiés : * L1218 : data_write_locked case MMR_SP — STL/STM/STLM to
MMR_SP * L3875 : F7Dx case 0xD — LD #k8u, SP * L4285 : MVMM register
transfer — dst==8 (SP via MMR enc 3-bit) Env-gated
CALYPSO_SP_ABS_TRACE=1, zéro coût si OFF. * Limite N premiers writes
verbatim + histo per-PC (cap SP_ABS_HIST_MAX). / #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; static SpAbsEntry g_sp_abs_hist[SP_ABS_HIST_MAX]; static
unsigned g_sp_abs_used = 0; static unsigned g_sp_abs_total = 0; static
int g_sp_abs_enabled = -1; static unsigned g_sp_abs_log_cap = 50;
static 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”, g_sp_abs_log_cap, SP_ABS_HIST_MAX); } } if
(g_sp_abs_enabled <= 0) return; g_sp_abs_total++; / Per-PC histo
/ 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”, 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 if SP lands in suspect zone
(0x3fb0..0x3fbf = BSP read region * OR 0x2b80..0x2c00 = 0xfd2a A=AR4
historical) */ 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 ?)”,
s->insn_count, s->pc, new_val); } }
/* === MVPD overlay occupancy trace (2026-05-25) === * Bucket writes
à data[0x0080..0x27FF] en buckets de 0x80 words. * Dump occupancy à la
fin de la boot phase (insn cap) ou périodiquement. * Objectif :
identifier quelles sub-ranges de [0x0080..0x27FF] sont * chargées par
MVPD au boot (= code overlay). Critique pour décider si * BSP buffer
peut vivre dans la read-region [0x0000..0x03A3] sans * écraser du code
en cours d’exécution. * Env gates : * CALYPSO_MVPD_TRACE=1 active
(default OFF) * CALYPSO_MVPD_BOOT_LIMIT=N cap insn pour dump (default
500000) / #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) static uint32_t g_mvpd_buckets[MVPD_BUCKETS_N]; /
80 buckets */ static int g_mvpd_trace_enabled = -1; static unsigned
g_mvpd_boot_limit = 0; static int g_mvpd_dumped = 0;
static 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 = 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”,
MVPD_RANGE_LO, MVPD_RANGE_HI, MVPD_BUCKET_SZ, MVPD_BUCKETS_N,
g_mvpd_boot_limit); } }
static 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]++; }
static 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)”, 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”, lo, hi,
g_mvpd_buckets[b]); } /* Verdict pour decision buffer placement : si
[0x0080..0x03A3] * (correlator read region, bucket 0..6) a peu/zéro
writes → safe * pour BSP DMA. Sinon il faut une autre zone. */ 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”, 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 (2026-05-25 — pour run 0x6000 dual-purpose)
=== * Capture AR3/AR4/AR5 à l’entrée du correlator FB-det + les data
reads * pendant son exécution. Objectif : valider empiriquement que le
firmware * lit son input I/Q dans [0x0000..0x03A3] (assertion TODO.md:13
jamais * exercée avec real data — l’A/B précédent mesurait WR-SITE
pré-BSP). * Env-gated CALYPSO_CORRELATOR_TRACE=1, zéro coût si OFF.
Correlator range : [0x8d00..0x9000] (FB-det handler PROM0).
2026-05-25 night : range étendu de 0x8F80 → 0x9000. Évidence
runtime * (d_fb_det WATCH-READ) montrait des reads à PC=0x8FAC et 0x8FB5
qui * étaient HORS l’ancien filtre → CORR-ENTRY=0 alors que firmware
FAIT * des accès dans la zone FB-det. Range élargi pour capturer ces
hits. À l’entrée from-outside : log AR0..7, SP, ST0/1. *
Pendant exec : log les data_read addr (top N uniques, capped pour *
éviter explosion log sur runs longs). / #define CORR_PC_LO 0x8d00
#define CORR_PC_HI 0x9000 / exclusif / #define
CORR_READ_HIST_MAX 128 typedef struct { uint16_t addr; uint32_t count; }
CorrReadEntry; static CorrReadEntry
g_corr_read_hist[CORR_READ_HIST_MAX]; static unsigned g_corr_read_used =
0; static int g_corr_trace_enabled = -1; / -1 uninit, 0 off, 1 on
/ static unsigned g_corr_entry_count = 0; static unsigned
g_corr_entry_log_cap = 100000; / uncap : voir le par-frame post-+3s
*/
/* Posés par calypso_trx.c quand l’ARM écrit d_task_md=5 (commande
FB). * La sonde D_TASK_MD-RD timestampe les reads DSP par rapport à ce
write * (test H1 : EA write ARM vs EA read DSP + ordre). / uint32_t
g_arm_taskmd5_insn = 0; uint16_t g_arm_taskmd5_ea = 0; static uint64_t
g_corr_read_total = 0; static uint16_t g_corr_last_pc = 0xFFFF; /
track PC transitions */
static 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”,
CORR_PC_LO, CORR_PC_HI, CORR_READ_HIST_MAX, g_corr_entry_log_cap); }
}
/* CORR-ENTRY tracker : appelé au top-of-loop pour chaque insn
dispatch. * Détecte transition PC out→in du range FB-det. Log les
premières N * entrées avec contexte AR/SP/ST. / static 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”, 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 (c web reframe 2026-05-25 night2)
======== Sondes diagnostiques POST-désassemblage fc50-fc6f,
sans aucun fix. * Trois questions à trancher : * A) B @ PC=0xfbd9
vaut-il une valeur cohérente avant SUB #8, B, A ? * (= si B
faux upstream, F2xx fix donne A faux downstream) * B) A @ PC=0xfbdb
juste après SUB → AR4 setup à PC=0xfbf3 → corruption ? * C) Le bit 4 du
flag 0x3DC0 (= testé par BITF à fc63) est-il jamais set ? * Si jamais
set par aucune routine DSP → BCD NTC à fc66 toujours branche → *
fc50-fc6f loop forever sans toucher au body. Env-gated :
CALYPSO_FBDB_PROBE=1 active toutes les sondes. * Coût off : 1 compare +
1 branch par opcode (négligeable). */ static int g_fbdb_probe_enabled =
-1; static unsigned g_fbdb_probe_log_cap = 100; static unsigned
g_fbdb_probe_count_b = 0; static unsigned g_fbdb_probe_count_a = 0;
static unsigned g_fbdb_probe_count_fbf3 = 0; static unsigned
g_addr3dc0_wr_count = 0; static 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)”); } }
/* Hook called from c54x_run top-of-loop, before c54x_exec_one. /
static 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”, 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”, 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”,
g_fbdb_probe_count_fbf3, (unsigned long long)a, (unsigned)(a &
0xFFFF), s->insn_count); } }
/* === STUCK-STATE PC+XPC histogram (c web reframe 2026-05-25 night3)
===== Sonde diagnostique : quand le DSP est en “stuck state”
(= INTM=1 ET * BRINT0 pending dans IFR), on enregistre PC+XPC. Permet
d’identifier * la VRAIE boucle de blocage XPC-qualifiée — sans présumer
que c’est * fc50 ou autre PC particulier. Le PC HIST
classique ne distingue pas les pages XPC (= ambiguous “fc50” * peut être
page 0x1F mirror ou 0x28/0x38 etc.). Entry/exit du stuck
state logué (= delimit la fenêtre). * Top-20 PC+XPC dump périodique (=
quand stuck dure). Env-gated CALYPSO_STUCK_PROBE=1. Coût off
: 1 bit-check + 1 branch. */ #define STUCK_HIST_SIZE 64 typedef struct {
uint16_t pc; uint8_t xpc; uint32_t count; } StuckHistEntry; static
StuckHistEntry g_stuck_hist[STUCK_HIST_SIZE]; static unsigned
g_stuck_hist_used = 0; static int g_stuck_probe_enabled = -1; static int
g_stuck_active = 0; static uint32_t g_stuck_duration = 0; static
uint64_t g_stuck_start_insn = 0; static 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”);
} }
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:”, 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”, i + 1, g_stuck_hist[i].pc, g_stuck_hist[i].xpc,
g_stuck_hist[i].count); } }
/* === FORCE-INTM-ONESHOT (c web reframe 2026-05-25 night4)
=============== Sonde d’arbitrage : quand INTM=1 ET BRINT0
(IFR bit 5) pending, * forcer UNE SEULE FOIS INTM=0 pour permettre
dispatch. Observer ce * qui se passe ensuite via les tracers existants
(CORR-ENTRY, a_sync_demod * writes, RETE log, INTM-TRANS).
Partitionne l’arbre : * - snr/toa réels après dispatch → INTM est le
SEUL blocker * - garbage / rien → aval cassé aussi (≥2 bugs) OU
corruption ISR state IMPORTANT : c’est une SONDE
diagnostique, PAS un fix. Le one-shot * permet d’observer sans masquer
un comportement régulier. Si on confirme * “INTM est le seul blocker”,
le vrai fix sera côté ISR (= pourquoi pas * de RETE), pas un INTM-clear
systématique. Env-gated CALYPSO_FORCE_INTM_ONESHOT=1. /
static int g_force_intm_oneshot_enabled = -1; static int
g_force_intm_oneshot_done = 0; static uint64_t g_force_intm_oneshot_insn
= 0; / CALYPSO_FORCE_INTM_AT_PC=0xfc6f : restreindre le force au PC
donné (= * point sûr = RET du compute kernel fc50 par exemple). 0xFFFF
sentinel * = pas de restriction PC (= comportement v1 = première
opportunité). */ static 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 ; defaut OFF ; calypso_wire.env:=1) *
masque : le RSBX INTM 0xa51b que le firmware ne joue pas. Avec le
PC-gate le * bloc POSE EN PLUS l’IT (s->ifr |= s->imr &
0x3000) : il fabrique * l’evenement, il n’ouvre pas seulement la
fenetre. * retirer : quand INTM passe a 0 par le chemin ROM (trace
INTM-TRANS). * NB : run.sh le signale deja comme “NON-nominal”. /
static void force_intm_oneshot_check(C54xState s) { if
(g_force_intm_oneshot_enabled < 0) { const char e =
getenv(“CALYPSO_FORCE_INTM_ONESHOT”); / Gate PROPRE : ON seulement
si =1 ; =0 ou unset -> OFF. (NB : ce oneshot * masque le livelock
vec28 en clearant INTM 1x ; utile tant que le sur-fire * frame-IT n est
pas corrige a la racine BSP.) / g_force_intm_oneshot_enabled = (e
&& e == ‘1’) ? 1 : 0; /* Optional PC gate : si
CALYPSO_FORCE_INTM_AT_PC=0xXXXX présent, * fire seulement quand PC
matche. Permet de départager state- * corruption vs aval-cassé per c web
: force à un PC sûr (= RET * fc6f, idle dispatcher, etc.) au lieu de
mid-compute fc57. / const char pc_e =
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é)”, 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)”); }
} } 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] FAIRE FIRE L IFR.
Au go-live (0xa4e1) IMR=0x3000 (bit frame * demasque) mais IFR=0 ->
aucune IT pending, clearer INTM ne fait rien. Le * frame-IT n est jamais
latche (modele IT c54x incomplet). Donc AU PC cible : * on FORCE l IFR
frame pending (IFR |= bits demasques) PUIS on clear INTM -> * l IT
part. Sans PC-gate : ancien comportement (fire sur IT deja pending).
/ 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; /
rien de demasque a forcer / s->ifr |= unmasked; / <– pose
l IT frame pending / } 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”,
(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”, s->st1); }
/* === INT3 cycle tracer + control-flow signature (c web reframe
2026-05-25 night5) Sonde décisive pour départager F1 (=
pourquoi ISR INT3 ne RETE pas). Per cycle INT3 : * - START :
INT3 dispatched (vec=19) → reset trace, log cycle_id + entry PC * -
DURING : chaque branch conditionnelle exécutée → (PC, op, target, taken)
* - END (good) : RETE fire → dump trace tagged GOOD + insn count * - END
(orphan) : nouveau INT3 dispatch avant RETE → dump previous tagged *
ORPHAN-NEXT-INT3 + reason Diff offline good_cycle vs
orphan_cycle → 1ère branche qui diverge * = trigger du bug. À cette
branche, lire l’état testé = la vraie cause. Cappé 256
branches/cycle (= overflow tagué pour borne). * Env-gated
CALYPSO_INT3_CYCLE_TRACE=1. / #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; static Int3BranchEvent
g_int3_trace[INT3_BRANCH_TRACE_MAX]; static unsigned g_int3_trace_count
= 0; static int g_int3_trace_overflow = 0; static int
g_int3_cycle_active = 0; static uint64_t g_int3_cycle_id = 0; static
uint16_t g_int3_cycle_entry_pc = 0; static uint64_t
g_int3_cycle_entry_insn = 0; static 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.”, 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.
/ static 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) :”, (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”, 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”, (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. /
static 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.
/ static 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”, (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”, 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. / static 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”, (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”, (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. / static 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”, addr, old_val, new_val, set_mask, pc, insn, (set_mask & 0x0010)
? “** BIT 4 SET ***” : ““); } }
static 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”, addr, val, !!(val &
0x0010), pc, insn); } }
static 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++;
} }
static 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”, 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”, k + 1, g_corr_read_hist[k].addr,
g_corr_read_hist[k].count); } }
/* === DSP throughput emission (2026-05-14 evening) ===
Émet
[c54x] INSN-COUNT-STATS total=N delta=N elapsed_ms=N rate=N/s
toutes * les 1M insn. Lu en stéréo par : * - test_dsp_throughput_5x
(milestones, static) * - test_dsp_throughput_above_threshold
(observability, runtime) * Seuil pytest : 50M/s (marge ×2 sous les
100M/s historiques). */ #include <time.h> static struct { uint64_t
last_logged_insn; struct timespec last_logged_ts; } g_throughput;
static 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”,
(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 (2026-05-14
evening) === Cible : identifier la zone DARAM lue par la
routine FB-det sans préjuger. * Compteurs cumulatifs par plage +
snapshot/delta à chaque “trigger PC” * (sites qui écrivent d_fb_det,
identifiés par grep ZERO-WR + WR-SITE). Plages mutuellement
exclusives : * RR_MMRS [0x0000..0x005F] registres MMR C54x * RR_LOW
[0x0060..0x03A3] zone correlator linéaire (hypothèse 05-14) * RR_APIRAM
[0x0800..0x27FF] API RAM partagée ARM/DSP (hypothèse β) * RR_TARGET
[0x3FB0..0x3FFF] où BSP DMA écrit par défaut * RR_WRAP [0xFC5D..0xFFED]
zone correlator wrap BK=176 (AR2/AR7) * RR_OTHER tout le reste (incluant
overlay 0x80..7FF, debord 0x4000+, etc.) Trigger PCs : 5
sites observés écrivant d_fb_det (4 ZERO-WR rares + 0x8f51 * en boucle
50 fois). Le delta entre 2 triggers consécutifs = reads * cumulés dans
la fenêtre amont. Cap à 200 triggers loggés pour ne pas * flooder. */
enum { RR_MMRS, RR_LOW, RR_APIRAM, RR_TARGET, RR_WRAP, RR_OTHER, RR_NUM
};
static struct { uint64_t cumulative[RR_NUM]; uint64_t
snapshot[RR_NUM]; uint64_t trigger_count; } g_read_stats;
static 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]++; }
static void read_stats_trigger_check(C54xState s) { /
Trigger PC réduit à 0x8f51 uniquement (FB-det compute loop, 50
hits/sweep). * 2026-05-14 — Run précédent : trigger list large {0x8f51,
0x778a, 0x9ac0, * 0x9ad0, 0x9b00, 0x821a} → 0x821a en boot mailbox poll
loop (14 insns * entre hits) a dévoré les 200 lignes de cap avant que
0x8f51 ne fire. * Les autres PCs étaient init/reset (1-3 hits chacun sur
tout le run). * Cap remonté à 5000 pour couvrir plusieurs sweeps FB-det.
*/ 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”, (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 racine without spéculation. */ #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;
static XferLog g_xfer_ring[NOP_RING_N]; static unsigned g_xfer_idx;
static AWriteLog g_awrite_ring[NOP_RING_N]; static unsigned
g_awrite_idx; static int g_nop_tripped;
static 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”;
}
static 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++; }
static 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. / static inline int
pc_in_nop_region(const C54xState s, uint16_t pc, uint8_t xpc) { if
(xpc != 0) return 0; /* banque sup : géré ailleurs / if (pc >=
0x7000) return 0; / PROM0 + PROM1 mirror = valid / if
((s->pmst & PMST_OVLY) && pc >= 0x80 && pc
< 0x2800) return 0; / OVLY DARAM mapping = valid */ return 1;
}
static 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("================================================");
}
static uint16_t data_read_locked(C54xState s, uint16_t addr);
/ [2026-07-28] DEMODIO : prototype — le helper est defini plus bas
mais * data_read, qui l appelle, vient avant. / static void
dio_note(C54xState s, const char *rw, uint16_t addr, uint16_t
val);
/* FBWATCH (2026-05-30 soir) : sonde FB-dispatch gatée par une env
DÉDIÉE * (CALYPSO_FBWATCH=1), résolue UNE fois en static int → check int
cheap, PAS * via calypso_debug_enabled (master-gate reste 0, 127 gates
court-circuités → * QEMU temps-réel → mobile vivant). Déclaré ici (avant
data_read_locked qui * l’utilise). Filme : page-read / dispatch 0x833b /
0x9ac0 / d_fb_det / canary. */ static int g_fbwatch_on = -1;
static uint16_t data_read(C54xState s, uint16_t addr) { /
[2026-07-26 golive-mac] WATCH-9F00-RD : l’etage demod deroule 0x9f00 *
ecrit son resultat en 0x2a00 (workzone). Son ENTREE = ce qu’il LIT hors
* du workzone. On trace les lectures quand PC in [0x9f00..0x9fb8] et
addr * HORS 0x2a00..0x2b27 pour localiser le vrai buffer IQ source (a
nourrir a * la place de 0x2a00). Gate CALYPSO_WATCH_9F00_RD. /
/ [2026-07-27] FB-STREAM : injecte un echantillon FCCH FRAIS a
chaque lecture * de la cellule sample du demod (0x9213 I / 0x9215 Q)
-> vraie fenetre dans * 0x2a00, sans dependre de la cadence. Modelise
le DMA on-chip. Gate CALYPSO_FB_STREAM. / / [2026-07-27]
cellules I/Q du demod configurables : WATCH-9F00-RD a montre * que le
demod lit 0x9260/0x9261 (pas 0x9213/0x9215). CALYPSO_FB_STREAM_CELL(Q).
*/ static uint16_t _fscI = 0, _fscQ = 0; if (_fscI == 0) { const char *c
= getenv(“CALYPSO_FB_STREAM_CELL”); _fscI = c ? (uint16_t)strtol(c,
NULL, 0) : 0x9213; const char *q = 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 (lecture)
(CALYPSO_FB_STREAM, defaut OFF) * masque : l absence de DMA on-chip. On
INJECTE un echantillon frais a * chaque lecture de la cellule au lieu qu
un peripherique * remplisse le tampon. * retirer : quand le tampon est
alimente par le vrai chemin (BSP -> DARAM). * Note : inerte avec
CORR_ENTRY=0x94f5 — les cellules 0x9260/61 n y sont * jamais lues
(mesure 2026-07-28, WATCH-9F00-RD). */ if (_fs < 0) _fs =
getenv(“CALYPSO_FB_STREAM”) ? 1 : 0; if (_fs) { static uint16_t _si,
_sq; static int _hv = 0; uint16_t _rv; if (addr == _fscI) { _hv =
calypso_dsp_shunt_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”, 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 = getenv(“CALYPSO_WATCH_9F00_RD”) ? 1 : 0; if (_r9) {
/* Lectures du chemin actif (0x9f00..0x9fb8) SANS exclusion : localise
les * cellules SOURCE lues avant le fill workzone 0x2a00 (0x9213/0x9215
IQ ?). */ 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”,
s->pc, addr, s->data[addr], s->insn_count); } } /* Correlator
read tracer (env-gated CALYPSO_CORRELATOR_TRACE=1). * Record addr
seulement quand PC ∈ [CORR_PC_LO..CORR_PC_HI) (FB-det range). * Range
étendu 2026-05-25 night à 0x8d00..0x9000 (cf comment block au L639). *
Lazy-init ici plutôt qu’au top-of-loop pour rester centralisé. * Coût
quand OFF : 1 compare + 1 branch (g_corr_trace_enabled). / if
(g_corr_trace_enabled > 0 && s->pc >= CORR_PC_LO
&& s->pc < CORR_PC_HI) { corr_read_record(addr); } /
IQ-READ tracer (2026-05-30) : qui lit le buffer DMA BSP [0x2a00..0x2b27]
? * Confirme que le corrélateur FB consomme bien la vraie I/Q écrite par
le * BSP, et à quel PC (= le vrai site corrélateur). Cap 60, ~zéro coût
hors zone. / if (addr >= 0x2a00 && addr < 0x2b28
&& s->data[addr] != 0) { static unsigned iqr = 0, iqseen = 0;
iqseen++; / boot (first 60) + DÉTECTION : tire aussi 1/8000 après
insn>50M pour * voir ce que le corrélateur lit VRAIMENT à l’instant
FB-det (insn~71M), * pas seulement le buffer stale du boot (2026-06-02).
/ if (iqr < 60 || (s->insn_count > 50000000u &&
(iqseen % 8000) == 0)) { uint16_t val = s->data[addr]; / A/B
(accumulateurs corr complexe, sign-ext 40b) + valeurs aux * AUTRES
pointeurs (candidats réf cos/sin) PENDANT la lecture I/Q. / 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”, 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 (2026-06-02, spec CC-web) : à l’instant détection, *
dumpe UNE fois le span contigu data[0x2a00..0x2a1f] (ce que le *
corrélateur PEUT lire) ET bsp_buf[0..31] (ce que le BSP a écrit). * data
span = waveform & bsp_buf = waveform -> buffer OK -> bug =
AR3/corrélateur (lit que [0]). * data span = [0] puis DC, bsp_buf =
waveform -> livraison PORTR ne copie que [0] (stride/len). * bsp_buf
= DC -> conversion cs16 BSP tronque. */ 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, “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, “”); } } /* MTTCG : protège DARAM
access (DSP + ARM-OVLY peuvent racer). * Sans MTTCG : mutex non contesté
(overhead minimal). */ 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);
/* [2026-07-28] RMAP : carte agregee des adresses LUES par les PC d
une plage. * Symetrique de WMAP. Voir en-tete du patch rmap.py. */
#define RMAP_PCS 48 static struct { uint16_t pc; uint32_t n; uint16_t
a[8]; uint8_t na; uint16_t amn, amx; } g_rmap[RMAP_PCS]; static int
g_rmap_n; static uint32_t g_rmap_tot; static int g_rmap_on = -1; static
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”, 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, “”); } }
static void rmap_note(uint16_t addr, uint16_t pc) { if (g_rmap_on
< 0) { const char e = getenv(“CALYPSO_RMAP”); g_rmap_on = (e
&& atoi(e) > 0) ? 1 : 0; const char lo =
getenv(“CALYPSO_RMAP_PCLO”), *hi = 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”, 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) {
rmap_note(addr, s->pc); { / [2026-07-28] WZREAD : voir en-tete
du patch (gate CALYPSO_WZWRITE). */ static int _wr = -1; static unsigned
_wrn = 0; if (_wr < 0) _wr = getenv(“CALYPSO_WZWRITE”) ? 1 : 0; if
(_wr && addr == 0x2c00 && s->pc == 0xa07c &&
_wrn < 40) { /* v3 : seule la lecture du noyau MAC nous interesse
(0x9aba = boucle * de normalisation, bruyante et deja caracterisee en
B4B). */ _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”,
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); } } { /*
[2026-07-28] DMAWATCH (lecture) : voir en-tete du patch. */ static int
_dw2 = -1; static unsigned _dwr = 0; if (_dw2 < 0) _dw2 =
getenv(“CALYPSO_DMAWATCH”) ? 1 : 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”, addr, _nm, s->data[addr], s->pc,
s->insn_count); } } { /* [2026-07-28] DEMODRD : voir en-tete du
patch. */ static int _dr = -1; static unsigned _drn = 0; if (_dr < 0)
_dr = getenv(“CALYPSO_DEMODRD”) ? 1 : 0; if (_dr && _drn < 60
&& s->pc == 0x9fb5) { /* seule la lecture des echantillons */
_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”, 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); } } { /* [2026-07-27] SLOTSRC-RD : quelle adresse
contient le stub 0xab38 ? */ static int _sr = -1; static unsigned _srn =
0; if (_sr < 0) _sr = getenv(“CALYPSO_SLOTSRC”) ? 1 : 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”, 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 (2026-07-02, gated
CALYPSO_MEM_WATCH_2B80) : le correlateur * FB (PC=0xee38) lit
data[0x2b97] via AR3 (STM hardcode ROM), region * [0x2b80,0x2c00)
distincte du buffer DMA BSP [0x2a00,0x2b28). Hypothese : * cette region
n’est jamais peuplee (reste a 0) -> le MAC accumule zero en * boucle.
Log tout READ dans cette fenetre (valeur, PC), cap 200. / if (addr
>= 0x2b80 && addr < 0x2c00) { static int mw2b80_en = -1;
if (mw2b80_en < 0) mw2b80_en = getenv(“CALYPSO_MEM_WATCH_2B80”) ? 1 :
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”, addr, s->data[addr],
s->pc, s->insn_count); } } } / PROBE 2026-05-31 frame-IT :
valeur FIGÉE des flags polled + qui polle. * La valeur lue (jamais
changée) = ce que le BSP doit produire/toggler. / 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”, addr, s->data[addr], s->pc,
(uint16_t)(s->a & 0xFFFF), !!(s->st0 & ST0_TC),
s->insn_count); fr_n++; } / one-shot : dump overlay de la loop
0x010b (hors dump ROM) + état IT * (IFR/IMR/INTM) pendant le spin →
tranche source-dead vs IMR-masqué * vs INTM-collé (review c web). /
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”, 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):”); for (uint16_t a = 0x0100; a
<= 0x0118; a++) fprintf(stderr, “[c54x] prog[0x%04x]=0x%04x”, 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”)) { / UNCAP (gated par token)
+ EA/page/val + Δ depuis le write ARM=5. * Si après le write (Δ>0) la
valeur lue ≠ 5 → le 5 n’atteint pas cette * EA : compare ARM5_EA (EA
écrite par l’ARM) vs EA (EA lue par le DSP). * - mêmes EA, val≠5 →
timing/ordre (read avant write dans la frame) * - EA ≠ ARM5_EA (stride
page) → parité de flip w_page/r_page. / 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”, 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 (générique, gated CALYPSO_DEBUG=WATCH-RD + env *
CALYPSO_WATCH_RD_ADDR=0xNNNN) : log toute LECTURE d’une adresse data *
arbitraire (PC + valeur lue + insn) — voir QUI lit une cellule *
pointeur-dispatcher (ex. 0x3af7) et AVEC QUELLE valeur (0 avant *
peuplement vs valeur valide après). Symétrique du WATCH-WR. / {
static int watch_rd_addr = -1; if (watch_rd_addr < 0) { const char
e = 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) : le busy-loop dispatcher (d1xx↔︎da0d) * polle
la zone flag DARAM[0x60-0x70]. On veut voir EN STEADY-STATE (post *
+1.9s) CE qu’il lit (quel slot) et si ce flag devient jamais non-zéro *
(= qqn le pose). Throttlé : 1ère/40000 par slot pour éviter l’explosion
* du busy-loop, + TOUTE valeur non-zéro (l’évènement qui compte). /
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”, 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 : DSP lit
db_w->d_burst_d ? * 0x0801 (W_PAGE_0 + offset 1), 0x0815 (W_PAGE_1 +
offset 1). * Si DSP read voit 0,1,2,3 séquentiel → ARM écrit
correctement db_w. * Si DSP read voit 0 toujours → ARM ne configure pas
burst_id. * Si DSP ne lit jamais → DSP ne consulte pas db_w pour le
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”, 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 toutes les 50000
reads : histogramme valeurs lues / 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”, page, (unsigned long
long)dbw_total[page], (unsigned long long)dbw_per_val[page][0],
(unsigned long long)dbw_per_val[page][1], (unsigned long
long)dbw_per_val[page][2], (unsigned long long)dbw_per_val[page][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 pour identifier la routine PM. Deux ranges : *
[0x3fb0..0x3fbf] = buffer BSP (samples I/Q) * [0x3dcf..0x3dd5] = buffer
scratch dominant (78k+52k reads observés) * Compte par PC, dump top-10
toutes les 50k reads dans chaque range. * Plus compteur d’entrée par PC
dominant pour distinguer *”PM cassée” vs “PM jamais appelée” (vu IRQ
rate 1.5 Hz). / 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, “”); } } 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, “”); } } /* === CANARY-READ probe
(2026-05-28 method 3) === * Quand CALYPSO_BSP_INJECT_CANARY=1, le BSP
overwrite tous les samples * avec 0xCAFE. Si le DSP lit 0xCAFE depuis
une addr, c’est que cette * addr est dans son chemin de lecture des
samples = la vraie addr cible * pour CALYPSO_BSP_DARAM_ADDR. Trace cap
100 pour eviter le flood. */ 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”, 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”, 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 (anti-spam) : premiers 60 + tout CHANGEMENT de valeur. Le spin
* bootloader 0xb424 lit data[0x0fff]=0x0001 1.36 Md de fois → l'ancien
* `%10000` crachait 135k lignes (20 Mo, QEMU ramé, osmocon LOST). On garde le
* signal exact (transition IDLE 0x0001 → commande 0x0002/0x0004) sans le bruit. */
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 — REAL DSP word address is 0x08F8.
* Mapping: ARM 0xFFD001F0 (BASE_API_NDB 0xFFD001A8 + 36 words × 2)
* = DSP word 0x0800 + 0x1F0/2 = 0x08F8.
* Earlier 0x01F0 was the ARM byte-offset, NOT a DSP word address —
* watching it logged unrelated DARAM 0x01F0 (junk). Now we trace
* the real slot the firmware polls. */
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);
}
}
/* === DIAG-FORCE-DARAM62 ===
* Pinned diag (env-gated, default OFF) : when set, override the read
* of daram[0x62] inside the dispatcher loop (PC ∈ 0xCC62..0xCC6F) to
* return 1 instead of the actual stored value. Goal: force the
* dispatcher's "branch if flag != 0" to fire and observe whether the
* DSP escapes the loop and jumps to api[0x1f0c]=0x770c (the dispatch
* target). Three outcomes (binary diagnostic) :
* - PC leaves cc62..cc6f → 0x770c and new code paths run :
* loop hypothesis correct, flag is the gate, INT3 ISR is the
* missing writer (next step: trace writes to confirm).
* - PC reaches 0x770c then returns to cc62 immediately :
* flag is set but handler bails because something else missing
* (a_cd[] init, NDB cell, ...).
* - No change : branch / compare is more subtle than read.
* This is a force-test, not a fix — remove or env-leave-off after. */
/* === DSP idle dispatcher trace (PC ∈ 0xCC62..0xCC6F) ===
* The DSP gets stuck in this PROM0 loop polling task slots. Dump the
* exact (PC, addr, value, AR2..AR5) for the first N reads so we can
* see WHICH memory location the dispatcher inspects to decide whether
* to branch out (task_md ? db_r ? api_ram ? other ?). Capped to keep
* log size manageable.
*
* Captures all reads (DARAM + API RAM + MMR) so we don't miss the
* critical poll address. */
/* === BOOT-POLL-RD probe (2026-05-28) ===
* Trace data reads in the boot polling loop (DARAM 0x00ed..0x00ff =
* OVLY mirror of PROM0[0x70ed..0x70ff]). Per CLAUDE.md DSP_ROM_MAP :
* "0x7026-0x71FF = Boot polling loop (writes API RAM tables)".
* Goal : identifier QUELLE adresse le firmware polle pour décider
* de sortir → quel signal devrait être émis par notre émulateur
* peripherals pour débloquer légitimement (au lieu du drift AR1 bug).
* 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 probe (2026-05-28) ===
* Trace data reads in the FB-det correlator inner body
* (PROM0[0x9aba..0x9abf] = RPTBD body of publish routine at 0x9aaf+).
* Goal : identifier QUELLE zone DARAM le correlateur lit. Si addr ∈
* [0x3fb0..0x3fbf] → bonne zone (BSP RX), valeur = sample. Si addr
* ailleurs (low DARAM, MMR, ...) → bug d'addressing : correlateur
* regarde un buffer vide → A reste à 0 → publish 0 → no lock.
* 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);
}
}
}
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 (2026-05-28) ===
* Histogramme des reads addr ∈ [0x4000..0xFFFF] (zone haute DARAM).
* Complementaire au DARAM RD HIST existant (low DARAM). Goal :
* identifier sur quelles addresses upper le DSP polle ses samples.
* Permet de trouver la vraie zone DARAM cible pour
* CALYPSO_BSP_DARAM_ADDR sans brute-force. Dump top-16 tous les
* 100k reads. Skip les 1M premieres insn pour eviter le bruit boot. */
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");
}
}
}
/* === BSP discovery: trace data reads in FB-det handler ===
* Wide range over the PROM0 user-code area: handler PCs observed in
* timeout traces cluster around 0x7e92..0x7eb8 (the FB-det inner
* loop), so we widen the catch zone to 0x7000..0x7fff. */
/* FB-det / dispatcher subroutine trace.
* The 0x7e80..0x7eb8 wrapper CALLS into 0x81a5/0x81c8 with AR5=0x0e4c
* (the FB sample buffer). Cover both ranges to catch both wrapper
* polls and inner correlator reads. Skip 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 0x08E2 (d_dsp_page) */
if (addr == 0x08E2) {
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 PRODUCTEUR : le DSP re-lit-il d_dsp_page PAR-FRAME ? (env one-shot) */
if (g_fbwatch_on < 0) g_fbwatch_on = getenv("CALYPSO_FBWATCH") ? 1 : 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);
}
}
/* 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++;
}
}
/* [2026-07-26 WF golive-handshake] PROBE-3FAD-GATE : capture EXACTE de la
* valeur lue par le BITF *(0x3fad),#0x8000 @0x8753 (le SEUL verrou du
* dispatcher FB : CC 0xa0a0 -> kernel 0xa076). f930=CC (call cond, TC=1),
* donc les CALL 0x90b0/b8/c8/ed/914d reviennent : rien ne bloque le sweep,
* seul bit15 de 0x3fad decide. Si val&0x8000 ici => TC=1 => kernel PRIS.
* Si val&0x8000==0 alors qu'RX-FBFLAGS l'a pose => un clearer per-frame
* (XPC=0 0xace8/0xad04/0xad24 ou overlay XPC=2 0x28040/0x282d0 : 76f8 3fad
* 0000) l'a efface entre l'ecriture BSP et le sweep DSP. Cap 200, gate
* CALYPSO_PROBE_3FAD_GATE. Zero cout/effet quand OFF. */
if (addr == 0x3fad && s->pc == 0x8753 && calypso_rxfb_fired) { /* [fix v3] gate = RX-FBFLAGS a REELLEMENT pose 3fad bit15 (definitif) */
static int p3_en = -1; static unsigned p3_n = 0;
if (p3_en < 0) p3_en = getenv("CALYPSO_PROBE_3FAD_GATE") ? 1 : 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);
}
/* [2026-07-26 FIX v3] le clearer per-frame (76f8 3fad 0000) efface bit15
* entre l ecriture BSP et ce sweep -> on le RE-POSE ici, sur le chemin de
* LECTURE DSP, quand RX-FBFLAGS l a arme -> le BITF @0x8753 voit TC=1 ->
* CC 0xa0a0 -> kernel 0xa076. Gate CALYPSO_FORCE_3FAD_KERNEL. */
/* @BEQUILLE — FORCE_3FAD_KERNEL (CALYPSO_FORCE_3FAD_KERNEL, EXISTS, defaut OFF)
* masque : le producteur du flag "burst pret" data[0x3fad] bit15. Un clearer
* per-frame (76f8 3fad 0000) l'efface entre l'ecriture BSP et le sweep
* DSP ; on le RE-POSE sur le chemin de lecture @0x8753.
* retirer : quand le poseur natif (chaine RX/BRINT0) tient bit15 jusqu'au BITF
* @0x8753 — c.-a-d. quand PROBE_3FAD_GATE voit bit15=1 sans ce gate.
* NB : conditionne par calypso_rxfb_fired, pose par CALYPSO_RX_FBFLAGS.
*/
{ static int _fk = -1; if (_fk < 0) _fk = getenv("CALYPSO_FORCE_3FAD_KERNEL") ? 1 : 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 2026-05-30) : capture les
data_write vers * la zone pile, pour nommer AU POPM ST0 @0xf48b qui a écrit le slot lu (clobber * sous
SP, famille circulaire/dual-operand). No-fire au dump = personne n’a *
écrit le slot dans la frame → slot STALE → SP désaligné (POP sans PUSH).
/ typedef struct { uint16_t addr, val, pc, op; } StkwEv; #define
STKW_RING_N 48 static StkwEv g_stkw_ring[STKW_RING_N]; static unsigned
g_stkw_idx = 0; static 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 = getenv(“CALYPSO_ORPHAN”) ? 1 : 0; /*
env dédiée (hors CALYPSO_DEBUG → master reste 0, anti-Heisenbug) /
if (!g_orphan_on) return; if (addr < 0x1000 || addr > 0x6000)
return; / zone pile (SP dérive 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 : nomme le CALL/push qui pose la valeur
orpheline (défaut * 0x3125, override CALYPSO_TRACK_STKVAL=0xNNNN) →
corréler avec RCD@0x765c *
et POPM@0xf48b en ordre
insn. / { static int tval = -2; if (tval == -2) { const char te
= 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”, val, addr, s->pc,
prog_fetch(s, s->pc), s->sp, s->insn_count); } }
static void data_write(C54xState s, uint16_t addr, uint16_t val)
{ / [2026-07-27] WATCH-ACD : le DSP (opcode) ecrit-il a_cd
(0x9D2..0x9E0) et * clobbe-t-il l ecriture DIRECTE du shunt (SI1-4)
apres un reset ? Gate CALYPSO_WATCH_ACD. */ if (addr >= 0x09D2
&& addr <= 0x09E0) { static int _wac = -1; if (_wac < 0)
_wac = getenv(“CALYPSO_WATCH_ACD”) ? 1 : 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”,
addr, val, s->data[addr], s->pc, s->insn_count); } } /*
[2026-07-26 golive-mac] WATCH-2A00 : trace toute ecriture OPCODE vers le
* buffer IQ 0x2a00..0x2a07 (capture si le DSP lui-meme repose 0x12ed
apres * feed_iq). feed_iq ecrit s->data[] EN DIRECT (hors data_write)
-> invisible * ici : si 0x12ed apparait ici c’est un writer opcode
DSP. s=%p pour comparer * avec le pointeur feed_iq. Gate
CALYPSO_WATCH_2A00. */ if (addr >= 0x2a00 && addr <=
0x2a07) { static int _w2a = -1; if (_w2a < 0) _w2a =
getenv(“CALYPSO_WATCH_2A00”) ? 1 : 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”, addr, val,
s->data[addr], s->pc, (void)s, s->insn_count); } } /
[2026-07-27 golive-mac] WATCH-9200 : les cellules 0x9210-0x9218 /
0x9260-0x9261 * sont lues par le demod (0x9fab-0x9fb5) comme source IQ
mais restent CONSTANTES * (0xff06/0x04a3) -> workzone 0x2a00 plat.
Trace TOUTE ecriture opcode vers cette * region pour voir si qqun
l’alimente per-frame (et d’ou). Gate CALYPSO_WATCH_9200. */ if ((addr
>= 0x9210 && addr <= 0x9220) || (addr >= 0x9260
&& addr <= 0x9262)) { static int _w92 = -1; if (_w92 < 0)
_w92 = getenv(“CALYPSO_WATCH_9200”) ? 1 : 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”, addr,
val, s->data[addr], s->pc, s->insn_count); } } /* [2026-07-26
golive-mac] WATCH-RESULT : trace les ecritures OPCODE vers les *
cellules resultat FB natif (= adresses shunt_legit, confirmees osmocom
NDB) : * d_fb_det 0x08F8, a_sync_demod TOA/PM/ANGLE/SNR 0x08FA..0x08FD.
Montre le * SHADOW que le correlateur natif produit, meme quand d_fb_det
reste 0. * Gate CALYPSO_WATCH_RESULT. */ if (addr >= 0x08F8
&& addr <= 0x08FD) { static int _wr = -1; if (_wr < 0) _wr
= getenv(“CALYPSO_WATCH_RESULT”) ? 1 : 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”, addr, _nm,
val, s->data[addr], s->pc, s->insn_count); } } /* [2026-07-25]
WATCH-0810 (gated CALYPSO_WATCH_0810) : trace toute ecriture * DSP-side
a data[0x0810] (d_ctrl_system / B_TASK_ABORT bit15) avec le PC * auteur.
Complete ARM-WRITE-0810 (cote trx). Ensemble : QUI repose bit15 * apres
le clear ARM ? (self-clear DSP @0xa549
attendu ; le wire CTRLSYS * ecrit s->data[] directement, hors
data_write -> invisible ici = c’est LUI * le re-setter s’il
n’apparait pas). Cap 200. / if (addr == 0x0810) { static int w810 =
-1; if (w810 < 0) w810 = getenv(“CALYPSO_WATCH_0810”) ? 1 : 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”, val, s->data[0x0810], s->pc,
s->insn_count); } } / [2026-07-25] RANK1b FIX (workflow overlay,
context-safe) : le prologue ISR * overlay 0x013b fait POPD (0x3fcd)
= depile l’adresse de retour HW 0x72d5 dans data[0x3fcd], puis 23
PSHM (sauvegarde contexte), puis PSHD (0x3fcd) + RET qui
DEPILE data[0x3fcd] = retourne a 0x72d5 -> reboucle sur le *
scheduler 0x7234 (self-loop). Le chemin NATIF doit retourner a 0xa4e4 *
(init lineaire ORM/RSBX/IMR -> dispatcher 0x8341 -> LUT FB ->
correlateur * AR3=0x2a00). On substitue la valeur ecrite par 0x013b
UNIQUEMENT (pas * l’epilogue 0x011e qui ecrit aussi 0x3fcd) : les 23
PSHM s’executent, SP * reste equilibre, le RET depile 0xa4e4 ->
chemin natif SANS storm (vs le * PC-redirect qui sautait le contexte).
Gate CALYPSO_FIX_3FCD (def off). / if (addr == 0x3fcd &&
s->pc == 0x013b) { / @BEQUILLE —
FIX_3FCD (CALYPSO_FIX_3FCD, EXISTS, defaut OFF ; calypso_wire.env:=1) *
masque : le prologue ISR overlay 0x013b depile une adresse de retour HW
* (0x72d5) au lieu de 0xa4e4 ; la branche reelle = un vectoring *
d’interruption qui empile la bonne adresse de retour. * retirer : des
que le frame-IT vectorise vers 0xa4e4 sans substitution * (data[0x3fcd]
vaut 0xa4e4 sans le gate). */ static int f3f = -1; if (f3f < 0) f3f =
getenv(“CALYPSO_FIX_3FCD”) ? 1 : 0; if (f3f && val != 0xa4e4) {
static unsigned f3n = 0; if (f3n++ < 8) fprintf(stderr, “[c54x]
FIX-3FCD : data[0x3fcd] 0x%04x -> 0xa4e4” “(retour natif go-live, pas
0x72d5 self-loop) PC=0x013b insn=%u”, val, s->insn_count); val =
0xa4e4; } }
/* WRITE-WATCH (2026-06-24, RO) : qui ECRIT 0x434f (FIFO write ptr),
* 0x434e (FIFO read ptr), 0x3f6d (soft-vector go-live) — avec quel PC.
* Tranche empiriquement ECRITURE-ACTIVE (par un chemin) vs residu/defaut :
* Garde1 = FIFO init-vive vs jamais-setup ; Garde2 = soft-vector pose
* activement vers 0xa4df vs valeur de reset. NE JAMAIS deduire ces valeurs
* de la narration quand ce watch peut les LIRE. Cap 80. */
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 (2026-06-24, RO) : data[0x3f70] bit1 = le flag qui fait sortir la
* wait-loop (test 0xa4d4 -> RET si TC). Log UNIQUEMENT quand bit1 (0x0002) est
* ECRIT -> si ca ne fire JAMAIS, le DSP ne pose jamais "frame prete" = racine
* confirmee. Writers de 0x0002 : 0x710c/0xa5bd/0xb3ef/0xde9c. Cap 40. */
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 (2026-06-24, RO) : la table de vecteurs runtime vit en DARAM
* a 0x0080 (IPTR=1, OVLY). Mesure live : vec19(FRAME)@0xcc et vec21(BRINT0)
* @0xd4 sont des stubs RETE(0xf4eb)/NOP a froid, alors que vec17/20/22/24..
* portent de vrais handlers (FB 0xf8xx). Question falsifiable (H-A vs H-B) :
* le firmware installe-t-il JAMAIS un vrai branch au mot-0 de FRAME/BRINT0,
* et a quel PC/insn ? Watch les 8 mots des 2 slots + tout mot-0 de la table
* [0x80..0xFC] ou un FB-branch (0xf8xx) est ecrit. Cap 200. */
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 (revival dsp 2026-06-22, RO) : qui sème data[0x4c5b/0x4c5c]
* (table de tâches lue par LD *(0x4c5c),A puis CALA ; devrait venir du
* handler FRAME 0xA04C). 0 write = jamais semée → CALA A=0 → 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);
}
/* === SENTINELLE cohérence ARM<->DSP (CALYPSO_FBDET_SENTINEL=1) ===
* Force toute écriture DSP de d_fb_det (0x08f8) à 0xDEAD. L'ARM lit ce mot
* via arm=0x01f0 -> s->dsp->data[0x08f8] (calypso_trx.c). La sonde "ARM RD
* d_fb_det" (token TRX) montre alors :
* ARM lit 0xDEAD -> mémoire COHÉRENTE (même cellule) -> H1 mort, c'est H3 (PM/seuil).
* ARM lit 0x0000 -> DÉSYNC ARM<->DSP -> H1 confirmé.
* NE PAS combiner avec CALYPSO_FORCE_TOA (qui override la lecture 0x01f0). */
{
static int sent = -1;
if (sent < 0) { const char *e = 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"); }
/* [2026-07-26] SHUNT_LEGIT : override le clobber natif de d_fb_det. Le DSP
* natif ecrit 0x08f8=0 (pas de detection, mur RANK3) et ecrase la detection
* gr-gsm transportee par le shunt. Quand SHUNT_LEGIT + gr-gsm a decode
* (sb_valid), on FORCE la valeur ecrite a 1 -> l ARM lit FB found -> vrai flux. */
{
/* @BEQUILLE — SHUNT_LEGIT (force d_fb_det cote ecriture DSP) (CALYPSO_SHUNT_LEGIT
* ou CALYPSO_SHUNT_NO_LEGIT =1)
* masque : le mur RANK3 — le correlateur natif ecrit data[0x08f8]=0 et ecrase
* la detection transportee depuis gr-gsm. On FORCE a 1 toute ecriture
* DSP de d_fb_det des que sb_valid.
* retirer : quand le correlateur natif pose d_fb_det lui-meme.
*/
static int _lg = -1;
if (_lg < 0) { const char *e = getenv("CALYPSO_SHUNT_LEGIT"); const char *nl = getenv("CALYPSO_SHUNT_NO_LEGIT"); _lg = ((e && *e=='1') || (nl && *nl=='1')) ? 1 : 0; }
if (_lg && addr == 0x08f8 && calypso_dsp_shunt_sb_valid()) {
val = 1;
}
/* [2026-07-26 RANK5] force a_pm (rxlev) sur le VRAI array lu par
* l'ARM : calypso_trx.c lit s->dsp->data[off/2+0x800], PAS api_ram.
* a_pm read page 0 = data[0x830..0x832], page 1 = data[0x844..0x846]
* (ARM off 0x60/0x88 -> /2+0x800). Le DSP les ecrit a 0 (pas de vraie
* mesure) -> on force la valeur calibree trf6151 -> rxlev stable. */
{
/* @BEQUILLE — TRF_RXLEV + TRF_TARGET_RF (CALYPSO_TRF_RXLEV=1, fallback
* CALYPSO_SHUNT_LEGIT=1 ; cible CALYPSO_TRF_TARGET_RF, defaut -60)
* masque : a_pm que le DSP ecrit a 0 (aucune mesure de puissance). On substitue
* apm_for_rf(TARGET_RF) sur le tableau reellement lu par l'ARM
* (data[0x834-0x836] / [0x848-0x84A]). Le niveau RF cible est une
* constante decretee, pas une mesure.
* retirer : quand a_pm natif est non nul. Le modele trf6151 (gain suivi par TSP)
* reste legitime — seule la CIBLE figee est la bequille.
* NB : ce site ne teste PAS SHUNT_NO_LEGIT ; c'est shunt_no_legit.env qui
* pose TRF_RXLEV=1 explicitement.
*/
static int _tp = -1, _tgt = -60;
if (_tp < 0) {
const char *d = getenv("CALYPSO_TRF_RXLEV");
const char *l = getenv("CALYPSO_SHUNT_LEGIT");
const char *t = getenv("CALYPSO_TRF_TARGET_RF");
_tp = ((d && *d=='1') || (l && *l=='1')) ? 1 : 0;
if (t && *t) _tgt = atoi(t);
}
/* DSP 33-36 : db_r = {..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[base0x28+12+0x800]=data[0x834..0x836] ; page 1 =
* data[0x3C+12+0x800]=data[0x848..0x84A]. (0x830/0x844 = a_serv_demod!) */
if (_tp && ((addr >= 0x0834 && addr <= 0x0836) ||
(addr >= 0x0848 && addr <= 0x084A))) {
val = calypso_trf6151_apm_for_rf(_tgt);
}
}
}
if (sent && addr == 0x08f8) {
uint16_t orig = val;
if (sent == 1) val = 0xDEAD; /* mode FORCE (test cohérence) */
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 : ring écritures pile */
/* ORPHAN : tracker stores directs zone [0x1100..0x1140] (vecteur légit vs
* vierge). Slots au-dessus de SP_base → jamais touchés par un 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);
}
/* ANGLE-WR tracer (2026-05-30) : qui écrit a_sync_demod[ANGLE]=0x08fc
* (et TOA=0x08fa, SNR=0x08fd) = la SORTIE du vrai détecteur de fréquence
* FCCH. Capture A/B (corr complexe) + réf (AR3/4/5) au store → le vrai
* site de corrélation fréquentielle. Cap 40. */
/* FBMODE-WR : qui écrit d_fb_mode (0x08f9) et avec quelle valeur (large
* vs étroit) ? Si bascule étroit après le rejet boot → plus de cold-acq. */
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, one-shot, hors master-gate) === */
if (g_fbwatch_on < 0) g_fbwatch_on = getenv("CALYPSO_FBWATCH") ? 1 : 0;
if (g_fbwatch_on) {
/* (1) qui ÉCRIT le flag dispatch FB (slot 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) le DSP écrit-il une détection FB (d_fb_det 0x08f8 non-zéro) ? */
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) LE FLAG PRODUCTEUR : qui écrit data[0x585f] (mot d'état foreground/ISR) ?
* bit7 (0x0080) pollé par le foreground @0xf7af, bit8 (0x0100) par les ISR.
* Si jamais écrit / bit7 jamais posé = le producteur du flag manque. */
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 ***" : "");
}
/* la table de dispatch est-elle peuplée ? data[0x4c5b] = cible BACC A @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);
}
}
if (addr >= 0x08fa && addr <= 0x08fd) {
static unsigned aw = 0;
if (aw < 40) {
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 : voir data_read ci-dessus. */
qemu_mutex_lock(&calypso_pcb_daram_lock);
data_write_locked(s, addr, val);
qemu_mutex_unlock(&calypso_pcb_daram_lock);
}
/* [2026-07-27] FLOWTRACE : voir en-tete du patch. / static FILE
g_flow_f = NULL; static long g_flow_budget = -2; static 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 = 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”, 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”, 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”); }
}
/* [2026-07-28] WMAP : carte agregee des ecrivains d une plage
data[]. * Voir en-tete du patch wmap.py — un agregat, pas un flux :
aucun plafond de * lignes, donc aucune fenetre a rater (les 3 sondes
precedentes ont toutes ete * tronquees). Sortie periodique : PC, n,
valeurs distinctes, min/max. */ #define WMAP_PCS 64 static struct {
uint16_t pc; uint32_t n; uint16_t v[8]; uint8_t nv; uint16_t mn, mx;
uint16_t addr0; uint32_t n0; } g_wmap[WMAP_PCS]; static int g_wmap_n;
static uint32_t g_wmap_tot; static int g_wmap_on = -1; static 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”, 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”, 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”); } }
/* v3 : battement — prouve que la sonde est VIVANTE meme a zero
ecriture. * Appele sur chaque write hors plage ; n imprime qu une fois
par tranche de * 5e6 writes globaux, en rappelant le total DANS la plage
(0 = vrai 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)“, (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 = getenv(“CALYPSO_WMAP”);
g_wmap_on = (e && atoi(e) > 0) ? 1 : 0; const char lo =
getenv(“CALYPSO_WMAP_LO”), hi = 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 =
getenv(“CALYPSO_WMAP_LO2”), *hi2 = 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”, 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; /* v2 : cellule temoin figee */
}
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;
/* v2 : le critere de SIGNAL est la variation dans le TEMPS a adresse FIXE. */
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;
}
/* v3 : seuil bas + tick temporel, pour que l ABSENCE soit mesurable. */
#define WMAP_TICK 2000 if (++g_wmap_tot % WMAP_TICK == 0)
wmap_dump(); }
/* [2026-07-28] DEMODIO : voir en-tete du patch demodio.py. */ static
int g_dio_on = -1; static uint64_t g_dio_after; static unsigned g_dio_n;
static uint16_t g_dio_pclo, g_dio_pchi;
static void dio_init(void) { const char e =
getenv(“CALYPSO_DEMODIO”); g_dio_on = (e && atoi(e) > 0) ? 1
: 0; const char a = getenv(“CALYPSO_DEMODIO_AFTER”); g_dio_after =
a && a ? strtoull(a, NULL, 0) : 40000000ULL; const char
lo = getenv(“CALYPSO_DEMODIO_PCLO”), *hi =
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”, 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; fprintf(stderr, “[c54x] DEMODIO %s PC=0x%04x op=0x%04x
addr=0x%04x val=0x%04x” “A=%lld B=%lld T=0x%04x AR2=%04x AR3=%04x
AR4=%04x AR5=%04x insn=%u”, rw, s->pc, prog_fetch(s, s->pc), addr,
val, (long long)a, (long long)b, s->t, 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) { dio_note(s, “W”, addr, val); wmap_note(addr, val,
s->pc); flow_log(“W”, addr, val, s->pc, s->insn_count); {
/ [2026-07-28] WZWRITE : qui remplit l entree du noyau ? (voir
en-tete) */ static int _wz = -1; static unsigned _wzn = 0; if (_wz <
0) _wz = getenv(“CALYPSO_WZWRITE”) ? 1 : 0; if (_wz && addr ==
0x2c00 && (s->pc == 0x9fd5 || s->pc == 0x9ab1) &&
_wzn < 40) { /* v3 : filtre par PC PRODUCTEUR — 0xa03d/a042/a079
(init MAC) exclus, * ils saturaient le plafond et masquaient les bursts
suivants. */ _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”, 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); } } { /*
[2026-07-28] ERRWATCH : qui pose d_error_status ? (voir en-tete) */
static int _ew = -1; static unsigned _ewn = 0; if (_ew < 0) _ew =
getenv(“CALYPSO_ERRWATCH”) ? 1 : 0; /* v3 : sur d_error_status (0x08D5)
on ignore les ecritures de 0 — * elles sont le nettoyage de boot et
consommaient tout le plafond. */ 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”,
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); } } { /* [2026-07-28]
DMAWATCH (ecriture). */ static int _dw3 = -1; static unsigned _dww = 0;
if (_dw3 < 0) _dw3 = getenv(“CALYPSO_DMAWATCH”) ? 1 : 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”, addr, val, s->data[addr],
s->pc, s->insn_count); } } { /* [2026-07-28] BOOTCMD cote DSP :
qui ecrase la commande ? */ static int _bc2 = -1; static unsigned _bc2n
= 0; if (_bc2 < 0) _bc2 = getenv(“CALYPSO_BOOTCMD”) ? 1 : 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”, addr,
s->data[addr], val, s->pc, s->insn_count, (addr == 0x0FFF) ? ”
<<<< CELLULE DE COMMANDE” : ““); } } { /* [2026-07-27]
DISPTAB-WR : qui remplit la table de dispatch ? */ static int _dt = -1;
static unsigned _dtn = 0; if (_dt < 0) _dt =
getenv(“CALYPSO_DISPTAB”) ? 1 : 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”, addr, val, s->pc, s->insn_count); } } /* [2026-07-27]
DEMOD-NOCLOBBER (gated CALYPSO_DEMOD_NOCLOBBER) : l etage * demod emule
(PC 0x9fb8=I / 0x9fe2=Q) remplit le buffer d entree du * correlateur
avec des paires CONSTANTES (0000,52ed) — prouve par la trace * de flux —
ecrasant la vraie FCCH deposee par feed_iq (FB_IQ_DARAM=1). * On ignore
ces ecritures : feed_iq devient autoritaire sur 0x2a00. / { /
@BEQUILLE — DEMOD_NOCLOBBER
(CALYPSO_DEMOD_NOCLOBBER, atoi>0, defaut OFF) * masque : l’etage
demod emule ecrit des paires constantes dans le buffer * d’entree
correlateur [0x2a00,0x2b28) et ecrase la FCCH reelle * deposee par
feed_iq ; la vraie branche = un demod qui consomme * l’I/Q RX au lieu de
produire des constantes. Ici on SUPPRIME * l’ecriture (return) au lieu
de corriger le producteur. * retirer : des que l’etage demod
0x9f95-0x9fe2 lit une source I/Q reelle, ou * des que FB_IQ_OWNS=1 rend
feed_iq seul proprietaire de 0x2a00. */ static int _nc = -1; if (_nc
< 0) { const char *e = 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)”, s->pc, addr, val); return; } } /*
MEM-WATCH-2B80 (2026-07-02, gated CALYPSO_MEM_WATCH_2B80) : voir le *
commentaire jumeau dans data_read_locked. Log tout WRITE dans *
[0x2b80,0x2c00), cap 200 – confirme/infirme si un boot-copy peuple *
jamais cette region avant que le correlateur la lise. / /
[2026-07-27] B4 (gated CALYPSO_B4) : watchpoint d_fb_det (0x08f8) ->
distingue * “le DSP ecrit 0” (correlateur conclut negatif) de “jamais
ecrit” (chemin non * atteint). Deux bugs differents. */ if (addr ==
0x08f8) { static int _b4 = -1; static unsigned _b4n = 0; if (_b4 < 0)
_b4 = getenv(“CALYPSO_B4”) ? 1 : 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”, s->data[0x08f8], val, s->pc,
s->xpc, s->insn_count); } } /* [2026-07-27] B1 boot-copy watch
(gated CALYPSO_B1) : ecritures dans la * table de reference
[0x2c00,0x2c10) avec PC source -> confirme (ou non) la * boot-copy
0x76f8->0x2c00 et QUI l ecrit. */ if (addr >= 0x2c00 &&
addr < 0x2c10) { static int _b1w = -1; static unsigned _b1wn = 0; if
(_b1w < 0) _b1w = getenv(“CALYPSO_B1”) ? 1 : 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”, addr, s->data[addr], val, s->pc, s->xpc,
s->insn_count); } } if (addr >= 0x2b80 && addr <
0x2c00) { static int mw2b80_wen = -1; if (mw2b80_wen < 0) mw2b80_wen
= getenv(“CALYPSO_MEM_WATCH_2B80”) ? 1 : 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”, addr, s->data[addr], val, s->pc,
s->insn_count); } } } /* SONDE GAP-1 : transitions du pointeur de
handler data[0x3f5e] (machine à * états L1) + cellules de contrôle API
0x0908/0x0909 au même instant. Montre * si le scheduler avance l’état ou
reste figé sur 0x7013 (FB), et d’où (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”, s->data[0x3f5e], val,
s->pc, s->data[0x0908], s->data[0x0909], s->data[0x0945],
s->insn_count); }
/* SONDE GAP-1 : qui ECRIT data[0x0c36] (le pointeur de TACHE CALA'd a 0xb3a5,
* nul -> derail) ? Catch des writes DSP (y compris indirect via 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);
}
/* [2026-07-23] STATE435B-WR : qui ecrit data[0x435b] (mot d etat de la SM
* go-live 0xa4e4 : bits 0x10/0x40/0x100 gatent la progression + l enable INTM).
* =0 -> SM bloquee. Trouver qui doit le peupler (ARM ? task done ?). Cap 40. */
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);
}
/* [2026-07-23] WATCH-09BC-WR : data[0x09bc] deja annote "flag ARM" (comment
* SM-TRACE ligne ~12352). Full-ROM scan (2026-07-23) montre que le gate go-live
* 0xa544 (BITF *(0x09bc),1 ; BC 0xa549 si NTC) SAUTE le seul BACC natif
* (0xa546-0xa548, via d[0x3fe0]=0x70ce) vers le bootstrap operationnel 0x7102
* CALL 0xd247 SAUF SI bit0 de data[0x09bc] est deja pose. AUCUN site trouve
* dans PROM0-3 ne fait un ORM/SET clair de ce bit (seulement 2 clears ANDM a
* 0x70fc/0x712d + un site ambigu 0xcf53 au decode incertain). Ce watch confirme
* en RUNTIME : (a) qui ecrit 0x09bc et avec quelle valeur, (b) si bit0 est
* JAMAIS mis a 1 nativement. Cap 40, READ-ONLY. */
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);
}
/* [2026-07-23] WATCH-000B-WR : data[0x000b] teste par BITF au moins 2x dans
* le dispatcher background (0xdeb6: BITF *(0x000b),0x4000 ; 0xdec2: BITF
* *(0x000b),0x2000 -- bits 14 et 13). User hypothese : "l'histoire des 11"
* (11x tpu_enq_at(0) dans l1s_rx_win_ctrl pour FB, jamais modelise en timing
* -- cf calypso_tpu.c) -- est-ce que 0x000b (=11 decimal, coincidence
* d'ADRESSE pas de compteur a priori) est le mot que le sequenceur TPU est
* cense faire progresser via ces 11 delais, et qui reste bloque a 0 faute de
* timing modelise ? Ce watch confirme en RUNTIME si data[0x000b] est ECRIT
* par QUOI QUE CE SOIT nativement (natif = pas de hack). Si jamais ecrit ->
* BITF y lit toujours 0 -> TC toujours faux -> boucle jamais debloquee par
* cette voie -> confirme/infirme l'hypothese. Cap 40, READ-ONLY. */
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);
}
/* [2026-07-23] WATCH-0810-WR : data[0x0810] = db_w->d_ctrl_system (write-page
* MCU->DSP, offset 16), champ nomme confirme via osmocom-bb (dsp_api.h:75
* "Control Register for RESET/RESUME"). Bit15 = B_TASK_ABORT
* (l1_environment.h:365), ARM le pose dans l1s_reset() ("abort RF tasks, dsp
* will reset current+pending tasks"). Le DSP teste CE bit a 0xa53c/0xa53f
* (BITF *(AR1+0x10),0x8000, AR1=0x0800) : bit15 SET -> tombe vers
* a541-a544-a546-0x09bc-0xd247 (chemin operationnel/bootstrap) ; bit15 CLEAR
* -> saute a 0xa575 (court-circuit, jamais bootstrap). Confirme si/quand
* l'ARM ecrit ce bit, et sa valeur au moment ou le DSP le lit. Cap 40. */
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);
}
/* [2026-07-23] DISPATCH-CELL-RESEED (READ-ONLY) : d[0x43d8]/d[0x3fd4]/d[0x4368]
* confirmes CONSTANTS (0xab38/0xc1fa/0xaff9) sur tout runtime observe cette
* session -- watch WRITE pour prouver/refuter qu'ils sont jamais reseedes vers
* autre chose (ferme definitivement le gap "static constant" du workflow xref-scan). */
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);
}
/* [2026-07-23] FBDET-WR (READ-ONLY, INCONDITIONNEL) : data[0x08F8] = d_fb_det, le
* champ NDB que le firmware ARM lit pour savoir si le corrélateur a trouve une FCCH.
* JAMAIS vu ecrit nativement cette session. Watch total (pas de cap) -- champ critique. */
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);
}
/* [2026-07-23] READY-WR : qui peuple la readiness du go-live (0xaad5 lit
* *data[0x434e]=*0x4340 et *data[0x434f]=*0x7f75 ; A=0 -> 0xa4cd skip enable).
* Watch les pointeurs (0x434e/0x434f) ET les cibles (0x4340/0x7f75). Cap 40. */
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);
}
/* [2026-07-23] 3FCD-WR : qui ecrit data[0x3fcd] (la CIBLE du RET@0x0157 :
* le handler frame 0x013b fait PSHD *(0x3fcd) puis RET -> saute a data[0x3fcd].
* =0 en QEMU -> derail. Trouver qui/quoi doit le peupler. Cap 40. */
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);
}
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);
}
/* SONDE GAP-1 : qui ECRIT data[0x43c0] (le pointeur de dispatch lu par le
* tremplin 0xb40e/0xb40f bacc), qui vaut 0xf074 (adresse de table = 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);
}
/* SONDE Phase B DISPVAL-WR : qui ECRIT la valeur 0xf074 (= base LUT, la
* cible de bacc fautive) dans la table de dispatch 0x4300-0x43ff ? Nomme
* le planteur du pointeur de handler corrompu. Cap 60. */
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 (2026-06-24) : qqn reecrit-il le slot dispatch terminal
* data[0x4387] avec un vrai handler (!= 0xab38 idle) ? (test H1). Cap 40. */
if (addr == 0x4387) {
static unsigned s4=0;
if (s4++<12) {
/* [2026-07-22] contexte COMPLET : d ou vient la valeur 0x%04x ? (index
* de dispatch). Dump AR/A/B/DP/ST0 + les 6 mots prog autour du store
* pour reconstruire le calcul d index de la table 0xab10. */
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)));
}
}
/* SONDE Phase B SEED-WR : qui ecrit la base de pile data[0x5ac8..0x5acc] =
* l'adresse de retour que le RET du handler no-op (0xab38) depile ? Si
* jamais ecrit (que le memset 0x88 en dessous) -> seed retour-scheduler
* jamais pose -> idle-path correct mais init manquante (#2). Cap 40. */
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);
}
/* [2026-07-22] VECWATCH (gated CALYPSO_AR0_DEBUG) : attrape la VALEUR 0x71f4
* (vecteur go-live trampoline) ecrite N'IMPORTE OU, depuis insn 0. Tranche :
* si 0x71f4 est ecrit a une addr != 0x5ac8 -> write egare (bug SP/adressage) ;
* si jamais ecrit -> init au reset non modelisee. Meme fonction que SEED-WR
* (prouve firing). Cap 40. */
if (val == 0x71f4) {
static int vw_en = -1;
if (vw_en < 0) vw_en = getenv("CALYPSO_AR0_DEBUG") ? 1 : 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 probe (2026-05-28) ===
* Trace writes to a_sync_demod cells [0x08FA..0x08FD] (= TOA/PM/ANGLE/SNR
* per NDB layout, indices D_TOA=0 D_PM=1 D_ANGLE=2 D_SNR=3).
* Filter OUT les PCs stale-AR connus (0x821a 0x8213 0x8217) qui sont
* juste du AR4-walk garbage. Le but est de capturer le VRAI publisher
* (= la routine FB-det code DSP qui calcule les vraies valeurs). Si
* apres tout le run on ne voit que les 3 PCs garbage → le vrai code
* n'est jamais atteint. Cap 200 entries. */
if (addr >= 0x08FA && addr <= 0x08FD) {
if (s->pc != 0x821a && s->pc != 0x8213 && s->pc != 0x8217) {
static unsigned sd_log;
const unsigned LIMIT = 200;
if (sd_log < LIMIT) {
const char *name = (addr == 0x08FA) ? "TOA"
: (addr == 0x08FB) ? "PM"
: (addr == 0x08FC) ? "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 probe (2026-05-28) ===
* Trace specifically writes to d_fb_det (DSP word 0x08F8) by PC.
* Run post-option-A shows d_fb_det stuck at 0x1255 (96×), no varied
* lock signature. Question : ONE site écrit toujours 0x1255, OU plusieurs
* sites écrivent (mais ARM ne consume que celui qui produit 0x1255) ?
* Snapshot par-événement : PC + B accumulator (= source du STH/STL),
* prev_op (= instruction précédente, contexte d'addressing), trail des
* 4 PCs précédents pour identifier la routine. 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);
}
}
/* [2026-07-22] WATCH-VEC : writes vers table de vecteurs IT (0x0080-0x00FF)
* + dispatch 0x013b (0x0138-0x013c). Tranche (a) jamais ecrit / (b) ecrit-
* droppe / (c) corrompu. Env dediee CALYPSO_WATCH_VEC. */
{
static int wv = -1;
if (wv < 0) { const char *e = 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);
}
}
/* === SP-CATASTROPHE fix : DROM[0x9187] silicon-correct read-only ===
* Per SPRU172C : when PMST.DROM=1, the DSP ROM in data space is
* read-only. Our emulator previously allowed writes which corrupted
* data[0x9187] (0xFF86 → 0xF6B7) via a walking `STH B,*AR2+` inside
* the RPTB body [0x815E..0x8176]. Dispatcher at 0x8341..0x8353 then
* read the corrupted value and computed CALAD-A target = 0x70C3
* (= the CALA-A opcode itself in PROM0) instead of the legit MAC
* routine 0x8261, falling into a self-call loop. Each iteration
* pushed one word, SP eventually reached MMR_SP (0x0018), the push
* aliased SP itself → SP-CATASTROPHE Δ=+28843.
*
* Surgical : only data[0x9187] needs protection (the dispatcher LUT
* slot). Wider ranges break other firmware paths (calibrated against
* historical SARAM-overlay writability). Drop silently — matches
* silicon. Probe first 20 attempts for diag. */
/* 2026-05-29 v2 : protège la COLONNE LUT du dispatcher dans la DROM, PAS
* toute la DROM (le full-DROM v1 bloquait le scratch firmware 0x9380/
* 0x93c2/0xd4xx → readback stale → DSP coincé en 0xebf0). Le dispatcher
* lit une LUT par-tâche à data[(DP<<7)|0x07] = 0x9187, 0x9207, 0x9287…
* (toujours offset 0x07 ; DP = numéro de tâche). Le `STH B,*AR2+` walking
* (RPTB 0x815E-0x8176) corrompait 0x9207 (natif 0xff72 → 0xf6b7) →
* CALAD-A=0x70c3 au lieu de 0x8239 → self-CALA → SP drain → snr=0.
* On bloque uniquement les writes DROM à offset 0x07 (la colonne LUT) :
* le scratch firmware est à d'autres offsets (0x00/0x42/0x60…), préservé. */
/* DROM-LUT column protection — garde PMST.DROM RETIRÉE 2026-05-29.
* Preuve (run 459M, /root/qemu.log) : PMST=0x70c4 vu 149× = PMST(MMR 0x1D)
* lui-même clobbé par le self-CALA 0x70c3 (SP drain) → bit DROM(0x08)=0 →
* la garde se désactivait elle-même → LUT 0x9207 re-corrompue → 0x70c3 →
* boucle auto-entretenue (148838× le self-CALA). DROM-W-DROP n'avait fiché
* que 2× (DROM encore =1) puis silence. Le firmware ne tourne JAMAIS
* légitimement en DROM=0 (PMST légit = 0xffa8/0xffb8, DROM=1 dans les deux)
* → offset 0x07 read-only SANS garde DROM. */
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;
}
/* 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 probe : watch-write sur la zone [0x2bc0..0x2bff] (64 mots).
* 2026-05-14 evening — COEFFS-DUMP a montré une séquence init→clear→use :
* insn=2M PC=0x9b05 vraies coeffs (f320, a660, ...)
* insn=3M PC=0x9abc pattern uniforme 0x0001 (suspect)
* insn=4M PC=0x9abd ALL ZERO (clear)
* insn=9M+ PC=0x8f51 ALL ZERO toujours (read par correlator)
*
* v2 (run précédent cap 200) : 3 clusters identifiés —
* 0x8216 (wk=OTHER, 23 hits) = vraies coeffs
* 0x9ace (wk=8, 64 hits) = clear partiel
* 0x9abf (wk=8x, 113 hits) = pattern 0x0001
* Cap atteint à insn=1.65M. On veut savoir si 0x8216 refire après.
*
* v3 (ce patch) :
* - Per-PC counter global sur tout le run (jamais capé)
* - Throttled log : 500 premiers + transitions PC + 1/100k insns
* - Summary périodique tous les 5M insns dump tous les PCs avec count>0
* Tranche (1) re-fire 0x8216 manqué vs (2) one-shot définitif. */
/* Timing trackers par cluster (utilisés par la sonde 0x8f51) — gardés
* en dehors du helper car cluster-specific. Le watch_write_zone_check
* factorise le compteur per-PC + log + 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);
}
/* INVARIANT (gate CALYPSO_INVARIANTS, defaut off) : pointeurs du correlateur.
* AR4 = write ptr -> doit prendre >2 valeurs distinctes (sinon boucle 2-mots).
* AR5 = read ptr I/Q -> doit vivre dans le buffer [0x2a00..0x2b27] (sinon le
* correlateur lit hors buffer = kernel 0xa076 jamais atteint, AR5=0xdb7b). */
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);
}
/* A_CD-WR : a_cd[15] in NDB starts at DSP word 0x09D0 (= API byte 0x03A0,
* = NDB byte offset 0x1F8). 15 words = [0x09D0..0x09DE].
* Cible : tracker si le DSP CCCH demod (DSP_TASK_ALLC) écrit ses résultats. */
{
static WatchWriteState wws_a_cd;
watch_write_zone_check(s, addr, val, "A_CD", 0x09d0, 0x09de, &wws_a_cd);
/* A_CD-BY-BURST : corrélation a_cd[] writes avec d_burst_d courant.
* Si DSP fait burst 0→1→2→3 → ~25% des writes par burst_id.
* Si on voit 0 writes avec burst=3 → DSP n'écrit jamais la fin de
* séquence, d'où firmware ARM nb_resp bail (sous-cause #3). */
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[0],
(unsigned long long)a_cd_by_burst[1],
(unsigned long long)a_cd_by_burst[2],
(unsigned long long)a_cd_by_burst[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 (2026-05-15 midi) — watch d_burst_d à 0x0829 (page 0)
* et 0x083D (page 1). Mesures : per-PC counter, transition matrix,
* histogramme. Tranche la sous-cause :
* 0,1,2,3 séquentiel → DSP signale correct, bug ARM-side
* 0,1,2 jamais 3 → DSP cale au 4e burst (sous-cause #3)
* pas de write → DSP n'écrit pas cette cellule du tout
*/
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; /* propage pour 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 (2026-05-15 fin journée) — watch d_task_d à 0x0828
* (page 0) et 0x083C (page 1), READ side de la struct db_buf_r.
* ARM L1 prim_rx_nb lit ce champ via dsp_api.db_r->d_task_d et bail
* avec puts("EMPTY") si == 0. 60 EMPTY printf observés sous synth=1
* banc d'essai déterministe : DSP n'écrit jamais cette cellule (ou
* écrit 0). Cette probe trace : qui écrit ? quand ? avec quelle valeur ?
* Distinguer entre :
* - DSP ne touche jamais 0x0828/0x083C
* - DSP écrit val=0 (clear/init seulement)
* - DSP écrit val=24 (DSP_TASK_ALLC) mais ARM lit avant le 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 (générique, gated CALYPSO_DEBUG=WATCH-WR + env
* CALYPSO_WATCH_WR_ADDR=0xNNNN) : log tout write vers une adresse data
* arbitraire — pour tracer qui écrit (ou jamais) une cellule pointeur-
* dispatcher SARAM (ex. 0x3af7) qui tombe à 0 → CALA→0 → bootstub. */
{
static int watch_wr_addr = -1;
if (watch_wr_addr < 0) {
const char *e = getenv("CALYPSO_WATCH_WR_ADDR");
watch_wr_addr = (e && *e) ? (int)strtol(e, NULL, 0) : 0;
}
if (watch_wr_addr && addr == (uint16_t)watch_wr_addr) {
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);
}
}
/* WATCH-WRITE on the same mailbox slots tracked in data_read.
* Whoever writes them — DSP or ARM via api_ram alias — gets logged
* so we can attribute the source of the value the firmware polls. */
/* WATCH-WRITE 0x3dd2 — la cellule sur laquelle 0x75db poll en boucle
* (37M reads/15s). Identifier qui écrit (et qui ne le fait pas).
* Cas 1 : zéro write → un bloc compute ne fire jamais.
* Cas 2 : write boot only → init OK mais set steady-state manquant.
* Cas 3 : writes périodiques avec valeur jamais matchée par le test
* à 0x75db → bug dans le compute en amont. */
/* WATCH-3FBE — observ. writes sur la zone [0x3fb0..0x3fbf] qui inclut
* 0x3fbe (le slot pop=0 du bootstub-entry insn=3995013). Le BSP DMA
* bypasse data_write_locked (direct s->data[]) donc ce hook ne voit
* QUE les writes via instructions DSP (STL/STM/STLM/STH). Si zéro
* write vu avant insn=3995013 → le firmware ne pousse jamais à cet
* addr → trajectoire SP divergente (RETD à 0x8ed1 sans CALL apparié).
* Env-gated CALYPSO_WATCH_3FBE=1, zéro coût sinon. */
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);
}
}
}
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));
}
}
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]. FORCE-DARAM62=1 (env) proves that
* setting data[0x62]=1 makes the DSP escape and reach 0x770c, so
* this range gates the runtime task pipeline. ARM-side writes to
* the API page mirror at +0x0800 (calypso_trx.c calypso_dsp_write)
* but never to DARAM 0x40..0x90 — so any value here must come from
* DSP-self stores (ST/STH/STM/...) or stay zero forever. Capture
* EVERY write with PC+INTM+insn so we can attribute the source.
* INTM annotation lets us tell 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");
}
}
}
/* 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 (2026-06-24, RO, INCONDITIONNEL) : historique COMPLET des
* ecritures IMR (old->new), etat bit12=vec28(scheduler)/bit3=vec19/
* bit5=vec21. Run a montre IMR=0x52fd (bit12 SET!) efface par 0xb37e
* -> on veut QUI ecrit 0x52fd (writer non-STM) + si un re-arm suit. Cap 80. */
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);
}
}
{
/* [2026-07-25] Le toggle IMR bit9 (0xddf9 ANDM / 0xde84 ORM) est
* PILOTE PAR LE TPU (masquage dynamique par fenetre burst), PAS a
* figer statiquement -> tentative PIN-IMR retiree (diag user).
* IMR = ce que le firmware/TPU ecrit (fidele). Le seul re-arm
* conserve est le fix bit5/BRINT0 dynamique (voir KEEP-IMR bloc
* ~13230) qui restaure bit5 uniquement quand il TOMBE. */
}
s->imr = val; return;
case MMR_IFR: {
/* [2026-07-23] IFR-CLEAR-W probe (unconditional, capped) : est-ce
* que le code go-live ecrit directement IFR (write-1-to-clear) sur
* bit5(BRINT0)/bit12(frame) SANS jamais dispatcher -- acquittement
* logiciel qui jette le pending au lieu de le servir ? */
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);
}
s->ifr &= ~val; return; /* write 1 to clear */
}
case MMR_ST0: s->st0 = val;
/* DISP-ENTRY : trace restauration ST0 entière (POPM ST0/STLM) =
* chemin NON-LDP qui change DP. C'est ICI que DP devient 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 (C-sweep) */
if (g_orphan_on > 0 && (s->pc == 0xf48b || s->pc == 0x7737 || (val & 0x1FF) == 0x124)) {
/* POPM ST0 @0xf48b : le slot juste poppé = data[sp-1]. Cherche
* son dernier écrivain dans le ring pile. NO-WRITER = slot stale
* → SP désaligné (POP sans PUSH apparié, cas b de CC). */
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-EVENTS ring : les push/pop récents → le return RET-family
* déséquilibré (pop delta>0 sans push apparié) qui décale 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_abs_track(s, val, 0); /* site=0 : MMR_SP via STL/STM/STLM */
s->sp = val;
return;
case MMR_BK:
/* PROBE 2026-06-01 : qui écrit BK ? (BK=0 casse l'adressage circulaire
* → runaway AR2 0xfa98/0xf17c). Nomme le writer + valeur. À RETIRER. */
{
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 (revival dsp 2026-06-22, RO) : un-gated — tranche « IPTR
* relocalisé 0x140 perdu vs jamais émis ». 300 premiers + TOUJOURS si 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 w0 = prog_read(s, a + 0);
uint16_t w1 = prog_read(s, a + 1);
uint16_t w2 = prog_read(s, a + 2);
uint16_t w3 = prog_read(s, a + 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 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 × 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;
if (s->api_ram)
s->api_ram[woff] = val;
{ /* [2026-07-28] FBDET-API (a) cote DSP : voir en-tete du patch. */
static int _fa = -1; static unsigned _fan = 0;
if (_fa < 0) _fa = getenv("CALYPSO_FBDET_API") ? 1 : 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 (0x08E2) */
if (addr == 0x08E2) {
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)]);
}
/* d_fb_det (NDB word 36 = DSP data 0x08F8). The DSP correlator
* output here is treated as Q15-signed by the firmware FB-det
* path — small unsigned BSIC was a wrong assumption. Log every
* write unconditionally (thinned past 200) and dump the
* adjacent NDB cells [0x08F0..0x0900] so we can see correlator
* + flag + a_sync_demod fields together. */
/* Silent NDB cells watch — d_fb_mode (binary "FB matched" flag,
* THE actual trigger ARM tests), a_sync_PM (power), a_sync_SNR
* (SNR). All read as 0 by ARM during 200M run despite d_fb_det
* varying. Confirms: DSP never declares valid detection.
* Three discriminating outcomes:
* (α) never written → "FB confirmed" code path unreached
* (β) written =0 explicitly → DSP scans, never matches threshold
* (γ) written !=0 but ARM reads 0 → coherence bug */
/* W1C latch system removed 2026-05-28 (FBSB host-side synth purge).
* The only env-gated override on the a_sync_demod read path is now
* CALYPSO_FORCE_ANGLE_ZERO (calypso_trx.c). DSP writes pass
* straight through to s->data[] and ARM reads them direct. */
/* Full a_sync_demod + d_fb_mode WR watch — every cell, no PC
* filter (so we catch real-fb-det writes AND stomp candidates).
* Stomp zone PC=0x06xx tagged for easy grep. */
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;
}
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 ===
* Race-window observed (memory `project_fbdet_threshold_blocker`):
* DSP writes high SNR (e.g. 0x7902, 0x7766) at fb-det PCs, then
* SOMETHING zeroes d_fb_det before ARM reads. ARM sees 200×
* 0x0000 → no FB found → endless L1CTL_FBSB_REQ retries.
*
* Capture EVERY write of val=0 to 0x08F8 with full context so
* we identify the zero-ifying PCs and reconstruct the condition
* (threshold check, post-correlation reset, error path, etc.).
* Cap 200 events. */
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++;
}
}
/* === NEW 2026-05-15 : SET non-zero trace + SET→CLEAR delta ===
*
* Symétrique au ZERO-WR. Capture chaque write de val != 0 à 0x08F8
* pour identifier QUI set le FB found, et combien de cycles ça
* tient avant qu'un PC clear ne l'écrase.
*
* Si delta SET→CLEAR < 100 insn → bug opcode tape immédiatement
* (style POPM fix). Si delta = milliers d'insn → race timing
* légitime entre DSP set et 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);
}
/* PROBE 2026-05-31 frame-IT : qui écrit les flags polled par le DSP wedgé
* (data[0x006e], data[0x585f]) ? Tranche (a) ISR-relocate vs (b) HW-write.
* Si AUCUN write ou valeur jamais "attendue" → flag jamais posé = deadlock.
* À RETIRER après diag. */
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 probe (revival dsp 2026-06-22, read-only) : QUI ecrit le slot
* SB a_serv_demod[D_TOA] db_r (p0 data[0x0830] / p1 data[0x0844]) et a_sch[3]
* (p0 data[0x083a] / p1 data[0x084e]) ? Trou de couverture qui a induit le
* faux "stale". Tranche scatter-write (PC=0xa1d6 ?) vs autre vs jamais ecrit.
* Logue PC, op, A.low, val. Cape 300. */
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++;
}
}
/* PROBE 2026-05-31 : qui écrit d_fb_mode (0x08f9) avec du GARBAGE (>1) ?
* Le détecteur tourne mais d_fb_mode=0x435b (≠0/1) → fenêtre/scaling faux.
* Hypothèse : runaway AR2/BK=0 le corrompt. Nomme le corrupteur. À RETIRER. */
if (addr == 0x08f9 && val > 1) { /* GARBAGE uniquement (val ≠ 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++;
}
}
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[]. / static inline uint32_t
c54x_prog_xlate(const C54xState s, uint16_t addr16) { /* FIX
2026-06-02 (ROOT CAUSE #2 — runaway 0xee00) : seule la fenêtre *
0x8000-0xDFFF est bankée par XPC (overlay PROM0/2/3). 0xE000-0xFFFF est
* de la ROM FIXE (PROM1, mirror à prog[0xE000+]) — NON bankée sur le *
silicium. L’ancien >= 0x8000 appliquait XPC à 0xE000+
aussi → quand * XPC=3, PC=0xee00 fetchait prog[0x3ee00] (au-delà des 8K
de PROM3) = vide * → exécution de PROM nulle (op=0x0000) → runaway de
l’étage FB * post-corrélateur. La ROM haute doit ignorer XPC. / if
(addr16 >= 0x8000 && addr16 < 0xE000) { return
(((uint32_t)s->xpc << 16) | addr16) & (C54X_PROG_SIZE - 1);
} return addr16; / 0x0000-0x7FFF on-chip + 0xE000-0xFFFF PROM1 ROM
(XPC-indépendant) */ }
static uint16_t prog_fetch(C54xState *s, uint16_t pc) { if
((s->pmst & PMST_OVLY) && pc >= 0x80 && pc
< 0x2800) return s->data[pc]; return s->prog[c54x_prog_xlate(s,
pc)]; }
static uint16_t prog_read(C54xState *s, uint32_t addr) { uint16_t
addr16 = addr & 0xFFFF; if ((s->pmst & PMST_OVLY) &&
addr16 >= 0x80 && addr16 < 0x2800) return
s->data[addr16]; return s->prog[c54x_prog_xlate(s, addr16)]; }
static void attribute((unused)) 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 >= 0x80 && 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 *
================================================================ */
/* MOD-MISMATCH probe helper (2026-06-01) : adressage circulaire
canonique * C54x (SPRU172/tic54x-dis.c). step ±1 ou ±AR0, |step| <=
BK attendu. * BK=0 → linéaire (règle #6396 : STM #0,BK délibéré, on ne
wrappe pas). * NB : pur calcul de référence pour la sonde — N’ALTÈRE PAS
l’exécution. */ static uint16_t c54x_circ_ref(uint16_t ar, int step,
uint16_t bk) { if (bk == 0) return (uint16_t)(ar + step); uint16_t base
= ar - (ar % bk); int idx = (int)(ar % bk) + step; if (idx >=
(int)bk) idx -= bk; else if (idx < 0) idx += bk; return
(uint16_t)(base + idx); }
/* Resolve Smem operand: direct or indirect addressing. * Returns the
data memory address. / static 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 for * THIS instruction. ARP (in ST0) is then updated
to ARF for the * NEXT direct-Smem reference. Earlier this code used
arp(s) for * cur_arp, which made every indirect insn operate on the *
PREVIOUS insn’s ARF — off-by-one. Symptoms: BANZD AR1- after
STL AR2+ would decrement AR2 instead of AR1 (BANZD test
against AR2 stayed non-zero forever, AR1 frozen). Diagnosed * via
5×500M-insn STATE-DUMP showing AR1=0x1c / AR2=0x2b0c * frozen across 2B
insns at PC=0xa2c2..0xa2ca. / indirect = true; int mod =
(opcode >> 3) & 0x0F; int nar = opcode & 0x07; int cur_arp
= nar; uint16_t addr = s->ar[cur_arp]; uint16_t ar_before =
s->ar[cur_arp]; /* MOD-MISMATCH probe : base avant post-modify */
/* PROBE 2026-05-31 convergence modes : 1er usage de chaque AR comme base
* d'adresse. Si la valeur == reset (AR0=0xff75/0x5aad, AR6=0/0xbae6,
* AR7=0/0x1e44) → read-before-write → reset load-bearing = driver de la
* divergence bin/c54x. À RETIRER. */
{
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);
}
}
/* AR2-FLOOR guard : le pointeur d'écriture corrélateur (AR2) peut
* sous-déborder le buffer DARAM (0x0800) jusqu'à l'espace MMR
* (0x1E=XPC, 0x00=IMR) → clobber. WARN-log diag (token AR2-FLOOR) ;
* DROP expérimental (env CALYPSO_AR2_FLOOR_DROP=1) redirige l'accès
* vers un scratch pour voir si le corrélateur converge sans le crash. */
if (cur_arp == 2 && addr < 0x0820) {
/* @BEQUILLE — AR2_FLOOR_DROP (CALYPSO_AR2_FLOOR_DROP, EQ1, defaut OFF)
* masque : le calcul d'adresse d'AR2 dans le correlateur, qui sous-deborde le
* buffer DARAM 0x0800 jusqu'a l'espace MMR (0x00=IMR, 0x1E=XPC) et le
* clobbe. Le drop redirige l'acces vers 0xFFFF au lieu de corriger le
* pointeur. NB : le LOG est gate par le jeton DEBUG=AR2-FLOOR, le DROP
* ne l'est PAS.
* retirer : quand AR2 reste dans [0x0800,0x2b28) sur tout le kernel FB
* (compteur du jeton AR2-FLOOR a 0 sur un run complet).
*/
static int ar2_drop = -1;
if (ar2_drop < 0) {
const char *e = getenv("CALYPSO_AR2_FLOOR_DROP");
ar2_drop = (e && *e == '1') ? 1 : 0; /* env-gated, OFF par défaut */
}
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 : empêche le clobber MMR (expérience) */
}
/* 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 : encodage canonique C54x (tic54x-dis.c:506-518, vérifié
* cross-run via sonde MOD-MISMATCH 2026-06-01 : QEMU divergeait sur
* 5/6/9/10/11 — signe inversé 5/6, mauvais op 9/10, wrap absent 11).
* Ordre réel : 4=-0B 5=-0 6=+0 7=+0B 8=-% 9=-0% 10=+% 11=+0%.
* Circulaire (8-11) via c54x_circ_ref → BK=0 reste LINÉAIRE (règle
* #6396 : STM #0,BK délibéré, confirmé par sonde BK-WR). */
case 0x4: /* *ARn-0B (bit-reversed) — reverse-carry différé, voir GAP */
s->ar[cur_arp] -= s->ar[0];
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 (bit-reversed) — reverse-carry différé, voir GAP */
s->ar[cur_arp] += s->ar[0];
break;
/* GAP bitrev (4/7) : ±AR0 plat, signe correct, reverse-carry ignoré.
* OK hors-FFT ; tout chemin FCCH/SCH bit-reverse mal-adresserait en
* silence. À implémenter si une sonde le montre exercé en bitrev. */
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)
*
* The bootloader at PROM0 0xb429 uses MOD=15 (`LDU *(0x0ffe), A`)
* to read BL_ADDR_LO. Misdecoding 15 as "AR + lk circular"
* produced AR0+0x0ffe instead of 0x0ffe — one of the multiple
* subtle off-by-AR bugs that left A=0 after the load. */
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 */
uint16_t lk = prog_fetch(s, s->pc + 1);
uint16_t v = s->ar[cur_arp] + lk;
if (s->bk) {
uint16_t base = s->ar[cur_arp] - (s->ar[cur_arp] % s->bk);
if (v >= base + s->bk) v -= s->bk;
}
s->ar[cur_arp] = v;
addr = v;
s->lk_used = true;
break;
}
case 0xF: /* *(lk) — absolute address */
addr = prog_fetch(s, s->pc + 1);
s->lk_used = true;
break;
}
/* PROBE 2026-06-01 MOD-MISMATCH : delta silicium-correct EN PARALLÈLE
* (n'altère PAS l'exécution — pur compare). Confirme sur le flux réel
* que seuls mods 5/6/9/10/11 divergent ET que la firmware les touche.
* Réf : tic54x-dis.c:506-518 (MOD canonique), macros tic54x.h:97-98
* identiques à l'extraction QEMU. À RETIRER après validation du patch. */
{
int16_t a0 = (int16_t)s->ar[0];
uint16_t bk = s->bk;
uint16_t sil; /* AR attendu côté silicium */
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 */
}
/* quels mods la firmware touche (1er hit chacun) */
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);
}
/* divergence silicium vs QEMU (modes 0..11 seulement) */
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 */
s->st0 = (s->st0 & ~ST0_ARP_MASK) | (nar << ST0_ARP_SHIFT);
return addr;
} else {
/* Direct addressing: DP:offset */
*indirect = false;
uint16_t offset = opcode & 0x7F;
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). / static uint16_t
resolve_lmem(C54xState s, uint16_t opcode) { if (!(opcode &
0x80)) { /* Direct (DP-relative) — dmad pair, no post-mod. /
uint16_t dp = s->st0 & ST0_DP_MASK; return (uint16_t)(((dp
<< 7) | (opcode & 0x7F)) & 0xFFFE); } int mod = (opcode
>> 3) & 0x0F; int nar = opcode & 0x07; uint16_t addr =
s->ar[nar] & 0xFFFE; 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] & 0xFFFE; break; /*
+ARn / case 0x4: case 0x5: s->ar[nar] -= s->ar[0]; break;
/* ARn-0(B) / case 0x6: case 0x7: s->ar[nar] += s->ar[0];
break; /* ARn+0(B) / 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)) &
0xFFFE); s->lk_used = true; break; case 0xD: s->ar[nar] +=
prog_fetch(s, s->pc + 1); addr = s->ar[nar] & 0xFFFE;
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] & 0xFFFE; s->lk_used = true;
break; } case 0xF: addr = (uint16_t)(prog_fetch(s, s->pc + 1) &
0xFFFE); s->lk_used = true; break; } return addr; }
/* SP ledger for IRQ-asymmetry diag (web 2026-05-23). * Pushes/pops
counted by SP delta sign in dispatch loop (c54x_run). * IRQ entries
counted explicitly in c54x_interrupt_ex with word count. * Periodic dump
in dispatch loop shows whether net_words ≈ 0 (balanced) * or drifts
(indicates push/pop word-count asymmetry, e.g. IRQ entry * pushes 1 word
but FRET pops 2 → drift -1/IRQ-cycle → SP wraps). / static struct {
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; } g_sp_ledger;
/* Xmem operand decode per binutils tic54x.h (XMEM/XMOD/XARX macros)
: * XMEM(OP) = bits [7:4] of opcode (the Xmem 4-bit nibble) * XMOD =
nibble bits [3:2] : 0=AR, 1=AR-, 2=AR+, 3=AR+0% * XARX
= nibble bits [1:0] + 2 (= AR2..AR5 only, no AR0/AR1/AR6/AR7)
Xmem is INDIRECT-ONLY (no DP-relative direct mode, unlike Smem). Using *
resolve_smem on an Xmem operand mis-decodes the low byte as Smem direct
* addressing whenever bit 7 is clear, which lands writes in MMR space *
(0x00-0x1F) — empirically observed at PC=0x8a46 op=0x9918 (STL
B,AR2) 2026-05-23, stomp SP=0x4800→0x0000 cascading to IMR=0 →
DSP idle forever. Fix 2026-06-01 : xmod=3 (AR+0%)
désormais CIRCULAIRE modulo BK via c54x_circ_ref (BK=0→linéaire,
règle #6396). Appliqué à tous les handlers * duaux (resolve_xmem, MVDD,
MAC D0-D9, MASA DB, SQDST DC) — était linéaire * addr + AR0
→ drift 16-bit (runaway AR2 @0xfa98, op
0xd3dc Ymem AR2+0%). Cohérent avec le handler ST||LD C8-CB qui
wrappait déjà correctement. * NB : la convention 1/2 (±) diffère entre
handlers (MVDD 1=- 2=+ vs MAC * 1=+ 2=-) — incohérence séparée NON
traitée ici, à mesurer (sonde). / static 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% circulaire modulo BK (BK=0→linéaire) — fix 2026-06-01 / }
return addr; }
/* ================================================================ *
Instruction execution *
================================================================ */
/* Execute one instruction. Returns number of words consumed (1 or
2). / / PC ring buffer for pre-IDLE trace */ static uint16_t
pc_ring[256]; static int pc_ring_idx = 0;
/* Évalue une condition C54x depuis l’octet bas de l’opcode, per
binutils * condition_codes[] (opcodes/tic54x-opc.c) : CC1=0x40 (test
accu), CCB=0x08 * (accu B sinon 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. * Identique à l’évaluation du handler RC/RCD
(correcte). Remplace l’ancien * décode (op>>4)&0xF des
handlers CC/CCD qui lisait le MAUVAIS champ (seuls * UNC/AEQ justes par
coïncidence ; NEQ/LT/LEQ/GT/GEQ/TC/C faux) → mauvais * call/no-call dans
la power-scan 0xb1xx (CC[TC] f930) → push manquants → * over-pop SP →
orphelin 0x80fd @0x94f3 → self-CALA 0x70c3
(=28868). * cf doc/SP_CATASTROPHE_70c4_SEQUENCE.md, vérifié sonde
CC-MISMATCH. / static 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 / if ((cc & 0x70) == 0x70) return
ov; / AOV/BOV / if ((cc & 0x70) == 0x60) 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; } } if ((cc
& 0x30) == 0x30) return !!(s->st0 & ST0_TC); if ((cc &
0x30) == 0x20) return !(s->st0 & ST0_TC); if ((cc & 0x0C) ==
0x0C) return !!(s->st0 & ST0_C); if ((cc & 0x0C) == 0x08)
return !(s->st0 & ST0_C); return true; }
/* Faithful per-instruction interrupt LEVEL check (gated
CALYPSO_C54X_IRQ_LEVEL). * The base model services interrupts only at
the c54x_interrupt_ex call edge: an * IFR bit latched while INTM=1 is
never taken later. Real C54x re-checks pending * unmasked interrupts at
each instruction boundary. This restores that, so an * armed frame IT
(INT3/bit3) fires once INTM drops -> native frame ISR runs. /
/ === Frame-IT LEVEL hold + PRIO (2026-07-25)
============================ * La frame-IT (bit12/vec28, scheduler
0x7234 -> kernel FB) est posee en EDGE par * c54x_interrupt_ex a
chaque trame. Mesure : INTM=1 ~permanent (wait-loop 0xdde6 * + sections
critiques 0xb52x) -> la fenetre INTM=0 de 5 insns coincide rarement *
avec bit12 pendant -> vec28 dispatchee 80x sur ~36000 trames ->
kernel FB affame * (AR5 jamais 0x2a00, fb0_att=0). Deux correctifs GATES
: * - LEVEL : maintenir bit12 asserte dans l IFR jusqu a ce que vec28
VECTORISE * (re-assert chaque insn), pour que la prochaine transition
INTM 1->0 * l attrape a coup sur (= “vectoriser a la transition,
sinon garder”). * - PRIO : quand bit12 ET un bit de priorite plus basse
(ex bit5/BRINT0) pendent * dans la meme fenetre, prendre bit12 (frame)
en 1er au lieu du ctz brut, * sinon BRINT0 vole la fenetre rare et
re-masque (INTM=1). / static bool g_frame_it_level = false; /
@BEQUILLE — FRAME_IT_LEVEL
(CALYPSO_FRAME_IT_LEVEL, EQ1, defaut OFF) * masque : la fenetre INTM=0
trop rare du firmware. Re-assert IFR bit12 a CHAQUE * insn tant que
vec28 n’a pas vectorise — l’IFR c54x est a latch * d’evenement, il n’a
pas de mode “level”. * retirer : quand la cadence INTM du firmware
suffit a attraper l’IT au vol. / static bool frame_it_level_on(void)
{ static int c = -1; if (c < 0) { const char e =
getenv(“CALYPSO_FRAME_IT_LEVEL”); c = (e && e == ‘1’) ? 1 :
0; } return c; } / @BEQUILLE —
FRAME_IT_PRIO (CALYPSO_FRAME_IT_PRIO, EQ1, defaut OFF) * masque : la
priorite d’interruption. Force b=12 au lieu de ctz(pend) pour que * la
frame passe devant BRINT0/bit5 — sur c54x la priorite est fixee * par le
numero de vecteur, elle n’est pas configurable. * retirer : quand BRINT0
et la frame ne se disputent plus la meme fenetre * (livraison BSP a la
bonne cadence). / static bool frame_it_prio_on(void) { static int c
= -1; if (c < 0) { const char e =
getenv(“CALYPSO_FRAME_IT_PRIO”); c = (e && *e == ‘1’) ? 1 : 0; }
return c; }
static bool c54x_irq_level_check(C54xState s) { static int en =
-1; if (en < 0) { const char _d = getenv(“CALYPSO_DSP”); en =
(getenv(“CALYPSO_C54X_IRQ_LEVEL”) || (_d && !strcmp(_d,
“c54x”))) ? 1 : 0; } /* natif revive / if (!en) return false; /
LEVEL hold : tant que la frame-IT n a pas ete vectorisee (vec28), garder
* bit12 pendant dans l IFR -> la prochaine fenetre INTM=0 la prend.
/ if (g_frame_it_level && frame_it_level_on()) { s->ifr
|= (1u << 12); } / [2026-07-22] LEVELCHK-DBG (gated
CALYPSO_AR0_DEBUG) : quand IMR!=0 (fenetre * armee), pourquoi l’IT frame
n’est-elle pas prise ? Tranche INTM vs IPTR vs * pend=0. C’est le verrou
du mur terminal Frontiere A. / { static int lcdbg = -1; if (lcdbg
< 0) lcdbg = getenv(“CALYPSO_AR0_DEBUG”) ? 1 : 0; if (lcdbg
&& s->imr && s->insn_count > 4000) { /
skip boot-reset noise, vise go-live / 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”,
!!(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
despite INTM-TRANS showing IFR=0x1020/0x1030 * (bit5=BRINT0 +
bit12=frame pending) right at INTM 1->0 (RETE) moments. * This traces
EVERY early-return path of this function so we can see * empirically
which gate is blocking dispatch, instead of reasoning about * it
statically (this session has been burned by that repeatedly). */ {
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 : RPT re-executes the same instruction (same PC, * same
insn_count) hundreds/thousands of times without advancing – * confirmed
via s->rpt_active/rpt_count (calypso_c54x.c ~14371: “RPT: * after
executing an instruction while repeat is active, re-execute * the SAME
instruction… continue” – skips insn_count++). That was * exhausting our
cap on ONE repeat loop. Only log on a NEW insn_count * so the cap covers
distinct instructions, not RPT spin. */ 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”, _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”); } } if ((s->st1 & ST1_INTM) ||
s->delay_slots != 0) return false; /* Ne pas vectoriser tant que le
ROM n a pas relocalise IPTR (reset=0x1ff -> * table en 0xff80 =
garbage). Attendre IPTR reloue (typiquement 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 /
/ PRIO : la frame (bit12/vec28) prime sur les bits plus bas (BRINT0
bit5) qui * voleraient la fenetre rare et re-masqueraient INTM. Gate
CALYPSO_FRAME_IT_PRIO. / if (frame_it_prio_on() && (pend
& (1u << 12))) { b = 12; } int vec = b + 16; / C54x:
maskable IMR bit b -> vector b+16 / / VEC28 remap (comme
c54x_interrupt_ex/VEC28-EXP) : la frame IT tape sur * vec19/bit3 = stub
RETE ; le VRAI scheduler frame est vec28 (data[0xf0]->0x7234). *
Gated CALYPSO_DSP_FRAME_VEC28. On consomme le bit3 de l IFR mais on
vectorise 28. / { / @BEQUILLE —
DSP_FRAME_VEC28 (chemin IRQ-LEVEL) (CALYPSO_DSP_FRAME_VEC28, EXISTS) *
masque : le mapping ligne-frame-TPU -> vecteur DSP. Le modele livre
l’IT frame * sur vec19/bit3 (= stub RETE) ; on la reroute vers
vec28/bit12. * retirer : quand calypso_tpu.c cable la ligne frame sur le
bon vecteur a la source. * PIEGE : allumee sans etre demandee des que
CALYPSO_DSP=c54x (test ci-dessous). / static int lv28 = -1; if (lv28
< 0) { const char _d = getenv(“CALYPSO_DSP”); lv28 =
(getenv(“CALYPSO_DSP_FRAME_VEC28”) || (_d && !strcmp(_d,
“c54x”))) ? 1 : 0; } /* natif revive / if (lv28 && b == 3)
vec = 28; } s->ifr &= ~(1u << b); if (b == 12)
g_frame_it_level = false; / frame-IT vectorisee -> relache le
LEVEL hold / s->sp–; data_write(s, s->sp, (uint16_t)s->pc);
/ [2026-07-22] FIX DRIFT SP (racine du storm bootstub) : pousser
XPC SEULEMENT * en mode etendu (xpc!=0). Le firmware sort l ISR via POPM
ST1 + RCD (pop 1w=PC), * PAS RETE (pop 2w, path mort cf l.5294). Quand
xpc=0 (pas de paging), pousser * XPC laisse un mot orphelin JAMAIS
depile -> drift SP +1/IT -> SP wrap -> le RET * bootstub 0xab38
pop mem[0x5ac8]=0 au lieu de mem[0x5ac7]=retour -> PC=0 storm. * Vrai
c54x standard = push PC seul. Legacy: CALYPSO_IT_PUSH_XPC_ALWAYS=1.
/ { static int always = -1; if (always < 0) { const char e =
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); 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”, b, vec,
s->pc, iptr, s->imr, s->ifr, s->insn_count); return true;
}
/* [2026-07-28] GATE UNIFIE DES CORRECTIFS D EMULATION. *
CALYPSO_FIXES=FIX_UN,FIX_DEUX active des correctifs nommes *
CALYPSO_FIXES=all les active tous * (absent) aucun — comportement d
origine strictement inchange PROTOCOLE : on pose TOUS les
correctifs surs derriere ce gate d un coup, on teste * SOUS CHARGE
MAXIMALE (camp + LU + SMS, pas un simple boot), et DES QU UN CORRECTIF *
EST CONFIRME on efface la CONDITION, pas le correctif :
on retire le * if (calypso_fix_enabled("FIX_...")) { et ses
accolades, le code reste et devient * inconditionnel. Son nom disparait
alors de la liste des gates. * Ce gate est un SAS TEMPORAIRE, jamais une
option de configuration : une bequille * reste, un sas se vide. Ne
jamais y laisser vieillir un correctif valide. / static bool
calypso_fix_enabled(const char name) { static char buf[1024];
static int init = 0; if (!init) { const char e =
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)”, 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;
}
static 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); / [2026-07-27] B1 (gated CALYPSO_B1) : au kernel MAC
0xa076, dump la table * de reference du correlateur data[0x2c00..0x2c0f]
+ checksum -> tranche si * elle est peuplee (boot-copy
0x76f8->0x2c00 faite) ou VIDE (on correle * contre du zero). Le moins
cher / binaire. */ { static int _b1 = -1; static unsigned _b1n = 0; if
(_b1 < 0) _b1 = getenv(“CALYPSO_B1”) ? 1 : 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”, _ck,
s->insn_count); } } /* [2026-07-25] TEST-3FAE (gated
CALYPSO_FORCE_3FAE) : le handler FB poll * data[0x3fae] bit8 (0x0100)
via BITF @0x90c8/0x90ed/0x9128 puis BC TC
-> il * attend ce flag “burst pret” que RIEN n ecrit -> boucle
infinie, kernel * 0xa076 jamais atteint. On force le flag dans le
handler pour confirmer qu il * debloque vers le kernel (=> ensuite
wire depuis la chaine RX/BRINT0). / { / [2026-07-25] CORR-BANK2
(gated) : forcer XPC=2 dans la region corrélateur * -> le handler FB
tourne depuis PROM2 (overlay different) au lieu de PROM0. * Test “voir
si bank2 debloque”. Risque derail (RET/contexte). / / @BEQUILLE — CORR_BANK (CALYPSO_CORR_BANK,
VALEUR, defaut -1/OFF) * masque : la selection d’overlay/banque du
handler FB. On ECRASE s->xpc a * chaque instruction de
[0x8d00..0xa200] au lieu que le dispatcher * natif pose la bonne banque.
* retirer : quand le dispatcher CALA @0xb01e resout la banque correcte lui-meme *
(XPC observe == banque attendue sans forcage). * PIEGE : la valeur “0”
N’ETEINT PAS — elle force XPC=0. Seul unset coupe. / static int cbk
= -2; if (cbk == -2) { const char e = getenv(“CALYPSO_CORR_BANK”);
cbk = (e && e) ? atoi(e) : -1; } / -1=off ; 0..3 = XPC
force / 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, defaut OFF) * masque : l’ecriture des flags de handshake FB que
RIEN n’implemente — * data[0x3faa] bit2/bit8, [0x3fab] bit8, [0x3fae]
bit8. Poses a CHAQUE * instruction du handler (xpc=0, pc
0x8d00..0xa200). * retirer : quand la chaine RX/BRINT0 ecrit ces flags
(RANK2 resolu). / static int f3ae = -1; if (f3ae < 0) f3ae =
getenv(“CALYPSO_FORCE_3FAE”) ? 1 : 0; if (f3ae && s->xpc == 0
&& s->pc >= 0x8d00 && s->pc <= 0xa200) {
/ TOUTE la handshake FB-det que le handler poll (0x8866 + 0x90xx) :
* 0x3faa bit2/bit8, 0x3fab bit8, 0x3fae bit8. Decouple RANK3 du feed *
RX mort (RANK2) pour voir si le kernel se debloque. /
s->data[0x3faa] |= 0x0104; s->data[0x3fab] |= 0x0100;
s->data[0x3fae] |= 0x0100; } } / [2026-07-25] CORR-FLOW (gated
CALYPSO_CORR_FLOW) : trace FACTUELLE du flux du * handler FB en banc0
(0x8d00..0xa200, XPC=0) — PC/opcode BRUT + flags ST0(TC,C) * + A +
AR0/AR4/AR5. Permet de VERIFIER nous-memes (contre SPRU172) OU/POURQUOI
le * flux quitte le kernel MAC 0xa076 (lit 0x2a00). Marque
0xa076/0x9a80. Cap 8000. / { static int cf = -1; static unsigned cfn
= 0; if (cf < 0) cf = getenv(“CALYPSO_CORR_FLOW”) ? 1 : 0; /
Range ELARGIE : inclut 0x8866 (sous-routine handshake, <0x8d00) +
0xa076. * Trace AUSSI AR3 (ptr CMPS/coeff) et AR1/AR2 pour voir le setup
pointeurs. / / Skip la boucle de copie 0x8866-0x886c (op 8091,
~134x/appel) qui bouffait * tout le budget log -> le cap est reserve
au VRAI flux (state-machine + * progression vers 0x93a5). Dedup aussi
les PC repetes consecutifs. / static uint16_t cf_lastpc = 0; if (cf
&& s->xpc == 0 && s->pc >= 0x8600 &&
s->pc <= 0xa200 && cfn < 20000 / [2026-07-26 WF]
ne tracer QUE quand une vraie tache FB/SB est active * (task_md=5/6)
-> capture la fenetre POST-fix (fn>=6866) au lieu de * s epuiser
sur le spinning idle pre-fix (+0.8s). / &&
(s->data[0x0804] == 5 || s->data[0x0804] == 6 ||
s->data[0x0818] == 5 || s->data[0x0818] == 6 || s->pc >=
0xa000) / [fix] trace AUSSI le flux post-gate 0xa0xx (task_md=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”, 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 */
/* === CORR-TRACE (2026-06-02) : trace instruction-par-instruction la boucle
* MAC du corrélateur FB autour de 0x8576 à l'instant détection. Montre
* PC/opcode/AR3/A AVANT chaque instr : si AR3 ne bouge pas d'une ligne à
* l'autre, ou si A n'accumule pas, on a le coupable (post-incr *AR3+ / RPT
* / MAC). One-shot ~60 instr. CALYPSO_DEBUG=CORR-TRACE. */
/* DERAIL-EE00 (2026-06-02) : attrape le saut DANS la zone PROM vide 0xee00
* (op=0x0000) post-fix SACCD. Logge le PC source + opcode + XPC pour
* trancher runaway firmware (branche fausse) vs bug paging XPC (adresse
* légitime bankée fetchée page 0). One-shot ~12. */
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 = getenv("CALYPSO_CORR_LO"); const char *h = 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 (2026-05-29) ===
* Track AR1/AR2/AR6/AR7 transitions to 0 — when an AR pointer
* becomes 0, any subsequent indirect store *ARx will write to
* data[0x00] = IMR MMR (= clobber). Documented as the 2026-05-25
* fix reason (cf c54x_reset comment). Capture l'instruction qui
* a fait la transition (= last_exec_pc + s->prog[last_exec_pc])
* pour identifier le coupable. Gated CALYPSO_DEBUG=AR_CLOBBER. */
{
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 && 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, s->data[0x08E2], 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 && 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 && 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]);
/* la boucle go-live 0xa4de-0xa4e8 (pourquoi 0xa4e1 reboucle) + le
* soft-vector data[0x3f6d] qui pilote le 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]);
/* + le code du scheduler 0x7234 pour reconfirmer CALL 0x013b */
fprintf(stderr, "[c54x] PROG[0x7234..]= %04x %04x %04x %04x\n",
s->prog[0x7234], s->prog[0x7235], s->prog[0x7236], s->prog[0x7237]);
}
}
uint8_t hi4 = (op >> 12) & 0xF;
uint8_t hi8 = (op >> 8) & 0xFF;
/* [2026-07-28] LOT DE CORRECTIFS DE LONGUEUR — gate CALYPSO_FIXES (voir
* calypso_fix_enabled). Chaque entree cite binutils tic54x-opc.c, dont le 2e
* champ EST le nombre de mots. Le decodeur consommait 2 mots la ou ces
* instructions n en font qu 1, ce qui desynchronise tout le decodage suivant. */
{ /* [2026-07-28] Les correctifs ci-dessous sans appel a calypso_fix_enabled()
* sont VALIDES et INCONDITIONNELS (verifies en SHUNT_LEGIT sous charge et en
* NATIVE_HELPED avec retour du SHADOW-DADST). Ceux qui portent encore un
* calypso_fix_enabled("FIX_...") sont dans le SAS : formellement corrects mais
* INFIRMES PAR LA MESURE, voir leur commentaire. */
/* LD Xmem, SHFT, dst — binutils { "ld", 1,3,3, 0x9400, 0xFE00, {OP_Xmem,OP_SHFT,OP_DST} }
* (etait decode MVDK/MVKD sur 2 mots) */
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 }
* (etait decode MVDP sur 2 mots) */
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 }
* (etait decode ADD/SUB #lk sur 2 mots) */
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 }
* (etaient decodes AND #lk / MACP / MACD sur 2 mots).
* On execute la partie LD et on laisse la partie parallele : approximatif sur le
* RESULTAT, mais la LONGUEUR redevient juste et le flux cesse de deriver. */
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} }.
* Un MMR est une valeur 16 bits NON SIGNEE (un pointeur, un compteur, un
* registre d etat) : le sign-etendre transforme AR=0x8000 en une valeur
* negative de 40 bits. SPRU172C : « LDM MMR, dst : dst = MMR », sans
* extension de signe (LDU porte explicitement « uns », LDM n a pas de
* variante signee). */
if ((op & 0xFE00) == 0x4800 && calypso_fix_enabled("FIX_LDM_ZEROEXT")) {
int mmr = op & 0x7F;
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 est un operande LONG (2 mots) : le pointeur doit donc avancer de 2, pas
* de 1. Une post-modification de 1 decale tout le balayage d un tableau de mots
* longs — l erreur est silencieuse et cumulative. */
if ((op & 0xFE00) == 0x4E00) {
int src = (op >> 8) & 1;
int64_t v = src ? s->b : s->a;
uint8_t sm = op & 0xFF;
if (sm & 0x80) { /* indirect : *ARx avec post-modif */
int ar = sm & 0x7, mod = (sm >> 3) & 0xF;
uint16_t a = s->ar[ar];
data_write(s, a, (uint16_t)((v >> 16) & 0xFFFF));
data_write(s, a + 1, (uint16_t)(v & 0xFFFF));
if (mod == 0x2) s->ar[ar] = a + 2; /* *ARx+ : +2, pas +1 */
else if (mod == 0x1) s->ar[ar] = a - 2; /* *ARx- : -2, pas -1 */
return 1;
}
{ /* direct : DP:offset */
uint16_t a = (uint16_t)(((s->st0 & ST0_DP_MASK) << 7) | (sm & 0x7F));
data_write(s, a, (uint16_t)((v >> 16) & 0xFFFF));
data_write(s, a + 1, (uint16_t)(v & 0xFFFF));
return 1;
}
}
/* STL/STH src, SHFT, Xmem — binutils { "stl"/"sth", 1,.., 0x9800/0x9A00, 0xFE00,
* {OP_SRC1,OP_SHFT,OP_Xmem} }. Le champ SHFT (bits 3-0) etait ignore : la valeur
* stockee n avait pas la bonne echelle. */
if (((op & 0xFE00) == 0x9800 || (op & 0xFE00) == 0x9A00)
&& calypso_fix_enabled("FIX_STL_STH_SHFT")) {
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} }. Deux champs distincts : bit 9 = SRC
* (l accumulateur source) et bit 8 = DST. Le bit 9 etait ignore, donc la
* soustraction partait toujours du meme accumulateur. */
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 - (((int64_t)(int16_t)v) << 16);
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 } couvre 0x85 (src = B)
* (etait decode MVPD sur 2 mots). Miroir exact du handler 0x84 deja valide. */
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 }
* (etait decode MVDD sur 2 mots) */
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 (CALYPSO_DEBUG=DISP-ENTRY, c web 2026-05-29) : discriminateur
* préemption-IT vs clobber. Logge UNIQUEMENT l'entrée dispatcher 0x8341,
* avec DP/ST0/SP/AR2 + état IT (INTM/IFR/INT3-pending) + contexte de la
* DERNIÈRE IT servie (vec, Δinsn, PC+DP foreground préemptés) + prédiction
* du slot LUT qui sera lu à 0x834d = data[(DP<<7)|0x07] → handler vs garbage.
* DIFF entrées OK (DP=0x124) vs KO (DP≠0x124) : si KO ⟺ IT récente (Δinsn
* petit, fg_dp=DP-KO) → (b) préemption confirmée, root = INTM/IT. */
/* ORACLE (border, debug pas fix) : CALYPSO_FORCE_DP=0x124 force le champ DP
* de ST0 à l'entrée dispatcher 0x8341. Si FB lock + AFC converge → le bit
* est load-bearing, la chasse au DP périmé est justifiée. Sinon → faute DSP
* plus profonde DERRIÈRE le dispatcher, et chasser 0x3125 est prématuré. */
/* [2026-07-22] FORCE-DISPATCH (gated CALYPSO_FORCE_DISPATCH=1) : le scheduler
* frame 0x7234 (atteint via vec28) DERAILLE vers 0x013b car DP est garbage
* (d_dsp_page=0xf600). On force DP=0x124 (la page GSM correcte, ORACLE) a
* l'entree 0x7234 -> empeche le derail -> le flux natif atteint le dispatcher
* 0x8341 -> LUT tache FB -> correlateur 0x8d00. Gate force-dispatch. */
if (s->pc == 0x7234) {
/* [2026-07-22] DUMP one-shot du scheduler 0x7234 (gated AR0_DEBUG) : que
* fait-il, dou vient 0x013b (branche indirecte sur quel pointeur ?). */
if (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], s->data[0x08E2], 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, defaut OFF ;
* calypso_wire.env:=1)
* masque : le scheduler frame 0x7234 est atteint avec DP garbage et d_dsp_page
* a 0, donc la LUT 0x8341 ne resout pas et la tache GSM/FB n'est jamais
* dispatchee. On force DP=0x124 + data[0x08E2]=data[0x0584]=0x0002.
* retirer : des que le prologue 0x013b restaure un DP valide et que le producteur
* de d_dsp_page ecrit B_GSM_TASK (bit1) par le chemin ARM.
*/
static int fd = -1;
if (fd < 0) { const char *e = getenv("CALYPSO_FORCE_DISPATCH"); fd = (e && atoi(e) > 0) ? 1 : 0; }
if (fd) {
uint16_t old = (uint16_t)(s->st0 & 0x1FF);
uint16_t oldpg = s->data[0x08E2];
s->st0 = (uint16_t)((s->st0 & ~0x1FF) | (0x124 & 0x1FF));
/* donne B_GSM_TASK (bit1) a d_dsp_page (0x08E2) + copie shunt (0x0584)
* pour que la sous-routine 0x013b dispatche la tache GSM/FB. */
s->data[0x08E2] = 0x0002; /* B_GSM_TASK | w_page=0 */
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);
}
}
if (s->pc == 0x8341) {
/* @BEQUILLE — FORCE_DP (+ FORCE_DP_FROM comme scope) (CALYPSO_FORCE_DP, VALEUR,
* defaut OFF)
* masque : le champ DP de ST0 a l'entree du dispatcher est un residu de pile
* (over-pop / ST0 non restaure) et non la page de donnees attendue.
* retirer : des que la sonde DISP-ENTRY montre le dispatcher OK sans forcage,
* c.-a-d. quand l'equilibre de pile ST0 push/pop est sain.
*/
static int inited = 0, force_dp = -1, force_from = -1;
if (!inited) {
inited = 1;
const char *e = getenv("CALYPSO_FORCE_DP");
force_dp = (e && *e) ? (int)strtol(e, NULL, 0) : -1;
const char *ef = getenv("CALYPSO_FORCE_DP_FROM"); /* SCOPÉ : ne force que si DP==FROM */
force_from = (ef && *ef) ? (int)strtol(ef, NULL, 0) : -1; /* -1 = global (ancien) */
}
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 (s->pc == 0x8341 && calypso_debug_enabled("DISP-ENTRY")) {
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) { /* dispatcher BAD → dump ring ST0 push/pop (C-sweep) */
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) : trace le dispatcher de tâches
* 0x8341-0x8353 qui calcule la cible CALAD (0x8353 = CALAD A). Le bug :
* A_L finit = 0x70c3 (garbage) au lieu d'une entrée de la branch-table
* 0x8359 (B 0x8365/0x8394/...). On veut A à l'ENTRÉE (0x8341) = l'index
* pré-chargé par l'appelant (sélecteur de tâche / d_task_md). Si A est
* déjà garbage à 0x8341 → bug upstream confirmé (dispatcher innocent). */
if (s->pc >= 0x8341 && s->pc <= 0x8354 && calypso_debug_enabled("DISP-TRACE")) {
static unsigned disp_n = 0;
if (disp_n++ < 300) {
/* À 0x834d (op 0x6f07 = LD Smem<<1,A) : calcule l'EA direct exact
* (DP<<7)|dma et logge la valeur lue — c'est elle qui devient A.
* Légit = 0xff86 (→ A_L=0x8261) ; corrompu = 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 : log toute transition INTM 0→1.
* Le SSBX INTM orphelin se cache entre insn=89.83M (last write 0x3dd2)
* et insn=98.38M (entrée wait permanente). Cap à 200 transitions pour
* éviter le flood au boot ; capture le PC qui a fait passer INTM à 1
* et l'adresse de retour stack pour identifier le caller. */
{
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-10)&255], pc_ring[(pc_ring_idx-9)&255],
pc_ring[(pc_ring_idx-8)&255], pc_ring[(pc_ring_idx-7)&255],
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]);
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 = MVDD-family — documented cascade writer
* (`project_dtaskd_corruption_8e8x`) that writes garbage values
* into NDB cells (random vals at d_fb_det vs legitimate 0x001e).
* Track AR fields + B accumulator + source address read to find
* if it's true firmware compute or 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
* 15× 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-8)&255], pc_ring[(pc_ring_idx-7)&255],
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]);
}
}
}
/* === SEED-SOURCE probe (env-gated CALYPSO_PROBE_BOOTSTUB=1) ===
* Probe at PC=0xf8de (CALA B → 0x7700) — the SINGLE source event
* that spawns the entire boot-stub RET-loop cascade (per session
* 2026-05-24 BOOTSTUB-ENTRY analysis : 1 ENTER-7700 → 435 entries
* to PC=0x0000). Captures full state BEFORE the CALA fires :
* - SP + stack contents (what subsequent POPs will pull)
* - A, B (B = jump target)
* - AR0..AR7, ST0, ST1
* - 10-PC trail (extends visibility upstream of 0xf8de).
* Goal: identify whether the function containing 0xf8de was itself
* called with proper push, and what was supposed to be on stack
* when POPM ST0 + RCD UNC fire at dispatcher 0x7706/0x7707. */
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+0)], 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->data[(uint16_t)(s->sp+6)], s->data[(uint16_t)(s->sp+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-8)&255], pc_ring[(pc_ring_idx-7)&255],
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]);
}
}
}
/* === BOOTSTUB-ENTRY probe (env-gated CALYPSO_PROBE_BOOTSTUB=1) ===
* Traces every entry to PC=0x0000 (boot stub LDMM SP,B + RET).
* Boot stub re-entered at runtime is the documented-never-nailed
* seed of 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 (2026-05-29) ===
* Trigger à PC=0xFFCC (= INT3 vector entry, IPTR=0x1FF + vec 19*4).
* Capture les ~32 PCs suivants pour voir le chemin ISR.
* Objectif : identifier où DSP saute hors path attendu (= soit RSBX
* INTM dans zone 0xA4D0+, soit retour normal via RETE). Si DSP finit
* à 0x0000 boot stub → identifier l'opcode/PC qui dérive le saut.
* Gated par CALYPSO_DEBUG=INT3_VEC ou ALL. */
{
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 : à PC=0x8f51 (le PC qui écrit d_fb_det).
* Snapshot AR0..AR7 + data[AR0/1/2] + BK + A pour identifier la
* zone DARAM lue par le correlator FB-det au moment de produire
* sa valeur d'output. Comparer la zone source avec le BSP DMA
* target (default 0x3fb0..0x3fbf) :
* - zone source = BSP target → correlator lit bien les samples
* - zone source ≠ BSP target → mismatch source/sink, blocker
* structurel : DSP attend les samples ailleurs que là où le
* BSP les écrit. Suite : tracer init AR, table coeffs, ou
* MAC sur autre buffer. */
/* COEFFS-TABLE-DUMP : 1× au tout début + à chaque sweep FB-det.
* Dump data[0x2bc0..0x2bcF] (zone censée contenir les coefficients
* du correlator selon AR4 observé). 2026-05-14 : capture étendue
* D_FB_DET-WR-SITE a révélé data[AR4]=0x0000 sur 50 hits → la table
* de coeffs est VIDE en mémoire. Vérifier ici si elle l'est aussi
* en boot et si quelqu'un l'écrit jamais. */
{
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) {
/* Cap bumpé 50 → 500 (2026-05-14 night) pour couvrir plusieurs
* sweeps FB-det au lieu du seul premier. + stats agrégées sur
* tous les fires (cap n'est que pour le log per-fire). */
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 detection : AR3 retombe en dessous de la
* dernière valeur observée → nouveau sweep commence.
* Log le sweep précédent (count non-zero + A final + insn). */
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);
}
/* Stats summary toutes les 100 fires de 0x8f51 — distribution
* AR4-in-zone + histogramme val[AR4] sur tout l'historique. */
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 : à chaque trigger PC (sites d_fb_det), émet delta
* des reads par plage depuis le trigger précédent. Tranche entre :
* - dominant LOW → correlator lit la zone [0..0x3A3]
* - dominant APIRAM → samples viennent via API RAM (ARM-driven)
* - dominant WRAP → correlator tourne sur le wrap PROM1 mirror
* - dominant OTHER → zone non cataloguée à identifier */
read_stats_trigger_check(s);
throughput_tick(s->insn_count);
/* WAIT-A21A probe : à PC=0xa21a, snapshot INTM + IMR + IFR.
* Tranche H1/H2/H3 :
* INTM=1 + IFR=0 + IMR plein → H3 strict, hardware silencieux
* INTM=1 + IFR≠0 + IMR plein → H3 + IRQ pending bloquée (BUG)
* INTM=0 → H1/H2 (IRQ servable mais path
* vers 0x7740 cassé en amont) */
/* === CORR-PUBLISH-A probe (2026-05-28) ===
* À PC=0x9ac0 (juste avant STL A → *AR2-), snapshot A complet +
* AR2 (= adresse de publication). Cap 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 : à l'entrée 0x7740, log le contexte caller.
* data[sp] = adresse de retour pushée par le CALL/CALLD précédent.
* INTM=1 → on est dans un IRQ context. Permet de distinguer
* "appelé via IRQ ISR" vs "appelé via flow régulier", et de
* remonter la chaîne caller→callee jusqu'à l'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-10)&255], pc_ring[(pc_ring_idx-9)&255],
pc_ring[(pc_ring_idx-8)&255], pc_ring[(pc_ring_idx-7)&255],
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]);
}
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; /* AOV */
else if (cc == 0x60) cond = !(s->st0 & ST0_OVA); /* ANOV */
else if (cc == 0x78) cond = (s->st0 & ST0_OVB) != 0; /* BOV */
else if (cc == 0x68) cond = !(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 = RSBX INTM (enable interrupts), F4E3 = SSBX INTM (disable interrupts) */
/* 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;
/* [2026-07-26 WF golive-mac] FB-ENERGY REROUTE : le dispatch FB actif
* (CALA @0xb01e) resout vers le correlateur SYMBOLE 0x8d00 / stub 0xab38,
* qui ne touche jamais le buffer IQ 0x2a00 ni le kernel 0xa076. On
* redirige la CALA vers l entree du correlateur ENERGIE FB : 0x94f5
* (0x9500 pose AR4=0x2a00 -> f274 a033 -> a040 -> f273 a076). NB : sur ce
* chemin AR5=0x2c00 (reference), l IQ 0x2a00 est en AR4/AR1 (PAS AR5).
* Gate CALYPSO_FB_ENERGY ; entree override CALYPSO_FB_CORR_ENTRY. */
if (is_call && src_pc == 0xb01e) {
/* [2026-07-27] CALA-FB : cible NATIVE du dispatcher + d_task_md,
* loggee AVANT tout reroute (voir en-tete du patch). */
{ static int _cf = -1; static unsigned _cfn = 0;
if (_cf < 0) _cf = getenv("CALYPSO_CALA_FB") ? 1 : 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, defaut OFF)
* masque : l absence de DISPATCH NATIF vers le correlateur.
* On REROUTE l execution vers 0x9500 / 0x94f5 au lieu
* de laisser le firmware y arriver par son chemin.
* retirer : quand BRINT0 (vec 21) est servie et que le chemin
* natif atteint le correlateur seul.
* ⚠️ Toute mesure prise sous ce reroute est une mesure SOUS
* BEQUILLE : en natif pur, cet etage n est JAMAIS execute
* (mesure 2026-07-28 : CALYPSO_WATCH_9F00_RD = 0). */
const char *_e = getenv("CALYPSO_FB_ENERGY"); _fbe = (_e && atoi(_e) > 0) ? 1 : 0;
const char *_p = 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 (commande FB) */
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;
}
}
/* SURGICAL 2026-05-30 : self-CALA black-hole capture. Fire UNE
* fois quand un CALA cible lui-même dans la zone 0x7000-0x70FF
* (= le trou noir 0x70c3). Donne le DP hérité + le slot LUT lu
* au dispatcher 0x834d (ea/val) + le POPM ST0 et le LDP qui ont
* posé ce DP — le coupable complet, sans spam (≠ 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);
/* Dump pile autour de SP : montre l'orphelin (0xf487) et
* ses voisins. Si le vrai ST0 (genre 0x0xxx/0x4xxx, DP
* plausible) est à SP±1, c'est un imbalance d'1 mot. Les
* mots PC-shaped (0xf4xx/0x7xxx) empilés = drain cumulatif. */
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 : les 28 derniers push/pop (pc:op delta).
* Cherche un push (delta<0) sans pop apparié = la fuite. */
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");
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 acc, no push, no delay)
* F4E7 = FCALA A FL_FAR (far call acc, push 2 mots, no delay)
* F5E6 = FBACC B / F5E7 = FCALA B (acc B variants)
*
* Per binutils tic54x-opc.c (FL_FAR flag) and SPRU172C :
* XPC = A(22:16), PC = A(15:0). FCALA push XPC puis ret_pc (PC+1),
* ordre compatible avec FRET (F4E4 — pop PC d'abord puis XPC).
*
* 2026-05-27 (c web review): non-delayed variants WERE NOP-fallthrough
* via the F4E0-F4FF block below. Pre-XPC-fix le code far-acc n'était
* jamais atteint, post-fix il l'est → silent control-flow derailment.
* Sémantique identique au FCALAD/FBACCD (F6E6/F6E7) existant, sans
* delay slots. */
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 : push XPC first (deeper in stack), then ret_pc (top).
* FRET (F4E4) pops PC d'abord puis XPC — ordre compatible. */
s->sp = (s->sp - 1) & 0xFFFF;
data_write(s, s->sp, s->xpc);
uint16_t ret_pc = (uint16_t)(s->pc + 1);
s->sp = (s->sp - 1) & 0xFFFF;
data_write(s, s->sp, ret_pc);
}
s->xpc = new_xpc;
s->pc = tgt;
return 0;
}
/* F4E0-F4FF catch-all : NOP par défaut, sauf opcodes connus qui ont
* leur handler dédié. Comment historique "RSBX/SSBX" était faux
* (RSBX=F4B0, SSBX=F5B0 hors range). En fait ce range contient :
* F4E1: IDLE 1 ← exception (handler ligne ~4058)
* F4E2: BACC A (handler ligne ~3920)
* F4E3: CALA A (handler ligne ~3920)
* F4E4: FRET ← exception (handler ligne ~4041)
* F4E6: FBACC (handler ligne ~3974)
* F4E7: FCALA (handler ligne ~3974)
* F4EB: RETE ← exception (handler ligne ~4012)
* Sans l'exception F4E1, IDLE 1 était silencieusement avalé en NOP,
* empêchant DSP de signaler s->idle=true → IRQ handler ne dispatchait
* jamais → INTM stuck à 1 (2026-05-29 fix). */
if (op >= 0xF4E0 && op <= 0xF4FF &&
op != 0xF4E1 && op != 0xF4E4 && 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;
/* [2026-07-23] IT return SYMETRIQUE : l'entree IT (level_check l.4229 /
* interrupt_ex) pousse XPC SEULEMENT si xpc!=0 (mode etendu). Un XPC
* pousse vaut 0..3 ; un PC (0x0080+) est >3. Donc RETE ne depile XPC
* QUE si le sommet est un XPC valide (<=3) -> pop 1 quand PC seul pousse
* (xpc=0), pop 2 en mode etendu. Corrige le drift SP +1/IT (over-pop) qui
* faisait deriver SP -> RETE @0x0107 lisait PC-comme-XPC + garbage-comme-PC
* -> PC=0 (derail go-live cycle). L'ancien pop-2-inconditionnel supposait
* push-2 toujours, faux depuis le fix push-XPC-conditionnel (l.4219). */
uint16_t top = data_read(s, s->sp);
if (top <= 3) { /* XPC valide au sommet -> il a ete pousse */
s->xpc = top & 3; s->sp++; /* pop XPC */
}
uint16_t ra = data_read(s, s->sp); s->sp++; /* pop PC */
s->st1 &= ~ST1_INTM;
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). Pop PC + 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 FCALL/FCALLD push (also unconditional, see below).
* 2026-04-28 — fixed: was conditional on PMST_APTS (bit 4) which is
* actually AVIS (Address Visibility) per SPRU131G — has no stack
* semantics. The misnomer caused FRET to skip XPC pop when AVIS=0,
* leading to stack imbalance against FCALL FAR which always pushes 2. */
if (op == 0xF4E4) {
uint16_t ra = data_read(s, s->sp); s->sp++;
uint16_t prev_xpc = s->xpc;
uint16_t nx = data_read(s, s->sp); s->sp++;
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 legitime (masque & 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);
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 = (op >> 8) & 1, dst = (op >> 9) & 1;
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 = (op >> 8) & 1, dst = (op >> 9) & 1;
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;
}
/* F486/F586: MAX src (mask FEFF, 1 word) — keep max of A,B
* F-AUDIT 2026-05-25 v5 : était à 0xF492 (= roltc per binutils).
* binutils tic54x-opc.c : "max" 1,1,1, 0xF486, 0xFEFF
* → constant moved from 0xF492 to 0xF486 (impl est correct). */
if ((op & 0xFEFF) == 0xF486) {
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;
}
/* F487/F587: MIN src (mask FEFF, 1 word) — keep min of A,B
* F-AUDIT 2026-05-25 v5 : était à 0xF493 (= cmpl per binutils).
* binutils : "min" 1,1,1, 0xF487, 0xFEFF
* → constant moved from 0xF493 to 0xF487. */
if ((op & 0xFEFF) == 0xF487) {
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;
}
/* === MAC/MAS family Smem,SRC (0x28xx..0x2Fxx, mask FE00, 1 word).
* Per tic54x-opc.c + tic54x_hi8_map.md :
* 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 bit) : si set, produit shift << 1 (Q15*Q15 = Q31).
*
* BUG observé : MAC family non-implémentée → DSP correlator
* ne fait jamais d'accumulation, A reste stale → a_sync_ANG
* écrit 0x498D constant (garbage acc state).
* Implémentation Smem-only ici (variantes Xmem/Ymem dual-MAC
* 0xA000..0xBFFF non couvertes). */
if ((op & 0xFC00) == 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;
/* bit 8 selects SRC accumulator (A=0/B=1).
* Actually per binutils encoding bit 9 is op variant (mac/mas)
* and bit 8 is round (R). The SRC selector is bit 0 of Smem?
* No — looking at tic54x table: opcode 0x2800/FE00 encodes :
* bits 9..15 = op family (mac/mas/macr/masr)
* bit 8 = SRC (A=0, B=1)
* bits 0..7 = Smem
* Mais l'encoding pose mac=0x28xx (bit 8=0=A), 0x29xx (bit 8=1=B). */
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 consumed + s->lk_used;
}
/* MACR/MASR (mask FE00, base 0x2A00/0x2E00) : same + round +0x8000.
* bit 9 distingue add/sub : déjà géré ci-dessus via mac_sub. mais le
* round est sur les opcodes 0x2A.../0x2E... → bit 10 ? Re-check */
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, ¯_ind);
int16_t macr_mem = (int16_t)data_read(s, macr_addr);
int32_t macr_prod = (int32_t)(int16_t)s->t * (int32_t)macr_mem;
if (s->st1 & ST1_FRCT) macr_prod <<= 1;
macr_prod += 0x8000; /* round */
macr_prod &= ~0xFFFF; /* zero low half after round */
int macr_dst = (op >> 8) & 1;
int64_t *macr_acc = macr_dst ? &s->b : &s->a;
int64_t macr_term = (int64_t)(int32_t)macr_prod;
if (macr_sub) macr_term = -macr_term;
*macr_acc = sext40((*macr_acc + macr_term) & 0xFFFFFFFFFFULL);
return consumed + s->lk_used;
}
/* 0x3500 MACA Smem [, B] (mask FF00, 1 word) — B = B + A.hi * data[Smem].
* Spécial : utilise A.hi (= A[31:16]) comme multiplicateur. */
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);
int16_t maca_ahi = (int16_t)((s->a >> 16) & 0xFFFF);
int32_t maca_prod = (int32_t)maca_ahi * (int32_t)maca_mem;
if (s->st1 & ST1_FRCT) maca_prod <<= 1;
s->b = sext40((s->b + (int64_t)(int32_t)maca_prod) & 0xFFFFFFFFFFULL);
return 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);
int16_t masa_ahi = (int16_t)((s->a >> 16) & 0xFFFF);
int32_t masa_prod = (int32_t)masa_ahi * (int32_t)masa_mem;
if (s->st1 & ST1_FRCT) masa_prod <<= 1;
s->b = sext40((s->b - (int64_t)(int32_t)masa_prod) & 0xFFFFFFFFFFULL);
return consumed + s->lk_used;
}
/* 0x3700 MACAR Smem [, B] = MACA + 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);
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);
return 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);
int16_t mpya_ahi = (int16_t)((s->a >> 16) & 0xFFFF);
int32_t mpya_prod = (int32_t)mpya_ahi * (int32_t)mpya_mem;
if (s->st1 & ST1_FRCT) mpya_prod <<= 1;
s->b = sext40((int64_t)(int32_t)mpya_prod);
return 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 consumed + s->lk_used;
}
/* 0x3200 LD Smem, ASM (mask FF00, 1 word) — ASM = data[Smem] & 0x1F (5 bits). */
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 consumed + s->lk_used;
}
/* 0x3400 BITT Smem (mask FF00, 1 word) — TC = data[Smem] bit
* (15 - T(3:0)). Per binutils tic54x-opc.c "bitt" 0x3400 / 0xFF00.
*
* BUG observé pré-fix : BITT non-implémenté → TC stale →
* ROLTC (0xF492) immédiatement après à PC=0x9abf utilise TC
* faux → A.LOW computé garbage → STL A,*AR2- à PC=0x9ac0
* écrit a_sync_ANG = 0x498D constant (= résidu, pas un vrai
* phase compute). DSP correlator angle inopérant.
* binutils : { "bitt", 1,1,1, 0x3400, 0xFF00, {OP_Smem}, FL_SMR } */
if ((op & 0xFF00) == 0x3400) {
bool bitt_ind;
uint16_t bitt_addr = resolve_smem(s, op, &bitt_ind);
uint16_t bitt_val = data_read(s, bitt_addr);
int bitt_idx = 15 - (s->t & 0xF);
int bitt_b = (bitt_val >> bitt_idx) & 1;
if (bitt_b) s->st0 |= ST0_TC; else s->st0 &= ~ST0_TC;
return consumed + s->lk_used;
}
/* F492/F592: ROLTC src (rotate left through TC, mask FEFF, 1 word)
* F-AUDIT 2026-05-25 v5 : NOUVEAU handler. binutils :
* "roltc" 1,1,1, 0xF492, 0xFEFF — était mis-décodé en MAX.
* Semantic SPRU172C : src bit 31 → TC, src << 1, src bit 0 ← TC_old.
* Bug observé pré-fix : A_low devenait 0 systématiquement via le
* faux MAX (A=B if A<B) à PC=0x9abf, causant cascade STL→IMR=0. */
if ((op & 0xFEFF) == 0xF492) {
int src = (op >> 8) & 1;
int64_t *acc = src ? &s->b : &s->a;
int64_t v = *acc & 0xFFFFFFFFFFLL;
int new_tc = (int)((v >> 31) & 1);
int old_tc = (s->st0 & ST0_TC) ? 1 : 0;
*acc = sext40(((v << 1) | (int64_t)old_tc) & 0xFFFFFFFFFFULL);
if (new_tc) s->st0 |= ST0_TC; else s->st0 &= ~ST0_TC;
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). */
if ((op & 0xFEFF) == 0xF48F) {
int src = (op >> 8) & 1;
int64_t val = sext40(src ? s->b : s->a);
/* Check bits 39 and 38 — if they differ, shift left */
int bit39 = (val >> 39) & 1;
int bit38 = (val >> 38) & 1;
if (bit39 != bit38) {
val = sext40(val << 1);
if (src) s->b = val; else s->a = val;
s->t = (uint16_t)(s->t - 1);
}
/* TC flag: set if shift occurred */
if (bit39 != bit38)
s->st0 |= ST0_TC;
else
s->st0 &= ~ST0_TC;
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 = (s->st0 >> 8) & 1; /* 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 = (s->st0 >> 8) & 1;
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 = (op >> 8) & 1, dst = (op >> 9) & 1;
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;
}
/* F493/F593: CMPL src (complement, mask FCFF, 1 word)
* F-AUDIT 2026-05-25 v5 : était à 0xF486. binutils :
* "cmpl" 1,1,2, 0xF493, 0xFCFF, {OP_SRC,OPT|OP_DST}
* → constant moved from 0xF486 to 0xF493. Mask FEFF→FCFF
* (= permet variant SRC=B via bit 9). */
if ((op & 0xFCFF) == 0xF493) {
int src = (op >> 8) & 1;
int64_t *acc = src ? &s->b : &s->a;
*acc = sext40(~(*acc) & 0xFFFFFFFFFFULL);
return consumed + s->lk_used;
}
/* F49F/F59F: RND src (round, mask FCFF, 1 word)
* F-AUDIT 2026-05-25 v5 : était à 0xF487. binutils :
* "rnd" 1,1,2, 0xF49F, 0xFCFF, {OP_SRC,OPT|OP_DST} */
if ((op & 0xFCFF) == 0xF49F) {
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 = (op >> 8) & 1, dst = (op >> 9) & 1;
int64_t sv = sext40(src ? s->b : s->a);
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 = (op >> 8) & 1, dst = (op >> 9) & 1;
int64_t sv = sext40(src ? s->b : s->a);
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) */
if ((op & 0xFCFF) == 0xF482) {
int src = (op >> 8) & 1, dst = (op >> 9) & 1;
int64_t sv = sext40(src ? s->b : s->a);
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 = (op >> 8) & 1, dst = (op >> 9) & 1;
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 = (op >> 8) & 1, dst = (op >> 9) & 1;
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 = (op >> 8) & 1, dst = (op >> 9) & 1;
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 src,shift,dst — arithmetic shift accumulator */
int src = (op >> 8) & 1, dst = (op >> 9) & 1;
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;
}
/* [2026-07-03] FIX-SFTL-RSBX-COLLISION (gated CALYPSO_FIX_SFTL_RSBX,
* default OFF). Per doc/opcodes/tic54x_hi8_map.md:141-143, hi8 0xF4-0xF7
* base add/shift pattern (mask 0xFCE0/0xF400) explicitly excludes RSBX
* (0xF4B0/0xFDF0) and SSBX (0xF5B0/0xFDF0): nibble low-byte high-nibble
* == 0xB (bits 7:4 = 1011) is ALWAYS rsbx/ssbx for every hi8 in
* {F4,F5,F6,F7} (bit9=ST0/ST1, bit8=rsbx/ssbx), NEVER a legal SFTL shift
* amount. This check's mask (0xFCE0) leaves bit4 don't-care and is not
* gated by hi8, so it silently swallows 0xF4Bx/F5Bx/F6Bx/F7Bx (incl.
* RSBX INTM=0xF6BB, SSBX INTM=0xF7BB) as a bogus accumulator shift
* BEFORE the real hi8==0xF6/0xF7 handlers further down ever run.
* Independent of and orthogonal to CALYPSO_FIX_MVDM -- does not touch
* that opcode family. Default OFF: behavior unchanged unless enabled. */
{
static int fix_sftl_rsbx = -1;
if (fix_sftl_rsbx < 0)
fix_sftl_rsbx = getenv("CALYPSO_FIX_SFTL_RSBX") ? 1 : 0;
/* NATIF 2026-07-20 : RSBX/SSBX (low-byte nibble 0xB = 0x?Bx) ne sont
* JAMAIS un shift accumulator legal (cf binutils tic54x-opc.c). Exclusion
* INCONDITIONNELLE du pattern shift -> ils tombent dans leur vrai handler
* RSBX/SSBX. C etait CALYPSO_FIX_SFTL_RSBX (default OFF) -> rendu natif.
* Sans ca, RSBX INTM=0xF6BB etait avale en shift bidon -> INTM jamais clear. */
if ((op & 0xFCE0) == 0xF4A0 &&
(op & 0xF0) != 0xB0) {
/* SFTL src,shift,dst — logical shift accumulator */
int src = (op >> 8) & 1, dst = (op >> 9) & 1;
int shift = op & 0x1F; if (shift > 15) shift -= 32;
uint64_t uv = (uint64_t)((src ? s->b : s->a) & 0xFFFFFFFFFFULL);
if (shift >= 0) uv <<= shift; else uv >>= (-shift);
uv &= 0xFFFFFFFFFFULL;
if (dst) s->b = sext40(uv); else s->a = sext40(uv);
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. */
/* Note: 0xF4E4 = IDLE (handled above, not FRET).
* Real FRET = 0xF072 (algebraic), handled in F0xx section. */
/* 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 = (op >> 8) & 1, dst = (op >> 9) & 1;
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 = (op >> 8) & 1, dst = (op >> 9) & 1;
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). */
if ((op & 0xFEFF) == 0xF48F) {
int src = (op >> 8) & 1;
int64_t val = sext40(src ? s->b : s->a);
/* Check bits 39 and 38 — if they differ, shift left */
int bit39 = (val >> 39) & 1;
int bit38 = (val >> 38) & 1;
if (bit39 != bit38) {
val = sext40(val << 1);
if (src) s->b = val; else s->a = val;
s->t = (uint16_t)(s->t - 1);
}
/* TC flag: set if shift occurred */
if (bit39 != bit38)
s->st0 |= ST0_TC;
else
s->st0 &= ~ST0_TC;
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 = (s->st0 >> 8) & 1; /* 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 = (s->st0 >> 8) & 1;
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 = (op >> 8) & 1, dst = (op >> 9) & 1;
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 = (op >> 8) & 1, dst = (op >> 9) & 1;
int64_t sv = sext40(src ? s->b : s->a);
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 = (op >> 8) & 1, dst = (op >> 9) & 1;
int64_t sv = sext40(src ? s->b : s->a);
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) */
if ((op & 0xFCFF) == 0xF482) {
int src = (op >> 8) & 1, dst = (op >> 9) & 1;
int64_t sv = sext40(src ? s->b : s->a);
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 = (op >> 8) & 1, dst = (op >> 9) & 1;
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 = (op >> 8) & 1, dst = (op >> 9) & 1;
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 = (op >> 8) & 1, dst = (op >> 9) & 1;
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 src,shift,dst — arithmetic shift accumulator */
int src = (op >> 8) & 1, dst = (op >> 9) & 1;
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;
}
/* [2026-07-03] FIX-SFTL-RSBX-COLLISION (gated CALYPSO_FIX_SFTL_RSBX,
* default OFF). Per doc/opcodes/tic54x_hi8_map.md:141-143, hi8 0xF4-0xF7
* base add/shift pattern (mask 0xFCE0/0xF400) explicitly excludes RSBX
* (0xF4B0/0xFDF0) and SSBX (0xF5B0/0xFDF0): nibble low-byte high-nibble
* == 0xB (bits 7:4 = 1011) is ALWAYS rsbx/ssbx for every hi8 in
* {F4,F5,F6,F7} (bit9=ST0/ST1, bit8=rsbx/ssbx), NEVER a legal SFTL shift
* amount. This check's mask (0xFCE0) leaves bit4 don't-care and is not
* gated by hi8, so it silently swallows 0xF4Bx/F5Bx/F6Bx/F7Bx (incl.
* RSBX INTM=0xF6BB, SSBX INTM=0xF7BB) as a bogus accumulator shift
* BEFORE the real hi8==0xF6/0xF7 handlers further down ever run.
* Independent of and orthogonal to CALYPSO_FIX_MVDM -- does not touch
* that opcode family. Default OFF: behavior unchanged unless enabled. */
{
static int fix_sftl_rsbx2 = -1;
if (fix_sftl_rsbx2 < 0)
fix_sftl_rsbx2 = getenv("CALYPSO_FIX_SFTL_RSBX") ? 1 : 0;
if ((op & 0xFCE0) == 0xF4A0 &&
(fix_sftl_rsbx2 == 0 || (op & 0xF0) != 0xB0)) {
/* SFTL src,shift,dst — logical shift accumulator */
int src = (op >> 8) & 1, dst = (op >> 9) & 1;
int shift = op & 0x1F; if (shift > 15) shift -= 32;
uint64_t uv = (uint64_t)((src ? s->b : s->a) & 0xFFFFFFFFFFULL);
if (shift >= 0) uv <<= shift; else uv >>= (-shift);
uv &= 0xFFFFFFFFFFULL;
if (dst) s->b = sext40(uv); else s->a = sext40(uv);
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) {
/* FIRS catch RETIRÉ (2026-05-25 v3, Claude web review).
*
* Le bloc `if (hi8 == 0xF1) { FIRS treatment }` qui était ici
* était FAUX : per binutils tic54x-opc.c, vrai FIRS = 0xE000
* mask 0xFF00 (handled separately at the 0xE0 case), JAMAIS
* 0xF1xx. Le catch-all capture-tout 0xF1xx faisait :
* s->a = sext40((int64_t)sum << 16);
* → A_low = 0 inconditionnellement → STL A,*AR2- à PC=0x9ac0
* écrivait 0 à mem[AR2] qui se trouvait être MMR_IMR (0x12 via
* self-aliasing) → IMR cleared → DSP bloqué (bloqueur #2).
*
* Diagnostic via A provenance tracer (CALYPSO_A_TRACE_PC=0x9ac0)
* a montré last_writer = PC=0x9abd op=0xf1fe = `SFTL A,-2,B`
* (binutils mask 0xFCE0 base 0xF0E0). SFTL handler EXISTE à
* L3915, capture correctement 0xF1FE & 0xFCE0 = 0xF0E0.
*
* Après retrait du catch, les 0xF1xx tombent dans :
* - SFTL/AND/OR/XOR 1-word (mask FCE0) : L3915
* - AND/OR/XOR/MAC #lk<<16 (mask FCFF) : L3852
* - AND/OR/XOR #lk+shift (mask FCF0) : L3886
* Si une opcode 0xF1xx n'a pas de handler (par ex. add/sub lk
* variants avec DST=B), tombe à F4xx unhandled NOP log à la fin
* du bloc → diagnostic visible. */
/* 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;
}
/* 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;
}
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: RSBX/SSBX */
if ((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;
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 = dst_sel ? &s->b : &s->a;
switch (sub6) {
case 0: *dst = sext40(src_val + ((int64_t)(int16_t)op2 << 16)); break;
case 1: *dst = sext40(src_val - ((int64_t)(int16_t)op2 << 16)); break;
case 2: dst = src_sel ? &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 = src_sel ? &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), bits 9:8 = src,dst
*
* Fix 2026-05-25 v4 : src/dst inversés. Per binutils
* tic54x convention (et confirmé par L3915 même opcode
* famille mais handler shadowed) : bit 9 = SRC,
* bit 8 = DST. L'inversion mettait dst=A pour 0xf1fe
* (= SFTL A,-2,B), qui calculait A = B>>2 au lieu de
* B = A>>2. Si B=0 → A=0 → STL A,*AR2- pose 0 à IMR
* (bloqueur #2 racine, cf [[blocker-2-dsp-dispatcher]]).
* Diagnostic via A-AT-PC tracer : 8454 fires A_low=0
* last_writer=0xf1fe @PC=0x9abd. */
int src_sel = (op >> 9) & 1; /* bit 9 = SRC (was bit 8) */
int dst_sel = (op >> 8) & 1; /* bit 8 = DST (was bit 9) */
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 (shift >= 0) shifted = sv << shift;
else shifted = sv >> (-shift);
switch (aop) {
case 4: *dst = sext40(sv) & sext40(shifted); break;
case 5: *dst = sext40(sv) | sext40(shifted); break;
case 6: *dst = sext40(sv) ^ sext40(shifted); break;
case 7: { uint64_t uv = (uint64_t)(sv & 0xFFFFFFFFFFULL);
if (shift >= 0) uv <<= shift; else uv >>= (-shift);
*dst = sext40(uv & 0xFFFFFFFFFFULL); } break;
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).
* Push PC+4 (return = past CALLD + 2 delay slots), PUIS exécute les
* 2 delay-slots via delayed_pc/delay_slots AVANT de brancher.
* Fix 2026-05-30 : était saut immédiat (s->pc=op2; return 0) → les
* 2 delay-slots étaient SKIPPÉS ; si un slot contient un push/pop,
* la pile se désaligne d'1 mot → POPM ST0 ramasse un PC orphelin
* (ex. 0x80fd) → DP garbage → CALA 0x70c3 = trou noir → TOA figé →
* AFC bloqué. Arme la machinerie delay_slots (comme RCD/RETED). */
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). AUCUNE pile.
* Per tic54x-opc.c: bd 0xF273 mask 0xFFFF. Le vrai RETD = 0xFE00
* (hi8==0xFE, géré plus bas avec pop + delay_slots).
* Fix 2026-05-30 : était traité comme RETD (pop parasite) → SP
* désaligné d'1 mot par BD → POPM ST0 0xf48b pop un PC orphelin
* → DP=0x087 → dispatcher 0x8341 → CALAD 0x70c3 = trou noir.
* Identique au B (F073) ci-dessus : saut, pas de pile.
* Fix 2026-05-30 v2 : était saut IMMÉDIAT (skip des 2 delay-slots) →
* si un slot a un push/pop, pile désalignée → POPM ST0 orphelin →
* 0x70c3. Arme delay_slots=2 pour exécuter les slots avant le saut. */
op2 = prog_fetch(s, s->pc + 1);
consumed = 2;
s->delayed_pc = op2;
s->delay_slots = 2;
return consumed + s->lk_used;
}
/* === F2xx dispatch (audit F-class 2026-05-25) =====================
*
* Per binutils tic54x-opc.c, les ALU masks FCF0/FCFF/FCE0 couvrent
* F0xx/F1xx/F2xx/F3xx avec bit 9=SRC bit 8=DST (= convention
* binutils stricte). F2xx était le seul gap :
* - F0/F1 → handler legacy L3565 (convention REVERSED bit 8=src,
* gardée pour back-compat ; firmware s'y est calé)
* - F3 → handler dispatch L3966 (binutils convention OK)
* - F2 → fallthrough vers unimpl → 0xf210 tight loop at PC=0xfbd9
*
* Bug runtime résolu : op=0xf210 op2=0x0008 → `SUB #8,B,A` (next BC
* fbe2, ALEQ → wait loop pre-correlator). Confirmed across 3 silicon
* ROM dumps (3416, 3606, our local) — cf doc/datasheets/.
*
* Coverage (binutils strict) :
* - 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) restent gérés AVANT
* (handlers ci-dessus), inchangés. */
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: result = src + ((int64_t)(int16_t)op2 << 16); break;
case 0x1: result = src - ((int64_t)(int16_t)op2 << 16); break;
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 hit ici */
(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 & 0x8) ? (shift_raw - 16) : shift_raw;
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: result = src + lk_val; break;
case 0x1: result = src - lk_val; break;
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 = (shift >= 0) ? (dst_in << shift)
: (dst_in >> (-shift));
result = src & sh; break; }
case 0x5: { int64_t dst_in = dst_b ? s->b : s->a;
int64_t sh = (shift >= 0) ? (dst_in << shift)
: (dst_in >> (-shift));
result = src | sh; break; }
case 0x6: { int64_t dst_in = dst_b ? s->b : s->a;
int64_t sh = (shift >= 0) ? (dst_in << shift)
: (dst_in >> (-shift));
result = src ^ sh; break; }
case 0x7: { uint64_t usrc = (uint64_t)src & 0xFFFFFFFFFFULL;
result = (int64_t)((shift >= 0) ? (usrc << shift)
: (usrc >> (-shift)));
break; }
}
if (dst_b) s->b = sext40(result); else s->a = sext40(result);
return consumed + s->lk_used;
}
/* F2xx unmapped — log-once + NOP fallback. Si tu vois ce log,
* c'est qu'un bit pattern F2xx n'est pas couvert (audit incomplete). */
{ 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;
}
/* LMS Xmem, Ymem — Least Mean Square step (1-word dual-operand)
* Encoding: 1111 001D XXXX YYYY
* Per SPRU172C: dst += T * Xmem; Ymem += rnd(AH * T); T = Xmem
* Exclude F272 (RPTBD), F273 (RETD), F274 (CALLD) — exact-match
* opcodes that share the F2xx range but are handled below. */
/* REMOVED 2026-05-08 night : the previous "LMS Xmem,Ymem" handler
* for hi8 ∈ {0xF2, 0xF3} (excluding F272/F273/F274) was mis-decoded
* — it claimed encoding `1111 001D XXXX YYYY` but per binutils
* tic54x-opc.c LMS is actually :
*
* { "lms", 1,2,2, 0xE100, 0xFF00, {OP_Xmem,OP_Ymem}, ... }
*
* i.e. hi8 == 0xE1, NOT 0xF2/F3. The 0xE1 handler already exists
* (line ~3247) and is correct.
*
* The F2xx/F3xx range per binutils contains only :
* F272 RPTBD, F273 RETD, F274 CALLD (3 special-cases)
* F300-F31F INTR k (handled below)
* F330-F35F AND/OR/XOR with shift (mask FCF0) (handled below)
* F360-F367 ADD/SUB/AND/OR/XOR/MAC #lk (mask FCFF) (handled below)
* F380-F3FF AND/OR/XOR/SFTL src,SHIFT,DST (FCE0) (handled below)
* F320-F32F + F368-F37F unmapped (NOP fallback)
*
* The bogus LMS catch-all stole every F3xx instruction before the
* proper F3 dispatch could see it. For 0xF3E1 (= SFTL B,1,B,
* 4872 sites in firmware) it computed `new_ym = AH*T-derived junk`
* and called data_write(s, AR1, new_ym). When AR1=0, that wrote
* the junk to MMR_IMR. This is the IMR-thrash cascade observed
* post-0x76-fix at PC=0x8eb9.
*
* Discovered after the 0x76 fix exposed the second-level cascade.
* Trace evidence : IMR-W 0x0000→{0x0540, 0x0525, 0x082b, 0xfd57,
* 0xfacf, ...} all PC=0x8eb9 op=0xf3e1, INTM-TRANS XPC=0
* (confirms genuine PROM0 execution, not XPC artifact).
*
* Fix : let the existing F3 dispatch (line 2468+) handle F3xx
* properly. F2xx (other than F272/3/4) falls through to F-class
* NOP fallback — 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.
*
* REVERTED 2026-05-15 nuit : tentative de fix vers SPRU172C-strict
* cond eval (cond=0x20=NTC, cond=0x30=TC) a cassé le firmware DSP
* Calypso (DSP stuck à PC=0xcc51 / 0xfa95 selon régime, task=24
* tombait à 0). Le binaire DSP semble utiliser une convention
* dialectale où F82x/F83x s'attend au comportement ACC-based.
* Hypothèse alternative : BITF (0x61) émulé incorrectement, TC
* jamais set correctement → cond NTC/TC ne donne pas le bon
* résultat. Investiguer BITF avant de retenter le fix BC strict. */
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;
/* [2026-07-25] §4-G FIX (gated CALYPSO_C54X_FIX_BC, def OFF) : decode
* BC 0xF8 par le VRAI champ cond 8-bit (c54x_cond_true, ISA-fidele)
* au lieu de l heuristique ACC dialectale. F820=cc0x20(NTC),
* F830=cc0x30(TC). Corrige le flux du handler FB (branches 0x9062-
* 0x90f0) qui n atteint jamais le kernel MAC 0xa076. Gate OFF car le
* fix strict a casse 2x avant (TC/BITF) -> valider via chaine de tests. */
{
static int fixbc = -1;
if (fixbc < 0) fixbc = getenv("CALYPSO_C54X_FIX_BC") ? 1 : 0;
if (fixbc) {
if (c54x_cond_true(s, op & 0xFF)) { s->pc = op2; return 0; }
return consumed + s->lk_used;
}
}
/* FIX 2026-06-23 (deblocage bacc bootloader) : F820=bc ntc /
* F830=bc tc (SPRU172C cc=0x20 NTC, 0x30 TC). L'ancienne
* heuristique ACC (take=A!=0) gelait le poll @0xb427 (bc ntc
* apres cmpm#2 : doit sortir sur TC=1=data==2 -> bacc 0x7000).
* cmpm ET bitf posent TC correctement ; on l'utilise SEULEMENT
* quand l'instruction precedente est un poseur-de-TC
* (cmpm/bitf = 0x60xx/0x61xx, mask 0xFE00) - sinon heuristique
* ACC heritee pour les sites dispatcher (blanket TC-strict avait
* casse le 2026-05-15, avant le fix cmpm). */
/* [2026-07-02] go-live DSP : la boucle compute du state-machine
* handshake (0xde0d-0xde26) se termine par F830 de0d = BC TC.
* Rien dans la boucle ne pose TC (6d91=MAR, f5a9=RPT-fallback),
* donc sur vrai C54x TC=0 -> BC TC NON pris -> la boucle tourne
* UNE fois et tombe en 0xde28 vers le setter 0xde9c. L heuristique
* ACC heritee (take si A==0, et A=0 ici via f0e1) la piege en
* boucle infinie. Gate PC-range + env : semantique BC TC reelle
* uniquement sur 0xde0d-0xde26. Defaut OFF -> aucun risque ailleurs. */
static int bctc_sm = -1;
if (bctc_sm < 0) bctc_sm = getenv("CALYPSO_C54X_BCTC_SM") ? 1 : 0;
bool tc_strict = ((g_prev_op & 0xFE00) == 0x6000) ||
(bctc_sm && s->pc >= 0xde0d && s->pc <= 0xde26);
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);
}
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.
*
* Earlier this range was treated as plain B pmad — a bug that
* kept XPC=0 forever (DSP never reached 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, ACC-condition (2 mots). FIX 2026-06-23 (ROOT du
* derail SP) : ces opcodes sont des BC, PAS des BANZ (BANZ = 0x6Cxx, un
* encodage DISJOINT — SPRU172C:16188/16266). cc = octet bas : cc&0x40 =
* groupe ACC, cc&0x08 = B vs A, cc&0x07 = test {2:GEQ 3:LT 4:NEQ 5:EQ
* 6:GT 7:LEQ}. Le firmware @0x772f émet F844 = BC 0x7737,ANEQ (branche si
* A!=0). L'ANCIEN décode `BANZ AR4` testait/décrémentait AR4 au lieu de
* l'accumulateur → la branche tirait sur AR4!=0 → atterrissait sur le
* POPM ST0 @0x7737 SANS son PSHM ST0 (@0x770d, chemin disjoint) → pop
* orphelin → SP monte +1/tour → DP=0x124 → derail → SP collapse → spin.
* BC ne push RIEN (pile intacte) ; SURGICAL : 0x4/0x5 seul, 0x2/0x3
* (dialecte, fix BC strict reverté 2026-05-15) inchangés ; disjoint du
* catch-all 0x7000 (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 réservé */
}
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).
*
* 2026-04-29 — replaces previous "F320+ → LD #k9, DP" fallback
* which mass-mis-decoded 364 firmware sites. Wedge at PC=0x8eb9
* (0xF3E1 SFTL B,1,B) was directly tied to this bug.
* See doc/opcodes/0xF3.md for full spec. */
if (hi8 == 0xF3) {
/* === F300-F31F INTR k REMOVED (2026-05-25 night, audit F-class)
*
* AUDIT-FINDING : the "INTR k" handler placed at 0xF300-0xF31F
* was WRONG. Per binutils tic54x-opc.c L311 :
* { "intr", 1,1,1, 0xF7C0, 0xFFE0, ... } ← REAL INTR k
* INTR k base is 0xF7C0, NOT 0xF300. The F3xx range belongs to
* ALU #lk class (per mask 0xFCF0).
*
* Symptom captured runtime (CALYPSO_AR_TRACE=0x08) :
* PC=0xe9a2 op=0x8913 STLM B,AR3 fires 10243× with B=0 → AR3=0
* The preceding PC=0xe9a0 op=0xf310 was MEANT to be `SUB #5,B,B`
* (= B -= 5) but our wrong INTR handler pushed PC+1 and jumped
* to vec table → SUB never executed → B stayed 0 → AR3=0 →
* BANZ fc54,*AR3- loop infinite at fc50-fc6d → INT3 ISR never
* RETE → INTM=1 forever → IRQ subséquentes pending only.
*
* Fix : retirer ce handler ; F310 etc. tombent dans la FCF0
* dispatch ci-dessous (ADD/SUB/LD/AND/OR/XOR pour F3xx).
*
* A real INTR k handler should be added at F7Cx if firmware
* uses it — TODO. Pas urgent (zero F7Cx hits observed in run). */
/* 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: result = src + ((int64_t)(int16_t)op2 << 16); break;
case 0x1: result = src - ((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: { /* 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).
*
* 2026-05-25 night : ADD/SUB/LD ADDED here (étaient mis-décodés
* par le faux INTR k F300 retiré ci-dessus). Fix smoking-gun
* 0xf310 = SUB #lk,B,B au PC=0xe9a0 → B=0 → AR3=0 → loop fc50. */
if ((op & 0xFCF0) == 0xF000 || /* ADD #lk, SHIFT, src, [dst] */
(op & 0xFCF0) == 0xF010 || /* SUB ← FIX 0xf310 */
(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 & 0x8) ? (shift_raw - 16) : shift_raw;
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: result = src + lk_val; break; /* ADD */
case 0x1: result = src - lk_val; break; /* 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 = (shift >= 0) ? (dst_in << shift) : (dst_in >> (-shift));
result = src & sh;
break;
}
case 0x5: { /* OR */
int64_t dst_in = dst_b ? s->b : s->a;
int64_t sh = (shift >= 0) ? (dst_in << shift) : (dst_in >> (-shift));
result = src | sh;
break;
}
case 0x6: { /* XOR */
int64_t dst_in = dst_b ? s->b : s->a;
int64_t sh = (shift >= 0) ? (dst_in << shift) : (dst_in >> (-shift));
result = src ^ sh;
break;
}
case 0x7: { /* SFTL src,SHIFT,DST: DST = SRC << shift (logical) */
uint64_t usrc = (uint64_t)src & 0xFFFFFFFFFFULL;
result = (int64_t)((shift >= 0) ? (usrc << shift) : (usrc >> (-shift)));
break;
}
}
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); /* probe candidat 1 doc §7 */
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 — RETED in particular leaves
* INTM=1 forever, blocking every subsequent INT3 and stalling
* the firmware↔DSP frame loop (the original CLAUDE.md root bug).
*
* 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 (no INTM change). */
uint16_t ra = data_read(s, s->sp); s->sp++;
s->delayed_pc = ra;
s->delay_slots = 2;
return consumed + s->lk_used;
}
if (op == 0xF6E2 || op == 0xF6E3) {
/* CALAD-AT-8353-PROBE (c web review 2026-05-27) : at the
* exact site we know self-loops, dump XPC + full A + delay
* slot state. CALAD per SPRU172C preserves XPC ; the probe
* confirms XPC value at entry (1 → firmware was on far page,
* 0 → firmware threw far-pointer at near call). First hit only. */
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.
* Pop XPC + PC unconditionally (FL_FAR). FRETED also clears INTM.
* 2026-04-28 — fixed: was APTS-gated (= AVIS, 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) → tgt. XPC update is immediate
* (mirrors FRETED at line ~1639). FCALAD pushes ret PC+3,
* and (when APTS) pushes XPC first (so RETF/FRETD pops in
* order). 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): push XPC + return PC unconditionally (FL_FAR).
* 2026-04-28 — fixed: was APTS-gated (= AVIS, no stack semantics). */
s->sp = (s->sp - 1) & 0xFFFF;
data_write(s, s->sp, s->xpc);
uint16_t ret_pc = (uint16_t)(s->pc + 3);
s->sp = (s->sp - 1) & 0xFFFF;
data_write(s, s->sp, ret_pc);
}
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. */
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 0x3: s->ar[2] = k; break;
case 0x4: s->ar[3] = k; break;
case 0x5: s->ar[4] = k; break;
case 0x6: s->ar[5] = k; break;
case 0x7: s->ar[6] = k; break;
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 */
/* PROBE 2026-06-01 : 2e site d'écriture BK (LD #k8,BK). Nomme le
* writer + valeur. BK=0 casse l'adressage circulaire → runaway
* AR2 0xfa98/0xf17c. À RETIRER avec la sonde MMR_BK. */
{
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 : push XPC + return PC unconditionally (FL_FAR).
* Per binutils tic54x-opc.c (fcall 0xF980 mask 0xFF80, FL_FAR)
* and SPRU172C: FAR call always saves XPC for FRET to restore.
* 2026-04-28 — fixed: was APTS-gated (= AVIS, no stack semantics).
* Old behavior caused 281 firmware FCALL FAR sites to push only PC,
* imbalanced with 142 FRET pop expecting both PC + XPC. */
if ((op & 0x80) != 0) {
uint8_t new_xpc = (op & 0x7F) & 0x03;
static uint64_t fcall_total;
fcall_total++;
s->sp = (s->sp - 1) & 0xFFFF;
data_write(s, s->sp, s->xpc);
s->sp = (s->sp - 1) & 0xFFFF;
data_write(s, s->sp, (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;
}
/* FIX 2026-05-31 : cond décodée depuis l'octet bas (binutils
* condition_codes[]) via c54x_cond_true(). L'ancien (op>>4)&0xF
* lisait le mauvais champ → CC[TC/NEQ/LT/...] faux → push manquants
* power-scan 0xb1xx → over-pop → 0x80fd → self-CALA 0x70c3. */
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;
}
/* Fix 2026-07-03 : etend a FA20/FA30 (BCD pmad,NTC/TC, delayed
* sibling de F820/F830) le fix deja valide le 2026-06-23 pour BC.
* REVERT 2026-05-15 : evaluer la vraie cond TC/NTC EN BLOC pour
* tout FA00-FA7F cassait le firmware (DSP stuck loops) -- la
* plupart des callers FAxx ne posent pas TC de facon fiable
* avant de brancher. On mirror EXACTEMENT la technique BC :
* n evaluer la vraie cond QUE quand l opcode precedent est
* CMPM/BITF (0x60xx/0x61xx, pose TC de facon fiable) ; sinon,
* comportement inchange (branch always, deja valide sur). Zero
* risque de regression hors de ce cas etroit et sur. Delayed :
* 2 delay slots (meme mecanisme que FBD FAR juste au-dessus). */
{
uint8_t fa_sub = (op >> 4) & 0xF;
if (fa_sub == 0x2 || fa_sub == 0x3) {
bool fa_tc_strict = (g_prev_op & 0xFE00) == 0x6000;
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;
}
}
}
/* 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 : push XPC + return PC+4 unconditionally (FL_FAR delayed).
* Per binutils (fcalld 0xFB80 mask 0xFF80, FL_FAR|FL_DELAY).
* 2026-04-28 — fixed: was APTS-gated (= AVIS, no stack semantics). */
if ((op & 0x80) != 0) {
uint8_t new_xpc = (op & 0x7F) & 0x03;
static uint64_t fcalld_total;
fcalld_total++;
s->sp = (s->sp - 1) & 0xFFFF;
data_write(s, s->sp, s->xpc);
s->sp = (s->sp - 1) & 0xFFFF;
data_write(s, s->sp, (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;
}
/* FIX 2026-05-31 : cond décodée depuis l'octet bas via
* c54x_cond_true() (cf CC ci-dessus). */
bool take = c54x_cond_true(s, (uint8_t)(op & 0x7F));
if (take) {
/* FIX 2026-05-31 : CCD est DIFFÉRÉ — arme delay_slots=2 +
* delayed_pc (comme CALLD f274, fixé 2026-05-30), au lieu de
* sauter immédiatement (s->pc=op2; return 0) qui SKIPPAIT les
* 2 delay-slots → push perdu si un slot pousse → over-pop →
* 0x80fd → self-CALA 0x70c3. Retour poussé = pc+4 (past CCD +
* 2 slots). Not-taken : PC avance de consumed (2) = past CCD. */
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 : si on est en train de RET depuis le
* boot stub (PC ∈ 0x0000..0x0008), c'est la sortie du
* task-switch trampoline 0x701b/0x701d → 0x0000. Le ra
* poppé est le PC du task qui prend le contrôle. À insn≈90.2M
* (dernière transition INTM), ce PC = le task qui ne clear
* jamais INTM ensuite. */
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 : pour les 5 premiers POST-BOOTSTUB-RET,
* dump pc_ring[-64..-1] pour révéler le caller chain
* qui mène au stack-underflow loop. Gated par
* CALYPSO_DEBUG=BOOTSTUB_TRAIL. */
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");
}
/* Dump aussi 16 valeurs sur la pile à partir de 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. The
* old "skip delay slots" implementation broke FB-detection
* because slots like `LD #0, B` at PROM0 0x75ea were never
* run, leaving accumulator state stale and the dispatcher
* at 0x7700 looping forever.
*
* Fix: arm the existing delayed_pc/delay_slots machinery —
* pop the return address now, advance PC normally so the
* next 2 instructions execute as delay slots, then the
* main loop forces PC = delayed_pc. */
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 */
/* CMPS src, Smem — Compare, Select, and Store (Viterbi)
* Encoding: 1110 00SD IAAAAAAA (1 word)
* Per SPRU172C p.4-35: if |A(32-16)| >= |Smem| then TC=1,
* TRN = (TRN<<1)|1, dst=A; else TC=0, TRN=(TRN<<1), dst=Smem<<16 */
if ((op & 0xFC00) == 0xE000) {
int src_s = (op >> 9) & 1;
int dst_d = (op >> 8) & 1;
addr = resolve_smem(s, op, &ind);
uint16_t val = data_read(s, addr);
int64_t acc = src_s ? s->b : s->a;
int32_t ah = (int32_t)((acc >> 16) & 0xFFFF);
if (ah < 0) ah = -ah;
int32_t sv = (int16_t)val;
if (sv < 0) sv = -sv;
s->trn <<= 1;
if (ah >= sv) {
s->st0 |= ST0_TC;
s->trn |= 1;
} else {
s->st0 &= ~ST0_TC;
int64_t nv = (int64_t)(int16_t)val << 16;
if (dst_d) s->b = sext40(nv); else s->a = sext40(nv);
}
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% circ modulo BK — fix 2026-06-01 */
}
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% circ modulo BK — fix 2026-06-01 */
}
return consumed + s->lk_used;
}
if (hi8 == 0xE4) {
/* E4xx: BITF Smem, #lk (2-word) or BIT Smem, bit */
addr = resolve_smem(s, op, &ind);
op2 = prog_fetch(s, s->pc + 1);
consumed = 2;
uint16_t val = data_read(s, addr);
s->st0 = (val & op2) ? (s->st0 | ST0_TC) : (s->st0 & ~ST0_TC);
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.
* NOTE: This was previously decoded as CC (conditional call, 2-word)
* which caused stack overflow by pushing return addresses in a loop. */
int dst = (op >> 8) & 1;
uint8_t k = op & 0xFF;
int64_t v = (s->st1 & ST1_SXM) ? (int64_t)(int8_t)k : (int64_t)k;
/* [2026-07-23] FIX ISA : LD #k8u charge l'immediat dans les bits BAS (sext40(v)),
* PAS v<<16. Le <<16 mettait 0x39 en bits 16-23 -> au terminal mask-ROM :
* 0xb408 LD #0x39 + 0xb409 ADD #0x4387 donnait A=0x394387 -> STLM AR7=0x4387
* (slot IDLE data[0x4387]=0xab38) au lieu de 0x0039+0x4387=0x43C0 (slot go-live
* data[0x43c0]=0xa4c7). => terminal 0xb40f BACC vers idle 0xab38 = LE STORM.
* Verifie runtime (TERM-TRACE). Correct c54x SPRU172C : LD #k8 -> low bits.
* Env CALYPSO_LDK8_SHIFT16=1 restaure l'ancien comportement (A/B). */
static int _ldk8sh = -1;
if (_ldk8sh < 0) _ldk8sh = getenv("CALYPSO_LDK8_SHIFT16") ? 1 : 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 */
case 0xE4: /* SAT A */ if (s->st0 & ST0_OVA) s->a = (s->a < 0) ? (int64_t)0xFF80000000LL : 0x7FFFFFFFLL; break;
case 0xE5: /* SAT B */ if (s->st0 & ST0_OVB) s->b = (s->b < 0) ? (int64_t)0xFF80000000LL : 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} }
* = 1 MOT, et SPRU172C §4 (FRAME adjusts SP by a signed 8-bit imm).
* FRAME -N alloue le cadre (SP descend) ; FRAME +N le libère.
*
* BUG FIX 2026-05-31 : ce handler décodait 0xEExx en "BCD pmad,cond"
* (branche conditionnelle différée 2-MOTS) — FAUX sur les 2 plans :
* (1) longueur : 1 mot, pas 2 → désync de tout l'aval
* (2) sémantique : SP+=k8, pas une branche
* BCD n'existe même pas en 0xEE (le vrai bc=0xF8, cc=0xF9, bcd=0xFA).
* 124 sites 0xEExx en PROM0, dont le chemin de boot (paires
* FRAME #-1/#+1 = prologue/épilogue). Le SP jamais ajusté → over-pop
* (SP-EVENTS pops>pushes) → POPM ST0 @0x94f3 ramasse l'orphelin
* 0x80fd → DP=0x0fd → dispatcher LUT garbage → self-CALA 0x70c3 →
* écrit 0x70c4 (=28868) dans d_fb_det/a_pm → rxlev/TOA poison.
* cf doc/SP_CATASTROPHE_70c4_SEQUENCE.md. */
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 (we mirror via mvpd_src state).
*
* 2026-05-27 c web review revert : a speculative 23-bit fix
* (A.high → XPC override) was tried but contradicts SPRU131G and
* did not move the symptom — reverted to canonical semantics. */
if (hi8 == 0x7E) {
addr = resolve_smem(s, op, &ind);
/* GAP-1/Phase B fix (2026-06-24) : sous RPT, la 1ere iteration part
* de A_low (base source), PAS du mvpd_src stale d'un READA precedent.
* Sans ca, `RPT #N ; READA *ARx+` copiait depuis la mauvaise zone ROM
* -> table de dispatch (0x4380+) remplie de garbage (0xf074) -> bacc
* vers la LUT au lieu du vrai handler FB (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;
{ static int reada_log = 0; if (reada_log++ < 20)
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)
*
* Was previously misdecoded as LDM MMR,dst (1 word) — copy/paste
* of the wrong mnemonic. The real LDM is 0x48xx mask 0xFE00,
* already correctly handled in the 0x4 group. Misdecoding caused
* PC to advance by 1 instead of 2-3 ; the literal then executed
* as a stray opcode. In particular the 0x4F00 (DST B,Lmem with
* DP=0 → MMR_IMR) stray write zeroed IMR forever, masking
* INT3+BRINT0 → DSP parked in RPTB at e9ab..e9b6 awaiting a
* frame interrupt that was never serviced. Fix 2026-05-08. */
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 = op & 0x7F;
op2 = prog_fetch(s, s->pc + 1);
consumed = 2;
/* WATCH-ST1-WRITE : MMR 0x07 = ST1. Capture toutes les
* écritures de ST1 (STM #lk, ST1) — incluant celles qui
* ne changent pas la valeur d'INTM mais redéfinissent
* tout le mot ST1. Sortie : valeur écrite, bit 11 (INTM),
* delta vs current ST1. Cap 200 entries pour boot, puis
* sample 1/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;
}
/* 0x72/0x73 (MVDM/MVMD) : RESTENT REVERTÉS (fallthrough STL générique).
*
* FINDING 2026-06-02 (cause (c) localisée + prouvée, mais fix bloqué) :
* DECODE-AUDIT gaté insn>250M a prouvé qu'à PC=0xf564 op=0x7215 = MVDM
* data[0x0014]→AR5, AU CŒUR de la boucle dispatch FB (0xf561-0xf588),
* était décodé 1-mot → l'opérande 0x0014 exécutée comme opcode (0xf565
* hi8=00) → desync chaque itération → AR5 (ptr handler tâche) jamais
* chargé → tâche FB jamais dispatchée → 0x9ac0 jamais ré-atteint
* past-boot → d_fb_det jamais armé. = cause (c) « décision jamais
* atteinte ». LE MIS-DÉCODE EST RÉEL.
*
* MAIS appliquer MVDM 2-mots (ISA-correct : data[MMR]=data[dmad]) — même
* 0x72 SEUL — RÉGRESSE en deadlock pire : le dispatch avance bien à
* PC=0xee38 (task_md=5 lu sur les 2 pages = progrès), mais AR3 y pointe
* HORS du buffer I/Q (0x2b97 > 0x2b28) → corrèle des zéros (A=0) → BSP ne
* livre plus (delivered=0) → INT3 ne fire plus (irq 3860→4) → deadlock.
* = le « bug compensateur upstream » du revert (REVERT_MVMD_KNOWLEDGE.md) :
* le setup d'AR3/pointeurs en amont de 0xee38 est aussi mal émulé, et le
* STL mis-décodé compensait. Critère de ré-application : fixer d'abord le
* deadlock 0xee38 (AR3 hors-buffer) — passe séparée. Voir
* project_state_20260602 mémoire. */
/* === PORTR 0x74 / PORTW 0x75 — longueur 3 mots si Smem absolu (revival
* c54x, 2026-06-23). 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 mots (opcode + PA) ; +1 mot quand le Smem utilise l'adressage
* absolu/long (binutils get_insn_size = words + has_lkaddr). Le catch-all
* générique `(op & 0xF800)==0x7000` ci-dessous les avalait en STL 1-mot
* (+lk) → perdait le mot PA → glissement d'alignement d'1 mot. Symptôme
* prouvé (oracle binutils-2.21.1) : PROM0 0xb416 `portw *(0x000e),0xf900`
* mal-dimensionné 2 mots → son PA 0xf900 relu comme un CC fantôme @0xb418
* → entrée nue dans l'épilogue 0x76f8 (POPM ST1 sans PSHM ST1) → sur-pop
* SP → collapse → d_fb_det=0. 128 `75f8` + 25 `74f8` dans le firmware.
* resolve_smem lit l'adresse Smem abs @pc+1 (pose lk_used) ; le PA suit
* @pc+1+lk_used — même convention que CMPM/BITF ci-dessous.
* NB : sémantique I/O réelle (PORTW: Smem→port PA ; PORTR: port PA→Smem,
* lien I/Q bsp_buf) = passe séparée ; ici on corrige d'abord la LONGUEUR
* (le 1er domino, falsifiable). */
if ((op & 0xFF00) == 0x7500) { /* PORTW Smem, PA */
addr = resolve_smem(s, op, &ind); /* applique le post-modify AR, pose lk_used si abs */
uint16_t pa = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
(void)pa; (void)addr; /* écriture port externe : non modélisée (TODO sémantique) */
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));
/* [2026-07-23] PORTR-ANY (READ-ONLY, inconditionnel) : compte TOUT hit
* PORTR quel que soit PA, pour distinguer "opcode jamais atteint" de
* "atteint mais PA != 0xF430/0x0034". Cap 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);
}
/* PA=0xF430 (ou 0x0034 legacy) = port RX BSP : livre l'echantillon
* I/Q suivant depuis bsp_buf (rempli par DMA radio ; bsp_pos reset
* par rafale). Le handler dead-code 0x8F (~8530) etait la ref
* ISA-fausse ("relocaliser PORTR vers 0x74 un jour") : c'est fait ici.
* Sans ca, notre length-fix no-op shadow-ait la vraie lecture I/Q ->
* l'AFC/correlateur ne recevait AUCUN echantillon -> jamais de
* convergence. GATED CALYPSO_FIX_PORTR. */
static int fix_portr = -1;
if (fix_portr < 0) fix_portr = getenv("CALYPSO_FIX_PORTR") ? 1 : 0;
if (fix_portr && (pa == 0xF430 || pa == 0x0034)) {
uint16_t iq = (s->bsp_pos < s->bsp_len) ? s->bsp_buf[s->bsp_pos++] : 0;
data_write(s, addr, iq);
static unsigned pr_n = 0;
if (pr_n++ < 50)
fprintf(stderr, "[c54x] PORTR-IQ #%u PA=0x%04x -> data[0x%04x]"
"=0x%04x pos=%d/%d PC=0x%04x insn=%u\n",
pr_n, pa, addr, iq, s->bsp_pos, s->bsp_len, s->pc,
s->insn_count);
}
(void)pa; (void)addr;
consumed = 2;
return consumed + s->lk_used;
}
/* [2026-07-23] 0x73xx: MVMD MMR, dmad — MMR -> data[dmad] (2-word, direction SAVE).
* BUG REEL (SP-CORRUPT watchpoint) : 0xa4f8 op=0x7318 = MVMD SP,0x3f6e = SAUVE SP.
* Le fallthrough STL generique CHARGEAIT SP depuis data[0x3f6e]=garbage -> SP=0xc905
* etc. -> derails eparpilles (0xa58d...) post-POPD. ISA-correct : lit le MMR
* (SP=0x18 alias data 0x0018) et ecrit data[dmad] ; SP/MMR INCHANGE. On ne fixe
* QUE 0x73 (MVMD, sens save, inoffensif) ; 0x72 (MVDM, sens charge) GARDE le revert
* documente (regression AR3-hors-buffer @0xee38, REVERT_MVMD_KNOWLEDGE.md). */
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}}.
* 2-mot base (opcode + dmad) + 1 mot si Smem absolu/long (mode 0xC..0xF,
* has_lkaddr). Ordre des operandes = dmad EN PREMIER (pc+1), lk Smem-abs
* EN SECOND (pc+2).
*
* GAP-1 ROOT (tracee 2026-06-23 via sonde AR3-TRIP) : le catch-all
* generique (op&0xF800)==0x7000 ci-dessous decodait 0x70xx en STL 1-mot
* (+lk), SOUS-CONSOMMANT le mot dmad. A PROM0 0xb3cc (`70f8 4356 00e3`)
* le 3e mot 0x00e3 etait alors execute comme `ADD *AR3(lk)` parasite ;
* de meme 0xb3d1=0x00db -> `ADD *AR3+0%,A` faisait AR3 += AR0(~=SP) a
* chaque tour -> AR3 balayait la memoire -> ecrasait data[0x0c36]
* (ptr tache) -> CALA 0 -> POST-BOOTSTUB-RET -> derail/boucle. Le decode
* de l'ADD lui-meme etait FIDELE ; le bug etait la LONGUEUR du MVKD amont.
* Note : 0x72/0x73 (MVDM/MVMD) restent volontairement non-fixes ici
* (revert documente, REVERT_MVMD_KNOWLEDGE.md) ; ce fix 0x70 est le
* prerequis « fixer d'abord le setup AR3 amont » mentionne la-bas. */
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;
if (mode >= 0xC) { /* Smem absolu/long : lk @pc+2 */
uint16_t lk = prog_fetch(s, s->pc + 2);
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) absolu */
smem_addr = lk;
}
s->st0 = (s->st0 & ~ST0_ARP_MASK) | (nar << ST0_ARP_SHIFT);
s->lk_used = true;
} else { /* direct ou indirect non-abs */
smem_addr = resolve_smem(s, op, &ind); /* +post-modify AR */
}
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]. MIROIR de MVKD.
* binutils tic54x-opc.c {mvdk,2,2,2,0x7100,0xFF00,{OP_Smem,OP_dmad}}.
* Encodage IDENTIQUE a MVKD 0x70 (Smem dans l'octet bas de l'opcode,
* dmad@pc+1, lk Smem-abs@pc+2) ; SEULE la direction du move est inversee :
* MVKD fait data[Smem]=data[dmad] ; MVDK fait data[dmad]=data[Smem].
*
* FINDING 2026-06-24 (recon workflow, desasm verifie adversarialement sur
* le vrai dump /opt/GSM/calypso_dsp.txt) : a PROM0 0xb3db-0xb3e3 TROIS MVDK
* 3-mots `71f8 4356 00e3` / `71f8 4357 00db` / `71f8 4355 00d3` RESTAURENT
* data[0x4356/4357/4355] (sauves par les 3 MVKD symetriques @0xb3cc avant
* les 3 CALL). Le catch-all `(op&0xF800)==0x7000` les decodait en STL 1-mot
* -> SOUS-CONSOMMAIT le mot dmad -> les operandes 00e3/00db/00d3 executees
* en ADD parasites (dont 0x00db = `ADD *AR3+0%%,A`, l'instr GAP-1) chaque
* trame -> corruption AR3/A + restore rate. MEME CLASSE que le fix MVKD 0x70.
* 0x71 est data<->data (PAS MMR) -> ORTHOGONAL au revert 0x72/0x73
* (REVERT_MVMD_KNOWLEDGE.md, qui concerne la corruption MMR via STL). */
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;
if (mode >= 0xC) { /* Smem absolu/long : lk @pc+2 */
uint16_t lk = prog_fetch(s, s->pc + 2);
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) absolu */
smem_addr = lk;
}
s->st0 = (s->st0 & ~ST0_ARP_MASK) | (nar << ST0_ARP_SHIFT);
s->lk_used = true;
} else { /* direct ou indirect non-abs */
smem_addr = resolve_smem(s, op, &ind); /* +post-modify AR */
}
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]) / 0x73 MVMD MMR,dmad (data[dmad]<-MMR).
* 2-mot (opcode + dmad ; MMR = octet bas, mappee a data 0x00-0x1f que
* data_read/data_write routent vers les registres). GATED CALYPSO_FIX_MVDM
* car REVERT_MVMD_KNOWLEDGE.md documente une regression (deadlock 0xee38,
* AR3 hors buffer I/Q) quand on fixe AVANT le setup AR3 amont. Ce setup =
* MVKD 0x70 (GAP-1) + MVDK 0x71 = MAINTENANT FAITS -> critere de
* re-application atteint. Debloque la sous-routine go-live 0xaad5
* (7211 434f MVDM data[0x434f]->AR1 ; 7210 434e ; 7310 434e MVMD AR0->
* data[0x434e]) dont le mis-decode (catch-all STL 1-mot) fige A=0 -> garde
* BC 0xa4cd (AEQ, A==0) jamais relachee -> RSBX INTM 0xa51b jamais atteint. */
{
static int fix_mvdm = -1;
/* [2026-07-23] DEFAULT ON : fix ISA MVDM/MVMD (decode conforme tic54x-opc).
* Debloque la SM go-live 0xaad5 (A fige a 0 sinon) -> D_TASK_MD-RD 0->1859,
* DSP lit db_w + atteint dispatcher trame. Regression 2026-05-15 (0xfd23/fd25)
* levee (setup amont MVKD 0x70/MVDK 0x71 fait). OFF via CALYPSO_FIX_MVDM_OFF. */
if (fix_mvdm < 0) fix_mvdm = getenv("CALYPSO_FIX_MVDM_OFF") ? 0 : 1;
if (fix_mvdm && (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 (fix_mvdm && (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;
}
}
/* 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;
}
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 → CMPM *(0x0fff), 2
* to wait for ARM-side BL_CMD_STATUS write. Without TC being set
* the subsequent BC NTC always branches back, looping forever.
* Was previously folded into the generic 0x6000-0x67FF "LD" path
* which set the accumulator instead and never updated TC. */
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 : capture EXACTE au site de poll foreground (0xf7af/0xf7b7)
* — addr résolue + valeur data_read + TC. ID le flag jamais vrai. */
if (g_fbwatch_on > 0 && (s->pc == 0xf7af || s->pc == 0xf7b7)
&& s->insn_count > 100000) { /* post-wire (1er wire @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 (2026-05-15 nuit) — pour confirmer si TC
* est set correctement. Hypothèse : si BITF appelle souvent mais
* tc_after=1 rarement → masque/mem_val pattern empêche TC=1,
* ce qui fait que BC NTC branche toujours et `ST #1, d_task_d`
* à PROM 0x9ab1 n'est jamais atteint. Format :
* BITF-PROBE #N PC=0xXXXX addr=0xXXXX mem=0xXXXX mask=0xXXXX
* tc_before=N tc_after=N
* Cap 200 + 1/1000 ensuite. */
{
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;
}
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
* mis-decodes them all as LD Smem,T (1-word), causing PC drift +1
* word and the lk/pmad operand executing as parasitic instruction.
* 1259 (ANDM/ORM/XORM/ADDM) + 304 (BANZ/BANZD) = 1563 sites in ROM.
*
* 2026-04-28 — companion fix to 0x6F00 already inserted below.
* See doc/opcodes/0x68_0x6F.md for spec. */
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);
data_write(s, addr, (uint16_t)((int16_t)v + lk));
consumed = 2;
/* TODO: TC/OVM/SXM flag effects per SPRU172C (verify) */
return consumed + s->lk_used;
}
if ((op & 0xFF00) == 0x6C00) {
/* BANZ pmad, Sind — branch if ARx (selected by ARF in op[2:0])
* is non-zero. Test on PRE-modify value; resolve_smem applies
* post-mod regardless of branch outcome. Previously read ARP
* from ST0 (the PREVIOUS instruction's nar) — wrong AR was
* tested. Cf resolve_smem comment for the off-by-ARP bug. */
int nar = op & 0x07;
uint16_t pre = s->ar[nar];
resolve_smem(s, op, &ind);
uint16_t pmad = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
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];
resolve_smem(s, op, &ind);
uint16_t pmad = prog_fetch(s, s->pc + 1 + (s->lk_used ? 1 : 0));
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
* mis-decodes 0x6Fxx as LD Smem,T (1-word), causing PC drift +1 word
* and the lk-side operand to be executed as parasitic instruction.
* 544 sites in firmware ROM. See doc/opcodes/0x68_0x6F.md for spec.
*
* 2026-04-28 — fix introduced for wedge at PC=0x8353 (CALAD A self-loop)
* caused by 0x6F07 0x0C41 mis-decoded → 0x0C41 executed as parasitic
* SUB Smem,TS,A → A_low=0xFFFA → A_low=0x8353 after subsequent 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) {
/* DEAD CODE since 2026-04-28: all 0x68xx-0x6Fxx now intercepted
* by specific handlers above (ANDM/ORM/XORM/ADDM/BANZ/BANZD/
* extended-0x6F00) plus the existing 0x6Dxx MAR. This generic
* "LD Smem, T" fallback was the source of the 2107-site mass
* mis-dispatch that caused PC drift on every 0x68xx-0x6Fxx
* encounter. Kept here for safety in case a previously unseen
* sub-encoding slips through; if you ever see this trigger,
* the new handler above for the matching 0xNN00 prefix is
* incomplete. See doc/opcodes/0x68_0x6F.md. */
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
*
* Critical: bootloader at PROM0 0xb429 does `LDU *(0x0ffe), A`
* (op=0x12f8 + lk=0x0ffe) to read BL_ADDR_LO, then BACC A to that
* target. The previous "case 0x1: SUB" decoded this as a subtract,
* leaving A=0 and the BACC dropping into 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);
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;
v &= 0xFFFFFFFF0000LL; /* clear low 16 after rounding */
if (dst) s->b = sext40(v); else s->a = sext40(v);
return consumed + s->lk_used;
}
/* [2026-07-28] sub 4..7 : la moitie LOGIQUE de la famille tombait dans le
* `default` ci-dessous et etait exécutée comme un LD. Encodages : table
* projet doc/opcodes/tic54x_hi8_map.md + SPRU172C (tableaux 2-7/2-8/2-9
* et SUBC p.4-192). Smem est ZERO-etendu sur 40 bits : l exemple TI de
* AND (p.4-12) donne A=00 00FF 1200 & Smem=0x1500 -> A=00 0000 1000.
* Impact mesure : 0x1860 (AND) lu comme LD mettait A=15 au lieu de A&15,
* d ou T=31 et un `LD Smem,TS` decalant de +31 qui saturait l accumulateur
* (A=0x80000000) et aplatissait la sortie du demod. */
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 — soustraction conditionnelle (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);
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 (patch #2 diag, gated CALYPSO_DEBUG=LDU-PTR) : au site qui
* charge A pour le CALA->0 (defaut PC=0xfa7e, override
* CALYPSO_TRACE_LDU_PC=0xNNNN). Dump l'EA lue + valeur + indirect +
* AR/DP pour nommer la case = 0 (pointeur table non init / EA fausse). */
{
static int ldu_trace_pc = -1;
if (ldu_trace_pc < 0) {
const char *e = 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
* Previous handler always shifted by 16 — wrong for plain ADD/SUB.
*/
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 */
/* [2026-07-28] sub 3 = ADDC (0x0600) et sub 7 = SUBB (0x0E00) : ils tombaient
* dans le traitement ADD/SUB generique, donc SANS la retenue. SPRU172C :
* « ADDC Smem, src : src = src + Smem + C »
* « SUBB Smem, src : src = src - Smem - C »
* binutils : addc 0x0600/0xFE00, subb 0x0E00/0xFE00, 1 mot chacun.
* NB : on suit la lettre du manuel (- C). Certaines implementations de SUBB
* soustraient l emprunt (~C) ; si une mesure le montrait, corriger ICI. */
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);
}
{
int64_t c = with_carry ? ((s->st0 & ST0_C) ? 1 : 0) : 0;
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:
/* 3xxx: MAC / MAS — mais d'ABORD SQURA (§4-A, fix 2026-06-23). */
addr = resolve_smem(s, op, &ind);
{
uint16_t val = data_read(s, addr);
/* SQURA Smem, src (0x38/0x39, mask 0xFE00) : src = src + Smem*Smem.
* Per tic54x_hi8_map.md l.46 (0x3800/0xFE00, bit8=src A=0x38/B=0x39).
* Le case 0x3 « blind-MAC » exécutait SQURA comme `acc += T*Smem` →
* énergie (somme de carrés) calculée avec le mauvais opérande/signe →
* A reste ≤0 à PROM0 0x76ff/0x7700 → RCD LEQ@0x75e8 prend la sortie
* anticipée → saute le corps qui pousse ST1 → POPM ST1@0x7706 sur-pope
* (1er pop orphelin insn 146) → DP=0x124 → handler garbage → SP collapse.
* SQURA accumule un CARRÉ (contribution ≥0) → A>0 → RCD ne prend pas. */
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);
/* [2026-07-28] SPRU172C : « SQURA Smem, src : src = src + Smem * Smem,
* T = Smem ». L ecriture de T manquait : toute instruction suivante qui
* utilise T (MAC, LD Smem,TS, ...) travaillait sur une valeur perimee. */
s->t = 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:
/* 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;
case 0x4: case 0x5: /* SQUR Smem, A/B */
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 — sub of shifted Smem from acc */
addr = resolve_smem(s, op, &ind);
int64_t val = (int64_t)(int16_t)data_read(s, addr) << 16;
*acc_dst = sext40(*acc_dst - val);
return consumed + s->lk_used;
}
if (op8 == 0x44 || op8 == 0x45) {
/* LD Smem << 16, dst */
addr = resolve_smem(s, op, &ind);
int64_t val = (int64_t)(int16_t)data_read(s, addr) << 16;
*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) {
/* [2026-07-28] RPT Smem — charge le compteur de repetition SIMPLE (RC),
* PAS le compteur de bloc (BRC). SPRU172C : « RPT Smem : Repeat single,
* RC = Smem ». binutils : rpt 0x4700/0xFF00, 1 mot.
* L ancien code ecrivait s->brc : le RPT n avait donc aucun effet sur la
* repetition (rpt_count restait a sa valeur precedente) et BRC etait
* corrompu au passage. On aligne sur le handler RPT #k8u (0xEC00) qui est
* correct : avancer le PC et rendre 0 pour que le dispatcher re-execute
* l instruction SUIVANTE, pas le RPT lui-meme. */
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;
s->pc += 1;
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);
*acc_dst = sext40((int16_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, various forms.
* NOTE: 0x56xx/0x57xx are SFTL/SFTA with Smem (1-word), NOT MVPD.
* MVPD is at 0x8Cxx (hi8=0x8C). The old 0x56 MVPD decode was wrong
* and caused writes to MMR_SP via resolve_smem, corrupting the stack. */
{
/* === Dual long-word DADST/DSADT, Lmem,dst (1 word) — fix revival
* dsp (2026-06-22). Encodage SPRU172C : 0101 101D = DADST (0x5A/5B),
* 0101 111D = DSADT (0x5E/5F) ; D(bit8)=dst (0=A,1=B) ; bits[7:0]=Smem
* (Lmem). Sans ce handler ces opcodes tombaient dans le bloc SFTA/SFTL
* ci-dessous → corrélateur FCCH aplati en "dst>>=ASM", d_fb_det=0
* (sonde SHADOW-DADST : shiftLike=1, walked=0). Sémantique (vérifiée
* sur les exemples chiffrés SPRU172C) :
* C16=1 (dual-16, non saturé) :
* 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-mod = ±2 (long-operand, via resolve_lmem). Le mode C16
* réel est lu à l'exécution (ST1.C16, suivi par SSBX/RSBX C16). */
uint8_t dl_hi = (op >> 8) & 0xFF;
if (dl_hi >= 0x50 && dl_hi <= 0x5F) {
/* === Famille dual long-word COMPLÈTE (SPRU172C, sweep §4-E 2026-06-22) :
* 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). Lmem 32-bit via resolve_lmem (post-mod ±2),
* branche sur ST1.C16. Vérifié bit-à-bit vs exemples chiffrés SPRU172C
* (DADD/DADST/DSADT). Avant : 0x50-59/5C-5D tombaient en SFTA/SFTL. */
uint16_t laddr = resolve_lmem(s, op);
uint16_t lhi = data_read(s, laddr);
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;
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) *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) *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) *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);
} 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 add ; else sub */
}
*dst = sext40(r);
}
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 */
/* ---- Dual-operand MAC Xmem, Ymem, dst (1-word) ----
* 0x90: MAC Xmem,Ymem,A 0x92: MAC Xmem,Ymem,B
* 0x91: MACR Xmem,Ymem,A 0x93: MACR Xmem,Ymem,B
* Same encoding as 0xA4 family: OOOO OOOD XXXX YYYY */
if (hi8 == 0x90 || hi8 == 0x91 || hi8 == 0x92 || hi8 == 0x93) {
/* FIX 2026-06-22 (sweep) : décodage Xmem/Ymem 2-bit SPRU131G T.5-6/5-8
* (Xmod[7:6] Xar[5:4] Ymod[3:2] Yar[1:0], AR=field+2, mod 1=*AR- 2=*AR+
* 3=*AR+0%) au lieu du raw 3-bit/1-bit. */
int xar_m = ((op >> 4) & 0x03) + 2;
int yar_m = (op & 0x03) + 2;
int xmod_m = (op >> 6) & 0x03;
int ymod_m = (op >> 2) & 0x03;
uint16_t xval_m = data_read(s, s->ar[xar_m]);
uint16_t yval_m = data_read(s, s->ar[yar_m]);
switch (xmod_m) { case 1: s->ar[xar_m]--; break; case 2: s->ar[xar_m]++; break;
case 3: s->ar[xar_m] = c54x_circ_ref(s->ar[xar_m], +(int16_t)s->ar[0], s->bk); break; }
switch (ymod_m) { case 1: s->ar[yar_m]--; break; case 2: s->ar[yar_m]++; break;
case 3: s->ar[yar_m] = c54x_circ_ref(s->ar[yar_m], +(int16_t)s->ar[0], s->bk); break; }
int64_t prod_m = (int64_t)(int16_t)s->t * (int64_t)(int16_t)xval_m;
if (s->st1 & ST1_FRCT) prod_m <<= 1;
if (hi8 & 0x01) prod_m += 0x8000; /* round */
int dst_m = (hi8 & 0x02) ? 1 : 0;
if (dst_m) s->b = sext40(s->b + prod_m);
else s->a = sext40(s->a + prod_m);
s->t = yval_m;
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;
s->prog[op2] = data_read(s, addr);
return consumed + s->lk_used;
}
/* AUDIT FIX 2026-05-08 night : STL ↔ STH swap.
* 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} }
* Old decoder claimed 0x98/99=STH and 0x9A/9B=STL — exactly inverted.
* Effect: every STL/STH-with-shift in firmware wrote the WRONG half
* of the accumulator. Hot pattern in DSP code (post-MAC scaling),
* so this corrupted ~half of all data writes from compute paths.
* Shift application is intentionally simplified (no SHFT decode)
* matching prior-art handlers — Tier B will add proper 4-bit shift
* decode from low nibble. Mirror swap : write low for 0x98/99,
* write high for 0x9A/9B, src bit 8 selects A/B. */
if (hi8 == 0x98 || hi8 == 0x99) {
/* STL src, SHFT, Xmem — store LOW (acc&0xFFFF).
* FIX 2026-05-23 : Xmem operand decoded via resolve_xmem (per
* binutils OP_Xmem), not resolve_smem. The latter mis-mapped
* low byte 0x00-0x1F with bit 7=0 to MMR space, clobbering SP/
* IMR/IFR. Empirical proof : PC=0x8a46 op=0x9918 stomp SP→0
* captured by existing SP-CATASTROPHE probe. */
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).
* FIX 2026-05-23 : same as STL above — Xmem decoded via
* resolve_xmem (per binutils), not resolve_smem. See STL block. */
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 */
/* SACCD src, Xmem, cond — Conditional accumulator store
* Encoding: 1001 11SD XXXX COND per SPRU172C p.4-152 */
if ((op & 0xFC00) == 0x9C00) {
int src_s = (op >> 9) & 1;
int64_t acc = src_s ? s->b : s->a;
/* FIX 2026-06-02 (ROOT CAUSE FB-det) : opérande Xmem décodé via
* resolve_xmem (xar=((op>>4)&3)+2 = AR2-5, + xmod post-modify),
* exactement comme STL/STH 0x98-0x9B. L'ancien `(op>>4)&0x07` lisait
* le MAUVAIS AR (AR1 au lieu de AR3 pour op=0x9e9b) et `(op>>7)&1` un
* faux sens → AR3 jamais incrémenté → la boucle de recherche de pic
* FCCH (@0x8576 RPTB) relisait sample[0] 15× → corrélation figée,
* d_fb_det garbage, rxlev plancher, FBSB jamais fermé. resolve_xmem
* applique le post-incrément ; ne PAS re-modifier en fin de handler. */
uint16_t xaddr = resolve_xmem(s, op);
int cond = op & 0x0F;
/* Evaluate condition */
int take = 0;
switch (cond) {
case 0x0: take = (acc == 0); break; /* EQ */
case 0x1: take = (acc != 0); break; /* NEQ */
case 0x2: take = (acc > 0); break; /* GT */
case 0x3: take = (acc < 0); break; /* LT */
case 0x4: take = (acc >= 0); break; /* GEQ */
case 0x5: take = (acc == 0); break; /* AEQ */
case 0x6: take = (acc > 0); break; /* AGT */
case 0x7: take = (acc <= 0); break; /* LEQ/ALEQ */
default: take = 0; break;
}
int asm_val = asm_shift(s);
if (take) {
/* Store shifted accumulator high part */
int64_t shifted = acc << (asm_val > 0 ? asm_val : 0);
if (asm_val < 0) shifted = acc >> (-asm_val);
uint16_t val = (uint16_t)((shifted >> 16) & 0xFFFF);
data_write(s, xaddr, val);
} else {
/* Read and write back (no change) */
uint16_t val = data_read(s, xaddr);
data_write(s, xaddr, val);
}
/* post-modify Xmem déjà appliqué par resolve_xmem (cf FIX ci-dessus) */
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 : value at SP popped to MMR, SP++.
*
* Bug fix 2026-05-08 : 0x8Axx était précédemment mal décodé en
* MVDK Smem,dmad (qui est en réalité 0x7100 mask 0xFF00). Le
* pattern PSHM/POPM symétrique du firmware (e.g. PROM0 0x7013-0x7023
* sauve/restaure 6 MMRs autour d'un CALA) ne fonctionnait jamais
* post-CALA → ST1 jamais restauré → INTM=1 dwell perpétuel
* → IRQ vectoring bloqué → DSP wait stuck → L1 mort.
* Le case MVDK ci-dessous devient dead code mais est laissé pour
* référence historique. */
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 == MMR 0x07.
* Discrimine (a) POPM ST1 jamais exécuté vs (b) exécuté mais
* la valeur poppée a déjà INTM=1 → restaure 1, ne clear jamais.
* Silent par défaut. */
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;
}
/* OBSOLETE — superseded by POPM above. The 0x8Axx range belongs to
* POPM per tic54x-opc.c, not MVDK (which is 0x7100 mask 0xFF00).
* Kept commented for one revision so any caller depending on the
* old (incorrect) behaviour is forced to be re-examined. */
/* 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`):
* if decoded as 2-word MVDM the emulator eats the next opcode
* (0xb42e = 0xf84c, a BC), then jumps into 0xb431 (MACR family)
* with an uninitialised T register, producing A=0x10 — which
* the immediately-following BACC A at 0xb430 then uses as the
* jump target, dropping 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 = op & 0x7F;
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;
}
/* [2026-07-23] 0x8Bxx: POPD Smem — pop top-of-stack into data memory.
* Symetrique de PSHD (0x4B) : PSHM/POPM = 0x4A/0x8A ; PSHD/POPD = 0x4B/0x8B.
* ETAIT MANQUANT -> tombait en NOP 1-mot. Bug reel : l'overlay handler frame
* 0x013b fait `POPD *(0x3fcd)` (depile le retour du CALL 0x013b), PSHM x24,
* `PSHD *(0x3fcd)` (repush le retour), RET. Sans POPD : retour enterre sous
* les saves, data[0x3fcd]=0, RET->0 -> DERAIL-ZERO from=0x0157 (go-live cycle).
* resolve_smem pose lk_used pour le mode abs (0xf8) -> 2-mots correct. */
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) {
/* AUDIT FIX 2026-05-08 night : was stubbed NOP because old
* decoder claimed MVDD (2-word, wrong). 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.
* Range 0x8000-0x80FF = STL A, Smem (since bit 8 = 0 here).
* Stubbing this silently dropped every STL A in the firmware ;
* variables that should have been written to DARAM kept stale
* values (junk-state cascade). Mirror of the existing 0x82
* STH-with-shift handler but no shift here. */
addr = resolve_smem(s, op, &ind);
data_write(s, addr, (uint16_t)(s->a & 0xFFFF));
return consumed + s->lk_used;
}
if (hi8 == 0x8C) {
/* AUDIT FIX 2026-05-08 night : was MVPD pmad,Smem (2 mots,
* prog→data move). 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 }
* Real MVPD is at 0x7C — the 0x8C handler should be ST T, Smem
* (1 mot, store T register to data memory). Run-trace confirms
* 0 MVPD hits with the old handler, meaning firmware did not
* issue any 0x7Cxx → our wrong 0x8C MVPD was never triggered
* for legitimate MVPD anyway (PROM0 OVLY happens via DSP
* bootloader, not via 0x7C MVPD instruction). Switching to
* ST T,Smem is safe and unblocks the legitimate ST T pattern
* used after MAC for T persistence. Old MVPD-LOG instrumentation
* removed — was dead-code in current run. */
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). 1 MOT (+1 si long-offset/absolu → 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
* = compare les 2 moitiés 16-bit 2s-comp de l'accu, stocke la MAX, TRN/TC
* tracent le gagnant. Cœur de la recherche de pic FCCH (Viterbi).
*
* FIX 2026-06-02 (audit décodeur DECODE-AUDIT) : 0x8E était décodé MVDP
* 2-mots et 0x8F PORTR 2-mots → chaque paire CMPS A/CMPS B consécutive
* (op=8e94 op2=8f93 dans la zone FB-det 0xa0xx) voyait le 2e CMPS BOUFFÉ
* comme phantom-pmad → désync corrélateur, d_fb_det jamais armé. SÛR :
* l'assembleur TI encode MVDP=0x7D, PORTR=0x74 — jamais 0x8E/0x8F ; et
* l'I/Q arrive par DMA DARAM (data[0x2a00]), pas par opcode PORTR (audit
* 0x8F=0 exécution). Le vieux handler 0x8F=PORTR est neutralisé plus bas. */
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;
}
/* SUPERSEDED 2026-06-02 : 0x8F = CMPS B (traité par le handler CMPS
* 0x8E/0x8F ci-dessus, qui return avant d'arriver ici). Ce bloc PORTR
* est ISA-faux (vrai PORTR=0x74) et jamais atteint (audit 0x8F=0 exec ;
* I/Q via DMA DARAM). Gardé en dead-code (if(0)) pour réf si on relocalise
* PORTR vers 0x74 un jour. */
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;
}
/* REVERTED 2026-05-15 nuit : handlers 0x72/0x73 RETIRÉS.
* Voir doc/REVERT_MVMD_KNOWLEDGE.md et premier emplacement revert
* ci-dessus (avant le bloc `(op & 0xF800) == 0x7000`). 0x86/0x87
* restent comme avant (DUPLICATE MVDM/MVMD au lieu de STH A/B ASM
* vrai — non swappés). */
/* 0x86/0x87 : STH src, ASM, Smem — store HIGH (acc>>16) shifted by ASM,
* to Smem (1-WORD). bit8 = src (0=A, 1=B). Per tic54x_hi8_map.md L95 :
* { "sth", 0x8600, 0xFE00, ASM variant } ← 1 mot, mask 0xFE00.
*
* FIX 2026-06-02 (bug #3, ROOT CAUSE AR3-zero) : l'ancien décode était
* MVDM dmad,MMR (0x86) / MVMD MMR,dmad (0x87) en 2 MOTS — faux sur deux
* axes : (1) longueur (2 au lieu de 1) → consommait l'opcode suivant
* → désync du flux de décode en cascade (= SP→0xcade observé) ; (2) ne
* touchait jamais l'AR du Smem → AR3 figé/0 dans la boucle corrélateur
* FB (IQ-READ @0x7e6f montrait AR3=0000 sur op=0x8693 @0x7e71 = STH A,
* ASM, *AR3+ : AR3 doit post-incrémenter pour balayer le buffer I/Q
* 0x2a00+). Mirror EXACT du handler 0x84 (STL A,ASM,Smem) déjà validé,
* mais store HIGH word au lieu de LOW. resolve_smem applique le
* post-incrément du Smem indirect → ne PAS re-modifier l'AR ici.
*
* SÛR vs le revert 0x72/0x73 (REVERT_MVMD_KNOWLEDGE.md) : ORTHOGONAL.
* L'assembleur TI encode MVDM=0x72, MVMD=0x73 — JAMAIS à 0x86/0x87.
* Donc aucun 0x86xx/0x87xx de la ROM n'est une vraie MVDM/MVMD : c'est
* toujours un STH. Le side-effect dont dépend le firmware est sur 0x73
* (site 0x8208 op=0x7317), inchangé par ce fix. */
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;
}
/* AUDIT FIX 2026-05-15 fin journée : 0x81/0x82/0x83 mal décodés.
* Per tic54x-opc.c + SPRU172C :
* 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 (with shift) [FAIT]
* sth 0x8600 / 0xFE00 → 0x86..0x87 STH src,ASM,Smem (with shift) [FAIT]
* [2026-07-28] les deux variantes ASM sont IMPLEMENTEES (handlers hi8==0x84
* et hi8==0x86/0x87 ci-dessus, tous deux via asm_shift()), et asm_shift()
* est conforme au manuel (ASM = ST1[4:0] signe, -16 <= ASM <= 15). Le
* "[TODO]" precedent etait perime et a coute une fausse piste en remontant
* la sortie du demod (0x8694 = STH A,ASM,*AR4+ ecrit data[0x2a00]) : le
* decalage EST applique. Le vrai bug de ce chemin etait ailleurs — les
* opcodes logiques 0x1800/1A00/1C00/1E00 (AND/OR/XOR/SUBC) decodes comme
* un LD, cf. le case 0x1 plus haut.
* bit 8 = src (0=A, 1=B). Old code applied asm_shift incorrectly
* to 0x81/0x82 (basic variants — no shift) AND used s->a for 0x81
* (should be s->b). Le bug causait toutes les STL B / STH * vers
* adressing indirect *ARn à écrire la mauvaise valeur ; en particulier
* d_burst_d (DSP word 0x0829/0x083D) et d_task_d (0x0828/0x083C) du
* NDB CCCH demod ARM bail dans prim_rx_nb.c::l1s_nb_resp avec
* "EMPTY" et "BURST ID 33414!=N" sous synth=1 banc d'essai. */
/* 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;
}
/* 8Bxx: MVDK with long address */
if (hi8 == 0x8B) {
/* STUB-NOP : tic54x dit 0x8B = POPD Smem (1-word).
* Ancienne classification qemu = MVDK long-addr 2-word (incorrect).
* Voir doc/opcodes/tic54x_hi8_map.md. Neutralisé. */
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;
}
/* AUDIT FIX 2026-05-15 fin journée : 0x83 misclassifié comme WRITA
* (qui est en réalité 0x7F per tic54x-opc.c). Vrai 0x83 = STH B, Smem.
* Et 0x84 misclassifié comme READA (vrai = 0x7E). Vrai 0x84 = STL A,
* ASM, Smem (with shift). 0x85..0x87 idem TODO. */
/* 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) — TODO compléter
* variantes 0x85 (STL B), 0x86 (STH A), 0x87 (STH B) with ASM shift.
* Pour l'instant fix uniquement 0x84 vers la sémantique tic54x correcte. */
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) {
/* FIX revival dsp 2026-06-22 : décodage Xmem/Ymem 2-bit (SPRU131G
* Table 5-6/5-8, identique à resolve_xmem et au handler D0-D9) au
* lieu du raw 3-bit. L'ancien (op>>4)&7 / op&7 + post-mod 1-bit lisait
* les MAUVAIS AR : ex op=0xb4f5 @PC=0xf170 (corrélateur TOA steady-
* state) donnait Xmem=AR7/Ymem=AR5 (hors-buffer) au lieu de Xmem=AR5
* (*AR5+0% circ) / Ymem=AR3 (*AR3-) → Ymem lu hors-buffer=0 → T=0 →
* MAC=0 → A garbage → a_sync_demod[D_TOA]=garbage (0xc3f0). Format :
* 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% circ (BK, +AR0). */
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]);
/* 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;
}
/* Multiply T * Xmem */
int64_t prod = (int64_t)(int16_t)s->t * (int64_t)(int16_t)xval_d;
if (s->st1 & ST1_FRCT) prod <<= 1;
/* Round if R bit set (odd hi8) */
if (hi8 & 0x01) prod += 0x8000;
/* Determine dest and operation.
* FIX 2026-05-29 (binutils tic54x-opc.c, confirmé) : 0xB4-0xB7 =
* MACR = mac+round = ADDITION (0xB400/0xFC00), PAS soustraction.
* MAS/MASR (soustraction) = 0xB8-0xBF, gérés dans des handlers
* séparés (L7582/L7564). Ce handler ne reçoit que 0xA4-A7 + 0xB0-B7,
* tous des accumulations (ADD). L'ancien `is_sub=(hi8 0xB4..B7)`
* inversait le signe de l'op dominant du corrélateur FCCH (0xb4aa)
* → corrélation = -B*T*X → jamais de pic → snr=0 → pas de FB lock. */
int is_sub = 0;
int dst_b;
if (hi8 >= 0xA4 && hi8 <= 0xA7) dst_b = (hi8 >= 0xA6);
else if (hi8 >= 0xB4 && hi8 <= 0xB7) dst_b = (hi8 >= 0xB6);
else dst_b = (hi8 & 0x02) ? 1 : 0; /* 0xB0/B1→A, 0xB2/B3→B */
if (dst_b) {
if (is_sub) s->b = sext40(s->b - prod);
else s->b = sext40(s->b + prod);
} else {
if (is_sub) s->a = sext40(s->a - prod);
else s->a = sext40(s->a + prod);
}
/* T = Ymem */
s->t = yval_d;
return consumed + s->lk_used;
}
/* SQDST Xmem, Ymem — Squared Distance (1-word dual-operand)
* Encoding: 1010 0001 XXXX YYYY
* Per SPRU172C: B += (AH - Xmem)^2; A = Ymem << 16; T = Xmem */
if (hi8 == 0xA1) {
/* Xmem/Ymem 2-bit SPRU131G T.5-6/5-8 (sweep ; régression 0xfe36 ÉCARTÉE :
* derail byte-identique insn=193093 avec ce handler reverté → hors cause). */
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) {
/* FIX 2026-06-22 (sweep) : décodage Xmem/Ymem 2-bit SPRU131G T.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; }
int16_t ah_p = (int16_t)((s->a >> 16) & 0xFFFF);
int64_t prod_p = (int64_t)ah_p * (int64_t)(int16_t)s->t;
if (s->st1 & ST1_FRCT) prod_p <<= 1;
prod_p += 0x8000; /* round */
s->b = sext40(s->b + prod_p);
s->a = sext40((int64_t)(int16_t)xval_p << 16);
s->t = yval_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) {
/* Check if it's actually LDMM (BA) or POPM (BD) — those are handled below */
if (hi8 == 0xBA) goto ba_handler;
/* FIX 2026-06-22 (sweep) : décodage Xmem/Ymem 2-bit SPRU131G T.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; }
int64_t prod_b8 = (int64_t)(int16_t)s->t * (int64_t)(int16_t)xval_b8;
if (s->st1 & ST1_FRCT) prod_b8 <<= 1;
if (hi8 & 0x01) prod_b8 += 0x8000;
int dst_b8 = (hi8 & 0x02) ? 1 : 0;
/* MAS: subtract */
if (dst_b8) s->b = sext40(s->b - prod_b8);
else s->a = sext40(s->a - prod_b8);
s->t = yval_b8;
return consumed + s->lk_used;
}
ba_handler: if (hi8 == 0xAA || hi8 == 0xAB) { /* STUB-NOP : tic54x
dit 0xAA/AB = LD variant. * Ancienne classification qemu = STLM src,MMR
(incorrect — STLM * est en 0x88/0x89, déjà correctement décodé ligne
4046). * Voir doc/opcodes/tic54x_hi8_map.md. Neutralisé. / return 1;
} if (hi8 == 0xBA) { / LDMM MMR, dst — load MMR value into
accumulator * Per SPRU172C: dst[15:0] = MMR, dst[31:16] = sign-ext
(SXM)/0 * BUG FIX 2026-05-24 : was sext40(v << 16)
which put MMR in * dst[31:16], wrong half. The boot-stub at 0x0000 (LDMM
SP,B) * is supposed to return B = current SP for caller’s use ; with *
the << 16 bug, B[31:16] = SP, B[15:0] = 0, breaking any *
downstream caller that reads B as 16-bit SP value. / 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; } 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; } if (hi8 == 0xA0) { /* A0xx: accumulator operations —
LD/NEG/ABS/NOT/SFTA/SFTL/SAT * Per SPRU172C: * A000/A001: LD B,A / LD
A,B * A004/A005: NOT A / NOT B * A008/A009: NEG A / NEG B * A00A/A00B:
ABS A / ABS B * A00C/A00D: MAX A / MAX B (sat + clip) * A00E/A00F: MIN A
/ MIN B * bit7=0: SFTA dst, SHIFT — 1010 0000 0SSS SSSD (arith shift) *
bit7=1: SFTL dst, SHIFT — 1010 0000 1SSS SSSD (logical shift) *
A098/A099: SAT A / SAT B / uint8_t sub = op & 0xFF; if (sub ==
0x00) { s->a = s->b; } else if (sub == 0x01) { s->b = s->a;
} else if (sub == 0x04) { s->a = sext40(~s->a); } / NOT A
/ else if (sub == 0x05) { s->b = sext40(~s->b); } / NOT B
/ else if (sub == 0x08) { s->a = sext40(-s->a); } / NEG A
/ else if (sub == 0x09) { s->b = sext40(-s->b); } / NEG B
/ else if (sub == 0x0A) { s->a = sext40((s->a < 0) ?
-s->a : s->a); } / ABS A / else if (sub == 0x0B) {
s->b = sext40((s->b < 0) ? -s->b : s->b); } / ABS B
/ else if (sub == 0x98) { / SAT A / if (s->a >
0x7FFFFFFFFFLL) s->a = 0x7FFFFFFFFFLL; else if (s->a <
-0x8000000000LL) s->a = -0x8000000000LL; s->st0 &= ~ST0_OVA; }
else if (sub == 0x99) { / SAT B / if (s->b >
0x7FFFFFFFFFLL) s->b = 0x7FFFFFFFFFLL; else if (s->b <
-0x8000000000LL) s->b = -0x8000000000LL; s->st0 &= ~ST0_OVB; }
else if (sub & 0x80) { / SFTL dst, SHIFT — logical shift,
bits[6:1]=shift, bit[0]=dst / int shift = (sub >> 1) &
0x3F; if (shift & 0x20) shift |= ~0x3F; / sign-extend 6-bit
/ int dst = sub & 1; int64_t acc = dst ? &s->b :
&s->a; 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 >=
0x10) { / SFTA dst, SHIFT — arithmetic shift, bits[6:1]=shift,
bit[0]=dst / int shift = (sub >> 1) & 0x3F; if (shift
& 0x20) shift |= ~0x3F; / sign-extend 6-bit / int dst = sub
& 1; int64_t acc = dst ? &s->b : &s->a; if (shift
>= 0) acc = sext40(acc << shift); else acc =
sext40(acc >> (-shift)); } 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 = MACR Xmem, Ymem, B (1-word,
handled above with MAC family). * Fix 2026-05-29 : avant ce handler
décodait 0xB3xx comme * LD #lk, dst (2-word), ce qui
faisait drift le PC de +1 dans * une routine RPTBD à 0x820e..0x820f →
boucle infinie au PC= * 0x821a (= IMR clobber via délai-slot du BANZD).
/ / 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:
/* C/Dxxx: PSHM, POPM, PSHD, POPD, RPT, FRAME, etc. */
/* ---- Dual-operand MAC/MAS Xmem, Ymem, dst (1-word) ----
* 0xD0: MAC Xmem,Ymem,A 0xD2: MAC Xmem,Ymem,B
* 0xD1: MACR Xmem,Ymem,A 0xD3: MACR Xmem,Ymem,B
* 0xD4-0xD7: MAS variants (subtract)
*
* Encoding per binutils tic54x.h (XARX/YARX = ((C&0x3)+2)) :
* bits 7:6 Xmod | 5:4 Xar (AR2..AR5) | 3:2 Ymod | 1:0 Yar (AR2..AR5)
* Was 3-bit AR raw — same bug as C8/CB had (fixed 2026-05-08). Now
* aligned with binutils. Expected aftermath : new SP-CATASTROPHE on
* D-class opcodes when firmware ARs land at MMR — same root pattern
* as 0xc8be at PC=0xa0e7. That's correct exposure, not regression. */
if (hi8 >= 0xD0 && hi8 <= 0xD9 && hi8 != 0xDA) {
int xmod_c = (op >> 6) & 0x03;
int xar_c = ((op >> 4) & 0x03) + 2;
int ymod_c = (op >> 2) & 0x03;
int yar_c = (op & 0x03) + 2;
uint16_t xval_c = data_read(s, s->ar[xar_c]);
uint16_t yval_c = data_read(s, s->ar[yar_c]);
switch (xmod_c) {
case 0: break;
case 1: s->ar[xar_c]--; break; /* *AR- (SPRU131G T.5-8 : 01=dec) — fix 2026-06-22 */
case 2: s->ar[xar_c]++; break; /* *AR+ (10=inc) */
case 3: s->ar[xar_c] = c54x_circ_ref(s->ar[xar_c], +(int16_t)s->ar[0], s->bk); break; /* *AR+0% circ — fix 2026-06-01 */
}
switch (ymod_c) {
case 0: break;
case 1: s->ar[yar_c]--; break; /* fix 2026-06-22 (SPRU131G T.5-8) */
case 2: s->ar[yar_c]++; break;
case 3: s->ar[yar_c] = c54x_circ_ref(s->ar[yar_c], +(int16_t)s->ar[0], s->bk); break; /* *AR+0% circ (Ymem 0xd3dc @0xfa98) — fix 2026-06-01 */
}
/* MAC dual-mem formula : T × Xmem (pas X × Y per SPRU pure).
*
* 2026-05-08 retest empirique avec pipeline stable :
* T×X : BRC variable, A/B accumulator drift, d_fb_det reaches
* high SNR values (0x7902 / 0x7766) at moments
* X×Y : BRC=0 uniforme (201/201), A=B=0 forever, d_fb_det
* mostly 0 — correlation produces only zeros
*
* Le firmware Calypso s'appuie sur le pipeline c54x : T est
* latched depuis Ymem du MAC précédent (T = Y(post)). Ainsi
* MAC dual-mem effectivement calcule `T_old × X_current` =
* `Y[n-1] × X[n]`. Notre `prod = T × X` reproduit fidèlement
* cet effet pipelined. `X × Y` (les 2 du buffer courant) ne
* matche pas la sémantique attendue par le firmware. */
int64_t prod_c = (int64_t)(int16_t)s->t * (int64_t)(int16_t)xval_c;
if (s->st1 & ST1_FRCT) prod_c <<= 1;
if (hi8 & 0x01) prod_c += 0x8000; /* round */
int is_sub_c = (hi8 >= 0xD4);
int dst_c = (hi8 & 0x02) ? 1 : 0;
if (dst_c) {
if (is_sub_c) s->b = sext40(s->b - prod_c);
else s->b = sext40(s->b + prod_c);
} else {
if (is_sub_c) s->a = sext40(s->a - prod_c);
else s->a = sext40(s->a + prod_c);
}
s->t = yval_c;
return consumed + s->lk_used;
}
/* DBxx: MASA Xmem, Ymem, dst — MAC with accumulator sign extension
* Per SPRU172C: same as MAC but T loaded from Xmem instead of Ymem.
* dst += T * Xmem, T = Xmem
* Encoding fixed 2026-05-08 : same 2-bit AR + offset 2 + 2-bit mod
* format as the rest of the dual-operand class. */
if (hi8 == 0xDB) {
int xmod_db = (op >> 6) & 0x03;
int xar_db = ((op >> 4) & 0x03) + 2;
int ymod_db = (op >> 2) & 0x03;
int yar_db = (op & 0x03) + 2;
uint16_t xval_db = data_read(s, s->ar[xar_db]);
(void)data_read(s, s->ar[yar_db]); /* Ymem read (unused) */
switch (xmod_db) {
case 0: break;
case 1: s->ar[xar_db]--; break; /* fix 2026-06-22 (SPRU131G T.5-8) */
case 2: s->ar[xar_db]++; break;
case 3: s->ar[xar_db] = c54x_circ_ref(s->ar[xar_db], +(int16_t)s->ar[0], s->bk); break; /* *AR+0% circ — fix 2026-06-01 */
}
switch (ymod_db) {
case 0: break;
case 1: s->ar[yar_db]--; break; /* fix 2026-06-22 (SPRU131G T.5-8) */
case 2: s->ar[yar_db]++; break;
case 3: s->ar[yar_db] = c54x_circ_ref(s->ar[yar_db], +(int16_t)s->ar[0], s->bk); break; /* *AR+0% circ — fix 2026-06-01 */
}
int64_t prod_db = (int64_t)(int16_t)s->t * (int64_t)(int16_t)xval_db;
if (s->st1 & ST1_FRCT) prod_db <<= 1;
s->a = sext40(s->a + prod_db);
s->t = xval_db;
return consumed + s->lk_used;
}
/* DCxx: SQUR Xmem, dst — Square and accumulate (1-word dual-operand)
* Per SPRU172C p.4-165: T = Xmem, dst = dst + T * T
* Encoding fixed 2026-05-08 : same dual-operand format as D0-D9. */
if (hi8 == 0xDC) {
int xmod_dc = (op >> 6) & 0x03;
int xar_dc = ((op >> 4) & 0x03) + 2;
int ymod_dc = (op >> 2) & 0x03;
int yar_dc = (op & 0x03) + 2;
uint16_t xval_dc = data_read(s, s->ar[xar_dc]);
(void)data_read(s, s->ar[yar_dc]); /* Ymem pipeline read */
switch (xmod_dc) {
case 0: break;
case 1: s->ar[xar_dc]--; break; /* fix 2026-06-22 (SPRU131G T.5-8) */
case 2: s->ar[xar_dc]++; break;
case 3: s->ar[xar_dc] = c54x_circ_ref(s->ar[xar_dc], +(int16_t)s->ar[0], s->bk); break; /* *AR+0% circ — fix 2026-06-01 */
}
switch (ymod_dc) {
case 0: break;
case 1: s->ar[yar_dc]--; break; /* fix 2026-06-22 (SPRU131G T.5-8) */
case 2: s->ar[yar_dc]++; break;
case 3: s->ar[yar_dc] = c54x_circ_ref(s->ar[yar_dc], +(int16_t)s->ar[0], s->bk); break; /* *AR+0% circ — fix 2026-06-01 */
}
s->t = xval_dc;
int64_t prod_dc = (int64_t)(int16_t)xval_dc * (int64_t)(int16_t)xval_dc;
if (s->st1 & ST1_FRCT) prod_dc <<= 1;
s->a = sext40(s->a + prod_dc);
return consumed + s->lk_used;
}
/* CA/CB handled by the unified C8/C9/CA/CB block below. */
/* CF: variant parallel or DELAY */
if (hi8 == 0xCF) {
/* Treat as NOP for now — rare instruction */
return consumed + s->lk_used;
}
/* RPTB[D] pmad — Block repeat (2 words)
* C2xx: RPTB pmad, C3xx: RPTBD pmad (delayed)
* Per SPRU172C: RSA = PC+2, REA = pmad, BRAF = 1 */
if (hi8 == 0xC2 || hi8 == 0xC3 || hi8 == 0xC6 || hi8 == 0xC7) {
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 == 0xC5) {
/* STUB-NOP : tic54x dit 0xC5 = ST||family (parallel).
* Ancienne classification qemu = PSHM MMR (incorrect — vrai
* PSHM est en 0x4A, correctement décodé ligne 3816).
* Le sp-- ici causait des pushes fantômes. Neutralisé. */
return 1;
}
if (hi8 == 0xCD) {
/* STUB-NOP : tic54x dit 0xCD = ST||family (parallel).
* Ancienne classification qemu = POPM MMR (incorrect — vrai
* POPM est en 0x8A, fixé 2026-05-08).
* Le sp++ ici causait des pops fantômes. Neutralisé. */
return 1;
}
if (hi8 == 0xCE) {
/* STUB-NOP : tic54x dit 0xCE = ST||family (parallel).
* Ancienne classification qemu = FRAME #k (incorrect — FRAME
* n'a pas de hi8 fixe, encodage différent).
* Le sp+=k ici causait des sauts SP arbitraires. Neutralisé. */
return 1;
}
if (hi8 == 0xC4) {
/* C4xx: PSHD dmad (push data from absolute addr) */
op2 = prog_fetch(s, s->pc + 1);
consumed = 2;
s->sp--;
data_write(s, s->sp, data_read(s, op2));
return consumed + s->lk_used;
}
if (hi8 == 0xC0 || hi8 == 0xC1) {
/* PSHD Smem / RPT Smem variants */
addr = resolve_smem(s, op, &ind);
if (hi8 == 0xC0) {
/* PSHD Smem */
s->sp--;
data_write(s, s->sp, data_read(s, addr));
} else {
/* RPT Smem */
s->rpt_count = data_read(s, addr);
s->rpt_active = true; s->rpt_fresh = true;
s->pc += consumed;
return 0;
}
return consumed + s->lk_used;
}
if (hi8 == 0xCC) {
/* CCxx: SACCD Smem, ARmem — Store Acc Conditionally (1-word)
* Per SPRU172C: conditionally store AH or BH to Smem.
* Simplified: always store (condition always true). */
addr = resolve_smem(s, op, &ind);
data_write(s, addr, (uint16_t)((s->a >> 16) & 0xFFFF));
return consumed + s->lk_used;
}
if (hi8 == 0xDA) {
/* DAxx: RPTBD pmad (block repeat delayed, 2 words) */
op2 = prog_fetch(s, s->pc + 1);
consumed = 2;
s->rea = op2;
s->rsa = (uint16_t)(s->pc + 4); /* delayed: skip 2 delay slots */
s->rptb_active = true;
s->st1 |= ST1_BRAF;
return consumed + s->lk_used;
}
if (hi8 == 0xDD) {
/* STUB-NOP : tic54x dit 0xDD = ST||family (parallel) — base
* 0xDC00 mask 0xFC00. Ancienne classification qemu = POPD Smem
* (incorrect — vrai POPD en 0x8B, neutralisé en stub).
* Le sp++ ici causait le SP runaway post-POPM-fix observé
* 2026-05-08 (~13k faux pops en 64k insn). Neutralisé. */
return 1;
}
if (hi8 == 0xDE) {
/* STUB-NOP : tic54x dit 0xDE = ST||family (parallel).
* Ancienne classification qemu = POPD dmad 2-word (incorrect).
* Le sp++ ici causait le SP runaway. Neutralisé. */
return 1;
}
if (hi8 == 0xDF) {
/* DELAY Smem — shift delay line: data(Smem) → data(Smem+1)
* Per SPRU172C: used with RPT for FIR filter delay lines */
addr = resolve_smem(s, op, &ind);
uint16_t dval = data_read(s, addr);
data_write(s, addr + 1, dval);
return consumed + s->lk_used;
}
/* 0xC8/C9/CA/CB: ST SRC, Ymem || LD Xmem, DST (1-word parallel)
*
* Encoding per SPRU172C §5.5 (Parallel store + arithmetic format,
* cross-checked against tic54x-opc.c entry "0xC800/0xFC00 st||ld") :
*
* bit 15..10 : opcode (110010)
* bit 9 : reserved (used to disambiguate; here: 0 for C8/CA,
* bit 9 of 0xC9/CB still in opcode space — but the
* effective operand bits for parallel are 7:0)
* bit 8 : SRC accumulator select (0 = A, 1 = B)
* bits 7:6 : Xmod (0=*ARi 1=*ARi+ 2=*ARi- 3=*ARi+0%)
* bits 5:4 : Xar (00=AR2, 01=AR3, 10=AR4, 11=AR5) — only AR2..AR5
* bits 3:2 : Ymod (same encoding as Xmod)
* bits 1:0 : Yar (same encoding as Xar)
*
* Bug fix 2026-05-08 v2 evidence (DUAL-OP-INTERPRET log) :
* Previously decoded as `xar=(op>>4)&7`, `yar=op&7` (3-bit AR
* field) with bit 7 = Xmod ±, bit 3 = Ymod ±. That picked
* AR0/AR1 instead of AR2/AR3 and made post-mod always ± with
* no support for "no mod" or `*ARi+0%`. When firmware loaded
* AR1=0x0018 (= MMR_SP) for an unrelated reason, the *AR1
* write landed on the SP MMR slot — observed catastrophes
* Δ=+16601 / -16640 at PC=0x7818 / 0x786b are the consequence.
*
* Note on 0xCA/CB : per tic54x-opc.c, 0xC800 mask 0xFC00 covers
* 0xC800..0xCBFF for ST||LD (single instruction class). The
* earlier emulator split CA/CB into a separate block — that
* block is now removed, the C8..CB handler is unified here. */
if (hi8 >= 0xC8 && hi8 <= 0xCB) {
int s_acc = (hi8 & 0x01) ? 1 : 0; /* C9/CB store from B */
int xmod = (op >> 6) & 0x03;
int xar = ((op >> 4) & 0x03) + 2; /* AR2..AR5 */
int ymod = (op >> 2) & 0x03;
int yar = (op & 0x03) + 2; /* AR2..AR5 */
int d_acc = s_acc ? 0 : 1; /* LD into the OTHER acc */
int64_t st_val = s_acc ? s->b : s->a;
/* STLD-SP (patch #2 diag, gated CALYPSO_DEBUG=STLD-SP) : au site
* SP-CATASTROPHE (défaut PC=0xa0e7, env CALYPSO_TRACE_STLD_PC),
* dump la cible RÉELLE = AR[yar] PRÉ-modify + flag MMR_SP. Tranche
* le fork CC : si AR[yar]==MMR_SP(0x18) → le ST écrit SP (AR stale
* via STM skippé) ; sinon → write DARAM légal. */
{
static int stld_pc = -1;
if (stld_pc < 0) {
const char *e = getenv("CALYPSO_TRACE_STLD_PC");
stld_pc = (e && *e) ? (int)strtol(e, NULL, 0) : 0xa0e7;
}
if (s->pc == (uint16_t)stld_pc) {
C54_DBG("STLD-SP",
"STLD-SP op=0x%04x PC=0x%04x s_acc=%d yar=AR%d "
"tgt(AR%d_pre)=0x%04x is_MMR_SP=%d xar=AR%d AR%d=0x%04x "
"st_val=0x%010llx",
op, s->pc, s_acc, yar, yar, s->ar[yar],
(s->ar[yar] == MMR_SP), xar, xar, s->ar[xar],
(unsigned long long)(st_val & 0xFFFFFFFFFFULL));
}
}
data_write(s, s->ar[yar], (uint16_t)(st_val & 0xFFFF));
uint16_t ld_val = data_read(s, s->ar[xar]);
int64_t loaded = (int64_t)(int16_t)ld_val << 16;
if (d_acc) s->b = sext40(loaded); else s->a = sext40(loaded);
switch (xmod) {
case 0: break; /* *ARi (no mod) */
case 1: s->ar[xar]--; break; /* *ARi- (SPRU131G T.5-8 : 01=dec) — fix 2026-06-22 */
case 2: s->ar[xar]++; break; /* *ARi+ (10=inc) */
case 3: /* *ARi+0% — CIRCULAIRE modulo BK
* (était linear += AR0 → AR drift
* 16-bit vers 0x18=MMR_SP → SP-CATAS).
* Miroir du single-operand case 0xE. */
if (s->bk) {
uint16_t base = s->ar[xar] - (s->ar[xar] % s->bk);
uint16_t v = s->ar[xar] + s->ar[0];
if (v >= (uint16_t)(base + s->bk)) v -= s->bk;
s->ar[xar] = v;
} else {
s->ar[xar] += s->ar[0]; /* BK=0 → linéaire (pas de circ) */
}
break;
}
switch (ymod) {
case 0: break;
case 1: s->ar[yar]--; break; /* *ARi- (SPRU131G 01=dec) — fix 2026-06-22 */
case 2: s->ar[yar]++; break; /* *ARi+ (10=inc) */
case 3: /* *ARi+0% — circulaire modulo BK */
if (s->bk) {
uint16_t base = s->ar[yar] - (s->ar[yar] % s->bk);
uint16_t v = s->ar[yar] + s->ar[0];
if (v >= (uint16_t)(base + s->bk)) v -= s->bk;
s->ar[yar] = v;
} else {
s->ar[yar] += s->ar[0];
}
break;
}
return consumed + s->lk_used;
}
goto unimpl;
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; }
/* ================================================================ *
Main execution loop *
================================================================ */
/* DSP idle fast-forward — simulator optimisation, NOT a hack.
The Calypso DSP polls its task slots in NDB and write pages while
* waiting for ARM/TPU to post work. Empirically this dispatcher loop *
lives in PROM1 mirror at PC 0xe9ac..0xe9b7 (8-instruction body cycled *
~285k times per 1.4G insn window when nothing pending). Each iteration *
costs C-level MAC/branch emulation that ends up consuming 80%+ of host *
CPU for zero useful work, making QEMU run ~3x slower than wall-clock *
GSM and starving the BTS scheduler of CLK INDs. Detection: PC
inside the polling range AND all four task fields in * both write pages
are zero AND no interrupt pending. When confirmed, * advance
cycles/insn_count without invoking c54x_exec_one. The DSP * exits idle
naturally next iteration if either: * - ARM writes a task field
(mirrored via calypso_dsp_write to * s->data[0x0800+offset]) * - An
IRQ fires (calypso_c54x_interrupt_ex sets s->ifr) * - PC moves
outside the range (shouldn’t happen while polling) Env vars
(default ON) : * CALYPSO_DSP_IDLE_FF=0 disable *
CALYPSO_DSP_IDLE_RANGE=lo:hi override hex PC range / #define
DSP_IDLE_FF_MAX_RANGES 4 static bool dsp_idle_fast_forward(C54xState
s, int *consumed_out) { 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;
if (ff_enabled < 0) {
const char *e = 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 = 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);
}
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 calibration: real C54x at 65 MHz means 1 cycle ≈ 15 ns.
* The dispatcher body is ~8 instructions per pass (matches the 8 hot
* PCs observed). One pass ≈ 8 cycles ≈ 120 ns of *DSP* time.
*
* Per Claude web 2026-05-07 review: previously this returned a
* fixed 8-cycle skip per call regardless of host wall time. Combined
* with c54x_run(256000) that meant a single tick callback could
* burn through 32k FF iterations in microseconds host time but
* accumulate the full 256k cycles credit on the DSP — the net
* effect on QEMU virtual time was minimal (DSP cycles aren't a
* QEMU clock anyway), so this isn't itself the cause of the BTS
* timing skew. But to match wall-clock more honestly we now cap
* the FF run length per c54x_run invocation: at most enough skips
* to consume the budget (n_insns) without overshooting.
*
* The actual wall-clock alignment (CLK IND cadence) is owned by
* the TDMA timer in calypso_trx.c, not by 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;
}
/* === CALYPSO_TRAP_OOR hook v2 (root-cause probe SP descent
0..checkpoint) === * v2 redesign: T1/T2 dropped (scheduler exonerated →
SP clobber lives in * legit code, whitelist can’t see it). Pure
observability: * - g_sp_trail[256] : SP changes with |Δ|>32
(scheduler reloads, large * allocations) — skip push/pop ±1 noise. * -
sp_low watermark : every new low logged (PC-coalesced power-of-10) * —
catches BOTH absolute reloads AND push-drain runaway. * - Per-event
A_low captured (= candidate STL A,Smem source). * - Halt at fixed
checkpoint (env CALYPSO_TRAP_CHECKPOINT, default 4.2M * = just after the
insn=4.09M SP recovery 0x0008→0x2900). */
static struct { unsigned insn; uint16_t old_sp, new_sp, exec_pc,
exec_op, a_low; } g_sp_trail[256]; static unsigned g_sp_trail_idx =
0;
/* sp_low watermark — coalesced by PC */ static uint16_t g_sp_low =
0xFFFF; static uint16_t g_sp_low_pc = 0xFFFF; static unsigned
g_sp_low_hits_at_pc = 0; static unsigned g_sp_low_distinct_pcs = 0;
/* SP-decrement histogram per-PC (fix 2026-05-24 v3 — Claude web
correction). Gating par VALEUR SP (pas insn_count) — robuste
à : * - DSP idle fast-forward (dsp_idle_fast_forward L5937 inflate
insn_count * sans exécuter d’opcodes) * - jitter externe wall-clock
(bridge/osmocon/BTS sur UDP+PTY → instant * guest où arrive un burst
varie run-à-run, insn_count des events * déclenchés par bursts pas
stable) Logique : armé quand SP descend SOUS le plateau
(default < 0x2000, sous * 0x3fb0 où SP stationne 632k→3.5M insns du
run jackpot). Reste armé * jusqu’au dump quand SP < 0x0100 (proche
underflow). Pendant cette fenêtre, * compte chaque SP-décrement par PC.
3 bénéfices : * 1. Auto-aligne sur la descente quels que
soient FF et jitter externe * 2. Rend la question FF caduque (descente =
real insns, pas idle-poll) * 3. Exclut churn équilibré du plateau
(PSHM/POP matched à PCs distincts * pollue insn-window mais pas
SP-window — leaker domine mécaniquement) Override env : *
CALYPSO_SP_HIST_ARM (default 0x2000) — threshold pour armer *
CALYPSO_SP_HIST_DUMP (default 0x0100) — threshold pour dumper
Capture toutes voies SP-write : direct s->sp–, MMR_SP via data_write
* callback, IRQ push (audit couverture 2026-05-24 : tous paths passent *
par s->sp variable). / typedef struct { uint16_t pc; uint16_t
op_last; uint32_t dec_count; int32_t delta_sum; / négatif = drain
net / } SpDecEntry; #define SP_HIST_MAX 512 static SpDecEntry
g_sp_dec_hist[SP_HIST_MAX]; static unsigned g_sp_dec_used = 0; static
unsigned g_sp_dec_total_events = 0; static uint16_t
g_sp_dec_arm_threshold = 0; static uint16_t g_sp_dec_dump_threshold = 0;
static int g_sp_dec_enabled = -1; static int g_sp_dec_armed = 0; static
int g_sp_dec_dumped = 0; static unsigned g_sp_dec_arm_insn = 0; /
insn at which we armed / static uint16_t g_sp_dec_arm_sp = 0; /
SP value at arm */
/* === Raw SP ring buffer (Patch 3 — 2026-05-25, rev 2) === *
Per-iteration record of (insn, PC, SP, op) at top-of-loop, no filter, *
no sign classification. Plusieurs triggers configurables. Rev
1 (floor-cross) : a montré que le « plongeon SP→0 » est en réalité * un
wrap-forward par pops dans un boot-stub spiral (PC=0x0000/0x0001 * en
boucle, SP++ par RET). Le kill réel = un RET corrompu qui saute * à
0x0000 BIEN AVANT le wrap, dans [3.5M, 4.09M] insns. Floor-cross *
arrive 600k insns trop tard, déjà dans le spiral. Rev 2
(bootstub-entry) : trigger sur l’EDGE prev_pc ∉ [0x00,0x7F] → * s->pc
∈ [0x00,0x7F]. Capture la transition exacte = le RET fauteur * + son SP
+ le mot poppé (mem[topgate_last_sp]). Discrimine 2 bugs * radicalement
différents : * - SP valide (~0x3fbb) + mem[SP]=0 → return slot écrasé
par un * write sauvage. fd28-fd2a n’y change rien. À chasser autrement.
* - SP en non-stack (~0x2bc0) → famille 0xfd2a A=AR4. fd28-fd2a *
devient le fix. Env gates : * CALYPSO_SP_RING=1 active
(default OFF, zéro coût sinon) * CALYPSO_SP_RING_MAX=N cap dumps par run
(default 4) * CALYPSO_SP_RING_TRIG=mode floor|bootstub|both (default
bootstub) * CALYPSO_SP_RING_INSN_MIN=N skip first N insns (default
1000000 — le * firmware Calypso fait des CALL légitimes * au boot stub
0x0000/0x0001 en phase init, * le 1er CALL captérait un faux positif et
* consommerait le one-shot. Le vrai bug * observé est dans [3.5M, 4.09M]
insns) / #define SP_RING_SZ 4096 / must be power of 2 — bumped
512→4096 for * coverage des 1000s d’insns avant le spiral */ typedef
struct { unsigned insn; uint16_t pc; uint16_t sp; uint16_t op; uint16_t
_pad; } SpRingEntry; static SpRingEntry g_sp_ring[SP_RING_SZ]; static
unsigned g_sp_ring_head = 0; static uint64_t g_sp_ring_total = 0; static
int g_sp_ring_enabled = -1; static unsigned g_sp_ring_dump_count = 0;
static unsigned g_sp_ring_dump_max = 0; /* Trigger mode (rev 2) : 1 =
floor-cross, 2 = bootstub-entry, 3 = both / static int
g_sp_ring_trig_mode = 0; static 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”, 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”, 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 =
getenv(“CALYPSO_SP_RING_MAX”); g_sp_ring_dump_max = (m &&
m) ? (unsigned)strtoul(m, NULL, 0) : 4u; /* Trigger mode parse (rev
2). Default = bootstub (le seul utile post * rev-1 — floor-cross firait
dans le spiral, trop tard). / const char t =
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 = 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”, 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); } }
/* Rev 2 : detect edge PC entry into boot stub area [0x0000, 0x007F].
* topgate_last_pc = PC of 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 the RET pop (= topgate_last_sp +
1 if 1-word). * Capture verbose state + dump ring to identify the
corrupting RET. * Static cap : one detailed dump per run (le 1er, qui
contient le * caller; subsequent fires sont des re-entries du même
spiral). / static 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 boot phase : firmware fait des CALL légitimes au boot stub *
0x0000-0x0001 pendant l’init (LDMM SP,B est documenté). Le 1er * trigger
sans gate fire à insn=145 et consomme le 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]);
/* Diagnostic discriminator — match user's discrimination criteria :
* SP valide (~0x3fbb plage observée) + popped==0 → return slot
* écrasé par write sauvage. 0xfd2a est innocent.
* SP en zone non-stack (~0x2bc0 ou similar buffer) → famille
* 0xfd2a A=AR4. fd28-fd2a est le 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”, 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 par 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 par |delta_sum| (single-event jump corrupteur — 1 event huge). */
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 =
getenv(“CALYPSO_SP_HIST_ARM”); const char e_dump =
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”, g_sp_dec_arm_threshold, g_sp_dec_dump_threshold); } if
(!g_sp_dec_enabled) return; /* Patch 1 (2026-05-25) : freeze RETIRÉ.
L’ancien if (g_sp_dec_dumped) * return; faisait du
one-shot, donc tous les events post-1er-dump * étaient perdus. Sans
freeze, plusieurs dumps consécutifs si SP * reste sous threshold — c’est
borné en pratique par le rate-limit * du sp_ring_dump_max et par le
edge-detect dans le top-of-loop. */
/* Patch 2 (2026-05-25) : drop le cast (int16_t). Le wrap signé
* mis-classifiait les chutes high→low en pop. Pure int32 sub :
* 0x9006→0x0000 : delta = -36870 (correct, descent capturé)
* 0xC000→0x0000 : delta = -49152 (correct, descent capturé)
* Note : casse l'underflow wrap (0x2bc0→0xfff8 = +52280, vu comme
* pop), mais l'histo n'est plus la source de vérité pour le kill
* — c'est le ring buffer qui tranche. Histo = drift trickle uniquement. */
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 event AVANT le dump check (fix 2026-05-24 v4 — sinon un
* single-event jump qui franchit DUMP en une instruction est perdu). */
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 first 10 events verbatim — for single-event jumps the corrupteur
* est dans les premiers events (souvent un seul mot dans le histo). */
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 : APRÈS l'accounting, vérifier seuil dump.
* Patch 1 (2026-05-25) : edge-trigger only — dump quand SP croise
* sous le floor (sp_before >= threshold && sp_now < threshold).
* Rev 2 : gaté par g_sp_ring_trig_mode (bit 0 = floor). Par défaut
* bootstub seulement, parce que floor-cross fire dans le spiral
* (trop tard) — cf rev 1 finding. */
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”, 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, “TRAP
sp_low=0x%04x at last_pc=0x%04x hits_at_pc=%u distinct_pcs=%u”,
g_sp_low, g_sp_low_pc, g_sp_low_hits_at_pc, g_sp_low_distinct_pcs); /*
SP-HIST dump (fix v3 2026-05-24 — SP-windowed, no-insn-dep). */ 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):”); 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“, 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”, (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”, 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;
/* Log first 10 instructions of each run (for 2nd cycle debug) */
static int run_num = 0;
run_num++;
/* 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 (2026-05-15 nuit, per Claude web Q1).
* Hypothèse à valider : le path completion CCCH demod passe par PROM1
* (XPC=1) via le B 0x9ab1 à 0x19aac. Si XPC=1 jamais atteint → bug
* dans le route initial. Si atteint mais PC pas dans 0x9aac+ → entrée
* OK mais pas cette zone. Tracking :
* - insn count par XPC (0..3)
* - dernier PC visité par XPC
* - first_visit_insn par XPC (= quand on entre en XPC=N pour la 1ère fois)
* - ring buffer 16 derniers PCs visités sous XPC=1 (zone d'intérêt)
*/
{
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[0],
(unsigned long long)xpc_insn_count[1],
(unsigned long long)xpc_insn_count[2],
(unsigned long long)xpc_insn_count[3],
(unsigned long long)xpc_first_insn[0],
(unsigned long long)xpc_first_insn[1],
(unsigned long long)xpc_first_insn[2],
(unsigned long long)xpc_first_insn[3],
xpc_last_pc[0], xpc_last_pc[1], xpc_last_pc[2], xpc_last_pc[3]);
if (xpc1_pc_ring_count > 0) {
/* Dernier 16 PCs visités sous XPC=1 (ring buffer) */
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-16)&15],
xpc1_pc_ring[(xpc1_pc_ring_idx-15)&15],
xpc1_pc_ring[(xpc1_pc_ring_idx-14)&15],
xpc1_pc_ring[(xpc1_pc_ring_idx-13)&15],
xpc1_pc_ring[(xpc1_pc_ring_idx-12)&15],
xpc1_pc_ring[(xpc1_pc_ring_idx-11)&15],
xpc1_pc_ring[(xpc1_pc_ring_idx-10)&15],
xpc1_pc_ring[(xpc1_pc_ring_idx-9)&15],
xpc1_pc_ring[(xpc1_pc_ring_idx-8)&15],
xpc1_pc_ring[(xpc1_pc_ring_idx-7)&15],
xpc1_pc_ring[(xpc1_pc_ring_idx-6)&15],
xpc1_pc_ring[(xpc1_pc_ring_idx-5)&15],
xpc1_pc_ring[(xpc1_pc_ring_idx-4)&15],
xpc1_pc_ring[(xpc1_pc_ring_idx-3)&15],
xpc1_pc_ring[(xpc1_pc_ring_idx-2)&15],
xpc1_pc_ring[(xpc1_pc_ring_idx-1)&15]);
}
}
}
/* DISPATCH-CALLER probe (2026-05-15 nuit, per Claude web).
* Les 3 callers de 0x9aaf identifiés par scan PROM :
* PC=0x8815 : f074 9aaf (B 0x9aaf depuis table @0x8810)
* PC=0x9296 : f274 9aaf (BD 0x9aaf depuis routine spécifique)
* PC=0x9418 : f274 9aaf (BD 0x9aaf depuis autre routine)
* Log A, AR0..2, data[0x0828/9] à chaque 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 (Claude web 2026-05-15
* nuit étape 3). Diagnostic : valeur AR7 au moment du MVMD AR7,BRC à
* PC=0x8208, sa chaîne causale (16 derniers writes AR7), et l'état BRC
* post-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) Snapshot complet à chaque hit de 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) État RPTB après setup (PC=0x820c = delay slot post-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 (Claude web 2026-05-15 nuit, étape 2).
* Sample 1/1000 du context (PC/ST1/BRC/XPC) quand INT3 pending + INTM=1.
* Discrimine : (a) opcode set INTM=1 sans clear (variante POPM),
* (b) RPTB long non-interruptible (BRC > 0 partout),
* (c) STM ST1 / MVDM ST1 brut. Cf matrice Claude web. */
{
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 (Claude web 2026-05-15 nuit, étape 1).
* Diagnostic timing TDMA vs wall-clock : INT3 = frame interrupt
* (IMR bit 3, vec 19, addr 0xFFCC). Mesure fire/serviced/missed/latency.
* Discrimine : ISR mal vectorisée (service<fire), TPU/TSP fail (fire=0),
* compute trop lent (missed>0). Cause root LOST 3468 + variance XPC. */
{
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 (Claude web 2026-05-15 nuit).
* Le DSP tourne en XPC=2 dans zone hot 0xdf80..0xdfc0 (CCCH demod MAC loop).
* Discrimine entre 3 hypothèses :
* (1) compute jamais exit (threshold non franchi)
* (2) IRQ jamais fire (TPU/TSP source manquante)
* (3) IRQ fire mais pas serviced (INTM stuck ou ISR mal vectorisée)
* Matrice de décision basée sur exits_count + irq_pending_in_compute. */
{
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 (per Claude web option 3 hybride).
* Le dispatcher caller saute vers 0x8810 + task_id*3, où chaque entry =
* { 0xf4e4 (FRET ou padding), 0xf074 (B opcode), <target> }.
* On probe le PC qui correspond au début d'un entry (PC = 0x8810 + N*3).
* task_id estimé = (PC - 0x8810) / 3.
* Si entry exec OK → on lit data[PC+2] qui est le target. */
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 after ARP fix: b8e9..b906 (run 2, 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 disasm (run 3 stuck zone).
* Looking for B-self (f073 066f) vs IDLE n (f7e1/f7e2/f7e3)
* vs poll-and-branch. If IDLE found → emulator IDLE handler
* is the real bug (3 runs all hit the same opcode, terminate
* in different bassins because PMST/IPTR varies). */
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);
}
}
}
while (executed < n_insns && s->running && !s->idle) {
/* === SILICON-BOOT-ROM REDIRECT (réactivé 2026-05-30) ===========
* Le dump PROM ne contient PAS le mask-ROM silicon du Calypso. Sur
* vrai HW, ce ROM masqué tourne au reset, pose SP=0x5AC8 + MMR, puis
* saute à l'entrée firmware PROM0[0x7120] (= STM #0x5AC8,SP vérifié :
* prog[0x7120]=0x7718 STM #lk,SP, prog[0x7121]=0x5ac8). On MODÉLISE
* ce hardware manquant — ce n'est PAS un override d'instruction
* firmware : on route vers l'entrée firmware propre, qui fait elle-
* même son init SP.
*
* RÉGRESSION corrigée : retiré le 29/05 (c3ec660 « relancer via
* 0xFF80 réel »). Sans lui, reset → 0xff80(FB) → 0xb410 → CC → 0x76f8
* SANS jamais exécuter STM #0x5AC8,SP → SP coincé à 0x1100 (invalide,
* = aire MMR/AR0) → over-pop boot (net→-57) → return corrompu →
* self-CALA 0x70c3 → spirale (16M pushes) → PMST 0x70C4 fuit en
* TOA=28868 côté osmocon → FB jamais locké.
*
* Gate SP==0x1100 = cold-reset uniquement (valeur silicon-reset). Une
* fois SP=0x5AC8 posé par 0x7120, la condition retombe → les walks
* séquentiels firmware passant par 0xff80 plus tard NE sont PAS
* hijackés (cf SOFT-RESET-TRIG ci-dessous, insn>100k). */
/* EXPÉRIENCE 2026-05-30 (CC-web) testée et CONCLUE : poser SP=0x5AC8 sans
* rediriger le PC (laisser 0xff80→0xb410 tourner) → le reset-handler
* 0xb410 s'exécute MAIS n'appelle PAS le boot-init 0x7000-0x7025 (reste
* 1 hit incident) ; FB-dispatch échoue identiquement. Donc FB-dispatch
* n'est PAS la queue du boot-init = issue steady-state SÉPARÉE. Acquis :
* l'over-pop était 100% un artefact de SP=0x1100 (F@0x76f8 tourne propre
* depuis 0x5AC8, no wedge). On revient au redirect 0x7120 committé. */
/* === REDIRECT NEUTRALISÉ PAR DÉFAUT (2026-05-31) ===================
* Ce bloc MODÉLISE un mask-ROM TI absent du dump = un HACK (simulation).
* Méthode user : NE RIEN simuler. On laisse le vrai reset vector jouer
* (0xff80 = FB 0xb410 = vrai reset handler firmware) et on débugge
* CHAQUE bug réel de la chaîne de boot avec les valeurs qu'elle produit.
* Le redirect est conservé derrière CALYPSO_REDIR_LEGACY=1 UNIQUEMENT
* pour comparaison A/B ; OFF par défaut. Premier bug réel attendu sans
* lui (cf ancien commentaire) : à 0xb410 le CC saute le STM #0x5AC8,SP
* → SP reste 0x1100 → over-pop. C'est CE bug qu'on trace, pas qu'on
* contourne. */
/* @BEQUILLE — REDIR_LEGACY (CALYPSO_REDIR_LEGACY, EXISTS, defaut OFF)
* masque : le reset vector reel 0xff80 -> 0xb410 est detourne pour simuler le
* mask-ROM TI absent du dump (le commentaire ci-dessus l'assume).
* retirer : des que le vrai reset handler 0xb410 pose SP=0x5AC8 lui-meme
* (STM #0x5AC8,SP correctement decode) et que le boot deroule sans
* over-pop — c'est le bug a tracer, pas a contourner.
* NB : maitre de INITTAB et REDIR7000 ; exclut MASKROM_INIT.
*/
static int redir_legacy = -1;
if (redir_legacy < 0) redir_legacy = getenv("CALYPSO_REDIR_LEGACY") ? 1 : 0;
if (redir_legacy && s->pc == 0xFF80 && s->sp == 0x1100) {
static int redirect_log;
/* EXPÉRIENCE CALYPSO_REDIR7000 (2026-05-30) : le redirect→0x7120 saute
* l'init qui peuple les tables BACC-A (data[0x4c5b]/0x3fe1) → A=0 →
* boot stub → dispatch dormant. Test : poser SP=0x5AC8 (mask-ROM) +
* rediriger vers 0x7000 (init COMPLÈTE : tables + A) pour que BACC A
* atteigne la vraie entrée firmware. cf SESSION_2026-05-29 fix#2. */
/* @BEQUILLE — REDIR7000 (CALYPSO_REDIR7000, EXISTS, defaut OFF)
* masque : l'init des tables BACC-A (d[0x4c5b]/d[0x3fe1]) que le point d'entree
* 0x7120 suppose deja faite : on redirige le reset vers 0x7000.
* retirer : identique a INITTAB (table peuplee par le chemin firmware).
* NB : imbriquee dans REDIR_LEGACY, et ecrasee par INITTAB (else if).
*/
static int redir7000 = -1;
if (redir7000 < 0) redir7000 = getenv("CALYPSO_REDIR7000") ? 1 : 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++;
}
/* VALIDATION CALYPSO_INITTAB (env, réversible) : prouve que peupler la
* table de dispatch débloque FB. Pose SP, PUSH retour=0x7120, saute à
* 0xc704 (table-init) → peuple data[0x4c24-0x4c5d] → RET vers 0x7120 →
* boot normal continue AVEC table peuplée → BACC A atteint les vrais
* handlers. Débloque FB → root+fix prouvés ; sinon → table pas le seul. */
/* @BEQUILLE — INITTAB (CALYPSO_INITTAB, EXISTS, defaut OFF)
* masque : l'absence du mask-ROM TI qui peuple la table de handlers de tache
* 0x4c24-0x4c5d au reset ; sans elle 0x7120 fait BACC d[0x4c5b]=null.
* retirer : des que la table est peuplee par un chemin firmware (0xc704 atteint
* nativement apres le clear 0x8869) — sonde INSTALL-TRACE d[4c5c]!=0.
* NB : sans CALYPSO_REDIR_LEGACY, ce gate n'est jamais evalue.
*/
static int inittab = -1;
if (inittab < 0) inittab = getenv("CALYPSO_INITTAB") ? 1 : 0;
if (inittab) {
s->sp = 0x5AC8;
s->sp--; s->data[s->sp] = 0x7120; /* retour = boot normal */
s->pc = 0xc704; /* run table-init → RET 0x7120 */
} else if (redir7000) { s->sp = 0x5AC8; s->pc = 0x7000; }
else s->pc = 0x7120;
}
/* [2026-07-23] MASK-ROM TABLE-INIT (default ON) : le dump PROM ne contient
* pas le mask-ROM TI qui, au reset, PEUPLE la table de handlers de tache
* (0x4c04-0x4c5d) -- sinon 0 apres le clear RPTB 0x8869 -> l'entree firmware
* 0x7120 BACC d[0x4c5b]=null et 0x7025/0xd247/0xc8e9/corr ne tournent JAMAIS.
* On MODELISE ce HW absent : au cold-reset (PC=0xff80,SP=0x1100) pose SP=0x5AC8,
* PUSH retour=0xb410 (reset handler normal -> park b41c PRESERVE), saute 0xc704
* (fill table, RET @0xc826, adressage ABSOLU -> OK meme AR/DP non-init). INITTAB
* a prouve que peupler la table debloque FB. OFF via CALYPSO_MASKROM_INIT_OFF=1.
* Exclusif avec redir_legacy (qui gere deja 0xff80). */
if (!redir_legacy && s->pc == 0xFF80 && s->sp == 0x1100) {
/* @BEQUILLE — MASKROM_INIT (CALYPSO_MASKROM_INIT, EXISTS, defaut OFF)
* masque : identique a INITTAB — mask-ROM TI absent qui pose SP=0x5AC8 et
* peuple la table de handlers au cold-reset.
* retirer : meme condition qu'INITTAB (table peuplee par chemin firmware).
* NB : le commentaire ci-dessus renvoie a CALYPSO_MASKROM_INIT_OFF, variable
* qui N'EXISTE PAS — le gate reel est opt-in CALYPSO_MASKROM_INIT.
*/
static int mrti = -1;
if (mrti < 0) mrti = getenv("CALYPSO_MASKROM_INIT") ? 1 : 0; /* [2026-07-23] OPT-IN (default OFF) : le forcing boot-op derail (etat froid) ; garde pour 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; /* retour = entree firmware (BACC d[0x4c5b] peuple) */
s->pc = 0xc704; /* peuple table handlers -> RET 0x7120 -> operationnel */
}
}
/* [2026-07-23] TABLE RE-POPULATE apres le clear boot : la routine 0x8866-0x886a
* (RPTB memset 64 mots) WIPE la table handlers APRES le populate mask-rom (insn
* ~19793 > insn 92). Le firmware normal ferait clear->populate mais 0xc704 n'est
* jamais atteint apres le clear. Fix : au RET du clear (0x886a), si la table est
* vide, rediriger vers 0xc704 (populate ; son RET @0xc826 depile le meme retour
* = caller du clear). Self-heal a chaque clear. OFF via CALYPSO_MASKROM_INIT_OFF. */
/* [2026-07-23] BOOTSTRAP OPÉRATIONNEL 0xd247 : le sous-système op (install
* table handlers 0xc704 + slots TDMA 0xc867 + vecteurs) est AUTO-RÉFÉRENTIEL
* (appelé seulement depuis 0x7025, jamais bootstrappé -> mask-ROM absent). Sans
* lui : d[0x4c5c]=0 + d[0x3f6b]=0xd294(RET no-op) -> acquisition FB no-op ->
* d[3f70] jamais 2 -> corr jamais. On MODÉLISE le bootstrap mask-ROM : au terminal
* boot-init 0xb3e4 (état prêt : SP=0x5AC8, cellules seedées), one-shot run 0xd247
* (RET @0xd25f -> revient à 0xb3e4). OFF via CALYPSO_D247_OFF=1. */
if (s->pc == 0xb3e4) {
/* @BEQUILLE — D247 (CALYPSO_D247, EXISTS, defaut OFF)
* masque : l'absence du bootstrap mask-ROM TI qui, sur silicium, appelle le
* sous-systeme operationnel 0xd247 (install table handlers 0xc704 +
* slots TDMA 0xc867 + vecteurs) ; en QEMU 0xd247 n'a d'appelant natif
* qu'a PROM0 0x7102, bloc jamais atteint au boot froid. On PUSH le
* retour et on detourne le PC.
* retirer : des que le bloc appelant natif 0x70ce-0x7106 est atteint (sonde
* D247-TRACE site 0x7102 non nulle), OU des que la table d[4c5c] est
* peuplee par le chemin firmware.
* NB : le commentaire ci-dessus annonce CALYPSO_D247_OFF=1 — cette variable
* n'existe pas, le gate reel est opt-in CALYPSO_D247.
*/
static int _d247 = -1;
if (_d247 < 0) _d247 = getenv("CALYPSO_D247") ? 1 : 0; /* [2026-07-23] OPT-IN OFF : bootstrap pousse dans 0xc6a5 (init coeffs) mais boucle sur source vide. Garde 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; /* retour = terminal boot-init */
s->pc = 0xd247;
}
}
if (s->pc == 0x886a && s->data[0x4c5c] == 0) {
/* @BEQUILLE — REPOPULATE (CALYPSO_REPOPULATE, EXISTS, defaut OFF)
* masque : le memset RPTB 0x8866-0x886a wipe la table de handlers apres son
* peuplement, sans que le firmware rappelle 0xc704 ; la branche reelle
* = l'ordre firmware clear -> populate. On detourne le PC vers 0xc704.
* retirer : des que 0xc704 est atteint APRES le clear par le flot natif
* (D247-TRACE : d[4c41]/d[4c46] non nuls en fin de boot).
*/
static int mrti2 = -1;
if (mrti2 < 0) mrti2 = getenv("CALYPSO_REPOPULATE") ? 1 : 0; /* [2026-07-23] OPT-IN OFF : peupler 0x4c5c ne debloque PAS l acquisition FB (teste : fb0_att reste 0). Garde pour 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;
}
}
/* [2026-07-23] D247-TRACE (READ-ONLY, no state mutation) : le workflow de
* recon a montre que 0xd247 A un vrai appelant natif unique -- PROM0 0x7102,
* dans le bloc operationnel 0x70ce-0x7106 (PAS un stub mask-ROM orphelin comme
* suppose par BOOTSTRAP-D247 ci-dessus, qui l'appelait a tort au cold-reset
* ou SP est invalide -> derail 0x3350). Ces sondes verifient SANS RIEN FORCER :
* (a) exec_pc atteint-il 0x7102 nativement (le bloc appelant tourne-t-il) ?
* (b) 0xd247 fire-t-il, avec quel etat table avant/apres son RET (@0xd25f) ?
* (c) le clear 0x87ff (callers trouves dans PROM1 via FCALL, PAS PROM0) tourne-t-il,
* et AVANT ou APRES 0xd247 -- wipe-t-il le travail de 0xc704 ? d[4c41]/d[4c46]
* = 2 slots de la table lus par le dispatcher 0xc8e9 (CALA), indicateurs directs
* de succes d'install. Defaut ON, cap 20/site. OFF via CALYPSO_D247_TRACE_OFF=1
* (atoi, pas presence -- cf bug de gating INIT_435B_OFF corrige plus tot). */
{
static int _d247t = -1;
if (_d247t < 0) { const char *_e = 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);
}
}
/* [2026-07-23] CYCLE-TRACE (READ-ONLY) : cycle 1 (bit4 arme, CALYPSO_SEED_52FD)
* complete PROPREMENT a51c->a526->a529->a534->a537->a53c->a53f->a541->a544->a549
* ->a582->b522->011e (confirme HANDLER-PATH). Puis cycle 2+ tombe dans une boucle
* 0x71d7<->0x71db (146x observe) au lieu de refaire ce chemin. Cette sonde trace
* CHAQUE passage (pas cappe a 1) pour voir EXACTEMENT ou/quand ca diverge entre
* cycle 1 et cycle 2, + logge l entree dans le wrapper 0x71d3 (avant la boucle)
* avec l etat cle (d[3f92], d[5a00], d[435b]=IMR-shadow, IMR reel). Cap 80/site. */
{
static int _cyc = -1;
if (_cyc < 0) { const char *_e = 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)
fprintf(stderr, "[c54x] CYCLE-TRACE #%u a53c(BITF AR1+10,0x8000) AR1=0x%04x d[3f92]=0x%04x "
"data[0x0810]=0x%04x(B_TASK_ABORT=%d) fn=%u insn=%u\n",
n53c, s->ar[1], s->data[0x3f92], s->data[0x0810],
!!(s->data[0x0810] & 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);
}
}
/* [2026-07-23] CLUSTERB-8D21 (READ-ONLY) : cible CALLD jamais tracee avant, a
* l'INTERIEUR du range correlateur (0x8d00-0x9000), appelee UNIQUEMENT par les
* handlers task-type 4/6 (Cluster B). Desassemblage statique montre 2 RPTB/RPTBD
* imbriques + T=0x18(24, tap-count-shaped) + adressage MAR indirect circulaire --
* signature DSP signal-processing authentique (contraste net avec le cluster audio
* c1fa/c27b et les utilitaires bitmask 8f7f/8f9d, tous deux ecartes). Cap 30. */
{
static int _c8d21 = -1;
if (_c8d21 < 0) { const char *_e = 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);
}
}
/* [2026-07-23] BITF-000B-HIT (READ-ONLY) : les 2 BITF sur data[0x000b] trouves
* dans le dispatcher background (0xdeb6: BITF *(0x000b),0x4000 bit14 ;
* 0xdec2: BITF *(0x000b),0x2000 bit13). Hypothese user (screenshot
* INTM-TRANS + "l'histoire des 11") : est-ce que ce cycle go-live qui
* boucle sans jamais atteindre le correlateur attend un compteur/flag
* en 0x000b que seul un vrai timing sequenceur TPU (les 11 tpu_enq_at(0)
* de l1s_rx_win_ctrl, non modelise -- cf calypso_tpu.c) ferait progresser ?
* Logge data[0x000b] AVANT execution (= ce que BITF va tester) aux deux
* PC. Complement de WATCH-000B-WR (qui confirme si la cellule est meme
* ecrite). Cap 40 chacun. */
{
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);
}
/* [2026-07-23] CLUSTERB-SITES (READ-ONLY) : les 3 sites de dispatch task-type
* (0x8b01=task4/site2 = celui qui a tire une fois ; 0x8ac4=task3/site1 ;
* 0x8b8c=task6/site3, tres probablement SB_DSP_TASK=6). Logge task-type courant
* (d[0x4357]) + AR3 (attendu 0x2bc0 pour sites 2/3, pointeur I/Q). Cap 30/site. */
{
static int _cbs = -1;
if (_cbs < 0) { const char *_e = 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);
}
}
}
}
/* [2026-07-23] TASKTYPE-SRC (READ-ONLY) : 0xa6e9 = STL A,*(0x4357), source du code
* task-type interne qui pilote tout le dispatch Cluster B. Logge A pour identifier
* l'evenement amont qui produit chaque valeur. Cap 40. */
{
static int _tts = -1;
if (_tts < 0) { const char *_e = 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);
}
}
/* [2026-07-23] A546-HIT (READ-ONLY) : le seul BACC natif connu vers le bootstrap
* 0xd247 passe par 0xa546 (LD d[0x3fe0],A ; BACC A), lui-meme gate par
* BITF d[0x09bc],1 a 0xa544 (cf WATCH-09BC-WR). Confirme si ce chemin tire. */
{
static int _a546on = -1;
if (_a546on < 0) { const char *_e = 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);
}
}
/* [2026-07-23] C1FA-ENTRY (READ-ONLY) : 0xc1fa est la SEULE cible CALA
* jamais tracee du dispatch 0xa57c (LD d[0x3fd4],A ; CALA A), constante=0xc1fa
* a chaque hit (confirme statique par CALA-TRACE). Jamais disassemble ni
* instrumente jusqu'ici -- premiere sonde. Cap 20, dump prog[0xc1fa..+0x60]
* au 1er hit pour desassembler offline sans dependre d'un futur pass statique. */
{
static int _c1fa = -1;
if (_c1fa < 0) { const char *_e = 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));
}
}
}
/* [2026-07-23] CLUSTER-B-PROBE (READ-ONLY) : le workflow xref-scan a trouve un
* chemin dans PROM0 NON pollue par le bootstrap GPRS (Cluster A/0x87ff) qui mene
* vers 0x8f7f/0x8f9d (dans le range correlateur !) via un dispatcher per-item
* 0x86d4-0x871c. Cap 20/site, verifie si ce chemin est jamais atteint nativement. */
{
static int _clb = -1;
if (_clb < 0) { const char *_e = 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 (2026-05-28) ===
* SP-CATASTROPHE trace montre PC=0x7120 (boot init via notre override
* 0xFF80) re-firing à insn=190M. C'est un soft-reset interne firmware.
* Pour pinpointer le déclencheur : log toute arrivée à PC=0xFF80 ou
* PC=0x7120 APRÈS insn > 100k (= silicon reset initial déjà passé).
* Trail pc_ring[-16..-1] + SP/AR/IMR/IFR/INTM → on voit l'instr qui
* a sauté ici. */
if ((s->pc == 0xFF80 || s->pc == 0x7120) && s->insn_count > 100000) {
/* Deeper trail probe — gated par CALYPSO_DEBUG=SOFT_RESET_TRAIL.
* pc[-64..-1] permet de remonter ~64 instructions avant la
* réception du soft-reset pour identifier le 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-17)&255], pc_ring[(pc_ring_idx-16)&255],
pc_ring[(pc_ring_idx-15)&255], pc_ring[(pc_ring_idx-14)&255],
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-9)&255], pc_ring[(pc_ring_idx-8)&255],
pc_ring[(pc_ring_idx-7)&255], 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]);
srt_log++;
}
}
/* === PROM3-VISIT probe (2026-05-28) ===
* Compte les visites du DSP aux entries SB-decode candidats :
* 0x8167, 0x81ff, 0x82b8 (PROM3 dispatch SB candidates per session).
* Log à la première visite uniquement (insn_count + caller via ring),
* puis compteur silencieux. Si à la fin du run task=6 a fire 30×
* mais aucune visite → bug dispatch (item 5). Si visites OK mais
* sb_att=0 → bug demod plus profond. */
{
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);
}
}
/* === TOP-OF-LOOP SP CHOKEPOINT (fix 2026-05-24 v6 Claude web) ===
* Le hook SP existant est en BAS de boucle (L7471). Toute
* instruction qui sort tôt (goto unimpl, return, continue, handler
* qui sort de la dispatch chain) bypasse le hook → l'écriture SP
* a lieu mais n'est pas comptabilisée. Hier l'audit "tout passe
* par s->sp" était correct sur les SITES d'écriture mais ne
* vérifiait pas si le hook tourne pour ces instructions.
*
* Symptôme : 61 events captés vs descente attendue de 11k+ mots
* → la descente passe par bypass(es). Fix : observer s->sp à un
* CHOKEPOINT obligé (top de boucle), comparer avec la valeur de
* l'itération précédente. Bypass-proof par construction : on
* regarde la VALEUR à un point de passage, pas le SITE.
*
* Implementation : statics (persistent inter-c54x_run-calls). */
{
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) {
/* Compte l'instruction PRÉCÉDENTE qui a changé SP, quelle
* que soit sa voie de sortie (early-exit, return, etc.) */
sp_hist_account(topgate_last_pc, topgate_last_op,
topgate_last_sp, s->sp, s->insn_count);
}
/* Patch 3 rev 2 : bootstub-entry trigger (le bon signal post
* rev 1). Détecte l'edge prev_pc ∉ bootstub → cur_pc ∈ bootstub
* = le RET corrompu qui a sauté à 0x00XX. Capture verbose +
* dump ring contenant ~4096 iters d'approche. */
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 (2026-05-25 v3, Claude web review).
* Track A's last writer + dump at trigger PC. Resout fork
* NMI-vs-A-divergence avant impl invasive. */
a_track_init_lazy();
if (topgate_valid) {
a_track_iter(s, topgate_last_pc, topgate_last_op);
}
/* AR6 windowed snapshot (2026-05-25 v4) — disambigue AR6=0
* (base divergence) vs AR6=0x16 (self-alias feedback) au PC
* trigger. Env CALYPSO_AR6_AT_PC=0x821a + window. */
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 + dump-if-boot-phase-ended. */
mvpd_trace_init_lazy();
mvpd_trace_dump_if_due(s->insn_count);
/* Correlator entry trace : detect edge prev_pc ∉ [0x8d00..0x8f80]
* → cur_pc ∈ same range. Log full state (AR3/4/5 = buffer pointers
* probables) au moment de l'entrée. Dump des reads accumulés
* périodiquement (toutes 20 entrées) pour observer si pattern
* se stabilise vs varie entre runs. */
corr_trace_init_lazy();
if (g_corr_trace_enabled > 0 && topgate_valid) {
/* [2026-07-23] FIX : range obsolete 0x8f80 remplace par CORR_PC_HI
* (0x9000) -- ce duplicate ratait silencieusement les cibles Cluster B
* (0x8f9d/0x8fb8) trouvees par le workflow xref-scan. */
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 tous les 20 entrées pour observer si addr lues
* stabilisent (correlator répète) ou varient. */
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);
}
}
}
}
/* 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;
s->ifr &= ~(1 << imr_bit);
s->sp--;
data_write(s, s->sp, s->pc);
/* IT C54x = transition far : save XPC inconditionnel (APTS
* == AVIS, zéro sémantique pile) + force page 0 pour le fetch
* du vecteur (sinon vecteur lu via XPC vivant = bug racine). */
s->sp--;
data_write(s, s->sp, s->xpc);
s->st1 |= ST1_INTM;
/* corrélation IRQ (revival dsp 2026-06-23) : ce site de replay
* in-loop posait g_last_intr_* nulle part → les sondes
* HIGHVEC/DISP rataient les IT rejouées. On les pose ICI aussi,
* fg_pc capturé AVANT que s->pc soit écrasé par le vecteur. */
g_last_intr_insn = s->insn_count; g_last_intr_vec = vec;
g_last_intr_fg_pc = (uint16_t)s->pc; g_last_intr_fg_dp = dp(s);
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++;
/* Push counter at PC=0xb906 (and other suspected push sites).
* Logs at powers of 10 to track cadence. SP captured at 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);
/* [2026-07-23] TINT0 tick SYNC transitions INTM (intuition user) : a chaque
* RSBX INTM (1->0, re-enable), le go-live/handler attend le prochain TINT0.
* On rend TINT0 (vec20/bit4) pending -> pris immediatement quand INTM=0.
* Gate CALYPSO_TINT0_MASTER. C'est la vraie cadence (par slot, pas par frame). */
{
static int _t0i = -1;
if (_t0i < 0) _t0i = getenv("CALYPSO_TINT0_MASTER") ? 1 : 0;
static unsigned _t0period = 0;
if (_t0period == 0) { const char *_p = getenv("CALYPSO_TINT0_PERIOD"); _t0period = _p ? (unsigned)atoi(_p) : 1500; if (_t0period < 1) _t0period = 1500; }
static unsigned _t0last = 0;
/* [2026-07-23] THROTTLE : firer TINT0 a INTM 1->0 (prise propre) mais
* max 1x par _t0period insns (~cadence frame TDMA), sinon flood overlay
* a chaque micro-RSBX (63k/run) -> 200x lent. Sync transition + cadence. */
/* [2026-07-23] TINT0 CEDE A BRINT0 : vec20(bit4) < vec21(bit5) en priorite
* -> si on fire TINT0 quand BRINT0 est pending, TINT0 gagne toujours la
* fenetre INTM=0 et AFFAME BRINT0 (livraison I/Q). On ne fire/arme TINT0
* QUE si BRINT0 (IFR bit5) n'est PAS pending -> BRINT0 sert l'I/Q d'abord.
* Sur vrai HW TINT0=cadence frame (rare), s'interleave avec BRINT0/burst. */
/* [2026-07-23] FORCING RETIRE (hacky, cassait BRINT0). TINT0 vient
* maintenant du timer0 fidele (bloc TIMER0 tick) qui respecte l'IMR. */
(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)],
s->prog[(uint16_t)(s->pc + 2)],
s->prog[(uint16_t)(s->pc + 3)],
s->prog[(uint16_t)(s->pc + 4)],
s->prog[(uint16_t)(s->pc + 5)],
s->prog[(uint16_t)(s->pc + 6)],
s->prog[(uint16_t)(s->pc + 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 — firmware uses POPM ST1 + RCD pattern. The
* function itself is a no-op when probe disabled or no cycle
* active, so 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) : attribue chaque
* décrément net de SP à l'instruction qui vient de s'exécuter
* (last_exec_pc/op — capturés en fin de boucle précédente).
* Ces blocs tournent AVANT exec_one de l'itération courante, donc
* s->sp reflète le résultat de l'insn précédente = last_exec_pc.
* Isole l'instruction non-appariée qui draine SP dans le trampoline
* boot 0x0000↔0xffcd. Histogramme 8-slots + log des 120 premiers
* events. Silent par défaut. */
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[0], sd_cnt[0], sd_pc[1], sd_cnt[1],
sd_pc[2], sd_cnt[2], sd_pc[3], sd_cnt[3],
sd_pc[4], sd_cnt[4], sd_pc[5], sd_cnt[5],
sd_pc[6], sd_cnt[6], sd_pc[7], sd_cnt[7],
sd_events, s->sp);
}
}
}
sd_prev_sp = s->sp;
}
/* CALLSITE probe (CALYPSO_DEBUG=CALLSITE) : à l'épilogue RCD 0x7707,
* dump l'adresse de retour que RCD va popper + l'opcode du call-site
* (FCALL F9xx vs CALL F074) + pc-ring pré-RETD = park-vs-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) : toute transition de XPC avec
* l'instruction qui l'a causée (= origine du 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) : discrimine reset vs runaway.
* delta==-1 = post-décrément normal (progression, log tous les 200).
* delta!=-1 = reset/jump/load = LE discriminateur (#1 reset existe
* vs #2 jamais de reset). Reporte BK + la cible du reset. */
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;
}
/* TRACE: dump entry into 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);
/* Dump 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: dump 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: count visits per PC, dump top 20 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 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[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], top_pc[5], top_cnt[5],
top_pc[6], top_cnt[6], top_pc[7], top_cnt[7], top_pc[8], top_cnt[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[10], top_cnt[10], top_pc[11], top_cnt[11], top_pc[12], top_cnt[12],
top_pc[13], top_cnt[13], top_pc[14], top_cnt[14], top_pc[15], top_cnt[15],
top_pc[16], top_cnt[16], top_pc[17], top_cnt[17], top_pc[18], top_cnt[18],
top_pc[19], top_cnt[19]);
memset(pc_hist, 0, sizeof(pc_hist));
}
}
/* === Rolling PC sampler (v6 — find the REAL stuck zone) ===
* The cumulative-since-boot PC HIST shows 0xa218..0xa222 dominant
* because the init loop at 0xa222 (BANZD AR5, 60k iters) ran once
* early. After that, the DSP moved on but the cumulative histogram
* still shows those PCs at the top.
*
* BANZD-A222 traces (2026-05-08) confirmed AR5 was the actual loop
* counter (61523→61499 in 25 iter), not AR1. Loop finishes in
* ~984k insns (= 0.06% of a 1.7B run). Whatever IS currently
* burning DSP cycles is in a different zone, invisible to the
* cumulative top-N.
*
* Solution : rolling histogram per 100k-insn window. Resets each
* window so we always see "what is the DSP doing RIGHT NOW".
* Logs top-5 PCs of the most recent window. */
{
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 (Q-BRC investigation 2026-05-08 v5+v6) ===
* v5 hypothesis (BRC≈30770) was REFUTED by first 20 events :
* BRC=0 systematic, AR1=0 systematic, AR2 increments by 2,
* 16 insns between visits.
* v6 expands to capture the late-run behaviour : the cap=20 saturated
* at insn=48M while the run reached 2.4B. We now have :
* (a) cap=200 for early events
* (b) periodic sampler at 100k-visits intervals (late-run)
* (c) BANZD-A222 probe to capture the actual AR used by the
* branch-back instruction at 0xa222 op=0x6e81.
* 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 (v6) ===
* 0xa222 op=0x6e81 + opnd 0x8208 = `BANZD pmad, *Sind`.
* The *Sind operand decodes some AR but my v5 guess (AR1) was
* unverified — capture all ARs so we see which one is non-zero
* and how it evolves. If AR1=0 systematically, the branch test
* uses a different AR. Cap=200, plus periodic 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 + 0xa216 opnd 0x0092 = `ADD/SUB Smem,16,dst` per
* tic54x (2-word, mask FE00 base 0x4400). Logs A_pre / A_post and
* the Smem read so we can trace what value lands in dst (may feed
* BRC eventually). 30-event cap. */
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 (Q1 hypothesis test) ===
* Per Claude web v3 diag (2026-05-08) : routine 0xa0e0..0xa0e9 ends
* at PC=0xa0e7 op=0xc8be where AR4 is consistently 0x18 (=MMR_SP)
* pre-instruction → ST||LD writes to SP, catastrophe.
*
* Static dump shows two `XC 1, cond` instructions before 0xc8be :
* 0xa0e0 = 0xfd30 ; XC 1, cond=0x30 (TC)
* 0xa0e4 = 0xfd43 ; XC 1, cond=0x43 (ALT, A<0)
*
* Hypothesis : if XC condition evaluates to FALSE (TC bit not set, or
* A not negative), the conditional STM #lk, AR4 (likely at 0xa0e5) is
* SKIPPED → AR4 keeps stale value of 0x18 from earlier code path.
*
* Log every visit with : cond byte, TC/A/B flag values, AR4 value,
* and the next opcode (which would be skipped or executed). If the
* "taken" decision is consistently false at one of these XCs, that's
* the bug. Cap to 100 events 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)];
/* Mirror the condition decode from c54x_exec_one (case 0xF
* XC handler around line 1108+) — only the common subset. */
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 — FB-det inner correlator ===
* The DSP loops indefinitely in 0x8d2d..0x8d36. Static dump shows :
* 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 HIST counts (105k outer / 526k inner = 5×) confirm the 5-iter
* RPTB body is (0x8d32, 0x8d33, 0x8d34) repeated 5 times.
*
* Capture A_pre, T, AR2..AR5 at each PC inside this zone. Rate-limit :
* first 50 always (init + early convergence)
* every 5000th (steady-state cadence)
* when |A_after - last_logged_A| > 0x100000 (significant accumulator
* shift = convergence event worth dumping)
* Plus a dedicated "ENTER 0x8d2d" outer-iter counter that always logs
* A_pre at the OUTER entry, so we can tell whether the accumulator
* is reset between FB-det attempts (Observation 1 from session diag). */
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;
}
}
/* Dedicated outer-entry tracer at PC=0x8d2d : ALWAYS log A_pre on
* entry (cap to 200 events). If A is non-zero on outer entry,
* the accumulator wasn't reset between attempts — observation 1
* from 2026-05-08 session : 21× 0x2fb0 SNR could mean stuck
* accumulator 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 + 0xe981..0xe983 ===
* Diag v2 2026-05-08 : DSP locked in deterministic 7-instruction
* loop at 0xe9ac..0xe9b7 (PROM1 mirror), with outer 3-PC loop
* 0xe981..0xe983 reloading a BRC counter — pattern consistent
* with `RPTB end_addr` + outer reset. We need the actual opcodes
* to confirm/refute the RPTB hypothesis. One-shot dump on first
* entry into the body range, with surrounding context (a few
* words before for the RPTB instruction itself, and the outer). */
{
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]);
}
}
/* Track SP changes inside RPTB loops */
uint16_t sp_before = s->sp;
/* === Plan B captures (c web review) : snapshot for transfer ring,
* A-write ring, NOP-region guard. */
uint16_t pre_pc = s->pc;
uint8_t pre_xpc = s->xpc & 0x3;
uint16_t pre_op = prog_fetch(s, s->pc);
int64_t pre_a = s->a;
/* Trace EB04 loop — dump 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]);
/* Dump code around current PC (using prog_fetch for correct 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 functions contain PORTR PA=0xF430
* (read BSP sample). If DSP never visits them, the FB-det chain is
* dead. Targets identified by static analysis of 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);
}
}
/* Trace any write touching the dispatcher poll addresses
* data[0x4359] / data[0x3fab]. We never see them go non-zero;
* confirm whether ANY code path writes them. */
/* (handled in data_write — see below) */
/* Dispatcher hot loop trace at PROM0 0xb968-0xb9a4 — the state
* machine the DSP spins in when waiting for ARM tasks. Logs the
* first 8 visits per PC so we see the full conditional structure
* (which addresses it polls, which constants it compares to). */
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 vec area trace: log every PC visit in 0xFFCC-0xFFE0
* (INT3 + TINT0 + BRINT0 vec slots). Captures the 3 actual
* 4-word handlers our IRQ INT3 dispatch lands on at IPTR=0x1ff.
* 80 unique PCs max, log first 4 visits each. */
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]);
}
}
/* Trace DSP init - log once per unique PC in E900-E960 */
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);
}
}
/* Trace 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 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));
}
/* RPTB check moved below — must run AFTER `s->pc += consumed` so
* that when the body's last instruction has executed and PC has
* advanced to REA+1, the redirect to RSA is the FINAL operation
* on PC for this iteration. The previous placement (before PC
* advance) caused a 1-instruction off-by-one : redirect set
* pc=RSA, then `s->pc += consumed` bumped it to RSA+1, so the
* first body instruction was never re-executed across iterations
* (PC HIST showed body=[RSA+1..REA+1] instead of [RSA..REA]). */
/* Trace the IMR loop: how does the DSP reach 0x03F0? */
/* Trace 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-20)&255], pc_ring[(pc_ring_idx-19)&255],
pc_ring[(pc_ring_idx-18)&255], pc_ring[(pc_ring_idx-17)&255],
pc_ring[(pc_ring_idx-16)&255], pc_ring[(pc_ring_idx-15)&255],
pc_ring[(pc_ring_idx-14)&255], 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-9)&255],
pc_ring[(pc_ring_idx-8)&255], pc_ring[(pc_ring_idx-7)&255],
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]);
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--;
}
/* Execute instruction */
int consumed;
uint16_t exec_pc = s->pc;
/* [2026-07-22] TERMINAL-DISP : au tremplin 0xb40e (LD *AR7,A) / 0xb40f (BACC A),
* quel slot est lu ? AR7 = l index de dispatch. data[AR7] = handler choisi
* (0xab38 idle = storm). data[0x43c0] = le pointeur go-live (0xa4c7) VOISIN.
* Montre si le terminal lit le mauvais slot (0x4387 idle au lieu de 0x43c0). */
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);
}
/* [2026-07-22] FIX mask-ROM launch-vector (racine du storm, verifiee) :
* le scheduler boot fait `BACC A(=0xab38 idle=RET)` @0xb40f ; le RET depile
* mem[0x5ac8] = le VECTEUR DE LANCEMENT a la base de pile. Sur vrai HW ce mot
* est pre-charge (mask-ROM absent du dump) ; en QEMU il vaut 0 -> RET->PC=0
* -> storm. La bonne valeur = le pointeur go-live que le FIRMWARE LUI-MEME
* a ecrit a data[0x43c0] (=0xa4c7 = `ORM #0x3000,IMR` = arm IMR). On la
* derive (pas de constante magique) : mem[0x5ac8]=data[0x43c0] quand vide.
* => RET idle saute a l'arm IMR -> storm mort ET IMR arme. Ni seed 0x71f4
* (qui routait vers 0xa4df en SAUTANT l'arm IMR), ni poke arbitraire.
* Gate CALYPSO_MASKROM_GOLIVE_OFF=1 pour reproduire le storm brut (A/B). */
/* [2026-07-23] OVLY-TRACE : le handler frame 0x013b..0x0160 (overlay DARAM)
* derail au RET 0x0157 (pile vide). Trace pc/op/sp du 1er passage + dump du
* contenu overlay pour decoder ou est le desequilibre (PSHM non d-POPM /
* branche prise a tort avant les POPM). One-shot (1 frame). */
if (exec_pc >= 0x0100 && exec_pc <= 0x0160) {
static unsigned ot = 0; static int dumped = 0;
if (!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");
}
/* [2026-07-23] TEST gated CALYPSO_TEST_3FCD : le RET@0x0157 saute a
* data[0x3fcd]=0 (jamais ecrit). Le firmware installe un handler a
* data[0x3fce]=0xdf82 (voisin +1). Test : au PSHD (0x0154), si
* data[0x3fcd]==0, le derive de data[0x3fce] -> RET saute au handler.
* Prouve/refute que 0xdf82 est la bonne cible (table vecteurs decalee). */
if (exec_pc == 0x0154) {
/* @BEQUILLE — TEST_3FCD (CALYPSO_TEST_3FCD, EXISTS, defaut OFF)
* masque : data[0x3fcd] (adresse depilee par le RET @0x0157) n'est jamais
* ecrite ; le firmware installe un handler au voisin data[0x3fce].
* On derive l'un de l'autre.
* retirer : des que la table de vecteurs overlay est installee au bon offset
* (data[0x3fcd] non nul sans forcage) — ou immediatement si FIX_3FCD
* (meme cellule, PC 0x013b) est retenu comme mecanisme unique.
*/
static int t3 = -1;
if (t3 < 0) t3 = getenv("CALYPSO_TEST_3FCD") ? 1 : 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 (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);
}
/* [2026-07-23] FIX-DPAGE (test, gate CALYPSO_FIX_DPAGE defaut ON) : la ROM
* lit d_dsp_page @0x08d4 (0xa51c SM go-live + 0xc8ea dispatcher trame) mais
* l ARM/shunt postent la tache a 0x08E2 (offset +0x0E systematique, cf
* dsp_shunt.c:392). data[0x08d4]=0xf600 garbage -> la tache 0x0003 (@0x08E2)
* jamais vue -> corr jamais dispatche. On aligne : miroir 0x08E2->0x08d4
* avant le read ROM (pas de patch firmware). Prouve/refute que l offset EST
* le mur : si le DSP dispatche le corr apres -> confirme. */
if (exec_pc == 0xa51c || exec_pc == 0xc8ea) {
/* @BEQUILLE — FIX_DPAGE_OFF (CALYPSO_FIX_DPAGE_OFF, EXISTS-INV : la bequille est
* ACTIVE PAR DEFAUT, seule la presence de la variable la coupe)
* masque : le desaccord d'adresse d_dsp_page — la ROM lit 0x08d4, l'ARM/shunt
* postent la tache a 0x08E2 (+0x0E). La branche reelle = poster la
* tache a l'adresse que la ROM lit reellement.
* retirer : des que le producteur (calypso_dsp_shunt.c / calypso_arm2dsp.c)
* ecrit d_dsp_page a 0x08d4, ou des que l'offset +0x0E est corrige a
* la source.
* NB : le commentaire amont parle de CALYPSO_FIX_DPAGE — nom inexistant.
*/
static int fd = -1;
if (fd < 0) fd = getenv("CALYPSO_FIX_DPAGE_OFF") ? 0 : 1;
if (fd && s->data[0x08d4] != s->data[0x08E2]) {
static unsigned fm = 0;
if (fm++ < 8)
fprintf(stderr, "[c54x] FIX-DPAGE @0x%04x data[0x08d4] 0x%04x -> 0x%04x (=data[0x08E2]) insn=%u\n",
exec_pc, s->data[0x08d4], s->data[0x08E2], s->insn_count);
s->data[0x08d4] = s->data[0x08E2];
}
}
/* [2026-07-23] TEST INIT-435B (gate CALYPSO_INIT_435B defaut ON) : data[0x435b]
* = shadow IMR (les handlers tache OR/AND-ent des bits dedans : corr 0xbd3c
* ORM 0x10, etc.), et la SM go-live 0xa501/0xa582 le propage dans IMR. Il n est
* JAMAIS initialise en QEMU (STATE435B-WR vide) -> IMR=0 -> deadlock. On l amorce
* au masque IMR reset 0x52fd (comme le boot DSP reel devrait) une fois. Si ca
* arme IMR=0x52fd -> frame IT prise -> corr tourne -> self-sustain -> PROUVE. */
if (exec_pc == 0xa4e4) {
/* @BEQUILLE — INIT_435B (+ SEED_52FD) (CALYPSO_INIT_435B_OFF=0 => ACTIVE ;
* CALYPSO_SEED_52FD choisit la valeur ; les 4 profils .env posent 0)
* masque : l'initialisation du shadow IMR data[0x435b] par le boot DSP. Jamais
* ecrit en QEMU -> la SM 0xa582 propage IMR=0 -> deadlock. On injecte
* 0x52ed (ou 0x52fd avec SEED_52FD) a exec_pc==0xa4e4.
* retirer : quand une ecriture firmware sur 0x435b est observee avant 0xa4e4.
* PIEGE : le nom dit _OFF mais "=0" ACTIVE.
*/
static int i435 = -1;
if (i435 < 0) { const char *_e435 = getenv("CALYPSO_INIT_435B_OFF"); i435 = (_e435 && atoi(_e435)) ? 0 : 1; } /* [2026-07-23] fix gate: teste VALEUR (OFF=0 => actif) */
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] = getenv("CALYPSO_SEED_52FD") ? 0x52fd : 0x52ed; /* [2026-07-23] defaut 0x52ed (SANS bit4/TINT -> evite le clobber firmware 0xa509 qui strippe bit12/frame). 0x52fd=bit4 opt-in (casse le frame, prouve : firmware n utilise PAS TINT0) */
}
}
/* [2026-07-23] SM-TRACE : chemin complet de la SM go-live 0xa4e4-0xa5b5
* avec d_dsp_page aligne -> ou branche/reboucle-t-elle ? flags decisifs :
* d_dsp_page(0x3fb0), data[0x09bc](flag ARM), A(target dispatch). */
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);
}
/* [2026-07-23] TERM-TRACE : calcul d'index AR7 au terminal mask-ROM 0xb405-0xb412.
* Question : AR7 devient 0x4387 (idle) au lieu de 0x43c0 (go-live) ? le calcul
* LD#0x39 (0xb408) + ADD#0x4387 (0xb409) -> A=0x43c0 est-il perdu / mal-range dans AR7 ?
* Logge A + AR0-7 a CHAQUE insn de la zone. Gate CALYPSO_TERM_TRACE_OFF. */
{
static int _tt = -1;
if (_tt < 0) _tt = 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);
}
}
/* [2026-07-23] CALA-TRACE-WIDE : ELARGI (recommande workflow xref-scan) sur
* TOUTE la plage 0xa575-0xc300 (au lieu de fragments) et TOUS les transferts
* calcules (CALA/CALAD/BACC/FBACC/FCALA/FCALAD -- f4e2/f4e3/f4e6/f4e7/f5e2/f5e3/
* f5e6/f5e7/f6e6/f6e7), pas seulement CALA. Strategie empirique : capter N'IMPORTE
* QUEL saut calcule qui atterrit dans le range correlateur (0x8d00-0x9000), plutot
* que continuer le tracage statique exhaustif (3 workflows n'ont pas trouve la
* reference statique). Deux compteurs separes : hits "dans le range" (JAMAIS
* cappes, signal fort) et hits generaux (cap 200, pour contexte/pattern). */
{
static int _ctw = -1;
if (_ctw < 0) { const char *_e = getenv("CALYPSO_D247_TRACE_OFF"); _ctw = (_e && atoi(_e)) ? 0 : 1; }
if (_ctw && exec_pc >= 0x7000 && exec_pc <= 0xdfff) { /* [2026-07-23] ELARGI a tout PROM0 (plus de limite arbitraire) */
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);
}
}
}
}
/* [2026-07-23] INSTALL-TRACE : le bloc 0xc7xx installe la table de handlers de tache
* (STL A -> d[4c5c] a 0xc803). d[4c5c]=0 -> corr FB jamais dispatche. Ce bloc est-il
* atteint, et A vaut quoi a 0xc803 ? Litteraux voisins (d[4c5a]/d[4c5d]) = bloc atteint ?
* Gate CALYPSO_INSTALL_TRACE_OFF. */
{
static int _it = -1;
if (_it < 0) _it = 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);
}
}
/* [2026-07-23] BACC-C827-SRC : d'OU vient le saut vers 0xc827 (qui skippe l'install
* de la table de handlers 0xc7a0-0xc825) ? Traque prev_pc + op + A + AR quand on entre
* a 0xc827 sans fall-through (prev != 0xc825/0xc826). Gate CALYPSO_BACC_C827_OFF. */
{
static uint16_t _pp827 = 0;
static int _bsc = -1;
if (_bsc < 0) _bsc = 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;
}
/* [2026-07-23] PHASE-SM : la state-machine d[3f70] (phase go-live->operationnel).
* 0xddeb LD d[0x098a];BC si A==0 -> reset phase=0. 0xde86 LD d[0x098c]. 0xde9c ST#2.
* d[0x098a]/d[0x098c] = handshake ARM (l'ARM DOIT les poser !=0 pour avancer -> d[3f70]=2).
* Logge les points de decision + valeurs. Gate CALYPSO_PHASE_SM_OFF. */
{
static int _ps = -1;
if (_ps < 0) _ps = 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);
}
}
if (exec_pc == 0xa51c) { /* SM go-live lit d_dsp_page @0x08d4 (faux ?) */
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) { /* [2026-07-23] BC AEQ (0xf845) : pourquoi A==0 -> skip RSBX INTM (0xa4d0) ?
* 0xaad5 lit AR0=data[0x434e], AR1=data[0x434f] (ptrs) et calcule 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);
}
if (exec_pc == 0xb40f) {
/* [2026-07-23] DÉFAUT OFF : le storm est tué NATIVEMENT par le fix ISA LD #k8u
* (calypso_c54x.c:7702, le <<16 mettait AR7=0x4387 idle au lieu de 0x43c0 go-live).
* Ce hack (mem[0x5ac8]=data[0x43c0]) ne faisait que masquer ce bug -> plus nécessaire.
* Opt-in CALYPSO_MASKROM_GOLIVE=1 pour le réactiver (A/B). */
/* @BEQUILLE — MASKROM_GOLIVE (CALYPSO_MASKROM_GOLIVE, EXISTS, defaut OFF)
* masque : le vecteur de lancement mem[0x5ac8] a la base de pile, pre-charge
* par un mask-ROM absent du dump ; a 0, le RET du BACC idle saute a
* PC=0 (storm).
* retirer : DEJA INUTILE selon le commentaire ci-dessus — le fix ISA LD #k8u
* tue le storm nativement. A supprimer au prochain passage, ce n'est
* plus qu'une garde A/B.
* NB : le commentaire amont annonce CALYPSO_MASKROM_GOLIVE_OFF — inexistant.
*/
static int mrg = -1;
if (mrg < 0) mrg = getenv("CALYPSO_MASKROM_GOLIVE") ? 1 : 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);
/* === FBWATCH-ALIVE (canary) : PROUVE que la sonde est armée + sample PC
* foreground. Si CE log sort, g_fbwatch_on=1 et le silence des autres
* FBWATCH est RÉEL. S'il ne sort PAS, les probes étaient mortes. Fire
* garanti tous les ~20M insns (≈10 lignes sur le 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 : le foreground polle un flag via BITF @0xf7af/0xf7b7
* (RC NTC = boucle tant que TC=0). Capture l'adresse du flag (AR0..AR2 +
* data) + TC pour ID le bit jamais posé = ce qu'il faut câbler (modèle HW). */
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 (2) : le handler FB 0x9ac0 tourne-t-il ? (env one-shot) === */
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 : la routine d'init de la table de dispatch
* (0xc704, peuple data[0x4c24-0x4c5d] = cibles BACC-A/CALA) tourne-t-elle ?
* 0 hit = jamais atteinte = root confirmé (boot saute le setup-pass). */
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 (4) PRODUCTEUR/CONSOMMATEUR : le dispatch CALAD @0x833b
* tourne-t-il par-frame, et quelle adresse handler calcule-t-il dans A ?
* 0 ligne = dispatcher mort (producteur). A jamais 0x9ac0 = la jump-table/
* formule ne produit jamais le handler FB. A=0x9ac0 = FB dispatché mais
* ne détecte pas (bug handler). Cap haut pour voir la 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);
}
/* CORR-ENTRY tracker (env CALYPSO_CORRELATOR_TRACE=1) : capture
* transition out→in du range FB-det [0x8d00..0x9000). Cf top of
* file pour la lazy-init + l'évidence runtime 2026-05-25 night. */
corr_entry_track(s->pc, s);
/* FBDB-PROBE (env CALYPSO_FBDB_PROBE=1, c web reframe 2026-05-25 night2) :
* trace B@fbd9, A@fbdb (= post F2xx SUB), A@fbf3 (= before STLM A,AR4). */
fbdb_probe_check_pc(s->pc, s);
/* FORCE-INTM-ONESHOT (env CALYPSO_FORCE_INTM_ONESHOT=1, c web reframe
* 2026-05-25 night4) : sonde arbitrage — clear INTM UNE FOIS quand
* INTM=1 + BRINT0 pending. Observe via tracers existants si aval sain. */
force_intm_oneshot_check(s);
/* STUCK-PROBE (env CALYPSO_STUCK_PROBE=1, c web reframe 2026-05-25 night3) :
* capture PC+XPC histogramme quand INTM=1 + BRINT0 pending. */
stuck_probe_check(s);
/* === CALA-70C3 FORENSIC PROBES (2026-05-27, c web review) ===
* Pourquoi : DSP boucle infiniment sur CALA A à PROM0[0x70c3] avec
* A=0x0001_70c3 (auto-référence). A_H=0x0001 ne peut PAS venir d'un
* `LD Smem,A` sext40-é (qui donne A_H ∈ {0x0000, 0xFFFF}), donc
* writer = DLD upstream ou compose H+L. Probes pour identifier :
* 1. Source du jump vers 0x70c3 (XPC:PC + opcode@prev_pc), gated
* FIRST-HIT pour échapper à la pollution post-runaway (MMR XPC
* écrasé quand SP rampage à travers data[0x18..0x1F]).
* 2. Compteur LD@0x70c1 — si 0, confirme le jump direct (skip LD).
* 3. Dernier writer de A (PC qui a posé 0x0001_70c3 dans A).
* Active par défaut, coût ~3 branches/insn. */
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;
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-9)&255], pc_ring[(pc_ring_idx-8)&255],
pc_ring[(pc_ring_idx-7)&255], 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]);
}
{
/* DISP-ENTRY : prédécesseur = PC exécuté à l'itération précédente */
static uint16_t s_last_run_pc = 0;
static uint16_t s_last_run_op = 0;
g_prev_pc = s_last_run_pc;
g_prev_op = s_last_run_op;
s_last_run_pc = s->pc;
s_last_run_op = exec_op;
}
/* SONDE GAP-1 AR3-TRIP (2026-06-23, approche structurée non-decode) :
* pince la PREMIERE instruction qui fait sauter AR3 d'un GRAND pas.
* A ce point s->ar[3] reflete le resultat de l'instruction qui vient
* de tourner = g_prev_pc/g_prev_op. Les post-incr legitimes valent +-1/2 ;
* un saut >= 0x800 = chargement/modif suspecte. Flag special si delta==SP
* (la signature "AR3 += SP" qu'on a identifiee comme cause du derail).
* Cout : 1 sub + 1 cmp / insn, log cape a 80. */
{
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;
}
/* SONDE GAP-1 AR0-TRACE (2026-06-23) : AR0 est l'INDEX du *AR3+0% a
* 0xb3d1 (AR3 += AR0). On a etabli qu'AR0 ~= 0x5AC7 (~SP) = corrompu.
* Logge CHAQUE changement d'AR0 dans la fenetre init (insn<3000) avec
* l'instruction qui l'a pose (g_prev_pc/op) -> nomme le corrupteur. */
{
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];
}
/* Dump one-shot du contexte complet au 1er passage a 0xb3d1
* (ADD *AR3+0%,A) : AR0/AR3/BK/DP/ST1/A + 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);
}
}
/* === SONDE DETECTOR-TRACE (Phase B, 2026-06-23) ===================
* Trace instruction-par-instruction du detecteur FB / correlateur dans
* [0xf074..0xf0c0]. La region 0xf070-0xf0b0 EST une table sigmoide en
* XPC=0 (dump ROM) ; l'execution reelle est dans une AUTRE banque XPC.
* exec_op = prog_fetch(s->pc) respecte la banque -> on voit le VRAI
* opcode. On logge PC + XPC (= quelle banque) + op + A/B/T (le math de
* correlation : sample*coeff accumule). Si A/B restent 0 -> le math ne
* produit rien (opcode mal emule / sample non lu). Si A/B montent mais
* d_fb_det reste 0 -> bug de seuil/decision (croiser avec
* CALYPSO_FBDET_SENTINEL=2 qui monitore les writes a 0x08f8).
* Env CALYPSO_DETTRACE=1, cap 800. */
{
static int dettr_on = -1;
if (dettr_on < 0) dettr_on = getenv("CALYPSO_DETTRACE") ? 1 : 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");
}
}
}
/* SONDE Phase B BACC-IN : au bacc dispatch (0xb40f) et au LD *AR7,A
* (0xb40e) qui le precede, capture A + AR7 + data[AR7] = la source du
* pointeur 0xf074. Dit si le slot lu (data[AR7]) vaut deja 0xf074
* (pointeur faux) ou si A est corrompu autrement. Cap 40. */
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);
}
}
/* SONDE Phase B DISP-ENTRY (trisection 2026-06-24) : trace one-shot
* l'entree du dispatcher [0xb400..0xb40f] -> comment AR7 obtient sa
* valeur (immediat litteral vs registre d'index/evenement gele), le
* reset SP delibere (0xb403), et la pile au bacc. AR3/AR4 inclus (test
* "AR7 derive d'un registre gele"). Cap 64. */
/* SONDE Phase B LOOPTRACE (2026-06-24, élargie depuis DISP-ENTRY) :
* la maladie n'est PAS l'idle bénin -> POST-BOOTSTUB-RET tourne 640M de
* fois (storm PC=0) dès le 1er dispatch (insn 4398). On veut la boucle
* principale ENTIÈRE [0xb3c0..0xb410] (= prologue lecture d_dsp_page @0xb3cc
* + dispatcher @0xb400) AVEC le caller (g_prev_pc) -> voir comment 0xb401
* est atteint la 1ère fois, si word[0]=0x2900 @0xb400 est exécuté ou sauté,
* et pourquoi data[SP] (seed de retour) est vide quand l'idle-RET dépile
* -> PC=0. AR1 inclus (la copie READA arme AR1=0x4387). Cap 300. */
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);
}
}
/* === EXPERIENCE SEED-5AC8 (2026-06-24) : le chainon go-live ===========
* MECANISME VERIFIE sur le vrai dump : la frame body seme le soft-vector
* data[0x3f6d]=0xa4df @0xb405 (continuation GO-LIVE). Le trampoline
* 0x71f4 = `LD *(0x3f6d),A ; BACC A` honore ce vecteur -> 0xa4df ->
* 0xa4ca/0xa500 -> 0xa51b RSBX INTM (1er enable IT du run) -> 0xa51c lit
* d_dsp_page. MAIS le terminal 0xb40f BACC data[0x4387]=0xab38=RET depile
* mem[0x5ac8]=0 -> PC=0 -> storm, au lieu d'atteindre 0x71f4.
* ATTENTION (2026-07-22) : LE SEED N'EST PAS UN FIX. C'est un band-aid GATE
* (OFF par defaut, CALYPSO_SEED5AC8=1 pour l'activer). Poker mem[0x5ac8]
* MASQUE le vrai bug : verifie sur ROM (PROM0.bin, LE) le boot @0xb405 fait
* `ST #0xa4df, data[0x3f6d]` -> le soft-vector go-live vaut 0xa4df, qui SAUTE
* l'arm IMR (0xa4c7 `ORM #0x3000,IMR`) ET l'enable (0xa4d0 `RSBX INTM`). Donc
* meme mem[0x5ac8]=0x71f4 "correct" n'arme jamais l'IMR. La vraie cause du
* storm ET du no-enable reste a trouver (pourquoi 0xa4c7/0xa4d0 jamais
* atteints ; qui doit peupler mem[0x5ac8]). NE PAS traiter le seed en fix. */
{
/* @BEQUILLE — SEED_5AC8 (+ SEED5AC8_VAL) (CALYPSO_SEED5AC8, atoi>0, defaut OFF ;
* _VAL defaut 0x71f4, calypso_wire.env:=0xa4c7)
* masque : le peuplement de mem[0x5ac8] (mot depile par le RET terminal 0xab38,
* qui choisit l'entree go-live). Personne ne l'ecrit dans notre modele.
* retirer : quand on sait QUI ecrit mem[0x5ac8] sur silicium — le commentaire du
* bloc dit deja "NE PAS traiter le seed en fix".
*/
static int seed_on = -1;
/* GATE : seed OFF par defaut (band-aid), ON seulement si CALYPSO_SEED5AC8=1. */
if (seed_on < 0) { const char *e = getenv("CALYPSO_SEED5AC8"); seed_on = (e && atoi(e) > 0) ? 1 : 0; }
/* [2026-07-22] CABLE sur le STM #0x5ac8,SP du DSP (PC=0xb382) : le
* seed est desormais SOURCE du stack-init REEL du DSP (op=0x7718),
* pas d'un poke arbitraire au BACC terminal. Timing sur : aucune
* ecriture ne touche 0x5ac8 entre 0xb382 et le RET (0xab38). */
if (seed_on && exec_pc == 0xb382) {
/* [2026-07-22] valeur du seed configurable : le RET terminal depile
* mem[0x5ac8] pour choisir l entree go-live. 0x71f4 (defaut) ->
* trampoline -> 0xa4df (SAUTE l enable RSBX INTM 0xa4d0). 0xa4c7 ->
* entree par l ORM IMR -> RSBX INTM 0xa4d0 = enable natif -> la frame
* IT (proprement livree bit12) est alors PRISE. CALYPSO_SEED5AC8_VAL. */
static int sval = -1;
if (sval < 0) { const char *e = 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;
}
}
/* GOLIVE-WATCH (ungated) : le firmware atteint-il enfin la routine go-live
* (0xa4c9..0xa520) ou le trampoline 0x71f4 ? logge PC/op/INTM/IMR +
* soft-vector data[0x3f6d]. Cap 120. */
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);
}
/* [2026-07-22] AR0-DELTA (gated CALYPSO_AR0_DEBUG, RO) : chaque changement
* d'AR0 dans la fenetre boot -> localise l'instruction qui corrompt AR0
* (attendu ~0x5ac8 pour ecrire le vecteur go-live mem[0x5ac8]=0x71f4). */
{
static int ad_en = -1;
static uint16_t ar0_prev = 0xFFFF;
if (ad_en < 0) ad_en = getenv("CALYPSO_AR0_DEBUG") ? 1 : 0;
if (ad_en && s->insn_count < 12000 && s->ar[0] != ar0_prev
&& exec_pc != 0xb387) { /* skip le fill-loop qui noie le 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];
}
}
/* [2026-07-22] PROG-DUMP-B3D0 (gated CALYPSO_AR0_DEBUG, RO, one-shot) :
* desassemble la region qui seed data[0x3f6d]=0xa4df (@0xb405) et le
* BACC terminal 0xb40f, pour trouver le setup companion de mem[0x5ac8]. */
if (getenv("CALYPSO_AR0_DEBUG") && 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)]);
/* et le RET 0xab38 (cible du 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 (transfert -> 0xb3a3 sans push = LE bug) +
* trampoline 0x71f4. Doit etre un CALL empilant 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)]);
/* debut de routine cote 0xb3a3 (cible du transfert) */
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]);
/* LE 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]);
/* resultat du fill en memoire : constante + ou il s'arrete */
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]);
/* [2026-07-22] routine go-live 0xa4c0-0xa4e4 : ORM 0xa4c7, test
* wait-loop 0xa4d4 (cellules 0x098a/0x098c), pour porter vers ARM. */
/* store loop des vecteurs (0xb4c8-0xb4e0) + sa source (AR-setup) */
/* routine 0xa9ea (CALL @0xb3f3, push retour 0xb3f5) : ou est
* son over-pop (PSHM/POPM desequilibre) qui derive SP -> 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 post-0xa582 : installe-t-il vec28 (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 (2026-06-24, RO) : dynamique de l'automate qui garde le
* scheduler 0xa51c (qui ne tourne jamais, data[0x3fb0]=0). Trois faits :
* (1) IMR-DYN : le DSP execute-t-il ses sites d'armement IMR, et qui efface ?
* (2) WAIT-TEST : le flag 0x3f70 bit1 (sortie wait-loop) est-il jamais set au test ?
* (3) DE97-BR : le super-loop atteint-il la branche set-bit1 (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);
}
/* [2026-07-22] KEEP-IMR (gated CALYPSO_KEEP_IMR) : 0xb37e (STM #0,IMR)
* efface IMR ~47 insns apres que le go-live l'a arme -> la wait-loop
* tourne avec IMR sans les bits d'IT -> l'IT jamais prise.
* [2026-07-25 FIX BIT5 — diag video user] : l'ancienne version re-armait
* IMR=0x3000 (bits 12/13, vec28/29) mais SANS bit5 (BRINT0/vec21). Or
* BRINT0 = l'IT "buffer BSP recu" qui reveille le correlateur. Resultat
* mesure : IMR=0x52fd (bit5=1) arme 178x par le go-live puis ECRASE par
* 0xb37e, KEEP_IMR restaurait 0x3050/0x0050 (bit5=0) -> BRINT0 masque a
* vie -> IFR bit5 pending eternel -> correlateur jamais dispatche.
* FIX : re-armer la VRAIE image = le shadow d[0x435b] (=0x52fd, bit5
* inclus), et le faire des que bit5 TOMBE (pas seulement quand imr==0),
* sur toute la region go-live+background [0xa4ca..0xdea0]. Valeur de
* repli / override : CALYPSO_KEEP_IMR_VAL (defaut 0x52fd). */
{
/* @BEQUILLE — KEEP_IMR (+ KEEP_IMR_VAL) (CALYPSO_KEEP_IMR, EXISTS, defaut 1 en
* hack/native/native_helped/wire ; valeur de repli 0x52fd)
* masque : le clobber de l'IMR par 0xb37e (STM #0,IMR) et 0xa509 (strip bit12).
* On re-ecrit s->imr = data[0x435b] des que bit5/BRINT0 tombe, sur
* toute la region [0xa4ca..0xdea0].
* retirer : quand le firmware ne perd plus bit5 — c'est-a-dire quand la sequence
* go-live 0xa4c7/0xa51b/0xa582 se deroule dans le bon ordre.
*/
static int ki = -1; static uint16_t kiv = 0;
if (ki < 0) { ki = getenv("CALYPSO_KEEP_IMR") ? 1 : 0;
const char *e = 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 sans bit5 -> repli */
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];
/* FORCE-GOLIVE (etape1, gated CALYPSO_FORCE_GOLIVE) : release the
* go-live wait-loop by setting data[0x3f70] bit1 at the 0xa4d4 test
* (normally gated on control cells 0x098a/0x098c which the ARM leaves 0). */
/* @BEQUILLE — FORCE_GOLIVE (CALYPSO_FORCE_GOLIVE, atoi>0, defaut OFF ; hack.env vide)
* masque : la wait-loop go-live teste data[0x3f70] bit1, pose seulement par le
* setter 0xde9c, lui-meme conditionne aux cellules 0x098a/0x098c que
* l'ARM laisse a 0.
* retirer : des que le handshake ARM (ARM2DSP_BGEN) fait franchir 0xddf5 et que
* le setter natif 0xde9c s'execute.
*/
{ static int fg = -1; if (fg < 0) { const char *e = 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); } }
if ((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;
}
}
/* [2026-07-25] RANK1 : route frame ISR 0x013b -> 0x8341 (LUT FB native,
* setup COMPLET du correlateur : BRC/BK/data-ptr, que l'entree forcee
* 0x8d00 court-circuite -> boucle morte MAC diag). Le frame IT vectorise
* 0x00f0 -> 0x7234 (B 0x013b) -> prologue 0x013b qui deraille et n'atteint
* jamais 0x8341. On redirige le prologue vers 0x8341 quand l'IT vient du
* frame scheduler (g_prev_pc==0x7234). Gate CALYPSO_ISR_TO_8341 (def off). */
if (exec_pc == 0x013b) {
/* @BEQUILLE — ISR_TO_8341 (CALYPSO_ISR_TO_8341, EXISTS, defaut OFF)
* masque : le prologue ISR overlay 0x013b deraille et n'atteint jamais la LUT
* FB 0x8341 (setup complet BRC/BK/data-ptr du correlateur). On force
* s->pc = 0x8341.
* retirer : des que le prologue 0x013b se termine sur 0x8341 par son propre flot
* (meme condition que FIX_3FCD reussi).
* NB : calypso_wire.env fait un unset EXPLICITE — un ":=vide" sous set -a
* rallumerait ce gate EXISTS. Ne jamais le convertir en ":=".
*/
static int r8 = -1;
if (r8 < 0) r8 = getenv("CALYPSO_ISR_TO_8341") ? 1 : 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;
}
}
/* [2026-07-25] CORR-SETUP (diag user "setup AR/BK a l'entree 0x8d00,
* meme patch avec les constantes") : le correlateur 0x8d00 est atteint
* (via BSP-DISPATCH-FB) mais SANS le setup que la LUT native 0x8341 pose
* juste avant : STM #0x2f22,AR1 / #0x2be4,AR4 / #0x0060,AR5 (desassemble
* PROM0 0x8347/0x8349/0x834b). Sans ces pointeurs le MAC boucle a
* 0x8e8b/0x8e8c sans conclure. On INJECTE ces constantes a l'entree
* 0x8d00. Gate CALYPSO_CORR_SETUP ; override _AR1/_AR4/_AR5. */
if (exec_pc == 0x8d00) {
/* @BEQUILLE — CORR_SETUP (+ CORR_AR1/_AR4/_AR5) (CALYPSO_CORR_SETUP, EXISTS,
* defaut OFF)
* masque : le setup de pointeurs que la LUT native 0x8341 pose avant d'entrer
* en 0x8d00 (STM #0x2f22,AR1 / #0x2be4,AR4 / #0x0060,AR5). On INJECTE
* ces constantes a l'entree du correlateur.
* retirer : quand le chemin natif passe par 0x8341 avant 0x8d00 (au lieu d'y
* entrer par BSP_DISPATCH_FB).
* NB : idiome EXISTS — un ":=" vide l'ALLUMERAIT, d'ou le unset explicite
* de calypso_wire.env. Mesure : inefficace (AR reecrits avant 0x8e8b).
*/
static int cs = -1; static uint16_t a1 = 0, a4 = 0, a5 = 0;
if (cs < 0) {
cs = getenv("CALYPSO_CORR_SETUP") ? 1 : 0;
const char *e;
a1 = (e = getenv("CALYPSO_CORR_AR1")) && *e ? (uint16_t)strtoul(e,0,0) : 0x2f22;
a4 = (e = getenv("CALYPSO_CORR_AR4")) && *e ? (uint16_t)strtoul(e,0,0) : 0x2be4;
a5 = (e = 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);
/* POKE-A4C7-ONCE (2026-07-03, gated CALYPSO_POKE_A4C7_ONCE, DIAGNOSTIC
* ONLY -- falsification test, not a fix, revert after use). Addendum 20 :
* the go-live wait-loop entry at 0xa4ca is ALWAYS reached directly,
* skipping 0xa4c7 (ORM #0x3000,IMR -- arms bit12/vec28) 3 words earlier,
* which is 0-hit all session. This redirects the FIRST arrival at 0xa4ca
* to 0xa4c7 instead -- the CPU then naturally executes the real ROM ORM
* instruction and falls through back into 0xa4ca normally. No register/
* memory value is poked directly -- only the entry PC, once, to let the
* ROM's OWN arming instruction run. Tests: does IMR arm (0x3000), does
* the frame IT then vector to vec28, does d_fb_det become nonzero, and
* do data[0x3f70]/data[0x435b] populate too (single-root-cause test). */
if (exec_pc == 0xa4ca) {
/* @BEQUILLE — POKE_A4C7_ONCE (CALYPSO_POKE_A4C7_ONCE, atoi>0, defaut OFF)
* masque : 0xa4c7 (ORM #0x3000,IMR = armement IMR par la ROM) n'est jamais
* atteint : le flot entre a 0xa4ca en sautant l'instruction d'armement.
* On detourne le PC une fois.
* retirer : des que le chemin amont (0xa4cd BC AEQ, ou le setter de d[434e]/
* d[434f]) laisse tomber dans 0xa4c7.
* NB : calypso_hack.env le qualifie lui-meme de "falsification, pas un fix".
*/
static int poke_en = -1;
if (poke_en < 0) { const char *e = 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 (2026-07-03, gated CALYPSO_CALA_71DA, RO) : le wrapper
* generique save/dispatch/restore a 0x71c0-0x71f2 (PSHM x19 ; ST0=0,
* ST1=0x6900 ; CALA @0x71da ; POPM x19 ; RET) dispatche vers l adresse
* dans A. Log A juste avant le CALA -- determine si ce dispatcher
* appelle jamais autre chose qu un stub no-op (meme famille de boucle
* fermee auto-referentielle que data[0x4387]->0xab38, addendum 15). */
if (exec_pc == 0x71da) {
static int cala_en = -1;
if (cala_en < 0) cala_en = getenv("CALYPSO_CALA_71DA") ? 1 : 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++;
}
}
/* FORCE-IMR (2026-07-02, gate CALYPSO_C54X_FORCE_IMR=<hex>) : le ROM efface
* IMR (STM #0,IMR @0xb37e insn~1047) et ne le re-arme jamais avant le
* scheduler b41c -> la frame IT (INT3, IFR bit3) reste masquee -> spin.
* On OR les bits demandes dans IMR a chaque pas HORS ISR (INTM=0 window de
* IRQ-LEVEL le sert). Test falsifiable de la chaine IMR->IT->0x0fff->golive.
* Defaut OFF. Typique : 0x52fd (bit3 INT3 + bit5 BRINT0 + ...). */
{
/* @BEQUILLE — C54X_FORCE_IMR (CALYPSO_C54X_FORCE_IMR=<hex>, defaut OFF)
* masque : le re-armement de l'IMR apres le STM #0,IMR du mask-ROM @0xb37e, et
* le RSBX INTM que le ROM ne joue qu'apres go-live. On OR les bits dans
* l'IMR a chaque pas hors ISR et on clear INTM dans [0xb380..0xb440].
* retirer : quand la SM go-live atteint 0xa582 et pose l'IMR elle-meme.
*/
static int fimr = -1; static uint16_t fimrv = 0;
if (fimr < 0) { const char *e = 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;
}
/* La boucle idle scheduler b380-b440 doit tourner INTM=0 (attente IT).
* Le ROM ne clear INTM (RSBX @0xa51b) qu apres go-live (chicken-egg) ->
* on clear INTM HORS ISR uniquement dans l idle loop, pour que IRQ-LEVEL
* serve la frame IT latchee. Ne touche pas les ISR (PC bas / 7234). */
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;
}
}
/* FORCE-098 (etape A faithful, gated CALYPSO_FORCE_098=<hexval>) : juste AVANT
* les lectures LD *(0x098c)/(0x098a) des setters (0xde86 chemin GO ; 0xde94 ;
* 0xb3e4), pose data[0x098a]/[0x098c] = valeur non nulle cote DSP. Teste si le
* DSP prend alors la branche GO (0xddf5) puis setter bit1 puis go-live. */
{
/* @BEQUILLE — FORCE_098 (CALYPSO_FORCE_098=<hexval>, defaut vide/OFF ; hack.env)
* masque : l'ARM ne pose jamais les cellules de handshake d_background
* 0x098a/0x098c que la phase-SM 0xddeb/0xde86 relit.
* retirer : des que CALYPSO_ARM2DSP_BGEN pose ces cellules par le pont ARM
* (causalite correcte) — calypso_hack.env declare deja le remplacement.
*/
static int f98 = -1; static uint16_t f98v = 0;
if (f98 < 0) { const char *e = 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);
}
}
/* GO-LIVE FB-task hold (2026-07-25) — INTEGRATION NATIVE, remplace l'ancien
* FORCE poke (=0xC000, overwrite, mauvais bit). Le firmware efface d[0x3f92]
* par ST #0 @0xa4c4 puis DEVRAIT le re-armer par ORM #0x0800 @0xa539 — mais
* ce setter natif est skippe (d[5a00]==0x88), donc le bit tache-FB (0x0800)
* reste 0 a vie et le scheduler DSP ne dispatche jamais le correlateur. On
* REJOUE ici exactement ce que ferait l'ORM 0xa539, mais SEULEMENT quand l'ARM
* a effectivement commande le go-live (d[0x0810] bit15, pose par le wire
* CTRLSYS) : causalite correcte ARM->DSP, bon bit (0x0800, PAS 0xC000), OR (pas
* d'overwrite des autres bits scheduler). Gate CALYPSO_GOLIVE_TASKW, defaut OFF.
* 0x0810 est deja gere par le wire CTRLSYS (arm2dsp) -> plus de FORCE_0810. */
if (exec_pc >= 0xa4ca && exec_pc <= 0xa575) {
/* @BEQUILLE — GOLIVE_TASKW (CALYPSO_GOLIVE_TASKW, EQ1, defaut OFF)
* masque : le setter natif ORM #0x0800 @0xa539 est skippe (d[5a00]==0x88), donc
* le bit tache-FB de d[0x3f92] reste 0 et le scheduler ne dispatche
* jamais le correlateur. On rejoue l'instruction.
* retirer : des que 0xa539 est reellement execute (le predicat d[5a00] tient la
* bonne valeur), ce qui rend le rejeu redondant.
* NB : inerte sans ARM2DSP_CTRLSYS (exige data[0x0810] bit15).
*/
static int gt = -1;
if (gt < 0) { const char *e = getenv("CALYPSO_GOLIVE_TASKW");
gt = (e && *e == '1') ? 1 : 0; }
if (gt && (s->data[0x0810] & 0x8000)) {
s->data[0x3f92] |= 0x0800; /* rejoue 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);
}
}
/* SM-TRACE (gated CALYPSO_SM_TRACE) : trace instruction-par-instruction
* l'etat-machine handshake 0xdde0-0xde9f (route reclear 0xde8b vs setter
* 0xde9c). Montre PC/op/A/TC + les 5 cellules 0x098a..0x098e a chaque
* pas, pour voir OU le flot devie du chemin de9c et quelles valeurs le
* routeraient. Cap 400. */
if (exec_pc >= 0xdde0 && exec_pc <= 0xde9f) {
static int smt = -1; static unsigned smn = 0;
if (smt < 0) smt = getenv("CALYPSO_SM_TRACE") ? 1 : 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 (gated CALYPSO_B3_TRACE) : le scheduler idle 0xb380-0xb440 poll
* le mot de flags data[0x0fff] (b424 BITF 0x0fff,#2 ; b427 BC NTC b41c) et
* doit router vers le bloc go-live b3db-b3ef (b3ef = ST #2,0x3f70). Trace PC +
* data[0x0fff]/[0x08E2=d_dsp_page]/[0x3f70=golive]. Montre pourquoi la commande
* ARM (d_dsp_page bit1) n aboutit pas au bloc b3db. Cap 500. */
if (exec_pc >= 0xb380 && exec_pc <= 0xb440) {
static int b3t = -1; static unsigned b3n = 0;
if (b3t < 0) b3t = getenv("CALYPSO_B3_TRACE") ? 1 : 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 (2026-06-25, RO) : apres VEC28-FORCE, le handler vec28
* tourne mais n'atteint pas le dispatch 0xa51c. Trace le chemin exact :
* 0xf0(vecteur) -> 0x7234(handler) -> 0x013b(prologue) -> 0xa4e4(sched) ->
* 0xa4ff(CALL 0xb522) -> 0xa501/0xa507 -> 0xa51c(dispatch) / 0xa509(arm IMR).
* Voir OU la chaine devie. Cap 120. */
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 (2026-06-24, RO) : COMMENT le DSP entre dans la boucle
* d'attente 0xa4ca — le caller exact (exec_pc precedent hors region
* 0xa4ca..0xa4e2) + l'etat. Tranche : entree via go-live 0xa500 (normal,
* INTM deja cleared) vs branche/soft-vector directe (anormal = pas passe
* par le go-live, INTM encore set). Cap 20. */
{
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 (2026-06-24, RO) : la dispatch commande ARM 0xb19d -> go-live
* 0xa500 est-elle JAMAIS atteinte ? 0xa500 est deja sous GOLIVE-WATCH
* (jamais vu > 0xa4e2) ; ici on regarde l'amont 0xb19d. Si jamais hit ->
* le DSP ne traite pas d_dsp_page, coince dans la mauvaise boucle en
* amont du go-live. Cap 30. */
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 (2026-06-24) : la boucle go-live/AFC 0xa4ca ne relache jamais
* (BC 0xa4cd = AEQ A==0). A vient de CALL 0xaad5. On trace 0xaad5-0xaae6
* (le poseur de A) instruction par instruction + AR0/AR1/A/TC + les mots
* compteurs data[0x434e]/data[0x434f] et les flags candidats
* data[0x3f70]/data[0x3f92]/data[0x435b] : voit-on un compteur qui
* n'avance pas (bug decode/data) ou une attente d'IT (IMR=0 -> jamais) ?
* Logge aussi le verdict au gate 0xa4cd. Cap 100. */
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) { /* le gate BC AEQ : pourquoi 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) : 1ere transition INTM 1->0 du run = RSBX INTM
* enfin execute = interruptions globalement armees. LE signal de victoire. */
{
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;
}
/* === SONDES MVDK-FIX VALIDATION + OBSERVATION (2026-06-24) ===
* MVDESYNC : apres le fix MVDK 0x71, les mots-operandes ne doivent PLUS
* etre executes comme instructions. Fire si PC atterrit sur un mot
* operande MVKD (0xb3ce/d1/d4) ou MVDK (0xb3dd/e0/e3) = mis-decode
* residuel. SILENCE attendu = fix OK. Cap 40. */
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);
}
/* Valeurs des 2 slots CALA (le 3e = BACC 0xb40f deja a 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);
}
/* FINDING-2 unblock : l'init programme-t-il enfin IPTR=0x140 (base
* vecteurs 0xa000 -> INT3 @0xa04c, le vrai handler trame) ? 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);
}
}
/* SONDE post-bootstub-ret (GAP-1) : prologue @0x7013 (pshm contexte) et
* epilogue @0x7020 (popm ar1/st0/st1/pmst; ret). Si l'epilogue depile un
* contexte/retPC stale (pas de prologue/IT correspondant) -> desync pile
* post-bacc. retPC = data[SP+4] (apres les 4 popm). */
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);
}
/* GOLIVE-REDIRECT (2026-06-25, gated CALYPSO_DSP_GOLIVE_BOOT) : EXPÉRIENCE (B)
* preuve-de-racine. Au point où le DSP exécuterait la wait-loop à 0xa4df,
* redirige le FLUX (PC) vers 0xa4c7 (l'arme IMR go-live), UNE fois. Le DSP
* exécute ensuite 0xa4c7(ORM #0x3000,IMR)->0xa4ca->... EN FOREGROUND (il pose
* son propre contexte). PAS une vectorisation (pas de saut ISR sur contexte
* non posé) = équivalent « et si le soft-vector pointait 0xa4c7 ». TEST, pas fix. */
{
/* @BEQUILLE — GOLIVE_REDIRECT (CALYPSO_DSP_GOLIVE_BOOT, EXISTS, defaut OFF)
* masque : ecrit s->pc = 0xb3ec quand le DSP atteint 0xb3ff, c'est-a-dire le
* choix de soft-vector go-live que le boot ROM ne fait pas dans notre
* modele. Second effet : inhibe VEC28-FORCE (bloc c54x_interrupt_ex).
* retirer : quand data[0x3f6d] est peuple par le chemin ROM et pointe 0xa4c7.
*/
static int g_golive = -1;
if (g_golive < 0) g_golive = getenv("CALYPSO_DSP_GOLIVE_BOOT") ? 1 : 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;
}
}
}
/* SONDE GAP-1 DERAIL-ZERO : entrée dans la zone 0x0000-0x0008 = le CALA
* vers un pointeur de fonction NUL (slot dispatcher SARAM = 0). Logge le
* site du CALA (g_prev_pc), les accumulateurs (le 0), et les 4 mots ROM
* AVANT le CALA (= le `ld *(slot),b` -> l'adresse du slot nul a decoder). */
if (exec_pc <= 0x0008 && g_prev_pc > 0x0008) {
static unsigned dz = 0;
if (dz++ < 60) {
uint16_t p4 = prog_fetch(s, (uint16_t)(g_prev_pc - 4));
uint16_t p3 = prog_fetch(s, (uint16_t)(g_prev_pc - 3));
uint16_t p2 = prog_fetch(s, (uint16_t)(g_prev_pc - 2));
uint16_t p1 = prog_fetch(s, (uint16_t)(g_prev_pc - 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) {
/* [2026-07-22] SP-RING au 1er storm : les 24 derniers push/pop
* (pc:op delta) -> l instruction qui over-pop et derive 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 = op non-transfert (PSHM/POPM/FRAME/STM-SP legit, ou inconnu) */
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);
}
}
}
}
/* SONDE GAP-1 DERAIL-ORIGIN : premiere entree dans la zone table garbage
* 0xf090-0xf0a0 depuis l'EXTERIEUR = le saut/chute qui deraille. Logge
* d'ou (g_prev_pc), l'opcode, les ARs (deja garbage ou non), le 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);
}
}
/* SURGICAL : capture silencieuse du slot LUT lu au 0x834d (LD
* (DP<<7|0x07)<<1,A). 1 compare/insn, pas de log → ~zéro impact
* timing. Sert le probe BLACKHOLE-CALA (self-CALA 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-slot word-count fix 2026-05-31 */
/* ===== SHADOW-DADST pre-capture (RO, revival dsp 2026-06-22) —
* GATÉ SUR L'OPCODE DADST/DSADT (0x5a/0x5b/0x5e/0x5f) PARTOUT (pas le PC
* 0x9a80, qui est run-variant : narrow vs wide). Cette famille tombe en
* SFTL (case 0x5). Capture l'état AVANT pour ΔA/ΔB, marche AR5, et le
* résultat shadow-correct. Strictement RO. Le PC loggé révèle où elle tourne. */
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 = (sd_sub == 0x5a || sd_sub == 0x5b
|| sd_sub == 0x5e || sd_sub == 0x5f);
if (sd_armed) {
sd_lhi = s->data[s->ar[5]];
sd_llo = s->data[(uint16_t)(s->ar[5] + 1)];
}
consumed = c54x_exec_one(s);
/* SP-COLLAPSE probe (RO, revival dsp 2026-06-22) : attrape l'instruction
* EXACTE qui effondre SP sous 0x0800 (1ère + 30 suivantes) — le seed de
* toute la cascade (boot-stub / spin 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 (RO, revival dsp 2026-06-22) : capture le saut/vecteur
* qui transfère le contrôle DANS la ROM 0xfe00-0xff7f (data table + init
* 0xff00) = la VRAIE entrée du déraillement (le CALL statique 0xfe00 @0xfdd5
* ne s'exécute jamais → c'est un branchement CALCULÉ). prev_PC/op = coupable,
* A = la cible si c'est un CALA. 1ère + 20 suivantes. */
{
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 (RO, revival dsp 2026-06-23) : entrée dans la PAGE
* VECTEUR IPTR=0x1FF [0xFF80..0xFFFF] = là où l'IRQ atterrit (vec19=0xffcc,
* vec21=0xffd4). LA question décisive (fusion §2 vs bugs séparés) :
* le contrôle entre-t-il via DISPATCH D'INTERRUPTION (prev = foreground
* préempté, d_irq~0) ou via un BRANCH/CALL firmware (prev = un opcode de
* branche ciblant ici, d_irq grand) ? Un dump, deux mondes. Épingle aussi
* le RÉGIME (ARs, A, B) au point d'entrée. Strictement RO. */
{
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 (RO, revival dsp 2026-06-23) : épingle SANS AMBIGUÏTÉ le
* régime du run COURANT. Les vieux logs disent AR5=0x80/AR3=0x000b ; ce soir
* AR3=AR4=0x2ace figés dans le buffer I/Q. Deux régimes = deux fantômes ;
* on en debugge UN. Dump complet du fichier registre à la 1ère + 10000e +
* 1Me visite du spin foreground [0x82c0..0x82f0] (la boucle IQ-READ gelée). */
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 (RO, revival dsp 2026-06-23) : voir A basculer >0 avec le
* fix SQURA. Logue A aux 2 SQURA (0x76ff/0x7700) + à la décision RCD
* LEQ@0x75e8 (prend si A<=0). Avec le blind-MAC A restait <=0 (RCD prend,
* over-pop) ; avec SQURA A doit devenir >0 (RCD ne prend pas). */
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 (RO, revival dsp 2026-06-22) : évolution de A à travers le
* dispatcher 0x80b0-0x80c9 → quel LD pose A=0xfe36 (le handler garbage du
* FCALAD @0x80c8) et depuis quelle case data. */
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); /* le LD du derail */
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 (RO, revival dsp 2026-06-22) : tracke la DERNIÈRE
* instruction qui change DP (ST0[8:0]), et au moment où le dispatcher
* entre (0x80b0) logue DP + qui l'a posé → trouve qui met DP=0x124 (la
* mauvaise page = table coefficient 0x9200 au lieu d'un 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 + oracle (RO, logs only,
* cap = 40 premiers (boot) + 1/2000 (régime permanent) ) ===== */
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; /* ASM signé 5b */
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, signe TBC */
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 (2026-06-02, brief CC-web : audit différentiel) ===
* Inventaire du décodeur sur l'overlay corrélateur 0x8000-0x9FFF :
* logge UNE fois par (PC,op) distinct l'opcode brut, le mot suivant, et
* la LONGUEUR consommée (= consumed, inclut lk_used). À diff contre
* doc/opcodes/tic54x_hi8_map.md — colonne longueur d'abord (un mauvais
* len désync tout le flux suivant, cf bug 0x86/0x87→SP runaway). Vise à
* vider la classe « décode/longueur/mode » d'un coup au lieu de peler
* bug par bug. dedup par (PC,op) → capture aussi les variantes XPC d'un
* même Pag. Gate CALYPSO_DEBUG=DECODE-AUDIT, coût ~nul après couverture. */
static int da_lo = -1, da_hi = -1;
static long long da_insn = -1;
if (da_lo < 0) {
const char *l = getenv("CALYPSO_DA_LO"); const char *h = getenv("CALYPSO_DA_HI");
const char *n = getenv("CALYPSO_DA_INSN");
da_lo = l ? (int)strtol(l, NULL, 0) : 0x8000; /* overlay corrélateur par défaut */
da_hi = h ? (int)strtol(h, NULL, 0) : 0x9FFF; /* CALYPSO_DA_LO/HI pour élargir (ex. 0x7000..0xFFFF) */
da_insn = n ? strtoll(n, NULL, 0) : 0; /* CALYPSO_DA_INSN : skip le boot, viser la fenêtre détection (ex. 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 : trous PROM vide (runaway control-flow, pas un bug
* de DÉCODE) — ils empoisonnaient le cap au boot (sweep 0xCB00-0xD3FF). */
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);
}
}
/* [2026-07-22] INTM-TRANS : trace toute bascule du bit INTM (ST1 b11)
* avec le PC/opcode qui l'a causee. Repond a "INTM passe-t-il jamais a
* 0, et si oui qui le re-arme". Silent sauf CALYPSO_DEBUG=INTM-TRANS. */
{
static int g_intm_prev_tr = -1, g_intm_tr_en = -1;
if (g_intm_tr_en < 0) { const char *e = 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);
}
g_intm_prev_tr = intm_now_tr;
}
/* SP-event ring : enregistre tout changement de SP (push/pop) avec
* le PC/op responsable. Sert le dump BLACKHOLE-CALA. */
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++;
/* PROBE 2026-05-31 : SP qui PLONGE dans la zone API RAM (0x0700-0x0a00)
* = corruption → CALLD push clobber d_fb_det/d_fb_mode (0x08f8/9).
* Loggue l'instruction qui y fait entrer SP (transition depuis hors
* zone) + delta + voisinage pile. Nomme le setter de SP fautif. */
{
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 : appariement push/pop (gate ORPHAN) ===
* Nomme LE return orphelin (over-pop), pas les 15 victimes 0xc8be. */
if (g_shadow_on < 0) {
const char *eo = getenv("CALYPSO_ORPHAN"); /* env dédiée (hors 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 + RC cond */
int is_pshm = ((op&0xFF00)==0x4A00||(op&0xFF00)==0x4B00);
(void)is_call;
if (d < 0) { /* PUSH : SP a baissé */
int words = -d, w;
char kind = is_pshm ? 'P' : 'C'; /* PSHM=data ; reste=adresse retour */
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 a monté */
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) {
/* cible du return : RETD/RETED arment delayed_pc
* (commit différé), RET/FRET immédiat = s->pc. */
uint16_t ret_tgt = (s->delay_slots ? s->delayed_pc : s->pc);
/* dernier PUSH réel de g_spring (le CALL apparié
* manquant) : scan arrière sur 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 lu par ce return : écrit (vecteur
* légit) ou VIERGE (vrai 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);
}
}
/* Au TOUT premier orphan : dump complet du ring
* g_spring (reset→over-pop) pour compter push vs
* pop directement = racine structurelle vs 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-ancre après orphan */
}
}
/* === CORR-ABG probe (2026-05-30, c-web) : la FB-det est FRÉQUENTIELLE
* (FCCH = ton pur), pas un pic d'amplitude. Au site corrélateur 0xec07,
* capture A & B SÉPARÉS (= I/Q de la corr complexe), l'angle atan2(B,A)
* (= la fréquence vue par le détecteur), et les valeurs aux 4 pointeurs
* AR (data-I/Q vs table de réf cos/sin — vérifie que la réf est un vrai
* sinus, pas du garbage/zéro). Cap 30. */
/* DETECTOR-RUN (2026-05-30) : compteur d'exécutions du VRAI détecteur
* freq FCCH (0x9ac0). Pourquoi ne tourne-t-il qu'1× au boot ? Loggue
* insn + d_fb_mode (0x08f9, large vs étroit) + d_task_md (0x0804/0x0818)
* à chaque passage. */
/* [2026-07-27] B4-bis (gated CALYPSO_B4B) : trace du flux APRES le
* detecteur 0x9ac0 -> 0xec07 (decision freq + ecriture 0x08f8) ou boucle.
* + dump one-shot des opcodes 0x9ac0.. pour desassemblage. */
{
/* [2026-07-27] SCAN-08F8 (gated CALYPSO_SCAN_08F8) : one-shot, cherche
* les instructions dont un mot == 0x08f8 (adr d_fb_det) dans tout le
* bank courant -> writer de d_fb_det existe-t-il, et a quel PC ? */
static int _sc = -1; static int _scdone = 0;
if (_sc < 0) _sc = getenv("CALYPSO_SCAN_08F8") ? 1 : 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 = getenv("CALYPSO_B4B") ? 1 : 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) { /* kernel MAC = LECTURE des operandes (I/Q + coeffs) */
g_flow_armed = 1; /* FLOWTRACE : arme la fenetre autour du detecteur */
static int _b2k = -1; static unsigned _b2kn = 0;
if (_b2k < 0) _b2k = getenv("CALYPSO_B2AR") ? 1 : 0;
/* [fix] compteur HORS gate : le min/max doit couvrir TOUT le run
* (la version precedente se bloquait a la 1ere 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[2],(int)(int16_t)s->data[s->ar[2]],_INB(s->ar[2]),
s->ar[3],(int)(int16_t)s->data[s->ar[3]],_INB(s->ar[3]),
s->ar[4],(int)(int16_t)s->data[s->ar[4]],_INB(s->ar[4]),
s->ar[5],(int)(int16_t)s->data[s->ar[5]],_INB(s->ar[5]));
#undef _INB
}
}
{ /* [2026-07-27] ARWATCH : voir en-tete du patch. */
static int _aw = -1; static unsigned _awn = 0;
if (_aw < 0) _aw = getenv("CALYPSO_ARWATCH") ? 1 : 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);
}
}
{ /* [2026-07-27] TRACEFROM : voir en-tete du patch. */
static int _tf = -1; static uint16_t _tfpc = 0; static int _tfd = 0;
static int _tfn2 = 24; /* CALYPSO_TRACEFROM_N : longueur du dump */
static int _tfarm = 0; static unsigned _tfn = 0, _tfr = 0; static uint16_t _tfp = 0;
if (_tf < 0) { const char *e = getenv("CALYPSO_TRACEFROM");
_tf = (e && *e) ? 1 : 0;
if (_tf) _tfpc = (uint16_t)strtol(e, NULL, 0);
const char *n = 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;
}
}
{ /* [2026-07-28] CORROUT : voir en-tete du patch. */
static int _co = -1; static int _in_k = 0; static unsigned _con = 0;
if (_co < 0) _co = getenv("CALYPSO_CORROUT") ? 1 : 0;
if (_co) {
if (exec_pc >= 0xa070 && exec_pc <= 0xa0a0) { _in_k = 1; }
else if (_in_k) { /* on vient de QUITTER le noyau MAC */
_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");
}
}
}
}
{ /* [2026-07-28] VECTAB : voir en-tete du patch. */
static int _vt = -1; static int _vtdone = 0; static int _vtseen = 0;
if (_vt < 0) _vt = getenv("CALYPSO_VECTAB") ? 1 : 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");
}
}
{ /* [2026-07-28] SCANDATA : voir en-tete du patch. */
static int _sd2 = -1; static int _sddone = 0; static unsigned _sdhit = 0;
static uint16_t _lo = 0x76f8, _hi = 0x79f0;
if (_sd2 < 0) {
_sd2 = getenv("CALYPSO_SCANDATA") ? 1 : 0;
const char *a = getenv("CALYPSO_SCANDATA_LO");
const char *b = 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) { /* apres l init des tables */
_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);
}
}
}
{ /* [2026-07-28] DISPWATCH : voir en-tete du patch. */
static int _dw = -1; static unsigned _dwn = 0, _dwfb = 0;
if (_dw < 0) _dw = getenv("CALYPSO_DISPWATCH") ? 1 : 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);
}
}
}
{ /* [2026-07-28] SCANREF : voir en-tete du patch. */
static int _sr = -1; static uint16_t _srt = 0; static int _srd = 0;
if (_sr < 0) { const char *e = 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");
}
}
{ /* [2026-07-27] SCANFB : voir en-tete du patch. */
static int _sf = -1; static int _sfd = 0;
if (_sf < 0) _sf = getenv("CALYPSO_SCANFB") ? 1 : 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");
}
}
{ /* [2026-07-27] FBENTRY : voir en-tete du patch. */
static int _fe = -1; static int _dmp = 0; static int _in = 0; static int _after = 0;
static unsigned _n = 0;
if (_fe < 0) _fe = getenv("CALYPSO_FBENTRY") ? 1 : 0;
if (_fe) {
if (exec_pc >= 0x75e0 && exec_pc <= 0x79f0) { /* inclut le sous-prog 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" : ""); }
}
}
}
{ /* [2026-07-27] SCAN43D8 : voir en-tete du patch. */
static int _s4 = -1; static int _s4done = 0;
if (_s4 < 0) _s4 = getenv("CALYPSO_SCAN43D8") ? 1 : 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");
}
}
{ /* [2026-07-27] SLOTSRC : voir en-tete du patch. */
static int _ss = -1; static unsigned _ssn = 0;
if (_ss < 0) _ss = getenv("CALYPSO_SLOTSRC") ? 1 : 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);
}
}
{ /* [2026-07-27] FBCALL : voir en-tete du patch. */
static int _fc = -1;
if (_fc < 0) _fc = getenv("CALYPSO_FBCALL") ? 1 : 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;
}
}
{ /* [2026-07-27] DISPIDX : voir en-tete du patch. */
static int _di = -1; static unsigned _din = 0; static unsigned _idx = 0;
if (_di < 0) _di = getenv("CALYPSO_DISPIDX") ? 1 : 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);
}
}
}
{ /* [2026-07-27] DISPTAB-DUMP : contenu de la table au dispatcher. */
static int _dd2 = -1; static unsigned _ddn2 = 0;
if (_dd2 < 0) _dd2 = getenv("CALYPSO_DISPTAB") ? 1 : 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");
}
}
{ /* [2026-07-27] TASKGO : voir en-tete du patch. */
static int _tg = -1;
if (_tg < 0) _tg = getenv("CALYPSO_TASKGO") ? 1 : 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); }
}
}
{ /* [2026-07-27] AB38 : voir en-tete du patch. */
static int _ab = -1;
if (_ab < 0) _ab = getenv("CALYPSO_AB38") ? 1 : 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"); }
}
}
{ /* [2026-07-27] FBROUTE : voir en-tete du patch. */
static int _fr = -1;
if (_fr < 0) _fr = getenv("CALYPSO_FBROUTE") ? 1 : 0;
if (_fr) {
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-relatif */
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) { /* consommateur DARAM 0x2a00 (AR3 post-inc) = VRAIE entree corr */
static int _b2c = -1; static unsigned _b2cn = 0;
if (_b2c < 0) _b2c = getenv("CALYPSO_B2SEQ") ? 1 : 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"); }
}
{
/* [2026-07-27] DARAM-DUMP (gated CALYPSO_DARAM_DUMP) : voir en-tete.
* Ecrit le buffer d entree corr en binaire IQ16 -> mesurable par
* tools/corr_iq.py --src bursts (coh/dphi), pas juge a l oeil. */
static int _dd = -1; static FILE *_ddf = NULL; static unsigned _ddn = 0;
static uint16_t _ddpc = 0x9ac0; static unsigned _ddmax = 200;
if (_dd < 0) {
const char *e = 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 = getenv("CALYPSO_DARAM_DUMP_PC");
if (p && *p) _ddpc = (uint16_t)strtol(p, NULL, 0);
const char *m = getenv("CALYPSO_DARAM_DUMP_MAX");
if (m && *m) _ddmax = (unsigned)atoi(m);
_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");
}
}
/* [2026-07-27] C2 : ne filmer QUE les passages en recherche FCCH.
* Sans ce garde, le cap _ddmax etait consomme des le boot, pendant
* que d_fb_mode[0x08f9]==0 -> le dump ne montrait pas la phase FB et
* on en concluait a tort « le buffer ne contient jamais de FCCH ».
* Override CALYPSO_DARAM_DUMP_ANYMODE=1 pour revenir a l ancien. */
static int _ddany = -1;
if (_ddany < 0) { const char *e = 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; /* 0x2a00..0x2b27 = 296 mots = 148 paires I/Q */
hdr[0]='I'; hdr[1]='Q'; hdr[2]='1'; hdr[3]='6';
unsigned _fnv = calypso_daram_last_fn; /* fn GSM reel du dernier depot */
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[0x2a00 + _k];
unsigned char _b[2]; _b[0]=(unsigned char)(_v & 0xff); _b[1]=(unsigned char)(_v >> 8);
fwrite(_b, 1, 2, _ddf);
}
/* [2026-07-27] DARAM-SANITY : verdict en run sur ce qu'on vient
* de dumper. coh = |Sum z[k+1].conj(z[k])| / Sum|z[k+1]||z[k]| ;
* dphi = arg(Sum ...). Le kernel FB veut du FCCH @1SPS => dphi
* = +pi/2 (+1.571). +0.393 = 4 SPS non decime (remede nomme). */
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[0x2a00 + 2*(_k-1)];
double q0 = (int16_t)s->data[0x2a00 + 2*(_k-1) + 1];
double i1 = (int16_t)s->data[0x2a00 + 2*_k];
double q1 = (int16_t)s->data[0x2a00 + 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);
}
}
}
if (exec_pc == 0x9ac0) {
/* [2026-07-27] B2SEQ (gated CALYPSO_B2SEQ) : dump 16 paires (I,Q) de
* 0x2a00 (VRAIE entree corr, depot BSP/ADC). Pattern Fs/4 = FCCH :
* (a,0)(0,a)(-a,0)(0,-a).. ; quasi-constant = DC sans ton (entree vide). */
{ static int _b2s = -1; static unsigned _b2sn = 0;
if (_b2s < 0) _b2s = getenv("CALYPSO_B2SEQ") ? 1 : 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++;
/* [2026-07-27] B2AR (gated CALYPSO_B2AR) : ou pointent les AR du corr
* + valeur lue. AR5/AR3 dans [0x2a00..0x2b27] => lit la FCCH ; hors =>
* lit a cote (RANK3, pointeur mal initialise). */
{ static int _b2a = -1; static unsigned _b2an = 0;
if (_b2a < 0) _b2a = getenv("CALYPSO_B2AR") ? 1 : 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"); } }
/* [2026-07-27] B2 (gated CALYPSO_B2) : module accu A/B + max/indice
* sur les 296 mots entree(0x2a00) & workspace(0x2c00). Tranche nul vs
* plat-sans-pic. */
{
static int _b2 = -1; static unsigned _b2n = 0;
if (_b2 < 0) _b2 = getenv("CALYPSO_B2") ? 1 : 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) calculé à l'analyse (pas de math.h ici). */
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 (2026-05-30) : au store du TOA (PC=0x9ac0, STL A
* dans a_sync_demod) dumper A/B complets + AR + T + la fenêtre d'entrée
* lue (buffer BSP 0x2a00) → voir comment le corrélateur dérive le TOA
* (offset peak, wrap, référence) à partir d'une FCCH pourtant correcte.
* Cap 40, ~zéro coût hors site. */
if (exec_pc == 0xa0e7 || exec_pc == 0x9ac0) {
static unsigned cp_log = 0;
/* Ne fire QUE quand une vraie I/Q est présente (input non-nul),
* sinon le cap est gaspillé sur le boot (buffer vide). On teste
* 0x2a00 (BSP write) ET 0x2c00 (où AR3 pointe = lecture corr). */
/* Gate sur RX buffer 0x2a00 NON-NUL : ne fire que quand une vraie
* I/Q est présente (sinon gaspillé au boot/vide). Capture la vraie
* corrélation FCCH dès que le BSP livre. */
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 = PC qui vient
* d'exécuter. Si A a changé, c'est cet opcode qui a écrit A. */
if (s->a != a_before_exec) {
p_last_a_pc = exec_pc;
p_last_a_val = s->a;
}
/* === END CALA-70C3 FORENSIC PROBES === */
/* === BOOT-BRANCH probe : control-flow skeleton for boot phase ===
* Hunt the opcode that branches OVER the DSP init code (which should
* set SP=0x5AC8 + AR4/AR5 + IMR=0xFFFF). IMR-W trace localised the
* collateral damage to PC=0xf03a (insn=262501) and PC=0x8ebc
* (insn=5247868) via stale ARx=0 → STH B,*ARx+ → MMR_IMR=0. The bad
* branch happens earlier. Log every PC discontinuity (branch, call,
* return, IRQ entry) while insn_count <= 300000 + snapshot SP/IMR/
* AR4/AR5 — visualizes when each register got armed (or never did). */
if (s->insn_count <= 300000) {
/* c54x_exec_one does NOT advance s->pc for sequential insn — that
* happens in `s->pc += consumed` further down. So a real branch
* (or CALL/RET/IRQ entry) is the only thing that modifies s->pc
* during the exec itself. */
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);
}
}
}
}
/* INT3-CYCLE-TRACE (env CALYPSO_INT3_CYCLE_TRACE=1, c web reframe night5) :
* record branch decisions during INT3 ISR cycle. */
int3_cycle_track_branch(s, exec_pc, exec_op, consumed);
/* Detect SP changes — only log after init (insn > 490M) */
if (s->sp != sp_before && s->insn_count > 490000000) {
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 + delta histogram (2026-05-27, c web review) ===
* Trip on FIRST SP descent below SP_FLOOR — that snapshot is BEFORE
* the MMR auto-corruption (SP at MMR data[0x18]), so it captures the
* cause not the crash. Plus running delta histogram to identify
* leaking call/ret pairs (FAR push 2 vs near pop 1 = -1 per pair).
* Active by default — minimal cost (a few branches per insn). */
#define SP_FLOOR 0x0080
{
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;
}
/* Log first 80 SP changes + every 5000 — enough to characterize
* the leaking call/ret pair WITHOUT drowning the 1.3 GB 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);
}
}
/* === Plan B detectors (run once per insn 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
/* v2 SP observability — only when CALYPSO_TRAP_OOR=1.
* (a) sp_trail[256] : |Δ|>32 events (scheduler reloads + big allocs)
* (b) sp_low watermark : every new low, PC-coalesced power-of-10
* Gated to insn>33754 (after init stack 0x9022→0x5ac8 normal). */
{
static int trap_armed = -1;
if (trap_armed < 0) {
const char *e = cdbg_env("TRAP-OOR"); (void)e;
/* TRAP-OOR RETIRED 2026-05-29 : c'était l'analyse de descente
* SP / SP-CATASTROPHE, résolue (0x70c3 self-CALA / DROM-LUT /
* stub). Le site 2 faisait s->running=0 au checkpoint 4.2M =
* LE bottleneck (CALYPSO_DEBUG=ALL haltait le DSP). Forcé OFF. */
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);
/* SP-HIST per-PC accounting déplacé en TOP-of-loop chokepoint
* (fix v6 2026-05-24) — bypass-proof. Voir L6773.
* Pas d'appel ici sinon double-count. */
/* (a) trail — only big jumps (skip push/pop ±1..32 noise) */
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 (incl Δ=-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-LEDGER + SP-INTO-MMR probes RETIRÉS 2026-05-23 :
* info diagnostic déjà extraite (irq_entries=1 sur 144s, SP wrap
* via stack-relative writes en MMR). Ces probes fire à CHAQUE
* instruction → overhead non-négligeable sur DSP throughput
* (mesuré 9.1M insn/s vs 10M required = 9% slow). Reviendront en
* cas de régression. SP-CATASTROPHE garde la haut |Δ|>256. */
/* === SP catastrophic delta tracer ===
* Diag v2 2026-05-08 : SP went from 0x9c1e → 0x0001 in one window
* (lost ~40k stack words). The progressive-leak log above caps at
* 100 small deltas and misses the single catastrophic event.
* This block flags any |Δ| > 100 in one instruction — never
* capped — so the buggy STM/PSHM/POPM/RETE-corrupted-stack /
* FRAME-with-huge-offset is unambiguously identified the FIRST
* time it happens. ARs included so we can see if the ST/LD
* destination resolved to an MMR slot (e.g. *AR=0x18 → MMR_SP).
*
* Threshold raised from 100→256 on 2026-05-08 to filter legitimate
* FRAME #imm8s (signed 8-bit can be ±127). Real catastrophes from
* dual-op writing to 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);
}
}
/* === v2 TRAP-OOR firing point — fixed checkpoint halt ===
* T1/T2 dropped (v1 v2 redesign: scheduler at 0xfd2a exonerated,
* SP clobber lives in legit code → no PC whitelist nor SP edge
* can catch it). Halt at fixed insn checkpoint, dump trail+sp_low
* for offline analysis of full descent.
* Checkpoint configurable via CALYPSO_TRAP_CHECKPOINT (default
* 4200000 = just after the insn=4.09M SP recovery 0x0008→0x2900). */
{
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;
/* TRAP-OOR RETIRED 2026-05-29 : c'était l'analyse de descente
* SP / SP-CATASTROPHE, résolue (0x70c3 self-CALA / DROM-LUT /
* stub). Le site 2 faisait s->running=0 au checkpoint 4.2M =
* LE bottleneck (CALYPSO_DEBUG=ALL haltait le DSP). Forcé OFF. */
trap_armed = 0;
const char *c = 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;
}
}
/* === VARIATEUR DE VITESSE osmocon : le break est MAINTENANT en
* FIN d'itération (après s->pc += consumed + commit delay-slots),
* cf bloc plus bas. Casser ici (avant l'avance du pc) ré-exécutait
* l'instruction courante à la ré-entrée → double-pop des RET/RETD/RCD
* → over-pop SP → garbage DP → self-CALA 0x70c3. Fix 2026-05-30. */
/* === DUAL-OP-INTERPRET diagnostic ===
* Compare current decoder's AR field interpretation (3-bit fields)
* with SPRU172C's dual-operand encoding (2-bit AR fields + offset 2,
* AR2..AR5 only). If the two disagree on which AR is used and the
* SP-CATASTROPHE just fired, we have evidence the encoding is
* wrong. Cap to 100 entries to avoid log explosion. */
if ((exec_op & 0xFC00) == 0xC800 && (
(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 repeat is active,
* re-execute the SAME instruction (don't advance PC) until count=0. */
if (s->rpt_active && !s->idle) {
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; /* C54x has 16-bit PC (23-bit with XPC, but wrap at 16-bit) */
/* consumed == 0 means PC was set by branch */
/* [2026-07-23] SP-CORRUPT watchpoint : quelle instruction sort SP de la
* plage pile valide [0x5900,0x5c00] ? (derail 0xa58d SP=0xc905 post-POPD).
* Logge la 1ere transition dedans->dehors avec pc/op = le coupable. */
{
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);
}
g_wp_prev_sp = s->sp;
}
/* [2026-07-23] c54x on-chip TIMER0 tick — HORLOGE MANQUANTE (diag horloges +
* intuition user "tick TINT"). Le go-live arme IMR bit4 (TINT vec20) et ATTEND
* le timer, mais c etait une facade morte (registres TIM/PRD/TCR OK, mais AUCUN
* decrement -> jamais de TINT). On tick TIM (avec prescaler TDDR) par instruction ;
* a l underflow -> reload TIM=PRD + fire TINT vec20/bit4. Gate CALYPSO_DSP_TIMER_OFF
* (A/B), defaut ON. Le firmware demarre le timer (clear TSS) + configure PRD/TDDR. */
{
/* [2026-07-23] TINT0 MASTER CLOCK (modele du gap : le firmware arrete le
* timer DSP (TCR TSS=1) mais sur HW reel TINT0 = master clock TDMA. On fire
* TINT0 vec20/bit4 a cadence ~frame (fixe) independamment de TSS. Gate
* CALYPSO_TINT0_MASTER defaut ON, OFF via CALYPSO_TINT0_MASTER_OFF=1. */
{
/* [2026-07-23] fire crude per-2000-insn REMPLACE par sync frame-tick
* (dsp_shunt.c:430). Ce bloc desactive (garde pour A/B legacy). */
/* @BEQUILLE — TINT0_PERINSN (CALYPSO_TINT0_PERINSN, EXISTS, defaut OFF)
* masque : l'absence de base de temps DSP. Fire TINT0 (vec20/bit4) toutes les
* 2000 insns, sans aucun rapport avec la cadence TDMA.
* retirer : remplace par le tick TIMER0 fidele juste en dessous.
* ATTENTION : le commentaire "Ce bloc desactive" est FAUX — le code est execute,
* seule l'absence de la variable l'eteint.
*/
if (getenv("CALYPSO_TINT0_PERINSN")) {
static unsigned _t0c = 0;
if (++_t0c >= 2000) { _t0c = 0; c54x_interrupt_ex(s, 20, 4); }
}
}
static int _tmr = -1;
if (_tmr < 0) _tmr = getenv("CALYPSO_DSP_TIMER_OFF") ? 0 : 1;
/* @BEQUILLE — TINT0_MASTER (CALYPSO_TINT0_MASTER, EXISTS, defaut OFF hors profil
* WIRE — calypso.env/wire.env ne le posent que sous CALYPSO_WIRE=1)
* masque : la configuration du TIMER0 par le ROM (TCR/PRD). Le firmware arrete
* le timer (TSS=1) dans une init non-tournee ; on force PRD=0xFFFF et
* on tick MALGRE TSS, plus un fire TINT0 vec20/bit4 au frame-tick du
* shunt (calypso_dsp_shunt.c).
* retirer : quand la sequence d'init TIMER0 du ROM s'execute (TCR programme,
* TSS=0).
* NB : le 3e site historique est mort — neutralise par (void)_t0i;.
*/
static int _t0master = -1;
if (_t0master < 0) _t0master = getenv("CALYPSO_TINT0_MASTER") ? 1 : 0;
/* [2026-07-23] TIMER0 FIDELE : le firmware arrete le timer (TCR TSS=1) dans
* l'init op non-tournee. En mode TINT0_MASTER on modelise le ROM ayant
* configure+demarre le timer : on tick malgre TSS. PRD non configure (0/0xFFFF
* reset) -> underflow ~65536 insns ~= frame TDMA (13MHz). Fire TINT0 vec20/bit4
* a l'underflow ; c54x_interrupt_ex RESPECTE l'IMR (pas de forcing IMR/IFR). */
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 (vec20/bit4) "
"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_interrupt_ex(s, 20, 4); /* TINT : vec20, IMR bit4 */
} else {
s->data[TIM_ADDR]--;
}
} else {
s->timer_psc--;
}
}
}
/* === BRANCH-TRACE (2026-06-24, sonde amont event-starvation) ==========
* consumed==0 <=> PC pose par une branche/call/ret PRISE (sinon s->pc +=
* consumed sequentiel, ligne ci-dessus). On logge CHAQUE transfert de
* controle PRIS dans la fenetre boot/init [insn<6000], filtre du storm
* PC=0 (exec_pc!=0 && tgt!=0), avec site + opcode de branche + CIBLE +
* etat des conditions (TC, ACC=0?, A, ARx, IMR, INTM). BUT : voir QUEL
* branchement conditionnel detourne le firmware de l'install-vecteurs/
* enable-IRQ vers la garde idle = le mur amont. Discrimine les 2 cas du
* pari : (a) un BC/BANZ PRIS dont la cible court-circuite la pose IPTR
* (=jamais atteint) vs (b) le code atteint la pose sans effet (=bug MMR).
* Cap 700. */
if (consumed == 0 && exec_pc != 0 && s->pc != 0 && s->insn_count < 6000) {
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.
* RCD (and later CALLD/RETD/BD/CCD if extended) sets delayed_pc and
* delay_slots = 2. The two instructions following the RCD execute
* as normal pipeline slots; once both have completed the branch
* commits by forcing PC to delayed_pc. */
/* Delay-slot countdown — compté en MOTS, pas en instructions.
* BUG FIX 2026-05-31 : l'ancien code décrémentait dès l'itération qui
* arme delay_slots=2, PUIS d'1 par instruction → une SEULE instruction
* de delay-slot exécutée. OK pour un slot = 1 insn 2-mots (STM #k), mais
* pour un slot = DEUX insns 1-mot, la 2e était SAUTÉE. Quand cette 2e
* insn est un PSHM/PSHD (sites PROM0 0xb53a/0xc9a2/0xcaab = code
* power-scan que le mobile exécute en cell-search), le push est perdu →
* over-pop cumulé (~58 mots) → POPM ST0 @0x94f3 ramasse l'orphelin
* 0x80fd → DP=0x0fd → dispatcher 0x8341 lit la LUT garbage → CALAD
* 0x70c3 = self-CALA → écrit 0x70c4 (=28868) dans d_fb_det/a_pm →
* rxlev/TOA poison → NO_CELL_FOUND. cf doc/SP_CATASTROPHE_70c4_SEQUENCE.
* Le C54x a TOUJOURS 2 mots de delay (SPRU172C) : 1 insn 2-mots OU 2
* insns 1-mot. On décrémente du nombre de MOTS exécutés (= consumed), et
* on NE compte PAS l'itération qui arme (ds_before==0 = la branche
* elle-même ; le delay commence à l'instruction suivante). */
if (s->delay_slots > 0) {
if (ds_before == 0) {
/* itération de la branche différée elle-même : ne rien
* décrémenter ; delay_slots (=2) est un compteur de MOTS. */
} 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). */
if (s->rptb_active && !s->rpt_active && s->pc >= s->rea + 1) {
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 : dump périodique pour valider net_words→0 sur run long
* (métrique de balance push/pop post-yield-fix). ~1 compare/insn. */
if (s->insn_count - g_sp_ledger.last_dump_insn >= 20000000u) {
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);
}
/* === VARIATEUR DE VITESSE osmocon (gated, CALYPSO_DSP_YIELD=N) ===
* Le DSP c54x tourne SYNCHRONE dans tdma_tick sur le thread principal.
* Tous les N insns on sort de c54x_run → la mainloop pompe l'I/O
* (osmocon) puis délivre les IT au DSP.
* N PETIT = yield fréquent = osmocon rapide / DSP ralenti
* N GRAND = yield rare / 0 = OFF (legacy, DSP garde tout le budget)
* IMPÉRATIF : ne casser qu'à un BOUNDARY PROPRE — ici, après
* `s->pc += consumed` ET le commit des delay-slots (delay_slots==0).
* Sinon (a) l'instruction courante est ré-exécutée à la ré-entrée
* (double-pop) et (b) un IT délivré par la mainloop tomberait au
* milieu des delay-slots d'un RETD/RCD → retour différé corrompu.
* Les deux mènent à l'over-pop SP → DP garbage → self-CALA 0x70c3.
* (Valeur idéale = statique à déterminer ; gardée en env pour l'instant.) */
{
static int dsp_yield = -1;
if (dsp_yield < 0) {
const char *e = getenv("CALYPSO_DSP_YIELD");
/* Défaut statique 32768 (2^15) : cadence DSP↔osmocon/IT calée
* (valeur trouvée empiriquement, ON par défaut). 0 = OFF legacy
* seulement si CALYPSO_DSP_YIELD=0 explicite. */
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)");
}
/* Bien implémenté (fix 2026-05-30) : ne yielder qu'à un point
* INTERRUPTIBLE. Le yield rend la main à la mainloop qui délivre
* l'IT (INT3) au DSP ; sur vrai C54x une IT n'est prise qu'à INTM=0
* (hors section critique). Couper sur un simple compteur d'insns
* tombait en pleine séquence de dispatch (INTM=1, DP hérité avant
* LDP) → l'IT au resume corrompait DP/ST0 → CALAD vers la LUT
* (wedge 0x9207) ou self-CALA. On exige donc :
* - executed >= dsp_yield ET INTM=0 (point sûr), OU
* - executed >= 4×dsp_yield (cap dur : évite la famine mainloop
* si le firmware reste en INTM=1 anormalement longtemps).
* delay_slots==0 garde inchangée (jamais mid-branche-différée). */
/* 4 gardes = les 4 états non-interruptibles du C54x :
* delay_slots==0 : pas mid-branche-différée (RETD/RCD/CALLD/BD)
* !rpt_active : pas mid-RPT (single-repeat = NON interruptible
* sur HW jusqu'à RC épuisé ; RPTB l'est, lui)
* INTM==0 : interruptible (hors section critique/dispatch)
* (+ break après commit pc/delay = pas mid-instruction)
* Cap dur 4× : si INTM reste 1 anormalement, force le yield pour
* éviter la famine mainloop (cas "illégal" tracé ci-dessous). */
if (dsp_yield > 0 && s->delay_slots == 0 && !s->rpt_active &&
((executed >= (unsigned)dsp_yield && !(s->st1 & ST1_INTM)) ||
executed >= (unsigned)dsp_yield * 4u)) {
/* Preuve du gate : log les premiers breaks + tout break "cap-forcé"
* (INTM=1 = illégal toléré). Si on ne voit JAMAIS de cap-forcé sur
* N runs ET 0x9207 disparaît → le gate est prouvé, pas juste constaté. */
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; /* boundary propre + interruptible → mainloop sert I/O + IT */
}
}
}
return executed;
}
/* ================================================================ *
ROM loader *
================================================================ */
/* ================================================================ *
Init / Reset / Interrupts *
================================================================ */
C54xState c54x_init(void) { C54xState s = calloc(1,
sizeof(C54xState)); if (!s) return NULL; 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); }
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 = reset datasheet
propre : A=B=0 * (le snapshot a BL=0x60, appliqué seulement en mode bin)
/ / ── REVIEW registres : hardcode C ↔︎
calypso_dsp.Registers.bin (2026-05-31) ── * Le hardcode ci-dessous =
mode “c54x” = RESET DATASHEET PROPRE (champs * critiques alignés au
snapshot, champs bénins/garbage à 0). Les 3 modes * sont ainsi
orthogonaux (sélecteur plus bas) : * c54x = ce hardcode propre,
indépendant du fichier * bin = override depuis .Registers.bin VERBATIM
(snapshot exact, défaut) * hybrid = bin pour l’opérationnel + champs
critiques forcés propres * (IFR=0, AR0=0xFF75, BRC/RSA/REA=0) → ≈ c54x
sur ces champs Le .bin est un SNAPSHOT mi-exécution
(post-handshake bootloader). Review * champ par champ (verdict =
pertinence de la valeur AU RESET) : MMR valeur(bin=hardcode)
classe commentaire * IMR 0x00 0x52FD CRITIQUE-OK masque IRQ, identique 3
dumps * IFR 0x01 0x0008 BÉNIN bit3 INT3 pending ; INTM=1 masque * →
hybrid le met à 0 (datasheet pur) * ST0 0x06 0x181F CRITIQUE-OK DP=0x1F
* ST1 0x07 0x2900 CRITIQUE-OK INTM/SXM/XF * A 08-0a 0x000000 OK
accumulateur A = 0 * B 0b-0d 0x000060 BÉNIN BL=0x60 (rechargé avant
usage) * T 0x0E 0x0000 OK * TRN 0x0F 0xFF75 BÉNIN Viterbi, neutre au
reset * AR0 0x10 0x5AAD BÉNIN rechargé (LD @0x7120) ; hybrid=0xFF75 * AR1-5 11-15
invariants CRITIQUE-OK identiques 3 dumps (API_RAM, etc.) * AR6 0x16
0xBAE6 BÉNIN rechargé avant usage * AR7 0x17 0x1E44 BÉNIN idem * SP 0x18
0x1100 CRITIQUE-OK pile post-handshake * BK 0x19 0xFFF6 CRITIQUE-OK
circular buffer * BRC 0x1A 0x8FD7 GARBAGE reste RPTB mi-vol ; neutre *
RSA 0x1B 0xD9EC GARBAGE (rptb_active=false au reset) * REA 0x1C 0xBBEF
GARBAGE → hybrid les met à 0 * PMST 0x1D 0xFFA8 CRITIQUE-OK IPTR=0x1FF,
MP_MC, OVLY, DROM * XPC 0x1E 0x0000 — jamais overridé (page runtime,
fetch vec) CONCLUSION review : les champs CRITIQUE-OK
(SP/ST0/ST1/PMST/IMR/AR1-5/BK) * sont identiques bin↔︎hardcode et
pilotent le reset. Les divergences (IFR, * AR0/6/7, TRN, B, BRC/RSA/REA)
sont toutes BÉNIGNES ou GARBAGE et neutres * au reset (IT masquée par
INTM=1 ; AR rechargés ; rptb_active=false). Donc * aucune n’explique le
stuck FB (boucle BITF @0xf2cd sur
data[0x585f]). / / AR registers aligned with silicon spec
(doc/datasheets/README.md §3, * 2026-05-25). Cross-checked 3 ROM dumps
(3311/3416/3606) + local osmocom : * AR1=0x005F, AR2=0x0813, AR3=0x0014,
AR4=0x0003, AR5=0x0014 (invariant) * AR0=0xFF75, BK=0xFFF6 (local
osmocom dump values) * AR6, AR7 : non documenté invariant, garde 0
Précédent : memset 0 = init shortcut, même problème que SP/IMR. *
Symptôme : STL A,AR2 à PC=0x9ac0 avec AR2=0 écrivait à mem[0x00]=IMR
→ IMR cleared → toutes IRQ FRAME/BRINT0 masquées → DSP bloqué en
df9x. / / AR registers — mode c54x = reset datasheet propre. *
AR1-5 = invariants cross-dump (3311/3416/3606) = identiques au snapshot,
* gardés ici car CRITIQUES. AR0/AR6/AR7 = 0 (neutres : le firmware les *
recharge avant usage, ex. LD @0x7120). Le
snapshot a AR0=0x5aad, * AR6=0xbae6, AR7=0x1e44 → appliqués seulement en
mode bin. / memset(s->ar, 0, sizeof(s->ar)); s->ar[0] =
0x5AAD; / FIX 2026-05-31 : AR0 = valeur silicium (snapshot bin). *
PROUVÉ read-before-write à insn=1 (PC=0xb410 ORM * data[*AR0]) via sonde
AR-FIRSTUSE → AR0 reset est * load-bearing, l’ancien 0xFF75 (dump local,
“neutre”) * faisait diverger c54x vs bin dès la 1ʳᵉ instruction du *
boot. Aligné sur le silicium → convergence des modes. / s->ar[1]
= 0x005F; s->ar[2] = 0x0813; / API_RAM-related — clobber IMR si
=0 (cf 2026-05-25) / s->ar[3] = 0x0014; s->ar[4] = 0x0003;
s->ar[5] = 0x0014; s->t = 0; s->trn = 0; / TRN=0 (snapshot
0xff75, neutre au reset) / s->sp = 0x1100; s->bk = 0xFFF6;
/ SP+BK init aligned with silicon (2026-05-25). * 3 ROM dumps
(3311/3416/3606) + local : SP=0x1100 * post-bootloader-handshake. Let
firmware repoint * to its own stack (0x5AC8 historically observed) * via
init sequence, comme sur silicon réel. * Précédent : SP=0x5AC8 =
shortcut anticipant * la re-init firmware. Suspect d’être la racine * du
clobber AR5↔︎SP overlap à mem[0x3fbe]. * Voir doc/datasheets/README.md
§3-4. / / BRC/RSA/REA = 0 (reset datasheet propre). Le snapshot
capture des restes * de RPTB mi-vol (BRC=0x8fd7 RSA=0xd9ec REA=0xbbef) =
GARBAGE sans sens au * reset ; appliqués seulement en mode bin.
rptb_active=false (posé plus bas) * → ces registres ne sont consultés
qu’après qu’un RPTB les recharge. / s->brc = 0; s->rsa = 0;
s->rea = 0; / MMR reset values aligned with Calypso silicon (3
FreeCalypso ROM dumps + local). * Empirically validated 2026-04-28. See
doc/datasheets/README.md §3. * Previous QEMU values (st0=0,
st1=ST1_INTM, pmst=0xFFE0) were partial. / 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;
/ IMR aligned avec local osmocom dump * (doc/datasheets/README.md
§3, post- * bootloader-handshake). 0 était un autre * shortcut comme SP.
IRQ #2..#10 vus * INTM=1 IMR=0x0000 IFR=0x28 → IRQs * masquées toutes →
handlers jamais run * → flags dispatcher pas écrits → DSP * boucle
indéfiniment en df9x (= bloqueur * #2 chain FBSB). Fix 2026-05-25. /
s->ifr = 0; / IFR=0 (reset datasheet propre). Le snapshot a
0x0008 * (bit3 INT3 pending) ; appliqué seulement en mode bin. * Neutre
de toute façon : INTM=1 (ST1=0x2900) masque l’IT. / s->xpc = 0;
/ ===================== Sélecteur d’état reset registres
===================== * Trois modes, choisis par env
CALYPSO_DSP_REG_MODE : * “c54x” → hardcode C ci-dessus UNIQUEMENT (le
.bin chargé est ignoré). * “bin” → snapshot calypso_dsp.Registers.bin
override TOUT (verbatim). * “hybrid” → snapshot bin POUR les registres
opérationnels validés, MAIS * garde le hardcode pour les champs où le
.bin est jugé faux * par l’audit anti-drift (cf table plus haut) : * IFR
: bin=0x0008 (IRQ pending résiduel) → hardcode 0 * AR0 : bin=0x5aad (non
validé) → hardcode 0xFF75 * BRC/RSA/REA : bin=garbage RPTB mi-vol →
hardcode 0 * Défaut : “bin” si un .bin est chargé (continuité avec le
comportement * câblé par run.sh), sinon forcément le hardcode (rien à
overrider). * Tous lus une fois (reset appelé 2× : boot +
DSP_DL_STATUS_READY). / { static int reg_mode = -1; / 0=c54x
1=bin 2=hybrid / if (reg_mode < 0) { const char e =
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”/défaut / 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; /* Registres opérationnels — communs bin + 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 traité plus bas / s->ar[i] = r[0x10 + i];
s->sp = r[0x18]; s->bk = r[0x19]; s->pmst = r[0x1d]; if
(reg_mode == 1) { / BIN PUR (2026-06-25) : .bin VERBATIM, AUCUN
hardcode forcé. * Avant : IFR/BRC/RSA/REA forcés 0 (anti-drift) =
hardcodes * résiduels qui jetaient l’état silicium réel (IFR=0x0008 INT3
* pending notamment). Le snapshot EST l’état silicium → on le * respecte
intégralement. rptb_active=false (posé plus bas) => * BRC/RSA/REA non
consultés tant qu’un RPTB ne les recharge pas. / s->ifr =
r[0x01]; s->ar[0] = r[0x10]; s->brc = r[0x1a]; s->rsa =
r[0x1b]; s->rea = r[0x1c]; } else { / reg_mode == 2 : hybrid →
registres opérationnels du bin, * MAIS champs critiques forcés aux
valeurs datasheet pures * (cf audit anti-drift) pour un reset propre.
/ s->ifr = 0x0000; / pas d’IRQ pending au reset /
s->ar[0] = r[0x10]; / FIX 2026-05-31 : AR0 = snapshot silicium *
(0x5aad), pas le hardcode 0xFF75 : prouvé * read-before-write insn=1 →
load-bearing. / s->brc = 0x0000; s->rsa = 0x0000; s->rea =
0x0000; } / XPC jamais overridé (registre de page runtime ; le
fetch vecteur * reset à IPTR0x80 doit venir de la page 0 → XPC=0
conservé). / 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;
}
/* 2026-05-28 v3 : runtime conditional override (option A).
*
* v1 (static override prog[0xFF80] = B 0x7120) intercepted both the
* silicon reset AND every sequential firmware walk into address 0xff80.
* The firmware ROM contains a normal subroutine that includes a RET at
* 0xff79 popping 0xff7a + POPM/STM sequence walking through 0xff80
* during routine epilogues. The static override turned each such walk
* into a soft-reset → SP=0x5AC8 → state derailed → DSP stuck in
* boot-reset cycle, FB never stabilising past the first 4s.
*
* v3 fix : leave prog[0xFF80..0xFF83] as legitimate PROM1 mirror
* (= 0x56d0, 0x9631, 0xf820, 0xff89 from the dump). Apply the boot-init
* redirect at runtime ONLY when SP still holds the silicon-reset value
* 0x1100 (i.e., the very first reach of 0xff80 after silicon-reset,
* before any STM #imm,SP has run). See c54x_run main loop for the
* runtime check. */
/* Boot ROM stubs at 0x0000-0x007F.
* Discriminant test 2026-04-26 confirmed FRET stub did NOT block the
* firmware path to 0x0810 (reverting to NOPs gave identical PC HIST
* + same IMR change=0). FRET stub kept: prevents stack runaway when
* CALAA targets the stub area, with no downside.
*
* Fallback per slot (2026-05-29 v3 — RET@0x0000 équilibre le near-CALA) :
* - 0x0000/0x0001: RET (0xFC00) — pop ret_pc, retour ÉQUILIBRÉ.
* - rest (0x02..0x7F): FRET (0xF4E4) — retour-from-far (far-call→stub).
*
* Pourquoi RET@0x0000 (et pas IDLE/LDMM) : le firmware fait des near-CALA
* `CALA → 0x0000` avec A=0 (chemin handler-nul/défaut ; ex. LDU@0xfa7e lit
* un ptr de table = 0 → CALA A=0). Un near-CALA push 1 mot (ret_pc) ; RET
* pop 1 mot → ÉQUILIBRÉ → retour propre au caller → le boot continue.
* - IDLE (v2) ne retournait pas → halt+slide 0x0000→0x0002 → FRET-loop
* → fuite SP (0x1106→0x4d75).
* - LDMM SP,B+RET (old/good 2026-05-28) retournait MAIS posait SP=B :
* si B=0 → SP=0 → pop garbage ("wake-on-IRQ" loop de v1).
* Le rest reste FRET : les far-calls (FCALA, push 2) qui tombent dans la
* zone sont équilibrés par FRET (pop 2). Le firmware idle à son vrai
* point (IDLE de la table TDMA slots, cf doc/DSP_ROM_MAP.md). */
for (int i = 0; i < 0x80; i++)
s->prog[i] = 0xF4E4; /* FRET — retour-from-far (far-call-into-stub) */
s->prog[0x0000] = 0xFC00; /* RET — pop ret_pc, équilibre le near-CALA→0 */
s->prog[0x0001] = 0xFC00; /* RET — idem */
/* 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 (2026-05-25) — permet attribution causale dans
* les rapports/bundles. Cf review Claude web : "Le rapport ne peut pas
* s'attribuer à un état de code". On dump ici les valeurs reset
* silicon-aligned utilisées par CE binaire — si elles changent, le
* comportement firmware change. Lecture de qemu.log = identité du build. */
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 : si ces fixes sont retirés du source, ce log
* n'apparaîtra plus ou aura un format différent — preuve immédiate
* de quel binaire produit le 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 — diag source INT3
(RO) */
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; / VEC28-EXP (2026-06-25, gated CALYPSO_DSP_FRAME_VEC28)
: l’IT frame du modele * tape sur vec19/bit3 = stub RETE. Le VRAI
scheduler per-frame est vec28/bit12 * (0x7234 -> CALL 0xa4e4 -> LD
d_dsp_page 0xa51c -> correlateur -> d_fb_det), * arme dans l’IMR
voulu du DSP (0x52fd, bit12=1, machine-verifie). On (a) remappe * l’IT
frame vers vec28/bit12, et (b) la force a VECTORISER (pas wake-only)
quand * une tache GSM est postee (d_dsp_page bit1 = B_GSM_TASK) : la
1ere vectorisation * atteint go-live qui ARME IMR=0x52fd via 0xa582
-> ensuite tout vectorise seul. * Garde d_dsp_page : seulement APRES
que l’ARM a poste (boot DSP fini) -> pas de * derail boot. Fidele :
on corrige le mapping ligne-frame-TPU -> vecteur DSP + * une ligne
cablee vectorise ; aucun poke d’IMR/table/d_fb_det. / bool
frame_force = false; { / @BEQUILLE —
VEC28_REMAP / FRAME_IT_NATIVE (CALYPSO_DSP_FRAME_VEC28 ou *
CALYPSO_FRAME_IT_NATIVE ; le second est :=1 en
native/native_helped/wire) * masque : le mapping ligne-frame-TPU ->
vecteur DSP. Le modele livre l’IT frame * sur vec19/bit3 (= stub RETE) ;
on la reroute vers vec28/bit12, et le * mode non-natif va jusqu’a FORCER
la vectorisation (frame_force). * retirer : quand calypso_tpu.c cable la
ligne frame sur le bon vecteur a la * source et que la fenetre INTM du
firmware suffit a la prendre. * NB : CALYPSO_DSP_GOLIVE_BOOT lu plus bas
(g_noforce) inhibe VEC28-FORCE. / static int g_v28 = -1, g_native =
-1; if (g_v28 < 0) g_v28 = getenv(“CALYPSO_DSP_FRAME_VEC28”) ? 1 : 0;
if (g_native < 0) g_native = getenv(“CALYPSO_FRAME_IT_NATIVE”) ? 1 :
0; / [2026-07-22] CALYPSO_FRAME_IT_NATIVE : livraison PROPRE du
scheduler frame. * Remap l IT frame (vec19/bit3 = stub RETE) vers
vec28/bit12 (le vrai scheduler * HW frame-sync) et pose SEULEMENT IFR
bit12 -> prise naturelle par * c54x_irq_level_check quand INTM=0.
Remplace le frame_force crade de VEC28. / if ((g_v28 || g_native)
&& vec == C54X_INT_FRAME_VEC && imr_bit ==
C54X_INT_FRAME_BIT) { vec = 28; imr_bit = 12; static int g_noforce = -1;
if (g_noforce < 0) g_noforce = getenv(“CALYPSO_DSP_GOLIVE_BOOT”) ? 1
: 0; / [2026-07-22] FRAME-IT-PROBE (gated CALYPSO_FRAME_IT_PROBE) :
d_dsp_page * au moment de CHAQUE frame IT -> bit1 (B_GSM_TASK) set
ici ou pas ? / { static unsigned fitlog = 0, fithit = 0; int btask =
!!(s->data[0x08E2] & 2); if (getenv(“CALYPSO_FRAME_IT_PROBE”)
&& (fitlog < 20 || btask || (fitlog % 8000) == 0)) { if
(btask) fithit++; fprintf(stderr, “[c54x] FRAME-IT#%u d_dsp_page=0x%04x
(B_GSM_TASK=%d hits=%u)” “IMR=0x%04x INTM=%d idle=%d insn=%u”, fitlog,
s->data[0x08E2], btask, fithit, s->imr, !!(s->st1 &
ST1_INTM), s->idle, s->insn_count); } fitlog++; } if (!g_native
&& (s->data[0x08E2] & 0x0002) && !g_noforce) {
/ B_GSM_TASK pending, hors mode golive (frame_force = VEC28 crade,
PAS en mode natif) / frame_force = true; static unsigned fvlog = 0;
if (fvlog++ < 30) fprintf(stderr, “[c54x] VEC28-FORCE frame->vec28
d_dsp_page=0x%04x” “IMR=0x%04x idle=%d insn=%u”, s->data[0x08E2],
s->imr, s->idle, s->insn_count); } } } s->ifr |= (1 <<
imr_bit); if (imr_bit == 12 && frame_it_level_on())
g_frame_it_level = true; / arme le LEVEL hold frame */
/* SONDE INT3-RATE (2026-06-24 diag sur-delivrance) : chaque dispatch INT3
* (vec 19 = FRAME) avec le delta insn depuis le precedent. delta ~130 =
* sur-delivrance (BSP per-rafale) qui noie le DSP ; ~256000 = per trame
* (correct). Cap 80. */
if (vec == 19) {
static uint64_t last_i3 = 0;
static unsigned i3n = 0;
bool post_fb = s->insn_count > 160000u; /* ~fn 1206 : ordre FB livre */
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;
}
/* EXPÉRIENCE WIRE585F RETIRÉE 2026-05-30 : forcer data[0x585f] bit7 au frame-IRQ
* a prouvé (mem=0x0180 TC=1) que le mécanisme est sain MAIS que 0x585f n'est
* qu'UNE porte d'une chaîne (→ pose 0x3fd3 puis reboucle) — pas le verrou.
* Whack-a-mole démontré, pas supposé. cf [[feedback_debug_gate_heisenbug]]. */
bool unmasked = (s->imr & (1 << imr_bit)) != 0;
if (frame_force) unmasked = true; /* VEC28-EXP : ligne frame cablee -> vectorise */
/* @BEQUILLE — FIX_BRINT0_UNMASK (CALYPSO_FIXES=FIX_BRINT0_UNMASK, defaut OFF)
* masque : l absence d armement natif de l IMR bit 5 (BRINT0 / vec 21).
* retirer : des que la vraie branche d armement est implementee, OU
* immediatement si le test montre que la racine est en amont
* (vecteur 21 installe a zero).
* ⚠️ DIAGNOSTIC, a retirer, JAMAIS a confirmer. Ne compte pas comme un correctif.
* Repond a UNE question : BRINT0 est-elle le DERNIER verrou ou seulement le
* PROCHAIN ? Si le demasquage artificiel fait entrer le DSP dans le demod
* (CALYPSO_WATCH_9F00_RD passe de 0 a non-nul), c est le dernier ; sinon la
* racine est en amont — candidat : le vecteur 21 installe a 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;
}
/* [2026-07-23] SYNC-DISPATCH-PROBE (unconditional, capped) : c54x_interrupt_ex
* fait un dispatch SYNCHRONE ici (au moment de la levee) si INTM=0 -- sans
* log dedie contrairement a c54x_irq_level_check's "IRQ-LEVEL take". On veut
* savoir si BRINT0 (vec21) est en fait servi PAR CE CHEMIN, silencieusement,
* a chaque levee -- ce qui expliquerait "pend jamais vu par le poller" sans
* bug : les deux mecanismes ne se chevauchent simplement jamais dans le temps
* observe par LEVELCHK-EMPIRICAL. */
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 */
s->ifr &= ~(1 << imr_bit);
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); /* save XPC inconditionnel */
g_sp_ledger.irq_words_pushed++;
g_sp_ledger.irq_entries++;
g_sp_ledger.net_words += 2; /* PC+XPC poussés ici (hors ring exec) */
s->st1 |= ST1_INTM;
/* DISP-ENTRY : capture contexte préempté (DP foreground inchangé) */
g_last_intr_insn = s->insn_count; g_last_intr_vec = vec;
g_last_intr_fg_pc = (uint16_t)(s->pc + 1); g_last_intr_fg_dp = dp(s);
s->xpc = 0; /* fetch vecteur sur page 0 */
uint16_t iptr = (s->pmst >> PMST_IPTR_SHIFT) & 0x1FF;
s->pc = (iptr * 0x80) + vec * 4;
if (vec == 28) {
if (g_vec28_trace_en < 0)
g_vec28_trace_en = getenv("CALYPSO_TRACE_VEC28_STACK") ? 1 : 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.
* Garde delay_slots==0 (fix 2026-05-30) : faithful C54x — une IT
* n'est PAS reconnue entre une branche différée (RETD/RCD/CALLD/BD)
* et ses 2 delay-slots. Vectoriser mid-delay laisserait delay_slots
* armé → au retour (RETE) le commit delayed_pc se ferait dans le
* mauvais contexte → over-pop SP → DP garbage → self-CALA 0x70c3.
* IFR reste set (non clearé) → l'IT est servie au prochain appel,
* delay_slots étant retombé à 0 (max ~2 insns plus tard). */
/* FRAME-IT PRIO (gated CALYPSO_FRAME_IT_PRIO) : si la frame-IT (bit12/vec28)
* est latchee ET demasquee mais qu on s apprete a servir une IT de priorite
* plus basse (ex BRINT0 vec21, sur-livree par le BSP a chaque burst -> noie
* la frame), servir la FRAME d abord sur cette fenetre INTM=0. Le bit de l IT
* demandee reste pendant (non cleare, imr_bit ecrase) -> servie au prochain
* edge. Draine la frame-IT affamee (5908x pendante, 1x prise) -> vec28 ->
* scheduler 0x7234 -> dispatcher -> kernel FB. */
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; /* FIX livelock : relache le LEVEL hold (sinon bit12 re-asserte -> vec28 sur-fire 7x/trame -> 139k INTM-TRANS) */
}
s->ifr &= ~(1 << imr_bit);
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); /* save XPC inconditionnel */
g_sp_ledger.irq_words_pushed++;
g_sp_ledger.irq_entries++;
g_sp_ledger.net_words += 2; /* PC+XPC poussés ici (hors ring exec) */
s->st1 |= ST1_INTM;
/* DISP-ENTRY : capture contexte préempté (DP foreground inchangé) */
g_last_intr_insn = s->insn_count; g_last_intr_vec = vec;
g_last_intr_fg_pc = (uint16_t)s->pc; g_last_intr_fg_dp = dp(s);
s->xpc = 0; /* fetch vecteur sur page 0 */
uint16_t iptr = (s->pmst >> PMST_IPTR_SHIFT) & 0x1FF;
s->pc = (iptr * 0x80) + vec * 4;
if (vec == 28) {
if (g_vec28_trace_en < 0)
g_vec28_trace_en = getenv("CALYPSO_TRACE_VEC28_STACK") ? 1 : 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);
}
}
/* INT3-CYCLE-TRACE : hook cycle start for vec=19 (INT3 FRAME) */
if (vec == 19) {
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; }
void c54x_bsp_load(C54xState s, const uint16_t samples, int
n) { if (n > 2048) n = 2048; memcpy(s->bsp_buf, samples, n *
sizeof(uint16_t)); s->bsp_len = n; s->bsp_pos = 0;
/* 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 > 0 ? samples[0] : 0, n > 1 ? samples[1] : 0,
n > 2 ? samples[2] : 0, n > 3 ? samples[3] : 0,
n > 4 ? samples[4] : 0, n > 5 ? samples[5] : 0,
n > 6 ? samples[6] : 0, n > 7 ? samples[7] : 0);
}
}
================================================================================
FILE: hw/arm/calypso/calypso_dbg.c SIZE: 2832 bytes, 98 lines
================================================================================
/ Calypso QEMU runtime debug categories — implementation.
Parses CALYPSO_DBG env var once at init and exposes the mask via *
the global calypso_dbg_mask variable. The DBG() macro in calypso_dbg.h *
tests the mask before formatting/printing each log line.
SPDX-License-Identifier: GPL-2.0-or-later */ #include “qemu/osdep.h”
#include <stdio.h> #include <stdlib.h> #include
<string.h> #include “hw/arm/calypso/calypso_dbg.h”
uint32_t calypso_dbg_mask = 0;
static const struct { const char *name; enum calypso_dbg_cat cat; }
cat_table[] = { { “bsp”, DBG_BSP }, { “fb”, DBG_FB }, { “sp”, DBG_SP },
{ “corrupt”, DBG_CORRUPT }, { “unimpl”, DBG_UNIMPL }, { “hot”, DBG_HOT
}, { “xpc”, DBG_XPC }, { “call”, DBG_CALL }, { “f2”, DBG_F2 }, { “dump”,
DBG_DUMP }, { “boot”, DBG_BOOT }, { “l1ctl”, DBG_L1CTL }, { “trx”,
DBG_TRX }, { “pmst”, DBG_PMST }, { “rpt”, DBG_RPT }, { “mvpd”, DBG_MVPD
}, { “inth”, DBG_INTH }, { “tint0”, DBG_TINT0 }, }; #define N_CATS
(sizeof(cat_table) / sizeof(cat_table[0]))
static const uint32_t default_mask = (1u << DBG_CORRUPT) | (1u
<< DBG_UNIMPL);
static int once = 0;
void calypso_dbg_init(void) { if (once) return; once = 1;
const char *env = getenv("CALYPSO_DBG");
if (!env) {
calypso_dbg_mask = default_mask;
fprintf(stderr, "[dbg] CALYPSO_DBG unset → default (corrupt,unimpl)\n");
return;
}
if (!*env || strcmp(env, "none") == 0) {
calypso_dbg_mask = 0;
fprintf(stderr, "[dbg] CALYPSO_DBG=none → all silent\n");
return;
}
if (strcmp(env, "all") == 0) {
calypso_dbg_mask = (1u << DBG__COUNT) - 1;
fprintf(stderr, "[dbg] CALYPSO_DBG=all → every category enabled\n");
return;
}
/* Parse comma-separated list. */
char buf[512];
snprintf(buf, sizeof(buf), "%s", env);
calypso_dbg_mask = 0;
char *tok = strtok(buf, ",");
while (tok) {
/* trim leading spaces */
while (*tok == ' ') tok++;
size_t l = strlen(tok);
while (l > 0 && (tok[l-1] == ' ' || tok[l-1] == '\n')) tok[--l] = 0;
int found = 0;
for (size_t i = 0; i < N_CATS; i++) {
if (strcasecmp(tok, cat_table[i].name) == 0) {
calypso_dbg_mask |= (1u << cat_table[i].cat);
found = 1;
break;
}
}
if (!found && *tok)
fprintf(stderr, "[dbg] unknown category '%s'\n", tok);
tok = strtok(NULL, ",");
}
/* Always force corrupt + unimpl on unless explicit "none". */
calypso_dbg_mask |= default_mask;
fprintf(stderr, "[dbg] CALYPSO_DBG=%s → mask=0x%08x\n", env, calypso_dbg_mask);
}
================================================================================
FILE: hw/arm/calypso/calypso_debug.c SIZE: 3731 bytes, 121 lines
================================================================================
/ calypso_debug.c — env-gated probe lookup implementation
Une seule env : CALYPSO_DEBUG=“probe1,probe2,probe3,…” * Valeur
spéciale : “ALL” (ou contenant “ALL”) active tout. Probes
lookup : O(N) sur la liste parsée une fois au premier appel. * Pour
10-20 probes typiquement actifs, c’est négligeable.
Normalisation : nom de probe upper-case, ‘-’/’ ‘/’.’/‘/’ → ’_’. * Ex :
“IMR-W” matche entry “IMR_W” comme “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 = pas encore init, 0 = CALYPSO_DEBUG vide (toutes
sondes * OFF, fast-path inliné dans le header), 1 = au moins une sonde
active. */ int calypso_debug_master = -1;
/* Normalize probe name in-place : upper-case, separators → ‘’.
/ 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]);
}
}
/* Init unique du master gate : parse l’env et fixe
calypso_debug_master. * Appelé depuis l’inline calypso_debug_enabled()
du header au 1er passage. */ 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); }
/* Impl réelle (out-of-line). N’est atteinte que quand master == 1,
donc * parse_env_locked a déjà tourné — le bloc !s_inited reste par
sûreté. / 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;
}
================================================================================
FILE: hw/arm/calypso/calypso_dsp_helper.c SIZE: 45686 bytes, 801 lines
================================================================================
/ calypso_dsp_helper.c — mode-neutral NDB-write PRIMITIVES for
the Calypso * DSP shunt. Split out VERBATIM from calypso_dsp_shunt.c
(pure mechanical * move, no logic change). The shunt GLUE lives in
calypso_dsp_shunt.c and * DEFINES the shared state (g_shunt / g_canned)
declared in the internal * header below. */
#include “qemu/osdep.h” #include “hw/arm/calypso/calypso_trf6151.h”
#include “hw/arm/calypso/calypso_dsp_internal.h”
/* CALYPSO_DSP=c54x : route les ordres+I/Q vers le VRAI c54x (pas de
mock). * getenv lu une seule fois (idiome memoize du fichier). /
bool shunt_route_c54x(void) { static int v = -1; if (v < 0) { const
char e = getenv(“CALYPSO_DSP”); v = (e && strcmp(e, “c54x”)
== 0) ? 1 : 0; } return v; }
/* Tag de log : en mode no-shunt/c54x le shunt agit en ASSIST ->
ne pas * afficher [feed-daram-dsp] (trompeur). SHUNT_LOG/SHUNT_ERR
prefixent le tag runtime. / const char shunt_tag(void) { return
shunt_route_c54x() ? “[dsp/c54x]” : “[feed-daram-dsp]”; }
/* —- Helpers : read/write API RAM via AddressSpace (16-bit LE) —-
/ uint16_t shunt_read_w(uint32_t addr) { uint16_t v = 0; /
[2026-07-27] AS-NULL guard : le shunt peut etre inactif (g_shunt.as
jamais affecte) et des appelants natifs non gardes passent quand meme
ici -> SIGSEGV. */ if (!g_shunt.as) return 0;
dma_memory_read(g_shunt.as, addr, &v, sizeof(v),
MEMTXATTRS_UNSPECIFIED); return le16_to_cpu(v); }
void shunt_write_w(uint32_t addr, uint16_t v) { uint16_t le =
cpu_to_le16(v); /* [2026-07-27] AS-NULL guard : le shunt peut etre
inactif (g_shunt.as jamais affecte) et des appelants natifs non gardes
passent quand meme ici -> SIGSEGV. */ if (!g_shunt.as) return;
dma_memory_write(g_shunt.as, addr, &le, sizeof(le),
MEMTXATTRS_UNSPECIFIED); }
/* Lit l1s.current_time.fn (FN L1 du firmware) en ARM RAM.
current_time = champ 0 * de struct l1s_state @ 0x836508 ; fn = champ 0
de struct gsm_time -> offset 0. * C’est LE FN que le firmware utilise
pour ses blocs (BCCH/CCCH) et mémorise pour * la RACH. On gate la
présentation a_cd dessus (et NON s->fn = calypso_trx_get_fn, * qui
diffère de l1s d’un offset run-variant -> blocs CCCH décalés ->
AGCH raté). / / [2026-07-27] Resolution DYNAMIQUE d’un symbole
du firmware ELF (robuste aux * rebuilds). Chemin = env
CALYPSO_FIRMWARE_ELF, sinon l’arg -kernel de * /proc/self/cmdline. Parse
ELF32 LE .symtab. Retourne 0 si introuvable. / static uint32_t
shunt_fw_sym(const char want) { char path[1024]; path[0] = 0; const
char env = getenv(“CALYPSO_FIRMWARE_ELF”); if (env &&
env) { snprintf(path, sizeof(path), “%s”, env); } else { FILE
cf = fopen(“/proc/self/cmdline”, “rb”); if (cf) { static char
cl[16384]; size_t nr = fread(cl, 1, sizeof(cl) - 1, cf); fclose(cf);
cl[nr] = 0; for (size_t i = 0; i < nr; ) { size_t l = strlen(cl + i);
if (!strcmp(cl + i, “-kernel”) && i + l + 1 < nr) {
snprintf(path, sizeof(path), “%s”, cl + i + l + 1); break; } i += l + 1;
} } } if (!path[0]) return 0; 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)
((uint32_t)b[o] | ((uint32_t)b[(o)+1] << 8) | ((uint32_t)b[(o)+2]
<< 16) | ((uint32_t)b[(o)+3] << 24)) 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
/ 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); uint32_t val
= R32(symoff + o + 4); if (ni < strsz) { const char nm = (const
char )(b + stroff + ni); if (!strcmp(nm, want)) { ret = val; break;
} } } break; } } #undef R16 #undef R32 } g_free(b); return ret; }
uint32_t shunt_l1s_fn(void) { static uint32_t addr = 0; if (!addr) {
const char e = getenv(“CALYPSO_L1S_FN_ADDR”); if (e &&
e) addr = (uint32_t)strtoul(e, NULL, 0); else { addr =
shunt_fw_sym(“l1s”); if (!addr) addr = 0x836508; } } uint32_t v = 0; /*
[2026-07-27] AS-NULL guard : le shunt peut etre inactif (g_shunt.as
jamais affecte) et des appelants natifs non gardes passent quand meme
ici -> SIGSEGV. */ if (!g_shunt.as) return 0;
dma_memory_read(g_shunt.as, addr, &v, sizeof(v),
MEMTXATTRS_UNSPECIFIED); return le32_to_cpu(v); }
/* Lit last_rach.fn : le FN EXACT que le firmware a memorise pour la
DERNIERE RACH * (prim_rach.c:94 last_rach.fn = current_time.fn-1, pose
au tick l1s_tx_rach_resp) * et qu’il a envoye au mobile via
L1CTL_RACH_CONF (prim_rach.c:114). C’EST la valeur * que le mobile
compare a la req-ref de l’IMM ASSIGN (gsm48_rr.c:3372). La lire *
directement = match EXACT, sans le skew variable de g_rach_l1s_fn[ra]
(capture au * tick d_rach/cmd, -4 frames AVANT que le memo soit pose au
tick resp -> l’ecart * cmd<->resp varie par-RACH, c’est lui qui
faisait derailler tout adj fixe). * struct { uint32_t fn; uint16_t
band_arfcn; } last_rach @ 0x836500, fn @ offset 0. / uint32_t
shunt_last_rach_fn(void) { static uint32_t addr = 0; if (!addr) { const
char e = getenv(“CALYPSO_LAST_RACH_FN_ADDR”); if (e &&
e) addr = (uint32_t)strtoul(e, NULL, 0); else { addr =
shunt_fw_sym(“last_rach”); if (!addr) addr = 0x836500; } } uint32_t v =
0; / [2026-07-27] AS-NULL guard : le shunt peut etre inactif
(g_shunt.as jamais affecte) et des appelants natifs non gardes passent
quand meme ici -> SIGSEGV. */ if (!g_shunt.as) return 0;
dma_memory_read(g_shunt.as, addr, &v, sizeof(v),
MEMTXATTRS_UNSPECIFIED); return le32_to_cpu(v); }
uint32_t wp_base(uint8_t page_idx) { return page_idx ?
BASE_API_W_PAGE_1 : BASE_API_W_PAGE_0; } uint32_t rp_base(uint8_t
page_idx) { return page_idx ? BASE_API_R_PAGE_1 : BASE_API_R_PAGE_0;
}
bool shunt_is_canned(unsigned bit) { return (g_canned & bit) !=
0; }
/* [2026-07-22] Echo de d_burst_d pour RP_D_BURST_D. Le shunt echo le
burst * COMMANDE (WP_D_BURST_D), mais l1s_nb_resp attend le burst
DEMODULE (decale du * pipeline cmd->resp + toggle w_page/r_page)
-> mismatch systematique +1 * (BURST ID 2!=1). Gate
CALYPSO_SHUNT_BURST_M1=1 : echo (d_burst_d - 1) mod 4 * pour coller au
resp. Racine = le shunt echo le mauvais page/timing. / uint16_t
shunt_burst_echo(void) { / [2026-07-22] Phase-lock burst. Echoter
WP_D_BURST_D (l’horloge d_dsp_page) * DERIVE contre le schedule
per-burst du mobile -> BURST ID mismatch jittery. *
CALYPSO_SHUNT_BURST_FN=1 : calcule d_burst_d depuis le FN L1 REEL *
(shunt_l1s_fn(), l’horloge que le mobile lit) -> phase-locke. Offset
* ajustable CALYPSO_SHUNT_BURST_OFS (0..3) pour caler la phase. Defaut =
* echo WP (ancien comportement). / / @BEQUILLE — SHUNT_BURST_FN / _OFS / _M1
(CALYPSO_SHUNT_BURST_FN, _OFS, _M1) * masque : la derivation de
d_burst_d (0..3) depuis la fenetre TPU. Le shunt le * synthetise, soit
par echo de la commande ARM (+ofs), soit depuis * l1s_fn — aucune de ces
deux sources n’existe sur silicium. * retirer : quand la fenetre RX
TPU/BDLENA cadence le burst-id cote DSP (RANK2). * NB : SHUNT_BURST_M1
n’est consulte que si SHUNT_BURST_OFS est absente. / static int
fn_mode = -1, ofs = -99; if (fn_mode < 0) { / [2026-07-22] ECHO
= DEFAUT. Le fn ne peut PAS suivre un burst_id * block-relatif : les
blocs CCCH ne demarrent pas tous a 4-aligne * (starts
6,12,16,22,26,32,36,42,46 -> mix 0/2 mod 4) -> fn&3 chaotique.
* L echo suit la sequence de commande ARM (g_shunt.d_burst_d, capturee *
en shunt_latch_task) qui EST propre 0,1,2,3 par bloc. fn=experimental.
/ const char e = getenv(“CALYPSO_SHUNT_BURST_FN”); fn_mode = (e
&& e) ? atoi(e) : 0; } if (ofs == -99) { const char e =
getenv(“CALYPSO_SHUNT_BURST_OFS”); if (e && e) ofs =
atoi(e); else if (getenv(“CALYPSO_SHUNT_BURST_M1”)) ofs = -1; /
echo : la write page (commande ARM) precede le resp de +2 bursts *
(SCHED prim_rx_nb.c:213-214 : frame N -> resp(k)+cmd(k+2)) ->
ofs=-2. * Offset CONSTANT (pas jittery) : si residuel, c est -1 ou -3
(timing * intra-trame latch/resp), sweepable. -2 == +2 mod 4. */ else
ofs = fn_mode ? 2 : -2; } if (fn_mode) return
(uint16_t)(((int)shunt_l1s_fn() + ofs + 4) & 3); return
(uint16_t)((g_shunt.d_burst_d + ofs + 4) & 3); }
/* Valeur TOA pour a_*_demod[TOA] : cannée (23 = on-time) si CAN_TOA,
sinon le * TOA REEL mesuré par gr-gsm (sb_toa) dès qu’un SCH a été
décodé ; fallback 23 * tant qu’aucun SCH (pas 0 : évite de catastropher
l’alignement avant lock). */ int shunt_toa_val(void) { if
(shunt_is_canned(CAN_TOA)) return SHUNT_CANNED_TOA; return
g_shunt.sb_valid ? g_shunt.sb_toa : SHUNT_CANNED_TOA; }
/* Pack {bsic, t1, t2, t3} into 32-bit sb (inverse of
prim_fbsb.c:125-144). */ uint32_t shunt_encode_sb(uint8_t bsic, uint16_t
t1, uint8_t t2, uint8_t t3) { 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;
}
/* —- DISPATCH : FB writes NDB only —- / void
shunt_dispatch_fb(uint8_t page_idx) { / @BEQUILLE — INJECT_FB (CALYPSO_INJECT_FB=1, EQ1
strict — PAS de fallback * CALYPSO_SHUNT_LEGIT, contrairement aux 5
autres INJECT_) masque : la publication du resultat FB
(d_fb_det + a_sync_demod * TOA/PM/ANGLE/SNR) par le correlateur DSP. *
retirer : quand d_fb_det natif est produit. * NB : cette variable n’est
posee dans AUCUN .env livre -> shunt_dispatch_fb() * est un no-op
dans tous les profils, et avec lui le bloc SHUNT_REAL_FB * ci-dessous.
Le FB passe en realite par calypso_dsp_shunt.c * (api_ram[0x08F8..] +
real_fb_read). */ { static int _ginj = -1; if (_ginj < 0) { const
char *_e = getenv(“CALYPSO_INJECT_FB”); _ginj = (_e && *_e ==
‘1’) ? 1 : 0; } if (!ginj) return; } /* [2026-07-23] HACK injection
sortie, DEFAUT OFF (natif) ; =CALYPSO_INJECT_FB=1 pour reactiver /
/ [2026-07-22] REAL FB (gate CALYPSO_SHUNT_REAL_FB) : injecte les
valeurs * REELLES calculees depuis la RX (g_shunt.rx) au lieu des
cannes. / { static int real_fb = -1; /* @BEQUILLE — SHUNT_REAL_FB (helper)
(CALYPSO_SHUNT_REAL_FB, defaut OFF) * masque : idem — detection FB cote
hote a la place du correlateur DSP. * retirer : quand d_fb_det natif
fonctionne. / if (real_fb < 0) { const char e =
getenv(“CALYPSO_SHUNT_REAL_FB”); real_fb = (e && e == ‘1’) ?
1 : 0; } if (real_fb) { shunt_write_w(BASE_API_NDB + NDB_D_FB_DET,
g_shunt.rx_fb_det ? 1 : 0); shunt_write_w(BASE_API_NDB +
NDB_A_SYNC_DEMOD + D_TOA 2, g_shunt.rx_toa);
shunt_write_w(BASE_API_NDB + NDB_A_SYNC_DEMOD + D_PM * 2,
g_shunt.last_pm); shunt_write_w(BASE_API_NDB + NDB_A_SYNC_DEMOD +
D_ANGLE * 2, (uint16_t)g_shunt.rx_afc); shunt_write_w(BASE_API_NDB +
NDB_A_SYNC_DEMOD + D_SNR * 2, g_shunt.rx_snr);
shunt_write_w(rp_base(page_idx) + RP_D_TASK_MD, FB_DSP_TASK); return; }
} /* d_fb_det = 1 (“FOUND”). prim_fbsb.c:404 reads this from NDB. *
Canned CAN_FBDET = on force “trouvé” (pas de vrai détecteur FB ici).
/ / FBDET non-canné = état RÉEL de détection gr-gsm : “trouvé”
ssi un SCH a * été décodé (sb_valid). Avant lock → 0 (FB pas trouvé,
comme un vrai DSP). */ shunt_write_w(BASE_API_NDB + NDB_D_FB_DET,
(shunt_is_canned(CAN_FBDET) || g_shunt.sb_valid) ? 1 : 0);
/* a_sync_demod[4] @ NDB+0x4C, 4 consecutive 16-bit words. Read by
* read_fb_result (prim_fbsb.c:306-309) from NDB. Chaque mesure : valeur
* cannée si son token est dans CALYPSO_CANNED, sinon 0 (pas encore de
* vraie source → un-canner sans source casse, c'est voulu/visible). */
shunt_write_w(BASE_API_NDB + NDB_A_SYNC_DEMOD + D_TOA * 2, shunt_toa_val());
shunt_write_w(BASE_API_NDB + NDB_A_SYNC_DEMOD + D_PM * 2, shunt_is_canned(CAN_PM) ? SHUNT_CANNED_PM : g_shunt.last_pm);
shunt_write_w(BASE_API_NDB + NDB_A_SYNC_DEMOD + D_ANGLE * 2, shunt_is_canned(CAN_ANGLE) ? SHUNT_CANNED_ANGLE : 0);
shunt_write_w(BASE_API_NDB + NDB_A_SYNC_DEMOD + D_SNR * 2, (shunt_is_canned(CAN_SNR) || g_shunt.sb_valid) ? SHUNT_CANNED_SNR : 0);
/* Ack on the read page (echo). Not strictly required for the FB path
* (firmware reads d_fb_det from NDB, not read-page) but mirrors the
* real DSP's task-completion echo. */
shunt_write_w(rp_base(page_idx) + RP_D_TASK_MD, FB_DSP_TASK);
SHUNT_LOG("DISPATCH FB page=%u → d_fb_det=1 TOA=%d PM=0x%x "
"ANGLE=%d SNR=0x%x (NDB only)\n",
page_idx, SHUNT_CANNED_TOA, SHUNT_CANNED_PM,
SHUNT_CANNED_ANGLE, SHUNT_CANNED_SNR);
}
/* —- DISPATCH : SB writes READ PAGE only —- / void
shunt_dispatch_sb(uint8_t page_idx) { / @BEQUILLE — INJECT_SB (CALYPSO_INJECT_SB=1,
fallback CALYPSO_SHUNT_LEGIT=1) * masque : la production du burst SB
(BSIC/FN) par le DSP apres detection FCCH ; * ici l’encodage vient du
SCH decode par gr-gsm. * retirer : quand le correlateur natif enchaine
FB -> SB. */ { static int _ginj = -1; if (_ginj < 0) { const char
*_e = getenv(“CALYPSO_INJECT_SB”); _ginj = (_e && *_e == ‘1’) ?
1 : 0; if (!_ginj) { const char *_l = getenv(“CALYPSO_SHUNT_LEGIT”);
_ginj = (_l && *_l == ‘1’) ? 1 : 0; } } if (!_ginj) return; } /*
[2026-07-23] HACK injection sortie, DEFAUT OFF ; CALYPSO_INJECT_SB=1 OU
CALYPSO_SHUNT_LEGIT=1 (option3: ecrit le SB au format db_r read-page
natif) */ uint32_t rp = rp_base(page_idx);
/* gr-gsm (= le DSP) a-t-il poste un vrai SCH (BSIC/FN reels via UDP 4731) ?
* En mode no-canned (full-grgsm), tant qu'aucun SCH reel n'est arrive on ne
* dispatch PAS le SB : le firmware FBSB attend le vrai SCH, comme un vrai
* mobile. Pas de BSIC canne -> aucun masquage d'echec de decode. */
static int no_canned = -1;
if (no_canned < 0) {
const char *e = getenv("CALYPSO_SHUNT_NO_CANNED");
no_canned = (e && *e == '1') ? 1 : 0;
}
if (!g_shunt.sb_valid && no_canned) {
static unsigned waitlog = 0;
if (waitlog++ < 10)
SHUNT_LOG("SB: pas encore de SCH reel (gr-gsm) "
"-> pas de dispatch (no-canned, le firmware attend)\n");
return;
}
/* BSIC/FN : REELS (gr-gsm decode_sch) si dispo, sinon canned (legacy only).
* FN -> {t1,t2,t3} GSM : T1=FN/(26*51), T2=FN%26, T3=FN%51 (encode_sb derive T3'). */
uint8_t bsic = g_shunt.sb_valid ? g_shunt.sb_bsic : SHUNT_CANNED_BSIC;
uint32_t fn = g_shunt.sb_valid ? g_shunt.sb_fn : 0;
uint16_t t1 = (uint16_t)(fn / (26u * 51u));
uint8_t t2 = (uint8_t)(fn % 26u);
uint8_t t3 = (uint8_t)(fn % 51u);
/* a_sch[0] CRC bit clear = success (prim_fbsb.c:181, B_SCH_CRC=8).
* CAN_CRC canné = on FORCE le pass (0). Non-canné = pas de faux succès :
* sans vraie source CRC on écrit le bit d'échec → fail VISIBLE (le SB sera
* rejeté) au lieu de masquer. Défaut canné → pass → camping inchangé. */
shunt_write_w(rp + RP_A_SCH + 0 * 2,
(uint16_t)((shunt_is_canned(CAN_CRC) || g_shunt.sb_valid)
? 0x0000 : B_SCH_CRC)); /* pass RÉEL ssi SCH décodé */
/* sb = encode_sb(bsic, t1, t2, t3) → a_sch[3] | a_sch[4]<<16
* (prim_fbsb.c:198). Two separate 16-bit stores, both LE. */
uint32_t sb = shunt_encode_sb(bsic, t1, t2, t3);
shunt_write_w(rp + RP_A_SCH + 3 * 2, (uint16_t)(sb & 0xFFFF));
shunt_write_w(rp + RP_A_SCH + 4 * 2, (uint16_t)(sb >> 16));
/* a_sch[1] / a_sch[2] are unused by l1s_decode_sb; zero them. */
shunt_write_w(rp + RP_A_SCH + 1 * 2, 0x0000);
shunt_write_w(rp + RP_A_SCH + 2 * 2, 0x0000);
/* a_serv_demod[4] @ +0x10. read_sb_result reads from READ PAGE here,
* NOT NDB (prim_fbsb.c:148-151). Chaque mesure cannée/0 selon CALYPSO_CANNED. */
shunt_write_w(rp + RP_A_SERV_DEMOD + D_TOA * 2, shunt_toa_val());
shunt_write_w(rp + RP_A_SERV_DEMOD + D_PM * 2, shunt_is_canned(CAN_PM) ? SHUNT_CANNED_PM : g_shunt.last_pm);
shunt_write_w(rp + RP_A_SERV_DEMOD + D_ANGLE * 2, shunt_is_canned(CAN_ANGLE) ? SHUNT_CANNED_ANGLE : 0);
shunt_write_w(rp + RP_A_SERV_DEMOD + D_SNR * 2, (shunt_is_canned(CAN_SNR) || g_shunt.sb_valid) ? SHUNT_CANNED_SNR : 0);
/* Ack on read page. */
shunt_write_w(rp + RP_D_TASK_MD, SB_DSP_TASK);
SHUNT_LOG("DISPATCH SB page=%u → sb=0x%08x BSIC=%u FN=%u %s TOA=%d\n",
page_idx, sb, bsic, fn,
g_shunt.sb_valid ? "(gr-gsm REEL)" : "(canned legacy)", shunt_toa_val());
}
void shunt_dispatch_allc(uint8_t page_idx) { /* @BEQUILLE — INJECT_ACD (CALYPSO_INJECT_ACD=1,
fallback CALYPSO_SHUNT_LEGIT=1) * masque : l’etage NB du DSP qui devrait
remplir a_cd[0..14] (statut CRC + 23 * octets L2) apres demodulation
d’un bloc CCCH/BCCH. * retirer : quand le chemin natif correlateur ->
NB -> a_cd livre le bloc. * NB : garde d’entree de
shunt_dispatch_allc -> conditionne AUSSI toutes les * bequilles AGCH
/ SDCCH / SACCH / BCCH ci-dessous. */ { static int _ginj = -1; if (_ginj
< 0) { const char *_e = getenv(“CALYPSO_INJECT_ACD”); _ginj = (_e
&& *_e == ‘1’) ? 1 : 0; if (!_ginj) { const char *_l =
getenv(“CALYPSO_SHUNT_LEGIT”); _ginj = (_l && *_l == ‘1’) ? 1 :
0; } } if (!_ginj) return; } /* [2026-07-26] CALYPSO_INJECT_ACD=1 OU
SHUNT_LEGIT=1 (option3: SI3->a_cd) / / a_cd layout (cf
osmocom-bb prim_rx_nb.c) : * a_cd[0] = FIRE status bits
(B_FIRE0/B_FIRE1) -> 0x0000 = CRC pass * a_cd[1] = (reserved / BLUD
bit) -> 0x0000 * a_cd[2] = num_biterr -> 0x0000 * a_cd[3..14] = 23
bytes L2 frame (SI3 here) */ uint32_t addr_a_cd = BASE_API_NDB +
NDB_A_CD;
/* "sans hack" : CALYPSO_SHUNT_NO_CANNED=1 → on n'injecte JAMAIS le SI3
* canned. Tant que le démod réel (bridge gr-gsm via feed_si) n'a rien
* livré (si_valid=0), on ne dispatch rien → le firmware bail (pas de
* DATA_IND) → le mobile ne campe QUE sur le VRAI SI décodé de l'I/Q du
* BTS. C'est ça qui rend la victoire non-truquée : si le démod casse,
* rien ne campe (le bug est visible, pas masqué par le canned). */
static int no_canned = -1;
if (no_canned < 0) {
const char *e = getenv("CALYPSO_SHUNT_NO_CANNED");
no_canned = (e && *e == '1') ? 1 : 0;
}
if (no_canned && !g_shunt.si_valid)
return;
/* === AGCH (#11) : IMM ASSIGN présenté dans a_cd sur un bloc CCCH ===========
* Si un IMM ASSIGN est en attente, on le présente A LA PLACE du SI sur les
* blocs CCCH (combiné CCCH+SDCCH4 : fn%51 ∈ {6-9,12-19}). Le firmware, sur son
* read CCCH_COMB, tague chan_nr=0x90 -> gsm48_rr_rx_pch_agch -> rx_imm_ass ->
* gsm48_match_ra. Présenté sur CHAQUE bloc CCCH tant que valide (TTL) : le
* firmware le lit une fois, multi-présentation = robuste à l'alignement FN
* (RR dédup via cr_hist). Les SI restent inchangés (blocs BCCH). Tunables :
* CALYPSO_SHUNT_AGCH(=1 def), _AGCH_OFS (offset FN), _AGCH_TTL (ticks, def 100). */
/* @BEQUILLE — SHUNT_AGCH (+ _OFS / _TTL) (CALYPSO_SHUNT_AGCH, ON-sauf-0, defaut ON)
* masque : le decodage CCCH/AGCH par le DSP. On ecrit l'IMM-ASSIGN directement
* dans a_cd (statut CRC pass force) sur chaque bloc CCCH de la fenetre
* fn%51, avec TTL maison, au lieu que le DSP produise un bloc decode.
* retirer : quand le correlateur natif alimente a_cd et pose a_cd[0] reel.
* NB : SHUNT_AGCH_TTL a 3 consommateurs de semantiques differentes
* (peremption ici, expiry et fenetre de drop paging cote dsp_shunt.c).
*/
static int agch_on = -1, agch_ofs = 0, agch_ttl = 100;
if (agch_on < 0) {
const char *e = getenv("CALYPSO_SHUNT_AGCH"); agch_on = (!e || *e != '0') ? 1 : 0;
const char *o = getenv("CALYPSO_SHUNT_AGCH_OFS"); agch_ofs = o ? atoi(o) : 0;
const char *t = getenv("CALYPSO_SHUNT_AGCH_TTL"); if (t && *t) agch_ttl = atoi(t);
}
if (agch_on && g_shunt.agch_valid) {
if ((uint32_t)(g_shunt.tick_cnt - g_shunt.agch_tick) > (uint32_t)agch_ttl) {
g_shunt.agch_valid = false; /* périmé -> rendre la main aux SI */
} else {
/* gate sur le FN L1 FIRMWARE (l1s), pas s->fn : c'est l'horloge des
* vrais blocs CCCH du firmware -> alignement run-invariant. */
int tc = (int)((((long)shunt_l1s_fn() + agch_ofs) % 51 + 51) % 51);
int is_ccch = (tc >= 6 && tc <= 9) || (tc >= 12 && tc <= 19);
if (is_ccch) {
uint32_t aa = BASE_API_NDB + NDB_A_CD;
shunt_write_w(aa + 0, 0x0000); /* a_cd[0] FIRE = CRC pass */
shunt_write_w(aa + 2, 0x0000);
shunt_write_w(aa + 4, 0x0000);
const uint8_t *m = g_shunt.agch_buf;
for (int i = 0; i < 23; i += 2) {
uint8_t lo = m[i], hi = (i + 1 < 23) ? m[i + 1] : 0x2B;
shunt_write_w(aa + 6 + i, lo | (hi << 8));
}
uint32_t rpA = rp_base(page_idx);
shunt_write_w(rpA + RP_D_TASK_D, ALLC_DSP_TASK);
shunt_write_w(rpA + RP_D_BURST_D, shunt_burst_echo());
shunt_write_w(rpA + RP_A_SERV_DEMOD + D_TOA * 2, shunt_toa_val());
shunt_write_w(rpA + RP_A_SERV_DEMOD + D_PM * 2, shunt_is_canned(CAN_PM) ? SHUNT_CANNED_PM : g_shunt.last_pm);
shunt_write_w(rpA + RP_A_SERV_DEMOD + D_ANGLE * 2, 0);
shunt_write_w(rpA + RP_A_SERV_DEMOD + D_SNR * 2, SHUNT_CANNED_SNR);
static unsigned n_agch = 0;
if (n_agch++ < 40 || (n_agch % 50) == 0)
SHUNT_LOG("DISPATCH AGCH IMM-ASS #%u burst_d=%u "
"tc=%d -> a_cd (chan_nr=0x90 attendu)\n",
n_agch, g_shunt.d_burst_d, tc);
return; /* ce dispatch = l'IMM ASSIGN */
}
}
}
/* === SDCCH/4 SS0 DL (#2) : UA/AUTH presente dans a_cd sur le bloc SDCCH/4 ===
* Miroir EXACT de la branche AGCH ci-dessus. Si un bloc SDCCH DL est en
* attente (feed_sdcch), on le presente A LA PLACE du SI sur le bloc SDCCH/4
* SS0 (fn%51 in {22-25}). Le firmware (l1s_nb_cmd pose ALLC_DSP_TASK=24 pour
* TOUS les NB DL, SDCCH inclus) tourne MF_TASK_SDCCH4_0 a ce FN -> tague
* chan_nr=0x20 -> lapdm_dcch -> UA/AUTH -> L3. Gate sur shunt_l1s_fn() (FN L1
* firmware), PAS calypso_trx_get_fn(), comme l'AGCH. Tunables :
* CALYPSO_SHUNT_SDCCH(=1 def), _SDCCH_OFS (offset FN), _SDCCH_TTL (def 100). */
/* @BEQUILLE — SHUNT_SDCCH (+ _RING / _OFS / _TTL / _MAXPRES)
* (CALYPSO_SHUNT_SDCCH, ON-sauf-0, defaut ON)
* masque : idem AGCH pour le canal dedie — la ring rejoue le bloc L2 dans a_cd
* sur la fenetre fn%51 deduite de sdcch_ss, avec drop force apres
* MAXPRES presentations pour compenser un d_burst_d bloque.
* retirer : quand d_burst_d natif progresse 0->3 par bloc et que a_cd est
* alimente par le decodeur DSP.
*/
static int sdcch_on = -1, sdcch_ofs = 0, sdcch_ttl = 4000;
if (sdcch_on < 0) {
const char *e = getenv("CALYPSO_SHUNT_SDCCH"); sdcch_on = (!e || *e != '0') ? 1 : 0;
const char *o = getenv("CALYPSO_SHUNT_SDCCH_OFS"); sdcch_ofs = o ? atoi(o) : 0;
const char *t = getenv("CALYPSO_SHUNT_SDCCH_TTL"); if (t && *t) sdcch_ttl = atoi(t);
}
static int sdcch_ring_on = -1;
if (sdcch_ring_on < 0) { const char *e = getenv("CALYPSO_SHUNT_SDCCH_RING"); sdcch_ring_on = (!e || *e != '0') ? 1 : 0; }
/* [2026-07-27] g_shunt.sdcch_ss contient DIRECTEMENT la base DL fn%%51 de la
* voie dediee assignee (SDCCH/4 ou /8), calculee dans feed_agch. Fenetre DL =
* base..base+3 (NB_QUAD = 4 bursts). Remplace le hardcode SS0 (22-28). */
/* [2026-07-27] fenetre-UNION : shunt_dispatch_allc n'est appele que quand le
* firmware poste ALLC = a la sous-voie REELLE du mobile. On accepte donc l'UA
* sur toute la region SDCCH du type de canal (pas besoin de deviner SS) :
* SDCCH/4 : fn%%51 [22,39] (SS0-3) ; SDCCH/8 : [0,31] (SS0-7). */
int _lo = g_shunt.sdcch_ch8 ? 0 : 22;
int _hi = g_shunt.sdcch_ch8 ? 31 : 39;
if (sdcch_on && sdcch_ring_on) {
while (g_shunt.sdcch_ring_tail != g_shunt.sdcch_ring_head) {
uint32_t hidx = g_shunt.sdcch_ring_head % SDCCH_RING_N;
if ((uint32_t)(g_shunt.tick_cnt - g_shunt.sdcch_ring[hidx].tick) > (uint32_t)sdcch_ttl) {
g_shunt.evict_ttl++;
g_shunt.sdcch_ring[hidx].used = false; g_shunt.sdcch_ring_head++; continue;
}
int tc = (int)((((long)shunt_l1s_fn() + sdcch_ofs) % 51 + 51) % 51);
if (!(tc >= _lo && tc <= _hi)) break;
uint32_t aa = BASE_API_NDB + NDB_A_CD;
shunt_write_w(aa + 0, 0x0000); shunt_write_w(aa + 2, 0x0000); shunt_write_w(aa + 4, 0x0000);
const uint8_t *m = g_shunt.sdcch_ring[hidx].l2;
for (int i = 0; i < 23; i += 2) { uint8_t lo = m[i], hi = (i + 1 < 23) ? m[i + 1] : 0x2B; shunt_write_w(aa + 6 + i, lo | (hi << 8)); }
uint32_t rpA = rp_base(page_idx);
shunt_write_w(rpA + RP_D_TASK_D, ALLC_DSP_TASK);
shunt_write_w(rpA + RP_D_BURST_D, shunt_burst_echo());
shunt_write_w(rpA + RP_A_SERV_DEMOD + D_TOA * 2, shunt_toa_val());
shunt_write_w(rpA + RP_A_SERV_DEMOD + D_PM * 2, shunt_is_canned(CAN_PM) ? SHUNT_CANNED_PM : g_shunt.last_pm);
shunt_write_w(rpA + RP_A_SERV_DEMOD + D_ANGLE * 2, 0);
shunt_write_w(rpA + RP_A_SERV_DEMOD + D_SNR * 2, SHUNT_CANNED_SNR);
static unsigned n_sdcch = 0;
if (n_sdcch++ < 60 || (n_sdcch % 50) == 0)
SHUNT_LOG("DISPATCH SDCCH[ring] #%u fn=%u c=0x%02x burst_d=%u tc=%d depth=%u delta=%d\n",
n_sdcch, g_shunt.sdcch_ring[hidx].fn, m[1], g_shunt.d_burst_d, tc, g_shunt.sdcch_ring_tail - g_shunt.sdcch_ring_head,
(int)((int32_t)g_shunt.sdcch_ring[hidx].fn - (int32_t)shunt_l1s_fn()));
if ((n_sdcch % 20) == 0)
SHUNT_LOG("EVICT-STATS overflow=%u ttl=%u reps=%u\n", g_shunt.evict_overflow, g_shunt.evict_ttl, g_shunt.evict_reps);
/* [2026-07-27] anti-stall : drop garanti apres MAXPRES presentations
* meme si d_burst_d reste coince (mode DSP //) -> la ring draine, le
* UA frais n'est plus bloque derriere les blocs perimes. */
static int sdcch_maxpres = -1;
if (sdcch_maxpres < 0) { const char *e = getenv("CALYPSO_SHUNT_SDCCH_MAXPRES"); sdcch_maxpres = (e && *e) ? atoi(e) : 8; }
g_shunt.sdcch_ring[hidx].reps++;
int _by_reps = (g_shunt.sdcch_ring[hidx].reps >= (uint16_t)sdcch_maxpres);
if (g_shunt.d_burst_d >= 3 || _by_reps) {
if (_by_reps && g_shunt.d_burst_d < 3) g_shunt.evict_reps++;
g_shunt.sdcch_ring[hidx].used = false; g_shunt.sdcch_ring_head++;
}
if (g_shunt.sdcch_ring_tail == g_shunt.sdcch_ring_head) g_shunt.sdcch_valid = false;
return;
}
if (g_shunt.sdcch_ring_tail == g_shunt.sdcch_ring_head) g_shunt.sdcch_valid = false;
} else if (sdcch_on && g_shunt.sdcch_valid) {
if ((uint32_t)(g_shunt.tick_cnt - g_shunt.sdcch_tick) > (uint32_t)sdcch_ttl) {
g_shunt.sdcch_valid = false;
} else {
int tc = (int)((((long)shunt_l1s_fn() + sdcch_ofs) % 51 + 51) % 51);
if (tc >= _lo && tc <= _hi) {
uint32_t aa = BASE_API_NDB + NDB_A_CD;
shunt_write_w(aa + 0, 0x0000); shunt_write_w(aa + 2, 0x0000); shunt_write_w(aa + 4, 0x0000);
const uint8_t *m = g_shunt.sdcch_buf;
for (int i = 0; i < 23; i += 2) { uint8_t lo = m[i], hi = (i + 1 < 23) ? m[i + 1] : 0x2B; shunt_write_w(aa + 6 + i, lo | (hi << 8)); }
uint32_t rpA = rp_base(page_idx);
shunt_write_w(rpA + RP_D_TASK_D, ALLC_DSP_TASK);
shunt_write_w(rpA + RP_D_BURST_D, shunt_burst_echo());
shunt_write_w(rpA + RP_A_SERV_DEMOD + D_TOA * 2, shunt_toa_val());
shunt_write_w(rpA + RP_A_SERV_DEMOD + D_PM * 2, shunt_is_canned(CAN_PM) ? SHUNT_CANNED_PM : g_shunt.last_pm);
shunt_write_w(rpA + RP_A_SERV_DEMOD + D_ANGLE * 2, 0);
shunt_write_w(rpA + RP_A_SERV_DEMOD + D_SNR * 2, SHUNT_CANNED_SNR);
if (g_shunt.d_burst_d >= 3) g_shunt.sdcch_valid = false;
return;
}
}
}
/* === SACCH SDCCH/4 SS0 DL : presente le SI6 (B4) sur les slots SACCH du SS0 ===
* Sinon le mobile lit du garbage sur la SACCH dediee -> 'Short header 0x07'.
* Slots SACCH SS0 (combine CCCH+SDCCH/4, GSM 05.02) : fn%51 in {42-45} ET
* (fn/51)%2==0. Gate CALYPSO_SHUNT_SACCH (def ON). */
{
/* @BEQUILLE — SHUNT_SACCH (+ _PAR / _OFS) (CALYPSO_SHUNT_SACCH, ON-sauf-0, defaut ON)
* masque : la presentation du bloc SACCH dedie que le DSP devrait demoduler ;
* on rejoue sacch_buf dans a_cd sur une fenetre tc calculee a la main.
* retirer : quand le chemin natif demodule la SACCH.
*/
static int sacch_on = -1;
if (sacch_on < 0) { const char *e = getenv("CALYPSO_SHUNT_SACCH"); sacch_on = (!e || *e != '0') ? 1 : 0; }
if (sacch_on && g_shunt.sacch_have) {
long fn = shunt_l1s_fn();
int tc = (int)(((fn % 51) + 51) % 51);
int mf102 = (int)(((fn / 51) % 2 + 2) % 2);
/* [2026-07-26] FIX RLF-SACCH : le gate parite dur mf102==0 est
* FN-phase-fragile. l1s_fn se recale a l'execution (recale -556/-552),
* ce qui INVERSE la parite -> la SACCH dediee SS0 n'est plus presentee
* pendant des secondes (prouve : gap dispatch 723.28->732.18 avec
* sacch_have=true + 11 feed_sacch dans le trou) -> le mobile mesure
* rxlev=-110 sur ~50%% des blocs SACCH -> compteur radio-link epuise ->
* 'Radio link is released' (RLF) = mort dominante des LU. On rend la
* parite REGLABLE : CALYPSO_SHUNT_SACCH_PAR = 0(pair, legacy) / 1(impair)
* / 2(les deux, def) ; le firmware ne consomme que sur SON vrai bloc
* SACCH read, donc presenter sur les deux parites ne fait que garantir
* la presence quelle que soit la phase FN. CALYPSO_SHUNT_SACCH_OFS decale
* la fenetre tc si besoin. PAR=0 restaure le comportement d'origine. */
/* @BEQUILLE — SHUNT_SACCH_PAR (+ _OFS) (CALYPSO_SHUNT_SACCH_PAR, VALEUR, defaut 2)
* masque : PAR=2 presente sur LES DEUX parites mf102 parce que le recale de
* shunt_l1s_fn INVERSE la parite en cours de run — la bequille compense
* une derive d'horloge, pas une absence de decodage.
* retirer : quand l1s_fn ne subit plus de recale a l'execution (parite mf102
* stable) ; alors PAR=0 (parite paire seule) redevient correct.
*/
static int sac_par = -1, sac_ofs = 0;
if (sac_par < 0) {
const char *e = getenv("CALYPSO_SHUNT_SACCH_PAR"); sac_par = e ? atoi(e) : 2;
const char *o = getenv("CALYPSO_SHUNT_SACCH_OFS"); sac_ofs = o ? atoi(o) : 0;
}
int tco = (int)((((long)fn + sac_ofs) % 51 + 51) % 51);
int par_ok = (sac_par == 2) || (mf102 == sac_par);
if (tco >= 42 && tco <= 46 && par_ok) {
uint32_t aa = BASE_API_NDB + NDB_A_CD;
shunt_write_w(aa + 0, 0x0000);
shunt_write_w(aa + 2, 0x0000);
shunt_write_w(aa + 4, 0x0000);
const uint8_t *m = g_shunt.sacch_buf;
for (int i = 0; i < 23; i += 2) {
uint8_t lo = m[i], hi = (i + 1 < 23) ? m[i + 1] : 0x2B;
shunt_write_w(aa + 6 + i, lo | (hi << 8));
}
uint32_t rpA = rp_base(page_idx);
shunt_write_w(rpA + RP_D_TASK_D, ALLC_DSP_TASK);
shunt_write_w(rpA + RP_D_BURST_D, shunt_burst_echo());
shunt_write_w(rpA + RP_A_SERV_DEMOD + D_TOA * 2, shunt_toa_val());
shunt_write_w(rpA + RP_A_SERV_DEMOD + D_PM * 2, shunt_is_canned(CAN_PM) ? SHUNT_CANNED_PM : g_shunt.last_pm);
shunt_write_w(rpA + RP_A_SERV_DEMOD + D_ANGLE * 2, 0);
shunt_write_w(rpA + RP_A_SERV_DEMOD + D_SNR * 2, SHUNT_CANNED_SNR);
static unsigned n_sacch = 0;
if (n_sacch++ < 20 || (n_sacch % 50) == 0)
SHUNT_LOG("DISPATCH SACCH SI6 #%u tc=%d -> a_cd\n", n_sacch, tc);
return;
}
}
}
/* [FIX #3 corrige] Bloque le SI du camp UNIQUEMENT quand un SDCCH DEDIE est en
* attente (ring non-vide). PAS agch_valid : le PAGING (feed_agch) est continu
* pendant le camp et sa branche AGCH presente sur des blocs CCCH (!= blocs BCCH
* du SI) -> le bloquer tuait le SI -> camp casse. Le SDCCH n'est actif qu'en
* mode dedie (post-IMM-ASSIGN) -> sdcch_valid faux pendant le camp -> SI passe. */
if (g_shunt.sdcch_valid)
return;
/* (A) ROTATION par bloc : au début du bloc (burst 0) on avance au prochain
* type SI disponible et on le copie dans si_buf (STABLE pour les 4 bursts).
* Le mobile collecte ainsi TOUT le set (SI1/2/3/4) au fil des blocs au lieu
* du seul SI3. Round-robin = aucune dépendance FN (jitter-proof). */
if (g_shunt.d_burst_d == 0) {
for (int k = 1; k <= 6; k++) {
int s = (g_shunt.si_rr + k) % 6;
if (g_shunt.si_set_have[s]) {
memcpy(g_shunt.si_buf, g_shunt.si_set[s], 23);
g_shunt.si_rr = s;
break;
}
}
}
/* #12 ORDONNANCEMENT BCCH (no-hack) : présenter le SI UNIQUEMENT sur les
* blocs BCCH du multiframe-51 (TC = fn%51 ∈ [2,5]). Sur un bloc CCCH le SI3
* fuiterait en PCH/AGCH ("Unknown PCH/AGCH message"). d_fn = vraie FN (#4).
* Gated CALYPSO_SHUNT_BCCH_SCHED (défaut 1). */
/* @BEQUILLE — SHUNT_BCCH_SCHED (+ _OFS) (CALYPSO_SHUNT_BCCH_SCHED, EQ1, defaut OFF)
* masque : l'ordonnancement mf-51 que le DSP devrait imposer — on filtre a la
* main TC dans [2,5] pour eviter que le SI3 fuite en PCH/AGCH, avec une
* garde anti-famine qui degrade en "SI partout" apres 200 dispatches.
* retirer : quand le dispatcher natif presente a_cd sur le bon type de bloc.
* ATTENTION : le commentaire d'en-tete annonce "defaut 1" — PERIME, le code teste
* (e && *e=='1') donc le defaut est OFF.
*/
static int bcch_sched = -1, bcch_ofs = 0;
if (bcch_sched < 0) {
const char *e = getenv("CALYPSO_SHUNT_BCCH_SCHED");
bcch_sched = (e && *e == '1') ? 1 : 0; /* DEFAUT OFF (chan_nr pas le gate du camping) */
const char *o = getenv("CALYPSO_SHUNT_BCCH_OFS");
bcch_ofs = o ? atoi(o) : 0;
}
if (bcch_sched) {
/* FN = le device (vraie FN GSM de la BTS, alignée mf-51), PAS d_fn
* (que le firmware laisse à 0). Bloc BCCH non-combiné C0T0 = TC ∈ [2,5]
* (FCCH@0/10/.., SCH@1/11/.., BCCH@2-5, CCCH@6-9/12-15..). Offset
* réglable CALYPSO_SHUNT_BCCH_OFS si l'alignement dispatch≠bloc. */
static unsigned long n_disp = 0, n_bcch = 0, n_since_bcch = 0;
int tc = (int)((((long)calypso_trx_get_fn() + bcch_ofs) % 51 + 51) % 51);
int is_bcch = (tc >= 2 && tc <= 5);
n_disp++;
if (is_bcch) { n_bcch++; n_since_bcch = 0; } else n_since_bcch++;
if ((n_disp % 51) == 0)
SHUNT_LOG("#12 BCCH-sched: %lu disp / %lu BCCH "
"(tc=%d ofs=%d)\n", n_disp, n_bcch, tc, bcch_ofs);
/* Garde anti-famine : grace au boot (200 disp) + si 0 BCCH depuis 102
* dispatches (désalignement total) on présente quand même → dégrade
* vers "SI partout" au lieu de famine totale. */
if (!is_bcch && n_disp > 200 && n_since_bcch < 102) {
uint32_t addr0 = BASE_API_NDB + NDB_A_CD;
uint32_t rp_c = rp_base(page_idx);
shunt_write_w(addr0 + 0, 0x0003); /* a_cd[0] FIRE = CRC fail */
shunt_write_w(rp_c + RP_D_TASK_D, ALLC_DSP_TASK);
shunt_write_w(rp_c + RP_D_BURST_D, shunt_burst_echo());
return; /* pas de SI sur le CCCH */
}
}
/* a_cd[0..2] = status words. CAN_CRC canné = CRC pass (0) ; non-canné =
* pas de faux pass → FIRE=fail (0x0003) visible. a_cd[1/2] biterr = 0. */
shunt_write_w(addr_a_cd + 0,
(shunt_is_canned(CAN_CRC) || g_shunt.si_valid) ? 0x0000 : 0x0003); /* a_cd[0] FIRE : pass RÉEL ssi SI décodé */
shunt_write_w(addr_a_cd + 2, 0x0000); /* a_cd[1] */
shunt_write_w(addr_a_cd + 4, 0x0000); /* a_cd[2] */
/* a_cd[3..14] = 23B L2 frame, packé en 12 mots LE.
* Source : le SI RÉEL démodulé (gr-gsm ou C natif via feed_si) si dispo,
* sinon le SI3 canned (fallback). C'est le swap canned→réel = le "sans hack". */
const uint8_t *si = g_shunt.si_buf; /* no-hack : vrai SI grgsm seulement */
for (int i = 0; i < 23; i += 2) {
uint8_t lo = si[i];
uint8_t hi = (i + 1 < 23) ? si[i + 1] : 0x2B;
uint16_t w = lo | (hi << 8);
shunt_write_w(addr_a_cd + 6 + i, w); /* +6 = a_cd[3] base */
}
/* IMPORTANT : firmware prim_rx_nb.c:79 fait
* if (db_r->d_burst_d != burst_id) return 0;
* et attend la sequence burst 0,1,2,3 pour assembler la frame.
* On echo le d_burst_d que l'ARM a poste dans la read page pour que
* le check passe. Sinon le firmware bail avant dsp_memcpy_from_api()
* et n'envoie JAMAIS L1CTL_DATA_IND. */
/* [2026-07-22] DUAL-PAGE : le fix d'offset d_dsp_page a rendu page_idx
* alternant (avant il etait fige a 0 via le garbage 0xf600 = w_page&1). Or le
* mobile lit db_r[r_page] (r_page toggle INDEPENDAMMENT du w_page porte par
* d_dsp_page). On ecrit donc les champs read-page sur LES DEUX pages -> le
* mobile les lit quel que soit r_page. Gate CALYPSO_SHUNT_DUAL_PAGE (def ON). */
/* @BEQUILLE — SHUNT_CANNED + SHUNT_DUAL_PAGE (CALYPSO_SHUNT_CANNED, EXISTS -> "=0"
* l'ACTIVE ; CALYPSO_SHUNT_DUAL_PAGE, ON-sauf-0, defaut ON)
* masque : SHUNT_CANNED remplace a_serv_demod[PM] et [SNR] mesures par des
* constantes. SHUNT_DUAL_PAGE ecrit les champs read-page sur LES DEUX
* pages parce que le basculement de r_page cote lecture n'est pas
* modelise (on ne sait pas quelle page le mobile va lire).
* retirer : PM/SNR quand ils viennent du modele RF ou du DSP ; DUAL_PAGE quand
* r_page est deduit du protocole et non devine.
*/
static int canned_on = -1, dual = -1;
if (canned_on < 0) canned_on = getenv("CALYPSO_SHUNT_CANNED") ? 1 : 0;
if (dual < 0) { const char *ed = getenv("CALYPSO_SHUNT_DUAL_PAGE"); dual = (ed && *ed == '0') ? 0 : 1; }
for (int pg = 0; pg < 2; pg++) {
if (!dual && pg != page_idx) continue;
uint32_t rp = rp_base(pg);
shunt_write_w(rp + RP_D_TASK_D, ALLC_DSP_TASK);
shunt_write_w(rp + RP_D_BURST_D, shunt_burst_echo());
shunt_write_w(rp + RP_A_SERV_DEMOD + D_TOA * 2, shunt_toa_val());
shunt_write_w(rp + RP_A_SERV_DEMOD + D_PM * 2,
(canned_on || shunt_is_canned(CAN_PM)) ? SHUNT_CANNED_PM : g_shunt.last_pm);
shunt_write_w(rp + RP_A_SERV_DEMOD + D_ANGLE * 2, shunt_is_canned(CAN_ANGLE) ? SHUNT_CANNED_ANGLE : 0);
shunt_write_w(rp + RP_A_SERV_DEMOD + D_SNR * 2,
(canned_on || shunt_is_canned(CAN_SNR)) ? SHUNT_CANNED_SNR : g_shunt.rx_snr);
}
SHUNT_LOG("DISPATCH ALLC page=%u burst_d=%u -> SI3 a_cd[3..14] + a_serv_demod %s\n",
page_idx, g_shunt.d_burst_d, canned_on ? "CANNED(hack)" : "reel");
}
/* —- DISPATCH PM : tâche power-measurement (md=1). Écrit a_pm[3] @
+0x18, * que le power scan (l1s pm_cmd) lit pour dériver le rxlev. Sans
ça a_pm=0 → * rxlev=-110 (plancher) → la cellule est rejetée AVANT même
la sync, quel que * soit le SI. Valeur réglable via CALYPSO_SHUNT_PM
(défaut SHUNT_CANNED_PM, * haut → rxlev fort). C’est le pendant “scan”
du PM canné FB/SB. —- / void shunt_dispatch_pm(uint8_t page_idx) {
uint32_t rp = rp_base(page_idx); int pm_val; { / [2026-07-26 RANK5]
a_pm calibre via le modele trf6151 (gain vivant * suivi par TSP) : a_pm
= (rf_cible + system_inherent_gain + trf_gain)64, de sorte que
le firmware rapporte rf_cible dBm (rxlev fort) quel que * soit le gain
que l’AGC programme. CALYPSO_TRF_TARGET_RF (defaut -60). * Legacy :
CALYPSO_SHUNT_PM force une valeur brute a_pm (bypass modele). /
/ @BEQUILLE — SHUNT_PM
(CALYPSO_SHUNT_PM, VALEUR, defaut -1 = modele) * masque : la mesure de
puissance (tache PM md=1). Une valeur brute decretee est * ecrite dans
a_pm[0..2], court-circuitant meme le modele trf6151. * retirer : quand
le DSP produit a_pm depuis l’I/Q. * NB : shunt_dispatch_pm n’a AUCUN
gate INJECT_* — cette bequille est vivante * en NATIVE et NATIVE_HELPED
; seul SHUNT_NO_FAKE_PM=1 la coupe. / static int raw = -2; / -2
= pas encore lu / if (raw == -2) { const char e =
getenv(“CALYPSO_SHUNT_PM”); raw = (e && e) ? (int)strtol(e,
NULL, 0) : -1; / -1 = utiliser le modele / } if (raw >= 0) {
pm_val = raw; } else { / @BEQUILLE —
TRF_RXLEV + TRF_TARGET_RF (CALYPSO_TRF_RXLEV=1, fallback *
CALYPSO_SHUNT_LEGIT=1 ; cible defaut -60 dBm) * masque : a_pm que le
vrai DSP ecrit a 0 (aucune mesure). On substitue * apm_for_rf(TARGET_RF)
: le niveau RF cible est une constante decretee. * retirer : quand a_pm
natif est non nul. Le modele trf6151 (gain suivi par TSP) * reste
legitime — seule la CIBLE figee est la bequille. / static int trf =
-1, target = -60; if (trf < 0) { const char d =
getenv(“CALYPSO_TRF_RXLEV”); const char t =
getenv(“CALYPSO_TRF_TARGET_RF”); const char l =
getenv(“CALYPSO_SHUNT_LEGIT”); trf = ((d && d == ‘1’) || (l
&& l == ‘1’)) ? 1 : 0; if (t && t) target =
atoi(t); } pm_val = trf ? calypso_trf6151_apm_for_rf(target) :
SHUNT_CANNED_PM; } } shunt_write_w(rp + RP_A_PM + 0 2,
(uint16_t)pm_val); shunt_write_w(rp + RP_A_PM + 1 * 2,
(uint16_t)pm_val); shunt_write_w(rp + RP_A_PM + 2 * 2,
(uint16_t)pm_val); shunt_write_w(rp + RP_D_TASK_MD, PM_DSP_TASK); static
unsigned pm_log = 0; if (pm_log++ < 5) SHUNT_LOG(“DISPATCH PM page=%u
→ a_pm[0..2]=0x%04x (rxlev)”, page_idx, (uint16_t)pm_val); }
================================================================================
FILE: hw/arm/calypso/calypso_dsp_shunt.c SIZE: 134460 bytes, 2383 lines
================================================================================
/ calypso_dsp_shunt.c — DSP-side mock honoring the ARM↔︎DSP
API-RAM contract. When CALYPSO_DSP_SHUNT=1, the c54x emulator
is skipped entirely (no opcode * execution, no INTM gymnastics, no
DARAM-side compute). This file replaces * the DSP by a thin state
machine that respects the only protocol the ARM * firmware actually
sees: 1. ARM writes a task descriptor into W_PAGE_(w_page) —
d_task_d / * d_task_md / d_task_ra / d_burst_d / d_fn / … * 2. ARM
signals “go” by writing 0xFFD001A8 (NDB+0 = d_dsp_page) with * bit 1
(B_GSM_TASK) set; bit 0 carries the page index. * 3. DSP (= us) consumes
the task, computes the result, writes: * - FB result into NDB: d_fb_det
@+0x48, a_sync_demod[4] @+0x4C * - SB result into R_PAGE_(page_idx):
a_sch[5] @ +0x1E, a_serv_demod * [4] @ +0x10 * then the result is
visible at the NEXT TDMA frame. * 4. No separate “DSP done” IRQ: the TPU
FRAME IRQ (INTH bit 4) ticks * every 1ms and the ARM polls there.
Design notes (review by c-web 2026-05-26): * - Latch on write to
NDB+0, but SERVICE on the next FRAME IRQ tick. * This respects the ARM
firmware’s poste-then-wait-frame model and * gives multi-frame tasks (FB
search) a natural cadence. * - Disjoint write surfaces: FB goes to NDB
only, SB goes to READ PAGE * only. The fw’s read sites
(prim_fbsb.c:181/198/306/404) are the * ground truth. * - Offsets are
DWARF-validated against THE container ELF *
(/opt/GSM/firmware/board/compal_e88/layer1.highram.elf — sha256 *
27cd04…). NOT the host build — the container build was the one * loaded
by run.sh -kernel. Same offsets confirmed across both. * -
Canned phase 1 = dispatch each post on next FRAME IRQ. No * simulated
wide→narrow FB search; angle=0 keeps AFC loop from * iterating. TOA
tuned so synchronize_tdma yields bits_delta≈0. * - ALLC/NB UL/RA UL =
LOG_UNIMP. We don’t need them to clear * FBSB_CONF — those are
downstream of the current wall. */
#include “qemu/osdep.h” #include <math.h> #include “qemu/log.h”
#include “qemu/error-report.h” #include “exec/memory.h” #include
“exec/address-spaces.h” #include “hw/sysbus.h” #include “sysemu/dma.h”
#include “qemu/main-loop.h” #include “calypso_dsp_shunt.h” #include
“hw/arm/calypso/calypso_trf6151.h” #include
“hw/arm/calypso/calypso_twl3025.h” #include “calypso_c54x.h” /*
C54xState + c54x_bsp_load/run/interrupt_ex/wake (CALYPSO_DSP=c54x route)
/ #include “calypso_layer1.h” / calypso_l1_c_active() : ungate
SB/SI (+FB) sous CALYPSO_L1=c / #include
“hw/arm/calypso/calypso_dsp_internal.h” / shared state + NDB-write
primitives (split) / extern int g_c54x_int3_src; / diag source
INT3 (RO) */ #include <stdbool.h> #include <stdint.h>
#include <stdlib.h> #include <string.h> #include
<sys/socket.h> #include <netinet/in.h> #include
<arpa/inet.h> #include <unistd.h> #include <errno.h>
#include <sys/mman.h> #include <sys/stat.h> #include
<fcntl.h>
/* FN TDMA reelle (calypso_trx.c) pour recoder la FN du shunt (LATCH
d_fn=0) : * declaree dans calypso_dsp_internal.h (partagee avec le
helper). / extern void l1ctl_inject_dl_si(const uint8_t l2, int
l2len, uint32_t fn); /* FN-FIX : FN du dernier L1CTL_RACH_CONF (= memo
exact du mobile), capture dans * l1ctl_sock.c au moment de l’envoi au
mobile (race-free vs last_rach.fn@0x836500). / extern volatile uint32_t
g_last_rach_conf_fn; extern volatile uint32_t g_rach_conf_fn[256];
/ per-ra : FN exact du RACH_CONF keye par ra (defini l1ctl_sock.c)
*/
struct dsp_shunt_state g_shunt;
/* [2026-07-27] Value-list CALYPSO_SHUNT_LEGIT /
CALYPSO_SHUNT_NO_LEGIT : * =1 -> mode nu (rien de plus) * =DSP ->
lance AUSSI le DSP c54x en // (CALYPSO_DSP_RUN_C54X=1) * =NO_CANNED
-> mode sans cannes (CALYPSO_SHUNT_NO_CANNED=1) * =DSP,NO_CANNED
-> les deux (virgule/espace, casse libre) * Normalise UNE fois au
chargement (avant tout getenv cache statique) puis * canonicalise la
base a “1” pour que les ~20 checks existants (e==‘1’) restent
valides -> aucun site a modifier. / / @BEQUILLE — SHUNT_LEGIT / SHUNT_NO_LEGIT
(parapluies) (CALYPSO_SHUNT_LEGIT, * CALYPSO_SHUNT_NO_LEGIT ; EQ1 apres
canonicalisation) * masque : le mur RANK3 — le correlateur natif n’ecrit
jamais d_fb_det. Les * deux parapluies transportent la detection reelle
de gr-gsm vers le * resultat DSP (api_ram[0x08F8..0x08FD]), forcent
d_fb_det et a_pm cote * c54x, falsifient d_task_d / d_burst_d cote
lecture ARM, et servent de * FALLBACK implicite a une dizaine d’autres
gates (INJECT_, FEED_SI, TRF_RXLEV, UL_RACH_FROM_DRACH,
SHUNT_REAL_FB). * retirer : quand le correlateur natif pose d_fb_det
(RANK3 leve). * ATTENTION : ce constructeur fait des setenv() AVANT
main() — “=DSP” cree * CALYPSO_DSP_RUN_C54X=1 et “=NO_CANNED” cree
CALYPSO_SHUNT_NO_CANNED=1. * Ces variables apparaissent au manifeste
sans avoir ete tapees. * NB : SHUNT_NO_LEGIT n’est PAS un synonyme —
seuls 5 sites l’acceptent, * les autres ne retombent que sur
SHUNT_LEGIT. / static void attribute((constructor))
shunt_env_value_list(void) { static const char keys[2] = {
“CALYPSO_SHUNT_LEGIT”, “CALYPSO_SHUNT_NO_LEGIT” }; for (int k = 0; k
< 2; k++) { const char v = getenv(keys[k]); if (!v || !v ||
!strcmp(v, “0”)) continue; /* absent / off / if (strstr(v, “DSP”) ||
strstr(v, “dsp”)) setenv(“CALYPSO_DSP_RUN_C54X”, “1”, 1); / lance
le c54x en // / if (strstr(v, “NO_CANNED”) || strstr(v,
“no_canned”)) setenv(“CALYPSO_SHUNT_NO_CANNED”, “1”, 1); / mode
sans cannes / setenv(keys[k], “1”, 1); / canonicalise ->
checks e==‘1’ OK / } /* Manifeste de run : dump les CALYPSO_*
EFFECTIVES (post value-list) en tete * de log. Reproductibilite : ce
constructeur fait des setenv() AVANT main() * -> la config effective
differe de celle tapee ; on la trace. / { / [2026-07-27] C1
BUILD-STAMP : le 27/07 le binaire vivant a ete rebuilde * PENDANT le run
(inode supprime) et des heures ont ete perdues a comparer * des sources
disque avec un binaire different. L estampille de compilation * rend la
confusion impossible : elle est DANS le binaire qui tourne. */
fprintf(stderr, “[calypso-manifest] BUILD-STAMP compile le %s %s”,
DATE, TIME); fprintf(stderr,
“[calypso-manifest] ===== CALYPSO_* effectives (post value-list)
=====”); for (char **e = environ; e && e; e++) if
(!strncmp(e, “CALYPSO_”, 8)) fprintf(stderr, “[calypso-manifest]
%s”, *e); fprintf(stderr, “[calypso-manifest]
=================================================”); } }
/* SONDE B : table RA -> FN L1 firmware (l1s.current_time.fn) au
moment de la RACH. * Remplie par calypso_trx.c (hook write d_rach). Sert
à réécrire la req-ref de * l’IMM ASSIGN au FN exact que le mobile a
mémorisé (preuve que le FN = dernier mur). / static uint32_t
g_rach_l1s_fn[256]; volatile uint8_t g_last_recorded_ra = 0; /
per-ra FN-FIX : ra de la derniere RACH (lu par l1ctl_sock.c) /
static uint8_t g_rach_l1s_valid[256]; void
calypso_dsp_shunt_record_rach(uint8_t ra) { if (!g_shunt.active) return;
g_rach_l1s_fn[ra] = shunt_l1s_fn(); g_rach_l1s_valid[ra] = 1;
g_last_recorded_ra = ra; / per-ra FN-FIX : permet a l1ctl_sock de
keyer le RACH_CONF par ra */ }
/* SDCCH/SACCH UL sideband (#12) : QEMU publie la L2 montante
(a_cu[3..], 23o) vers * qemu_wrap via /dev/shm/calypso_sdcch_ul (fichier
régulier, pas un FIFO). qemu_wrap * l’encode (gsm0503_xcch) + module
(burst normal TSC7) + injecte sur le slot UL. * Layout 48o : seq@0(u32) l1s_fn@4(u32) fn@8(u32) task_u@12(u16) l1s%51@14(u8) l2[23]@16. / static void
calypso_sdcch_ul_publish(const uint8_t l2, uint16_t task_u,
uint32_t fn, uint32_t l1s_fn) { static int fd = -2; if (fd == -2) { fd =
open(“/dev/shm/calypso_sdcch_ul”, O_CREAT | O_RDWR, 0644); if (fd >=
0 && ftruncate(fd, 48) < 0) { /* best-effort / } } if (fd
< 0) return; / #2 PUBLISH-NO-IDLE : NE PAS republier la trame de
remplissage (UI, ctrl=0x03). * Le firmware poste pu_get_idle_frame()=01
03 01 dans a_cu entre les bursts SABM * (burst_id==0, rien en file L23).
Chaque publish bumpait seq -> ecrasait la SABM * transitoire (~4
frames) dans la slot unique du sideband AVANT que le consommateur *
(qemu_wrap, 1 pread/frame) ne l’echantillonne -> seul l’idle
remontait, jamais la * SABM (01 3f) -> osmo-bts jamais de SABM ->
jamais d’UA -> T200xN200 -> RR released. * En ne publiant QUE les
trames signalisantes (ctrl != 0x03), tout nouveau seq est * porteur et
ne peut plus etre clobbere par le fill -> la SABM tient jusqu’a ce
que * le consommateur sticky la capture. CALYPSO_UL_PUB_IDLE=1 retablit
l’ancien comportement. / / @BEQUILLE — UL_PUB_IDLE (CALYPSO_UL_PUB_IDLE,
EQ1, defaut OFF = filtre ACTIF) * masque : la profondeur du sideband UL
(slot unique, 1 pread/frame cote * qemu_wrap). Le filtre par defaut (ne
pas publier les trames de fill * ctrl==0x03) empeche l’ecrasement de la
SABM transitoire — il compense * l’absence de file. * retirer : quand le
sideband UL est une file (ring) et non un slot unique ; * alors publier
l’idle redevient sans risque. / static int pub_idle = -1; if
(pub_idle < 0) { const char e = getenv(“CALYPSO_UL_PUB_IDLE”);
pub_idle = (e && e == ‘1’) ? 1 : 0; } if (!pub_idle
&& l2[1] == 0x03) return; / trame de fill (UI) : ne pas
ecraser la signalisation / static uint32_t seq = 0; seq++; uint8_t
buf[48] = {0}; memcpy(buf + 4, &l1s_fn, sizeof(l1s_fn)); memcpy(buf
+ 8, &fn, sizeof(fn)); memcpy(buf + 12, &task_u,
sizeof(task_u)); buf[14] = (uint8_t)(l1s_fn % 51); memcpy(buf + 16, l2,
23); memcpy(buf + 0, &seq, sizeof(seq)); / seq en dernier
(publication) / if (pwrite(fd, buf, sizeof(buf), 0) < 0) { /
best-effort */ } }
static void shunt_poll_si_shm(void); /* fwd : poll SI shm
(gr-gsm→a_cd) / static bool shunt_grgsm_off(void); / fwd :
CALYPSO_SHUNT_NO_GRGSM */
/* —- LATCH : called on ARM write to NDB+0 (d_dsp_page) —- /
static void shunt_latch_task(uint16_t new_d_dsp_page) { if
(!(new_d_dsp_page & B_GSM_TASK)) { / [2026-07-27] d_dsp_page=0
= l1s_reset_hw() (fermeture canal dedie SMS/LU * ou Ctrl-C mobile).
Clear les latches IMM-ASS/SDCCH -> le gate SI se rouvre. * CHEMIN
VIVANT (l’ancien hook arm2dsp/trx.c 0x01A8 etait mort : d_dsp_page * vit
en API-RAM, pas en MMIO). Desactivable CALYPSO_L1_RESET_WIRE=0. / if
(new_d_dsp_page == 0) { / @BEQUILLE —
L1_RESET_WIRE (CALYPSO_L1_RESET_WIRE, ON-sauf-0, defaut ON) * masque :
la remise a zero d’etat que le DSP reel subit sur l1s_reset_hw(). * Ici
il n’y a pas d’etat DSP a reinitialiser, seulement des latches *
FABRIQUES par le shunt (IMM-ASSIGN, SDCCH) qui, non nettoyes, * bloquent
la reouverture du gate SI apres un Ctrl-C mobile. * retirer : avec les
latches eux-memes, c’est-a-dire avec les injections * INJECT_AGCH /
INJECT_SDCCH. / static int l1rst_on = -1; if (l1rst_on < 0) {
const char e = getenv(“CALYPSO_L1_RESET_WIRE”); l1rst_on = (e
&& e == ‘0’) ? 0 : 1; } if (l1rst_on) { static unsigned nrst
= 0; if (nrst++ < 30) SHUNT_LOG(“L1-RESET: d_dsp_page=0 -> clear
latches (SI revient)”); calypso_dsp_shunt_l1_reset(); } } return; /
not a real task signal (might be d_dsp_page=0 reset) */ }
uint8_t page_idx = (new_d_dsp_page & B_GSM_PAGE) ? 1 : 0;
uint32_t wp = wp_base(page_idx);
g_shunt.page_idx = page_idx;
g_shunt.d_task_d = shunt_read_w(wp + WP_D_TASK_D);
/* [2026-07-22] PERCMD : ne PAS ecraser d_burst_d ici (horloge scenario =
* aliasee, capture souvent cmd=0 debut de bloc) ; le mirror per-commande
* (calypso_dsp_shunt_wp_burst_write) en est le seul maitre -> X suit la
* vraie sequence 0,1,2,3. Gate off (=0) revient a l ancienne capture. */
/* @BEQUILLE — SHUNT_BURST_PERCMD (capture latch) (CALYPSO_SHUNT_BURST_PERCMD)
* masque : la derivation de d_burst_d (0..3) depuis la fenetre TPU. Le shunt
* la synthetise (echo de la commande ARM ou l1s_fn) au lieu de la lire
* du materiel.
* retirer : quand la fenetre RX TPU/BDLENA cadence le burst-id cote DSP (RANK2).
* PIEGE : idiome divergent entre les deux sites — ICI "(e && *e == 0) ? 0 : 1"
* (seule la chaine VIDE coupe) et dans wp_burst_write "*e == '0'"
* (la valeur "0" coupe). Poser =0 desarme le miroir ET laisse la
* capture desarmee : etat ni-l'un-ni-l'autre.
*/
{ static int pc = -1; if (pc < 0) { const char *e = getenv("CALYPSO_SHUNT_BURST_PERCMD");
pc = (e && *e == 0) ? 0 : 1; }
if (!pc) g_shunt.d_burst_d = shunt_read_w(wp + WP_D_BURST_D); }
g_shunt.d_task_u = shunt_read_w(wp + WP_D_TASK_U);
g_shunt.d_task_md = shunt_read_w(wp + WP_D_TASK_MD);
g_shunt.d_task_ra = shunt_read_w(wp + WP_D_TASK_RA);
g_shunt.d_fn = shunt_read_w(wp + WP_D_FN);
/* RECODE FN (#4) : le firmware poste souvent d_fn=0 (FBSB = recherche, pas
* de frame precise). On substitue la VRAIE FN TDMA pour le frame_nr aval
* (DATA_IND / sync). */
if (g_shunt.d_fn == 0)
g_shunt.d_fn = (uint16_t)(calypso_trx_get_fn() & 0xFFFF);
g_shunt.pending = true;
/* SDCCH/SACCH UL (#12 PIÈCE 1) : quand un NB UL est posté (d_task_u != 0,
* DUL_DSP_TASK=12 en dédié), lire la L2 a_cu[3..] (23o @ NDB 0x264+6, octets
* packés 2/mot) et la PUBLIER vers le sideband pour qemu_wrap (encode+module+
* injecte). a_cu[0..2]=header. La L2 porte le SABM / SACCH meas / I-frames. */
if (g_shunt.d_task_u != 0) {
uint8_t l2[23];
/* a_cu UL : l'offset exact de la trame LAPDm varie (header L1 SACCH 2o /
* type SABM-I-fill / packing) -> un offset fixe rate. On lit une FENETRE et
* on SCANNE le debut de trame : 1er octet = addr SDCCH valide (EA=1, SAPI 0/3)
* suivi d'un control non-fill. Base fenetre = gate CALYPSO_UL_ACU_OFS (def 6). */
static int acu_ofs = -1;
if (acu_ofs < 0) { const char *e = getenv("CALYPSO_UL_ACU_OFS"); acu_ofs = (e && *e) ? atoi(e) : 6; }
uint8_t win[30];
uint32_t wbase = BASE_API_NDB + 0x264u + (uint32_t)acu_ofs;
for (int i = 0; i < 30; i += 2) {
uint16_t w = shunt_read_w(wbase + i);
win[i] = (uint8_t)(w & 0xff);
if (i + 1 < 30) win[i + 1] = (uint8_t)((w >> 8) & 0xff);
}
int kk = -1;
for (int j = 0; j <= 6; j++) {
uint8_t a = win[j], c = win[j + 1];
int sapi = (a >> 2) & 7;
if ((a & 0x01) && (sapi == 0 || sapi == 3) &&
c != 0x2b && c != 0x00 && c != 0xff) { kk = j; break; }
}
if (kk < 0) kk = 0;
for (int i = 0; i < 23; i++) l2[i] = win[kk + i];
calypso_sdcch_ul_publish(l2, g_shunt.d_task_u,
calypso_trx_get_fn(), shunt_l1s_fn());
/* Log : quelques idle pour sanity (capé), mais TOUJOURS les frames NON-IDLE
* (ctrl=l2[1] != 0x03) -> capte le SABM (ctrl 0x3F) et les I-frames. */
static int ul_log = 0;
int non_idle = (l2[1] != 0x03);
if (non_idle || ul_log < 6) {
if (!non_idle) ul_log++;
SHUNT_LOG("SDCCH-UL%s task_u=0x%04x l1s%%51=%u "
"L2: %02x %02x %02x %02x %02x %02x %02x %02x\n",
non_idle ? " *NONIDLE*" : "", g_shunt.d_task_u,
(unsigned)(shunt_l1s_fn() % 51),
l2[0], l2[1], l2[2], l2[3], l2[4], l2[5], l2[6], l2[7]);
}
}
/* PM : valeur statique, écrite IMMÉDIATEMENT (pas de service déféré au
* prochain frame IRQ). Sinon le firmware lit a_pm AVANT le dispatch déféré
* → 0 stale → rxlev=-110. On écrit a_pm sur la page lue tout de suite. */
if (g_shunt.d_task_md == PM_DSP_TASK)
shunt_dispatch_pm(page_idx);
SHUNT_LOG("LATCH page=%u task_md=%u task_d=%u task_u=%u task_ra=%u fn=%u\n",
page_idx, g_shunt.d_task_md, g_shunt.d_task_d, g_shunt.d_task_u,
g_shunt.d_task_ra, g_shunt.d_fn);
}
/* —- Canned tuning —- TOA target : prim_fbsb.c does
last_fb->toa -= 23 then derives ntdma/qbits. * Picking
raw TOA=23 yields ntdma=0, qbits=0 → “perfectly on time”, which *
sidesteps the “DSP reports SB in bit that is N bits in the future” guard
* and the time_alignment becomes 0 (clean baseline for
synchronize_tdma). PM is shifted (>>3) by
read_fb_result / read_sb_result. 0x7000 raw → 0xE00 * after the shift,
well above any AFC/threshold. SNR is read raw and compared
against AFC_SNR_THRESHOLD. 0x7000 clears it * easily. ANGLE =
0 → ANGLE_TO_FREQ(0) = 0 → AFC correction null → the loop does * not
re-iterate looking for AFC convergence (c-web’s caution about * the AFC
loop spinning if angle is non-zero but unchanged). BSIC = 63
(max, matches osmo-bsc.cfg default
base_station_id_code 63). * t1=t2=t3=0 in encoded sb →
l1s_decode_sb yields time->fn = 0 (seeds the * mobile’s FN-counter at
zero, which is FN-agnostic for canned dispatch). * Real FN coherence is
a Phase 2 problem. / / —- CALYPSO_CANNED : énumère
EXPLICITEMENT chaque sortie DSP encore * FABRIQUÉE (canned) par le
shunt, au lieu de la cacher derrière une valeur * « plausiblement juste
». CSV insensible casse ; “FULL”/“ALL” = tout canné, * “NONE”/“=” vide =
rien. Var absente = DÉFAUT = CAN_DEFAULT (désormais RIEN * canné :
toutes les sorties sont pilotées par le vrai décode gr-gsm). On *
re-canne sélectivement avec CALYPSO_CANNED=. BSIC/SI ne sont PAS
ici : * déjà réels via gr-gsm / feed_si ; leur état est loggué au boot.
/ / TOUT DÉGATÉ (testés, camping tient), piloté par le vrai
état de décode gr-gsm : * FBDET = sb_valid (FB trouvé ssi SCH décodé) *
TOA = timing SCH réel gr-gsm * ANGLE = 0 (résidu réel post-correction
freq) * CRC = sb_valid/si_valid (pass ssi vraiment décodé, sinon fail) *
PM = 0 (a_serv_demod[PM] FB/SB ne pilote pas le rxlev — vient de
dispatch_pm) * SNR = sb_valid ? bon : 0 (gr-gsm a décodé = preuve SNR
suffisant ≥ seuil ; * magnitude pas mesurée mais conditionnée au vrai
décode, pas fabriquée * inconditionnellement → SI ne casse pas). *
DÉFAUT = RIEN canné. CALYPSO_CANNED= re-canne sélectivement ;
FULL=CAN_ALL. */
unsigned g_canned = CAN_DEFAULT; /* résolu dans
calypso_dsp_shunt_init */
static int can_tok_eq(const char a, const char b) { while
(a && b) { char ca = a, cb = b; if (ca >=
‘a’ && ca <= ‘z’) ca -= 32; if (cb >= ‘a’ && cb
<= ‘z’) cb -= 32; if (ca != cb) return 0; a++; b++; } return a ==
0 && b == 0; }
/* @BEQUILLE — CANNED (CALYPSO_CANNED,
LISTE ; vide/NONE = rien canne ; * defaut effectif = 0 dans tous les
profils livres) * masque : les sorties du correlateur/demodulateur DSP
non produites — * d_fb_det, TOA, PM, SNR, ANGLE, statut CRC — remplacees
par des * constantes plausibles (CAN_* dans calypso_dsp_internal.h). *
retirer : quand le correlateur natif produit ces six valeurs. La
variable est * deja a 0 partout : la retirer ne change que le
vocabulaire. / static unsigned shunt_parse_canned(void) { const char
e = getenv(“CALYPSO_CANNED”); if (!e) return CAN_DEFAULT; /* var
ABSENTE = défaut (= rien canné, tout réel) / if (!e ||
can_tok_eq(e, “NONE”)) return 0; /* “=” vide EXPLICITE = RIEN canné
/ if (can_tok_eq(e, “FULL”) || can_tok_eq(e, “ALL”)) return CAN_ALL;
char buf[160]; strncpy(buf, e, sizeof(buf) - 1); buf[sizeof(buf) - 1] =
0; unsigned m = 0; for (char t = strtok(buf, “,”); t; t =
strtok(NULL, “,”)) { if (can_tok_eq(t, “FBDET”)) m |= CAN_FBDET; else if
(can_tok_eq(t, “TOA”)) m |= CAN_TOA; else if (can_tok_eq(t, “PM”)) m |=
CAN_PM; else if (can_tok_eq(t, “SNR”)) m |= CAN_SNR; else if
(can_tok_eq(t, “ANGLE”)) m |= CAN_ANGLE; else if (can_tok_eq(t, “CRC”))
m |= CAN_CRC; else if (can_tok_eq(t, “FULL”) || can_tok_eq(t, “ALL”)) m
= CAN_ALL; else SHUNT_ERR(“CALYPSO_CANNED: token inconnu ‘%s’ ignore”,
t); } return m; }
/* Canned SI3 bytes — 23 L2-frame bytes (RR PD + SI3 mt + payload). *
Format conforme a osmocom-bb prim_rx_nb.c:154 : *
dsp_memcpy_from_api(rxnb.di->data, &dsp_api.ndb->a_cd[3], 23,
0); * Donc a_cd[0..2] = STATUS (CRC, biterr), a_cd[3..14] = 23B L2
frame. Layout L2+L3 RR SI3 : * [0]=0x49 LI=18 EL=1 [1]=0x06
RR PD [2]=0x1B SI3 mt * [3..4]=Cell ID * [5..7]=MCC/MNC encoded (0x00
0xF1 0x10 = MCC 001 MNC 01) * [8..9]=LAC * [10..11]=cell options + cell
select * [12..14]=RACH ctrl * [15..22] = padding 0x2B / /
SHUNT_CANNED_SI3_L2 RETIRÉ (no-hack 2026-06-03) : le SI vient *
UNIQUEMENT du vrai décode grgsm (g_shunt.si_buf via feed_si). */
static void shunt_dispatch_nb(uint8_t page_idx, uint16_t task_d) { /*
TODO : NB DL = decoded BCCH/CCCH burst payload into NDB a_cd[]. * NB UL
= consume burst bits from DARAM for TX (forwarded to bridge). */
SHUNT_LOG(“DISPATCH NB page=%u task_d=%u (TODO)”, page_idx, task_d);
}
/* —- TCH/F DL (JALON 1) : sideband /dev/shm/calypso_tch_dl ->
a_dd_0 —- * Producteur = qemu_wrap/gr-gsm (decode 8 bursts ->
gsm0503_tch_fr_decode -> 33o FR) ou * l’injecteur de test (tone 600).
Layout 48o : seq@0(u32 LE) fn@4(u32 LE) fr[33]@8. * Consume-once par seq (modele
calypso_rach_read). / static void calypso_tch_dl_poll(void) { static
int fd = -2; if (fd == -2) fd = open(“/dev/shm/calypso_tch_dl”, O_CREAT
| O_RDWR, 0644); / cree -> open une fois / if (fd < 0)
return; uint8_t buf[48]; if (pread(fd, buf, sizeof(buf), 0) !=
(ssize_t)sizeof(buf)) return; uint32_t seq; memcpy(&seq, buf, 4); if
(seq == 0 || seq == g_shunt.tch_dl_seq) return; / pas de nouvelle
trame */ g_shunt.tch_dl_seq = seq; memcpy(g_shunt.tch_dl_fr, buf + 8,
33); g_shunt.tch_dl_valid = true; }
/* Ecrit la trame FR 33o dans a_dd_0 (sub0). Le firmware
(prim_tch.c:322 sub0->a_dd_0) la * lit en fin-de-bloc (FN%13 in
{3,7,11}) ssi a_dd_0[0]&B_BLUD, puis L1CTL_TRAFFIC_IND->gapk. *
PIEGE #2 : packing BIG-ENDIAN intra-mot (dsp_memcpy_from_api BE=1,
dsp.c:278) = l’INVERSE * du a_cd existant (BE=0, lo|(hi<<8)). word
= (fr[i]<<8)|fr[i+1] ; mot impair = fr[32]<<8. / static
void shunt_dispatch_tch_dl(uint8_t page_idx) { (void)page_idx; /
a_dd_0 non page (T_NDB partage) / if (!g_shunt.tch_dl_valid) return;
uint32_t aa = BASE_API_NDB + NDB_A_DD_0; shunt_write_w(aa + 0, (1u
<< B_BLUD)); / a_dd_0[0] : B_BLUD=1, FIRE=00 (no error) /
shunt_write_w(aa + 2, 0x0000); / a_dd_0[1] : inutilise /
shunt_write_w(aa + 4, 0x0000); / a_dd_0[2] : num_biterr = 0 /
const uint8_t fr = g_shunt.tch_dl_fr; /* a_dd_0[3..] = 33o @ aa+6,
BE=1 / for (int i = 0; i < 32; i += 2) shunt_write_w(aa + 6 + i,
((uint16_t)fr[i] << 8) | fr[i + 1]); shunt_write_w(aa + 6 + 32,
(uint16_t)fr[32] << 8); / octet impair en poids fort */ }
/* CALYPSO_DSP=c54x : pilote le VRAI DSP depuis le frame tick du
shunt. * Les ordres (d_task_md/d_task_d/d_task_u/d_task_ra) sont DEJA
dans l’api_ram * partagee (c54x->api_ram == dsp_ram cote ARM), donc
pas de recopie du * descripteur ici. On (a) DMA la write-page API ->
DARAM 0x0586 (le trx skippe * cette DMA quand le shunt est actif, on la
refait nous-memes), (b) recharge le * dernier burst I/Q dans bsp_buf,
(c) leve INT3 (FRAME) + wake, (d) execute le * budget c54x_run.
FIX (verif report) : la write-page MCU->DSP est a
BASE_API_W_PAGE_0/1 * (0xFFD00000 / 0xFFD00028), PAS a BASE_API_NDB
(0xFFD001A8). On reutilise le * helper wp_base() existant. Replique la
DMA de trx calypso_dsp_done(@711) : *
data[0x0584]=page, data[0x0585]=fn, data[0x0586+i]=wp[i] (i<20), et
le mirror * api_ram[0x08E2 - C54X_API_BASE]=page (d_dsp_page cote DSP,
lu par le firmware). / / Lecture DIRECTE de l’espace data[] du
c54x pour une adresse API ARM, * SANS round-trip MMIO calypso_dsp_read
(qui prend calypso_pcb_daram_lock, * mutex non-recursif ->
re-lock/abort quand on est deja dans le contexte * frame-tick). Meme
mapping que calypso_dsp_read : ARM off O -> data[O/2+0x800]. /
static inline uint16_t shunt_c54x_api_rd(C54xState dsp, uint32_t
arm_addr) { return dsp->data[((arm_addr - 0xFFD00000UL) >> 1) +
0x0800]; }
/* [2026-07-24] CADENCE SPLIT : shunt_route_to_c54x() etait appelee
UNIQUEMENT * quand g_shunt.pending (= l’ARM vient de poster un NOUVEAU
d_dsp_page), soit * ~1 fois toutes les ~12 trames reelles en pratique
(mesure runtime : 708 insn * DSP/tour mais ~57ms reels/tour – le go-live
retry loop du DSP n’a donc * qu’une fraction de son temps reel pour
attraper la fenetre de survie de * data[0x0810]/d_ctrl_system,
contrairement au natif ou c54x_run tourne a * chaque trame SANS gate sur
un dispatch ARM frais). Le natif ne connait pas * ce probleme :
calypso_tdma_tick() appelle c54x_run() inconditionnellement * (gate
uniquement sur running/idle/shunt_active), jamais sur “tache neuve”.
Fix : separer le header (partie A, a besoin de page_idx/d_fn
FRAIS, donc * reste gate sur pending – cf le fix staleness du meme jour
juste au-dessus) * du wake+run (partie B, ne depend pas d’un dispatch
frais : rejoue le * dernier IQ connu, tire l’IT frame, tourne le budget
DSP – exactement ce * que native fait a chaque trame). Partie B devient
appelable a CHAQUE trame, * pending ou pas, pour retrouver une cadence
proche du natif (~217Hz au lieu * de ~17Hz). / static void
shunt_route_to_c54x_header(uint8_t page_idx) { C54xState dsp =
g_shunt.c54x; if (!dsp) return; fprintf(stderr, “[c54x-route] enter
page=%u dsp=%p”, (unsigned)page_idx, (void*)dsp);
/* (a) API write-page -> DARAM 0x0586 (replique de la DMA trx gatee a :711).
* wp_base(page_idx) = adresse MMIO absolue de la write-page (== dsp_ram).
* Le mot d_dsp_page (NDB+0 = 0xFFD001A8) est lu live (= s->dsp_ram[0x01A8/2]
* cote trx) pour data[0x0584] et le mirror 0x08E2. */
{
uint32_t wbase = wp_base(page_idx);
fprintf(stderr, "[c54x-route] a1 wbase=0x%08x\n", wbase);
/* [2026-07-24] FIX RACE : l'ancien lisait dsp->data[NDB_D_DSP_PAGE]=data[0x08E2]
* ICI-MEME (mid-frame), une cellule que data[0x08E2] toggle entre ce
* moment et le FRAME-IT qui suit dans CETTE MEME invocation (confirme
* runtime : a2 voit 186/186 fois 0x0000, FRAME-IT# voit 559/559 fois
* 0x0003, sur la MEME cellule -- pas un bug d'adresse, un bug de
* timing/staleness). page_idx (parametre) est DEJA la valeur fraiche,
* capturee au moment de l'ecriture ARM (cf ligne ~135 :
* page_idx = (new_d_dsp_page & B_GSM_PAGE) ? 1 : 0). On reconstruit
* dsp_page depuis cette valeur connue-fraiche au lieu de relire une
* cellule sujette a race, evitant de repropager du perime dans
* data[0x0584]/api_ram[0x08E2] ci-dessous. */
uint16_t dsp_page = B_GSM_TASK | page_idx;
fprintf(stderr, "[c54x-route] a2 dsp_page=0x%04x (from page_idx=%u, was: data[0x08E2]=0x%04x) "
"api_ram=%p\n",
dsp_page, (unsigned)page_idx, dsp->data[NDB_D_DSP_PAGE], (void*)dsp->api_ram);
dsp->data[0x0584] = dsp_page;
dsp->data[0x0585] = (uint16_t)(g_shunt.d_fn & 0xFFFF);
fprintf(stderr, "[c54x-route] a3 data-hdr-ok\n");
for (int i = 0; i < 20; i++)
dsp->data[0x0586 + i] = shunt_c54x_api_rd(dsp, wbase + (uint32_t)i * 2);
fprintf(stderr, "[c54x-route] a4 wp-copy-ok\n");
/* mirror d_dsp_page cote DSP (le firmware le lit a api_ram 0x08E2). */
if (dsp->api_ram)
dsp->api_ram[0x08E2 - C54X_API_BASE] = dsp_page;
}
fprintf(stderr, "[c54x-route] a-daram-ok\n");
}
static void shunt_route_to_c54x_run(void) { C54xState *dsp =
g_shunt.c54x; if (!dsp) return;
/* (b) rejoue le dernier burst I/Q (cs16 entrelace I,Q) dans bsp_buf. */
if (g_shunt.last_iq_valid && g_shunt.last_iq_n > 0)
c54x_bsp_load(dsp, (const uint16_t *)g_shunt.last_iq, g_shunt.last_iq_n);
fprintf(stderr, "[c54x-route] b-bsp-load-ok n=%d\n", g_shunt.last_iq_n);
/* (c) INT3 FRAME + wake : reveille le DSP s'il etait idle/halt. */
g_c54x_int3_src = 3;
/* [2026-07-22] FRAME_IT_NATIVE : tick propre — livre le scheduler frame
* (vec28/bit12) DIRECTEMENT au frame-tick, pas via le remap 19/3. Le
* c54x_irq_level_check le prend quand INTM=0 (prise naturelle, pas de force).
* = le vrai primitif HW frame-sync. Sinon (legacy) : vec19/bit3 (+remap VEC28). */
{
/* @BEQUILLE — FRAME_IT_NATIVE (CALYPSO_FRAME_IT_NATIVE, EXISTS ; :=1 en
* native / native_helped / wire)
* masque : l'absence de cablage frame-TPU -> vecteur DSP. On appelle
* directement c54x_interrupt_ex(dsp,28,12) au frame-tick au lieu de
* 19/3 (le stub vec19 est un RETE).
* retirer : quand le TPU delivre l'IT frame sur le bon vecteur tout seul
* (idem VEC28_REMAP cote calypso_c54x.c).
*/
static int fin = -1;
if (fin < 0) fin = getenv("CALYPSO_FRAME_IT_NATIVE") ? 1 : 0;
if (fin)
c54x_interrupt_ex(dsp, 28, 12); /* scheduler frame IT, tick propre */
else
c54x_interrupt_ex(dsp, C54X_INT_FRAME_VEC, C54X_INT_FRAME_BIT);
/* [2026-07-23] TINT0 MASTER CLOCK sync frame : fire TINT0 vec20/bit4 au
* MEME tick TDMA (pas per-2000-insn). Handler 0x72d3 = driver slots op. */
{
/* @BEQUILLE — TINT0_MASTER (fire au frame-tick) (CALYPSO_TINT0_MASTER, EXISTS,
* defaut OFF hors profil WIRE)
* masque : la configuration/demarrage du TIMER0 par le ROM. Le firmware arrete
* le timer (TSS=1) dans une init non-tournee ; on fabrique TINT0
* (vec20/bit4) a la cadence trame.
* retirer : quand la sequence d'init TIMER0 du ROM s'execute (TCR programme).
*/
static int _t0m = -1;
if (_t0m < 0) _t0m = getenv("CALYPSO_TINT0_MASTER") ? 1 : 0;
if (_t0m) c54x_interrupt_ex(dsp, 20, 4); /* TINT0 : vec20, IMR bit4 */
}
}
c54x_wake(dsp);
/* revive: c54x_run loop gate = (running && !idle). c54x_wake ne clear que
* idle ; en mode route_c54x le chemin trx qui posait running=true est gate
* off -> forcer running ici sinon la boucle c54x_run est sautee (0 insn). */
dsp->running = true;
fprintf(stderr, "[c54x-route] c-wake-ok running=%d idle=%d\n", dsp->running, dsp->idle);
/* (d) execute le budget (1 trame nominale ~256000 insns ; ajustable env). */
{
static int budget = -1;
if (budget < 0) {
const char *b = getenv("CALYPSO_DSP_BUDGET");
budget = (b && *b) ? atoi(b) : 256000;
if (budget <= 0) budget = 256000;
}
fprintf(stderr, "[c54x-route] d-pre-c54x_run budget=%d\n", budget);
c54x_run(dsp, budget);
fprintf(stderr, "[c54x-route] d-c54x_run-RETURNED\n");
}
}
/* —- Service hook : called from calypso_trx frame_irq tick —- /
/ [AFC loop-close v2] derniere mesure de frequence BRUTE du FCCH
(Hz), pour * recalculer rx_afc = brut - freq_deja_compensee_par_DAC a
CHAQUE tick (et pas * seulement quand feed_iq/det re-fire, ce qui cesse
une fois le DAC enroule). */ static double g_rx_raw_hz = 0.0; static int
g_rx_raw_valid = 0;
/* [DECAN PM 2026-07-26] rxlev a_pm depuis la VRAIE magnitude MAV
(last_pm) au lieu * de la cible -60 figee : rf_dbm =
20log10(last_pm/MAV_REF)+RF_REF, puis apm_for_rf (modele
trf6151). Ancrage MAV_REF->RF_REF (def 20929->-60 = niveau
courant) faute * de reference hardware ; la valeur SUIT desormais le
signal reel (fading/niveau). * Gate CALYPSO_DECAN_PM (ou master
CALYPSO_DECAN). OFF -> apm_for_rf(fallback). / / @BEQUILLE — DECAN_PM (+ _MAV_REF / _RF_REF)
(CALYPSO_DECAN_PM ou master * CALYPSO_DECAN, EQ1 ; OFF = bequille de
stabilisation) * masque : la mesure de puissance du DSP. ON : rf_dbm
derive de la magnitude * MAV via un ancrage arbitraire (MAV_REF ->
RF_REF). OFF : a_pm fige a * apm_for_rf(-60), une constante decretee. *
retirer : quand a_pm est produit par le DSP depuis l’I/Q (les deux
branches * disparaissent alors, ON comme OFF). / static uint16_t
shunt_pm_decan_apm(int fallback_target) { static int dc = -1; static
double mav_ref = 20929.0, rf_ref = -60.0; if (dc < 0) { / PM
sous le master DECAN (LU accept valide avec PM decan en shunt_legit *
2026-07-26) : plancher -75 + seuil last_pm>1000 gardent le camp.
/ const char M = getenv(“CALYPSO_DECAN”); const char p =
getenv(“CALYPSO_DECAN_PM”); dc = ((M && M[0] == ‘1’) || (p
&& p[0] == ‘1’)) ? 1 : 0; const char mr =
getenv(“CALYPSO_DECAN_PM_MAV_REF”); if (mr && mr) mav_ref =
atof(mr); const char rr = getenv(“CALYPSO_DECAN_PM_RF_REF”); if (rr
&& rr) rf_ref = atof(rr); if (mav_ref < 1.0) mav_ref =
1.0; } / [fix bootstrap 2026-07-26] seuil + plancher : a
l’acquisition last_pm peut * etre transitoirement infime -> rf ->
-inf -> apm=0 -> rxlev=-138 -> cell * rejetee ->
NO_CELL_FOUND -> JAMAIS de camp (chicken-egg : il faut camper * pour
avoir un bon last_pm). En dessous du seuil -> fallback -60 (campable)
; * au-dessus -> de-can borne a [-85,-40] dBm (suit le MAV sans
jamais rejeter). / if (dc && g_shunt.last_pm > 1000) {
double rf = 20.0 log10((double)g_shunt.last_pm / mav_ref) +
rf_ref; if (rf < -75.0) rf = -75.0; if (rf > -40.0) rf = -40.0;
int rfi = (int)(rf >= 0 ? rf + 0.5 : rf - 0.5); return
calypso_trf6151_apm_for_rf(rfi); } return
calypso_trf6151_apm_for_rf(fallback_target); }
void calypso_dsp_shunt_on_frame_tick(void) { if (!g_shunt.active)
return; shunt_poll_si_shm(); /* gr-gsm a-t-il ecrit un nouveau SI dans
le shm ? / calypso_tch_dl_poll(); / nouvelle trame FR DL dans
le sideband ? (toujours, hors gate pending) */
/* [2026-07-24] CADENCE FIX : le run C54x (wake+IT-frame+c54x_run) ne
* depend d'AUCUNE donnee fraiche de dispatch (page_idx/d_fn) -- seul le
* header ci-dessous en a besoin. On le sort du gate pending pour tourner
* a CHAQUE trame reelle, comme le fait calypso_tdma_tick() en natif (qui
* appelle c54x_run() inconditionnellement, jamais sur "tache neuve").
* Avant ce fix : c54x_run() ne tournait qu'aux ~1/12 trames ou l'ARM
* venait de poster un d_dsp_page frais (mesure : 708 insn DSP/tour mais
* ~57ms reels/tour) -- le DSP n'avait donc qu'une fraction de son temps
* reel pour attraper la fenetre de survie de data[0x0810]/d_ctrl_system. */
if (g_shunt.c54x && shunt_route_c54x()) {
static int run_c54x = -1;
if (run_c54x < 0) {
const char *e = getenv("CALYPSO_DSP_RUN_C54X");
run_c54x = (e && *e == '1') ? 1 : 0;
}
/* [2026-07-27] anti double-run : depuis le split du gate, le tick TDMA
* natif execute deja le DSP en mode ASSIST. Ce runner n a de sens que
* si le shunt SUBSTITUE (ou sur demande explicite). */
static int _drive = -1;
if (_drive < 0) { const char *d = getenv("CALYPSO_SHUNT_DRIVE_DSP");
_drive = (d && *d == '1') ? 1 : 0; }
if (run_c54x && (_drive || calypso_dsp_shunt_substitutes()))
shunt_route_to_c54x_run();
}
/* [2026-07-26] SHUNT_LEGIT (option 3) HORS gate pending : quand gr-gsm a
* DETECTE la SCH (sb_valid), transporter la vraie detection vers le DSP
* result (d_fb_det=1 + TOA/SNR reels) A CHAQUE trame, independamment du
* dispatch natif (qui ne pose jamais d_fb_det -- mur RANK3). L'ARM L1 lit
* d_fb_det -> deroule le vrai flux FBSB->SB->BSIC->sysinfo. Thread DSP = safe. */
{
/* @BEQUILLE — SHUNT_LEGIT (transport du resultat FB au frame-tick)
* (CALYPSO_SHUNT_LEGIT ou CALYPSO_SHUNT_NO_LEGIT =1)
* masque : le correlateur natif qui ne pose jamais d_fb_det. On ecrit
* directement api_ram[0x08F8..0x08FD] (d_fb_det=1, TOA/PM/ANGLE/SNR)
* et a_pm des que gr-gsm a decode la SCH (sb_valid).
* retirer : quand data[0x08f8] est ecrit par le DSP (RANK3 leve).
*/
static int legit = -1;
if (legit < 0) { const char *e = getenv("CALYPSO_SHUNT_LEGIT"); const char *nl = getenv("CALYPSO_SHUNT_NO_LEGIT"); legit = ((e && *e == '1') || (nl && *nl=='1')) ? 1 : 0; }
if (legit && g_shunt.c54x && g_shunt.sb_valid) {
/* L'ARM lit d_fb_det/snr/toa via calypso_dsp_shunt_real_fb_read
* (0x01F0/0x01FA/0x01F4) qui retourne g_shunt.rx_*. On pose ces
* champs depuis la detection gr-gsm reelle -> l'ARM voit FB found. */
g_shunt.rx_fb_det = 1;
/* [AFC loop-close v2 2026-07-26] recalcule rx_afc CHAQUE tick :
* brut(FCCH memorise) - freq DEJA compensee par le DAC courant
* (get_afc_hz). Sans ca, feed_iq/det gele rx_afc une fois le DAC
* enroule -> le firmware enroule au rail sur une valeur stale. Ici
* l'erreur effective DECROIT a mesure que le DAC monte -> converge. */
if (g_rx_raw_valid) {
double _eff = g_rx_raw_hz - calypso_twl3025_get_afc_hz();
double _a = _eff * (65536.0 / 86208.0);
if (_a > 32767.0) _a = 32767.0;
if (_a < -32768.0) _a = -32768.0;
g_shunt.rx_afc = (int16_t)_a;
static unsigned _afl = 0;
if ((_afl++ % 200) == 0)
fprintf(stderr, "[AFC-LOOP] raw=%.0fHz dac_hz=%.0f eff=%.0fHz -> rx_afc=%d\n",
g_rx_raw_hz, calypso_twl3025_get_afc_hz(), _eff, g_shunt.rx_afc);
}
/* [DECAN model-fidelity] garder le VRAI rx_snr (coherence feed_iq)
* quand DECAN_SNR actif, sinon le de-can SNR est defait ici. */
static int dsnr = -1;
if (dsnr < 0) {
const char *Ms = getenv("CALYPSO_DECAN");
const char *ss = getenv("CALYPSO_DECAN_SNR");
dsnr = ((Ms && Ms[0] == '1') || (ss && ss[0] == '1'));
}
if (!dsnr) g_shunt.rx_snr = 0x7000; /* SNR canne (fallback) */
g_shunt.rx_toa = (uint16_t)g_shunt.sb_toa;
/* [2026-07-26] FORMAT NATIF : ecrire la fenetre api_ram PARTAGEE
* (c54x->api_ram) EXACTEMENT comme le DSP no-shunt le ferait :
* DSP word W -> api_ram[W - C54X_API_BASE], lu par l'ARM sans intercept.
* shunt_dispatch_fb ecrivait BASE_API_NDB+NDB_D_FB_DET => mauvaise
* cellule (api_ram[0x550] au lieu de [0xF8]). Ici on pose le VRAI bloc
* FB (a_sync_demod) aux offsets natifs, ce que le firmware polle. */
if (g_shunt.c54x->api_ram) {
uint16_t *ar = g_shunt.c54x->api_ram;
ar[0x08F8 - C54X_API_BASE] = 1; /* d_fb_det = FOUND */
ar[0x08FA - C54X_API_BASE] = (uint16_t)g_shunt.sb_toa; /* a_sync_TOA */
ar[0x08FB - C54X_API_BASE] = g_shunt.last_pm; /* a_sync_PM */
ar[0x08FC - C54X_API_BASE] = (uint16_t)g_shunt.rx_afc; /* a_sync_ANG */
ar[0x08FD - C54X_API_BASE] = dsnr ? g_shunt.rx_snr : 0x7000; /* a_sync_SNR (DECAN) */
/* [2026-07-26 RANK5] a_pm (rxlev) au format natif : le vrai DSP
* ecrit a_pm=0 sur les read pages -> ecrase dispatch_pm. On pose
* directement, chaque tick (apres le run DSP), la valeur calibree
* trf6151 aux offsets read-page exacts (p0 woff 0x30..32, p1 0x44..46)
* lus par l1ddsp_meas_read (dsp_api.db_r->a_pm[i]). */
{
static int trf = -1, target = -60;
if (trf < 0) {
const char *d = getenv("CALYPSO_TRF_RXLEV");
const char *l = getenv("CALYPSO_SHUNT_LEGIT");
const char *t = getenv("CALYPSO_TRF_TARGET_RF");
trf = ((d && *d == '1') || (l && *l == '1')) ? 1 : 0;
if (t && *t) target = atoi(t);
}
if (trf) {
uint16_t apm = shunt_pm_decan_apm(target);
ar[0x30] = apm; ar[0x31] = apm; ar[0x32] = apm; /* read page 0 */
ar[0x44] = apm; ar[0x45] = apm; ar[0x46] = apm; /* read page 1 */
}
}
}
shunt_dispatch_fb(0);
/* SB : encode le burst SB depuis gr-gsm (BSIC=%d/sb_fn) sur les 2 pages
* -> l'ARM decode BSIC reel + FN au lieu de BSIC=0/vide. */
shunt_dispatch_sb(0);
shunt_dispatch_sb(1);
/* [2026-07-26 camp] SI -> a_cd sur le VRAI array data[] (le firmware lit
* dsp->data via calypso_trx.c:213, PAS dsp_ram ou vont les shunt_write_w).
* a_cd @ NDB_A_CD=0x1FC -> data word 0x9D2 (a_cd[0]), SI3 en a_cd[3]=0x9D5.
* Rotation SI1/2/3/4 toutes les 8 ticks (stable sur un bloc de 4 bursts)
* -> le mobile collecte tout le set au fil des blocs. Packing = m[i]|(m[i+1]<<8). */
/* [2026-07-26 LU] NE PAS ecraser a_cd avec le SI du camp quand un DL
* DEDIE (SDCCH UA/AUTH/LU-ACCEPT, ou AGCH IMM-ASSIGN) est en attente :
* dispatch_allc presente le UA/IMM-ASSIGN dans data[0x9D2] sur son bloc,
* et l'ecriture SI chaque tick le clobbait -> SABM jamais confirme (T3211
* retry). En mode dedie le mobile ne lit pas le BCCH -> SI inutile ici. */
/* [no-cell-info fix 2026-07-26] SI camp supprime SEULEMENT si un IMM
* ASSIGN (mt 0x3f/0x3a/0x3b) est pending sur l'AGCH (fenetre dediee : ne
* pas clobber le grant) -- PAS sur le PAGING de routine (0x21/0x22/0x24)
* qui tourne en continu en idle et affamait le SI3 (SI3 livre que pendant
* l'acquisition -> moniteur famine -> no-cell-info chaque seconde).
* agch_buf[2] = mt du dernier AGCH. LU intact (IMM ASSIGN protege). */
/* [2026-07-27 MT-SMS fix] EXPIRE l'IMM-ASSIGN latche : sans clear,
* agch_valid poisonnait le SI (gate ci-dessous) ET le paging (feed_agch)
* a vie -> no-cell-info permanent apres un canal dedie. Meme TTL que le
* drop paging (CALYPSO_SHUNT_AGCH_TTL, def 100). SI + paging reprennent. */
{
/* [2026-07-27] OPT-IN (defaut OFF apres regression MO-SMS shunt_legit) :
* l'expiry agch corrige le no-cell-info post-dedie MAIS clear le grant
* IMM-ASSIGN -> peut casser l'etablissement SDCCH. Activer via
* CALYPSO_SHUNT_AGCH_EXPIRE=1 (no-legit / experiences). */
/* @BEQUILLE — SHUNT_AGCH_EXPIRE (+ SHUNT_AGCH_TTL) (CALYPSO_SHUNT_AGCH_EXPIRE,
* atoi>0, defaut OFF ; shunt_no_legit.env:=1)
* masque : rien de reel — c'est le correctif d'une autre bequille : le latch
* IMM-ASSIGN fabrique par feed_agch empoisonne le gate SI et le paging
* a vie. On le fait expirer au bout d'un TTL.
* retirer : avec l'injection AGCH elle-meme (INJECT_AGCH) — quand a_cd est
* alimente par le decodeur natif, il n'y a plus de latch a expirer.
* NB : SHUNT_AGCH_TTL a 3 consommateurs de semantiques differentes.
*/
static int _agex_on = -1, _agex = 100;
if (_agex_on < 0) { const char *e = getenv("CALYPSO_SHUNT_AGCH_EXPIRE"); _agex_on = (e && atoi(e) > 0) ? 1 : 0;
const char *t = getenv("CALYPSO_SHUNT_AGCH_TTL"); if (t && *t) _agex = atoi(t); }
if (_agex_on && g_shunt.agch_valid && (uint32_t)(g_shunt.tick_cnt - g_shunt.agch_tick) > (uint32_t)_agex)
g_shunt.agch_valid = false;
}
if (g_shunt.si_valid && !g_shunt.sdcch_valid
&& !(g_shunt.agch_valid && (g_shunt.agch_buf[2] == 0x3f
|| g_shunt.agch_buf[2] == 0x3a || g_shunt.agch_buf[2] == 0x3b))
&& g_shunt.c54x && g_shunt.c54x->data) {
/* [no-cell-info fix v2 2026-07-26] rotation SI sur compteur LIBRE
* (avance CHAQUE tick, hors pending) et PAS sur tick_cnt qui stalle
* en idle (incremente seulement apres le gate pending, l.733) ->
* sinon si_rr FIGE une fois came et SI3 n'est plus livre en idle ->
* moniteur cellule servante famine -> no-cell-info chaque seconde. */
static unsigned si_rot = 0;
/* [2026-07-27] rotation SI accELErEe : avance si_rr a chaque bloc
* (mask 0) au lieu de tous les 8 -> le mobile re-collecte SI1+SI2+SI3
* VITE apres un dedie (sinon sync timeout -> No service post-SMS).
* Tunable CALYPSO_SHUNT_SI_ROT_MASK (0=chaque bloc, 7=ancien). */
/* @BEQUILLE — SHUNT_SI_ROT_MASK (CALYPSO_SHUNT_SI_ROT_MASK, VALEUR, defaut 7)
* masque : l'ordonnancement mf-51 des SI que le DSP devrait produire : on fait
* tourner a la main les slots si_set[] pour re-livrer SI1..SI4.
* retirer : quand a_cd est alimente par la demodulation native (les SI arrivent
* alors a leur place dans le multiframe).
*/
static int _rotmask = -1;
if (_rotmask < 0) { const char *e = getenv("CALYPSO_SHUNT_SI_ROT_MASK"); _rotmask = (e && *e) ? atoi(e) : 7; }
if ((si_rot++ & (unsigned)_rotmask) == 0) {
for (int k = 1; k <= 6; k++) {
int si = (g_shunt.si_rr + k) % 6;
if (g_shunt.si_set_have[si]) { g_shunt.si_rr = si; break; }
}
}
const uint8_t *si = g_shunt.si_set[g_shunt.si_rr];
uint16_t *d = g_shunt.c54x->data;
d[0x9D2] = 0x0000; /* a_cd[0] FIRE/CRC = pass */
d[0x9D3] = 0x0000; /* a_cd[1] */
d[0x9D4] = 0x0000; /* a_cd[2] num_biterr = 0 */
for (int i = 0; i < 23; i += 2) { /* a_cd[3..14] = data[0x9D5..0x9E0] */
uint8_t lo = si[i], hi = (i + 1 < 23) ? si[i + 1] : 0x2B;
d[0x9D5 + i / 2] = (uint16_t)(lo | (hi << 8));
}
/* [2026-07-26 camp] a_serv_demod des READ PAGES du NB (words 8..11) :
* read page0 = data[0x830..0x833], page1 = data[0x844..0x847].
* nb_resp lit ces 4 mots par burst pour l'AFC (afc_input) + rx_level.
* Quand BURST_OFS aligne les 4 bursts, ils sont TOUS traites -> sans
* ces valeurs, afc_input(garbage) fait DERIVER l'AFC -> sync perdue ->
* SI casses. On pose D_ANGLE=0 (aucune erreur de freq -> AFC stable),
* D_SNR haut (>AFC_SNR_THRESHOLD=2560), D_TOA=23, D_PM = a_pm calibre. */
{
/* [DECAN model-fidelity 2026-07-26] gate par modele (+ master
* CALYPSO_DECAN). OFF par defaut => cannes de stabilisation a
* l'identique (baseline LU-accept intact). ON => vraie sortie du
* modele emule (feed_iq / trf6151 / gr-gsm) pour verifier s'il
* tient le camp. rx_snr/rx_afc exigent CALYPSO_SHUNT_REAL_FB=1
* (sinon rx_snr reste 0x7000 canne et rx_afc reste 0). */
/* @BEQUILLE — DECAN_TOA / DECAN_SNR / DECAN_ANGLE / DECAN_PM (CALYPSO_DECAN_*
* ou master CALYPSO_DECAN ; l'ETAT OFF est la bequille)
* masque : OFF -> a_serv_demod des read-pages recoit des constantes cannees :
* TOA=23, SNR=0x7000, ANGLE=0, PM=apm_for_rf(-60), a la place de la
* sortie du modele (sb_toa gr-gsm, rx_snr / rx_afc de feed_iq, MAV).
* Elles masquent l'absence de mesure DSP native.
* retirer : quand le correlateur natif ecrit lui-meme a_sync_demod
* [TOA/PM/ANGLE/SNR].
* NB : dc_pm calcule ici n'alimente que la condition du fprintf ; le gate
* PM effectif est dans shunt_pm_decan_apm().
*/
static int dc_toa = -1, dc_pm, dc_snr, dc_ang;
if (dc_toa < 0) {
const char *M = getenv("CALYPSO_DECAN");
int m = (M && M[0] == '1');
const char *t = getenv("CALYPSO_DECAN_TOA");
const char *p = getenv("CALYPSO_DECAN_PM");
const char *s = getenv("CALYPSO_DECAN_SNR");
const char *a = getenv("CALYPSO_DECAN_ANGLE");
dc_toa = m || (t && t[0] == '1');
dc_pm = m || (p && p[0] == '1');
dc_snr = m || (s && s[0] == '1');
dc_ang = m || (a && a[0] == '1');
}
uint16_t pm_c = calypso_trf6151_apm_for_rf(-60);
uint16_t toa_v = (dc_toa && g_shunt.sb_valid) ? (uint16_t)g_shunt.rx_toa : 23;
uint16_t pm_v = shunt_pm_decan_apm(-60);
uint16_t ang_v = dc_ang ? (uint16_t)g_shunt.rx_afc : 0;
uint16_t snr_v = dc_snr ? g_shunt.rx_snr : 0x7000;
/* page 0 */ d[0x830]=toa_v; d[0x831]=pm_v; d[0x832]=ang_v; d[0x833]=snr_v;
/* page 1 */ d[0x844]=toa_v; d[0x845]=pm_v; d[0x846]=ang_v; d[0x847]=snr_v;
{
static unsigned _dcl = 0;
if ((dc_toa || dc_pm || dc_snr || dc_ang) && _dcl++ < 24)
fprintf(stderr, "[DECAN] wrote toa=%u(c23) pm=%u(c%u) ang=%d(c0) "
"snr=0x%x(c0x7000) | model: sb_valid=%d rx_toa=%u last_pm=%u "
"rx_afc=%d rx_snr=0x%x\n",
toa_v, pm_v, pm_c, (int16_t)ang_v, snr_v,
g_shunt.sb_valid, g_shunt.rx_toa, g_shunt.last_pm,
g_shunt.rx_afc, g_shunt.rx_snr);
}
}
static unsigned _acd = 0;
if (_acd++ < 5000)
SHUNT_LOG("CAMP: a_cd<-SI type_slot=%d have[0-5]=%d%d%d%d%d%d tick=%u",
g_shunt.si_rr, g_shunt.si_set_have[0],g_shunt.si_set_have[1],g_shunt.si_set_have[2],g_shunt.si_set_have[3],g_shunt.si_set_have[4],g_shunt.si_set_have[5], g_shunt.tick_cnt);
}
static unsigned _lg = 0;
if (_lg++ < 12)
SHUNT_LOG("SHUNT_LEGIT: detection gr-gsm reelle (fn=%u bsic=%d toa=%d) "
"-> d_fb_det pose au DSP result (hors pending, MAC court-circuite)",
g_shunt.sb_fn, g_shunt.sb_bsic, (int)g_shunt.sb_toa);
}
}
if (!g_shunt.pending) {
return;
}
g_shunt.tick_cnt++;
uint8_t page = g_shunt.page_idx;
uint16_t md = g_shunt.d_task_md;
uint16_t td = g_shunt.d_task_d;
/* Priority order: md tasks (FB/SB) > NB DL > NB UL > ALLC.
* Refine when canned policies land. */
if (shunt_route_c54x() && g_shunt.c54x) {
/* CALYPSO_DSP=c54x : overlay des écritures NDB gr-gsm (rxlev/FB/SB/SI réels)
* par-dessus le poison 0x70c4 du c54x -> le mobile campe et fait sa LU.
* Le RUN du VRAI c54x (route_to_c54x -> c54x_run) est OPT-IN car le revival
* est encore instable (crash qemu). Défaut : overlay seul = réception via le
* shunt, aucun c54x exécuté, pas de crash. CALYPSO_DSP_RUN_C54X=1 pour le lancer. */
{
static int run_c54x = -1;
if (run_c54x < 0) {
const char *e = getenv("CALYPSO_DSP_RUN_C54X");
run_c54x = (e && *e == '1') ? 1 : 0;
fprintf(stderr, "[c54x-gate] getenv RUN_C54X=%s CRASHPC=%s DSP=%s -> run_c54x=%d\n",
e ? e : "(null)",
getenv("CALYPSO_C54X_CRASHPC") ? getenv("CALYPSO_C54X_CRASHPC") : "(null)",
getenv("CALYPSO_DSP") ? getenv("CALYPSO_DSP") : "(null)", run_c54x);
}
if (run_c54x) shunt_route_to_c54x_header(page);
}
if (md == PM_DSP_TASK) do { static int nf_pm=-1; if(nf_pm<0){const char*e=getenv("CALYPSO_SHUNT_NO_FAKE_PM");nf_pm=(e&&*e=='1')?1:0;} if(!nf_pm) shunt_dispatch_pm(page); } while(0);
else if (md == FB_DSP_TASK) {
/* [2026-07-22] Gate le fake FB en revive : CALYPSO_SHUNT_NO_FAKE_FB=1
* -> skip le d_fb_det bidon (NDB only) pour laisser le VRAI correlateur
* produire le resultat (isole le go-live : d_fb_det reste 0 si bloque). */
static int no_fake_fb = -1;
if (no_fake_fb < 0) { const char *e = getenv("CALYPSO_SHUNT_NO_FAKE_FB"); no_fake_fb = (e && *e == '1') ? 1 : 0; }
/* [2026-07-26] MODE SHUNT_LEGIT : le MAC natif ne deroule pas (RANK3), mais
* gr-gsm DETECTE reellement la SCH. On TRANSPORTE cette vraie detection vers
* le DSP result (d_fb_det=1 + TOA/SNR reels) UNIQUEMENT quand sb_valid (=
* gr-gsm a decode). Pas de fake : l'ARM L1 deroule alors le VRAI flux
* FBSB->SB->BSIC->sysinfo natif, on court-circuite juste l'etage MAC. */
static int legit = -1;
if (legit < 0) { const char *e = getenv("CALYPSO_SHUNT_LEGIT"); const char *nl = getenv("CALYPSO_SHUNT_NO_LEGIT"); legit = ((e && *e == '1') || (nl && *nl=='1')) ? 1 : 0; }
if (legit && !no_fake_fb) { /* [2026-07-26] NO_FAKE_FB=1 retire le fake FB MEME en shunt_legit (isole le go-live natif) */
if (g_shunt.sb_valid) {
shunt_dispatch_fb(page);
static unsigned _lg = 0;
if (_lg++ < 12)
SHUNT_LOG("SHUNT_LEGIT: gr-gsm detecte (sb_fn=%u bsic=%d toa=%d) "
"-> d_fb_det transporte au DSP result (MAC natif court-circuite)",
g_shunt.sb_fn, g_shunt.sb_bsic, (int)g_shunt.sb_toa);
}
} else if (!no_fake_fb) {
shunt_dispatch_fb(page);
}
}
else if (md == SB_DSP_TASK && g_shunt.sb_valid) shunt_dispatch_sb(page);
if (td == ALLC_DSP_TASK) shunt_dispatch_allc(page);
} else if (md == PM_DSP_TASK) {
do { static int nf_pm=-1; if(nf_pm<0){const char*e=getenv("CALYPSO_SHUNT_NO_FAKE_PM");nf_pm=(e&&*e=='1')?1:0;} if(!nf_pm) shunt_dispatch_pm(page); } while(0);
} else if (md == FB_DSP_TASK) {
shunt_dispatch_fb(page);
} else if (md == SB_DSP_TASK) {
shunt_dispatch_sb(page);
} else if (td == ALLC_DSP_TASK) {
shunt_dispatch_allc(page);
} else if ((td & 0x7FFF) == TCHT_DSP_TASK) { /* TCH/F DL (JALON 1) : sub0 -> a_dd_0 */
shunt_dispatch_tch_dl(page);
} else if (td != 0) {
shunt_dispatch_nb(page, td);
}
/* RA UL (d_task_ra) handled separately — TBD when TX flow gated */
/* Mock task done. Real DSP would keep its state for multi-attempt
* tasks (FB search across 11 frames). Phase 1 canned can keep the
* pending bit set for FB until d_fb_det is consumed (zeroed by ARM
* in read_fb_result @ prim_fbsb.c:318). */
g_shunt.pending = false;
}
/* —- MMIO overlay on NDB+0 (d_dsp_page trigger) —- / static void
shunt_d_dsp_page_write(void opaque, hwaddr offset, uint64_t value,
unsigned size) { /* Write also commits the value in the underlying RAM
region; we * intercept here for the latch side-effect only. Caller’s
write * happens via the normal RAM path (this overlay is registered with
* higher priority but pass-through semantics). */
shunt_latch_task((uint16_t)value); }
static uint64_t shunt_d_dsp_page_read(void opaque, hwaddr offset,
unsigned size) { / Read passes through to RAM — ARM polls this for
handshake state. * We return the actual RAM value to be transparent. */
return shunt_read_w(BASE_API_NDB + NDB_D_DSP_PAGE); }
static const MemoryRegionOps shunt_ndb_trigger_ops = { .read =
shunt_d_dsp_page_read, .write = shunt_d_dsp_page_write, .endianness =
DEVICE_LITTLE_ENDIAN, .impl = { .min_access_size = 2, .max_access_size =
2 }, };
/* —- GSMTAP listener : reçoit le SI décodé par gr-gsm (front py) —-
* gr-gsm (grgsm_decode -m BCCH) sort des frames GSMTAP. On écoute sur un
port * UDP (CALYPSO_SHUNT_GSMTAP_PORT, défaut 4730 pour ne pas taper le
4729 du * BTS), on extrait le L2 (après le hdr GSMTAP de 16 o) des
frames BCCH, et on * appelle feed_si → a_cd. = le pont gr-gsm→a_cd, côté
qemu. / #define GSMTAP_HDR_LEN 16 #define GSMTAP_TYPE_UM 0x01
#define GSMTAP_CHANNEL_BCCH 0x01 #define GSMTAP_CHANNEL_AGCH 0x04 /
IMM ASSIGN (PORTE 3a / #11) / #define GSMTAP_CHANNEL_SDCCH4 0x07
/ SDCCH/4 SS0 DL (UA/AUTH / #2) / #define GSMTAP_CHANNEL_ACCH
0x80 / bit ACCH (SACCH) GSMTAP / #define GSMTAP_CHANNEL_SACCH
(GSMTAP_CHANNEL_SDCCH4 | GSMTAP_CHANNEL_ACCH) / 0x87 : SI5/SI6
reels */ static int g_gsmtap_fd = -1;
/* AGCH (#11) : range l’IMM ASSIGN forwardé par si_bridge (tag GSMTAP
AGCH 0x04) * dans agch_buf — DISTINCT de si_buf, pour que la rotation
des SI ne l’écrase pas. * shunt_dispatch_allc le présentera dans a_cd
sur un bloc CCCH (le firmware tague * alors chan_nr=0x90 ->
gsm48_rr_rx_pch_agch -> gsm48_rr_rx_imm_ass). / static void
calypso_dsp_shunt_feed_agch(const uint8_t l2, int len) { /* @BEQUILLE — INJECT_AGCH (CALYPSO_INJECT_AGCH=1,
fallback CALYPSO_SHUNT_LEGIT=1) * masque : la reception AGCH par le DSP
— l’IMM ASSIGN est capte hors bande * (GSMTAP gr-gsm) puis presente dans
a_cd sur un bloc CCCH. * retirer : quand le decodage AGCH natif alimente
a_cd. */ { static int _ginj = -1; if (_ginj < 0) { const char *_e =
getenv(“CALYPSO_INJECT_AGCH”); _ginj = (_e && *_e == ‘1’) ? 1 :
0; if (!_ginj) { const char *_l = getenv(“CALYPSO_SHUNT_LEGIT”); _ginj =
(_l && *_l == ‘1’) ? 1 : 0; } } if (!_ginj) return; } /*
[2026-07-23] HACK injection sortie, DEFAUT OFF (natif) ;
=CALYPSO_INJECT_AGCH=1 pour reactiver */ if (!l2 || len < 3)
return;
/* Priorite IMM ASSIGN : ne pas laisser un PAGING REQUEST (mt 0x21/0x22/0x24)
* ecraser un IMM ASSIGN (0x3f/0x3a/0x3b) encore valide en attente de
* presentation sur l'AGCH. L'IMM ASSIGN est la reponse time-critical au RACH
* (un seul agch_buf partage) ; le paging est best-effort. Sur un reseau a
* paging dense le flood clobberait sinon le grant -> RACH en boucle, pas de LU. */
{
uint8_t in_mt = l2[2];
int in_is_imm = (in_mt == 0x3f || in_mt == 0x3a || in_mt == 0x3b);
uint8_t cur_mt = g_shunt.agch_buf[2];
int cur_is_imm = (cur_mt == 0x3f || cur_mt == 0x3a || cur_mt == 0x3b);
if (!in_is_imm && g_shunt.agch_valid && cur_is_imm) {
static int ttl = -1;
if (ttl < 0) { const char *t = getenv("CALYPSO_SHUNT_AGCH_TTL");
ttl = (t && *t) ? atoi(t) : 100; }
if ((uint32_t)(g_shunt.tick_cnt - g_shunt.agch_tick) <= (uint32_t)ttl) {
static unsigned drop = 0;
if (drop++ < 20 || (drop % 200) == 0)
SHUNT_LOG("feed_agch: PAGING mt=0x%02x DROP "
"(IMM ASSIGN 0x%02x encore valide en attente)\n", in_mt, cur_mt);
return;
}
}
}
int n = len < 23 ? len : 23;
memcpy(g_shunt.agch_buf, l2, n);
for (int i = n; i < 23; i++) g_shunt.agch_buf[i] = 0x2B;
/* [2026-07-27] SONDE sous-voie SDCCH de l IMM-ASS (channel desc [4..6]) :
* confirme le mismatch SS0-hardcode vs sous-voie assignee (cause rejet SMS flaky). */
if (g_shunt.agch_buf[2] == 0x3f) {
uint8_t cd0 = g_shunt.agch_buf[4];
/* [2026-07-27] Base DL fn%%51 de la voie dediee assignee (GSM 05.02, cf
* firmware mframe_sched.c). chan_desc.chan_nr = cd0. Le RESEAU ICI assigne
* du SDCCH/8 (01SSS, DL base=SS*4) et parfois SDCCH/4 (001SS). On calcule
* la base DL et on la memorise pour presenter l'UA sur la BONNE fenetre. */
int _ss = -1, _base = -1; const char *_ct = "?";
if ((cd0 & 0xE0) == 0x20) { /* SDCCH/4 combined : 001SS */
_ss = (cd0 >> 3) & 0x03; _ct = "SDCCH/4";
static const int b4[4] = { 22, 26, 32, 36 };
_base = b4[_ss];
} else if ((cd0 & 0xC0) == 0x40) { /* SDCCH/8 : 01SSS, DL base = SS*4 */
_ss = (cd0 >> 3) & 0x07; _ct = "SDCCH/8";
_base = _ss * 4; /* 0,4,8,12,16,20,24,28 (mod 51) */
}
if (_base >= 0) { g_shunt.sdcch_ss = (uint8_t)_base; g_shunt.sdcch_ss_set = true;
g_shunt.sdcch_ch8 = ((cd0 & 0xC0) == 0x40); } /* base + type /4|/8 */
static unsigned _iac = 0;
if (_iac++ < 80) {
fprintf(stderr, "[dsp-shunt] IMM-ASS chan_desc=[%02x %02x %02x] %s SS=%d base=%d TN=%u req-ref=[%02x %02x %02x]\n",
g_shunt.agch_buf[4], g_shunt.agch_buf[5], g_shunt.agch_buf[6],
_ct, _ss, _base, cd0 & 0x07,
g_shunt.agch_buf[7], g_shunt.agch_buf[8], g_shunt.agch_buf[9]);
}
}
/* FN-FIX (le vrai fix, ON par defaut ; CALYPSO_REQREF_REWRITE=0 pour A/B) :
* reecrit la request-reference de l'IMM ASSIGN (octets L2 [8],[9]) au FN EXACT que
* le firmware a memorise pour la derniere RACH = last_rach.fn (@0x836500). RAISON :
* le FN de la req-ref vit dans l'horloge osmo-trx (sample-position, base ~2465144),
* le mobile le compare a SA propre horloge L1 (la valeur recue en L1CTL_RACH_CONF =
* last_rach.fn, prim_rach.c:114) ; ces deux compteurs free-running ont une phase de
* depart non controlee et variable par-RACH -> mismatch gsm48_rr.c:3382. Aucun
* cal_off/ul_fnoff/fn_adj cote device ne peut les aligner.
* On lit donc DIRECTEMENT last_rach.fn (la valeur que le mobile a memorisee, par
* construction) au lieu de g_rach_l1s_fn[ra]+adj : ce dernier capturait current_time
* au tick d_rach/cmd, soit -4 frames AVANT que le firmware pose last_rach.fn =
* current_time-1 au tick rach_resp -> skew variable (constate : adj devait passer de
* -1 a +1 entre deux runs). last_rach.fn n'a aucun skew ni collision RA (1 seule RACH
* en vol cote mobile). Le check du mobile (gsm48_rr.c:3372) est PUREMENT local : il
* matche (ra,T1,T2,T3) contre son propre cr_hist ; le FN est informationnel. Donc
* req-ref := last_rach.fn => match exact, sans constante FN magique ni adj.
* RA = L2[7] (log seulement). Encodage req-ref (04.08) :
* [8] = (T1'<<3) | (T3>>3) ; [9] = ((T3&7)<<5) | T2 ; T1'=(FN/1326)%32.
* adj=0 par defaut (last_rach.fn EST le memo) ; surchargeable CALYPSO_REQREF_ADJ. */
{
/* @BEQUILLE — REQREF_LAST_RACH / REQREF_PERRA / REQREF_REWRITE / REQREF_ADJ
* masque : la coherence FN entre l'horloge L1 du mobile et la req-ref emise par
* la BTS. On REECRIT agch_buf[8..9] (T1'/T2/T3) de l'IMM-ASSIGN recu
* pour qu'il matche last_rach.fn lu dans la RAM ARM — c'est-a-dire
* qu'on falsifie le message reseau pour compenser un skew local.
* retirer : quand shunt_l1s_fn() et calypso_trx_get_fn() sont alignees sur la
* SCH sans recale residuel (RANK4) — la req-ref native matche alors.
*/
static int reqref_rw = -1, reqref_perra = -1, rr_adj = -99999;
if (reqref_rw < 0) { const char *e = getenv("CALYPSO_REQREF_REWRITE"); reqref_rw = (e && *e == '1') ? 1 : 0; } /* defaut OFF : ancien rewrite GLOBAL (50% multi-RACH) */
if (reqref_perra < 0) { const char *e = getenv("CALYPSO_REQREF_PERRA"); reqref_perra = (e && *e == '0') ? 0 : 1; } /* defaut ON : req-ref PER-RA (FN exact du RACH_CONF keye par ra) */
if (rr_adj == -99999) { const char *e = getenv("CALYPSO_REQREF_ADJ"); rr_adj = e ? atoi(e) : 0; }
if ((reqref_perra || reqref_rw) && n >= 10 && g_shunt.agch_buf[2] == 0x3f) {
uint8_t ra = g_shunt.agch_buf[7];
/* [2026-07-27] FIX SMS (mt-sms-works) : prefere last_rach.fn@0x836500
* (FN EXACT memorise par le firmware = match req-ref garanti), defaut ON.
* Fallback auto sur g_rach_conf_fn[ra] si lecture=0. Gate CALYPSO_REQREF_LAST_RACH=0. */
static int use_lr = -1;
if (use_lr < 0) { const char *e = getenv("CALYPSO_REQREF_LAST_RACH"); use_lr = (e && *e == '0') ? 0 : 1; }
uint32_t lr = use_lr ? shunt_last_rach_fn() : 0;
uint32_t memo_fn = lr ? lr
: (reqref_perra && g_rach_conf_fn[ra]) ? g_rach_conf_fn[ra]
: (reqref_rw ? g_last_rach_conf_fn : 0); /* last_rach exact, sinon per-ra, sinon global */
{ static unsigned dbg = 0;
if (dbg++ < 40)
SHUNT_LOG("FN-FIX probe RA=0x%02x "
"memo_fn(RACH_CONF)=%u last_rach@500=%u l1s_fn=%u n=%d\n",
ra, memo_fn, shunt_last_rach_fn(), shunt_l1s_fn(), n); }
if (memo_fn) {
int64_t fn = (int64_t)memo_fn + rr_adj;
if (fn < 0) fn = 0;
uint16_t t1p = (uint16_t)(((uint32_t)fn / 1326u) % 32u);
uint8_t t2 = (uint8_t)((uint32_t)fn % 26u);
uint8_t t3 = (uint8_t)((uint32_t)fn % 51u);
g_shunt.agch_buf[8] = (uint8_t)((t1p << 3) | ((t3 >> 3) & 7));
g_shunt.agch_buf[9] = (uint8_t)(((t3 & 7) << 5) | (t2 & 0x1f));
static unsigned rwlog = 0;
if (rwlog++ < 30)
SHUNT_LOG("FN-FIX req-ref RA=0x%02x reecrite -> "
"fn=%u (T1'=%u T2=%u T3=%u) adj=%d [last_rach.fn]\n",
ra, (uint32_t)fn, t1p, t2, t3, rr_adj);
}
}
}
g_shunt.agch_valid = true;
g_shunt.agch_tick = g_shunt.tick_cnt;
SHUNT_LOG("feed_agch: IMM-ASS mt=0x%02x -> agch_buf "
"(a presenter sur bloc CCCH)\n", l2[2]);
}
/* SDCCH/4 SS0 DL (#2) : range le bloc L2 (UA/AUTH) forwarde par
si_bridge * (tag GSMTAP SDCCH4 0x07) dans sdcch_buf – DISTINCT de
si_buf/agch_buf. * shunt_dispatch_allc le presentera dans a_cd sur le
bloc SDCCH/4 SS0 * (fn%51 in {22-25}) ; le firmware tague alors
chan_nr=0x20 -> lapdm_dcch -> * UA/AUTH -> L3 (miroir de
feed_agch, SANS la sonde req-ref). / static void
calypso_dsp_shunt_feed_sdcch(const uint8_t l2, int len, uint32_t
fn) { /* @BEQUILLE — INJECT_SDCCH
(CALYPSO_INJECT_SDCCH=1, fallback CALYPSO_SHUNT_LEGIT=1) * masque : la
demodulation du SDCCH DL par le DSP (UA/AUTH/L3), remplacee par * les
blocs L2 forwardes par si_bridge. * retirer : quand le chemin natif
demodule le SDCCH. */ { static int _ginj = -1; if (_ginj < 0) { const
char *_e = getenv(“CALYPSO_INJECT_SDCCH”); _ginj = (_e && *_e ==
‘1’) ? 1 : 0; if (!_ginj) { const char *_l =
getenv(“CALYPSO_SHUNT_LEGIT”); _ginj = (_l && *_l == ‘1’) ? 1 :
0; } } if (!_ginj) return; } if (!l2 || len < 3) return; static int
ring_on = -1; if (ring_on < 0) { const char e =
getenv(“CALYPSO_SHUNT_SDCCH_RING”); ring_on = (!e || e != ‘0’) ? 1
: 0; } int n = len < 23 ? len : 23; if (!ring_on) {
memcpy(g_shunt.sdcch_buf, l2, n); for (int i = n; i < 23; i++)
g_shunt.sdcch_buf[i] = 0x2B; g_shunt.sdcch_valid = true;
g_shunt.sdcch_tick = g_shunt.tick_cnt; return; } if (fn && fn ==
g_shunt.sdcch_last_fn) return; g_shunt.sdcch_last_fn = fn; if
(g_shunt.sdcch_ring_tail - g_shunt.sdcch_ring_head >= SDCCH_RING_N) {
/* [2026-07-27] eviction d overflow TRACEE (etait silencieuse) : quand
le * ring sature, on drope le plus vieux -> perte de bloc DL. Log +
compteur * pour diagnostiquer un SMS/LU intermittent sans deviner. */
g_shunt.evict_overflow++; static unsigned n_ovf = 0; if (n_ovf++ < 40
|| (n_ovf % 100) == 0) SHUNT_LOG(“feed_sdcch: RING OVERFLOW #%u ->
drop head fn=%u c=0x%02x (depth=%u/%u)”, n_ovf,
g_shunt.sdcch_ring[g_shunt.sdcch_ring_head % SDCCH_RING_N].fn,
g_shunt.sdcch_ring[g_shunt.sdcch_ring_head % SDCCH_RING_N].l2[1],
g_shunt.sdcch_ring_tail - g_shunt.sdcch_ring_head, SDCCH_RING_N);
g_shunt.sdcch_ring[g_shunt.sdcch_ring_head % SDCCH_RING_N].used = false;
g_shunt.sdcch_ring_head++; } uint32_t idx = g_shunt.sdcch_ring_tail %
SDCCH_RING_N; memcpy(g_shunt.sdcch_ring[idx].l2, l2, n); for (int i = n;
i < 23; i++) g_shunt.sdcch_ring[idx].l2[i] = 0x2B;
g_shunt.sdcch_ring[idx].fn = fn; g_shunt.sdcch_ring[idx].tick =
g_shunt.tick_cnt; g_shunt.sdcch_ring[idx].reps = 0;
g_shunt.sdcch_ring[idx].used = true; g_shunt.sdcch_ring_tail++;
g_shunt.sdcch_valid = true; SHUNT_LOG(“feed_sdcch: ENQUEUE fn=%u
c=0x%02x [depth=%u]”, fn, l2[1], g_shunt.sdcch_ring_tail -
g_shunt.sdcch_ring_head); }
/* SACCH SS0 DL REELLE : SI5(0x1d)/SI6(0x1e) decodes par grgsm,
forwardes par * si_bridge (sub_type 0x87). Le bloc grgsm = 23o : [L1 hdr
2][LAPDm: 03 03 len * 06 mt L3…] -> exactement le layout B4 attendu
par le dispatch SACCH. On * garde la L3 REELLE mais on ZERO le header L1
(tx_power/TA) : les valeurs * osmo-bts ne sont pas pour notre air emule
(idem fabrication). sacch_real=true * fait CESSER la fabrication
SI3->SI6 (sinon SI3 du BCCH clobbe le SI5/SI6 reel). / static
void calypso_dsp_shunt_feed_sacch(const uint8_t l2, int len) { /*
@BEQUILLE — INJECT_SACCH
(CALYPSO_INJECT_SACCH=1, fallback CALYPSO_SHUNT_LEGIT=1) * masque : la
demodulation SACCH par le DSP (SI5/SI6 du canal dedie), * remplacee par
les blocs gr-gsm avec header L1 zerote. * retirer : quand le chemin
natif demodule la SACCH. */ { static int _ginj = -1; if (_ginj < 0) {
const char *_e = getenv(“CALYPSO_INJECT_SACCH”); _ginj = (_e &&
*_e == ‘1’) ? 1 : 0; if (!_ginj) { const char *_l =
getenv(“CALYPSO_SHUNT_LEGIT”); _ginj = (_l && *_l == ‘1’) ? 1 :
0; } } if (!_ginj) return; } /* [2026-07-23] HACK injection sortie,
DEFAUT OFF (natif) ; =CALYPSO_INJECT_SACCH=1 pour reactiver / if
(!l2 || len < 7) return; int n = len < 23 ? len : 23; /
trouve le RR header (06 1d / 06 1e) pour valider que c’est bien SI5/SI6
/ int rr = -1; for (int i = 2; i + 1 < n && i < 8;
i++) if (l2[i] == 0x06 && (l2[i + 1] == 0x1d || l2[i + 1] ==
0x1e)) { rr = i; break; } if (rr < 0) return; / pas un SI5/SI6
-> ignore / uint8_t s = g_shunt.sacch_buf; memcpy(s, l2, n);
for (int i = n; i < 23; i++) s[i] = 0x2b; s[0] = 0x00; /* L1 SACCH
header : tx_power -> neutre / s[1] = 0x00; / L1 SACCH header
: TA -> neutre / g_shunt.sacch_have = true; g_shunt.sacch_real =
true; / coupe la fabrication SI3->SI6 */ static unsigned nf = 0;
if (nf++ < 20 || (nf % 50) == 0) SHUNT_LOG(“feed_sacch REEL: SI%d %do
(mt=0x%02x) -> sacch_buf”, (l2[rr + 1] == 0x1d) ? 5 : 6, n, l2[rr +
1]); }
static void shunt_gsmtap_read(void opaque) { uint8_t buf[512];
for (;;) { ssize_t n = recv(g_gsmtap_fd, buf, sizeof(buf),
MSG_DONTWAIT); if (n < 0) { if (errno == EAGAIN || errno ==
EWOULDBLOCK) break; break; } if (n < GSMTAP_HDR_LEN + 1) continue;
uint8_t type = buf[2]; / GSMTAP hdr : type @2 / uint8_t sub_type = buf[12]; /
channel @12 / if (type !=
GSMTAP_TYPE_UM) continue; / L2 = buf+16 : [0]=pseudo-len, [1]=PD,
[2]=message type. * RR PD (0x06) requis pour BCCH/AGCH (SI/IMM-ASS) ; le
SDCCH/4 (#2) * porte de la LAPDm (buf[17]=controle, PAS un PD RR) ->
on n’exige * PAS 0x06 pour sub_type 0x07. / if (n <
GSMTAP_HDR_LEN + 3) continue; if (sub_type != GSMTAP_CHANNEL_SDCCH4
&& sub_type != GSMTAP_CHANNEL_SACCH &&
buf[GSMTAP_HDR_LEN + 1] != 0x06) continue; / pas RR PD (BCCH/AGCH)
/ uint8_t mt = buf[GSMTAP_HDR_LEN + 2]; if (sub_type ==
GSMTAP_CHANNEL_BCCH) { / (A) SET BCCH COMPLET
(SI1/2/3/4/2bis/2ter), pas juste SI3 — sinon le * mobile n’a jamais le
set complet (“No sysinfo yet”). feed_si range * chaque type dans son
slot, dispatch_allc tourne dessus. / switch (mt) { case 0x19: case
0x1a: case 0x1b: / SI1 SI2 SI3 / case 0x1c: case 0x1d: case
0x1e: / SI4 SI2bis SI2ter / break; default: continue; /
paging/SI13… sur BCCH : drop / } calypso_dsp_shunt_feed_si(buf +
GSMTAP_HDR_LEN, (int)n - GSMTAP_HDR_LEN); } else if (sub_type ==
GSMTAP_CHANNEL_AGCH) { / (#11) IMM ASSIGN / EXT / REJ + (#SMS)
PAGING REQ 1/2/3 -> agch_buf * (presente sur bloc CCCH -> firmware
chan_nr=0x90 -> gsm48_rr_rx_pch_agch * qui dispatch IMM-ASS vs PAGING
par msg type). 0x21=PAG_REQ_1 (pas SI13). / if (mt == 0x3f || mt ==
0x39 || mt == 0x3a || mt == 0x21 || mt == 0x22 || mt == 0x24)
calypso_dsp_shunt_feed_agch(buf + GSMTAP_HDR_LEN, (int)n -
GSMTAP_HDR_LEN); } else if (sub_type == GSMTAP_CHANNEL_SDCCH4) { /
(#2) SDCCH/4 SS0 DL (UA/AUTH) -> sdcch_buf (presente sur le bloc *
SDCCH/4 SS0, fn%51 in {22-25}). LAPDm : aucun filtre message-type * (le
gate canal = le FN cote si_bridge + le dispatch). / uint32_t sd_fn =
((uint32_t)buf[8] << 24) | ((uint32_t)buf[9] << 16) |
((uint32_t)buf[10] << 8) | (uint32_t)buf[11];
calypso_dsp_shunt_feed_sdcch(buf + GSMTAP_HDR_LEN, (int)n -
GSMTAP_HDR_LEN, sd_fn); } else if (sub_type == GSMTAP_CHANNEL_SACCH) {
/ SACCH SS0 DL : SI5/SI6 REELS (si_bridge fn%51 {42-45}) ->
sacch_buf * REEL (presente fn%51 {42-45}). Remplace la fabrication
SI3->SI6. / calypso_dsp_shunt_feed_sacch(buf + GSMTAP_HDR_LEN,
(int)n - GSMTAP_HDR_LEN); } / autres canaux : drop */ } }
static void shunt_gsmtap_init(void) { if (shunt_grgsm_off()) {
SHUNT_ERR(“gr-gsm COUPE : listener GSMTAP/SI :4730 NON arme” “->
si_buf (BCCH/SI) doit venir du DSP”); return; } const char p =
getenv(“CALYPSO_SHUNT_GSMTAP_PORT”); int port = (p && p) ?
atoi(p) : 4730; int fd = socket(AF_INET, SOCK_DGRAM, 0); if (fd < 0)
{ SHUNT_ERR(“GSMTAP socket() failed: %s”, strerror(errno)); return; }
int one = 1; setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one,
sizeof(one)); struct sockaddr_in sa; memset(&sa, 0, sizeof(sa));
sa.sin_family = AF_INET; sa.sin_port = htons((uint16_t)port);
sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK); if (bind(fd, (struct
sockaddr *)&sa, sizeof(sa)) < 0) { SHUNT_ERR(“GSMTAP bind(:%d)
failed: %s”, port, strerror(errno)); close(fd); return; } g_gsmtap_fd =
fd; qemu_set_fd_handler(fd, shunt_gsmtap_read, NULL, NULL);
SHUNT_ERR(“GSMTAP listener udp:127.0.0.1:%d → feed_si(a_cd)” “(gr-gsm grgsm_decode
-m BCCH y envoie le SI réel)”, port); }
/* —- SCH listener : recoit le BSIC/FN REELS decodes par gr-gsm (= le
DSP) —- * grgsm_relay_decode.py forwarde le tuple (‘sch’,bsic,fn) du
port measurements * de gsm.receiver en UDP {magic ‘SCH1’,
int32 bsic, int32 fn, LE} sur ce port * (CALYPSO_SHUNT_SCH_PORT, defaut
4731 — distinct du GSMTAP 4730). On stocke le * resultat ->
shunt_dispatch_sb encode le VRAI BSIC/FN au lieu de * SHUNT_CANNED_BSIC.
C’est le “DSP qui poste son decode SCH dans le NDB”. */ static int
g_sch_fd = -1;
static void shunt_sch_read(void opaque) { uint8_t buf[64]; for
(;;) { ssize_t n = recv(g_sch_fd, buf, sizeof(buf), MSG_DONTWAIT); if (n
< 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) break; break; }
if (n < 12 || memcmp(buf, “SCH1”, 4) != 0) continue; / pas notre
datagramme / uint32_t bsic_le, fn_le; memcpy(&bsic_le, buf + 4,
4); memcpy(&fn_le, buf + 8, 4); int32_t bsic =
(int32_t)le32_to_cpu(bsic_le); int32_t fn = (int32_t)le32_to_cpu(fn_le);
/ TOA reel : 3e int (>=16 o). Absent (ancien format 12 o) ->
on garde 23. * Clamp a +-64 qbits autour de 23 : au-dela = gr-gsm
desaligne, on retombe * sur 23 (on-time) pour ne pas catastropher
l’alignement firmware. */ int32_t toa = 23; if (n >= 16) { uint32_t
toa_le; memcpy(&toa_le, buf + 12, 4); toa =
(int32_t)le32_to_cpu(toa_le); if (toa < 23 - 64 || toa > 23 + 64)
toa = 23; } bool first = !g_shunt.sb_valid; g_shunt.sb_bsic =
(uint8_t)(bsic & 0x3f); g_shunt.sb_fn = (uint32_t)fn; g_shunt.sb_toa
= (int16_t)toa; g_shunt.sb_valid = true; static unsigned schlog = 0; if
(first || schlog++ < 20 || (schlog % 200) == 0) SHUNT_LOG(“SCH reel
(gr-gsm): BSIC=%d” “(ncc=%d bcc=%d) FN=%d TOA=%d%s”,
(int)g_shunt.sb_bsic, (g_shunt.sb_bsic >> 3) & 7,
g_shunt.sb_bsic & 7, (int)fn, (int)g_shunt.sb_toa, first ? ” [1er]”
: ““);
/* [2026-07-25] FN-ALIGN probe : mesure PROPRE de l'offset horloge.
* Le DSP suit s->fn (calypso_trx_get_fn) ; le SCH porte la FN REELLE du
* BTS. delta = sch_fn - trx_fn = l'offset a recaler (un vrai mobile cale
* son horloge sur le SCH ; ici le DSP garde l'horloge TRX offset).
* delta constant sur les SCH -> offset fixe pose a l'init (fix 1 ligne).
* toa = offset residuel intra-burst en qbits (23 = on-time). */
{
uint32_t trx_fn = calypso_trx_get_fn();
int32_t d = (int32_t)((uint32_t)fn - trx_fn);
static unsigned an = 0;
if (an++ < 60 || (an % 200) == 0)
fprintf(stderr, "[feed-daram-dsp] FN-ALIGN sch_fn=%u trx_fn=%u "
"delta=%d sch%%51=%u toa=%d\n",
(unsigned)fn, trx_fn, d, (unsigned)((uint32_t)fn % 51),
(int)g_shunt.sb_toa);
}
}
}
/* [2026-07-28] CALYPSO_SHUNT_NO_GRGSM : voir en-tete du patch. /
static bool shunt_grgsm_off(void) { static int v = -1; if (v < 0) {
const char e = getenv(“CALYPSO_SHUNT_NO_GRGSM”); v = (e &&
*e == ‘1’) ? 1 : 0; } return v != 0; }
static void shunt_sch_init(void) { if (shunt_grgsm_off()) {
SHUNT_ERR(“gr-gsm COUPE (CALYPSO_SHUNT_NO_GRGSM=1) : listener SCH :4731
NON arme” “-> sb_bsic/sb_fn/sb_toa doivent venir du DSP”); return; }
const char p = getenv(“CALYPSO_SHUNT_SCH_PORT”); int port = (p
&& p) ? atoi(p) : 4731; int fd = socket(AF_INET,
SOCK_DGRAM, 0); if (fd < 0) { SHUNT_ERR(“SCH socket() failed: %s”,
strerror(errno)); return; } int one = 1; setsockopt(fd, SOL_SOCKET,
SO_REUSEADDR, &one, sizeof(one)); struct sockaddr_in sa;
memset(&sa, 0, sizeof(sa)); sa.sin_family = AF_INET; sa.sin_port =
htons((uint16_t)port); sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK); if
(bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
SHUNT_ERR(“SCH bind(:%d) failed: %s”, port, strerror(errno)); close(fd);
return; } g_sch_fd = fd; qemu_set_fd_handler(fd, shunt_sch_read, NULL,
NULL); SHUNT_ERR(“SCH listener udp:127.0.0.1:%d → feed_sb(BSIC/FN reels” “gr-gsm) →
shunt_dispatch_sb (remplace SHUNT_CANNED_BSIC)”, port); }
/*
========================================================================
* Buffers partages (shm) — gr-gsm AU MILIEU du shunt DSP (pas de
FIFO/UDP). * ENTREE du DSP shunte : l’I/Q que la BSP livre (DARAM
0x2a00) est recopiee * ici via calypso_dsp_shunt_feed_iq() ; gr-gsm la
LIT. * SORTIE du DSP shunte : gr-gsm ECRIT le SI decode ; le shunt le
LIT au frame * tick (shunt_poll_si_shm) et le pousse dans a_cd ->
l’ARM le lit. * Semantique BUFFER (pas fifo) : un compteur de sequence
par sens ; le lecteur * poll le seq et ne consomme que s’il a change.
shm POSIX /calypso_dsp_shunt. *
======================================================================
/ #define SHM_NAME “/calypso_dsp_shunt” #define SHM_IQ_SLOTS 64
/ ring de bursts (absorbe les stalls du decode gr-gsm) /
#ifndef SHM_IQ_LEN #define SHM_IQ_LEN 320 / int16 par slot (>=
296 = 148 complexes cs16) */ #endif
struct shm_iq_slot { uint32_t fn; /* frame number du burst /
uint32_t n; / nb d’int16 valides (I,Q entrelaces) */ int16_t
iq[SHM_IQ_LEN]; };
struct dsp_shunt_shm { uint32_t magic; /* 0x43445350 = ‘CDSP’ /
/ — ENTREE : ring de bursts I/Q (shunt ecrit <- BSP, gr-gsm lit)
— / volatile uint32_t iq_wr; / nb total de bursts ecrits
(compteur write) / struct shm_iq_slot iq[SHM_IQ_SLOTS]; / —
SORTIE : SI decode (gr-gsm ecrit, shunt lit -> a_cd) — / volatile
uint32_t si_seq; / bumpe a chaque nouveau SI decode / uint32_t
si_len; / octets L2 (<=23) */ uint8_t si[32]; };
static struct dsp_shunt_shm g_shm; / [2026-07-27] FB-STREAM
: ring d’echantillons FCCH decimes (I/Q entrelaces) * que le DSP
consomme via intercept de lecture 0x9213/0x9215 (c54x.c). * Remplit une
VRAIE fenetre dans le workzone 0x2a00. FBS_RING = pow2. / #define
FBS_RING 16384 static int16_t g_fbs[FBS_RING]; static uint32_t g_fbs_wr,
g_fbs_rd; static uint32_t g_shm_last_si_seq; static FILE
g_iq_cfile2; /* cfile #2 FN-espace (zero-fill) -> test grgsm
SACCH / static int g_iq_fd = -1; / fd brut I/Q : fichier ou
FIFO live / static int g_iq_is_fifo = 0; / 1 = FIFO -> non
bloquant + drop / static char g_iq_path[256]; / chemin memorise
pour retry FIFO / static FILE g_iq_rec; /* record disque .cfile
contigu (rejeu), EN PLUS du live */
static void shunt_shm_init(void) { int fd = shm_open(SHM_NAME,
O_CREAT | O_RDWR, 0666); if (fd < 0) { SHUNT_ERR(“shm_open(%s): %s”,
SHM_NAME, strerror(errno)); return; } if (ftruncate(fd, sizeof(struct
dsp_shunt_shm)) != 0) { SHUNT_ERR(“ftruncate shm: %s”, strerror(errno));
close(fd); return; } void *m = mmap(NULL, sizeof(struct dsp_shunt_shm),
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); close(fd); if (m ==
MAP_FAILED) { SHUNT_ERR(“mmap shm: %s”, strerror(errno)); return; }
g_shm = m; g_shm->magic = 0x43445350; g_shm_last_si_seq =
g_shm->si_seq; SHUNT_ERR(“shm %s (=/dev/shm%s, %zu o) : I/Q in
(feed_iq->gr-gsm)” “+ SI out (gr-gsm->a_cd). gr-gsm AU MILIEU du
shunt.”, SHM_NAME, SHM_NAME, sizeof(struct dsp_shunt_shm));
/* Enregistrement .cfile (gr_complex fc32 I,Q normalise) de l'I/Q d'entree
* du DSP shunte, pour rejeu deterministe (grgsm_cfile_decode.py). Defaut
* /tmp/dsp_iq.cfile ; CALYPSO_SHUNT_IQ_CFILE= (vide) pour desactiver. */
const char *cf = getenv("CALYPSO_SHUNT_IQ_CFILE");
if (!cf)
cf = "/root/dsp_iq.cfile";
if (*cf) {
struct stat st;
g_iq_is_fifo = (stat(cf, &st) == 0 && S_ISFIFO(st.st_mode));
snprintf(g_iq_path, sizeof(g_iq_path), "%s", cf);
if (g_iq_is_fifo) {
g_iq_fd = open(cf, O_WRONLY | O_NONBLOCK); /* FIFO : jamais bloquant, pas de create */
if (g_iq_fd >= 0)
SHUNT_ERR("I/Q -> %s (FIFO live fc32, non bloquant)", cf);
else if (errno == ENXIO)
SHUNT_ERR("FIFO %s sans lecteur — open differe au feed", cf);
else
SHUNT_ERR("open(%s) FIFO: %s", cf, strerror(errno));
} else {
g_iq_fd = open(cf, O_WRONLY | O_CREAT | O_TRUNC, 0644); /* cfile rejeu */
if (g_iq_fd >= 0)
SHUNT_ERR("enregistre l'I/Q -> %s (cfile fc32)", cf);
else
SHUNT_ERR("open(%s) cfile: %s", cf, strerror(errno));
}
}
/* Record disque .cfile contigu (capture brute fc32) EN PLUS de la sortie live :
* la FIFO sert au live (FFT) sans rien garder, ce record garde tout pour le
* rejeu deterministe (grgsm_cfile_decode.py). Fichier regulier -> fwrite jamais
* bloquant. Defaut /dev/shm/dsp_iq.cfile ; CALYPSO_SHUNT_IQ_RECORD= (vide) pour
* desactiver. On evite le double-open si le record vise le meme fichier que la
* sortie live (cas live=fichier, pas FIFO). */
const char *rec = getenv("CALYPSO_SHUNT_IQ_RECORD");
if (!rec)
rec = "/dev/shm/dsp_iq.cfile";
if (*rec && !(g_iq_fd >= 0 && !g_iq_is_fifo && strcmp(rec, g_iq_path) == 0)) {
g_iq_rec = fopen(rec, "wb");
if (g_iq_rec)
SHUNT_ERR("record disque I/Q -> %s (cfile fc32 contigu)", rec);
else
SHUNT_ERR("fopen(%s) record: %s", rec, strerror(errno));
}
/* cfile #2 : reconstruction FN-espacee (zero-fill des trames manquantes) pour
* que grgsm retrouve la 51-mf et decode la SACCH (SI5/SI6). Test offline, ne
* touche PAS au cfile live. Active via CALYPSO_SHUNT_IQ_CFILE2=<chemin>. */
const char *cf2 = getenv("CALYPSO_SHUNT_IQ_CFILE2");
if (cf2 && *cf2) {
g_iq_cfile2 = fopen(cf2, "wb");
if (g_iq_cfile2)
SHUNT_ERR("cfile #2 FN-espace -> %s (gap zero-fill)", cf2);
}
}
/* ENTREE du DSP shunte : la BSP appelle ceci avec l’I/Q DL (cs16, n
int16 * entrelaces I,Q) qu’elle DMA dans la DARAM. Publie dans le shm
pour gr-gsm. / / [2026-07-22] Injection READ-SIDE FB/SB REELS.
Voir header. La detection * (feed_iq) met a jour g_shunt.rx_* ; c’est
ICI, sur le read MMIO ARM, qu’on * livre la valeur -> aucune
dependance de timing write/read intra-trame. * FB (NDB) : 0x01F0
d_fb_det, 0x01F4 TOA, 0x01F6 PM, 0x01F8 ANGLE, 0x01FA SNR * SB (db_r) :
0x0060 (page0) / 0x0088 (page1) a_serv_demod[D_TOA] / / @BEQUILLE — DECAN (effet de bord : allume
SHUNT_REAL_FB) (CALYPSO_DECAN=1) * masque : DECAN=1 suffit a allumer
l’intercept ci-dessous, et cette fonction * n’a AUCUNE garde
g_shunt.active : elle est appelee depuis * calypso_trx.c sur chaque read
MMIO ARM 16 bits. Or calypso_native.env * et calypso_native_helped.env
posent DECAN:=1 -> en mode “natif”, le * d_fb_det lu par l’ARM vient
de l’HOTE, jamais de l’API-RAM native. * Le mode natif ne mesure donc
pas le natif. * retirer : DECAN!=1 ET SHUNT_REAL_FB!=1 ET SHUNT_LEGIT!=1
(les 3 fallbacks), * ou ajouter une garde g_shunt.active en tete de
fonction. / bool calypso_dsp_shunt_real_fb_read(uint32_t off,
uint16_t out) { static int real_fb = -1; if (real_fb < 0) { /*
@BEQUILLE — SHUNT_REAL_FB (intercept du
resultat) (CALYPSO_SHUNT_REAL_FB, defaut OFF) * masque : le correlateur
DSP natif. Le resultat FB est calcule COTE HOTE * et court-circuite ce
que le DSP aurait produit. * retirer : quand d_fb_det natif est ecrit
par le DSP. * ⚠️ Cette bequille MASQUE le natif : ne jamais l activer
pour juger de * l etat du mode natif. Seule data[0x08f8] via
DETECTOR-RUN le mesure. / const char e =
getenv(“CALYPSO_SHUNT_REAL_FB”); const char dm =
getenv(“CALYPSO_DECAN”); / master DECAN implique REAL_FB /
real_fb = ((e && e == ‘1’) || (dm && dm[0] == ‘1’))
? 1 : 0; /* [2026-07-26] SHUNT_LEGIT implique l’intercept de lecture :
c’est LUI * qui livre rx_fb_det/rx_snr (detection gr-gsm) a l’ARM. Sans
ca, le * feed legit n’atteint jamais la lecture ARM. / if (!real_fb)
{ const char l = getenv(“CALYPSO_SHUNT_LEGIT”); real_fb = (l
&& l == ‘1’) ? 1 : 0; } } if (!real_fb) return false; switch
(off) { case 0x01F0: out = g_shunt.rx_fb_det ? 1 : 0; return true;
/* d_fb_det / case 0x01F4: out = g_shunt.rx_toa; return true;
/* a_sync TOA / case 0x01F6: out = g_shunt.last_pm; return
true; /* a_sync PM / case 0x01F8: out =
(uint16_t)g_shunt.rx_afc; return true; /* a_sync ANGLE(AFC) / case
0x01FA: out = g_shunt.rx_snr; return true; /* a_sync SNR /
/ SB via db_r a_serv_demod[D_TOA] (page0/page1) : seulement si SB
reel poste / case 0x0060: case 0x0088: if (g_shunt.sb_valid) {
out = g_shunt.rx_toa; return true; } return false; default: return
false; } }
void calypso_dsp_shunt_feed_iq(uint32_t fn, const int16_t iq, int
n) { if (!iq || n <= 0) return; if (!g_shm &&
!(shunt_route_c54x() && g_shunt.c54x)) return; / sans shm
ET sans route c54x, rien a faire / if (n > SHM_IQ_LEN) n =
SHM_IQ_LEN; / PM REEL : magnitude moyenne (MAV) du burst DL ->
g_shunt.last_pm. Pas de * sqrt/math.h ; signal-derive, plus de 0x7000
canne. Le dispatch l’ecrit * dans a_serv_demod[D_PM] -> rxlev reel
cote firmware. */ { 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); g_shunt.last_pm = (mav
> 0xffff) ? 0xffff : (uint16_t)mav; }
/* [2026-07-26 golive-mac] ROOT-CAUSE d_fb_det=0 : le kernel FB natif
* (reroute 0x94f5 -> a076) walk la DARAM 0x2a00 mais y lit une CONSTANTE
* DC 0x12ed (writer rx_burst degenere) -> MAC sur signal plat -> det=0.
* feed_iq DETIENT les vrais samples FCCH (coh=0.999). On les ecrit DIRECT
* en DARAM 0x2a00, DECIMES ->1-SPS (ce que le kernel attend).
* Gate CALYPSO_FB_IQ_DARAM ; FCCH-only si CALYPSO_FB_IQ_FCCH_ONLY.
* (Mettre CALYPSO_BSP_DIRECT_FEED=0 pour tuer le writer 0x12ed concurrent.) */
{
/* @BEQUILLE — FB_IQ_DARAM (+ _BASE, _FCCH_ONLY) (CALYPSO_FB_IQ_DARAM, atoi>0,
* defaut OFF ; native_helped.env:=1)
* masque : le DMA on-chip RX -> DARAM. feed_iq ecrit 0x128 mots d'IQ decime
* DIRECTEMENT dans g_shunt.c54x->data[base..], hors data_write — donc
* invisible de WATCH_2A00 / WATCH_9200 / WMAP.
* retirer : quand la chaine BSP -> BDLENA -> DARAM alimente le buffer seule
* (writer 0x12ed non degenere).
*/
static int _fid = -1, _decim = 4, _fcch = 0;
if (_fid < 0) {
const char *e = getenv("CALYPSO_FB_IQ_DARAM"); _fid = (e && atoi(e) > 0) ? 1 : 0;
const char *d = getenv("CALYPSO_BSP_IQ_DECIM"); if (d && *d) _decim = atoi(d);
if (_decim < 1) _decim = 1;
const char *f = getenv("CALYPSO_FB_IQ_FCCH_ONLY"); _fcch = (f && atoi(f) > 0) ? 1 : 0;
}
static uint16_t _iqbase = 0;
if (_iqbase == 0) {
const char *b = getenv("CALYPSO_FB_IQ_BASE");
_iqbase = (b && *b) ? (uint16_t)strtol(b, NULL, 0) : 0x2a00;
}
/* [2026-07-27 diag] HUNK-ENTER : prouve si feed_iq atteint le hunk marker,
* et expose les gardes (fid/c54x/data) qui decident l ecriture 0x2a00. */
{ static unsigned _he = 0;
if (_he++ < 20)
fprintf(stderr, "[feed-daram-dsp] HUNK-ENTER fid=%d c54x=%p data=%p n=%d fn=%u\n",
_fid, (void*)g_shunt.c54x,
g_shunt.c54x ? (void*)g_shunt.c54x->data : (void*)0, n, fn); }
int _p = (int)(fn % 51);
/* [2026-07-26] FCCH positions {1,11,21,31,41} (offset +1 vs canon 0/10/20/30/40,
* confirme par FN-ALIGN sch%51=1,21,31,41). */
int _is_fcch = (_p % 10 == 1) && (_p <= 41);
/* @BEQUILLE — FB_IQ_MARKER (CALYPSO_FB_IQ_MARKER, atoi>0, defaut OFF)
* masque : rien de reel — remplace l'IQ par une RAMPE 0x1000+woff pour tester
* la reachabilite de la vue DARAM du noyau. Court-circuite la branche
* IQ reelle (else if) et ignore FCCH_ONLY.
* retirer : des que la reachabilite est etablie ; ne jamais laisser en run.
*/
static int _mark = -1;
if (_mark < 0) { const char *m = getenv("CALYPSO_FB_IQ_MARKER"); _mark = (m && atoi(m) > 0) ? 1 : 0; }
if (_fid && _mark && g_shunt.c54x && g_shunt.c54x->data) {
/* TEST REACHABILITE : ecrit une RAMPE 0x1000+woff a CHAQUE frame (ignore
* iq + fcch). Si IQ-READ voit la rampe -> feed_iq atteint bien la vue
* DARAM du kernel (probleme = contenu iq). Sinon -> mismatch objet/mapping. */
uint16_t base = _iqbase; int dl = 0x128;
for (int woff = 0; woff < dl; woff++)
g_shunt.c54x->data[base + woff] = (uint16_t)(0x1000 + woff);
static unsigned _lm = 0;
if (_lm++ < 8)
fprintf(stderr, "[feed-daram-dsp] FB-IQ-MARKER fn=%u c54x=%p wrote RAMP 0x1000.. "
"base[0]=0x%04x base[1]=0x%04x base[2]=0x%04x\n", fn, (void*)g_shunt.c54x,
g_shunt.c54x->data[base], g_shunt.c54x->data[base+1], g_shunt.c54x->data[base+2]);
} else if (_fid && g_shunt.c54x && g_shunt.c54x->data && (!_fcch || _is_fcch)) {
uint16_t base = _iqbase; int dl = 0x128; int woff = 0;
for (int k = 0; 2*(k*_decim)+1 < n && woff < dl; k++) {
g_shunt.c54x->data[base + woff++] = (uint16_t)iq[2*(k*_decim)];
if (woff < dl)
g_shunt.c54x->data[base + woff++] = (uint16_t)iq[2*(k*_decim)+1];
}
static unsigned _l = 0;
if (_l++ < 16)
fprintf(stderr, "[feed-daram-dsp] FB-IQ-DARAM fn=%u p=%d wrote=%d decim=%d "
"s0=0x%04x s1=0x%04x s2=0x%04x\n", fn, _p, woff, _decim,
g_shunt.c54x->data[base], g_shunt.c54x->data[base+1],
g_shunt.c54x->data[base+2]);
}
}
/* [2026-07-27] FB-STREAM push : pousse l'IQ FCCH decime dans le ring que
* l'intercept 0x9213/0x9215 (c54x.c) sert au demod. Gate CALYPSO_FB_STREAM. */
{
static int _fs = -1, _fsd = 4;
/* @BEQUILLE — FB_STREAM (alimentation du ring) (CALYPSO_FB_STREAM, defaut OFF)
* masque : cote emetteur du meme contournement — pousse l IQ decime dans
* le ring que l intercept de lecture sert au demod.
* retirer : en meme temps que l intercept de lecture. */
if (_fs < 0) { const char *e = getenv("CALYPSO_FB_STREAM"); _fs = (e && atoi(e) > 0) ? 1 : 0;
const char *d = getenv("CALYPSO_FB_STREAM_DECIM"); if (d && *d) _fsd = atoi(d); if (_fsd < 1) _fsd = 1; }
if (_fs) {
/* [2026-07-27] SKIP frames all-zero (startup fn 0-4) : elles polluent le
* ring que le demod lit au front -> il tombe sur des zeros au lieu de la
* vraie FCCH poussee ensuite. On ne pousse que si la frame a du signal. */
int _nz = 0;
for (int i = 0; i < n && i < 64; i++) if (iq[i]) { _nz = 1; break; }
if (_nz) {
for (int k = 0; 2*(k*_fsd)+1 < n; k++) {
g_fbs[g_fbs_wr++ & (FBS_RING-1)] = iq[2*(k*_fsd)];
g_fbs[g_fbs_wr++ & (FBS_RING-1)] = iq[2*(k*_fsd)+1];
}
}
}
}
/* [2026-07-22] Detection FCCH REELLE (gate CALYPSO_SHUNT_REAL_FB) : coherence
* + dphi sur la vraie RX -> d_fb_det/AFC/SNR/TOA reels (bypass go-live DSP). */
{
/* @BEQUILLE — SHUNT_REAL_FB (calcul hote de rx_fb_det/AFC/SNR/TOA)
* (CALYPSO_SHUNT_REAL_FB=1 ou master CALYPSO_DECAN=1)
* masque : le correlateur DSP. Coherence + dphi calcules cote hote sur l'I/Q RX
* -> g_shunt.rx_* -> livres a l'ARM par real_fb_read.
* retirer : quand data[0x08f8] est ecrit par le DSP.
* ATTENTION : DECAN=1 SUFFIT a l'allumer, et native/native_helped/shunt_legit/
* shunt_no_legit posent tous DECAN=1 : mettre SHUNT_REAL_FB=0 ne coupe
* RIEN.
*/
static int real_fb = -1;
if (real_fb < 0) { const char *e = getenv("CALYPSO_SHUNT_REAL_FB"); const char *dm = getenv("CALYPSO_DECAN"); real_fb = ((e && *e == '1') || (dm && dm[0] == '1')) ? 1 : 0; } /* master DECAN implique REAL_FB */
if (real_fb) {
int nc = n / 2;
if (nc >= 8) {
double ar = 0, ai = 0, den = 0;
for (int k = 1; k < nc; 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;
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);
/* [DECAN/AFC-fix v2 2026-07-26] IQ @4 SPS (CONSTAT empirique : le
* burst le plus coherent, coh~0.9995, sort dphi~=0.39=pi/8, PAS
* pi/2). A 4 SPS le ton FCCH +fc/4 = +22.5deg = +pi/8 par sample ;
* fs = 4*270833 = 1083333. Nominal FIXE pi/8 (mon pi/2 v1 -> residu
* geant -> Angle=-32kHz hors capture -> FCCH jamais lock). */
double resid = dphi - M_PI/8.0; /* residu de phase/sample (rad) */
if (resid > M_PI) resid -= 2.0*M_PI;
if (resid < -M_PI) resid += 2.0*M_PI;
/* det = VRAI FCCH : ton pur (coh haute) ET proche du nominal (dans
* la fenetre de capture FB0 +/-20kHz -> |resid|<0.13). On ne met a
* jour l'AFC/SNR QUE sur le FCCH : feed_iq tourne sur CHAQUE burst,
* et un burst DATA a un dphi aleatoire (ex coh=0.96/dphi=0.062, pas
* le FCCH) qu'on injectait avant comme garbage -> AFC divergeait. */
int det = (coh > 0.95) && (fabs(resid) < 0.13);
g_shunt.rx_fb_det = det;
if (det) {
/* ANGLE fidele : Df_Hz = resid*fs/(2pi), fs=1083333 ;
* angle = Df*65536/86208 = resid*131072. BTS cale => residu~0
* => angle~0 => AFC converge (ANGLE=0 canne, mais MESURE). */
/* [AFC loop-close 2026-07-26] FERME la boucle : le firmware
* afc_correct enroule d_afc ; le modele twl3025 en deduit la
* frequence DEJA compensee (get_afc_hz) -> on la SOUSTRAIT de la
* mesure brute. Sans ca la mesure reste ~constante et le DAC
* s'enroule sans fin (-700 -> -1800...). Avec, l'erreur effective
* -> 0 => convergence (gain~1, init -700). fs=1083333 (4 SPS). */
double raw_hz = resid * (1083333.0 / (2.0 * M_PI));
g_rx_raw_hz = raw_hz; g_rx_raw_valid = 1; /* memo pour recompute per-tick */
double eff_hz = raw_hz - calypso_twl3025_get_afc_hz();
double a = eff_hz * (65536.0 / 86208.0);
if (a > 32767.0) a = 32767.0;
if (a < -32768.0) a = -32768.0;
g_shunt.rx_afc = (int16_t)a;
/* SNR fx6.10 (word=snr_dB*1024, seuil 2560=2.5dB) depuis M=coh^2. */
double M = coh * coh;
if (M > 0.9999) M = 0.9999;
double snr_db = 10.0 * log10(M / (1.0 - M));
if (snr_db < 0.0) snr_db = 0.0;
if (snr_db > 30.0) snr_db = 30.0;
int w = (int)(snr_db * 1024.0);
if (w > 0x7FFF) w = 0x7FFF;
g_shunt.rx_snr = (uint16_t)w;
}
/* det=0 (burst non-FCCH) : on GARDE le dernier rx_afc/rx_snr lockes
* (pas de garbage data). */
g_shunt.rx_toa = 23;
/* [2026-07-22] Ecrit d_fb_det+sync dans le NDB PAR FRAME (comme le vrai
* DSP qui tourne 12 frames apres 1 dispatch) -> l'ARM lit 1 sur chaque
* attempt (il le remet a 0 apres lecture, prim_fbsb.c:318). Fix desync. */
/* Ecrit dsp->data[] DIRECT (comme shunt_route_to_c54x l.396) : le
* shunt_write_w/dma_memory_write ne mirror PAS vers dsp->data ou
* l'ARM+fbsb lisent (0x08F8..). DSP-words : d_fb_det=0x08F8,
* a_sync_demod TOA=0x08FA PM=0x08FB ANG=0x08FC SNR=0x08FD. */
static unsigned _wl = 0;
if (_wl++ < 12)
fprintf(stderr, "[feed-daram-dsp] FB-WRITE-DBG det=%d c54x=%p data=%p api=%p\n",
det, (void*)g_shunt.c54x,
g_shunt.c54x ? (void*)g_shunt.c54x->data : (void*)0,
g_shunt.c54x ? (void*)g_shunt.c54x->api_ram : (void*)0);
/* [2026-07-22] Write async dsp->data[] SUPPRIME : la livraison
* FB/SB se fait desormais READ-SIDE (calypso_dsp_shunt_real_fb_read
* appelee par calypso_dsp_read), immunisee contre l'ordonnancement
* intra-trame. feed_iq ne fait QUE mettre a jour g_shunt.rx_*. */
static unsigned rfl = 0;
if (rfl < 20 || (det && rfl < 300)) {
fprintf(stderr, "[feed-daram-dsp] REAL-FB fn=%u nc=%d coh=%.3f dphi=%.3f "
"det=%d SNR=0x%04x AFC=%d\n", fn, nc, coh, dphi, det,
g_shunt.rx_snr, g_shunt.rx_afc);
rfl++;
}
/* [2026-07-28] SHUNT_DSP_FB : voir en-tete du patch. */
{
/* @BEQUILLE — SHUNT_DSP_FB (+ _ENTRY / _BUDGET / _MAX / _SP)
* (CALYPSO_SHUNT_DSP_FB, EQ1, defaut OFF)
* masque : l'ORDONNANCEMENT natif du correlateur. Le shunt sauvegarde le
* contexte c54x, force PC=ENTRY et SP=scratch, execute BUDGET
* instructions hors trame, puis restaure — le DSP n'a jamais decide
* d'entrer la.
* retirer : quand le dispatcher natif (frame-IT -> 0x8341 -> correlateur) atteint
* l'entree seul.
* NB : niche dans le bloc real_fb : inerte si DECAN=0 ET SHUNT_REAL_FB!=1.
*/
static int _sdf = -1; static uint16_t _entry = 0x94f5;
static int _budget = 20000; static unsigned _sdfn = 0;
static unsigned _sdfmax = 40; /* borne totale d excursions */
static uint16_t _spscratch = 0x5000; /* pile dediee a l excursion */
if (_sdf < 0) {
const char *e = getenv("CALYPSO_SHUNT_DSP_FB");
_sdf = (e && *e == '1') ? 1 : 0;
const char *p = getenv("CALYPSO_SHUNT_DSP_FB_ENTRY");
if (p && *p) _entry = (uint16_t)strtol(p, NULL, 0);
const char *b = getenv("CALYPSO_SHUNT_DSP_FB_BUDGET");
if (b && *b) _budget = atoi(b);
const char *m = getenv("CALYPSO_SHUNT_DSP_FB_MAX");
if (m && *m) _sdfmax = (unsigned)atoi(m);
const char *sp = getenv("CALYPSO_SHUNT_DSP_FB_SP");
if (sp && *sp) _spscratch = (uint16_t)strtol(sp, NULL, 0);
if (_sdf)
SHUNT_ERR("SHUNT_DSP_FB arme : entree=0x%04x budget=%d "
"(correlateur DSP pilote par le shunt ; REAL_FB reste l oracle)",
_entry, _budget);
}
/* borne stricte : au-dela, inerte — sinon on affame osmocon/mobile (28/07). */
if (_sdf && g_shunt.c54x && _sdfn < _sdfmax) {
C54xState *_d = g_shunt.c54x;
/* --- sauvegarde du contexte --- */
uint16_t _pc = _d->pc, _xpc = _d->xpc, _sp = _d->sp;
uint16_t _st0 = _d->st0, _st1 = _d->st1, _t = _d->t;
int64_t _a = _d->a, _b = _d->b;
bool _idle = _d->idle;
uint16_t _ar[8];
for (int _k = 0; _k < 8; _k++) _ar[_k] = _d->ar[_k];
/* --- excursion bornee dans le correlateur --- */
/* pile DEDIEE : l excursion ne doit jamais ecrire dans la pile du DSP
* (restaurer SP ne restaure pas le CONTENU ecrase). */
_d->sp = _spscratch;
_d->pc = _entry; _d->idle = false; _d->running = true;
c54x_run(_d, _budget);
_sdfn++;
if (_sdfn <= _sdfmax) {
fprintf(stderr, "[feed-daram-dsp] DSP-FB fn=%u PC=0x%04x "
"A=0x%010llx B=0x%010llx T=%04x AR3=%04x AR4=%04x AR5=%04x AR6=%04x\n"
" wz[2c00..07]=%04x %04x %04x %04x %04x %04x %04x %04x"
" | ORACLE det=%d coh=%.3f\n",
fn, _d->pc,
(unsigned long long)(_d->a & 0xFFFFFFFFFFULL),
(unsigned long long)(_d->b & 0xFFFFFFFFFFULL),
_d->t, _d->ar[3], _d->ar[4], _d->ar[5], _d->ar[6],
_d->data[0x2c00], _d->data[0x2c01], _d->data[0x2c02],
_d->data[0x2c03], _d->data[0x2c04], _d->data[0x2c05],
_d->data[0x2c06], _d->data[0x2c07], det, coh);
}
/* --- restauration : le DSP retrouve son etat exact --- */
_d->pc = _pc; _d->xpc = _xpc; _d->sp = _sp;
_d->st0 = _st0; _d->st1 = _st1; _d->t = _t;
_d->a = _a; _d->b = _b; _d->idle = _idle;
for (int _k = 0; _k < 8; _k++) _d->ar[_k] = _ar[_k];
}
}
}
}
}
/* CALYPSO_DSP=c54x : stash du dernier burst (cs16 I,Q) ; rejoue dans
* bsp_buf depuis shunt_route_to_c54x() au frame tick. */
if (shunt_route_c54x() && g_shunt.c54x) {
int m = (n > SHM_IQ_LEN) ? SHM_IQ_LEN : n;
memcpy(g_shunt.last_iq, iq, (size_t)m * sizeof(int16_t));
g_shunt.last_iq_n = m;
g_shunt.last_iq_fn = fn;
g_shunt.last_iq_valid = true;
}
if (g_shm) {
struct shm_iq_slot *slot = &g_shm->iq[g_shm->iq_wr % SHM_IQ_SLOTS];
slot->fn = fn;
slot->n = (uint32_t)n;
memcpy(slot->iq, iq, (size_t)n * sizeof(int16_t));
__sync_synchronize();
g_shm->iq_wr++; /* publie le burst (le lecteur poll iq_wr) */
}
/* Sorties fc32 (I,Q normalise) : (a) live -> FIFO (FFT, drop) ou fichier, via
* g_iq_fd ; (b) record disque contigu -> g_iq_rec (rejeu deterministe). fbuf
* calcule une seule fois et diffuse aux deux. */
if (g_iq_is_fifo && g_iq_fd < 0)
g_iq_fd = open(g_iq_path, O_WRONLY | O_NONBLOCK); /* retry : le lecteur est-il apparu ? */
if (g_iq_fd >= 0 || g_iq_rec) {
float fbuf[SHM_IQ_LEN];
for (int i = 0; i < n; i++)
fbuf[i] = (float)iq[i] / 32768.0f;
if (g_iq_fd >= 0) {
ssize_t w = write(g_iq_fd, fbuf, (size_t)n * sizeof(float));
if (w < 0 && g_iq_is_fifo && (errno == EAGAIN || errno == EWOULDBLOCK)) {
/* pipe plein -> drop ce burst (FFT live, perte tolerable) */
} else if (w < 0 && g_iq_is_fifo && (errno == EPIPE || errno == ENXIO)) {
close(g_iq_fd); g_iq_fd = -1; /* lecteur parti -> on reessaiera */
}
}
if (g_iq_rec) /* record disque : jamais bloquant */
fwrite(fbuf, sizeof(float), (size_t)n, g_iq_rec);
}
/* cfile #2 FN-espace : chaque burst TS0 a sa position de trame
* ((fn-base)*spf int16), trames manquantes zero-fillees -> grgsm retrouve la
* 51-mf -> SACCH (SI5/SI6) decodable. spf = int16/trame TDMA (def 2500=1x,
* sweepable via CALYPSO_IQ_CFILE_SPF pour le test offline). */
if (g_iq_cfile2) {
static int spf = -1; static uint32_t base_fn = 0; static int64_t pos = 0; static int have_base = 0;
if (spf < 0) { const char *e = getenv("CALYPSO_IQ_CFILE_SPF"); spf = (e && *e) ? atoi(e) : 2500; }
if (!have_base) { base_fn = fn; pos = 0; have_base = 1; }
int64_t target = (int64_t)fn - (int64_t)base_fn;
if (target < 0) target += 2715648; /* hyperframe wrap */
target *= spf;
int64_t gap = target - pos;
if (gap < 0 || gap > (int64_t)spf * 300) { base_fn = fn; pos = 0; gap = 0; } /* rebase si saut anormal */
static const float zeros[512] = {0};
while (gap > 0) { int c = gap > 512 ? 512 : (int)gap; fwrite(zeros, sizeof(float), (size_t)c, g_iq_cfile2); pos += c; gap -= c; }
float fbuf2[SHM_IQ_LEN];
for (int i = 0; i < n; i++) fbuf2[i] = (float)iq[i] / 32768.0f;
fwrite(fbuf2, sizeof(float), (size_t)n, g_iq_cfile2);
pos += n;
}
/* cfile #2 FN-espace : chaque burst TS0 a sa position de trame
* ((fn-base)*spf int16), trames manquantes zero-fillees -> grgsm retrouve la
* 51-mf -> SACCH (SI5/SI6) decodable. spf = int16/trame TDMA (def 2500=1x,
* sweepable via CALYPSO_IQ_CFILE_SPF pour le test offline). */
if (g_iq_cfile2) {
static int spf = -1; static uint32_t base_fn = 0; static int64_t pos = 0; static int have_base = 0;
if (spf < 0) { const char *e = getenv("CALYPSO_IQ_CFILE_SPF"); spf = (e && *e) ? atoi(e) : 2500; }
if (!have_base) { base_fn = fn; pos = 0; have_base = 1; }
int64_t target = (int64_t)fn - (int64_t)base_fn;
if (target < 0) target += 2715648; /* hyperframe wrap */
target *= spf;
int64_t gap = target - pos;
if (gap < 0 || gap > (int64_t)spf * 300) { base_fn = fn; pos = 0; gap = 0; } /* rebase si saut anormal */
static const float zeros[512] = {0};
while (gap > 0) { int c = gap > 512 ? 512 : (int)gap; fwrite(zeros, sizeof(float), (size_t)c, g_iq_cfile2); pos += c; gap -= c; }
float fbuf2[SHM_IQ_LEN];
for (int i = 0; i < n; i++) fbuf2[i] = (float)iq[i] / 32768.0f;
fwrite(fbuf2, sizeof(float), (size_t)n, g_iq_cfile2);
pos += n;
}
}
/* SORTIE du DSP shunte : gr-gsm a-t-il ecrit un nouveau SI ? Si oui
-> a_cd. / static void shunt_poll_si_shm(void) { if
(shunt_grgsm_off()) return; / SI shm = ecrit par gr-gsm */ if
(!g_shm) return; uint32_t seq = g_shm->si_seq; if (seq ==
g_shm_last_si_seq) return; __sync_synchronize(); g_shm_last_si_seq =
seq; uint32_t len = g_shm->si_len; if (len == 0 || len >
sizeof(g_shm->si)) return; calypso_dsp_shunt_feed_si(g_shm->si,
(int)len); }
/* —- init : called from machine setup when CALYPSO_DSP_SHUNT=1 —-
/ void calypso_dsp_shunt_init(MemoryRegion system_memory,
AddressSpace as) { / Actif si CALYPSO_DSP_SHUNT=1 OU
CALYPSO_L1=c : dans ce dernier cas le HLE * (calypso_layer1.c) pilote le
FB, mais SB (a_sch) + SI (a_cd) n’existent que * dans le shunt -> on
l’arme aussi pour fournir le chemin réception prouvé * (FB+SB+SI) qui va
jusqu’au LU accept. Le shunt on_frame_tick tourne ~1ms * après le tick
L1=c, donc ses écritures d_fb_det/a_sch/a_cd priment. / / @BEQUILLE — DSP_SHUNT (armement du mock)
(CALYPSO_DSP_SHUNT, EQ1 ; defaut 1 via * CALYPSO_MODE=full-grgsm dans
run.sh, PAS via un calypso.env) masque : le DSP entier. Un
mock cote ARM ecrit d_fb_det / a_sch / a_cd a sa * place. NB : le shunt
s’arme AUSSI via CALYPSO_L1=c ou simplement * CALYPSO_DSP=c54x
(shunt_route_c54x) — donc il est arme meme dans les * profils dits
“natifs”. * retirer : quand le correlateur natif produit d_fb_det seul
(RANK3 leve). / const char env = getenv(“CALYPSO_DSP_SHUNT”);
bool shunt_env_on = (env && strcmp(env, “1”) == 0); if
(!shunt_env_on && !calypso_l1_c_active() &&
!shunt_route_c54x()) { g_shunt.active = false; return; }
g_shunt.active = true;
g_shunt.as = as;
g_shunt.pending = false;
g_shunt.tick_cnt = 0;
/* Overlay the single d_dsp_page word as IO. The rest of the API RAM
* stays as plain RAM that the firmware reads/writes directly. */
MemoryRegion *trigger = g_new0(MemoryRegion, 1);
memory_region_init_io(trigger, NULL, &shunt_ndb_trigger_ops, NULL,
"calypso-dsp-shunt-trigger", 2);
memory_region_add_subregion_overlap(system_memory,
BASE_API_NDB + NDB_D_DSP_PAGE,
trigger,
/*priority=*/10);
/* Pont gr-gsm → a_cd : écoute le SI décodé (GSMTAP) et l'injecte. */
shunt_gsmtap_init();
/* Pont gr-gsm → SB : écoute le BSIC/FN réels (SCH) et les injecte dans
* shunt_dispatch_sb (remplace SHUNT_CANNED_BSIC). */
shunt_sch_init();
/* Buffers shm : gr-gsm au milieu du shunt (I/Q in + SI out, pas de fifo). */
shunt_shm_init();
/* CALYPSO_CANNED : résoudre + ÉNUMÉRER explicitement la dette restante. */
g_canned = shunt_parse_canned();
{
const char *no_canned = getenv("CALYPSO_SHUNT_NO_CANNED");
SHUNT_ERR("CALYPSO_CANNED (dette fabriquée EXPLICITE) : "
"FBDET=%d TOA=%d PM=%d SNR=%d ANGLE=%d CRC=%d "
"[non-canné=valeur réelle/0]. Hors var : BSIC=%s, SI=%s.",
!!(g_canned & CAN_FBDET), !!(g_canned & CAN_TOA),
!!(g_canned & CAN_PM), !!(g_canned & CAN_SNR),
!!(g_canned & CAN_ANGLE), !!(g_canned & CAN_CRC),
"réel via gr-gsm (fallback 63 si pas no-canned)",
(no_canned && *no_canned == '1')
? "réel via feed_si (no-canned, gate si absent)"
: "réel via feed_si (+ fallback legacy possible)");
}
SHUNT_ERR("active — c54x emulator should be skipped, "
"BSP DMA→DARAM should be gated. Watch /tmp/qemu.log for "
"LATCH/DISPATCH lines.");
}
/* Phase-2 hook (IPC integration) — calypso-ipc-device will call this
with * the result of GMSK demod from osmo-trx-ipc instead of canned
values. / void calypso_dsp_shunt_feed_fb_result(int found, int16_t
toa, int16_t pm, int16_t angle, int16_t snr) { / TODO Phase 2 */
(void)found; (void)toa; (void)pm; (void)angle; (void)snr; }
/* Point d’injection COMMUN du SI réel (2026-06-02) : gr-gsm (via
pont) OU la * démod C native appellent ceci avec une frame L2 de 23
octets décodée depuis * l’I/Q réel du BTS. Le shunt l’écrit ensuite dans
a_cd (shunt_dispatch_allc) * à la place du SI3 canned → “sans hack”,
vrai signal. len doit être 23 (XCCH * L2). Réécrit à chaque nouveau SI
(rotation SI1/2/3/4 du BCCH). / / [2026-07-22] DE-ALIAS du
d_burst_d. RACINE du jitter burst-ID : g_shunt.d_burst_d * etait capture
dans shunt_latch_task (horloge d_dsp_page/scenario) qui SOUS- *
ECHANTILLONNE le flux commande NB propre 0,1,2,3 -> sequence aliasee
periode-12, * phase figee au boot (non-deterministe). Ici on
capture+mirror A CHAQUE ecriture * ARM de la write-page d_burst_d
(WP_D_BURST_D : offset 0x0002 page0 / 0x002A page1), * 1:1 avec les
commandes -> plus d’aliasing. On ecrit l’echo (avec l’offset SCHED) *
sur LES DEUX read-pages pour que le mobile lise toujours la commande la
plus * recente quel que soit r_page. Gate CALYPSO_SHUNT_BURST_PERCMD
(defaut ON). / void calypso_dsp_shunt_wp_burst_write(uint32_t off,
uint16_t value) { / [2026-07-27] GARDE SHUNT-INACTIF : appele SANS
CONDITION depuis * calypso_dsp_write() (calypso_trx.c), y compris en
mode NATIF ou le * shunt n est pas arme. Sans cette garde, g_shunt.as ==
NULL et * shunt_write_w() segfault (backtrace gdb : address_space_write
as=0x0). * C est une feature du shunt : hors shunt, elle ne doit rien
faire. / if (!g_shunt.active) return; / @BEQUILLE — SHUNT_BURST_PERCMD (miroir
per-commande) (CALYPSO_SHUNT_BURST_PERCMD, * ON-sauf-0, defaut ON) *
masque : la derivation materielle de d_burst_d. On capture chaque
ecriture ARM * de la write-page et on MIROITE l’echo sur LES DEUX
read-pages, parce * que le r_page du mobile n’est pas modelise. *
retirer : quand la fenetre RX TPU cadence le burst-id et que r_page est
* modelise fidelement. / static int en = -1; if (en < 0) { const
char e = getenv(“CALYPSO_SHUNT_BURST_PERCMD”); en = (e &&
e == ‘0’) ? 0 : 1; } if (!en) return; if (off != 0x0002 &&
off != 0x002A) return; / WP_D_BURST_D page0/1 */ g_shunt.d_burst_d
= (uint16_t)(value & 3); uint16_t x = shunt_burst_echo();
shunt_write_w(BASE_API_R_PAGE_0 + RP_D_BURST_D, x);
shunt_write_w(BASE_API_R_PAGE_1 + RP_D_BURST_D, x); { static unsigned n
= 0, z = 0; if ((value & 3) != 0 && n < 40) { n++;
fprintf(stderr, “[feed-daram-dsp] WP-BURST-NONZERO off=0x%04x cmd=%u
-> X=%u insn”, (unsigned)off, value & 3, x); } else if ((value
& 3) == 0) { z++; if (z % 500 == 1) fprintf(stderr,
“[feed-daram-dsp] WP-BURST cmd=0 x%u (aucun non-zero: ARM ne commande
QUE burst 0)”, z); } } }
void calypso_dsp_shunt_feed_si(const uint8_t l2, int len) { if
(!l2 || len <= 0) { g_shunt.si_valid = false; return; } /
[2026-07-22] Gate test natif : CALYPSO_SHUNT_FEED_SI=0 coupe l’injection
du * SI reel dans a_cd -> a_cd ne se remplit QUE si la demod native
(corr 0x8d00 * -> NB) produit vraiment le bloc. Prouve natif vs
plomberie. Defaut ON (=1). / { / @BEQUILLE — SHUNT_FEED_SI
(CALYPSO_SHUNT_FEED_SI=1, fallback CALYPSO_SHUNT_LEGIT=1) * masque : la
production des blocs SI par la demodulation native (correlateur * 0x8d00
-> NB -> a_cd). Le SI vient de gr-gsm, est range par type dans *
si_set[0..5], avec fabrication d’un SI6 seed depuis le SI3. * retirer :
quand a_cd se remplit par la demodulation native. / static int fs =
-1; if (fs < 0) { const char e =
getenv(“CALYPSO_SHUNT_FEED_SI”); fs = (e && e == ‘1’) ? 1 :
0; if (!fs) { const char l = getenv(“CALYPSO_SHUNT_LEGIT”); /*
option3: SI3 gr-gsm -> a_cd / fs = (l && l == ‘1’) ?
1 : 0; } } /* [2026-07-26] fire aussi sous SHUNT_LEGIT / if (!fs) {
g_shunt.si_valid = false; return; } } int n = len < 23 ? len : 23;
/ (A) range la frame dans le slot de SON type (RR PD=0x06,
mt=l2[2]) : * SI1=0x19 SI2=0x1a SI3=0x1b SI4=0x1c SI2bis=0x1d
SI2ter=0x1e. * shunt_dispatch_allc tourne ensuite sur les slots dispo
(set complet). / int slot = -1; if (n >= 3 && l2[1] ==
0x06) { switch (l2[2]) { case 0x19: slot = 0; break; / SI1 /
case 0x1a: slot = 1; break; / SI2 / case 0x1b: slot = 2; break;
/ SI3 / case 0x1c: slot = 3; break; / SI4 / case 0x1d:
slot = 4; break; / SI2bis/ case 0x1e: slot = 5; break; /
SI2ter/ default: break; } } if (slot >= 0) {
memcpy(g_shunt.si_set[slot], l2, n); for (int i = n; i < 23; i++)
g_shunt.si_set[slot][i] = 0x2B; g_shunt.si_set_have[slot] = true; }
/ SI3 (slot 2) -> SEED SI6 fabrique (B4) pour la SACCH dediee,
UNIQUEMENT en * fallback tant qu’aucun SI5/SI6 REEL n’est arrive
(g_shunt.sacch_real). Des * que feed_sacch recoit le vrai SI5/SI6
(grgsm), sacch_real=true et ce bloc * ne tourne plus -> le SI3 du
BCCH ne clobbe plus le SACCH reel. Le seed evite * le ‘Short header 0x07
unsupported’ au tout debut d’un canal dedie (avant que * grgsm ait
decode la 1ere SACCH ~480ms). / if (slot == 2 && n >= 10
&& !g_shunt.sacch_real) { uint8_t s6 = g_shunt.sacch_buf;
memset(s6, 0x2b, sizeof(g_shunt.sacch_buf)); /* Layout B4 reel (lapdm.c)
: header L1 SACCH (2o, non strippe par la L1 * osmocom-bb) + LAPDm addr
+ LAPDm control UI (-> fmt B4, l3len=19) + L3. / s6[0] = 0x00;
/ L1 SACCH : tx_power / s6[1] = 0x00; / L1 SACCH : TA
/ s6[2] = 0x03; / LAPDm address : SAPI0, C/R, EA=1 / s6[3]
= 0x03; / LAPDm control : UI -> format B4 / s6[4] =
(uint8_t)((11 << 2) | 0x01); / L3 pseudo-length L=11 /
s6[5] = 0x06; / RR PD, skip=0 / s6[6] = 0x1e; / SYSTEM
INFORMATION TYPE 6 / s6[7] = l2[3]; s6[8] = l2[4]; / cell
identity (SI3 @3..4) / s6[9] = l2[5];
s6[10] = l2[6]; s6[11] = l2[7]; s6[12] = l2[8]; s6[13] = l2[9]; /
LAI (SI3 @5..9) / s6[14] = 0x0f;
/ cell options : radio-link-timeout long / s6[15] = 0xff;
/ NCC permitted : tous / / [16..22] = 0x2b rest octets (l3
total = [4..22] = 19o) / g_shunt.sacch_have = true; } / compat
/ fallback : si_buf = dernier reçu / memcpy(g_shunt.si_buf, l2, n);
/ pad fin avec 0x2B (filler LAPDm) si la frame est plus courte
/ for (int i = n; i < 23; i++) g_shunt.si_buf[i] = 0x2B;
g_shunt.si_valid = true; / Hop 5 : injecte AUSSI directement en
L1CTL DATA_IND -> mobile (gated * CALYPSO_SHUNT_DL_INJECT, defaut
ON). FN reelle via calypso_trx_get_fn. / { / @BEQUILLE — SHUNT_DL_INJECT
(CALYPSO_SHUNT_DL_INJECT, EQ1, defaut OFF ; * shunt_no_legit.env:=1) *
masque : TOUT le chemin descendant a_cd -> L1 firmware -> UART
-> L1CTL. Le SI * est pousse directement en L1CTL_DATA_IND vers le
mobile ; aucune * partie de la chaine emulee n’est exercee. C’est la
bequille la plus * intrusive du modele. * retirer : des que le SI
atteint le mobile via a_cd (deja le cas par defaut : * seul
calypso_shunt_no_legit.env la repose a 1). / static int inj = -1; if
(inj < 0) { const char e = getenv(“CALYPSO_SHUNT_DL_INJECT”);
inj = (e && e == ‘1’) ? 1 : 0; } / [2026-07-23] DEFAUT
OFF (natif) */ if (inj) l1ctl_inject_dl_si(g_shunt.si_buf, 23,
calypso_trx_get_fn()); } SHUNT_LOG(“feed_si: SI réel %d o injecté →
a_cd” “(L2[0..2]=%02x %02x %02x)”, n, l2[0], n > 1 ? l2[1] : 0, n
> 2 ? l2[2] : 0); }
/* Public getter — gate condition for BSP/TPU DMA into DARAM. /
bool calypso_dsp_shunt_sb_valid(void) { return g_shunt.sb_valid; } bool
calypso_dsp_shunt_si_valid(void) { return g_shunt.si_valid; } uint16_t
calypso_dsp_shunt_burst_d(void) { return shunt_burst_echo(); } /
d_burst_d gate (OFS/FN) */ bool calypso_dsp_shunt_active(void) { return
g_shunt.active; }
/* [2026-07-27] SPLIT DU GATE — voir en-tete du patch. * active() = l
infrastructure shunt est armee (overlay NDB, ponts, feeds). * VRAI aussi
en mode ASSIST (CALYPSO_DSP=c54x). * substitutes() = le shunt REMPLACE
le DSP (mock ARM) -> et LUI SEUL doit * gater les c54x_run natifs.
FAUX en mode assist, ou le vrai * DSP execute son firmware et doit
tourner a la cadence trame. / / @BEQUILLE — DSP_SHUNT (substitution du DSP)
(CALYPSO_DSP_SHUNT, EQ1, ou * CALYPSO_L1=c) * masque : l’execution du
DSP. Quand cette fonction retourne vrai, TOUS les * c54x_run natifs
(calypso_trx.c) sont gates : le mock remplace le * processeur de signal.
* retirer : quand le correlateur natif produit d_fb_det seul (RANK3
leve). / bool calypso_dsp_shunt_substitutes(void) { if
(!g_shunt.active) return false; if (calypso_l1_c_active()) return true;
/ L1=c : modele HLE remplace le DSP / { const char e =
getenv(“CALYPSO_DSP_SHUNT”); return (e && strcmp(e, “1”) == 0);
} /* mock explicite */ }
/* Public getter — mission courante du DSP (d_task_md, lu du
write-page ARM). * Sert a gater les wires inter-blocs (BSP BRINT0 / TPU
DSP_INT_PG) sur la * mission FB/SB reelle. Valeurs (osmo
l1_environment.h) : FB_DSP_TASK=5, * SB_DSP_TASK=6, TCH_FB=8, TCH_SB=9.
0 = pas de tache. / uint16_t calypso_dsp_shunt_get_task_md(void) {
if (g_shunt.d_task_md) return g_shunt.d_task_md; / [2026-07-27]
FALLBACK NATIF : hors shunt, g_shunt.d_task_md n est JAMAIS * alimente
(pose uniquement par shunt_latch_task) -> l accesseur renvoyait 0 *
en permanence et tuait silencieusement tous ses appelants cote natif. *
On lit alors la cellule API RAM du DSP : d_task_md page0 = 0x0804, *
page1 = 0x0818. La branche shunt ci-dessus reste prioritaire. */ if
(g_shunt.c54x && g_shunt.c54x->data) { uint16_t _a =
g_shunt.c54x->data[0x0804]; if (_a) return _a; return
g_shunt.c54x->data[0x0818]; } return 0; }
/* CALYPSO_DSP=c54x : relie le handle du VRAI DSP (depuis
calypso_mb.c). */ static bool g_c54x_early_booted = false; bool
calypso_dsp_shunt_early_booted(void) { return g_c54x_early_booted; }
/* [2026-07-27] FB-STREAM : pop la prochaine paire I/Q du ring. false
= underrun. / bool calypso_dsp_shunt_fb_stream_next(uint16_t
outI, uint16_t outQ) { if (g_fbs_rd + 1 >= g_fbs_wr) return
false; outI = (uint16_t)g_fbs[g_fbs_rd++ & (FBS_RING-1)];
outQ = (uint16_t)g_fbs[g_fbs_rd++ & (FBS_RING-1)]; /
[2026-07-27] B2IN (gated CALYPSO_B2IN) : mesure la VRAIE entree corr *
(0x9213/0x9215 = CE stream), pas la sortie 0x2a00. max|I|/|Q| + energie
+ * indice du max sur 296 -> tranche “entree morte/DC” vs “vrai ton
FCCH”. */ { static int _b2i = -1; static unsigned _n = 0, _imax = 0,
_qmax = 0; static int _iidx = -1; static uint64_t _e = 0; static
unsigned _wn = 0; if (_b2i < 0) _b2i = getenv(“CALYPSO_B2IN”) ? 1 :
0; if (_b2i) { int16_t _I = (int16_t)*outI, _Q = (int16_t)*outQ;
unsigned _ai = _I < 0 ? (unsigned)(-_I) : (unsigned)_I; unsigned _aq
= _Q < 0 ? (unsigned)(-_Q) : (unsigned)_Q; if (_ai > _imax) {
_imax = _ai; _iidx = (int)_wn; } if (_aq > _qmax) _qmax = _aq; _e +=
(uint64_t)_I * _I + (uint64_t)_Q * _Q; if (++_wn >= 296) { if (_n++
< 30) fprintf(stderr, “[dsp-shunt] B2IN (0x9213/0x9215) win=296
max|I|=%u@%d max|Q|=%u energy=%llu”, _imax, _iidx, _qmax, (unsigned long
long)_e); _wn = 0; _imax = 0; _qmax = 0; _iidx = -1; _e = 0; } } }
return true; }
/* [2026-07-27] Reset L1 (Ctrl-C mobile / L1CTL_RESET_REQ FULL) :
clear l’etat * transitoire du shunt (IMM-ASSIGN / SDCCH-DL latches par
un SMS) qui sinon * supprime le SI apres la relance -> le mobile ne
re-campe pas. Appele depuis * calypso_arm2dsp.c quand l’ARM ecrit
d_dsp_page=0 (l1s_reset_hw). / void calypso_dsp_shunt_l1_reset(void)
{ g_shunt.agch_valid = false; g_shunt.sdcch_valid = false;
g_shunt.sdcch_ss_set = false; / reset -> defaut base 22 (SDCCH/4
SS0) / / PAS de si_rr=0 : d_dsp_page=0 fire aussi sur les
mesures (l23_api.c:414) / * FBSB -> reset si_rr figerait la rotation
SI en no_canned. */ }
void calypso_dsp_shunt_set_c54x(C54xState *s) { g_shunt.c54x = s;
/* [c54x-earlyboot] FIX race d'ordre golive (2026-07-20, mode B).
* Root cause : l'ARM poste le golive (data[0x0fff]=cmd 2/4, data[0x0ffe]=entry)
* a fn=0/+0.073s, MAIS le c54x ne bootait qu'a +5.6s (1er shunt_route_to_c54x)
* -> son init-IDLE a 0xb419 (ST #1,*0xfff) ecrasait le 0x0002 de l'ARM -> spin
* eternel a 0xb41c. Etat PERSISTE entre wakes (verifie : 0xb419 ne tourne
* qu'une fois, insn accumule, meme objet DSP). Fix = booter le c54x ICI
* (machine-init, AVANT que le vCPU ARM tourne -> AVANT le golive), pour qu'il
* pose son IDLE et se parke a 0xb41c AVANT l'ecriture ARM. 0xb419 ne re-tourne
* plus (PC persiste) -> le 0x0002 survit -> le 1er wake shunt le consomme ->
* golive natif (le firmware fait son propre RSBX INTM). Zero FORCE_ : on force
* le QUAND du boot, aucune valeur de mailbox. One-shot, gate mode revive. */
/* [2026-07-27] DECOUPLE du routage shunt : l ordre de boot du c54x ne
* depend pas de CALYPSO_DSP=c54x. Gate sur CALYPSO_DSP_RUN_C54X seul,
* sinon le mode natif re-reset le DSP et ecrase la cmd bootloader de
* l ARM -> spin eternel a 0xb41c (voir en-tete du patch). */
{
static int rc = -1;
if (rc < 0) { const char *e = getenv("CALYPSO_DSP_RUN_C54X"); rc = (e && *e == '1') ? 1 : 0; }
if (s && rc) {
uint16_t pc0 = s->pc;
s->running = true;
c54x_run(s, 2000); /* reset(0xff80) -> 0xb419 (pose IDLE) -> park 0xb41c */
if (s->pc >= 0xb41c && s->pc <= 0xb428) {
g_c54x_early_booted = true; /* gate le re-reset trx:701 */
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 "
"-> B invalide, basculer sur A (execution continue)\n",
s->pc, s->insn_count);
}
}
}
/* Predicat dedie : shunt actif ET route c54x demandee. Utilise par *
calypso_trx.c pour autoriser la DMA page->DARAM en mode c54x sans *
reactiver le c54x_run du trx (le shunt possede c54x_run). */ bool
calypso_dsp_shunt_route_c54x_active(void) { return g_shunt.active
&& shunt_route_c54x(); }
================================================================================
FILE: hw/arm/calypso/calypso_fbsb.c SIZE: 3270 bytes, 99 lines
================================================================================
/ calypso_fbsb.c — QEMU-side FBSB state tracking (logs only)
2026-05-28 cleanup : all host-side synthesis
(publish_fb_found / * publish_sb_found / clear_fb / W1C latches /
on_frame_tick state * machine) removed. fbsb.c logs DSP task changes ;
fb0_attempt/sb_attempt sont des compteurs REELS * de dispatch
(2026-07-27, avant : figes a 0 = red herring qui trompait le diag).
FB/SB * detection is driven entirely by the DSP (real ROM or L1 stub via
* CALYPSO_DSP_L1_STUB=1) writing NDB cells, and ARM reads them *
directly. The only env-gated hack on this path is *
CALYPSO_FORCE_ANGLE_ZERO (calypso_trx.c).
SPDX-License-Identifier: GPL-2.0-or-later / #include
“calypso_fbsb.h” #include “calypso_full_pcb.h” / DARAM lock helpers
— cf gap #3 */ #include <stdio.h> #include <stdlib.h>
#include <string.h>
void calypso_fbsb_init(CalypsoFbsb s, uint16_t
ndb_word_base, uint16_t api_base) { if (!s) return; s->ndb =
ndb_word_base; s->api_base = api_base; calypso_fbsb_reset(s); }
void calypso_fbsb_reset(CalypsoFbsb *s) { if (!s) return; s->state
= FBSB_IDLE; s->fb0_attempt = 0; s->fb1_attempt = 0;
s->sb_attempt = 0; s->fb0_retries = 0; s->afc_retries = 0;
s->last_toa = 0; s->last_angle = 0; s->last_pm = 0;
s->last_snr = 0; s->fn_started = 0; }
void calypso_fbsb_on_dsp_task_change(CalypsoFbsb s, uint16_t
d_task_md, uint64_t fn) { fprintf(stderr, “[calypso-fbsb]
on_dsp_task_change task=%u fn=%lu state=%d”, d_task_md, (unsigned
long)fn, s ? (int)s->state : -1); fflush(stderr); if (!s) return;
switch (d_task_md) { case DSP_TASK_FB: s->fb0_attempt++; /
[2026-07-27] compteur REEL : nb de dispatch tache FB (etait fige a 0 =
red herring) / 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++; / [2026-07-27] compteur REEL : nb
de dispatch tache SB */ 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”, (unsigned long)fn);
fflush(stderr); } break; } case DSP_TASK_NONE: default: break; } }
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”, }; fprintf(stderr, “[fbsb] %s state=%s fb0_att=%u fb1_att=%u
sb_att=%u” “fb0_ret=%u afc_ret=%u last(snr=%u toa=%d ang=%d pm=%u)”, tag
? tag : ““, names[s->state], s->fb0_attempt, s->fb1_attempt,
s->sb_attempt, s->fb0_retries, s->afc_retries, s->last_snr,
s->last_toa, s->last_angle, s->last_pm); fflush(stderr); }
================================================================================
FILE: hw/arm/calypso/calypso_full_pcb.c SIZE: 6622 bytes, 210 lines
================================================================================
/ 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 “”, ##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)”, 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]); } }
================================================================================
FILE: hw/arm/calypso/calypso_invariants.c SIZE: 2773 bytes, 93 lines
================================================================================
/ calypso_invariants.c — screaming invariants + run manifest.
Philosophy (2026-07-25) : internal probes raise RESOLUTION,
not INDEPENDENCE * — a hole in the BSP model is a hole in the BSP probe.
So this file holds two * things that DO earn their keep : * 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?) is a separate matter and cannot *
come from this code — that is GSMTAP -> a decoder we did not write.
*/ #include “qemu/osdep.h” #include
“hw/arm/calypso/calypso_invariants.h” #include <stdarg.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
= getenv(“CALYPSO_INVARIANTS”); inv_enabled = (e && e[0] == ‘1’)
? 1 : 0; / DEFAUT OFF (securite boot) */ } return inv_enabled;
}
static void inv_summary(void) { if (!inv_n) { fprintf(stderr,
“[INVARIANT-SUMMARY] all invariants held this run”); return; } for (int
i = 0; i < inv_n; i++) { fprintf(stderr, “[INVARIANT-SUMMARY] %s : %u
violation(s)”, 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”, tag,
buf); fflush(stderr); } return ok; }
void calypso_manifest_once(void) { if (!inv_on()) { return; } /* gate
defaut OFF */ static int done; if (done) { return; } done = 1;
fprintf(stderr, “[MANIFEST] ===== run manifest : active CALYPSO_*
forcings =====”); int n = 0; for (char **e = environ; e &&
e; e++) { if (!strncmp(e, “CALYPSO_”, 8)) { fprintf(stderr,
“[MANIFEST] %s”, *e); n++; } } fprintf(stderr, “[MANIFEST] ===== %d
CALYPSO_* variables (see artifact appendix) =====”, n); fflush(stderr);
}
================================================================================
FILE: hw/arm/calypso/calypso_iota.c SIZE: 3402 bytes, 101 lines
================================================================================
/ 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 “”, ##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; }
================================================================================
FILE: hw/arm/calypso/calypso_layer1.c SIZE: 7849 bytes, 181 lines
================================================================================
/ calypso_layer1.c — HLE L1 model for the Calypso DSP
(CALYPSO_L1=c). Scaffold fonctionnel intermédiaire (PAS
l’endgame ; le revival LLE reste le but * faithful, gardé vivant
derrière CALYPSO_L1 off). Modélise le L1 du DSP en C, * cadencé par
calypso_tdma_tick. Lit l’I/Q en DARAM 0x2a00, écrit ses résultats * dans
l’API RAM où l’ARM (osmocom-bb, inchangé) les lit. ÉTAPE 1 —
détecteur FB/FCCH RÉEL (pas de forcing) : * Filtre adapté au ton FCCH
(+fc/4 → incrément de phase +22.5°/sample @4 SPS) : * C = Σ z[n]·exp(-j·n·inc) ; M = |C|²
/ (N·E) * Ton pur (FCCH) → M≈1 ; bruit/GMSK data → M≈1/N. Discrimine
VRAIMENT (le ratio * différentiel R était émoussé à 4 SPS).
Auto-alignant : M ne pique qu’aux trames * FCCH (observées à fn%51≈7-8,
offset fixe du multiframe BTS) → la trame de lock * devient l’ancre,
l’offset n’a pas à être connu. * Valeurs DÉRIVÉES (pas cannées) :
d_fb_det (détection réelle), snr (∝ cohérence M, *
>AFC_SNR_THRESHOLD=2560 quand locké), angle (résidu meanInc−nominal
≈0 → AFC * stable, comme le ANGLE=0 du shunt mais CALCULÉ), pm (dérivé
de E full-scale), * toa (on-time, le bsp livre le burst aligné).
SPDX-License-Identifier: GPL-2.0-or-later */ #include
“qemu/osdep.h” #include “hw/arm/calypso/calypso_layer1.h” #include
<math.h>
#ifndef M_PI #define M_PI 3.14159265358979323846 #endif
/* — offsets data[] (DSP word) / dsp_ram (ARM word) — / #define
L1_DARAM_IQ 0x2a00 / 148 paires int16 I/Q entrelacées / #define
L1_IQ_PAIRS 148 #define L1_NDB_D_FB_DET 0x08F8 / ndb->d_fb_det
/ #define L1_NDB_D_FB_MODE 0x08F9 / ndb->d_fb_mode
(0=wideband 1=narrow) / #define L1_NDB_A_SYNC 0x08FA /
a_sync_demod[0]=TOA [1]=PM [2]=ANGLE [3]=SNR */ #define WP0_TASK_MD
(0x0008/2) #define WP1_TASK_MD (0x0030/2) #define D_DSP_PAGE_WORD
(0x01A8/2)
#define FB_DSP_TASK 5 #define SB_DSP_TASK 6
/* incrément de phase nominal du ton FCCH : +fc/4 = +1625/24 kHz à 4
SPS * (fs=4×270.833k) → 2π·67708/1083333 = +22.5°/sample. / #define
L1_FCCH_INC_DEG 22.5 #define L1_FCCH_M_THRESH 0.40 /
M_data≈1/148≈0.007, M_fcch≈1 → seuil large / #define L1_E_MIN 1.0e6
/ énergie min pour considérer un burst présent / #define
AFC_SNR_THRESHOLD 2560 / afc.h : 2.5 dB en fx6.10 */
/* @BEQUILLE — L1 (CALYPSO_L1=c,
defaut absent) masque : le DSP entier. calypso_l1_c_active() rend
substitutes() vrai * (calypso_dsp_shunt.c) -> un modele L1
haut-niveau en C remplace la * couche 1 et gate les c54x_run natifs. *
retirer : quand le DSP emule tient la couche 1. Variable posee dans
aucun * profil livre : candidate au retrait pur et simple. / int
calypso_l1_c_active(void) { static int v = -1; if (v < 0) { const
char e = getenv(“CALYPSO_L1”); v = (e && e[0] == ‘c’) ? 1 :
0; } return v; }
/* Latch du d_task_md écrit par l’ARM : le poll tick-time rate le
transient * (write puis consommé au write-time, vu par calypso_fbsb).
Mis à jour depuis * calypso_dsp_write (calypso_trx.c) à chaque écriture
de la tâche. */ static uint16_t g_l1_task_md = 0; void
calypso_layer1_on_task_write(uint16_t md) { g_l1_task_md = md; }
struct fb_res { int found; double M, meanInc_deg, energy; int16_t
toa, pm, angle, snr; };
/* Détecteur FCCH : filtre adapté au ton +fc/4 + résidu de fréquence.
/ static struct fb_res l1_fb_detect(C54xState dsp) { struct
fb_res r = { 0 }; const double inc = L1_FCCH_INC_DEG * M_PI / 180.0;
const double rcos = cos(-inc), rsin = sin(-inc); /* rotator exp(-j·inc)
*/
double Cre = 0, Cim = 0, E = 0; /* C = Σ z[n]·exp(-j n inc) */
double pr = 1.0, pim = 0.0; /* phasor p_n = exp(-j n inc), p_0 = 1 */
double sre = 0, sim = 0; /* Σ d[n] (différentiel) pour meanInc */
double prevI = (double)(int16_t)dsp->data[L1_DARAM_IQ + 0];
double prevQ = (double)(int16_t)dsp->data[L1_DARAM_IQ + 1];
for (int n = 0; n < L1_IQ_PAIRS; n++) {
double I = (double)(int16_t)dsp->data[L1_DARAM_IQ + 2 * n];
double Q = (double)(int16_t)dsp->data[L1_DARAM_IQ + 2 * n + 1];
/* filtre adapté : C += z · p */
Cre += I * pr - Q * pim;
Cim += I * pim + Q * pr;
E += I * I + Q * Q;
/* avance phasor : p *= exp(-j inc) */
double npr = pr * rcos - pim * rsin;
double npim = pr * rsin + pim * rcos;
pr = npr; pim = npim;
/* différentiel d[n] = z[n]·conj(z[n-1]) (diagnostic / résidu) */
if (n > 0) {
sre += I * prevI + Q * prevQ;
sim += Q * prevI - I * prevQ;
}
prevI = I; prevQ = Q;
}
r.energy = E;
r.M = (E > 0.0) ? (Cre * Cre + Cim * Cim) / ((double)L1_IQ_PAIRS * E) : 0.0;
r.meanInc_deg = (sre != 0.0 || sim != 0.0) ? atan2(sim, sre) * 180.0 / M_PI : 0.0;
r.found = (r.M >= L1_FCCH_M_THRESH && E >= L1_E_MIN);
if (r.found) {
/* angle : résidu de fréquence (meanInc − nominal), ≈0 pour I/Q propre →
* freq_diff = ANGLE_TO_FREQ(angle) ≈ 0 → AFC ne chasse pas. DÉRIVÉ. */
double resid_deg = r.meanInc_deg - L1_FCCH_INC_DEG;
double a = resid_deg * 32.0; /* échelle fx provisoire (à calibrer) */
r.angle = (int16_t)(a > 32767 ? 32767 : a < -32768 ? -32768 : a);
/* snr : croît avec la cohérence M ; >2560 (AFC_SNR_THRESHOLD) quand locké. */
double snr = r.M * (double)0x7000;
r.snr = (int16_t)(snr > 32767 ? 32767 : snr);
/* pm : dérivé — l'I/Q est full-scale Q15 (rxlev fort). Lu >>3 par
* read_fb_result → 0x7000>>3 = 0xE00. */
r.pm = 0x7000;
/* toa : le bsp livre le burst aligné sur la fenêtre → on-time.
* read_fb_result fait toa-=23, donc 23 → 0 (on-time). */
r.toa = 23;
}
return r;
}
void calypso_layer1_tick(C54xState dsp, uint16_t dsp_ram,
uint32_t fn) { if (!dsp || !dsp_ram) { return; }
uint16_t page = dsp_ram[D_DSP_PAGE_WORD] & 1;
uint16_t md_poll = page ? dsp_ram[WP1_TASK_MD] : dsp_ram[WP0_TASK_MD];
/* latch prioritaire (le poll rate le transient task=5) */
uint16_t md = g_l1_task_md ? g_l1_task_md : md_poll;
struct fb_res r = l1_fb_detect(dsp);
/* log riche (calibration du seuil + visu de l'auto-alignement) */
static unsigned logn = 0;
if (logn++ < 5000) {
fprintf(stderr,
"[L1c] fn=%u m51=%u md=%u(poll=%u) M=%.3f meanInc=%.1f E=%.2e det=%d\n",
fn, fn % 51, md, md_poll, r.M, r.meanInc_deg, r.energy, r.found);
}
/* écriture firmware UNIQUEMENT quand le FB est demandé (évite des d_fb_det
* parasites hors recherche). NDB page-less → pas de souci de page. */
if (md == FB_DSP_TASK) {
if (r.found) {
dsp->data[L1_NDB_A_SYNC + 0] = (uint16_t)r.toa;
dsp->data[L1_NDB_A_SYNC + 1] = (uint16_t)r.pm; /* lu >>3 par l'ARM */
dsp->data[L1_NDB_A_SYNC + 2] = (uint16_t)r.angle;
dsp->data[L1_NDB_A_SYNC + 3] = (uint16_t)r.snr;
dsp->data[L1_NDB_D_FB_DET] = 1;
static unsigned fbn = 0;
if (fbn++ < 200) {
fprintf(stderr, "[L1c-FB] FOUND fn=%u m51=%u M=%.3f meanInc=%.1f "
"resid=%.1f -> d_fb_det=1 toa=%d pm=0x%04x ang=%d snr=%d\n",
fn, fn % 51, r.M, r.meanInc_deg,
r.meanInc_deg - L1_FCCH_INC_DEG,
r.toa, (uint16_t)r.pm, r.angle, r.snr);
}
} else {
dsp->data[L1_NDB_D_FB_DET] = 0;
}
}
}
================================================================================
FILE: hw/arm/calypso/calypso_mb.c SIZE: 14784 bytes, 353 lines
================================================================================
/ calypso_mb.c - Calypso development board machine * DEBUG
BUILD — verbose flash/memory debug 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/qdev-properties.h” #include
“hw/qdev-properties-system.h” #include “hw/block/flash.h” #include
“hw/char/serial.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 “sysemu/reset.h”
#include “hw/arm/calypso/calypso_soc.h” #include
“hw/arm/calypso/calypso_trx.h” /* C54xState + calypso_trx_get_dsp() +
calypso_trx_set_section_paths() */ #include “calypso_dsp_shunt.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)
typedef struct CalypsoMachineState { MachineState parent; ARMCPU
cpu; CalypsoSoCState soc; MemoryRegion xram; MemoryRegion bootrom;
char dsp_blob; /* -M calypso,dsp-blob=<path> —
DARAM fixture / / Explicit per-section ROM loads. Each writes
raw LE 16-bit words at * its silicon-correct DSP address. Bins produced
by dsp_txt2bin.py. / char dsp_prom0; /*
-M calypso,dsp-prom0=<path> → prog[0x07000+] /
char dsp_prom1; /* -M calypso,dsp-prom1=<path> →
prog[0x18000+] + mirror / char dsp_prom2; /*
-M calypso,dsp-prom2=<path> → prog[0x28000+] /
char dsp_prom3; /* -M calypso,dsp-prom3=<path> →
prog[0x38000+] / char dsp_drom; /*
-M calypso,dsp-drom=<path> → data[0x09000+] /
char dsp_pdrom; /* -M calypso,dsp-pdrom=<path> →
data[0x0E000+] / char dsp_registers; /*
-M calypso,dsp-registers=<path> → MMR reset snapshot
*/ } CalypsoMachineState;
#define TYPE_CALYPSO_MACHINE MACHINE_TYPE_NAME(“calypso”)
OBJECT_DECLARE_SIMPLE_TYPE(CalypsoMachineState, CALYPSO_MACHINE)
/ Firmware patches applied after ROM blobs are loaded into
memory. * Called from qemu_system_reset() which runs after machine_init.
1) NOP cons_puts: prevents console output from filling the
32-slot * msgb pool, which causes talloc panic during boot.
2) Talloc panic → retry with IRQs: if the pool fills despite (1), *
re-enable IRQs and retry instead of halting. The NOP at the * cons_puts
call site prevents recursive allocation. 3) handle_abort →
loop with IRQs enabled: prevents a stray data * abort from permanently
disabling IRQs and halting the system. / static void
calypso_machine_init(MachineState machine) { CalypsoMachineState
s = CALYPSO_MACHINE(machine); MemoryRegion sysmem =
get_system_memory(); Object cpuobj; Error err = NULL;
fprintf(stderr, "[MB] === calypso_machine_init START ===\n");
/* ---- CPU ---- */
cpuobj = object_new(machine->cpu_type);
s->cpu = ARM_CPU(cpuobj);
if (!qdev_realize(DEVICE(cpuobj), NULL, &err)) {
error_report_err(err);
exit(1);
}
/* ---- SoC ---- */
/* Push per-section ROM paths to the TRX layer BEFORE sysbus_realize, so
* that calypso_trx_init() loads each section into prog[]/data[] **before**
* c54x_reset(). The reset's PROM→DARAM auto-copy (0x7080..0x97FF →
* 0x80..0x27FF) reads from prog[]; the sections must be in place by
* then or the boot stub overlay ends up zero-filled. */
calypso_trx_set_section_paths(s->dsp_prom0, s->dsp_prom1,
s->dsp_prom2, s->dsp_prom3,
s->dsp_drom, s->dsp_pdrom);
calypso_trx_set_registers_path(s->dsp_registers);
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));
/* ---- External RAM ---- */
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);
fprintf(stderr, "[MB] XRAM @ 0x%08x (%d MiB)\n",
CALYPSO_XRAM_BASE, CALYPSO_XRAM_SIZE / (1024*1024));
/* ---- Flash NOR @ 0x00000000 ----
*
* Real Compal E88: Intel 28F320 (4 MiB) on CS0 at 0x00000000.
* 16-bit bus width (Calypso CS0 is 16-bit).
* Manufacturer 0x0089 = Intel, Device 0x0018 = 28F320J3.
* 64 KiB sectors.
*
* The loader does CFI queries here. If there's no pflash or
* something else shadows this address, we get "Failed to
* initialize flash!".
*/
DriveInfo *dinfo = drive_get(IF_PFLASH, 0, 0);
fprintf(stderr, "[MB] Flash: registering pflash_cfi01 @ 0x%08x\n",
CALYPSO_FLASH_BASE);
fprintf(stderr, "[MB] size=%d MiB, sector=64K, width=2 (16-bit)\n",
CALYPSO_FLASH_SIZE / (1024*1024));
fprintf(stderr, "[MB] mfr=0x0089 (Intel), dev=0x0018 (28F320J3)\n");
fprintf(stderr, "[MB] drive=%s\n", dinfo ? "attached" : "NONE (blank 0xFF)");
pflash_cfi01_register(CALYPSO_FLASH_BASE,
"calypso.flash",
CALYPSO_FLASH_SIZE,
dinfo ? blk_by_legacy_dinfo(dinfo) : NULL,
64 * 1024, /* sector size */
1, /* 8-bit bus width */
0x0089, /* Intel */
0x0018, /* 28F320J3 */
0, 0, 0);
fprintf(stderr, "[MB] Flash: pflash_cfi01 registered OK\n");
/* ---- Synthetic boot ROM at address 0 ----
*
* The real Calypso has internal ROM at 0x00000000 containing
* exception vector stubs that branch to IRAM exception handlers.
* OsmocomBB firmware installs handlers at IRAM+0x1C through IRAM+0x34.
* The boot ROM vectors use: ldr pc, [pc, #0x18] + address table.
*
* Layout (0x00-0x3F):
* 0x00-0x1C: ldr pc, [pc, #0x18] for each exception
* 0x20-0x3C: handler addresses in IRAM
*/
{
uint32_t bootrom_data[16];
/* ARM instruction: ldr pc, [pc, #0x18] = 0xe59ff018 */
for (int i = 0; i < 8; i++) {
bootrom_data[i] = 0xe59ff018;
}
/* Handler addresses (read via the ldr pc above):
* Each vector at offset N reads from offset N+0x20 */
bootrom_data[8] = 0x00820000; /* reset → _start */
bootrom_data[9] = 0x0080001C; /* undef → IRAM _undef_instr */
bootrom_data[10] = 0x00800020; /* SWI → IRAM _sw_interr */
bootrom_data[11] = 0x00800024; /* prefetch abort → IRAM */
bootrom_data[12] = 0x00800028; /* data abort → IRAM */
bootrom_data[13] = 0x0080002C; /* reserved → IRAM */
bootrom_data[14] = 0x00800030; /* IRQ → IRAM _irq */
bootrom_data[15] = 0x00800034; /* FIQ → IRAM _fiq */
memory_region_init_ram(&s->bootrom, NULL,
"calypso.bootrom", 64, &error_fatal);
memory_region_add_subregion_overlap(sysmem, 0x00000000,
&s->bootrom, 1);
/* Write vector table into the boot ROM RAM */
{
void *ptr = memory_region_get_ram_ptr(&s->bootrom);
memcpy(ptr, bootrom_data, sizeof(bootrom_data));
}
fprintf(stderr, "[MB] Boot ROM @ 0x00000000 (64 bytes, exception vectors)\n");
}
/* ---- Firmware load ---- */
if (machine->kernel_filename) {
uint64_t entry;
int ret;
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("Could not load firmware '%s'",
machine->kernel_filename);
exit(1);
}
entry = CALYPSO_XRAM_BASE;
}
cpu_set_pc(CPU(s->cpu), entry);
fprintf(stderr, "[MB] Firmware: '%s'\n", machine->kernel_filename);
fprintf(stderr, "[MB] entry=0x%08lx size=%d bytes\n",
(unsigned long)entry, ret);
}
/* ---- Optional DSP blob (test fixture, OFF when unset) ----
* `-M calypso,dsp-blob=<path>` loads a raw 16-bit LE word blob into
* DARAM at 0x0100 and forces the DSP to start executing there.
* Must run AFTER SoC realize: calypso_trx_init() (invoked inside SoC
* realize) calls c54x_reset(), which auto-copies PROM0[0x7080..]
* into DARAM[0x80..0x27FF]. Our blob load overwrites that region
* at 0x100+, and the PC override redirects fetch away from the
* default reset vector (0xFF80) to our blob. OVLY=1 (PMST default
* 0xFFA8) routes the fetch to DARAM. Address 0x0100 sits outside
* the API-RAM mirror window (0x800..0x27FF) so ARM-side writes do
* not clobber the blob during boot. */
if (s->dsp_blob) {
C54xState *dsp = calypso_trx_get_dsp();
if (!dsp) {
error_report("dsp-blob: DSP not initialized — cannot load '%s'",
s->dsp_blob);
exit(1);
}
int words = c54x_load_blob_daram(dsp, s->dsp_blob, 0x0100);
if (words < 0) {
error_report("dsp-blob: load failed for '%s'", s->dsp_blob);
exit(1);
}
c54x_set_initial_pc(dsp, 0x0100);
fprintf(stderr, "[MB] dsp-blob: loaded %d words at DARAM[0x100] "
"from '%s', PC=0x100 (OVLY=1)\n",
words, s->dsp_blob);
}
/* ---- DSP shunt (mock côté ARM, skip c54x) ----
* Activé via env CALYPSO_DSP_SHUNT=1. Ne touche au c54x que via le
* gate calypso_dsp_shunt_active() utilisé dans calypso_bsp.c et
* calypso_trx.c pour stopper les écritures DMA vers DARAM.
* Cf hw/arm/calypso/calypso_dsp_shunt.c. */
calypso_dsp_shunt_init(sysmem, &address_space_memory);
/* CALYPSO_DSP=c54x : relie le VRAI DSP au shunt pour la route c54x + overlay NDB
* (sinon g_shunt.c54x reste NULL et la branche route_c54x est morte). */
calypso_dsp_shunt_set_c54x(calypso_trx_get_dsp());
fprintf(stderr, "[MB] === Machine ready ===\n");
fprintf(stderr, "[MB] Flash: 0x%08x–0x%08x (%d MiB pflash_cfi01)\n",
CALYPSO_FLASH_BASE,
CALYPSO_FLASH_BASE + CALYPSO_FLASH_SIZE - 1,
CALYPSO_FLASH_SIZE / (1024*1024));
fprintf(stderr, "[MB] IRAM: 0x00800000–0x0083FFFF (256 KiB)\n");
fprintf(stderr, "[MB] XRAM: 0x%08x–0x%08x (%d MiB)\n",
CALYPSO_XRAM_BASE,
CALYPSO_XRAM_BASE + CALYPSO_XRAM_SIZE - 1,
CALYPSO_XRAM_SIZE / (1024*1024));
}
/* Generic string get/set macros for the DSP blob/section properties.
/ #define DEFINE_DSP_STR_PROP(name)
static char calypso_get_##name(Object obj, Error
errp)
{
return g_strdup(CALYPSO_MACHINE(obj)->name);
}
static void calypso_set_##name(Object obj, const char
value,
Error errp)
{
CalypsoMachineState s = CALYPSO_MACHINE(obj);
g_free(s->name);
s->name = g_strdup(value);
}
DEFINE_DSP_STR_PROP(dsp_blob) DEFINE_DSP_STR_PROP(dsp_prom0)
DEFINE_DSP_STR_PROP(dsp_prom1) DEFINE_DSP_STR_PROP(dsp_prom2)
DEFINE_DSP_STR_PROP(dsp_prom3) DEFINE_DSP_STR_PROP(dsp_drom)
DEFINE_DSP_STR_PROP(dsp_pdrom) DEFINE_DSP_STR_PROP(dsp_registers)
static void calypso_machine_class_init(ObjectClass oc, void
data) { MachineClass *mc = MACHINE_CLASS(oc); mc->desc =
“Calypso SoC development board (modular architecture)”; mc->init =
calypso_machine_init; mc->max_cpus = 1; mc->default_cpu_type =
ARM_CPU_TYPE_NAME(“arm946”); mc->default_ram_size = 0; mc->alias =
“calypso-high”;
object_class_property_add_str(oc, "dsp-blob",
calypso_get_dsp_blob,
calypso_set_dsp_blob);
object_class_property_set_description(oc, "dsp-blob",
"Path to a raw DSP blob loaded into DARAM at 0x100 with PC override "
"(DARAM fixture; OFF when unset).");
/* Per-section explicit ROM loads. Each .bin is raw LE 16-bit words
* starting at the section's silicon-correct DSP address. Produced by
* dsp_txt2bin.py from calypso_dsp.txt (or a custom assembler). */
#define REG_DSP_SECTION(prop, name, desc)
object_class_property_add_str(oc, prop, calypso_get_##name,
calypso_set_##name);
object_class_property_set_description(oc, prop, desc)
REG_DSP_SECTION("dsp-prom0", dsp_prom0,
"Path to PROM0 .bin → prog[0x07000..0x0DFFF] (28K words max)");
REG_DSP_SECTION("dsp-prom1", dsp_prom1,
"Path to PROM1 .bin → prog[0x18000..0x1FFFF] (32K) "
"+ auto-mirror prog[0xE000..0xFFFF]");
REG_DSP_SECTION("dsp-prom2", dsp_prom2,
"Path to PROM2 .bin → prog[0x28000..0x2FFFF] (32K words max)");
REG_DSP_SECTION("dsp-prom3", dsp_prom3,
"Path to PROM3 .bin → prog[0x38000..0x39FFF] (8K words max)");
REG_DSP_SECTION("dsp-drom", dsp_drom,
"Path to DROM .bin → data[0x09000..0x0DFFF] (20K words max)");
REG_DSP_SECTION("dsp-pdrom", dsp_pdrom,
"Path to PDROM .bin → data[0x0E000..0x0FFFF] (8K words max)");
REG_DSP_SECTION("dsp-registers", dsp_registers,
"Path to Registers .bin (MMR snapshot, words 0x00..0x1F) → applied as "
"the DSP reset state (IMR/IFR/ST0/ST1/T/TRN/AR0-7/SP/BK/BRC/RSA/REA/PMST)");
#undef REG_DSP_SECTION }
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)
================================================================================
FILE: hw/arm/calypso/calypso_sim.c SIZE: 31129 bytes, 794 lines
================================================================================
/ calypso_sim.c — ISO 7816 / GSM 11.11 SIM emulator for the
Calypso Replaces the historical 1-line ATR-pulse stub.
Implements: * - ATR delivery on CMDSTART * - APDU framing through the
TX/RX FIFO * - Minimum viable GSM 11.11 file system (MF, DF_GSM,
DF_TELECOM) * - Standard commands: SELECT (A4), READ_BINARY (B0),
READ_RECORD (B2), * GET_RESPONSE (C0), STATUS (F2), VERIFY_CHV (20), *
RUN_GSM_ALGORITHM (88) Test SIM identity (matches the
standard test PLMN 001/01): * IMSI = 001 01 0000000001 (15 digits) *
ICCID = 8901010000000000001 F (BCD swapped) * Ki = 00..00 (16 bytes —
auth always returns deterministic SRES/Kc) Bytes flow:
firmware writes APDU bytes one at a time to DTX. We parse * the 5-byte
ISO 7816 header, optionally collect data bytes (P3 of length * for
outgoing case 3) and dispatch. Response bytes are pushed to RX FIFO, *
IT_RX raised, IRQ pulsed. */
#include “qemu/osdep.h” #include “qemu/timer.h” #include
“qemu/main-loop.h” #include “exec/cpu-common.h” #include “hw/core/cpu.h”
#include “hw/arm/calypso/calypso_sim.h”
#define SIM_LOG(…) do { fprintf(stderr, “[sim]”
VA_ARGS); fputc(‘’, stderr); } while(0)
#define APDU_MAX_LEN 261 #define RX_FIFO_SIZE 512 #define
ATR_DELAY_NS 1000000 /* 1 ms simulated */
/* Minimal valid ATR (4 bytes): * TS = 0x3B (direct convention) * T0
= 0x02 (Y1=0 → no TA/TB/TC/TD interface bytes, * K =2 → 2 historical
bytes follow) * Hist[0..1] = 0x14 0x50 (arbitrary, identifies a test
card) * No TCK byte because no T!=0 protocol indicated. * Total bytes
the firmware will read = 4. */ static const uint8_t SIM_ATR[] = { 0x3B,
0x02, 0x14, 0x50 };
/* ———- file system ———————————————- */
typedef enum { EF_TRANSPARENT = 0, EF_LINEAR_FIXED = 1, EF_CYCLIC =
2, EF_DF = 0xF0, /* not a real EF — directory entry */ EF_MF = 0xF1, }
EfStructure;
typedef struct SimFile { uint16_t fid; uint16_t parent; uint8_t
structure; uint16_t size; /* total bytes (transparent) or rec_len * nrec
/ uint8_t rec_len; / records (linear/cyclic) / uint8_t
data[64]; / in-line storage (small EFs only) */ } SimFile;
/* SIM identity defaults (overridden at boot from the osmocom-bb
mobile * config — see load_config_from_file). EF_IMSI is GSM 11.11
BCD-packed * with the standard length-byte + parity-nibble layout.
*/
static SimFile sim_files[] = { { 0x3F00, 0x0000, EF_MF, 0, 0, {0} },
/* MF root / { 0x7F20, 0x3F00, EF_DF, 0, 0, {0} }, / DF_GSM
/ { 0x7F10, 0x3F00, EF_DF, 0, 0, {0} }, / DF_TELECOM / {
0x2FE2, 0x3F00, EF_TRANSPARENT, 10, 0, / EF_ICCID / { 0x98,
0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF1 } }, { 0x6F07,
0x7F20, EF_TRANSPARENT, 9, 0, / EF_IMSI / { 0x08, 0x09, 0x10,
0x10, 0x00, 0x00, 0x00, 0x00, 0xF1 } }, { 0x6F30, 0x7F20,
EF_TRANSPARENT, 24, 0, / EF_PLMNsel: 001 01 = FFFFFF empty list
/ { 0x00, 0xF1, 0x10, / PLMN 001 01 / 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, / EF_SST: services / { 0xFF, 0x33, 0xFF,
0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } }, {
0x6F78, 0x7F20, EF_TRANSPARENT, 2, 0, / EF_ACC: access class /
{ 0x00, 0x01 } }, { 0x6FAD, 0x7F20, EF_TRANSPARENT, 4, 0, / EF_AD:
admin data / { 0x00, 0x00, 0x00, 0x02 } }, { 0x6F7E, 0x7F20,
EF_TRANSPARENT, 11, 0, / EF_LOCI: location info / { 0xFF, 0xFF,
0xFF, 0xFF, / TMSI / 0xFF, 0xFF, 0xFF, / LAI = unknown
/ 0x00, 0x00, / TMSI time / 0xFF, 0x00 } }, / update
status: not updated / { 0x6F74, 0x7F20, EF_TRANSPARENT, 16, 0,
/ EF_BCCH / { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF } }, { 0x6F46, 0x7F20,
EF_TRANSPARENT, 17, 0, / EF_SPN */ { 0x01,
‘Q’,‘E’,‘M’,‘U’,‘-’,‘S’,‘I’,‘M’, ’ ‘,’ ‘,’ ‘,’ ‘,’ ‘,’ ‘,’ ‘,’ ’ } }, };
#define SIM_FILE_COUNT (sizeof(sim_files) / sizeof(sim_files[0]))
static SimFile find_file(uint16_t fid, uint16_t parent_pref) {
/ Try parent-scoped first (for relative selects), then global. */
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;
}
/* ———- IMSI BCD encoding ————————————— */
/* Encode a numeric IMSI string (e.g. “001010000000001”) into the GSM
11.11 * EF_IMSI 9-byte layout: * byte 0: length of the actual data
following (1..8) * byte 1: high nibble = first IMSI digit, low nibble =
parity * (parity = 9 if odd # digits, 1 if even) * bytes 2..N: pairs of
digits, low digit in low nibble * (last byte may have F filler in high
nibble) / 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; }
/* ———- card state ———————————————– */
struct CalypsoSim { qemu_irq irq; QEMUTimer atr_timer; QEMUTimer
wt_timer; /* fires IT_WT after RX FIFO drains * — tells firmware
“no more bytes coming” / / (Removed 2026-05-27)
clear_edge_timer / pending_edge_clear * supprimés. Justification
ground-truth dans le case CALYPSO_SIM_REG_IT * du read handler
(read-to-clear immédiat, pas W1C). / uint8_t ki[16]; / COMP128
secret key from mobile.cfg */ bool ki_valid;
/* register shadows */
uint16_t cmd, stat, conf1, conf2, maskit, it_cd;
/* IT register — bits set as events occur, cleared on read */
uint16_t it;
/* TX FIFO (firmware → SIM) — APDU assembly */
uint8_t apdu[APDU_MAX_LEN];
int apdu_pos; /* bytes received so far */
int apdu_expected; /* total expected length */
/* RX FIFO (SIM → firmware) */
uint8_t rx[RX_FIFO_SIZE];
int rx_head, rx_tail;
/* selected file context */
uint16_t selected_df; /* current directory (MF or DF_GSM/TELECOM) */
uint16_t selected_ef; /* last selected EF (for SELECT response) */
/* GET RESPONSE pending data */
uint8_t resp_buf[64];
int resp_len;
bool powered;
};
/* ———- RX FIFO ————————————————— */
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) { 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]); }
/* Forward decl / static void update_irq(CalypsoSim s);
/* IT_WT semantics: fires when no new char arrives within the
configured * “wait time” after the last byte. The osmocom-bb
sim_irq_handler uses * IT_WT to flag rxDoneFlag when calypso_sim_receive
was invoked with * expected_length=0 (open-ended ATR receive). We
schedule WT a few * milliseconds after the FIFO drains. / #define
WT_DELAY_NS 2000000 / 2 ms simulated */
static void fire_wt(void opaque) { CalypsoSim s = opaque; if
(rx_count(s) > 0) return; /* new bytes arrived — cancel WT */
s->it |= CALYPSO_SIM_IT_WT; SIM_LOG(“WT timeout fired (RX FIFO
empty)”); update_irq(s);
/* rxDoneFlag side-effect : géré dans calypso_sim_reg_read SIM_IT
* case (fires sur chaque IT_WT observé par le firmware, couvre toutes
* les SIM ops ATR/SELECT/READ_BINARY/etc). Voir doc complète là-bas.
* Le calypso_trx kick 200 Hz complète pour invalidation cache TB. */
}
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); }
/* IT_RX semantics per Calypso TRM: the bit is set whenever there is
* a byte waiting to be read in the RX FIFO (DRX). It auto-clears on *
read access to DRX (when the FIFO empties). We mirror that by *
recomputing IT_RX from the FIFO occupancy at every IT/DRX touch. /
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;
}
/* Update the IRQ line from the current IT register state. The
Calypso * INTH is level-sensitive — a pulse can be missed when the ARM
has * IRQs masked (CPSR I=1) at the moment of the rise/fall. We hold the
* line high while any unmasked IT bit is set. / 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); }
static void raise_rx_irq(CalypsoSim *s) { update_irq(s); }
/* (Removed 2026-05-27) clear_edge_cb supprimé — voir SIM_IT read
handler. */
/* ———- ATR delivery ——————————————— */
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); }
/* ———- SELECT response (FCP / response template) ————— */
static int build_select_response(CalypsoSim s, SimFile f,
uint8_t out) { / GSM 11.11 SELECT response (status info data).
22 bytes for EF, 22 bytes * for DF/MF. We build a simple but
firmware-friendly template. / int p = 0; out[p++] = 0x00; out[p++] =
0x00; / RFU / out[p++] = (f->size >> 8) & 0xFF;
/ file size MSB / out[p++] = f->size & 0xFF; / file
size LSB / out[p++] = (f->fid >> 8) & 0xFF; / file
ID MSB / out[p++] = f->fid & 0xFF; / file ID LSB /
if (f->structure == EF_DF || f->structure == EF_MF) { out[p++] =
(f->structure == EF_MF) ? 0x01 : 0x02; / file type /
out[p++] = 0x00; / RFU / out[p++] = 0x00; out[p++] = 0x00;
out[p++] = 0x00; out[p++] = 0x00; out[p++] = 0x00; out[p++] = 0x00;
out[p++] = 0x00; out[p++] = 0x09; / GSM data length / out[p++]
= 0x91; / file characteristics / out[p++] = 0x00; / num
child DFs / out[p++] = 0x00; / num child EFs / out[p++] =
0x04; / CHVs / out[p++] = 0x00; out[p++] = 0x83; / CHV1
status / out[p++] = 0x83; / unblock / out[p++] = 0x83;
/ CHV2 / out[p++] = 0x83; } else { out[p++] = 0x04; / file
type EF / out[p++] = 0x00; out[p++] = 0x00; out[p++] = 0x00;
out[p++] = 0x00; out[p++] = 0xAA; / access conditions /
out[p++] = 0x00; out[p++] = 0x00; out[p++] = 0x00; / file status
/ out[p++] = 0x02; / length of remaining / out[p++] =
(f->structure == EF_TRANSPARENT) ? 0x00 : (f->structure ==
EF_LINEAR_FIXED) ? 0x01 : 0x03; out[p++] = f->rec_len; / record
length */ } return p; }
/* ———- APDU dispatch ——————————————- */
static void respond_sw(CalypsoSim *s, uint8_t sw1, uint8_t sw2) {
rx_push(s, sw1); rx_push(s, sw2); }
/* For T=0, GSM 11.11 has special procedure: when response data
exists, * card sends: SW1=0x9F (or 0x6C/0x61) + SW2=length * Then
firmware does GET_RESPONSE to read the actual data. / 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; / 9F xx = “data available, do GET_RESPONSE
for xx bytes” */ 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];
/* Pick parent context: 3F00 reset, 7Fxx changes DF, 2Fxx EF under MF,
* 6Fxx EF under current DF. */
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("SELECT 0x%04x (%s) → %d bytes pending",
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; }
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); }
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) { /* Test SIM accepts any PIN. Real card would
compare and decrement * remaining attempts on mismatch. */ (void)p2;
(void)lc; (void)data; SIM_LOG(“VERIFY_CHV → always OK”); respond_sw(s,
0x90, 0x00); }
static void cmd_run_gsm_algo(CalypsoSim s, uint8_t lc, const
uint8_t data) { /* RAND in (16 bytes), SRES out (4 bytes) + Kc (8
bytes) = 12 bytes. / if (lc != 16) { respond_sw(s, 0x67, 0x00);
return; } / Test SIM: deterministic SRES = first 4 bytes of RAND ^
0xAA, Kc = 0. */ uint8_t resp[12]; 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 (RAND[0]=0x%02x) → SRES[0]=0x%02x”,
data[0], resp[0]); 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 p1 = s->apdu[2];
uint8_t p2 = s->apdu[3]; uint8_t p3 = s->apdu[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);
/* GSM 11.11 expects CLA=A0; ISO 7816 base allows other classes. */
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 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);
}
/* When firmware writes a byte to DTX, accumulate. After 5 header
bytes * we know the case (incoming/outgoing) and how much more to read.
/ G_GNUC_UNUSED static void apdu_tx_byte(CalypsoSim s, uint8_t
b) { if (s->apdu_pos < APDU_MAX_LEN) {
s->apdu[s->apdu_pos++] = b; } if (s->apdu_pos == 5) { /* For
simplicity: if INS is a known WRITE-class command (data sent * by
firmware), expect P3 more bytes. Otherwise (READ class), * dispatch
immediately. / 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; } / Procedure byte: T=0 expects card to
ACK by echoing INS. * The firmware reads this from DRX before sending
the data part. */ 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; } }
/* ———- public register interface ——————————- */
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; / card detected / v |=
CALYPSO_SIM_STAT_TXPAR; / parity always OK / 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; / Edge bits (NATR/WT/OV/TX) are read-clear;
level bit RX stays. AUDIT FIX 2026-05-08 night (Claude web Q2
hardening) : was * s->it &= CALYPSO_SIM_IT_RX; * which clears ANY
bit set after the snapshot (race with concurrent * fire_wt / IRQ
handlers raising new bits). Correct semantic : clear * only edge bits
that were observed in v, so a bit raised between * snapshot
and clear survives. RX bit always preserved (level). / / Per
OsmocomBB firmware spec (calypso/sim.c self-doc) : * NATR/WT/OV : clear
on read of REG_SIM_IT ← géré ici * TX : clear on write to REG_SIM_DTX ←
géré dans write handler * RX : implicit (level-sensitive via
refresh_it_rx) * Ancien code v & ~CALYPSO_SIM_IT_RX
clearait IT_TX par erreur. / uint16_t edge_seen = v &
(CALYPSO_SIM_IT_NATR | CALYPSO_SIM_IT_WT | CALYPSO_SIM_IT_OV); /
Read-to-clear immédiat (per OsmocomBB firmware spec).
Hardware Calypso REG_SIM_IT (firmware/calypso/sim.c L245/251/257 *
self-doc) : NATR/WT/OV cleared on read access to
REG_SIM_IT. * TX cleared on REG_SIM_DTX write. RX
level-sensitive (via FIFO). * sim_irq_handler L391-414 lit SIM_IT 1× et
n’écrit JAMAIS d’ack * — il s’appuie 100% sur le read-to-clear. NB :
read-to-clear ≠ * W1C (W1C = write-1-to-clear, read non-destructif,
comportement * HW différent). (Removed 2026-05-27) Defer 1µs
virtuel précédent escapait une * race cpu_io_recompile TB-truncation
INEXISTANTE : * - cputlb.c L1273-1274 : cpu_io_recompile longjmp via *
cpu_loop_exit_noexc AVANT memory_region_dispatch_read * - Le handler
MMIO fire 1× exactement par LDR (probe UART RBR * 1:1 ratio + probe
SIM_IT mem_io_pc identique sur 40 reads * confirment empiriquement) * -
Défer cassait le RC : sous icount=auto le firmware busy-poll * SIM_IT,
chaque read reschedulait le timer +1µs, clear jamais * fired, IT_WT
stays raised, IRQ stays asserted, ARM en * service IRQ perpétuel jamais
l’opportunité de re-LDR * rxDoneFlag → deadlock calypso_sim_powerup. * -
Cf. session_20260527 dans memory. */ s->it &= ~edge_seen;
update_irq(s);
/* Lighter instrumentation : just IT bits + FIFO state, no PC read.
* The ARM_PC access via env.regs[15] from inside an MMIO read may
* itself trigger TB recompile under -icount auto. Now we know all
* 5 reads come from PC=0x82249c (sim_irq_handler), the PC log
* isn't needed and removing it eliminates a potential TB-abort
* trigger separate from update_irq. */
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); /* maybe clear IT_RX */
/* (Moved 2026-05-27, probe-validated) schedule_wt déplacé dans
* MASKIT write handler. Avant : armé ici dès FIFO drained → WT
* fire pendant delay_ms(100) post-CMDSTART du firmware, AVANT le
* `bl calypso_sim_receive`. Handler set rxDoneFlag=1, puis
* sim_receive L351 écrase à 0 → poll forever.
* Probe [rxDone] confirme chrono : #3 WR val=1 PC=0x8228c4 vt=11ms,
* #4 WR val=0 PC=0x8229b4 vt=17ms (5ms après). Inversion fatale.
* Maintenant WT armé quand sim_receive unmask IT_WT → fire APRÈS. */
return b | (1 << 8); /* parity OK */
}
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)”); / AUDIT FIX 2026-05-08 night : was
schedule_atr() (1ms VIRTUAL * timer). Under -icount auto, virtual time
is rate-limited; * the firmware’s SIM driver enters a busy-loop polling
* rxDoneFlag (firmware data 0x830510) with IRQs masked * (PSR I=1)
before the timer fires, deadlocking the ARM CPU. * Direct delivery:
bytes in FIFO at MMIO write return time, * IRQ raised immediately. Same
effect as the timer being 0ns. * Equivalent under icount=off (timer
fires ~instantly anyway). / 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)) {
SIM_LOG(“RESET → ATR delivered (synchronous)”); 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; / Same audit fix as CMDSTART above. / if
(s->powered) deliver_atr(s); } break; case CALYPSO_SIM_REG_STAT:
s->stat = val; break; case CALYPSO_SIM_REG_CONF1: s->conf1 = val;
break; case CALYPSO_SIM_REG_CONF2: s->conf2 = val; break; case
CALYPSO_SIM_REG_IT: / W1C ignored / break; case
CALYPSO_SIM_REG_DRX: / read-only / break; case
CALYPSO_SIM_REG_DTX: apdu_tx_byte(s, (uint8_t)(val & 0xFF)); /
Per firmware spec : REG_SIM_IT_SIM_TX clears on write to DTX * (firmware
self-doc calypso/sim.c L264). Sans ça, IT_TX restait * latched
indéfiniment → TX path bloqué après le 1er byte. / s->it &=
~CALYPSO_SIM_IT_TX; update_irq(s); break; case CALYPSO_SIM_REG_MASKIT: {
/ Arm WT timer quand firmware unmask IT_WT (= sim_receive L358 : *
writew(~(MASK_RX|MASK_WT), MASKIT)). À ce moment,
sim_receive * a déjà fait son rxDoneFlag=0 (L351). Le WT fire APRÈS,
handler * set rxDoneFlag=1, poll exit. Ordre garanti dans toutes les *
icount modes. Condition : WT bit UNMASKED (bit clear) + FIFO
empty + IT_WT * not already set. Idempotent : timer_mod_ns ré-arme si
déjà armé. * Pas de check sur transition prev_mask→val (MASKIT default
BSS=0 * dans QEMU = déjà unmasked, transition jamais détectée). *
Probe-validated 2026-05-27. */ 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; }
}
/* ———- mobile.cfg parser ————————————— */
/* Parse the osmocom-bb layer23 mobile config: * ms 1 * test-sim *
imsi 001010000000001 * ki comp128 00 11 22 … 16 hex bytes * We pull the
imsi (→ EF_IMSI) and ki (→ s->ki for RUN_GSM_ALGORITHM). / 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 == ‘) p++; if (strncmp(p,
“test-sim”, 8) == 0) { in_test_sim = true; continue; } if (!in_test_sim)
continue; /* Section ends at a non-indented keyword (e.g. “ms”, “exit”,
“!”) / if (line[0] != ’ ’ && line[0] != ’ && line[0]
!= ’’) { 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->wt_timer =
timer_new_ns(QEMU_CLOCK_VIRTUAL, fire_wt, s); /* (Removed 2026-05-27)
clear_edge_timer — read-to-clear immédiat */ s->selected_df = 0x3F00;
s->selected_ef = 0x3F00;
/* Pull IMSI / Ki from the same config the layer23 `mobile` is using.
* The launcher (run_new.sh) sets CALYPSO_SIM_CFG to its $MOBILE_CFG,
* so the SIM and the mobile stay in sync without us hardcoding any
* path. If the env var is missing we keep the file-system defaults. */
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;
}
================================================================================
FILE: hw/arm/calypso/calypso_soc.c SIZE: 17036 bytes, 429 lines
================================================================================
/ Calypso SoC - TI Calypso DBB (Digital Baseband) * DEBUG
BUILD — verbose memory map logging SPDX-License-Identifier:
GPL-2.0-or-later */
#include “qemu/osdep.h” #include “qapi/error.h” #include
“hw/sysbus.h” #include “hw/irq.h” #include “hw/qdev-properties.h”
#include “hw/core/cpu.h” /* current_cpu, CPUClass::get_pc — rxDone probe
/ #include “exec/memory.h” #include “exec/address-spaces.h” #include
“hw/misc/unimp.h” #include “sysemu/sysemu.h” #include
“hw/arm/calypso/calypso_soc.h” #include
“hw/arm/calypso/calypso_full_pcb.h” / PCB orchestrator init */
#include “hw/arm/calypso/calypso_trx.h”
/* Global references for TDMA tick to kick UART RX (both channels). *
g_uart_irda must be polled too — under -icount, the per-UART REALTIME *
rx_poll_timer fires at wall rate but CPU runs at virtual rate, so PTY *
data backs up. Polling from tdma_tick (VIRTUAL clock) keeps IRDA RX *
drained in lockstep with the rest of the emulator. /
CalypsoUARTState g_uart_modem; CalypsoUARTState *g_uart_irda;
#include “chardev/char-fe.h” #include “chardev/char.h” #include
“qemu/error-report.h” #include “hw/arm/calypso/calypso_uart.h” #include
“hw/arm/calypso/calypso_debug.h”
/* —- Memory map —- / #define CALYPSO_IRAM_BASE 0x00800000
#define CALYPSO_IRAM_SIZE (256 1024)
/* —- Peripheral addresses —- */ #define CALYPSO_INTH_BASE 0xFFFFFA00
#define CALYPSO_TIMER1_BASE 0xFFFE3800 #define CALYPSO_TIMER2_BASE
0xFFFE3C00 #define CALYPSO_SPI_BASE 0xFFFE3000 #define
CALYPSO_KEYPAD_BASE 0xFFFE4800
#define CALYPSO_UART_IRDA 0xFFFF5000 #define CALYPSO_UART_MODEM
0xFFFF5800
/* —- IRQ numbers —- */ #define IRQ_TIMER1 1 #define IRQ_TIMER2 2
#define IRQ_UART_MODEM 7 #define IRQ_SPI 13 #define IRQ_UART_IRDA 18
/* —- Stub MMIO —- */
static uint64_t calypso_mmio8_read(void o, hwaddr a, unsigned s)
{ return 0; } static void calypso_mmio8_write(void o, hwaddr a,
uint64_t v, unsigned s) {} static const MemoryRegionOps
calypso_mmio8_ops = { .read = calypso_mmio8_read, .write =
calypso_mmio8_write, .endianness = DEVICE_NATIVE_ENDIAN, .impl = {
.min_access_size = 1, .max_access_size = 1 }, };
static uint64_t calypso_mmio16_read(void o, hwaddr a, unsigned s)
{ return 0; } static void calypso_mmio16_write(void o, hwaddr a,
uint64_t v, unsigned s) {} static const MemoryRegionOps
calypso_mmio16_ops = { .read = calypso_mmio16_read, .write =
calypso_mmio16_write, .endianness = DEVICE_NATIVE_ENDIAN, .impl = {
.min_access_size = 2, .max_access_size = 2 }, };
/* —- CNTL register (EXTRA_CONF) at 0xFFFFFD00 —- * Bit [8:9] =
bootrom mapping control * When cleared (0), IRAM is aliased at
0x00000000 * When set (3), internal ROM is mapped at 0x00000000
On real Calypso, firmware calls calypso_bootrom(0) to disable *
bootrom and enable IRAM at address 0 for exception vectors. / static
uint64_t calypso_cntl_read(void opaque, hwaddr offset, unsigned
size) { CalypsoSoCState *s = CALYPSO_SOC(opaque); if (offset == 0)
return s->extra_conf; return 0; }
static void calypso_cntl_write(void opaque, hwaddr offset,
uint64_t value, unsigned size) { CalypsoSoCState s =
CALYPSO_SOC(opaque); if (offset != 0) return;
s->extra_conf = (uint16_t)value;
/* Bits [9:8] control bootrom/IRAM mapping at address 0 */
bool bootrom_enabled = (value >> 8) & 3;
if (!bootrom_enabled && !s->iram_at_zero) {
/* Map IRAM at address 0 (higher priority than flash) */
MemoryRegion *sysmem = get_system_memory();
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;
fprintf(stderr, "[SOC] CNTL: IRAM aliased at 0x00000000 (bootrom disabled)\n");
} else if (bootrom_enabled && s->iram_at_zero) {
/* Remove IRAM alias */
memory_region_del_subregion(get_system_memory(), &s->iram_alias);
object_unparent(OBJECT(&s->iram_alias));
s->iram_at_zero = false;
fprintf(stderr, "[SOC] CNTL: IRAM alias removed (bootrom enabled)\n");
}
}
static const MemoryRegionOps calypso_cntl_ops = { .read =
calypso_cntl_read, .write = calypso_cntl_write, .endianness =
DEVICE_NATIVE_ENDIAN, .impl = { .min_access_size = 2, .max_access_size =
2 }, };
static uint64_t calypso_kp_read(void o, hwaddr a, unsigned s) {
return 0xFF; } static void calypso_kp_write(void o, hwaddr a,
uint64_t v, unsigned s) {} static const MemoryRegionOps
calypso_keypad_ops = { .read = calypso_kp_read, .write =
calypso_kp_write, .endianness = DEVICE_NATIVE_ENDIAN, };
static void add_stub(MemoryRegion sys, const char name,
hwaddr base, const MemoryRegionOps ops) { MemoryRegion mr =
g_new(MemoryRegion, 1); memory_region_init_io(mr, NULL, ops, NULL, name,
0x100); memory_region_add_subregion(sys, base, mr); fprintf(stderr,
“[SOC] stub ‘%s’ @ 0x%08lx (0x100)”, name, (unsigned long)base); }
/* ================================================================ *
rxDoneFlag PROBE (env-gated CALYPSO_RXDONE_PROBE=1) *
================================================================ *
Overlay IO region 4 bytes @ 0x008302d4 (= rxDoneFlag, BSS firmware). *
Intercepts reads/writes, logs PC+vtime+seqnum+value, forwards to
backing. * Replicates ce que faisait
gdb watch *(unsigned int*)0x008302d4 + log, * sans gdb
attaché (donc sans perturbation hbreak TCG). Caveat (c web
2026-05-27) : transforme cette 4-byte zone en MMIO, * perturbe le chemin
d’accès TCG (slow-path au lieu de direct RAM load). * OK pour capturer
la chronologie en un run ; ne pas laisser actif en prod. */ static
uint32_t rxdone_probe_backing = 0; static uint64_t rxdone_probe_seq =
0;
static uint64_t rxdone_probe_read(void opaque, hwaddr addr,
unsigned size) { uint8_t p = (uint8_t
)&rxdone_probe_backing; uint64_t val = 0; if (size == 4) val =
rxdone_probe_backing; else if (size == 2) val = (uint16_t )(p +
addr); else val = (p + addr); /* RD log silenced by default
(busy-poll = flood). Decommente si besoin. / / fprintf(stderr,
“[rxDone] RD +%lx size=%u = 0x%lx”, addr, size, val); */ return val;
}
static void rxdone_probe_write(void opaque, hwaddr addr, uint64_t
val, unsigned size) { uint8_t p = (uint8_t
*)&rxdone_probe_backing; uint32_t prev = rxdone_probe_backing;
/* Guest PC at the write instant. cpu->cc->get_pc is the generic
* CPUClass accessor (avoids target/arm/cpu.h include here). */
uint64_t pc = 0;
if (current_cpu && current_cpu->cc && current_cpu->cc->get_pc) {
pc = current_cpu->cc->get_pc(current_cpu);
}
uint64_t vt = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
if (size == 4) rxdone_probe_backing = (uint32_t)val;
else if (size == 2) *(uint16_t *)(p + addr) = (uint16_t)val;
else *(p + addr) = (uint8_t)val;
rxdone_probe_seq++;
fprintf(stderr,
"[rxDone] #%llu WR +%lu size=%u val=0x%lx (was 0x%x) PC=0x%lx vt=%lu\n",
(unsigned long long)rxdone_probe_seq,
(unsigned long)addr, size,
(unsigned long)val, prev,
(unsigned long)pc,
(unsigned long)vt);
}
static const MemoryRegionOps rxdone_probe_ops = { .read =
rxdone_probe_read, .write = rxdone_probe_write, .endianness =
DEVICE_NATIVE_ENDIAN, .impl = { .min_access_size = 1, .max_access_size =
4 }, .valid = { .min_access_size = 1, .max_access_size = 4 }, };
static MemoryRegion rxdone_probe_mr;
static void rxdone_probe_install(MemoryRegion sysmem, DeviceState
dev) { const char e = cdbg_env(“RXDONE”); if (!e || e[0] !=
‘1’) return; memory_region_init_io(&rxdone_probe_mr, OBJECT(dev),
&rxdone_probe_ops, NULL, “rxdone_probe”, 4); / Priority 1 >
IRAM (priority 0) → ARM accès au 4-byte 0x008302d4 * passe par cette IO
region. Reads gdb (debug accès) idem. */
memory_region_add_subregion_overlap(sysmem, 0x008302d4,
&rxdone_probe_mr, 1); fprintf(stderr, “[rxDone] PROBE installed @
0x008302d4 (overlay prio=1,” “covers 4 bytes rxDoneFlag)”); }
/* ================================================================ *
SoC realize *
================================================================ */
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;
fprintf(stderr, "[SOC] === calypso_soc_realize START ===\n");
/* ---- IRAM at 0x00800000 ONLY ----
* NO alias at 0x00000000 — flash lives there (board-level).
*/
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);
fprintf(stderr, "[SOC] IRAM @ 0x%08x (%d KiB) — NO alias at 0x00000000\n",
CALYPSO_IRAM_BASE, CALYPSO_IRAM_SIZE / 1024);
/* rxDoneFlag debug probe — overlay IO sur 0x008302d4 si env activé.
* Install APRÈS IRAM (subregion_overlap avec prio plus haute). */
rxdone_probe_install(sysmem, dev);
/* ---- INTH ---- */
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);
/* Pass INTH's output IRQs (parent_irq, parent_fiq) through
* the SoC device so the board can connect them to the CPU.
* This avoids the ordering issue where sysbus_connect_irq
* captures a NULL qemu_irq before the board connects it. */
sysbus_pass_irq(sbd, SYS_BUS_DEVICE(&s->inth));
#define INTH_IRQ(n) qdev_get_gpio_in(DEVICE(&s->inth), (n))
/* ---- Timer 1 ---- */
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(IRQ_TIMER1));
/* timer #1 = hwtimer lu par check_lost_frame() : active le latch frame-locked
* (supprime le spam LOST sous REALTIME/-icount). */
calypso_timer_register_lost(DEVICE(&s->timer1));
/* ---- Timer 2 ---- */
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(IRQ_TIMER2));
/* ---- I2C stub ---- */
DeviceState *i2c_dev = qdev_new("calypso-i2c");
sysbus_realize_and_unref(SYS_BUS_DEVICE(i2c_dev), &error_fatal);
sysbus_mmio_map(SYS_BUS_DEVICE(i2c_dev), 0, 0xFFFE1800);
/* ---- SPI ---- */
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(IRQ_SPI));
/* ---- UART MODEM ---- */
{
Chardev *chr = qemu_chr_find("modem");
if (!chr) chr = serial_hd(0);
fprintf(stderr, "[SOC] UART modem: chardev → %s\n",
chr ? (chr->label ? chr->label : "(no label)") : "NULL");
object_initialize_child(OBJECT(dev), "uart-modem",
&s->uart_modem, TYPE_CALYPSO_UART);
qdev_prop_set_string(DEVICE(&s->uart_modem), "label", "modem");
if (chr) {
qdev_prop_set_chr(DEVICE(&s->uart_modem), "chardev", chr);
}
if (!sysbus_realize(SYS_BUS_DEVICE(&s->uart_modem), &err)) {
error_propagate(errp, err); return;
}
sysbus_mmio_map(SYS_BUS_DEVICE(&s->uart_modem), 0, CALYPSO_UART_MODEM);
sysbus_connect_irq(SYS_BUS_DEVICE(&s->uart_modem), 0,
INTH_IRQ(IRQ_UART_MODEM));
g_uart_modem = &s->uart_modem;
/* L1CTL socket: sercomm↔L1CTL relay for OsmocomBB mobile */
{
const char *l1ctl_path = getenv("L1CTL_SOCK");
l1ctl_sock_init(&s->uart_modem, l1ctl_path ? l1ctl_path : "/tmp/osmocom_l2");
}
}
/* ---- UART IRDA ---- */
{
Chardev *chr = qemu_chr_find("irda");
if (!chr) chr = serial_hd(1);
object_initialize_child(OBJECT(dev), "uart-irda",
&s->uart_irda, TYPE_CALYPSO_UART);
qdev_prop_set_string(DEVICE(&s->uart_irda), "label", "irda");
if (chr) {
qdev_prop_set_chr(DEVICE(&s->uart_irda), "chardev", chr);
}
if (!sysbus_realize(SYS_BUS_DEVICE(&s->uart_irda), &err)) {
error_propagate(errp, err); return;
}
sysbus_mmio_map(SYS_BUS_DEVICE(&s->uart_irda), 0, CALYPSO_UART_IRDA);
sysbus_connect_irq(SYS_BUS_DEVICE(&s->uart_irda), 0,
INTH_IRQ(IRQ_UART_IRDA));
g_uart_irda = &s->uart_irda;
}
/* ---- TRX bridge (pure hardware) ---- */
{
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
/* ---- Stubs ----
*
* IMPORTANT: NO stub at 0x00000300 ("calypso.low300")!
* That address falls inside the flash range 0x00000000–0x003FFFFF
* and would shadow pflash CFI queries → "Failed to initialize flash!"
*/
add_stub(sysmem, "calypso.keypad", CALYPSO_KEYPAD_BASE, &calypso_keypad_ops);
add_stub(sysmem, "calypso.tmr6800", 0xFFFE6800, &calypso_mmio8_ops);
add_stub(sysmem, "calypso.mmio_80xx", 0xFFFE8000, &calypso_mmio8_ops);
add_stub(sysmem, "calypso.conf", 0xFFFEF000, &calypso_mmio16_ops);
add_stub(sysmem, "calypso.mmio_98xx", 0xFFFF9800, &calypso_mmio16_ops);
add_stub(sysmem, "calypso.dpll", 0xFFFFF000, &calypso_mmio16_ops);
add_stub(sysmem, "calypso.rhea", 0xFFFFF900, &calypso_mmio16_ops);
add_stub(sysmem, "calypso.clkm", 0xFFFFFB00, &calypso_mmio16_ops);
add_stub(sysmem, "calypso.mmio_fcxx", 0xFFFFFC00, &calypso_mmio16_ops);
/* CNTL (EXTRA_CONF) - controls IRAM-at-zero mapping */
memory_region_init_io(&s->cntl_iomem, OBJECT(dev), &calypso_cntl_ops, s,
"calypso.cntl", 0x100);
memory_region_add_subregion(sysmem, 0xFFFFFD00, &s->cntl_iomem);
s->extra_conf = 0x0300; /* bootrom enabled at reset */
s->iram_at_zero = false;
add_stub(sysmem, "calypso.dio", 0xFFFFFF00, &calypso_mmio8_ops);
/* NO calypso.low300 — it overlaps flash! */
/* Catch-all (lowest priority) */
{
MemoryRegion *mr = g_new(MemoryRegion, 1);
memory_region_init_io(mr, NULL, &calypso_mmio8_ops, NULL,
"calypso.catchall", 0x100000);
memory_region_add_subregion_overlap(sysmem, 0xFFF00000, mr, -1);
}
/* === PCB orchestrator init (threading PCB) ====================
* Spawn les threads optionnels par puce/unité quand CALYPSO_PCB_THREADS=1
* ou granulaire CALYPSO_PCB_THREAD_X=1 / CALYPSO_PCB_TICK_THREADS=1.
* Sans wire ici, les gates dans calypso_trx.c / calypso_tint0.c
* skip leur re-arm sans qu'aucun thread ne les remplace → ticks meurent. */
{
CalypsoPcb *pcb = calypso_pcb_init(NULL);
if (pcb) {
calypso_pcb_start_threads(pcb);
}
}
fprintf(stderr, "[SOC] === calypso_soc_realize DONE ===\n");
}
/* —- QOM boilerplate —- */
static Property calypso_soc_properties[] = {
DEFINE_PROP_END_OF_LIST(), };
static void calypso_soc_class_init(ObjectClass oc, void
data) { DeviceClass *dc = DEVICE_CLASS(oc); dc->realize =
calypso_soc_realize; device_class_set_props(dc, calypso_soc_properties);
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)
================================================================================
FILE: hw/arm/calypso/calypso_tint0.c SIZE: 4205 bytes, 134 lines
================================================================================
/ 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 */
#define TINT0_LOG(fmt, …)
fprintf(stderr, “[tint0]” fmt “”, ##VA_ARGS)
/* calypso_trx.c implements the actual frame work (DSP run, IRQs,
UART) / extern void calypso_tint0_do_tick(uint32_t fn); / orch
CLK→BTS is now driven from TINT0 (2× rate via 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;
/* Removed 2026-05-16 : compteur `[tint0]` retiré — dans la machine
* Calypso actuelle, calypso_tint0_start() n'est jamais appelé
* (le tick virtual est piloté par calypso_tdma_tick dans
* calypso_trx.c). Si tu armes tint0 plus tard pour de vrai, remets
* un compteur ici. Voir REPORT_CLAUDE_WEB_20260515_TIMING.md. */
/* 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 timer — gated par CALYPSO_PCB_TICK_THREADS. Si threading
* actif (= pcb spawn tint0 thread qui self-paces), on N'arme PAS le
* QEMUTimer pour éviter double-tick. */
{
static int pcb_threaded = -1;
if (pcb_threaded < 0) {
const char *e = 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 pour pcb tick thread — call la même body que le
QEMUTimer * callback. À appeler avec 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 ever advances. Resetting on every
* TINT0 start makes the firmware believe it just synchronized to a
* fresh hyperframe each run, which is the "fn injection" hack the
* user flagged 2026-04-07 night. Whoever owns the master clock
* (calypso_tint0_set_fn from a network-derived source) should seed
* fn before calling start; otherwise it inherits whatever value 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; }
================================================================================
FILE: hw/arm/calypso/calypso_tpu.c SIZE: 10799 bytes, 246 lines
================================================================================
/ calypso_tpu.c – TPU (Time Processing Unit) sequencer
emulation The ARM firmware (osmocom-bb-transceiver
include/calypso/tpu.h: *
tpu_enq_at/tpu_enq_move/tpu_enq_sync/tpu_enq_wait/tpu_enq_offset/ *
tpu_enq_dsp_irq/tpu_enq_sleep, calypso/tsp.c) programs a small *
micro-instruction scenario into TPU RAM, then commits it by writing *
TPU_CTRL_EN (dsp_end_scenario(), see calypso_dsp_done() in *
calypso_trx.c). On real silicon the TPU sequencer walks this program *
against its own qbit clock (QBITS_PER_TDMA=5000 per tpu.h): AT/WAIT *
advance or pause the sequencer’s position within the current TDMA *
frame (wrapping into the next frame when the target has already *
passed), SYNCHRO/OFFSET load bias registers, MOVE writes TSP/DSP *
peripheral registers. [2026-07-23] Full rewrite (v3)
implementing every opcode in * osmocom-bb-transceiver’s
enum tpu_instr/enum tpu_reg_int, per user *
direction (“on doit implementer toutes les operations TPU, check * osmo”
/ “sinon ou tu veux qu’il fasse ses operations si le TPU n’est * emule
nulle part”). v1 only handled MOVE to TSP_CTRL1/CTRL2/TX_1 * (single
byte) and dropped AT/OFFSET/SYNCHRO/WAIT entirely (instant, *
whole-scenario execution at commit time) – confirmed by the user’s * own
reading of the INTM-TRANS retry-loop logs: “on n’a implemente * nulle
part ce truc des onze trames”, i.e. l1s_rx_win_ctrl()’s *
for(i=0;i<11;i++) tpu_enq_at(0) (comment: “Delay 11 full
TDMA * frames”) had zero timing effect. v3 : real
qbit-position sequencer, replayed across true TDMA frame * ticks
(calypso_tpu_sequencer_tick(), called once per frame from *
calypso_tdma_tick()) instead of firing the whole scenario instantly. *
AT(t)/WAIT(t) advance a per-scenario qbit cursor (0..4999); when the *
target has already been passed (AT) or the relative advance overflows *
5000 (WAIT), that means N real frame boundaries must elapse first – *
modeled as N frame-tick pauses (our tick granularity is one frame, * not
one qbit, so sub-frame ordering within a single tick is still *
coalesced, but cross-frame delays are now genuinely real). Eleven *
consecutive AT(0) each force exactly one wrap (the cursor is always *
already at/after 0) = 11 real frame ticks, matching the “11 frames” *
intent. Full MOVE address map (enum tpu_reg_int): TX_1..TX_4
(multi-byte TSP * payload per calypso/tsp.c’s tsp_write(), MSB-first,
byte count from * TSP_CTRL1’s low 5 bits), TSP_CTRL1 (dev_idx<<5 |
bitlen-1), TSP_CTRL2 * (WR bit triggers the transfer), TSP_ACT_L/U
(tsp_act_update() enable * lines, 16-bit state), TSP_SET1-3 (tsp_setup()
static clock/CS config), * DSP_INT_PG (tpu_enq_dsp_irq(), TX-multislot
only – confirmed via * source: only caller is l1s_tx_multi_win_ctrl(),
not l1s_rx_win_ctrl()), * GAUGING_EN. Only dev_idx==0 (TWL3025, the sole
consumer wired up * today via calypso_iota_tsp_write) has a downstream
hardware model; * everything else is correctly parsed/decoded and logged
rather than * silently dropped, so a gap is visible instead of hidden.
One physical sequencer exists on real silicon (TPU RAM = one
buffer * the firmware overwrites before each re-arm) : a new commit *
(TPU_CTRL_EN) supersedes whatever was in flight, even mid-wait – *
faithful to hardware, not a simplification.
SPDX-License-Identifier: GPL-2.0-or-later */ #include “qemu/osdep.h”
#include “hw/arm/calypso/calypso_trx.h” #include
“hw/arm/calypso/calypso_debug.h” #include
“hw/arm/calypso/calypso_tsp.h”
#define TPU_LOG(fmt, …)
do { if (calypso_debug_enabled(“TPU”))
fprintf(stderr, “[calypso-tpu]” fmt “”, ##VA_ARGS); }
while (0)
/* TPU-native MOVE addresses (not TSP device registers – those are
owned * by calypso_tsp.h/calypso_tsp_owns_addr()). / #define
TPUI_DSP_INT_PG 0x10 / tpu_enq_dsp_irq() = tpu_enq_move(this, 1) */
#define TPUI_GAUGING_EN 0x11
/* TPU instruction opcode field (bits 15:13), osmocom’s
enum tpu_instr. */ #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
/* —- Sequencer state : one physical sequencer, like real silicon —-
/ static struct { uint16_t insns[CALYPSO_TPU_RAM_SIZE / 2]; int len;
int cursor; int qbit; / position within the current TDMA frame
/ int wait_frames; / >0 : paused, resume on tick when it
hits 0 / bool active; C54xState dsp; uint16_t tpu_regs;
/ CalypsoTRX’s regs[], for SYNCHRO/OFFSET */ } seq;
static void seq_exec_move(uint8_t addr, uint8_t data, uint32_t fn) {
if (calypso_tsp_owns_addr(addr)) { calypso_tsp_move(addr, data, fn);
return; } switch (addr) { case TPUI_DSP_INT_PG: if (seq.dsp &&
(data & 0x01)) { /* tpu_enq_dsp_irq(): sequenceur -> DSP
directement. Seul * appelant reel (tpu_window.c:199) =
l1s_tx_multi_win_ctrl() * (TX multislot), PAS l1s_rx_win_ctrl() (RX/FB)
– confirme * runtime (2026-07-23, 0 hit sur un run FB/SB natif complet).
* Geree quand meme (vrai trou de modelisation sinon). BRINT0 * (vec21,
IMR bit5) = seul vecteur natif dont le handler ROM * est installe (stub
RETE) mais jamais pris avant ce fix. * Log inconditionnel (pas besoin de
CALYPSO_DEBUG) : evenement * rare/diagnostique cle. */ static int
dsp_int_pg_log = 0; if (++dsp_int_pg_log <= 20) fprintf(stderr,
“[calypso-tpu] DSP_INT_PG MOVE data=0x%02x fn=%u” “-> BRINT0 (vec21)
#%d”, data, fn, dsp_int_pg_log); c54x_interrupt_ex(seq.dsp, 21, 5); }
break; case TPUI_GAUGING_EN: TPU_LOG(“GAUGING_EN <- 0x%02x fn=%u (no
consumer)”, data, fn); break; default: TPU_LOG(“MOVE unknown addr=0x%02x
data=0x%02x fn=%u”, addr, data, fn); break; } }
/* Run from the current cursor until AT/WAIT pauses us again (frame *
boundary must elapse first), or we reach the end of the scenario (real *
SLEEP / two padding zeros). / static void seq_run(uint32_t fn) {
while (seq.active && seq.cursor < seq.len) { uint16_t insn =
seq.insns[seq.cursor]; if (insn == 0x0000) { / Skip zero words:
Rhea bus 32-bit-alignment padding or the * final SLEEP (TPU_INSTR_SLEEP
is literally encoded as 0x0000). * Only stop once we’ve seen at least
one real instruction, and * only if the NEXT word is also zero (two
consecutive zeros = * real SLEEP, not padding) – heuristic proven
necessary by * the original single-shot implementation. / int next =
seq.cursor + 1; bool had_any = seq.cursor > 0; if (had_any &&
(next >= seq.len || seq.insns[next] == 0x0000)) { seq.active = false;
/ SLEEP: stop the sequencer / return; } seq.cursor++; continue;
} uint8_t opcode = (insn >> 13) & 0x7; uint16_t payload = insn
& 0x1FFF; / 13-bit time/data field */
if (opcode == TPU_OP_AT) {
/* AT(t): tpu_enq_at() already reduces t via tpu_mod5000(), so
* payload is the absolute target qbit (0..4999) within a TDMA
* frame. If we've already passed it this frame, reaching it
* again means waiting for the NEXT frame's occurrence -- one
* real frame tick. l1s_rx_win_ctrl()'s `for(11) tpu_enq_at(0)`
* hits this branch all 11 times (cursor is never < 0). */
seq.cursor++;
if (payload > seq.qbit) {
seq.qbit = payload;
continue;
}
seq.qbit = payload;
seq.wait_frames = 1;
return;
}
if (opcode == TPU_OP_WAIT) {
/* WAIT(t): "wait a certain period, in GSM qbits" -- relative
* advance from the current cursor. */
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) {
/* "Loading delta synchro/offset value in TPU register" --
* pure register load, no cursor/time effect of its own.
* TPU_SYNCHRO=0x000E, TPU_OFFSET=0x000C (byte offsets, /2 for
* the uint16 regs[] array) per calypso_trx.h. */
if (seq.tpu_regs) {
if (opcode == TPU_OP_SYNCHRO) seq.tpu_regs[TPU_SYNCHRO / 2] = payload;
else seq.tpu_regs[TPU_OFFSET / 2] = payload;
}
TPU_LOG("%s <- 0x%04x fn=%u", opcode == TPU_OP_SYNCHRO ? "SYNCHRO" : "OFFSET",
payload, fn);
seq.cursor++;
continue;
}
if (opcode == TPU_OP_MOVE) {
uint8_t addr = insn & 0x1F;
uint8_t data = (insn >> 5) & 0xFF;
seq_exec_move(addr, data, fn);
seq.cursor++;
continue;
}
/* opcode == TPU_OP_SLEEP but insn != 0x0000 can't happen (SLEEP's
* only bits are the opcode field, always encodes as plain 0x0000,
* handled above) -- unreachable in practice, skip defensively. */
seq.cursor++;
}
seq.active = false;
}
void calypso_tpu_run_scenario(uint16_t tpu_ram, C54xState
dsp, uint32_t fn) { calypso_tpu_run_scenario_regs(tpu_ram, dsp, fn,
NULL); }
void calypso_tpu_run_scenario_regs(uint16_t tpu_ram, C54xState
dsp, uint32_t fn, uint16_t tpu_regs) { / New commit
supersedes whatever was in flight – one physical * sequencer, firmware
overwrote TPU RAM before re-arming EN, exactly * like real hardware
would. tsp_act (TSPACT enable lines) is the one * piece of state that
legitimately survives across commits, matching * tsp.c’s static
tspact_state. */ 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.dsp = dsp; 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); }
================================================================================
FILE: hw/arm/calypso/calypso_trf6151.c SIZE: 3557 bytes, 101 lines
================================================================================
/ calypso_trf6151.c — modele de gain RF frontend TRF6151.
Porte de osmocom-bb src/target/firmware/rf/trf6151.c
(get_gain_reg) + * board/compal/rffe_dualband.c (SYSTEM_INHERENT_GAIN) +
layer1/agc.c * (agc_inp_dbm8_by_pm) + layer1/prim_pm.c (l1ddsp_meas_read
: pm = a_pm>>3). Chaine firmware : * pm_level =
dsp_api.db_r->a_pm[i] >> 3 (prim_pm.c) * bb_dbm = pm_level / 8
(1/8 dBm -> dBm) * rf_dbm = bb_dbm - (SYSTEM_INHERENT_GAIN +
trf6151_get_gain()) (agc.c) * => a_pm = bb_dbm * 64 = (rf_dbm +
total_gain) * 64. Le firmware programme trf6151 par TSP (dev
1) : tsp_write(uid,16,reg|val). * REG_RX (adresse 0) porte le gain (FE
high/low bits[10:9], VGA bits[15:11]). * On decode REG_RX a chaque write
pour suivre le gain vivant (l’AGC le baisse * quand le signal est fort)
-> a_pm recalcule -> rf cible tenu quel que soit * le gain choisi
par le firmware. SPDX-License-Identifier: GPL-2.0-or-later */
#include “qemu/osdep.h” #include <stdlib.h> #include
“hw/arm/calypso/calypso_trf6151.h”
/* —- constantes portees telles quelles du firmware —- / #define
SYSTEM_INHERENT_GAIN 71 / rffe_dualband.c / #define
TRF6151_FE_GAIN_LOW 7 / trf6151.c / #define
TRF6151_FE_GAIN_HIGH 27 #define TRF6151_VGA_GAIN_MIN 14 #define
RX_VGA_GAIN_SHIFT 11 / REG_RX bits[15:11] / / REG_RX
addresse = 0 dans l’enum trf6151_reg ; le firmware transmet reg|val, *
donc les 3 bits de poids faible du mot = l’adresse registre. */ #define
TRF6151_REG_RX 0 #define TRF6151_REG_ADDR_MASK 0x7
/* REG_RX au reset (trf6151_reg_cache) : vga=40, FE high -> gain
67. */ #define TRF6151_REG_RX_RESET 0x9E00
static uint16_t g_reg_rx = TRF6151_REG_RX_RESET;
/* Port exact de trf6151_get_gain_reg() : gain frontend depuis
REG_RX. */ 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: /* valeurs intermediaires non utilisees par le firmware */ 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) { /*
Le trf6151 est le device RF frontend. En pratique dev 1 (dev 0 = TWL3025
* ABB, deja consomme ailleurs). On accepte 1..7 et on ne retient que les
* writes REG_RX (adresse 0) qui portent le gain. / static int
trf_dev = -1; if (trf_dev < 0) { const char e =
getenv(“CALYPSO_TRF_TSP_DEV”); trf_dev = (e && e) ? atoi(e)
: 1; } if (dev_idx != (uint8_t)trf_dev) { return; } if ((word &
TRF6151_REG_ADDR_MASK) != TRF6151_REG_RX) { return; / pas un write
REG_RX -> pas le registre de gain */ } g_reg_rx = (uint16_t)word;
}
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; /* baseband dBm vise */ int apm;
if (bb_dbm < 0) {
bb_dbm = 0; /* le baseband ne descend pas sous 0 dBm ici */
}
apm = bb_dbm * 64; /* a_pm = bb_dbm*64 (l1ddsp_meas_read: pm=a_pm>>3, bb=pm/8) */
if (apm > 0xFFFF) {
apm = 0xFFFF;
}
return (uint16_t)apm;
}
================================================================================
FILE: hw/arm/calypso/calypso_trx.c SIZE: 104924 bytes, 2127 lines
================================================================================
/ calypso_trx.c — Calypso hardware emulation + DSP C54x
emulation * No sockets. Firmware speaks UART only. DSP results in shared
RAM. * SPDX-License-Identifier: GPL-2.0-or-later / #include
“qemu/osdep.h” #include “hw/arm/calypso/calypso_arm2dsp.h” #include
“qapi/error.h” #include “qemu/timer.h” #include “qemu/error-report.h”
#include “qemu/main-loop.h” #include “sysemu/runstate.h” /
runstate_is_running() — gate DSP tick on ARM halt / #include
“exec/address-spaces.h” #include “hw/irq.h” #include
“hw/arm/calypso/calypso_trx.h” #include “hw/arm/calypso/calypso_uart.h”
#include “hw/arm/calypso/calypso_c54x.h” #include
“hw/arm/calypso/calypso_timer.h” / calypso_timer_lost_frame_tick()
/ #include “hw/arm/calypso/calypso_full_pcb.h” / api_ram_lock
pour MTTCG race fix */ #include “hw/arm/calypso/calypso_bsp.h” #include
“hw/arm/calypso/calypso_iota.h” #include
“hw/arm/calypso/calypso_twl3025.h” #include
“hw/arm/calypso/calypso_sim.h” #include “hw/arm/calypso/calypso_fbsb.h”
#include “chardev/char-fe.h” #include <sys/socket.h> #include
<netinet/in.h> #include <arpa/inet.h> #include
<fcntl.h> #include <unistd.h>
extern CalypsoUARTState g_uart_modem; extern CalypsoUARTState
g_uart_irda; /* calypso_dsp_shunt_record_rach() : prototype dans
calypso_dsp_shunt.h */
#include “hw/arm/calypso/calypso_debug.h” #define TRX_LOG(fmt,
…)
do { if (calypso_debug_enabled(“TRX”))
fprintf(stderr, “[calypso-trx]” fmt “”, ##VA_ARGS); }
while (0)
/* CALYPSO_TIMER=1 enables timer-side fprintf tracing (frame_irq,
tdma_tick, * kick). =0 (default) drops the calls entirely so the run is
silent and * stderr-pipe backpressure (TSLOG → python flush-per-line)
can’t throttle * the TCG main thread. Cached once via getenv. /
static bool calypso_timer_log(void) { static int on = -1; if (on < 0)
{ const char e = cdbg_env(“TIMER”); on = (e && (e ==
‘1’ || e == ‘y’)) ? 1 : 0; } return on; }
#define DSP_API_W_PAGE0 0x0000 #define DSP_API_W_PAGE1 0x0028 #define
DSP_API_NDB 0x01A8 #define DB_W_D_TASK_D 0 #define DB_W_D_BURST_D 1
#define DB_W_D_TASK_U 2 #define DB_W_D_BURST_U 3 #define DB_W_D_TASK_MD
4 #define DB_W_D_BACKGROUND 5 #define DB_W_D_DEBUG 6 #define
DB_W_D_TASK_RA 7 /* RACH access task — separate from d_task_u /
/ No PM/FB/SB stubs — the DSP handles everything via shared API RAM
*/
typedef struct CalypsoTRX { qemu_irq irqs; MemoryRegion
dsp_iomem; uint16_t dsp_ram[CALYPSO_DSP_SIZE / 2]; uint8_t dsp_page;
bool dsp_booted; uint32_t boot_frame; 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 *dsp_timer; uint32_t fn; bool
tdma_running;
/* C54x DSP emulator */
C54xState *dsp;
bool dsp_init_done; /* DSP reached first IDLE after boot */
/* CLK UDP: send each TDMA tick to bridge so it's clock-slave */
int clk_fd;
struct sockaddr_in clk_peer;
} CalypsoTRX;
static CalypsoTRX *g_trx;
#include “qemu/atomic.h” #include “calypso_dsp_shunt.h” #include
“calypso_layer1.h” /* CALYPSO_L1=c : HLE L1 scaffold (FB via corrélation
host) */
/* FBSB host-side orchestration. Reintroduced after preNoCell
refactor * (28 Apr) accidentally removed the wire. The bridge delivers
I/Q from * a fixed cos/sin LUT (no AFC DAC feedback in QEMU), so the DSP
* correlator cannot converge across iterations. This wire publishes *
synthetic clean FB/SB results at the NDB level when ARM dispatches *
FB_DSP_TASK, allowing the L1→L2→L3 stack to progress toward Location *
Update without requiring physical RF AFC simulation. / static
CalypsoFbsb g_fbsb; static bool g_fbsb_inited; / Définis dans
calypso_c54x.c — posés ici quand l’ARM écrit d_task_md=5, * lus par la
sonde D_TASK_MD-RD (test H1 timing/EA write-vs-read). */ extern uint32_t
g_arm_taskmd5_insn; extern uint16_t g_arm_taskmd5_ea;
/* All firmware patches removed — verified that the
layer1.highram.elf * runs unmodified against the current QEMU emulation
(PM scan, FBSB, * RESET cycle stable for >1 minute with NO patches
applied). History — patches removed and why each was actually
unnecessary: * cons_puts NOP (0x82a1b0) : function has a UART
fall-through path * taken when its LCD ctx flag is 0 (the * default).
printf_buffer is filled by * vsnprintf upstream and read by the *
fw_console poller in fw_console.c. * puts NOP (0x829ea0) : puts is a
one-instruction tail call to * sercomm_puts; it was never broken. * 5x
BL NOP in frame_irq : these are bl printf / bl puts calls * that became
safe once cons_puts/puts * were left alone. * talloc pool 32->148 :
pool exhaustion never observed in the * current run profile. * talloc
retry loop : same — never reached. * abort_irqs inf-loop fixup :
handle_abort never entered with the * IRQ controller fixes from earlier
* sessions. * sim_handler -> BX LR : l1a_l23_handler progresses
through SIM * polling without blocking under the * current SIM register
stub responses. If any of these regress, look first at the
underlying QEMU subsystem * (LCD MMIO, talloc memory pool, IRQ
controller, SIM stub) rather than * re-introducing a firmware patch.
*/
uint32_t calypso_trx_get_fn(void) { if (!g_trx) { return 0; } /*
RANK4 recale FN (gate CALYPSO_DL_FN_OFFSET, DEFAUT 0 = inerte ->
identique * au clean). Offset signe applique a la reference FN de tous
les consumers * (shunt feed, BSP match, FN-ALIGN) pour caler la FN DSP
sur la SCH BTS. A * -556, la FCCH tombe dans la bonne trame. NB : trop
large (touche aussi la * FN UL/DATA_IND) -> a n’activer que pour
l’alignement correlateur. / / @BEQUILLE — DL_FN_OFFSET (CALYPSO_DL_FN_OFFSET,
VALEUR, defaut 0 = inerte) * masque : l’absence de synchronisation de la
FN emulee sur la SCH du BTS. * L’offset signe est applique a la SOURCE
de FN, donc a tous les * consommateurs (BSP match, feed shunt, FN-ALIGN,
et aussi UL / * DATA_IND — porte trop large, assume dans le commentaire
d’origine). * retirer : quand la FN est calee sur la SCH recue (recalage
a la source), * l’offset mesure tombant a 0 dans FN-PROBE / FN-ALIGN.
/ static int off = 0, off_init = 0; if (!off_init) { off_init = 1;
const char e = getenv(“CALYPSO_DL_FN_OFFSET”); if (e && *e)
{ off = atoi(e); } } return (uint32_t)((int64_t)g_trx->fn + off);
}
/* —- DSP API RAM —- / / [2026-07-26 camp] Latch per-page du
d_burst_d COMMANDE par l’ARM (db_w), * pour l’echo per-burst reel
(CALYPSO_SHUNT_BURST_ECHO=2). Index = parite de * page : [0]=write off
0x0002 (wp p0), [1]=off 0x002A (wp p1). resp(b) (prio -4) * lit AVANT
cmd(b+2) (prio 0) dans la meme trame -> le latch tient le burst *
courant -> echo 0,1,2,3 exact, sans jitter, sans OFS. / uint32_t
shunt_l1s_fn(void); / decl (calypso_dsp_internal.h) / /
[2026-07-26 camp] FIFO des burst_id commandes par l’ARM
(db_w->d_burst_d) : * push sur write WP (calypso_dsp_write), pop sur
lecture d_task_d (1x/nb_resp), * reset au debut de bloc BCCH (gap
shunt_l1s_fn). Distingue burst 0 de burst 2 * (une latch/parite ne le
peut pas) + immunise le double-read (pop 1x/nb_resp, * valeur figee
s_burst_cur). Deterministe, sans OFS/FN/ECHO. */ static uint8_t
s_bd_ring[8]; static unsigned s_bd_w = 0, s_bd_r = 0; static uint16_t
s_burst_cur = 0; static uint32_t s_bd_last_wfn = 0xFFFFFFFF;
static uint64_t calypso_dsp_read(void opaque, hwaddr offset,
unsigned size) { CalypsoTRX s = opaque; if (offset >=
CALYPSO_DSP_SIZE) return 0; { /* [2026-07-28] FIND32 : voir en-tete du
patch. */ static int _f3 = -1; static unsigned _f3n = 0; static uint16_t
_f3v = 0x0020; if (_f3 < 0) { _f3 = getenv(“CALYPSO_FIND32”) ? 1 : 0;
const char v = getenv(“CALYPSO_FIND32_VAL”); if (v &&
v) _f3v = (uint16_t)strtol(v, NULL, 0); } if (_f3 && _f3n
< 40 && s->dsp_ram && (offset & 1) == 0) {
uint16_t _v = s->dsp_ram[offset / 2]; if (_v == _f3v) { _f3n++;
unsigned _dspw = 0x0800 + (unsigned)(offset / 2); fprintf(stderr,
“[calypso-trx] FIND32 off=0x%04x (mot DSP 0x%04x) = 0x%04x” “| NDB+%d
mots | fn=%u”, (unsigned)offset, _dspw, _v, (int)(((int)offset - 0x01A8)
/ 2), s->fn); } } } { /* [2026-07-28] ERRREAD : voir en-tete du
patch. */ static int _er = -1; static unsigned _ern = 0; if (_er < 0)
_er = getenv(“CALYPSO_ERRREAD”) ? 1 : 0; if (_er && offset >=
0x01A8 && offset <= 0x01AE && _ern < 40) { _ern++;
unsigned _w = (unsigned)(offset / 2); unsigned _dspw = 0x0800 + _w;
uint16_t _arm = s->dsp_ram ? s->dsp_ram[_w] : 0xDEAD; uint16_t
_dsp = (s->dsp && _dspw < C54X_DATA_SIZE) ?
s->dsp->data[_dspw] : 0xDEAD; fprintf(stderr, “[calypso-trx]
ERRREAD off=0x%04x (mot DSP 0x%04x, %s)” “vue_ARM=0x%04x vue_DSP=0x%04x
%s fn=%u”, (unsigned)offset, _dspw, offset == 0x01A8 ? “d_dsp_page” :
offset == 0x01AA ? “d_error_status” : “(voisin)”, _arm, _dsp, (_arm !=
_dsp) ? “<<<< LES DEUX VUES DIVERGENT” : “(coherent)”,
s->fn); } }
/* === Hypothesis #4 probe : ARM reads R_PAGE_X (= DSP responses) ===
* ARM lit a_pm via R_PAGE_X. R_PAGE_0 = 0x0050, R_PAGE_1 = 0x0078.
* Si firmware lit toujours R_PAGE_0 (jamais R_PAGE_1) → r_page jamais
* flipped → reading garbage from previous page après DSP write.
* Gated par CALYPSO_DEBUG=R_PAGE_SPLIT. */
if (calypso_debug_enabled("R_PAGE_SPLIT")) {
bool is_r0 = (offset >= 0x0050 && offset < 0x0078);
bool is_r1 = (offset >= 0x0078 && offset < 0x00A0);
if (is_r0 || is_r1) {
static unsigned r0_count = 0, r1_count = 0;
if (is_r0) r0_count++; else r1_count++;
if ((r0_count + r1_count) <= 30 || ((r0_count + r1_count) % 500) == 0) {
fprintf(stderr,
"[calypso-trx] R_PAGE_SPLIT r0=%u r1=%u (last off=0x%04x fn=%u)\n",
r0_count, r1_count, (unsigned)offset, s->fn);
}
}
}
/* === FIX 2026-05-15 : DSP→ARM mirror was missing ===
*
* Bug : `s->dsp_ram[]` et `s->dsp->data[]` sont deux arrays distincts.
* Le write path (calypso_dsp_write line 258) mirror ARM→DSP, mais le
* read path lisait seulement dsp_ram[] → toutes les écritures DSP étaient
* invisibles pour ARM. Verrouille tout le projet depuis ~6 mois :
* d_fb_det reste vu à 0 par firmware → FBSB_CONF=FAIL → mobile coincé.
*
* Fix : lire depuis dsp->data[] qui est la source de vérité (DSP writes
* via opcode + ARM writes mirrorés par calypso_dsp_write).
* Fallback sur dsp_ram[] si s->dsp pas encore alloué (pre-realize). */
/* Sous lock daram_lock pour la lecture cohérente vs DSP-thread writes.
* src est un pointeur DANS dsp->data[] ; on copie la valeur sous lock
* puis on relâche avant le reste de la logique pour minimiser la
* section critique. */
uint64_t val;
if (s->dsp && s->dsp->data) {
calypso_pcb_daram_lock_acquire();
uint16_t *src = &s->dsp->data[offset/2 + 0x0800];
val = (size == 2) ? src[0] :
(size == 4) ? ((uint32_t)src[0] | ((uint32_t)src[1] << 16)) :
((uint8_t *)src)[offset & 1];
calypso_pcb_daram_lock_release();
} else {
uint16_t *src = &s->dsp_ram[offset/2];
val = (size == 2) ? src[0] :
(size == 4) ? ((uint32_t)src[0] | ((uint32_t)src[1] << 16)) :
((uint8_t *)src)[offset & 1];
}
/* CALYPSO_FORCE_TOA=<N> (env gated, rigolo) : force une détection FB
* complète vue par l'ARM, sans toucher le DSP. osmocom prim_fbsb.c
* n'atteint read_fb_result (lecture TOA dans ndb->a_sync_demod[D_TOA]
* @0x01F4) QU'APRÈS que d_fb_det (@0x01F0) = "FOUND". Donc forcer le TOA
* seul ne suffit pas : on force tout le bloc résultat FB sur le read ARM.
* 0x01F0 d_fb_det = 1 (FOUND) 0x01F4 a_sync_TOA = N (23 = on-time)
* 0x01F8 a_sync_ANGLE = 0 (AFC ne diverge pas) 0x01FA a_sync_SNR = haut */
/* Étendu 2026-06-02 : FORCE_TOA force le bloc FB (a_sync_demod @0x01F0-FA,
* NDB) ET le bloc SB (a_serv_demod[D_TOA], db_r). Sinon le SB lit du garbage
* → l1s_sbdet_resp calcule "SB N bits in the future?!?" → sync rejeté →
* BSIC=0, pas de sysinfo. Forcer a_serv_demod[D_TOA]=force_toa (=23) fait
* `toa-=23 → 0` → passe le check `toa > bits_delta`. db_r page0=0xFFD00050
* (off 0x50) / page1=0xFFD00078 (off 0x78), struct DSP33-36 a_serv_demod
* @word8 → D_TOA = off 0x60 (p0) / 0x88 (p1). */
/* [2026-07-22] Injection READ-SIDE REAL_FB/SB : PRECEDE (et court-circuite)
* le FORCE_TOA canned. Livre la derniere detection FCCH reelle (g_shunt.rx_*)
* sur le read MMIO ARM -> immunise d_fb_det/a_sync_demod/SB-TOA contre
* l'ordonnancement intra-trame. Gate CALYPSO_SHUNT_REAL_FB. */
bool real_fb_hit = false;
if (size == 2) {
uint16_t rv;
if (calypso_dsp_shunt_real_fb_read((uint32_t)offset, &rv)) {
val = rv;
real_fb_hit = true;
}
}
/* [2026-07-26 camp] db_r->d_task_d (read page 0 @off 0x50 / page 1 @off 0x78,
* word 0) : le DSP clear la commande NB -> l1s_nb_resp lit 0 -> puts("EMPTY")
* et bail avant a_cd. Sous SHUNT_LEGIT + si_valid (a_cd rempli), si le firmware
* lit d_task_d=0, retourner ALLC_DSP_TASK(24) -> il continue vers a_cd. d_burst_d
* (off 0x52/0x7A) reste la valeur du firmware (db_r==db_w) -> match burst_id. */
if (size == 2 && (offset == 0x0050 || offset == 0x0078)) {
static int _cl = -1;
if (_cl < 0) { const char *l = getenv("CALYPSO_SHUNT_LEGIT"); const char *nl = getenv("CALYPSO_SHUNT_NO_LEGIT"); _cl = ((l && *l=='1') || (nl && *nl=='1')) ? 1 : 0; }
if (_cl && calypso_dsp_shunt_si_valid()) {
/* d_task_d lu 1x/nb_resp (prim_rx_nb.c:77) -> POP le prochain burst_id
* du FIFO. Stable pour les 1-2 lectures d_burst_d du meme nb_resp. */
s_burst_cur = s_bd_ring[s_bd_r++ & 7u];
if (val == 0) val = 24; /* ALLC_DSP_TASK : evite EMPTY */
}
}
/* [2026-07-26 camp] db_r->d_burst_d (read page @off 0x52 / 0x7A) : le pipeline
* nb_cmd/nb_resp decale le burst_id commande vs demodule -> "BURST ID x!=y" et
* le firmware n'atteint jamais burst 3 (ou a_cd est lu). On retourne 3 :
* nb_resp(3) matche (3==3) et lit a_cd/SI ; nb_resp(0/1/2) bail (mesures
* non-critiques). Gate SHUNT_LEGIT + si_valid. */
if (size == 2 && (offset == 0x0052 || offset == 0x007A)) {
static int _cb = -1;
if (_cb < 0) { const char *l = getenv("CALYPSO_SHUNT_LEGIT"); const char *nl = getenv("CALYPSO_SHUNT_NO_LEGIT"); _cb = ((l && *l=='1') || (nl && *nl=='1')) ? 1 : 0; }
if (_cb && calypso_dsp_shunt_si_valid()) {
/* SOURCE UNIQUE : miroir per-page du burst_id commande par l'ARM.
* db_w->d_burst_d est latche par parite dans calypso_dsp_write :
* write 0x0002 -> s_wp_burst_d[0] (read-page 0, off 0x0052)
* write 0x002A -> s_wp_burst_d[1] (read-page 1, off 0x007A)
* resp(b) lit RP(b&1) AVANT que cmd(b+2) (prio 0) ne reecrive la meme
* parite -> valeur = burst b, FIGEE sur le double-read (line 83+113).
* Deterministe, sans s->fn, sans OFS, sans compteur. */
/* r_page = !(burst&1) (verifie runtime : resp(b) lit la read-page de
* PARITE OPPOSEE au burst) -> lire le latch de parite inverse a l'offset :
* read 0x0052 (RP0) -> latch[1] ; read 0x007A (RP1) -> latch[0]. */
/* -1 : le reset FIFO se cale sur le 1er cmd du bloc (souvent burst 1,
* burst 0=valeur 0), d'ou un offset de phase constant +1 -> on corrige. */
val = (uint16_t)((s_burst_cur + 3) & 3); /* FIFO -1 (phase) */
}
}
if (!real_fb_hit && size == 2) {
/* @BEQUILLE — FORCE_TOA (CALYPSO_FORCE_TOA, VALEUR, defaut -1/OFF)
* masque : tout le bloc resultat FB/SB (d_fb_det, a_sync_demod TOA/ANGLE/SNR,
* a_serv_demod[D_TOA]) : oracle canne cote read MMIO ARM.
* retirer : quand d_fb_det natif est ecrit par le DSP.
* PIEGE : "0" ACTIVE le gate (TOA force a 0) ; seul unset/absent coupe.
* NB : deja court-circuitee par SHUNT_REAL_FB/DECAN (garde !real_fb_hit).
*/
static int force_toa = -2; /* -2 = uninit, -1 = off */
if (force_toa == -2) {
const char *e = getenv("CALYPSO_FORCE_TOA");
force_toa = (e && *e) ? (int)strtol(e, NULL, 0) : -1;
if (force_toa >= 0)
fprintf(stderr, "[calypso-trx] CALYPSO_FORCE_TOA=%d (FB a_sync_demod + SB a_serv_demod[D_TOA] forcés)\n", force_toa);
}
if (force_toa >= 0) {
switch (offset) {
/* --- bloc FB (a_sync_demod, NDB @0x01F0) --- */
case 0x01F0: val = 1; break; /* d_fb_det = FOUND */
case 0x01F4: val = (uint16_t)force_toa; break; /* a_sync_TOA */
case 0x01F8: val = 0; break; /* a_sync_ANGLE = 0 */
case 0x01FA: val = 0x7000; break; /* a_sync_SNR high */
/* --- bloc SB (a_serv_demod[D_TOA], db_r page 0 et 1) --- */
case 0x0060: case 0x0088:
val = (uint16_t)force_toa; break; /* SB TOA → 23 : passe le check "future" */
default: break; /* 0x01F2/0x01F6 + reste inchangés */
}
}
}
/* CALYPSO_FORCE_NB=1 (gate NB demod, 2026-06-02) : l1s_nb_resp bail "EMPTY"
* si db_r->d_task_d==0 (le DSP NB demod ne tourne pas) → jamais de DATA_IND
* BCCH → pas de SI. Force d_task_d≠0 (word 0 du db_r : page0 off 0x50 /
* page1 off 0x78) pour passer "EMPTY" → le firmware émet le DATA_IND (que
* CALYPSO_FORCE_AGCH remplit ensuite avec un SI/IMM-ASS). Révèle ensuite le
* check d_burst_d (offset 0x52/0x7A). */
if (size == 2 && (offset == 0x0050 || offset == 0x0078 ||
offset == 0x0052 || offset == 0x007A)) {
/* @BEQUILLE — FORCE_NB (CALYPSO_FORCE_NB, EQ1, defaut OFF)
* masque : la publication DSP de db_r->d_task_d / d_burst_d. On falsifie le
* read ARM (d_task_d 0->1, d_burst_d recopie de db_w) pour passer
* le bail "EMPTY" de l1s_nb_resp.
* retirer : quand le DSP NB demod ecrit lui-meme la read-page.
* NB : les memes offsets sont deja traites plus haut par le bloc
* SHUNT_LEGIT/NO_LEGIT + si_valid — conflit potentiel.
*/
static int force_nb = -1;
if (force_nb < 0) {
const char *e = getenv("CALYPSO_FORCE_NB");
force_nb = (e && *e == '1') ? 1 : 0;
}
if (force_nb) {
if ((offset == 0x0050 || offset == 0x0078) && val == 0) {
val = 1; /* d_task_d → non-zéro : passe le "EMPTY" */
} else if (offset == 0x0052 || offset == 0x007A) {
/* d_burst_d ← db_w->d_burst_d (= le burst_id que le firmware a
* commandé via dsp_load_rx_task) → passe le check
* d_burst_d != burst_id. db_w d_burst_d : p0 DSP word 0x801,
* p1 0x815 (db_w p0=0xFFD00000 off0x02, p1=0xFFD00028 off0x2A). */
if (s->dsp && s->dsp->data)
val = s->dsp->data[(offset == 0x0052) ? 0x801 : 0x815];
else
val = s->dsp_ram[(offset == 0x0052) ? 0x01 : 0x15];
}
}
}
/* DSP boot handshake: firmware polls DL_STATUS until it reads BOOT */
if (offset == DSP_DL_STATUS_ADDR && !s->dsp_booted) {
if (++s->boot_frame > 3) {
s->dsp_ram[DSP_DL_STATUS_ADDR/2] = DSP_DL_STATUS_BOOT;
s->dsp_ram[DSP_API_VER_ADDR/2] = DSP_API_VERSION;
s->dsp_ram[DSP_API_VER2_ADDR/2] = 0;
s->dsp_booted = true;
TRX_LOG("DSP boot ver=0x%04x", DSP_API_VERSION);
val = DSP_DL_STATUS_BOOT;
}
}
/* ARM-read trace on d_fb_det / d_fb_mode / a_sync_demod cells:
* 0x01F0 = d_fb_det (DSP word 0x08F8)
* 0x01F2 = d_fb_mode (DSP word 0x08F9)
* 0x01F4..0x01FA = a_sync_demod[0..3] (TOA/PM/ANGLE/SNR)
* Capped + thinned. Goal: confirm whether ARM polls these cells and
* what value it sees vs what DSP wrote. If ARM never reads while DSP
* writes 0x095b → ARM-side mapping/timing bug. */
if (offset >= 0x01F0 && offset <= 0x01FE && (offset & 1) == 0) {
static unsigned arm_rd_log = 0;
static unsigned arm_rd_mode = 0;
arm_rd_log++;
bool is_mode = (offset == 0x01F2);
if (is_mode) arm_rd_mode++;
/* d_fb_mode: log EVERY read (no cap) — race-window check.
* Other cells: thinned. */
bool log_it = is_mode ||
(arm_rd_log <= 200 || (arm_rd_log % 5000) == 0) ||
(val != 0 && offset == 0x01F0);
if (log_it) {
const char *name =
(offset == 0x01F0) ? "d_fb_det" :
(offset == 0x01F2) ? "d_fb_mode" :
(offset == 0x01F4) ? "a_sync_TOA" :
(offset == 0x01F6) ? "a_sync_PM" :
(offset == 0x01F8) ? "a_sync_ANG" :
(offset == 0x01FA) ? "a_sync_SNR" : "unk";
TRX_LOG("ARM RD %s [arm=0x%04x dsp_word=0x%04x] = 0x%04x sz=%d fn=%u #%u",
name, (unsigned)offset, (unsigned)(offset/2 + 0x0800),
(unsigned)val, size, s->fn, arm_rd_log);
}
}
/* ARM-read trace on a_cd[0..14] : CCCH demod result buffer (15 words).
* DSP words 0x09D0..0x09DE → ARM bytes 0x03A0..0x03BD.
* Goal : confirmer si ARM L1 prim_rx_nb consomme effectivement a_cd[]
* quand task=24 (ALLC) fire et A_CD-WR remplit le buffer. Si compteur=0
* mais A_CD-WR>0, le mur DATA_IND est avant la lecture (firmware ne
* s'arme pas sur l'event CCCH). Si compteur>0 mais DATA_IND=0, le
* mur est downstream (check db_r->d_burst_d ou autre dans
* prim_rx_nb.c::l1s_nb_resp). */
if (offset >= 0x03A0 && offset <= 0x03BD && (offset & 1) == 0) {
static unsigned arm_rd_a_cd = 0;
arm_rd_a_cd++;
if (arm_rd_a_cd <= 200 || (arm_rd_a_cd % 1000) == 0) {
unsigned word_idx = (unsigned)((offset - 0x03A0) / 2);
TRX_LOG("ARM RD a_cd[%u] [arm=0x%04x dsp_word=0x%04x] = 0x%04x sz=%d fn=%u #%u",
word_idx, (unsigned)offset, (unsigned)(offset/2 + 0x0800),
(unsigned)val, size, s->fn, arm_rd_a_cd);
}
}
return val;
}
/* === Sideband RACH (NO-HARDCODE)
============================================ * Le firmware ecrit la
VRAIE RACH dans d_rach (mot NDB 0x023A = byte 0x0474) : * value =
(ra<<8) | (bsic<<2). On la publie au device (qemu_wrap
ul_drain) via * un fichier REGULIER /dev/shm/calypso_rach (PAS un FIFO
-> pas de blocage). * Layout fige (16 octets), partage avec
qemu_wrap.c. Single-writer/single-reader, * pwrite atomique 16o, seq
ecrite en dernier. Remplace le RA=3 hardcode du device. / static
void calypso_rach_publish(uint8_t ra, uint8_t bsic, uint32_t fn) {
static int fd = -2; if (fd == -2) { fd = open(“/dev/shm/calypso_rach”,
O_CREAT | O_RDWR, 0644); if (fd >= 0 && ftruncate(fd, 16)
< 0) { / best-effort / } } if (fd < 0) return; static
uint32_t seq = 0; seq++; uint8_t buf[16] = {0}; buf[4] = ra; buf[5] =
bsic; memcpy(buf + 8, &fn, sizeof(fn)); memcpy(buf + 0, &seq,
sizeof(seq)); / seq en premier mais ecrit atomiquement / if
(pwrite(fd, buf, sizeof(buf), 0) < 0) { / best-effort */ } }
static void calypso_dsp_write(void opaque, hwaddr offset,
uint64_t value, unsigned size) { CalypsoTRX s = opaque; if (offset
>= CALYPSO_DSP_SIZE) return; { /* [2026-07-28] BOOTCMD cote ARM :
commande bootloader DSP (voir en-tete). */ static int _bc = -1; static
unsigned _bcn = 0; if (_bc < 0) _bc = getenv(“CALYPSO_BOOTCMD”) ? 1 :
0; if (_bc && offset >= 0x0FF8 && offset <= 0x0FFF
&& _bcn < 40) { _bcn++; const char *_nm = (offset == 0x0FFE)
? “BL_CMD_STATUS (2/4=download)” : (offset == 0x0FFC) ? “BL_ADDR_LO” :
(offset == 0x0FFA) ? “BL_SIZE” : (offset == 0x0FF8) ? “BL_ADDR_HI” :
“(autre)”; fprintf(stderr, “[calypso-trx] BOOTCMD ARM off=0x%04x %s
<- 0x%04x fn=%u”, (unsigned)offset, _nm, (unsigned)value, s->fn);
} } { /* [2026-07-28] FBDET-API (b) cote ARM : ecriture MMIO de d_fb_det
* (mot DSP 0x08F8 -> offset 0x01F0) et du bloc a_sync_demod. */
static int _fb = -1; static unsigned _fbn = 0; if (_fb < 0) _fb =
getenv(“CALYPSO_FBDET_API”) ? 1 : 0; if (_fb && offset >=
0x01F0 && offset <= 0x01FB && _fbn < 40) { _fbn++;
fprintf(stderr, “[calypso-trx] FBDET-API ARM off=0x%04x (mot 0x%04x,
%s)” ” <- 0x%04x size=%u fn=%u“, (unsigned)offset, (unsigned)(0x0800
+ offset / 2), offset == 0x01F0 ?”d_fb_det” : “a_sync_demod”,
(unsigned)value, size, s->fn); } } /* [2026-07-22] de-alias burst-ID
: mirror d_burst_d par commande /
calypso_dsp_shunt_wp_burst_write((uint32_t)offset, (uint16_t)value);
/ [2026-07-26 camp] PUSH FIFO du burst_id commande
(db_w->d_burst_d, word1 : * page0 @0x0002 / page1 @0x002A). Reset au debut d’un bloc BCCH : les
cmd0..3 * sont a frames L1 CONSECUTIVES (shunt_l1s_fn +1) ; gros trou
avant cmd0 du * bloc suivant -> fn != last+1 => reset FIFO ->
alignement 0,1,2,3 sans OFS. / if (size == 2 &&
((uint32_t)offset == 0x0002 || (uint32_t)offset == 0x002A)) { if
(calypso_dsp_shunt_si_valid()) { uint32_t wfn = shunt_l1s_fn(); if (wfn
!= s_bd_last_wfn + 1) { s_bd_w = 0; s_bd_r = 0; } / nouveau bloc */
s_bd_last_wfn = wfn; s_bd_ring[s_bd_w++ & 7u] = (uint8_t)(value
& 3); } }
/* [2026-07-22] WR-RAW (ungated, cap 60) : voir TOUS les writes ARM qui
* passent par ce hook -> l'ARM commande-t-il le DSP ici, ou tout bypasse ? */
{
/* [2026-07-22] cible les writes OPERATIONNELS (fn>100, hors zeroisage boot) :
* write-page (task_d/md 0x00-0x50) + NDB (d_dsp_page 0x1A8+). Voit-on
* l'ARM commander le DSP (task + B_GSM_TASK) ? */
static unsigned wraw = 0;
/* CIBLE demod-command uniquement (task_d p0=0x00/p1=0x28, task_md p0=0x08/p1=0x30,
* d_dsp_page 0x1A8), val!=0, TOUTES trames -> l'ARM commande-t-il jamais, et
* SEED5AC8 change-t-il ca ? Skip AFC/ABB (bruit). */
/* Elargi : toute la plage NDB (0x01A8-0x0210) non-nulle -> trouver le VRAI
* offset de d_dsp_page (valeur 0x0002/0x0003 = B_GSM_TASK|w_page). */
if (value != 0 && ((offset >= 0x01A8 && offset < 0x0210) ||
offset==0x0000||offset==0x0008||offset==0x0028||offset==0x0030)
&& wraw < 80) {
wraw++;
const char *z = (value==0x0002||value==0x0003) ? " <== B_GSM_TASK! (d_dsp_page?)" :
(offset == 0x01A8) ? " <== NDB+0" :
(offset == 0x0000 || offset == 0x0028) ? " <== task_d" :
" <== task_md";
fprintf(stderr, "[calypso-trx] WR-OP off=0x%04x val=0x%04x size=%u fn=%u%s\n",
(unsigned)offset, (unsigned)value, size, s->fn, z);
}
}
/* === Unconditional probe : count ALL writes by offset range ===
* Gated par CALYPSO_DEBUG=DSP_WRITE_COUNT. Bucket par 0x40-byte zone
* pour voir si ARM hit les bonnes zones (page 0 task 0x00-0x1F, page 1
* task 0x28-0x47, NDB 0x1A8+). Si compteurs = 0 dans la PM zone alors
* que pm_resp fire → write path ne passe PAS par ce hook. */
if (calypso_debug_enabled("DSP_WRITE_COUNT")) {
static uint64_t c_p0 = 0, c_p1 = 0, c_ndb = 0, c_other = 0;
if (offset < 0x0028) c_p0++;
else if (offset < 0x0050) c_p1++;
else if (offset >= 0x01A8 && offset < 0x0800) c_ndb++;
else c_other++;
uint64_t tot = c_p0 + c_p1 + c_ndb + c_other;
if (tot <= 30 || (tot % 1000) == 0) {
fprintf(stderr,
"[calypso-trx] DSP_WRITE_COUNT p0=%llu p1=%llu ndb=%llu other=%llu "
"(last off=0x%04x val=0x%llx sz=%u fn=%u)\n",
(unsigned long long)c_p0, (unsigned long long)c_p1,
(unsigned long long)c_ndb, (unsigned long long)c_other,
(unsigned)offset, (unsigned long long)value, size, s->fn);
}
}
if (size == 2) s->dsp_ram[offset/2] = value;
else if (size == 4) { s->dsp_ram[offset/2] = value; s->dsp_ram[offset/2+1] = value >> 16; }
else ((uint8_t *)s->dsp_ram)[offset] = value;
/* Mirror to DSP s->data[] so prog_fetch in OVLY mode sees ARM writes
* to the shared API/DARAM region. On real silicon dsp_ram and the DSP
* DARAM share one physical memory; without this mirror, ARM writes
* land in dsp_ram only and the DSP executes the stale (boot-time
* MVPD-copied) value via prog_fetch. */
if (s->dsp) {
uint16_t dsp_word = offset/2 + 0x0800;
/* [2026-07-23] ARM-WRITE-0810 (READ-ONLY) : trace le mirror ARM->DSP
* specifiquement pour d_ctrl_system (dsp_word 0x0810, ARM offset 0x20).
* La sonde precedente (WATCH-0810-WR, cote C54x data_write) etait AVEUGLE
* aux writes ARM (ce mirror ecrit s->dsp->data[] DIRECTEMENT, sans passer
* par data_write()). Cette sonde-ci confirme si le mirror ARM marche
* reellement pour ce mot precis. */
if (dsp_word == 0x0810) {
static unsigned aw810_zero = 0, aw810_nz = 0;
/* val!=0 (surtout bit15=0x8000, B_TASK_ABORT) = JAMAIS cappe : c'est
* l'ecriture qu'on cherche (l1s_abort_cmd, deferee via tdma_schedule,
* peut arriver bien apres le burst de zeros du boot memset). */
if (value != 0)
fprintf(stderr, "[calypso-trx] ARM-WRITE-0810 *** NONZERO *** #%u offset=0x%04x "
"val=0x%04x size=%u dsp_word=0x0810 (avant: data[0x0810]=0x%04x) fn=%u "
"dsp_insn=%u\n",
++aw810_nz, (unsigned)offset, (unsigned)value, size,
s->dsp->data[dsp_word], s->fn,
s->dsp ? s->dsp->insn_count : 0);
else if (aw810_zero++ < 100000)
fprintf(stderr, "[calypso-trx] ARM-WRITE-0810 zero #%u offset=0x%04x val=0x%04x "
"size=%u fn=%u dsp_insn=%u\n", aw810_zero, (unsigned)offset, (unsigned)value,
size, s->fn, s->dsp ? s->dsp->insn_count : 0);
}
calypso_pcb_daram_lock_acquire();
if (size == 2) {
s->dsp->data[dsp_word] = (uint16_t)value;
} else if (size == 4) {
s->dsp->data[dsp_word] = (uint16_t)value;
s->dsp->data[dsp_word + 1] = (uint16_t)(value >> 16);
}
calypso_pcb_daram_lock_release();
/* size==1 byte: skip — sub-word writes to DSP data are unusual
* and would need careful endianness handling; falls back to the
* dsp_ram-only path which is fine for the sub-word case. */
}
/* Debug: log task-related writes to write pages (d_task_d/u/md/ra) */
if ((offset == 0x0000 || offset == 0x0004 || offset == 0x0008 ||
offset == 0x000E || offset == 0x0028 || offset == 0x002C ||
offset == 0x0030 || offset == 0x0036) && value != 0) {
static int wp_log = 0;
if (++wp_log <= 100)
TRX_LOG("DSP WR [0x%04x] = 0x%04x (sz=%d) fn=%u",
(unsigned)offset, (unsigned)value, size, s->fn);
}
/* === d_task_md probe — fires SANS filter value=0 ===
* Si d_task_md write = 0 (= memset only), pm_cmd jamais appelé.
* Si d_task_md write = 1 (= pm_cmd writes), notre probe ARM TASK WR
* devrait fire — mais on voit count=0 → contradiction à investiguer.
* Gated par CALYPSO_DEBUG=D_TASK_MD_ALL. */
if ((offset == 0x0008 || offset == 0x0030) && size == 2) {
if (calypso_debug_enabled("D_TASK_MD_ALL")) {
static unsigned dtm_log = 0;
if (dtm_log < 30 || (dtm_log % 100) == 0) {
fprintf(stderr,
"[calypso-trx] D_TASK_MD_ALL #%u off=0x%04x val=0x%04x fn=%u\n",
dtm_log, (unsigned)offset, (unsigned)value, s->fn);
dtm_log++;
}
}
}
/* === Hypothesis #1 probe : d_dsp_page WR (NDB+0 = ARM 0x01A8) ===
* Écrit par dsp_end_scenario(): `ndb->d_dsp_page = B_GSM_TASK | w_page`.
* Si jamais hit → dsp_end_scenario jamais fired → w_page stuck à 0.
* Gated par CALYPSO_DEBUG=D_DSP_PAGE. */
if (offset == 0x01A8) { /* [2026-07-22] ungated any-size : l'ARM ecrit-il d_dsp_page ? */
static unsigned ddp_any = 0;
if (ddp_any++ < 30)
fprintf(stderr, "[calypso-trx] DDP-ANY WR val=0x%04x size=%u (B_GSM_TASK=%d) fn=%u insn-arm\n",
(unsigned)value, size, !!(value & 2), s->fn);
}
if (offset == 0x01A8 && size == 2) {
if (calypso_debug_enabled("D_DSP_PAGE")) {
static unsigned ddp_log = 0;
if (ddp_log < 50) {
fprintf(stderr,
"[calypso-trx] D_DSP_PAGE WR #%u val=0x%04x (B_GSM_TASK=%d w_page=%d) fn=%u\n",
ddp_log, (unsigned)value,
!!(value & 2), !!(value & 1), /* B_GSM_TASK=(1<<1)=0x02, w_page=bit 0 */
s->fn);
ddp_log++;
}
}
}
/* === Hypothesis #2 probe : ARM WR per-page split (= cur_bucket advance) ===
* Si bucket n'avance pas, tous les ARM TASK WR continuent à page 0.
* Compteur séparé page 0 vs page 1 sur task_d/task_md à chaque frame. */
if (calypso_debug_enabled("PAGE_SPLIT")) {
bool is_p0 = (offset == 0x0000 || offset == 0x0008 || offset == 0x000E ||
offset == 0x000A);
bool is_p1 = (offset == 0x0028 || offset == 0x0030 || offset == 0x0036 ||
offset == 0x0032);
if ((is_p0 || is_p1) && value != 0 && size == 2) {
static unsigned p0_count = 0, p1_count = 0;
if (is_p0) p0_count++; else p1_count++;
if ((p0_count + p1_count) <= 30 || ((p0_count + p1_count) % 50) == 0) {
fprintf(stderr,
"[calypso-trx] PAGE_SPLIT p0=%u p1=%u (last off=0x%04x val=%u fn=%u)\n",
p0_count, p1_count, (unsigned)offset, (unsigned)value, s->fn);
}
}
}
/* AFC hook : firmware afc_load_dsp() écrit dsp_api.db_w->d_afc.
* Word 15 du WP : page0 = byte 0x001E, page1 = byte 0x0046.
* Propage le DAC value vers TWL3025 → rotation samples BSP.
* Chaîne complete : firmware → ce hook → twl3025 → BSP rotation. */
if ((offset == 0x001E || offset == 0x0046) && size == 2) {
int16_t dac_value = (int16_t)(uint16_t)value;
calypso_twl3025_set_afc_dac(dac_value);
static int afc_log = 0;
if (++afc_log <= 50)
TRX_LOG("AFC WR page=%d dac=%d hz=%.1f fn=%u",
(offset == 0x001E) ? 0 : 1, dac_value,
calypso_twl3025_get_afc_hz(), s->fn);
}
/* d_rach offset finder — circular buffer of recent NDB writes.
* NDB starts at byte offset 0x01A8 in API RAM (= dsp_ram + 0x01A8).
* We capture every non-zero ARM-side write to NDB range and dump the
* last 16 entries when d_task_ra commits (0x000E page0 or 0x0036 page1).
* The d_rach value matches the pattern (ra<<8) | (bsic<<2) — the ra
* byte mirrors what the mobile L3 just announced in `RANDOM ACCESS`.
* Once observed, set CALYPSO_NDB_D_RACH_OFFSET to the matching word
* index (= (offset - 0x01A8) / 2 + 0xD4 in the convention used by
* calypso_bsp.c). */
{
#define D_RACH_RING_SIZE 128
struct ndb_wr_entry { hwaddr off; uint32_t val; uint32_t fn; uint32_t insn; uint8_t sz; };
static struct ndb_wr_entry ring[D_RACH_RING_SIZE];
static int idx;
static int dump_count;
/* Capture all sizes (1/2/4) over the full NDB + post-NDB region
* (NDB extent varies by DSP firmware version; widen to 0x0800 to
* be safe, restrict later once the actual d_rach offset is pinned).
* Filter only zero-value writes to keep the ring useful. */
if (offset >= 0x01A8 && offset < 0x0800 && value != 0 &&
(size == 1 || size == 2 || size == 4)) {
ring[idx % D_RACH_RING_SIZE] = (struct ndb_wr_entry){
offset, (uint32_t)value, s->fn, s->dsp ? s->dsp->insn_count : 0,
(uint8_t)size
};
idx++;
}
bool task_ra_commit =
(offset == DSP_API_W_PAGE0 + DB_W_D_TASK_RA * 2 ||
offset == DSP_API_W_PAGE1 + DB_W_D_TASK_RA * 2) && value != 0;
if (task_ra_commit && dump_count < 30) {
dump_count++;
uint32_t commit_insn = s->dsp ? s->dsp->insn_count : 0;
TRX_LOG("D_RACH-FINDER task_ra commit @0x%04x = 0x%04x fn=%u insn=%u — full ring (last 128 NDB writes):",
(unsigned)offset, (unsigned)value, s->fn, commit_insn);
int n = (idx < D_RACH_RING_SIZE) ? idx : D_RACH_RING_SIZE;
int start = idx - n;
for (int i = 0; i < n; i++) {
int k = (start + i) % D_RACH_RING_SIZE;
uint32_t v = ring[k].val;
int32_t d_insn = (int32_t)(commit_insn - ring[k].insn);
uint8_t ra = (uint8_t)((v >> 8) & 0xFF);
uint8_t low = (uint8_t)(v & 0xFF);
uint8_t bsic = low >> 2;
/* Mark entries within the "RACH window" (last 1000 insn
* before commit) — those are the candidates worth scanning
* by eye for ra match against mobile L3 log. Older entries
* are init/unrelated but kept in the dump for offline
* correlation when filtering misses the d_rach write. */
const char *tag = (d_insn >= 0 && d_insn <= 1000) ? "*HOT*" : "";
fprintf(stderr,
"[trx] D_RACH-FINDER #%d off=0x%04x val=0x%04x sz=%u "
"d_insn=%+d ra=0x%02x bsic=0x%02x fn=%u %s\n",
i, (unsigned)ring[k].off, v, ring[k].sz,
-d_insn, ra, bsic, ring[k].fn, tag);
}
}
}
/* NO-HARDCODE : publie la VRAIE RA+FN au mot d_rach (byte = word*2). Tire a
* CHAQUE ecriture d_rach par le firmware -> fiable, independant de la voie
* d_task_ra/page (qui rate cote shunt LATCH). value = (ra<<8)|(bsic<<2). */
{
static uint32_t dr_byte = 0;
if (!dr_byte) {
const char *e = getenv("CALYPSO_NDB_D_RACH_OFFSET");
uint32_t w = (e && *e) ? (uint32_t)strtoul(e, NULL, 0) : 0x023A;
dr_byte = w * 2; /* 0x023A word -> 0x0474 ARM byte */
}
if (offset == dr_byte && value != 0 && (size == 2 || size == 4)) {
uint8_t ra = (uint8_t)((value >> 8) & 0xFF);
calypso_rach_publish(ra, (uint8_t)((value & 0xFF) >> 2), s->fn);
calypso_dsp_shunt_record_rach(ra); /* SONDE B : l1s.current_time.fn par RA */
/* [2026-07-26 PORT LU] SHUNT_LEGIT avale d_task_ra -> le poll UL natif
* ne tire jamais (RACH encode #0). On emet l'access-burst depuis le
* signal FIABLE = l'ecriture d_rach. 1 write = 1 burst (pas de sticky). */
{
/* @BEQUILLE — UL_RACH_FROM_DRACH (CALYPSO_UL_RACH_FROM_DRACH ; si absente,
* retombe sur CALYPSO_SHUNT_LEGIT ; shunt_no_legit.env:=1)
* masque : le poll UL natif, qui ne tire jamais l'access-burst parce que
* SHUNT_LEGIT avale d_task_ra. On emet le burst depuis l'ecriture
* ARM de d_rach (1 write = 1 burst, sans sticky).
* retirer : quand d_task_ra atteint le producteur UL sans etre consomme par
* le shunt.
* IDIOME : "if (e) ulr = (*e=='1'); else ulr = SHUNT_LEGIT" -> poser =0 la
* coupe MEME sous SHUNT_LEGIT=1, contrairement aux INJECT_*.
*/
static int ulr = -1;
if (ulr < 0) {
const char *e = getenv("CALYPSO_UL_RACH_FROM_DRACH");
if (e) ulr = (*e == '1');
else { const char *l = getenv("CALYPSO_SHUNT_LEGIT"); ulr = (l && *l == '1'); }
}
if (ulr) calypso_bsp_send_rach_ra(ra, (uint8_t)((value & 0xFF) >> 2), s->fn, 0);
}
}
}
/* DSP bootloader mailbox writes (osmocom-bb dsp.c BL_*).
* ARM byte → DSP word mapping (api_ram[w] ↔ ARM byte w*2):
* ARM 0x0FF8 BL_ADDR_HI ↔ DSP word 0x0FFC
* ARM 0x0FFA BL_SIZE ↔ DSP word 0x0FFD
* ARM 0x0FFC BL_ADDR_LO ↔ DSP word 0x0FFE (BACC target)
* ARM 0x0FFE BL_CMD_STATUS ↔ DSP word 0x0FFF (poll value)
* Trace every write so we can confirm the handshake actually reaches
* the cells the bootloader at PROM0 0xb41c-0xb430 reads. */
if (offset == 0x0FF8 || offset == 0x0FFA ||
offset == 0x0FFC || offset == 0x0FFE) {
const char *name = (offset == 0x0FF8) ? "BL_ADDR_HI" :
(offset == 0x0FFA) ? "BL_SIZE" :
(offset == 0x0FFC) ? "BL_ADDR_LO" :
"BL_CMD_STATUS";
static unsigned bl_log;
if (++bl_log <= 200)
TRX_LOG("BL ARM WR %s [arm=0x%04x dsp_word=0x%04x] = 0x%04x sz=%d fn=%u",
name, (unsigned)offset, (unsigned)(offset/2 + 0x0800),
(unsigned)value, size, s->fn);
}
/* Log task writes for debugging — no interception, no faking.
* The DSP handles all tasks via shared API RAM. */
{
hwaddr w0_md = DSP_API_W_PAGE0 + DB_W_D_TASK_MD * 2;
hwaddr w1_md = DSP_API_W_PAGE1 + DB_W_D_TASK_MD * 2;
hwaddr w0_d = DSP_API_W_PAGE0 + DB_W_D_TASK_D * 2;
hwaddr w1_d = DSP_API_W_PAGE1 + DB_W_D_TASK_D * 2;
if ((offset == w0_md || offset == w1_md ||
offset == w0_d || offset == w1_d) && value != 0) {
/* CALYPSO_L1=c : latch le d_task_md écrit par l'ARM (le poll tick-time
* rate ce transient, l1s efface la write-page chaque frame). */
if (calypso_l1_c_active() && (offset == w0_md || offset == w1_md)) {
calypso_layer1_on_task_write((uint16_t)value);
}
static unsigned task_log = 0;
/* Always log non-PM tasks (value != 1) so FB_TASK=5 / SB=6
* surfaces no matter when it occurs. PM=1 thinned. */
bool is_pm = (value == 1);
if (!is_pm || task_log < 100 || (task_log % 500) == 0)
TRX_LOG("ARM TASK WR [0x%04x] = %u fn=%u",
(unsigned)offset, (unsigned)value, s->fn);
task_log++;
/* Test H1 : mémorise insn DSP + EA data DSP quand l'ARM commande
* FB (d_task_md=5), pour que la sonde D_TASK_MD-RD timestampe les
* reads DSP par rapport à ce write et compare les EA. */
if (value == 5 && s->dsp) {
g_arm_taskmd5_insn = s->dsp->insn_count;
g_arm_taskmd5_ea = (uint16_t)(offset/2 + 0x0800);
}
/* === TASK6-IRQ snapshot (2026-05-28) ===
* À chaque ARM TASK WR = 6 (SB demanded), snapshot IMR + IFR du
* DSP. Bit 5 = BRINT0 (BSP RX DMA-complete). Discrimine deux
* causes pour "SB jamais locké" :
* IMR_bit5 = 0 + IFR_bit5 = 0 → bit 5 jamais armé par firmware
* (= bug STM-vers-MMR upstream, ou firmware skip arm)
* IMR_bit5 = 1 + IFR_bit5 = 0 → bit 5 armé mais aucune source
* d'IT ne le set → émulateur McBSP DMA-complete pas modélisé
* IMR_bit5 = 1 + IFR_bit5 = 1 → bit 5 armé + pending, mais
* ISR ne dispatch pas vers PROM3 → bug dispatcher (item 5)
* IMR_bit5 = 0 + IFR_bit5 = 1 → impossible normalement (IFR
* set sans IMR = source assert sans arm — bug émulateur) */
if (value == 6 && s->dsp) {
static unsigned t6_log;
if (t6_log < 50) {
uint16_t imr = s->dsp->imr;
uint16_t ifr = s->dsp->ifr;
TRX_LOG("TASK6-IRQ #%u fn=%u IMR=0x%04x (bit5=%d) "
"IFR=0x%04x (bit5=%d) insn=%u",
t6_log, s->fn, imr, !!(imr & (1<<5)),
ifr, !!(ifr & (1<<5)),
s->dsp->insn_count);
t6_log++;
}
}
/* FBSB orchestration hook: ARM has just written d_task_md.
* Initialise on first call, then log task changes (no host-
* side synthesis remaining as of 2026-05-28 cleanup). */
if (!g_fbsb_inited) {
uint16_t *ndb_target = (s->dsp && s->dsp->data)
? &s->dsp->data[0x0800]
: s->dsp_ram;
calypso_fbsb_init(&g_fbsb, ndb_target, 0x0800);
g_fbsb_inited = true;
TRX_LOG("fbsb init ok ndb_base=0x0800 target=%s",
(s->dsp && s->dsp->data) ? "dsp->data" : "dsp_ram (fallback)");
}
if (g_fbsb_inited) {
TRX_LOG("fbsb hook fired task=%u fn=%u",
(unsigned)value, s->fn);
calypso_fbsb_on_dsp_task_change(&g_fbsb,
(uint16_t)value,
(uint64_t)s->fn);
}
}
}
/* DSP page */
if (offset == DSP_API_NDB) s->dsp_page = value & 1;
/* DSP status */
if (offset == DSP_DL_STATUS_ADDR) {
if (value == 0) { s->dsp_booted = false; s->boot_frame = 0; TRX_LOG("DSP reset"); }
else if (value == DSP_DL_STATUS_READY) {
s->dsp_ram[DSP_API_VER_ADDR/2] = DSP_API_VERSION;
s->dsp_ram[DSP_API_VER2_ADDR/2] = 0;
/* Unmask API IRQ (IRQ15) in INTH */
{
uint16_t mask;
cpu_physical_memory_read(0xFFFFFA08, &mask, 2);
mask &= ~(1 << 15);
cpu_physical_memory_write(0xFFFFFA08, &mask, 2);
TRX_LOG("DSP ready — unmasked API IRQ (mask=0x%04x)", mask);
}
/* Reset C54x DSP — boot runs in TDMA ticks (parallel with ARM).
* Skip if dsp-blob fixture is active: another reset would
* re-run the PROM→DARAM auto-copy and overwrite the loaded
* blob plus the PC override. */
if (s->dsp && calypso_dsp_shunt_early_booted()) {
/* revive c54x : DSP deja boote+parke a machine-init (early-boot).
* NE PAS re-reset : le re-boot re-ecrirait 0xb419 ST #1 = IDLE(1)
* PAR-DESSUS la cmd bootloader COPY_BLOCK(2)+entry de l'ARM. En la
* preservant, le DSP parke lit 2 -> 0xb424 LDU/BACC -> saute a l'entry. */
TRX_LOG("C54x DSP reset SKIPPED — early-booted, preserve bootloader cmd");
} else if (s->dsp && !s->dsp->blob_loaded) {
c54x_reset(s->dsp);
s->dsp->running = true;
s->dsp_init_done = false;
s->dsp_ram[0x01A8/2] = 0;
TRX_LOG("C54x DSP reset — boot via TDMA ticks");
} else if (s->dsp) {
TRX_LOG("DSP_DL_STATUS_READY received but dsp-blob mode "
"active — skipping reset (PC=0x%04x preserved)",
s->dsp->pc);
}
}
}
}
static const MemoryRegionOps calypso_dsp_ops = { .read =
calypso_dsp_read, .write = calypso_dsp_write, .endianness =
DEVICE_LITTLE_ENDIAN, .valid = {.min_access_size=1,.max_access_size=4},
.impl = {.min_access_size=1,.max_access_size=4}, };
/* —- TPU —- / static void calypso_dsp_done(void opaque) {
CalypsoTRX *s = opaque; s->tpu_regs[TPU_CTRL/2] &=
~TPU_CTRL_EN;
/* Hardware DMA: copy API write page → DSP DARAM 0x0586.
* Triggered by firmware writing TPU_CTRL with EN bit (dsp_end_scenario).
* This is the ONLY place DMA happens — same as real Calypso.
*
* GATED par CALYPSO_DSP_SHUNT : si le shunt est actif, on skip
* complètement cette DMA — le mock écrit les résultats directement
* dans NDB/read-page et le c54x est inactif (pas de consommateur).
* HYBRIDE (RANK2, CALYPSO_TPU_RX_WIRE=1) : on lève ce gate pour laisser la
* commande de tâche ARM (task_md=5 FB) atteindre le DSP DARAM 0x0586 même
* sous shunt, condition pour que le vrai corrélateur DSP soit dispatché.
* Réversible : sans l'env, comportement inchangé. */
/* @BEQUILLE — TPU_RX_WIRE (DMA de tache ARM->DARAM 0x0586) (CALYPSO_TPU_RX_WIRE,
* EXISTS, defaut OFF ; calypso_wire.env:=1)
* masque : sous shunt la DMA page-ecriture ARM->DSP est fermee, donc la
* commande de tache (task_md=5 FB) n'atteint jamais le DSP et le
* correlateur entre sans mission. Le meme gate pose plus bas le bit
* tache FB d[0x3f92]|=0x0800 a la place de l'ORM natif 0xa539.
* retirer : quand le shunt ne substitue plus le DSP (la DMA redevient legitime)
* et que 0xa539 s'execute reellement.
*/
static int trx_rxw = -1;
if (trx_rxw < 0) trx_rxw = getenv("CALYPSO_TPU_RX_WIRE") ? 1 : 0;
if (s->dsp && s->dsp_ram[0x01A8/2] != 0 &&
(!calypso_dsp_shunt_active() || trx_rxw)) {
uint16_t page = s->dsp_ram[0x01A8/2] & 1;
uint16_t *wp = page ?
&s->dsp_ram[DSP_API_W_PAGE1/2] : &s->dsp_ram[DSP_API_W_PAGE0/2];
/* Log proof that ARM wrote tasks before DMA */
uint16_t task_d = wp[DB_W_D_TASK_D];
uint16_t task_u = wp[DB_W_D_TASK_U];
uint16_t task_md = wp[DB_W_D_TASK_MD];
if (task_d || task_u || task_md) {
static int dma_task_log = 0;
if (++dma_task_log <= 50)
TRX_LOG("DMA proof: ARM wrote task_d=%u task_u=%u task_md=%u page=%u fn=%u",
task_d, task_u, task_md, page, s->fn);
}
/* Ordre canonique daram < api_ram. Section critique unique pour
* la mirror DMA write page → DSP DARAM. */
calypso_pcb_daram_lock_acquire();
qemu_mutex_lock(&calypso_pcb_api_ram_lock);
s->dsp->data[0x0584] = s->dsp_ram[0x01A8/2];
s->dsp->data[0x0585] = s->fn & 0xFFFF;
for (int i = 0; i < 20; i++)
s->dsp->data[0x0586 + i] = wp[i];
if (s->dsp->api_ram)
s->dsp->api_ram[0x08D4 - C54X_API_BASE] = s->dsp_ram[0x01A8/2];
/* WIRE d[0x3f92] (RANK2, CALYPSO_TPU_RX_WIRE) : quand l'ARM commande la
* tâche FB (task_md=5), poser le bit tâche FB dans le task-word du
* scheduler DSP d[0x3f92]|=0x0800. Le setter natif (ORM 0xa539) est skippé
* car d[5a00]==0x88 -> sans ça d[3f92] reste 0 à vie. Fires à chaque DMA
* de commande FB (task_md=5), indépendant de BDLENA. */
if (trx_rxw && task_md == 5)
s->dsp->data[0x3f92] |= 0x0800;
qemu_mutex_unlock(&calypso_pcb_api_ram_lock);
calypso_pcb_daram_lock_release();
}
/* TPU sequencer scenario interpretation lives in calypso_tpu.c (full
* opcode set: AT/WAIT/SYNCHRO/OFFSET/MOVE/SLEEP, replayed across real
* TDMA frame ticks -- see calypso_tpu_sequencer_tick() below). */
calypso_tpu_run_scenario_regs(s->tpu_ram, s->dsp, s->fn, s->tpu_regs);
qemu_irq_raise(s->irqs[CALYPSO_IRQ_API]);
} static void calypso_tdma_start(CalypsoTRX *s);
/* === CLK-master pthread
================================================= Sends a
4-byte FN counter UDP packet to calypso-ipc-device every * 4.615 ms
wall-clock. Uses clock_nanosleep(CLOCK_MONOTONIC, ABSTIME) * for sub-µs
precision — bypasses the QEMU mainloop ±20ms jitter that * the previous
in-tick send had. The CLK packet drives the qfn-paced UL in
calypso-ipc-device * (qemu_wrap.c), which then advances osmo-trx-ipc’s
TX timeline and * generates CLK_IND to BTS. Précision wall ici =
précision drift TRX↔︎BTS. The pthread maintains its own
g_wall_fn counter. tdma_tick reads it * (via __atomic_load) so the
DSP/BSP work uses wall-aligned FN values. */
#include <time.h> #include <pthread.h>
static volatile uint32_t g_wall_fn = 0; static volatile bool
g_clk_master_running = false; static pthread_t g_clk_master_thread;
static int g_clk_master_fd = -1; static struct sockaddr_in
g_clk_master_peer;
/* GSM TDMA frame = 1250 samples / 270833.33 sps = 60/13 ms = 4 615
384,6 ns. * Fix 2026-05-30 : était 4615000 (arrondi 384 ns TROP
RAPIDE/frame). Ce drain * QEMU plus rapide que le fill device
(PERIOD_NS×2=4615384, calypso-ipc-device * qemu_wrap.c) vidait lentement
la FIFO DL (profondeur ~2) → underrun ~+30s → * “FIFO empty” → IPC LOST
→ I/Q figées. Match exact = plus de drift structurel. / / Match
EXACT le device osmo-trx (qemu_wrap.c PERIOD_NS=2307692 ×2 = 4615384) *
pour biais ZÉRO sur la FIFO DL. NB : osmocom-bb trxcon (sched_trx.c)
utilise * l’arrondi 4615000 côté host, mais le feed I/Q est cadencé par
osmo-trx = la * radio = 4615384 sample-exact. C’est CETTE valeur qu’il
faut matcher. / #define WALL_TDMA_NS 4615384LL / = device
osmo-trx (1250 smpl / 270833,33 sps) */
static void clk_master_loop(void arg) { (void)arg; struct
timespec next; clock_gettime(CLOCK_MONOTONIC, &next);
/* Période TDMA configurable : si l'émulation c54x ne tient pas le 4.615ms
* wall réel (→ osmocon LOST), ralentir UNIFORMÉMENT toute la timeline via
* CALYPSO_TDMA_NS (le device heartbeat lit la MÊME var → osmo-trx/BTS
* suivent → cohérent à vitesse réduite). Défaut = sample-exact réel. */
long long wall_ns = WALL_TDMA_NS;
const char *e = getenv("CALYPSO_TDMA_NS");
if (e && *e) { long long v = atoll(e); if (v >= WALL_TDMA_NS) wall_ns = v; }
fprintf(stderr,
"[clk-master] pthread armed (CLOCK_MONOTONIC ABSTIME, %lld ns/frame%s)\n",
wall_ns, (wall_ns != WALL_TDMA_NS) ? " [SLOWED via CALYPSO_TDMA_NS]" : "");
while (g_clk_master_running) {
next.tv_nsec += wall_ns;
while (next.tv_nsec >= 1000000000LL) {
next.tv_nsec -= 1000000000LL;
next.tv_sec += 1;
}
int rc = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL);
if (rc != 0 && rc != EINTR) {
/* Unrecoverable — log once and bail. */
static int err_logged = 0;
if (!err_logged++) {
fprintf(stderr, "[clk-master] clock_nanosleep rc=%d, exiting\n", rc);
}
break;
}
uint32_t fn = __atomic_add_fetch(&g_wall_fn, 1, __ATOMIC_RELEASE)
% GSM_HYPERFRAME;
if (g_clk_master_fd >= 0) {
uint8_t pkt[4];
pkt[0] = (fn >> 24) & 0xFF;
pkt[1] = (fn >> 16) & 0xFF;
pkt[2] = (fn >> 8) & 0xFF;
pkt[3] = fn & 0xFF;
(void)sendto(g_clk_master_fd, pkt, 4, 0,
(struct sockaddr *)&g_clk_master_peer,
sizeof(g_clk_master_peer));
}
}
return NULL;
}
static void calypso_trx_start_clk_master_thread(CalypsoTRX *s) { if
(g_clk_master_running) return; g_clk_master_fd = s->clk_fd;
g_clk_master_peer = s->clk_peer; g_clk_master_running = true;
pthread_create(&g_clk_master_thread, NULL, clk_master_loop, NULL);
pthread_setname_np(g_clk_master_thread, “cal-clk-master”);
TRX_LOG(“CLK-master pthread started (4.615ms wall, jitter-free)”); }
/* Called by calypso_tint0.c on each TDMA frame tick. * Forward
declaration — actual tdma_tick is defined below. / static void
calypso_tdma_tick(void opaque); /* Prototype visible to tint0
(declared extern there) / void calypso_tint0_do_tick(uint32_t fn);
void calypso_tint0_do_tick(uint32_t fn) { if (!g_trx) return;
g_trx->fn = fn; / d_dsp_page is toggled by the DSP firmware
itself (PC=0x1748), * NOT by ARM or the emulator. Don’t touch it here.
*/ calypso_tdma_tick(g_trx); }
static uint64_t calypso_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 calypso_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) { static int tpu_log = 0;
if (++tpu_log <= 50) TRX_LOG(“TPU_CTRL WR val=0x%04x (EN=%d
DSP_EN=%d) fn=%u”, (unsigned)val, !!(val&TPU_CTRL_EN),
!!(val&TPU_CTRL_DSP_EN), s->fn); } if (off==TPU_CTRL &&
(val&TPU_CTRL_EN)) { s->tpu_regs[TPU_CTRL/2] &=
~(TPU_CTRL_EN|TPU_CTRL_IDLE); /* DMA immediately — no timer delay. The
firmware has already * finished writing the write page before setting
TPU_CTRL_EN. * A 1ns timer caused a race condition where the DMA would
fire * before the write page was fully populated. /
calypso_dsp_done(s); } if (off==TPU_INT_CTRL) { static int ictrl_log =
0; if (++ictrl_log <= 30) TRX_LOG(“INT_CTRL WR val=0x%02x
(MCU_FRAME=%d DSP_FRAME=%d DSP_FORCE=%d) fn=%u”, (unsigned)val,
!!(val&ICTRL_MCU_FRAME), !!(val&ICTRL_DSP_FRAME),
!!(val&ICTRL_DSP_FRAME_FORCE), s->fn); } if (off==TPU_INT_CTRL
&& !(val&ICTRL_MCU_FRAME) && !s->tdma_running)
calypso_tdma_start(s); if (off==TPU_IT_DSP_PG) s->dsp_page=val&1;
} static const MemoryRegionOps calypso_tpu_ops = {
.read=calypso_tpu_read,.write=calypso_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 calypso_tpu_ram_read(void o,hwaddr off,unsigned
sz){CalypsoTRXs=o;return(off/2<CALYPSO_TPU_RAM_SIZE/2)?s->tpu_ram[off/2]:0;}
static void calypso_tpu_ram_write(void o,hwaddr off,uint64_t
v,unsigned sz){ CalypsoTRXs=o; if(off/2<CALYPSO_TPU_RAM_SIZE/2)
s->tpu_ram[off/2]=v; / Probe gated par CALYPSO_DEBUG=TPU_RAM.
Log les 50 premières writes * + chaque 1000ème pour visualiser le rythme
de programmation TPU * par le firmware (l1s_rx_win_ctrl, tpu_enq_,
etc.). / static unsigned tpu_ram_wr = 0; tpu_ram_wr++; if
(tpu_ram_wr <= 50 || (tpu_ram_wr % 1000) == 0) { if
(calypso_debug_enabled(“TPU_RAM”)) { fprintf(stderr, “[calypso-trx]
TPU_RAM WR #%u off=0x%04x val=0x%04x fn=%u”, tpu_ram_wr, (unsigned)off,
(unsigned)v, s->fn); } } } static const MemoryRegionOps
calypso_tpu_ram_ops={.read=calypso_tpu_ram_read,.write=calypso_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},};
/* —- TSP —- / static uint64_t calypso_tsp_read(void
o,hwaddr off,unsigned
sz){CalypsoTRXs=o;return(off==TSP_RX_REG)?0xFFFF:(off/2<CALYPSO_TSP_SIZE/2)?s->tsp_regs[off/2]:0;}
static void calypso_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
calypso_tsp_ops={.read=calypso_tsp_read,.write=calypso_tsp_write,.endianness=DEVICE_LITTLE_ENDIAN,.valid={.min_access_size=1,.max_access_size=4},.impl={.min_access_size=1,.max_access_size=4},};
/* —- ULPD —- / static uint64_t calypso_ulpd_read(void
o,hwaddr off,unsigned sz){
CalypsoTRXs=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 calypso_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
calypso_ulpd_ops={.read=calypso_ulpd_read,.write=calypso_ulpd_write,.endianness=DEVICE_LITTLE_ENDIAN,.valid={.min_access_size=1,.max_access_size=2},.impl={.min_access_size=1,.max_access_size=2},};
/* —- SIM (forwarded to calypso_sim.c) —- / static uint64_t
calypso_sim_read(void o, hwaddr off, unsigned sz) { CalypsoTRX
s = o; return calypso_sim_reg_read(s->sim, off); } static void
calypso_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 calypso_sim_ops = { .read =
calypso_sim_read, .write = calypso_sim_write, .endianness =
DEVICE_LITTLE_ENDIAN, .valid = { .min_access_size = 1, .max_access_size
= 4 }, .impl = { .min_access_size = 1, .max_access_size = 4 }, };
/* —- vCPU idle governor (host CPU-leak / thermal fix) ——————- * The
osmocom-bb L1 firmware (apps/layer1/main.c) runs a side-effect-free *
super-loop with NO WFI: * while (1) { l1a_compl_execute();
osmo_timers_update(); * sim_handler(); l1a_l23_handler(); } * On silicon
a dedicated baseband core spinning is free. Under -icount auto * QEMU
must emulate that spin at ~real-time and therefore pins one host core *
at 99.9% forever (observed: the vCPU/TCG thread, PC bouncing across *
l1a_compl_execute/l1a_l23_handler — the empty poll, not real work).
Fix: we are called from the frame-IRQ lower callback (~1
ms after the * raise), i.e. once the per-frame work for this TDMA tick
is done. If the * guest PC is back inside the L1 idle super-loop, park
the vCPU * (cs->halted = 1). The next TPU-frame / UART (L1CTL) / SIM
interrupt clears * halted and resumes execution exactly where it left
off — invisible to the * guest because the loop is a pure poll. Under
icount the halt lets QEMU * warp virtual time to the next timer and the
host core sleeps. Safety: * - cpu_handle_halt() refuses to
halt while an IRQ is pending * (cpu_has_work) → never stalls active
interrupt servicing. * - PC-gating to [lo,hi] → we only park while
genuinely in the L1 * super-loop, never mid-DSP / mid-handler real work.
Outside the window * we do nothing, so other code paths cannot regress.
* - Opt-out: CALYPSO_CPU_IDLE=0. Window override (hex): *
CALYPSO_IDLE_PC_LO / CALYPSO_IDLE_PC_HI. Window 0 = halt whenever no *
IRQ is pending (rely on cpu_has_work only). / static void
calypso_cpu_idle_park(void) { static int enabled = -1; static uint64_t
lo, hi, parked_n; if (enabled < 0) { const char e =
getenv(“CALYPSO_CPU_IDLE”); const char l =
getenv(“CALYPSO_IDLE_PC_LO”); const char h =
getenv(“CALYPSO_IDLE_PC_HI”); enabled = (e && e == ‘0’) ? 0
: 1; lo = l ? strtoull(l, NULL, 0) : 0x00823000ULL; /
l1a_l23_handler .. / hi = h ? strtoull(h, NULL, 0) : 0x00826000ULL;
/ .. l1a_compl_execute */ fprintf(stderr, “[cpu-idle] governor %s
window=[0x%llx,0x%llx]”, enabled ? “ON (opt-out CALYPSO_CPU_IDLE=0)” :
“OFF”, (unsigned long long)lo, (unsigned long long)hi); } if (!enabled)
return;
CPUState *cs = first_cpu;
if (!cs) return;
uint64_t pc = (cs->cc && cs->cc->get_pc) ? cs->cc->get_pc(cs) : 0;
if (lo && hi && (pc < lo || pc >= hi))
return; /* not in the L1 idle loop — leave it running */
cs->halted = 1;
cpu_exit(cs); /* break the current TB so the halt takes now */
if ((++parked_n % 5000) == 0 && calypso_timer_log())
fprintf(stderr, "[cpu-idle] parked #%llu pc=0x%llx\n",
(unsigned long long)parked_n, (unsigned long long)pc);
}
/* —- TDMA —- / static void calypso_frame_irq_lower(void o){
/* Frame IRQ lower counter — log thinned 1/1000 pour drift detection.
/ static uint64_t firq_lower_n = 0; firq_lower_n++; if
((firq_lower_n % 1000) == 0 && calypso_timer_log()) {
fprintf(stderr, “[frame_irq] lower #%llu t_virt=%lld”, (unsigned long
long)firq_lower_n, (long long)qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL)); }
qemu_irq_lower(((CalypsoTRX)o)->irqs[CALYPSO_IRQ_TPU_FRAME]);
/* DSP shunt service hook (no-op si shunt off). Servir APRÈS le lower
* pour que le mock écrive ses résultats entre deux ticks ARM. */
calypso_dsp_shunt_on_frame_tick();
/* Per-frame work for this tick is done — park the vCPU if the guest is
* back in its idle super-loop, so the host core sleeps until the next
* interrupt instead of spinning at 100%. See calypso_cpu_idle_park(). */
calypso_cpu_idle_park();
}
/* CALYPSO_TDMA_REALTIME=1 : pin tdma_timer to QEMU_CLOCK_REALTIME so
* the 4.6 ms GSM frame cadence is wall-clock, independent of guest *
cycle rate. Fixes L23 sync timeouts under icount=auto (tdma_tick * was
firing at ~17 Hz instead of 217 Hz when virtual time lagged). * Default
unset = VIRTUAL clock (legacy behaviour). Decision made * once at first
tick and cached. / static QEMUClockType calypso_tdma_clock(void) {
static int cached = -1; if (cached < 0) { / DEFAULT VIRTUAL
(2026-05-29 v2, single-domain) : tout le système * (ARM, DSP, radio via
clk-master FN, mobile) doit partager UNE base * de temps = le temps
virtuel QEMU, comme le HW partage l’horloge RF. * Le défaut REALTIME
(wall-clock) faisait courir la radio/mobile à * 100% wall pendant que
l’ARM virtuel traîne à ~7% → drift → LOST. * En VIRTUAL le tdma_tick
devient maître du FN (cf section 0) et la * radio suit le rythme virtuel
: lent en wall mais zéro drift, et * insensible au debug/charge host.
Opt-in wall via CALYPSO_TDMA_REALTIME=1. / const char e =
getenv(“CALYPSO_TDMA_REALTIME”); cached = (e && *e == ‘1’) ? 1 :
0; fprintf(stderr, “[calypso-trx] tdma_timer clock = %s”, cached ?
“REALTIME (wall-clock 217 Hz, opt-in)” : “VIRTUAL (single-domain,
default)”); } return cached ? QEMU_CLOCK_REALTIME : QEMU_CLOCK_VIRTUAL;
}
static void calypso_tdma_tick(void opaque) { CalypsoTRX s =
opaque;
/* Halt-sync : if the ARM CPU is paused (GDB stop, monitor stop),
* also pause DSP ticking. Otherwise tdma_timer (REALTIME) keeps
* firing, c54x_run keeps advancing the DSP, qemu.log keeps growing
* — making GDB inspection useless because the system state drifts
* under the breakpoint. Re-arm timer so we resume cleanly. */
if (!runstate_is_running()) {
if (s->tdma_running) {
timer_mod_ns(s->tdma_timer,
qemu_clock_get_ns(calypso_tdma_clock()) + GSM_TDMA_NS);
}
return;
}
int64_t entry_t = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
int64_t t_clk = 0, t_uart = 0, t_dspboot = 0, t_dspirq = 0,
t_bsp = 0, t_ul = 0;
/* Sync s->fn to the wall-clock fn from clk_master_thread. The
* pthread is the source of truth for "current GSM frame number" —
* it ticks at exact 4.615ms wall using clock_nanosleep ABSTIME.
* Si le pthread est encore en init (g_wall_fn=0), on garde notre
* propre compteur en fallback pour ne pas freeze le DSP. */
{
if (calypso_tdma_clock() == QEMU_CLOCK_VIRTUAL) {
/* SINGLE-DOMAIN (default) : le tdma_tick VIRTUAL est le MAÎTRE du
* FN — il avance g_wall_fn d'une frame par tick virtuel ET envoie
* le CLK à la radio (cf section 0). La radio (ipc-device/trx-ipc)
* suit donc le temps virtuel de QEMU → zéro drift ARM↔radio↔mobile.
* (Le pthread wall clk-master n'est PAS démarré dans ce mode.) */
uint32_t fn = __atomic_add_fetch(&g_wall_fn, 1, __ATOMIC_RELEASE)
% GSM_HYPERFRAME;
s->fn = fn;
if (s->clk_fd >= 0) {
uint8_t pkt[4];
pkt[0] = (fn >> 24) & 0xFF; pkt[1] = (fn >> 16) & 0xFF;
pkt[2] = (fn >> 8) & 0xFF; pkt[3] = fn & 0xFF;
(void)sendto(s->clk_fd, pkt, 4, 0,
(struct sockaddr *)&s->clk_peer, sizeof(s->clk_peer));
}
} else {
/* REALTIME (opt-in) : le pthread wall clk-master est maître, on le
* suit. Si encore en init (g_wall_fn=0), fallback compteur local. */
uint32_t wfn = __atomic_load_n(&g_wall_fn, __ATOMIC_ACQUIRE);
if (wfn != 0) {
s->fn = wfn % GSM_HYPERFRAME;
} else {
s->fn = (s->fn + 1) % GSM_HYPERFRAME;
}
}
}
/* TPU sequencer: advance any AT/WAIT-paused scenario by one real TDMA
* frame (the 11x tpu_enq_at(0) FB-window delay is now genuinely
* spread across 11 ticks instead of firing instantly). */
calypso_tpu_sequencer_tick(s->fn);
/* TDMA tick counter — log thinned 1/1000 (~4.6s wall) pour drift detection.
* Variables locales pour cumul DSP insn (utilisées plus bas). */
static uint64_t tdma_ticks = 0;
static uint64_t dsp_insn_total = 0;
tdma_ticks++;
int dsp_n_exec_2 = 0, dsp_n_exec_5 = 0; /* updated by c54x_run calls */
/* ── 0. CLK send delegated to clk_master_thread (jitter-free) ── */
t_clk = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
/* ── 1. UART poll: deliver pending chardev bytes to firmware ── */
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);
}
t_uart = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
/* ── 2. DSP boot phase ── */
/* DSP budget per c54x_run call. 256000 ≈ 1 frame nominale du c54x réel
* (≈104 MHz × 4.615 ms = 480k cycles total, ici budget par appel × 2 appels).
* Sous DSP-overload (fb-det compute), 2× ce budget = ~18.6 ms wall sur le
* tdma_tick alors que la frame GSM dure 4.615 ms → drift wall/qfn 3.6×.
* Override via CALYPSO_DSP_BUDGET pour mesurer A/B sans recompiler. Voir
* REPORT_CLAUDE_WEB_20260516_DSP_OVERRUN.md. */
static int dsp_budget = -1;
if (dsp_budget < 0) {
const char *e = getenv("CALYPSO_DSP_BUDGET");
dsp_budget = (e && *e) ? atoi(e) : 256000;
if (dsp_budget < 1000) dsp_budget = 1000;
TRX_LOG("CALYPSO_DSP_BUDGET = %d insn/c54x_run (default 256000)",
dsp_budget);
}
/* GATE DSP_SHUNT : si le shunt est actif, le mock cote ARM remplace
* la DSP. Skip TOUS les c54x_run -> le c54x emule n'execute aucune
* instruction, ne touche pas a la DARAM, ne fabrique pas de d_dsp_page
* concurrent avec le mock. */
if (s->dsp && s->dsp->running && !s->dsp_init_done && !calypso_dsp_shunt_substitutes()) {
if (!s->dsp->idle)
dsp_n_exec_2 = c54x_run(s->dsp, dsp_budget);
if (s->dsp->idle) {
s->dsp_init_done = true;
TRX_LOG("DSP init complete (first IDLE reached)");
}
} else if (calypso_dsp_shunt_substitutes() && !s->dsp_init_done) {
/* En shunt mode, on saute l'init DSP "boot" — le mock prend le relais. */
s->dsp_init_done = true;
}
t_dspboot = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
/* ── 3. DMA is NOT done here ──
* On real Calypso, the TPU scenario triggers the DMA when the
* firmware writes TPU_CTRL with EN bit. This happens in
* calypso_dsp_done() (the TPU_CTRL_EN timer callback).
* Doing DMA here would copy a STALE write page because the
* firmware hasn't written the new tasks yet (it writes them
* in l1s_compl() which runs in the IRQ4 handler AFTER this tick). */
/* ── 4. DSP frame interrupt ──
* Three conditions for periodic INT3 fire:
* - INT_CTRL.ICTRL_DSP_FRAME (bit 2) = persistent enable at TPU,
* polarity INVERTED (bit clear = enabled).
* - DSP IMR bit 3 (C54X_INT_FRAME_BIT) = mask enable at DSP.
* Empirically: firing INT3 while IMR bit 3 = 0 perturbs the
* firmware boot path (DSP wakes from IDLE without expecting it,
* takes wrong code path, never reaches IMR-init at PC=0x0810,
* dead-locks). Respecting IMR matches the "hardware INT line
* gated by IMR" model used on Calypso.
* - TPU_CTRL.DSP_EN (bit 4) = one-shot force, alternative path.
* Bypasses IMR (explicit hardware override). */
if (s->dsp && s->dsp->running) {
bool was_idle = s->dsp->idle;
bool tpu_armed = !(s->tpu_regs[TPU_INT_CTRL/2] & ICTRL_DSP_FRAME);
static int _natfr = -1; if (_natfr < 0) _natfr = (getenv("CALYPSO_FRAME_IT_NATIVE") || getenv("CALYPSO_DSP_FRAME_VEC28")) ? 1 : 0;
bool imr_armed = !!(s->dsp->imr & (1 << (_natfr ? 12 : C54X_INT_FRAME_BIT))); /* [2026-07-23] bit12 en natif (remap) */
bool periodic_armed = tpu_armed && imr_armed;
bool force_pulse = !!(s->tpu_regs[TPU_CTRL/2] & TPU_CTRL_DSP_EN);
/* FIX DOUBLE-INT3 : quand la route c54x du shunt est active, c'est
* shunt_route_to_c54x() qui fire l'INT3 frame. Ne PAS le double-firer ici,
* sinon le c54x reçoit 2 IT frame/tick -> déraille -> crash qemu. */
if ((periodic_armed || force_pulse) && !calypso_dsp_shunt_route_c54x_active()) {
c54x_interrupt_ex(s->dsp, C54X_INT_FRAME_VEC, C54X_INT_FRAME_BIT);
if (force_pulse)
s->tpu_regs[TPU_CTRL/2] &= ~TPU_CTRL_DSP_EN;
/* periodic_armed: do NOT clear — hardware-persistent enable. */
}
/* ── 5. Run DSP (RX path : FBSB demod, BCCH/CCCH decode) ──
* Budget partagé avec section 2 via static `dsp_budget` (env var
* CALYPSO_DSP_BUDGET). NE PAS supprimer ce 2e appel — il porte le
* compute RX critique (Claude web review 2026-05-16).
*
* GATE DSP_SHUNT : skip si shunt actif (cf section 2 commentaire). */
if (!s->dsp->idle && !calypso_dsp_shunt_substitutes()) {
dsp_n_exec_5 = c54x_run(s->dsp, dsp_budget);
}
/* CALYPSO_L1=c : pilote le modèle L1 HLE APRÈS le c54x RX (qui ne produit
* rien d'exploitable) -> d_fb_det + a_sync_demod sont les dernières écritures
* de la frame. Lit l'I/Q injectée en DARAM 0x2a00 et corrèle le FCCH. */
/* Ne PAS piloter le modèle L1=c quand le shunt est actif : il écrirait
* d_fb_det=0 par-dessus le d_fb_det=1 du shunt (clobber -> FB perdu).
* Le shunt possède alors la réception (FB+SB+SI). Le modèle L1=c ne tourne
* que sans shunt (chemin HLE pur). */
if (calypso_l1_c_active() && !calypso_dsp_shunt_active()) {
calypso_layer1_tick(s->dsp, s->dsp_ram, s->fn);
}
/* Do NOT clear tasks here — the firmware's l1s_compl() does
* dsp_api_memset() on the write page at the start of each frame,
* before tdma_sched_execute() writes new tasks. Clearing here
* would erase tasks that the scheduler just programmed. */
/* Only pulse API IRQ when DSP naturally reaches IDLE. */
if (!was_idle && s->dsp->idle) {
qemu_irq_raise(s->irqs[CALYPSO_IRQ_API]);
}
}
t_dspirq = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
/* [tdma] log : drift detection + budget DSP réel consommé par tick.
* Cadence 1/1000 ticks (~4.6s wall en steady state). Indique :
* - tick #N (compteur cumulé)
* - fn (frame number)
* - t_virt (entry timestamp en ns virtual)
* - dsp_n_exec_2 (insn DSP exec dans section 2 — DSP boot/idle phase)
* - dsp_n_exec_5 (insn DSP exec dans section 5 — RX path post-IRQ)
* - budget = CALYPSO_DSP_BUDGET (default 256000)
* Si dsp_n_exec_* << dsp_budget en steady state, ça signifie que le
* DSP atteint IDLE avant d'épuiser son budget — on peut réduire le
* budget sans dégrader. Si dsp_n_exec_* == dsp_budget en steady state,
* le DSP est saturé et réduire le budget va casser fb-det. */
dsp_insn_total += (uint64_t)(dsp_n_exec_2 + dsp_n_exec_5);
if ((tdma_ticks % 1000) == 0) {
fprintf(stderr,
"[tdma] tick #%llu fn=%u t_virt=%lld "
"dsp_n_exec_2=%d dsp_n_exec_5=%d dsp_insn_total=%llu budget=%d\n",
(unsigned long long)tdma_ticks, s->fn, (long long)entry_t,
dsp_n_exec_2, dsp_n_exec_5,
(unsigned long long)dsp_insn_total, dsp_budget);
}
/* ── 6. BSP DL delivery is now driven by wall-clock drain timer in
* calypso_bsp.c (bsp_drain_cb @ 5ms REALTIME). Decoupling fix 2026-05-24:
* under icount=auto, tdma_tick fires too slowly (17 Hz) to drain the
* BTS UDP stream (217 Hz arrival) — bursts went stale before delivery.
* The drain timer runs at wall rate matching BTS, while DSP continues
* to process samples at its virtual-clock pace. */
t_bsp = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
/* ── 6b. UL burst poll ──
* The MCU→DSP write page exposes three independent UL task fields:
* d_task_u (word 2) — generic UL: SDCCH/SACCH/FACCH/TCH NB
* d_task_ra (word 7) — RACH access burst (8 info bits → AB)
* d_burst_u (word 3) — TN selector
* Each UL kind has its own d_task_*; the firmware (prim_rach.c,
* prim_tx_nb.c) writes whichever applies. We must poll all of them
* — polling only d_task_u silently drops every RACH attempt. */
{
uint16_t *wp = s->dsp_page ?
&s->dsp_ram[DSP_API_W_PAGE1 / 2] : &s->dsp_ram[DSP_API_W_PAGE0 / 2];
uint16_t task_u = wp[DB_W_D_TASK_U];
uint16_t task_ra = wp[DB_W_D_TASK_RA];
uint8_t tn = wp[DB_W_D_BURST_U] & 0x07;
if (task_ra != 0 && s->dsp) {
/* RACH: dsp_task_iq_swap(RACH_DSP_TASK, arfcn, 1) packs
* task ID + ARFCN. The 8-bit RACH info is in NDB d_rach.
* Burst encoding (gsm0503_rach_ext_encode) belongs in the
* BSP UL path — see calypso_bsp.c.
*
* IMPORTANT : zero-init bits[148] before encode. libosmocoding
* fills only the 41-bit sync + 36-bit FIRE-encoded data + 3-bit
* tail (~80 bits total in the AB structure). The remaining 60
* bits of guard period (positions 88..147) are NOT written by
* the encoder ; without zero-init we'd transmit stack garbage
* in the guard period, which BTS RACH detector treats as
* out-of-sync noise → silent drop. Confirmed empirically via
* burst hex print : same 8 trailing bits across all RAs before
* this fix. */
uint8_t bits[148] = {0};
if (calypso_bsp_tx_rach_burst(s->fn, bits)) {
calypso_bsp_send_ul(tn, s->fn, bits);
static int rach_log = 0;
if (++rach_log <= 20)
TRX_LOG("UL RACH task=0x%04x tn=%u fn=%u",
task_ra, tn, s->fn);
}
wp[DB_W_D_TASK_RA] = 0;
}
if (task_u != 0 && s->dsp) {
/* NB UL : same zero-init reasoning as RACH path. */
uint8_t bits[148] = {0};
if (calypso_bsp_tx_burst(tn, s->fn, bits)) {
calypso_bsp_send_ul(tn, s->fn, bits);
static int ul_log = 0;
if (++ul_log <= 20)
TRX_LOG("UL NB task=0x%04x tn=%u fn=%u",
task_u, tn, s->fn);
}
wp[DB_W_D_TASK_U] = 0;
}
}
t_ul = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
/* ── 7. TPU FRAME IRQ → ARM L1 scheduler ── */
{
static FILE *firq_log = NULL;
static int firq_count = 0;
static int64_t prev_firq_t = 0;
if (firq_count < 2000 && calypso_timer_log()) { /* DISABLED for baseline — re-enable by setting >0 */
if (!firq_log) firq_log = fopen("/tmp/frame_irq.log", "w");
if (firq_log) {
int64_t now = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
int64_t dt = prev_firq_t ? (now - prev_firq_t) : 0;
int64_t target = now + GSM_TDMA_NS;
fprintf(firq_log, "[frame-irq] raise t_virt=%" PRId64
" dt=%" PRId64 " next_target=%" PRId64
" gap_to_target=%" PRId64 " fn=%u #%d\n",
now, dt, target, (target - now), s->fn, firq_count);
prev_firq_t = now;
firq_count++;
}
}
}
/* Fige timer #1 sur la grille de trame pour cette IRQ délivrée -> le firmware
* check_lost_frame() voit un pas de 1875 exact (fin du spam LOST). */
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) + 1000000);
/* ── 8. Re-arm TDMA timer ──
* FIX: anchor on entry_t (start of tick), not on exit_t. Otherwise
* the work_dt of the body cumulates into the deadline and the TDMA
* cadence drifts to (work_dt + GSM_TDMA_NS) instead of staying at
* GSM_TDMA_NS exact.
*
* Si déjà en retard (work_dt > GSM_TDMA_NS), sauter aux frames suivantes
* pour rester aligné sur la grille TDMA. Mimique silicon : la(les) frame(s)
* sont perdues mais le timer ne dérive pas et le main loop n'est pas saturé
* par des back-to-back catch-up. */
{
/* === Monotonic timer (drift-free rearm) 2026-05-28 ===
*
* Previous code used `entry_t_clk + GSM_TDMA_NS` as the next target.
* entry_t_clk = wall time when the handler was actually dispatched,
* which already includes any BQL/IRQ/work latency from the previous
* fire. Therefore target absorbed that latency : on every late
* dispatch (~200µs typical at 217 ticks/s), the next deadline
* drifted by +200µs. After 1 wall second : ~45ms accumulated drift.
* BTS measured 207 FN/sec wall vs expected 217 FN/sec — exactly
* the 4.6% gap.
*
* Fix : anchor target on `last_target + GSM_TDMA_NS` (the IDEAL
* deadline of the previous tick), not on `now`. Drift no longer
* accumulates. If a deadline is already in the past at wake-up
* (handler took >4.615ms), skip frames to stay on the absolute
* TDMA grid and advance FN to match (mimics silicon : late frames
* are *lost*, not retransmitted, but the timeline never lags).
*
* Activé seulement quand CALYPSO_TDMA_REALTIME=1 (= REALTIME clock).
* En mode VIRTUAL legacy, virtual time advance is already lockstep
* with guest cycles → drift par construction.
*/
QEMUClockType tclk = calypso_tdma_clock();
int64_t now = qemu_clock_get_ns(tclk);
static int64_t last_target = 0;
if (last_target == 0) {
/* First tick: seed last_target from entry time so initial
* scheduling is normal-paced. */
last_target = (tclk == QEMU_CLOCK_REALTIME)
? now
: entry_t;
}
int64_t target = last_target + GSM_TDMA_NS;
int skipped = 0;
while (target <= now) {
target += GSM_TDMA_NS;
skipped++;
}
last_target = target;
/* No FN catchup needed — s->fn is sync'd to g_wall_fn at entry,
* which is incremented by clk_master_thread independently. */
{
static int rearm_log_count = 0;
if (rearm_log_count < 50) {
fprintf(stderr, "[rearm-fix] last_target=%" PRId64 " target=%" PRId64
" now=%" PRId64 " gap_to_now=%" PRId64 " skipped=%d fn=%u\n",
last_target - (int64_t)GSM_TDMA_NS, target, now,
target - now, skipped, s->fn);
rearm_log_count++;
}
}
if (skipped > 0 && (s->fn % 100 == 0) && calypso_timer_log()) {
fprintf(stderr, "[tdma-skip] fn=%u skipped=%d work_dt=%" PRId64 "\n",
s->fn, skipped, now - entry_t);
}
if (s->tdma_running) {
timer_mod_ns(s->tdma_timer, target);
}
}
{
static FILE *tick_log = NULL;
static int tick_count = 0;
if (tick_count < 500 && calypso_timer_log()) {
if (!tick_log) tick_log = fopen("/tmp/tdma_tick.log", "w");
if (tick_log) {
int64_t exit_t = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
fprintf(tick_log, "[tdma-tick] entry=%" PRId64 " exit=%" PRId64
" work_dt=%" PRId64 " fn=%u #%d\n",
entry_t, exit_t, (exit_t - entry_t), s->fn, tick_count);
tick_count++;
}
}
}
/* Profile per sub-block: identifie quelle section consomme work_dt. */
{
static FILE *prof_log = NULL;
static int prof_count = 0;
if (prof_count < 200) {
if (!prof_log) prof_log = fopen("/tmp/tdma_profile.log", "w");
if (prof_log) {
int64_t exit_t = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
fprintf(prof_log, "[prof] fn=%u clk=%" PRId64 " uart=%" PRId64
" dspboot=%" PRId64 " dspirq=%" PRId64 " bsp=%" PRId64
" ul=%" PRId64 " irq=%" PRId64 " total=%" PRId64
" #%d\n",
s->fn,
t_clk - entry_t,
t_uart - t_clk,
t_dspboot - t_uart,
t_dspirq - t_dspboot,
t_bsp - t_dspirq,
t_ul - t_bsp,
exit_t - t_ul,
exit_t - entry_t,
prof_count);
prof_count++;
}
}
}
}
static void calypso_tdma_start(CalypsoTRX *s) { if
(s->tdma_running) return; s->tdma_running = true; s->fn = 0;
TRX_LOG(“TDMA started”); timer_mod_ns(s->tdma_timer,
qemu_clock_get_ns(calypso_tdma_clock()) + GSM_TDMA_NS); }
/* —- kick —- * Periodic CPU exit + main-loop wake. Whose role is to
force the event * loop to service fd handlers (UDP bridge sockets,
chardev) even when * the guest is in long TCG bursts. AUDIT
FIX 2026-05-08 night : reverted to QEMU_CLOCK_REALTIME (was * moved to
VIRTUAL on 2026-05-07 based on a faulty diagnosis). Rationale
per Claude web event-loop audit : * - Under -icount, VIRTUAL warps with
guest progress. A VIRTUAL-clock * kick fires “in sync” with the guest =
tautologically useless, * cpu_exit becomes a no-op (we’re already in the
main loop when the * timer dispatches), and the kick contributes
nothing. * - REALTIME on the other hand advances independently and
guarantees * that fd handlers are serviced at wall-time intervals
regardless * of guest TCG burst length. This is precisely the original
purpose. * - The 2026-05-07 claim that REALTIME-driven cpu_exit was
blocking * VIRTUAL TDMA timers was wrong : cpu_exit terminates the
current * burst, the main loop runs the next one immediately, and
virtual * time is not gated on cpu_exit calls. The real
culprit blocking the bridge under icount was the *
main_loop_wait(false) recursive call in
calypso_uart_rx_poll * (fixed in calypso_uart.c same session), not this
kick timer. / static QEMUTimer g_kick_timer; static void
calypso_kick_cb(void o){ / AUDIT INSTRUMENTATION 2026-05-08
night : confirm kick fires under * -icount auto. Per Claude web : if 0
hits in 5s wall → REALTIME timer * not armed correctly with icount. If
N≈1000 hits/5s (5ms period) → * timer fires but cpu_exit/notify don’t
propagate to scheduler. */ static unsigned kick_n; kick_n++; if ((kick_n
<= 30 || (kick_n % 200) == 0) && calypso_timer_log()) {
uint64_t vt = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL); uint64_t rt =
qemu_clock_get_ns(QEMU_CLOCK_REALTIME); fprintf(stderr, “[kick] fire #%u
vt=%lu rt=%lu”, kick_n, (unsigned long)vt, (unsigned long)rt); }
CPUState*cpu=first_cpu;if(cpu)cpu_exit(cpu);qemu_notify_event();
{
static int pcb_threaded = -1;
if (pcb_threaded < 0) {
const char *e = getenv("CALYPSO_PCB_TICK_THREADS");
pcb_threaded = (e && e[0] == '1') ? 1 : 0;
}
if (!pcb_threaded) {
timer_mod_ns(g_kick_timer,qemu_clock_get_ns(QEMU_CLOCK_REALTIME)+5000000);
}
}
}
/* === Public invokers pour pcb tick threads
============================ * Appelés depuis calypso_full_pcb.c thread
bodies, avec BQL held. */ void calypso_trx_kick_invoke(void); void
calypso_trx_tdma_tick_invoke(void); void
calypso_trx_frame_irq_lower_invoke(void);
void calypso_trx_kick_invoke(void) { calypso_kick_cb(NULL); }
void calypso_trx_tdma_tick_invoke(void) { if (g_trx)
calypso_tdma_tick(g_trx); }
void calypso_trx_frame_irq_lower_invoke(void) { if (g_trx)
calypso_frame_irq_lower(g_trx); }
/* —- Sercomm burst transport (DLCI 4) —- */
/* RX burst from bridge (DL) — store in DSP RAM for firmware to read
/ void calypso_trx_rx_burst(const uint8_t data, int len) { if
(!g_trx || len < 8) return; CalypsoTRX *s = g_trx;
uint8_t tn = data[0] & 0x07;
uint32_t fn = ((uint32_t)data[1]<<24)|((uint32_t)data[2]<<16)|
((uint32_t)data[3]<<8)|(uint32_t)data[4];
/* Sync FN */
s->fn = fn % GSM_HYPERFRAME;
static int rx_count = 0;
if (++rx_count <= 5 || (rx_count % 1000) == 0)
TRX_LOG("RX_BURST #%d TN=%d FN=%u len=%d", rx_count, tn, fn, len);
/* No stubs — bursts go to BSP via UDP (calypso_bsp.c), not here.
* The DSP processes them and writes results to shared API RAM. */
(void)tn;
}
/* TX burst: send UL burst from DSP write page via UART TX as sercomm
DLCI 4 / static void calypso_trx_send_ul_burst(CalypsoTRX s,
uint16_t task_u) { if (!g_uart_modem || task_u == 0) return;
/* Read UL burst from write page.
* d_burst_u at word 3, burst data follows in NDB a_cu area. */
uint16_t *wp = s->dsp_page ?
&s->dsp_ram[DSP_API_W_PAGE1 / 2] : &s->dsp_ram[DSP_API_W_PAGE0 / 2];
/* Build TRXD v0 TX packet: TN(1) FN(4) PWR(1) bits(148) */
uint8_t pkt[6 + 148];
uint8_t tn = wp[3] & 0x07; /* d_burst_u has TN info */
uint32_t fn = s->fn;
pkt[0] = tn;
pkt[1] = (fn >> 24) & 0xFF;
pkt[2] = (fn >> 16) & 0xFF;
pkt[3] = (fn >> 8) & 0xFF;
pkt[4] = fn & 0xFF;
pkt[5] = 0; /* TX power */
/* Read burst bits from NDB UL area — for now send dummy burst */
memset(&pkt[6], 0, 148);
/* Wrap in sercomm DLCI 4 and send via UART TX */
uint8_t frame[512];
int pos = 0;
frame[pos++] = 0x7E; /* FLAG */
/* Header: DLCI + CTRL, with escaping */
uint8_t hdr[2] = { 0x04, 0x03 };
for (int i = 0; i < 2; i++) {
if (hdr[i] == 0x7E || hdr[i] == 0x7D) {
frame[pos++] = 0x7D;
frame[pos++] = hdr[i] ^ 0x20;
} else {
frame[pos++] = hdr[i];
}
}
/* Payload with escaping */
int pkt_len = 6 + 148;
for (int i = 0; i < pkt_len && pos < 500; i++) {
if (pkt[i] == 0x7E || pkt[i] == 0x7D) {
frame[pos++] = 0x7D;
frame[pos++] = pkt[i] ^ 0x20;
} else {
frame[pos++] = pkt[i];
}
}
frame[pos++] = 0x7E; /* FLAG */
/* Write to UART chardev (goes to PTY → bridge reads it) */
qemu_chr_fe_write_all(&g_uart_modem->chr, frame, pos);
}
void calypso_trx_tx_burst_poll(void) { if (!g_trx) return; /* Check
if firmware wrote a UL task / CalypsoTRX s = g_trx; uint16_t
wp = s->dsp_page ? &s->dsp_ram[DSP_API_W_PAGE1 / 2] :
&s->dsp_ram[DSP_API_W_PAGE0 / 2]; uint16_t task_u =
wp[DB_W_D_TASK_U]; if (task_u != 0) { calypso_trx_send_ul_burst(s,
task_u); wp[DB_W_D_TASK_U] = 0; / clear after sending */ } }
/* Expose DSP state to machine_init for the
-M calypso,dsp-blob= fixture. * Returns NULL if
calypso_trx_init() hasn’t run or the ROM load failed. / C54xState
calypso_trx_get_dsp(void) { return g_trx ? g_trx->dsp : NULL;
}
/* Per-section ROM bin paths, set by mb.c machine_init BEFORE
sysbus_realize * so that trx_init can load each section into
prog[]/data[] before * c54x_reset() — the reset’s
PROM→DARAM auto-copy needs prog[] populated. / static const char
g_section_prom0; static const char g_section_prom1; static
const char g_section_prom2; static const char g_section_prom3;
static const char g_section_drom; static const char
g_section_pdrom; static const char g_section_registers;
void calypso_trx_set_section_paths(const char prom0, const char
prom1, const char prom2, const char prom3, const char
drom, const char pdrom) { g_section_prom0 = prom0;
g_section_prom1 = prom1; g_section_prom2 = prom2; g_section_prom3 =
prom3; g_section_drom = drom; g_section_pdrom = pdrom; }
void calypso_trx_set_registers_path(const char *registers) {
g_section_registers = registers; }
/* —- Init —- / void calypso_trx_init(MemoryRegion sysmem,
qemu_irq irqs) { CalypsoTRX s = g_new0(CalypsoTRX, 1); g_trx =
s; s->irqs = irqs; s->clk_fd = -1; TRX_LOG(“=== Calypso hardware
init ===”);
memory_region_init_io(&s->dsp_iomem,NULL,&calypso_dsp_ops,s,"calypso.dsp_api",CALYPSO_DSP_SIZE);
memory_region_add_subregion(sysmem,CALYPSO_DSP_BASE,&s->dsp_iomem);
s->dsp_ram[DSP_DL_STATUS_ADDR/2]=DSP_DL_STATUS_READY; s->dsp_ram[DSP_API_VER_ADDR/2]=DSP_API_VERSION; s->dsp_booted=true;
memory_region_init_io(&s->tpu_iomem,NULL,&calypso_tpu_ops,s,"calypso.tpu",CALYPSO_TPU_SIZE);
memory_region_add_subregion(sysmem,CALYPSO_TPU_BASE,&s->tpu_iomem);
memory_region_init_io(&s->tpu_ram_iomem,NULL,&calypso_tpu_ram_ops,s,"calypso.tpu_ram",CALYPSO_TPU_RAM_SIZE);
memory_region_add_subregion(sysmem,CALYPSO_TPU_RAM_BASE,&s->tpu_ram_iomem);
memory_region_init_io(&s->tsp_iomem,NULL,&calypso_tsp_ops,s,"calypso.tsp",CALYPSO_TSP_SIZE);
memory_region_add_subregion(sysmem,CALYPSO_TSP_BASE,&s->tsp_iomem);
memory_region_init_io(&s->ulpd_iomem,NULL,&calypso_ulpd_ops,s,"calypso.ulpd",CALYPSO_ULPD_SIZE);
memory_region_add_subregion(sysmem,CALYPSO_ULPD_BASE,&s->ulpd_iomem);
s->sim = calypso_sim_new(s->irqs[CALYPSO_IRQ_SIM]);
memory_region_init_io(&s->sim_iomem,NULL,&calypso_sim_ops,s,"calypso.sim",CALYPSO_SIM_SIZE);
memory_region_add_subregion(sysmem,CALYPSO_SIM_BASE,&s->sim_iomem);
s->tdma_timer = timer_new_ns(calypso_tdma_clock(), calypso_tdma_tick, s);
s->dsp_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL,calypso_dsp_done,s);
s->frame_irq_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL,calypso_frame_irq_lower,s);
g_kick_timer = timer_new_ns(QEMU_CLOCK_REALTIME,calypso_kick_cb,NULL);
timer_mod_ns(g_kick_timer,qemu_clock_get_ns(QEMU_CLOCK_REALTIME)+5000000);
/* C54x DSP emulator — explicit ROM loading only.
*
* Two modes, both opt-in (no implicit/hardcoded ROM path anymore) :
* 1. Per-section (machine props dsp-prom0/prom1/prom2/prom3/drom/pdrom
* set via mb.c before sysbus_realize). Each section is written at
* its silicon-correct DSP address, BEFORE c54x_reset so the
* PROM→DARAM auto-copy sees the bytes.
* 2. DARAM blob (-M calypso,dsp-blob=<path>). No ROM is loaded; the
* blob in DARAM[0x100..] is the only DSP code. mb.c applies it
* after c54x_reset.
*
* If neither is set, the DSP runs with empty prog[]/data[]. No more
* legacy candidate-loop fallback (was: CALYPSO_DSP_ROM env + hardcoded
* /opt/GSM/calypso_dsp.txt). Use dsp_txt2bin.py to produce per-section
* .bin files from a legacy .txt dump if needed. */
{
s->dsp = c54x_init();
if (s->dsp) {
c54x_set_api_ram(s->dsp, s->dsp_ram);
bool have_sections = g_section_prom0 || g_section_prom1 ||
g_section_prom2 || g_section_prom3 ||
g_section_drom || g_section_pdrom;
const char *blob = getenv("CALYPSO_DSP_BLOB");
/* Blob wins over per-section: when both are set (shouldn't happen
* if run.sh is used, but defensive), the DARAM blob is the only
* code source, sections are ignored. The C54x emulator can't
* sensibly execute both at once. */
if (blob && *blob) {
TRX_LOG("DSP ROM mode: dsp-blob (CALYPSO_DSP_BLOB=%s) — "
"no ROM loaded, blob in DARAM is the only code", blob);
if (have_sections) {
TRX_LOG(" (per-section paths were also set but are "
"ignored — blob takes priority)");
}
} else if (have_sections) {
TRX_LOG("DSP ROM mode: explicit per-section loads");
if (g_section_prom0) {
c54x_load_section(s->dsp, g_section_prom0, 0x07000, true);
}
if (g_section_prom1) {
/* PROM1 = page 1, chargée en full-address 0x18000+
* (atteignable via XPC=1). PAS de mirror low-64K : la
* plage 0xE000-0xFFFF = PDROM (vecteurs IT), pas PROM1.
* (Fix 2026-05-29 : le mirror clobbait les vecteurs f4eb.) */
c54x_load_section(s->dsp, g_section_prom1, 0x18000, true);
}
if (g_section_prom2) {
c54x_load_section(s->dsp, g_section_prom2, 0x28000, true);
}
if (g_section_prom3) {
c54x_load_section(s->dsp, g_section_prom3, 0x38000, true);
}
if (g_section_drom) {
c54x_load_section(s->dsp, g_section_drom, 0x09000, false);
}
if (g_section_pdrom) {
/* PDROM = program-DATA ROM : visible côté DATA (0xE000+)
* ET côté PROGRAMME (page-0 haute 0xE000-0xFFFF) où vit la
* table de vecteurs IT (f4eb). Charge les deux espaces. */
c54x_load_section(s->dsp, g_section_pdrom, 0x0E000, false);
c54x_load_section(s->dsp, g_section_pdrom, 0x0E000, true);
}
} else {
TRX_LOG("DSP ROM mode: NONE — empty prog[]/data[]. "
"Use -M calypso,dsp-prom0=.. (et al.) or dsp-blob=..");
}
/* Reset + bsp_init: silicon-valid state regardless of ROM mode.
* machine_init may layer a DARAM blob via the dsp-blob hook
* after this returns. */
/* ROMMAP probe (CALYPSO_DEBUG=ROMMAP) : 4 cases qui tranchent
* le mapping vecteur IT (PDROM vs mirror PROM1) — pré-loader-fix. */
if (s->dsp && calypso_debug_enabled("ROMMAP"))
fprintf(stderr,
"[c54x] ROMMAP data[0x0ffcc]=0x%04x prog[0x0ffcc]=0x%04x "
"prog[0x1ffcc]=0x%04x prog[0x2ffcc]=0x%04x\n",
s->dsp->data[0x0ffcc], s->dsp->prog[0x0ffcc],
s->dsp->prog[0x1ffcc], s->dsp->prog[0x2ffcc]);
/* Register snapshot: load into reg_init[] BEFORE reset so
* c54x_reset() applies it as the authoritative MMR reset state
* (like the ROM sections above, but for the register file). */
if (g_section_registers)
c54x_load_registers(s->dsp, g_section_registers);
c54x_reset(s->dsp);
calypso_bsp_init(s->dsp);
}
}
TRX_LOG("=== Hardware ready ===");
/* CLK UDP: QEMU sends TDMA ticks to bridge on port 6700.
* Bridge is clock-slave — no independent timer.
*
* Le send est délégué à un pthread dédié (clk_master_thread) pour
* éviter le jitter ±20ms du QEMU mainloop dispatcher sur le
* tdma_timer callback. Le pthread utilise clock_nanosleep ABSTIME
* sur CLOCK_MONOTONIC → précision sub-µs au déclenchement, contre
* ~ms via QEMU timer. */
{
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd >= 0) {
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
s->clk_fd = fd;
memset(&s->clk_peer, 0, sizeof(s->clk_peer));
s->clk_peer.sin_family = AF_INET;
s->clk_peer.sin_port = htons(6700);
s->clk_peer.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
TRX_LOG("CLK UDP → bridge 127.0.0.1:6700");
/* Le pthread wall clk-master n'est démarré qu'en mode REALTIME.
* En VIRTUAL (défaut), c'est le tdma_tick qui envoie le CLK (FN
* virtuel-paced) — pas de pthread wall (sinon double-maître + drift). */
if (calypso_tdma_clock() == QEMU_CLOCK_REALTIME) {
calypso_trx_start_clk_master_thread(s);
TRX_LOG("clk-master wall pthread started (REALTIME mode)");
} else {
TRX_LOG("clk-master = tdma_tick virtual-paced (VIRTUAL mode, no wall pthread)");
}
}
}
}
================================================================================
FILE: hw/arm/calypso/calypso_tsp.c SIZE: 4906 bytes, 113 lines
================================================================================
/ calypso_tsp.c – Calypso DBB internal TSP (Time Serial Port)
device layer See calypso_tsp.h. Mirrors
osmocom-bb-transceiver’s calypso/tsp.c *
(tsp_write/tsp_setup/tsp_act_update) from the receiving end: decodes *
the MOVE instructions the TPU sequencer (calypso_tpu.c) hands us back *
into the higher-level TSP transactions the firmware originally issued.
[2026-07-23] Extracted from calypso_tpu.c per user direction
(split * mirrors the real firmware’s tpu.c/tsp.c separation;
“calypso_tsp.c * etc” – keep growing this as more radio-chip protocol
layers get * properly modeled instead of stubbed).
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_debug.h”
/* calypso_iota.c – the only downstream consumer wired up today
(TWL3025, * TSP device 0: BDLENA/BULENA burst-window enable byte). */
void calypso_iota_tsp_write(uint8_t data, uint8_t expected_tn);
#define TSP_LOG(fmt, …)
do { if (calypso_debug_enabled(“TPU”))
fprintf(stderr, “[calypso-tsp]” fmt “”, ##VA_ARGS); }
while (0)
/* Latched state, persists across TPU scenarios exactly like the real
* firmware’s statics (tsp.c’s tspact_state) and the real
TSP shift * registers (TX_n/CTRL1 stay loaded until the next MOVE
overwrites them, * independent of which TPU-RAM scenario wrote them).
/ static struct { uint8_t tx[4]; / TX_1..TX_4 -> tx[0..3]
/ uint8_t ctrl1; uint16_t act; / TSPACT enable-line state
(tsp_act_update()) */ } 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[0] = data; break; case TPUI_TX_2:
tsp.tx[1] = data; break; case TPUI_TX_3: tsp.tx[2] = data; break; case
TPUI_TX_4: tsp.tx[3] = data; break; case TPUI_TSP_CTRL1: tsp.ctrl1 =
data; break; case TPUI_TSP_CTRL2: if (data & TPUI_CTRL2_WR) { /*
tsp_write(dev_idx, bitlen, dout) in calypso/tsp.c: CTRL1 = *
(dev_idx<<5)|(bitlen-1), written just before this WR pulse. *
Reconstruct dout MSB-first from however many TX_n bytes * bitlen
actually spans. / 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]; if (dev_idx == 0) { / TWL3025 (audio/ABB companion):
BDLENA/BULENA control byte, * always bitlen<=8 in practice ->
tx[0] alone is correct * (matches the original single-byte
implementation). / calypso_iota_tsp_write(tsp.tx[0], 0); } else {
/ dev 1 = RF frontend (trf6151) : on suit le gain programme *
(REG_RX) pour caler le rxlev cote PM MEAS. /
calypso_trf6151_tsp_write(dev_idx, dout); TSP_LOG(“WR dev=%u bitlen=%u
dout=0x%08x fn=%u (trf6151 gain=%d)”, dev_idx, bitlen, dout, fn,
calypso_trf6151_total_gain_db()); } } break; case TPUI_TSP_ACT_L: if
(data != (tsp.act & 0xff)) { tsp.act = (tsp.act & 0xff00) |
data; TSP_LOG(“ACT_L -> 0x%02x (tspact=0x%04x) fn=%u”, data, tsp.act,
fn); } break; case TPUI_TSP_ACT_U: if (data != (tsp.act >> 8)) {
tsp.act = (tsp.act & 0x00ff) | ((uint16_t)data << 8);
TSP_LOG(“ACT_U -> 0x%02x (tspact=0x%04x) fn=%u”, data, tsp.act, fn);
} break; case TPUI_TSP_SET1: case TPUI_TSP_SET2: case TPUI_TSP_SET3:
/ tsp_setup(): static clock-edge/CS-polarity config, not part of *
the runtime burst-timing path. Logged for visibility only. */
TSP_LOG(“SET%d <- 0x%02x fn=%u”, addr - TPUI_TSP_SET1 + 1, data, fn);
break; default: TSP_LOG(“move to unowned addr=0x%02x data=0x%02x fn=%u”,
addr, data, fn); break; } }
================================================================================
FILE: hw/arm/calypso/calypso_twl3025.c SIZE: 10561 bytes, 232 lines
================================================================================
/ calypso_twl3025.c — TWL3025 (Iota) ABB AFC chip model
Modèle minimal du TWL3025 (Analog Baseband) pour propager l’AFC du
* firmware vers les samples BSP en quasi-temps réel. Chaîne
silicon : * firmware afc_load_dsp() écrit dsp_api.db_w->d_afc =
dac_value * DSP lit d_afc et le serialise vers TWL3025 via TSP * TWL3025
décode le register write → AFC DAC (13 bits, ±4096) * DAC pilote VCXO 13
MHz → décalage fréquence baseband En QEMU : * -
calypso_twl3025_set_afc_dac() appelé depuis la chaîne TSP avec * la
valeur DAC écrite par le firmware * - calypso_twl3025_apply_phase()
rotation des samples BSP par * dac_value × afc_slope (Hz), au taux *
baseband 270.833 kHz, avec offset * déterministe basé sur FN/TN
Le modèle est ARMÉ PAR DÉFAUT (= fait son taf chip-level, pas
d’env * gate). Override via CALYPSO_TWL3025_AFC_HZ=N pour injecter un
offset * constant (= diag, court-circuite la chaîne DAC).
SPDX-License-Identifier: GPL-2.0-or-later */
#include “qemu/osdep.h” #include <math.h>
#include “hw/arm/calypso/calypso_twl3025.h” #include
“hw/arm/calypso/calypso_debug.h”
#ifndef M_PI #define M_PI 3.14159265358979323846 #endif
/* TWL3025 / Compal E88 vcxocal constants (board gta0x/afcparams.c
osmocom-bb) : * afc_slope = 454 (entier OsmocomBB, board E88/Openmoko) *
afc_initial_dac_value = -700 (= calibration board E88, compense le *
trim du quartz physique) * d_afc range : ±4095 (13-bit signed DAC)
En QEMU les samples osmo-bts arrivent déjà à la freq carrier
exacte * (= comme si le silicon avait DEJA appliqué la calibration
-700). Donc * on interprète le DAC firmware relativement à la
calibration baseline : * effective_dac = dac_value -
AFC_INITIAL_DAC_VALUE * DAC=-700 (= calib idéale) → effective=0 → no
rotation * DAC=0 (= “+700 LSB au-dessus de la calib”) → effective=+700 →
+200900Hz / / FIX 2026-05-30 (AFC convergence = dernier mur
FBSB) : la sensibilité VCXO * physique DOIT être cohérente avec la
boucle firmware osmocom afc_correct() : * delta_LSB =
(AFC_NORM_FACTOR_GSM × freq_error_Hz) / afc_slope * AFC_NORM_FACTOR_GSM
= 2^15/947 = 34.6 ; afc_slope = 454 (board E88) * Boucle stable (gain≈1)
⟺ VCXO = afc_slope / norm_factor = 454/(2^15/947) * ≈ 13.12 Hz/LSB. *
L’ancienne 287.0 donnait un gain de boucle ~22× → sur-correction → le
DAC * oscille (-700↔︎-162) → rotation FCCH énorme (±150 kHz) hors capture
DSP * (±20 kHz) → FB jamais détecté/accepté → mur FBSB. / #define
TWL3025_AFC_NORM_FACTOR_GSM (32768.0 / 947.0) #define TWL3025_AFC_SLOPE
287.0 / [fix] compal_e88 = 287, PAS 454 (gta0x) -> gain boucle
~1 / #define TWL3025_AFC_SLOPE_HZ_PER_LSB (TWL3025_AFC_SLOPE /
TWL3025_AFC_NORM_FACTOR_GSM) / ⚠️ TESTING 2026-05-29 v2 : baseline
-700 + init dac=-700 (cal Compal/Pirelli). * osmocom : afc_reset() ->
dac=afc_initial_dac_value(-700) PUIS afc_correct * CONVERGE. Le modèle
DOIT démarrer à -700 (effective=0, pas de rotation) et * tourner avec le
firmware. Bug v1 : twl.dac_value=0 au boot + baseline -700 * ->
effective=+700 = +200kHz spurious AVANT que le firmware charge sa cal
-> * FCCH hors capture DSP (±20kHz) -> jamais détecté. Fix : init
dac=-700 dans * twl3025_lazy_env -> effective=0 au boot ; set_afc_dac
filtre les writes 0 du * firmware (garde -700) ; converge dès que
afc_correct ajuste le DAC. / #define TWL3025_AFC_INITIAL_DAC_VALUE
(-700) #define GSM_SAMPLE_RATE_HZ 270833.0 / 1 sps GSM baseband
*/
/* GSM TDMA timing constants (1 sample/bit BSP rate) : * 1 frame =
4.615 ms = 1250 samples * 1 slot = 156.25 symbols ≈ 156 samples (rounded
down) */ #define SAMPLES_PER_FRAME 1250U #define SAMPLES_PER_SLOT
156U
static struct { int16_t dac_value; /* current DAC reg value (=
dernière écriture firmware) / int force_hz; /
CALYPSO_TWL3025_AFC_HZ override (diag, default 0 = off) / bool
env_loaded; int afc_enabled; / CALYPSO_TWL3025_AFC (défaut 1=on, =0
désactive la rotation AFC) / uint64_t dac_writes; / compteur
diag */ 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, VALEUR ; 0 = inerte) * masque :
la boucle AFC fermee (DAC firmware -> pente Hz/LSB -> rotation des
* samples). Une valeur non nulle fige un offset constant en Hz et *
supprime la convergence. * retirer : jamais necessaire — mettre 0 (ou
unset) suffit ; c’est un outil de * diagnostic. NB : calypso_hack.env la
pose a 0 ET l’exporte : * inoffensive en valeur, mais PRESENTE dans
l’environnement. / const char h =
getenv(“CALYPSO_TWL3025_AFC_HZ”); twl.force_hz = (h && h) ?
atoi(h) : 0; / Gate maître de la boucle AFC (rotation des samples
RX par l’offset VCXO). * Défaut ON (=1) : l’AFC fait partie du chemin
nominal osmocom. Opt-out * CALYPSO_TWL3025_AFC=0 pour livrer l’I/Q brute
(debug, AFC désactivée). / const char ae =
getenv(“CALYPSO_TWL3025_AFC”); twl.afc_enabled = (ae && ae
== ‘0’) ? 0 : 1; / Init DAC au point de calibration (-700) = “VCXO
nominal” en QEMU : * le firmware démarre son AFC à afc_initial_dac_value
puis converge. * Sans ça (dac=0 au boot) le modèle voit +700 LSB =
+200kHz spurious. */ twl.dac_value = TWL3025_AFC_INITIAL_DAC_VALUE;
fprintf(stderr, “[twl3025] chip model armed (slope=%.1f Hz/LSB, AFC=%s,
force_hz=%d)”, 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 dual-write 0-clobber pattern : firmware writes d_afc to
* BOTH dsp_api pages each frame (page A=0 inherited init, page B=
* real value). Sur silicon, le TWL3025 register est sticky via
* TSP serialization — le 0 transient n'a pas le temps de propager
* avant le -700 effectif. Notre modèle voit les 2 writes en MMIO
* direct → oscillation 0↔-700.
*
* Heuristique : ignorer un write dac=0 si la valeur courante est
* déjà set (non-zero). Le firmware reset légitime via afc_reset
* écrit afc_initial_dac_value (=-700 sur Compal E88), pas 0. */
if (dac_value == 0 && twl.dac_value != 0) {
return;
}
if (twl.dac_value != dac_value) {
twl.dac_writes++;
/* Throttled log : initial 20 writes puis toutes les 1000 pour
* visualiser convergence sans noyer le log AFC. */
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 =
écriture firmware - calibration baseline. * Cf comment en tête du
fichier sur la sémantique baseline. / int32_t effective_dac =
(int32_t)twl.dac_value - TWL3025_AFC_INITIAL_DAC_VALUE; return
(double)effective_dac TWL3025_AFC_SLOPE_HZ_PER_LSB; }
double calypso_twl3025_get_afc_phase_step(void) { twl3025_lazy_env();
if (!twl.afc_enabled) return 0.0; /* CALYPSO_TWL3025_AFC=0 → pas de
rotation AFC / double hz = calypso_twl3025_get_afc_hz(); if (hz ==
0.0) return 0.0; / Phase step per sample = 2π × freq / fs. * Signe
: VCXO + → osc UP → received baseband freq DOWN (down-conv). *
Compensation = rotation samples par -phase_step. / return -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();
/* ⚠️ NON-DÉFINITIF / TESTING 2026-05-29 : marqueur — confirme si CETTE
* fonction tourne en boucle et avec quel offset (hz). Si elle applique
* un offset énorme (ex +200900 Hz) sur chaque burst → elle tue la FCCH
* (capture DSP ±20 kHz) → FB jamais détecté → boucle FBSB. */
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=0 et pas de force_hz : no-op */
/* Sample offset absolu depuis FN=0,TN=0 = phase de reference système.
* Permet la continuité phase entre bursts : burst N+1 starts at
* (N+1) × 1250 + tn × 156, donc cos/sin restent cohérents. */
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 gardé : on ne recharge pas l’env
au reset (état chip). */ }
================================================================================
FILE: hw/arm/calypso/doc/opcodes/tic54x-opc.c SIZE: 26523 bytes, 496
lines
================================================================================
/* Table of opcodes for the Texas Instruments TMS320C54X Copyright 1999,
2000, 2001, 2005, 2007, 2009 Free Software Foundation, Inc. Contributed
by Timothy Wall (twall@cygnus.com)
This file is part of the GNU opcodes library.
This library 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 3, or (at your option) any
later version.
It 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.
You should have received a copy of the GNU General Public License
along with this file; see the file COPYING. If not, write to the Free
Software Foundation, 51 Franklin Street - Fifth Floor, Boston, MA
02110-1301, USA. */
#include “sysdep.h” #include “dis-asm.h” #include
“opcode/tic54x.h”
/* these are the only register names not found in mmregs */ const
symbol regs[] = { { “AR0”, 16 }, { “ar0”, 16 }, { “AR1”, 17 }, { “ar1”,
17 }, { “AR2”, 18 }, { “ar2”, 18 }, { “AR3”, 19 }, { “ar3”, 19 }, {
“AR4”, 20 }, { “ar4”, 20 }, { “AR5”, 21 }, { “ar5”, 21 }, { “AR6”, 22 },
{ “ar6”, 22 }, { “AR7”, 23 }, { “ar7”, 23 }, { NULL, 0} };
/* status bits, MM registers, condition codes, etc / / some
symbols are only valid for certain chips… / const symbol mmregs[] =
{ { “IMR”, 0 }, { “imr”, 0 }, { “IFR”, 1 }, { “ifr”, 1 }, { “ST0”, 6 },
{ “st0”, 6 }, { “ST1”, 7 }, { “st1”, 7 }, { “AL”, 8 }, { “al”, 8 }, {
“AH”, 9 }, { “ah”, 9 }, { “AG”, 10 }, { “ag”, 10 }, { “BL”, 11 }, {
“bl”, 11 }, { “BH”, 12 }, { “bh”, 12 }, { “BG”, 13 }, { “bg”, 13 }, {
“T”, 14 }, { “t”, 14 }, { “TRN”, 15 }, { “trn”, 15 }, { “AR0”, 16 }, {
“ar0”, 16 }, { “AR1”, 17 }, { “ar1”, 17 }, { “AR2”, 18 }, { “ar2”, 18 },
{ “AR3”, 19 }, { “ar3”, 19 }, { “AR4”, 20 }, { “ar4”, 20 }, { “AR5”, 21
}, { “ar5”, 21 }, { “AR6”, 22 }, { “ar6”, 22 }, { “AR7”, 23 }, { “ar7”,
23 }, { “SP”, 24 }, { “sp”, 24 }, { “BK”, 25 }, { “bk”, 25 }, { “BRC”,
26 }, { “brc”, 26 }, { “RSA”, 27 }, { “rsa”, 27 }, { “REA”, 28 }, {
“rea”, 28 }, { “PMST”,29 }, { “pmst”,29 }, { “XPC”, 30 }, { “xpc”, 30 },
/ ’c548 only / / optional peripherals / /
optional peripherals / { “M1F”, 31 }, { “m1f”, 31 }, { “DRR0”,0x20
}, { “drr0”,0x20 }, { “BDRR0”,0x20 }, { “bdrr0”,0x20 }, / ’c543,
545 / { “DXR0”,0x21 }, { “dxr0”,0x21 }, { “BDXR0”,0x21 }, {
“bdxr0”,0x21 }, / ’c543, 545 / { “SPC0”,0x22 }, { “spc0”,0x22
}, { “BSPC0”,0x22 }, { “bspc0”,0x22 }, / ’c543, 545 / {
“SPCE0”,0x23 }, { “spce0”,0x23 }, { “BSPCE0”,0x23 }, { “bspce0”,0x23 },
/ ’c543, 545 / { “TIM”, 0x24 }, { “tim”, 0x24 }, { “PRD”, 0x25
}, { “prd”, 0x25 }, { “TCR”, 0x26 }, { “tcr”, 0x26 }, { “SWWSR”,0x28 },
{ “swwsr”,0x28 }, { “BSCR”,0x29 }, { “bscr”,0x29 }, { “HPIC”,0x2C }, {
“hpic”,0x2c }, / ’c541, ’c545 / / ’c541, ’c545 / {
“DRR1”,0x30 }, { “drr1”,0x30 }, { “DXR1”,0x31 }, { “dxr1”,0x31 }, {
“SPC1”,0x32 }, { “spc1”,0x32 }, / ’c542, ’c543 / / ’c542,
’c543 / { “TRCV”,0x30 }, { “trcv”,0x30 }, { “TDXR”,0x31 }, {
“tdxr”,0x31 }, { “TSPC”,0x32 }, { “tspc”,0x32 }, { “TCSR”,0x33 }, {
“tcsr”,0x33 }, { “TRTA”,0x34 }, { “trta”,0x34 }, { “TRAD”,0x35 }, {
“trad”,0x35 }, { “AXR0”,0x38 }, { “axr0”,0x38 }, { “BKX0”,0x39 }, {
“bkx0”,0x39 }, { “ARR0”,0x3A }, { “arr0”,0x3a }, { “BKR0”,0x3B }, {
“bkr0”,0x3b }, / ’c545, ’c546, ’c548 / / ’c545, ’c546,
’c548 / { “CLKMD”,0x58 }, { “clkmd”,0x58 }, / ’c548 /
/ ’c548 */ { “AXR1”,0x3C }, { “axr1”,0x3c }, { “BKX1”,0x3D }, {
“bkx1”,0x3d }, { “ARR1”,0x3E }, { “arr1”,0x3e }, { “BKR1”,0x3F }, {
“bkr1”,0x3f }, { “BDRR1”,0x40 }, { “bdrr1”,0x40 }, { “BDXR1”,0x41 }, {
“bdxr1”,0x41 }, { “BSPC1”,0x42 }, { “bspc1”,0x42 }, { “BSPCE1”,0x43 }, {
“bspce1”,0x43 }, { NULL, 0}, };
const symbol condition_codes[] = { /* condition codes */ { “UNC”, 0
}, { “unc”, 0 }, #define CC1 0x40 #define CCB 0x08 #define CCEQ 0x05
#define CCNEQ 0x04 #define CCLT 0x03 #define CCLEQ 0x07 #define CCGT
0x06 #define CCGEQ 0x02 #define CCOV 0x70 #define CCNOV 0x60 #define
CCBIO 0x03 #define CCNBIO 0x02 #define CCTC 0x30 #define CCNTC 0x20
#define CCC 0x0C #define CCNC 0x08 { “aeq”, CC1|CCEQ }, { “AEQ”,
CC1|CCEQ }, { “aneq”, CC1|CCNEQ }, { “ANEQ”, CC1|CCNEQ }, { “alt”,
CC1|CCLT }, { “ALT”, CC1|CCLT }, { “aleq”, CC1|CCLEQ }, { “ALEQ”,
CC1|CCLEQ }, { “agt”, CC1|CCGT }, { “AGT”, CC1|CCGT }, { “ageq”,
CC1|CCGEQ }, { “AGEQ”, CC1|CCGEQ }, { “aov”, CC1|CCOV }, { “AOV”,
CC1|CCOV }, { “anov”, CC1|CCNOV }, { “ANOV”, CC1|CCNOV }, { “beq”,
CC1|CCB|CCEQ }, { “BEQ”, CC1|CCB|CCEQ }, { “bneq”, CC1|CCB|CCNEQ }, {
“BNEQ”, CC1|CCB|CCNEQ }, { “blt”, CC1|CCB|CCLT }, { “BLT”, CC1|CCB|CCLT
}, { “bleq”, CC1|CCB|CCLEQ }, { “BLEQ”, CC1|CCB|CCLEQ }, { “bgt”,
CC1|CCB|CCGT }, { “BGT”, CC1|CCB|CCGT }, { “bgeq”, CC1|CCB|CCGEQ }, {
“BGEQ”, CC1|CCB|CCGEQ }, { “bov”, CC1|CCB|CCOV }, { “BOV”, CC1|CCB|CCOV
}, { “bnov”, CC1|CCB|CCNOV }, { “BNOV”, CC1|CCB|CCNOV }, { “tc”, CCTC },
{ “TC”, CCTC }, { “ntc”, CCNTC }, { “NTC”, CCNTC }, { “c”, CCC }, { “C”,
CCC }, { “nc”, CCNC }, { “NC”, CCNC }, { “bio”, CCBIO }, { “BIO”, CCBIO
}, { “nbio”, CCNBIO }, { “NBIO”, CCNBIO }, { NULL, 0 } };
const symbol cc2_codes[] = { { “UNC”, 0 }, { “unc”, 0 }, { “AEQ”, 5
}, { “aeq”, 5 }, { “ANEQ”, 4 }, { “aneq”, 4 }, { “AGT”, 6 }, { “agt”, 6
}, { “ALT”, 3 }, { “alt”, 3 }, { “ALEQ”, 7 }, { “aleq”, 7 }, { “AGEQ”, 2
}, { “ageq”, 2 }, { “BEQ”, 13 }, { “beq”, 13 }, { “BNEQ”, 12 },{ “bneq”,
12 }, { “BGT”, 14 }, { “bgt”, 14 }, { “BLT”, 11 }, { “blt”, 11 }, {
“BLEQ”, 15 },{ “bleq”, 15 }, { “BGEQ”, 10 },{ “bgeq”, 10 }, { NULL, 0 },
};
const symbol cc3_codes[] = { { “EQ”, 0x0000 }, { “eq”, 0x0000 }, {
“LT”, 0x0100 }, { “lt”, 0x0100 }, { “GT”, 0x0200 }, { “gt”, 0x0200 }, {
“NEQ”, 0x0300 }, { “neq”, 0x0300 }, { “0”, 0x0000 }, { “1”, 0x0100 }, {
“2”, 0x0200 }, { “3”, 0x0300 }, { “00”, 0x0000 }, { “01”, 0x0100 }, {
“10”, 0x0200 }, { “11”, 0x0300 }, { NULL, 0 }, };
/* FIXME – also allow decimal digits / const symbol status_bits[]
= { / status register 0 / { “TC”, 12 }, { “tc”, 12 }, { “C”, 11
}, { “c”, 11 }, { “OVA”, 10 }, { “ova”, 10 }, { “OVB”, 9 }, { “ovb”, 9
}, / status register 1 */ { “BRAF”,15 }, { “braf”,15 }, { “CPL”, 14
}, { “cpl”, 14 }, { “XF”, 13 }, { “xf”, 13 }, { “HM”, 12 }, { “hm”, 12
}, { “INTM”,11 }, { “intm”,11 }, { “OVM”, 9 }, { “ovm”, 9 }, { “SXM”, 8
}, { “sxm”, 8 }, { “C16”, 7 }, { “c16”, 7 }, { “FRCT”, 6 }, { “frct”, 6
}, { “CMPT”, 5 }, { “cmpt”, 5 }, { NULL, 0 }, };
const char *misc_symbols[] = { “ARP”, “arp”, “DP”, “dp”, “ASM”,
“asm”, “TS”, “ts”, NULL };
/* Due to the way instructions are hashed and scanned in
gas/config/tc-tic54x.c, all identically-named opcodes must be
consecutively placed
Items marked with “PREFER” have been moved prior to a more costly
instruction with a similar operand format.
Mnemonics which can take either a predefined symbol or a memory
reference as an argument are arranged so that the more restrictive
(predefined symbol) version is checked first (marked “SRC”). /
#define ZPAR 0,{OP_None} #define REST 0,0,ZPAR #define XREST ZPAR const
insn_template tic54x_unknown_opcode = { “???”, 1,0,0,0x0000, 0x0000,
{0}, 0, REST}; const insn_template tic54x_optab[] = { / these must
precede bc/bcd, cc/ccd to avoid misinterpretation */ { “fb”,
2,1,1,0xF880, 0xFF80, {OP_xpmad}, B_BRANCH|FL_FAR|FL_NR, REST}, { “fbd”,
2,1,1,0xFA80, 0xFF80, {OP_xpmad}, B_BRANCH|FL_FAR|FL_DELAY|FL_NR, REST},
{ “fcall”, 2,1,1,0xF980, 0xFF80, {OP_xpmad}, B_BRANCH|FL_FAR|FL_NR,
REST}, { “fcalld”,2,1,1,0xFB80, 0xFF80, {OP_xpmad},
B_BRANCH|FL_FAR|FL_DELAY|FL_NR, REST},
{ “abdst”, 1,2,2,0xE300, 0xFF00, {OP_Xmem,OP_Ymem}, 0, REST}, {
“abs”, 1,1,2,0xF485, 0xFCFF, {OP_SRC,OPT|OP_DST}, 0, REST}, { “add”,
1,1,3,0xF400, 0xFCE0, {OP_SRC,OPT|OP_SHIFT,OPT|OP_DST}, 0,
REST},/SRC/ { “add”, 1,2,3,0xF480, 0xFCFF,
{OP_SRC,OP_ASM,OPT|OP_DST}, 0, REST},/SRC/ { “add”,
1,2,2,0x0000, 0xFE00, {OP_Smem,OP_SRC1}, FL_SMR, REST}, { “add”,
1,3,3,0x0400, 0xFE00, {OP_Smem,OP_TS,OP_SRC1}, FL_SMR, REST}, { “add”,
1,3,4,0x3C00, 0xFC00, {OP_Smem,OP_16,OP_SRC,OPT|OP_DST}, FL_SMR, REST},
{ “add”, 1,3,3,0x9000, 0xFE00, {OP_Xmem,OP_SHFT,OP_SRC1}, 0,
REST},/PREFER/ { “add”, 2,2,4,0x6F00, 0xFF00,
{OP_Smem,OPT|OP_SHIFT,OP_SRC,OPT|OP_DST}, FL_EXT|FL_SMR, 0x0C00, 0xFCE0,
XREST}, { “add”, 1,3,3,0xA000, 0xFE00, {OP_Xmem,OP_Ymem,OP_DST}, 0,
REST}, { “add”, 2,2,4,0xF000, 0xFCF0,
{OP_lk,OPT|OP_SHIFT,OP_SRC,OPT|OP_DST}, 0, REST}, { “add”, 2,3,4,0xF060,
0xFCFF, {OP_lk,OP_16,OP_SRC,OPT|OP_DST}, 0, REST}, { “addc”,
1,2,2,0x0600, 0xFE00, {OP_Smem,OP_SRC1}, FL_SMR, REST}, { “addm”,
2,2,2,0x6B00, 0xFF00, {OP_lk,OP_Smem}, FL_NR|FL_SMR, REST}, { “adds”,
1,2,2,0x0200, 0xFE00, {OP_Smem,OP_SRC1}, FL_SMR, REST}, { “and”,
1,1,3,0xF080, 0xFCE0, {OP_SRC,OPT|OP_SHIFT,OPT|OP_DST}, 0, REST}, {
“and”, 1,2,2,0x1800, 0xFE00, {OP_Smem,OP_SRC1}, FL_SMR, REST }, { “and”,
2,2,4,0xF030, 0xFCF0, {OP_lk,OPT|OP_SHFT,OP_SRC,OPT|OP_DST}, 0, REST}, {
“and”, 2,3,4,0xF063, 0xFCFF, {OP_lk,OP_16,OP_SRC,OPT|OP_DST}, 0, REST},
{ “andm”, 2,2,2,0x6800, 0xFF00, {OP_lk,OP_Smem}, FL_NR, REST}, { “b”,
2,1,1,0xF073, 0xFFFF, {OP_pmad}, B_BRANCH|FL_NR, REST}, { “bd”,
2,1,1,0xF273, 0xFFFF, {OP_pmad}, B_BRANCH|FL_DELAY|FL_NR, REST}, {
“bacc”, 1,1,1,0xF4E2, 0xFEFF, {OP_SRC1}, B_BACC|FL_NR, REST}, { “baccd”,
1,1,1,0xF6E2, 0xFEFF, {OP_SRC1}, B_BACC|FL_DELAY|FL_NR, REST}, { “banz”,
2,2,2,0x6C00, 0xFF00, {OP_pmad,OP_Sind}, B_BRANCH|FL_NR, REST}, {
“banzd”, 2,2,2,0x6E00, 0xFF00, {OP_pmad,OP_Sind},
B_BRANCH|FL_DELAY|FL_NR, REST}, { “bc”, 2,2,4,0xF800, 0xFF00,
{OP_pmad,OP_CC,OPT|OP_CC,OPT|OP_CC}, B_BRANCH|FL_NR, REST}, { “bcd”,
2,2,4,0xFA00, 0xFF00, {OP_pmad,OP_CC,OPT|OP_CC,OPT|OP_CC},
B_BRANCH|FL_DELAY|FL_NR, REST}, { “bit”, 1,2,2,0x9600, 0xFF00,
{OP_Xmem,OP_BITC}, 0, REST}, { “bitf”, 2,2,2,0x6100, 0xFF00,
{OP_Smem,OP_lk}, FL_SMR, REST}, { “bitt”, 1,1,1,0x3400, 0xFF00,
{OP_Smem}, FL_SMR, REST}, { “cala”, 1,1,1,0xF4E3, 0xFEFF, {OP_SRC1},
B_BACC|FL_NR, REST}, { “calad”, 1,1,1,0xF6E3, 0xFEFF, {OP_SRC1},
B_BACC|FL_DELAY|FL_NR, REST}, { “call”, 2,1,1,0xF074, 0xFFFF, {OP_pmad},
B_BRANCH|FL_NR, REST}, { “calld”, 2,1,1,0xF274, 0xFFFF, {OP_pmad},
B_BRANCH|FL_DELAY|FL_NR, REST}, { “cc”, 2,2,4,0xF900, 0xFF00,
{OP_pmad,OP_CC,OPT|OP_CC,OPT|OP_CC}, B_BRANCH|FL_NR, REST}, { “ccd”,
2,2,4,0xFB00, 0xFF00, {OP_pmad,OP_CC,OPT|OP_CC,OPT|OP_CC},
B_BRANCH|FL_DELAY|FL_NR, REST}, { “cmpl”, 1,1,2,0xF493, 0xFCFF,
{OP_SRC,OPT|OP_DST}, 0, REST}, { “cmpm”, 2,2,2,0x6000, 0xFF00,
{OP_Smem,OP_lk}, FL_SMR, REST}, { “cmpr”, 1,2,2,0xF4A8, 0xFCF8,
{OP_CC3,OP_ARX}, FL_NR, REST}, { “cmps”, 1,2,2,0x8E00, 0xFE00,
{OP_SRC1,OP_Smem}, 0, REST}, { “dadd”, 1,2,3,0x5000, 0xFC00,
{OP_Lmem,OP_SRC,OPT|OP_DST}, 0, REST}, { “dadst”, 1,2,2,0x5A00, 0xFE00,
{OP_Lmem,OP_DST}, 0, REST}, { “delay”, 1,1,1,0x4D00, 0xFF00, {OP_Smem},
FL_SMR, REST}, { “dld”, 1,2,2,0x5600, 0xFE00, {OP_Lmem,OP_DST}, 0,
REST}, { “drsub”, 1,2,2,0x5800, 0xFE00, {OP_Lmem,OP_SRC1}, 0, REST}, {
“dsadt”, 1,2,2,0x5E00, 0xFE00, {OP_Lmem,OP_DST}, 0, REST}, { “dst”,
1,2,2,0x4E00, 0xFE00, {OP_SRC1,OP_Lmem}, FL_NR, REST}, { “dsub”,
1,2,2,0x5400, 0xFE00, {OP_Lmem,OP_SRC1}, 0, REST}, { “dsubt”,
1,2,2,0x5C00, 0xFE00, {OP_Lmem,OP_DST}, 0, REST}, { “estop”,
1,0,0,0xF4F0, 0xFFFF, {OP_None}, 0, REST}, /* undocumented / {
“exp”, 1,1,1,0xF48E, 0xFEFF, {OP_SRC1}, 0, REST}, { “fbacc”,
1,1,1,0xF4E6, 0xFEFF, {OP_SRC1}, B_BACC|FL_FAR|FL_NR, REST}, {
“fbaccd”,1,1,1,0xF6E6, 0xFEFF, {OP_SRC1}, B_BACC|FL_FAR|FL_DELAY|FL_NR,
REST}, { “fcala”, 1,1,1,0xF4E7, 0xFEFF, {OP_SRC1}, B_BACC|FL_FAR|FL_NR,
REST}, { “fcalad”,1,1,1,0xF6E7, 0xFEFF, {OP_SRC1},
B_BACC|FL_FAR|FL_DELAY|FL_NR, REST}, { “firs”, 2,3,3,0xE000, 0xFF00,
{OP_Xmem,OP_Ymem,OP_pmad}, 0, REST}, { “frame”, 1,1,1,0xEE00, 0xFF00,
{OP_k8}, 0, REST}, { “fret”, 1,0,0,0xF4E4, 0xFFFF, {OP_None},
B_RET|FL_FAR|FL_NR, REST}, { “fretd”, 1,0,0,0xF6E4, 0xFFFF, {OP_None},
B_RET|FL_FAR|FL_DELAY|FL_NR, REST}, { “frete”, 1,0,0,0xF4E5, 0xFFFF,
{OP_None}, B_RET|FL_FAR|FL_NR, REST}, { “freted”,1,0,0,0xF6E5, 0xFFFF,
{OP_None}, B_RET|FL_FAR|FL_DELAY|FL_NR, REST}, { “idle”, 1,1,1,0xF4E1,
0xFCFF, {OP_123}, FL_NR, REST}, { “intr”, 1,1,1,0xF7C0, 0xFFE0,
{OP_031}, B_BRANCH|FL_NR, REST}, { “ld”, 1,2,3,0xF482, 0xFCFF,
{OP_SRC,OP_ASM,OPT|OP_DST}, 0, REST},/SRC/ { “ld”,
1,2,3,0xF440, 0xFCE0, {OP_SRC,OPT|OP_SHIFT,OP_DST}, 0,
REST},/SRC/ / alternate syntax / { “ld”, 1,2,3,0xF440,
0xFCE0, {OP_SRC,OP_SHIFT,OPT|OP_DST}, 0, REST},/SRC/ { “ld”,
1,2,2,0xE800, 0xFE00, {OP_k8u,OP_DST}, 0, REST},/SRC/ { “ld”,
1,2,2,0xED00, 0xFFE0, {OP_k5,OP_ASM}, 0, REST},/SRC/ { “ld”,
1,2,2,0xF4A0, 0xFFF8, {OP_k3,OP_ARP}, FL_NR, REST},/SRC/ {
“ld”, 1,2,2,0xEA00, 0xFE00, {OP_k9,OP_DP}, FL_NR, REST},/PREFER
/ { “ld”, 1,2,2,0x3000, 0xFF00, {OP_Smem,OP_T}, FL_SMR,
REST},/SRC/ { “ld”, 1,2,2,0x4600, 0xFF00, {OP_Smem,OP_DP},
FL_SMR, REST},/SRC/ { “ld”, 1,2,2,0x3200, 0xFF00,
{OP_Smem,OP_ASM}, FL_SMR, REST},/SRC/ { “ld”, 1,2,2,0x1000,
0xFE00, {OP_Smem,OP_DST}, FL_SMR, REST}, { “ld”, 1,3,3,0x1400, 0xFE00,
{OP_Smem,OP_TS,OP_DST}, FL_SMR, REST}, { “ld”, 1,3,3,0x4400, 0xFE00,
{OP_Smem,OP_16,OP_DST}, FL_SMR, REST}, { “ld”, 1,3,3,0x9400, 0xFE00,
{OP_Xmem,OP_SHFT,OP_DST}, 0, REST},/PREFER/ { “ld”,
2,2,3,0x6F00, 0xFF00, {OP_Smem,OPT|OP_SHIFT,OP_DST}, FL_EXT|FL_SMR,
0x0C40, 0xFEE0, XREST}, { “ld”, 2,2,3,0xF020, 0xFEF0,
{OP_lk,OPT|OP_SHFT,OP_DST}, 0, REST}, { “ld”, 2,3,3,0xF062, 0xFEFF,
{OP_lk,OP_16,OP_DST}, 0, REST}, { “ldm”, 1,2,2,0x4800, 0xFE00,
{OP_MMR,OP_DST}, 0, REST}, { “ldr”, 1,2,2,0x1600, 0xFE00,
{OP_Smem,OP_DST}, FL_SMR, REST}, { “ldu”, 1,2,2,0x1200, 0xFE00,
{OP_Smem,OP_DST}, FL_SMR, REST}, { “ldx”, 2,3,3,0xF062, 0xFEFF,
{OP_xpmad_ms7,OP_16,OP_DST}, FL_FAR, REST},/pseudo-op/ { “lms”,
1,2,2,0xE100, 0xFF00, {OP_Xmem,OP_Ymem}, 0, REST}, { “ltd”,
1,1,1,0x4C00, 0xFF00, {OP_Smem}, FL_SMR, REST}, { “mac”, 1,2,2,0x2800,
0xFE00, {OP_Smem,OP_SRC1}, FL_SMR, REST}, { “mac”, 1,3,4,0xB000, 0xFC00,
{OP_Xmem,OP_Ymem,OP_SRC,OPT|OP_DST}, 0, REST}, { “mac”, 2,2,3,0xF067,
0xFCFF, {OP_lk,OP_SRC,OPT|OP_DST}, 0, REST}, { “mac”, 2,3,4,0x6400,
0xFC00, {OP_Smem,OP_lk,OP_SRC,OPT|OP_DST}, FL_SMR, REST}, { “macr”,
1,2,2,0x2A00, 0xFE00, {OP_Smem,OP_SRC1}, FL_SMR, REST}, { “macr”,
1,3,4,0xB400, 0xFC00, {OP_Xmem,OP_Ymem,OP_SRC,OPT|OP_DST},FL_SMR, REST},
{ “maca”, 1,2,3,0xF488, 0xFCFF, {OP_T,OP_SRC,OPT|OP_DST}, FL_SMR,
REST},/SRC/ { “maca”, 1,1,2,0x3500, 0xFF00, {OP_Smem,OPT|OP_B},
FL_SMR, REST}, { “macar”, 1,2,3,0xF489, 0xFCFF,
{OP_T,OP_SRC,OPT|OP_DST}, FL_SMR, REST},/SRC/ { “macar”,
1,1,2,0x3700, 0xFF00, {OP_Smem,OPT|OP_B}, FL_SMR, REST}, { “macd”,
2,3,3,0x7A00, 0xFE00, {OP_Smem,OP_pmad,OP_SRC1}, FL_SMR, REST}, {
“macp”, 2,3,3,0x7800, 0xFE00, {OP_Smem,OP_pmad,OP_SRC1}, FL_SMR, REST},
{ “macsu”, 1,3,3,0xA600, 0xFE00, {OP_Xmem,OP_Ymem,OP_SRC1}, 0, REST}, {
“mar”, 1,1,1,0x6D00, 0xFF00, {OP_Smem}, 0, REST}, { “mas”, 1,2,2,0x2C00,
0xFE00, {OP_Smem,OP_SRC1}, FL_SMR, REST}, { “mas”, 1,3,4,0xB800, 0xFC00,
{OP_Xmem,OP_Ymem,OP_SRC,OPT|OP_DST}, 0, REST}, { “masr”, 1,2,2,0x2E00,
0xFE00, {OP_Smem,OP_SRC1}, FL_SMR, REST}, { “masr”, 1,3,4,0xBC00,
0xFC00, {OP_Xmem,OP_Ymem,OP_SRC,OPT|OP_DST}, 0, REST}, { “masa”,
1,2,3,0xF48A, 0xFCFF, {OP_T,OP_SRC,OPT|OP_DST}, 0, REST},/SRC/
{ “masa”, 1,1,2,0x3300, 0xFF00, {OP_Smem,OPT|OP_B}, FL_SMR, REST}, {
“masar”, 1,2,3,0xF48B, 0xFCFF, {OP_T,OP_SRC,OPT|OP_DST}, 0, REST}, {
“max”, 1,1,1,0xF486, 0xFEFF, {OP_DST}, 0, REST}, { “min”, 1,1,1,0xF487,
0xFEFF, {OP_DST}, 0, REST}, { “mpy”, 1,2,2,0x2000, 0xFE00,
{OP_Smem,OP_DST}, FL_SMR, REST}, { “mpy”, 1,3,3,0xA400, 0xFE00,
{OP_Xmem,OP_Ymem,OP_DST}, 0, REST}, { “mpy”, 2,3,3,0x6200, 0xFE00,
{OP_Smem,OP_lk,OP_DST}, FL_SMR, REST}, { “mpy”, 2,2,2,0xF066, 0xFEFF,
{OP_lk,OP_DST}, 0, REST}, { “mpyr”, 1,2,2,0x2200, 0xFE00,
{OP_Smem,OP_DST}, FL_SMR, REST}, { “mpya”, 1,1,1,0xF48C, 0xFEFF,
{OP_DST}, 0, REST}, /SRC/ { “mpya”, 1,1,1,0x3100, 0xFF00,
{OP_Smem}, FL_SMR, REST}, { “mpyu”, 1,2,2,0x2400, 0xFE00,
{OP_Smem,OP_DST}, FL_SMR, REST}, { “mvdd”, 1,2,2,0xE500, 0xFF00,
{OP_Xmem,OP_Ymem}, 0, REST}, { “mvdk”, 2,2,2,0x7100, 0xFF00,
{OP_Smem,OP_dmad}, FL_SMR, REST}, { “mvdm”, 2,2,2,0x7200, 0xFF00,
{OP_dmad,OP_MMR}, 0, REST}, { “mvdp”, 2,2,2,0x7D00, 0xFF00,
{OP_Smem,OP_pmad}, FL_SMR, REST}, { “mvkd”, 2,2,2,0x7000, 0xFF00,
{OP_dmad,OP_Smem}, 0, REST}, { “mvmd”, 2,2,2,0x7300, 0xFF00,
{OP_MMR,OP_dmad}, 0, REST}, { “mvmm”, 1,2,2,0xE700, 0xFF00,
{OP_MMRX,OP_MMRY}, FL_NR, REST}, { “mvpd”, 2,2,2,0x7C00, 0xFF00,
{OP_pmad,OP_Smem}, 0, REST}, { “neg”, 1,1,2,0xF484, 0xFCFF,
{OP_SRC,OPT|OP_DST}, 0, REST}, { “nop”, 1,0,0,0xF495, 0xFFFF, {OP_None},
0, REST}, { “norm”, 1,1,2,0xF48F, 0xFCFF, {OP_SRC,OPT|OP_DST}, 0, REST},
{ “or”, 1,1,3,0xF0A0, 0xFCE0, {OP_SRC,OPT|OP_SHIFT,OPT|OP_DST}, 0,
REST},/SRC/ { “or”, 1,2,2,0x1A00, 0xFE00, {OP_Smem,OP_SRC1},
FL_SMR, REST}, { “or”, 2,2,4,0xF040, 0xFCF0,
{OP_lk,OPT|OP_SHFT,OP_SRC,OPT|OP_DST}, 0, REST}, { “or”, 2,3,4,0xF064,
0xFCFF, {OP_lk,OP_16,OP_SRC,OPT|OP_DST}, 0, REST}, { “orm”,
2,2,2,0x6900, 0xFF00, {OP_lk,OP_Smem}, FL_NR|FL_SMR, REST}, { “poly”,
1,1,1,0x3600, 0xFF00, {OP_Smem}, FL_SMR, REST}, { “popd”, 1,1,1,0x8B00,
0xFF00, {OP_Smem}, 0, REST}, { “popm”, 1,1,1,0x8A00, 0xFF00, {OP_MMR},
0, REST}, { “portr”, 2,2,2,0x7400, 0xFF00, {OP_PA,OP_Smem}, 0, REST}, {
“portw”, 2,2,2,0x7500, 0xFF00, {OP_Smem,OP_PA}, FL_SMR, REST}, { “pshd”,
1,1,1,0x4B00, 0xFF00, {OP_Smem}, FL_SMR, REST}, { “pshm”, 1,1,1,0x4A00,
0xFF00, {OP_MMR}, 0, REST}, { “ret”, 1,0,0,0xFC00, 0xFFFF, {OP_None},
B_RET|FL_NR, REST}, { “retd”, 1,0,0,0xFE00, 0xFFFF, {OP_None},
B_RET|FL_DELAY|FL_NR, REST}, { “rc”, 1,1,3,0xFC00, 0xFF00,
{OP_CC,OPT|OP_CC,OPT|OP_CC}, B_RET|FL_NR, REST}, { “rcd”, 1,1,3,0xFE00,
0xFF00, {OP_CC,OPT|OP_CC,OPT|OP_CC}, B_RET|FL_DELAY|FL_NR, REST}, {
“reada”, 1,1,1,0x7E00, 0xFF00, {OP_Smem}, 0, REST}, { “reset”,
1,0,0,0xF7E0, 0xFFFF, {OP_None}, FL_NR, REST}, { “rete”, 1,0,0,0xF4EB,
0xFFFF, {OP_None}, B_RET|FL_NR, REST}, { “reted”, 1,0,0,0xF6EB, 0xFFFF,
{OP_None}, B_RET|FL_DELAY|FL_NR, REST}, { “retf”, 1,0,0,0xF49B, 0xFFFF,
{OP_None}, B_RET|FL_NR, REST}, { “retfd”, 1,0,0,0xF69B, 0xFFFF,
{OP_None}, B_RET|FL_DELAY|FL_NR, REST}, { “rnd”, 1,1,2,0xF49F, 0xFCFF,
{OP_SRC,OPT|OP_DST}, FL_LP|FL_NR, REST}, { “rol”, 1,1,1,0xF491, 0xFEFF,
{OP_SRC1}, 0, REST}, { “roltc”, 1,1,1,0xF492, 0xFEFF, {OP_SRC1}, 0,
REST}, { “ror”, 1,1,1,0xF490, 0xFEFF, {OP_SRC1}, 0, REST}, { “rpt”,
1,1,1,0x4700, 0xFF00, {OP_Smem}, B_REPEAT|FL_NR|FL_SMR, REST}, { “rpt”,
1,1,1,0xEC00, 0xFF00, {OP_k8u}, B_REPEAT|FL_NR, REST}, { “rpt”,
2,1,1,0xF070, 0xFFFF, {OP_lku}, B_REPEAT|FL_NR, REST}, { “rptb”,
2,1,1,0xF072, 0xFFFF, {OP_pmad}, FL_NR, REST}, { “rptbd”, 2,1,1,0xF272,
0xFFFF, {OP_pmad}, FL_DELAY|FL_NR, REST}, { “rptz”, 2,2,2,0xF071,
0xFEFF, {OP_DST,OP_lku}, B_REPEAT|FL_NR, REST}, { “rsbx”, 1,1,2,0xF4B0,
0xFDF0, {OPT|OP_N,OP_SBIT}, FL_NR, REST}, { “saccd”, 1,3,3,0x9E00,
0xFE00, {OP_SRC1,OP_Xmem,OP_CC2}, 0, REST}, { “sat”, 1,1,1,0xF483,
0xFEFF, {OP_SRC1}, 0, REST}, { “sfta”, 1,2,3,0xF460, 0xFCE0,
{OP_SRC,OP_SHIFT,OPT|OP_DST}, 0, REST}, { “sftc”, 1,1,1,0xF494, 0xFEFF,
{OP_SRC1}, 0, REST}, { “sftl”, 1,2,3,0xF0E0, 0xFCE0,
{OP_SRC,OP_SHIFT,OPT|OP_DST}, 0, REST}, { “sqdst”, 1,2,2,0xE200, 0xFF00,
{OP_Xmem,OP_Ymem}, 0, REST}, { “squr”, 1,2,2,0xF48D, 0xFEFF,
{OP_A,OP_DST}, 0, REST},/SRC/ { “squr”, 1,2,2,0x2600, 0xFE00,
{OP_Smem,OP_DST}, FL_SMR, REST}, { “squra”, 1,2,2,0x3800, 0xFE00,
{OP_Smem,OP_SRC1}, FL_SMR, REST}, { “squrs”, 1,2,2,0x3A00, 0xFE00,
{OP_Smem,OP_SRC1}, FL_SMR, REST}, { “srccd”, 1,2,2,0x9D00, 0xFF00,
{OP_Xmem,OP_CC2}, 0, REST}, { “ssbx”, 1,1,2,0xF5B0, 0xFDF0,
{OPT|OP_N,OP_SBIT}, FL_NR, REST}, { “st”, 1,2,2,0x8C00, 0xFF00,
{OP_T,OP_Smem}, 0, REST}, { “st”, 1,2,2,0x8D00, 0xFF00,
{OP_TRN,OP_Smem}, 0, REST}, { “st”, 2,2,2,0x7600, 0xFF00,
{OP_lk,OP_Smem}, 0, REST}, { “sth”, 1,2,2,0x8200, 0xFE00,
{OP_SRC1,OP_Smem}, 0, REST}, { “sth”, 1,3,3,0x8600, 0xFE00,
{OP_SRC1,OP_ASM,OP_Smem}, 0, REST}, { “sth”, 1,3,3,0x9A00, 0xFE00,
{OP_SRC1,OP_SHFT,OP_Xmem}, 0, REST}, { “sth”, 2,2,3,0x6F00, 0xFF00,
{OP_SRC1,OPT|OP_SHIFT,OP_Smem}, FL_EXT, 0x0C60, 0xFEE0, XREST}, { “stl”,
1,2,2,0x8000, 0xFE00, {OP_SRC1,OP_Smem}, 0, REST}, { “stl”,
1,3,3,0x8400, 0xFE00, {OP_SRC1,OP_ASM,OP_Smem}, 0, REST}, { “stl”,
1,3,3,0x9800, 0xFE00, {OP_SRC1,OP_SHFT,OP_Xmem}, 0, REST}, { “stl”,
2,2,3,0x6F00, 0xFF00, {OP_SRC1,OPT|OP_SHIFT,OP_Smem}, FL_EXT, 0x0C80,
0xFEE0, XREST }, { “stlm”, 1,2,2,0x8800, 0xFE00, {OP_SRC1,OP_MMR}, 0,
REST}, { “stm”, 2,2,2,0x7700, 0xFF00, {OP_lk,OP_MMR}, 0, REST}, {
“strcd”, 1,2,2,0x9C00, 0xFF00, {OP_Xmem,OP_CC2}, 0, REST}, { “sub”,
1,1,3,0xF420, 0xFCE0, {OP_SRC,OPT|OP_SHIFT,OPT|OP_DST}, 0,
REST},/SRC/ { “sub”, 1,2,3,0xF481, 0xFCFF,
{OP_SRC,OP_ASM,OPT|OP_DST}, 0, REST},/SRC/ { “sub”,
1,2,2,0x0800, 0xFE00, {OP_Smem,OP_SRC1}, FL_SMR, REST}, { “sub”,
1,3,3,0x0C00, 0xFE00, {OP_Smem,OP_TS,OP_SRC1}, FL_SMR, REST}, { “sub”,
1,3,4,0x4000, 0xFC00, {OP_Smem,OP_16,OP_SRC,OPT|OP_DST}, FL_SMR, REST},
{ “sub”, 1,3,3,0x9200, 0xFE00, {OP_Xmem,OP_SHFT,OP_SRC1}, 0, REST},
/PREFER/ { “sub”, 2,2,4,0x6F00, 0xFF00,
{OP_Smem,OPT|OP_SHIFT,OP_SRC,OPT|OP_DST}, FL_EXT|FL_SMR, 0x0C20, 0xFCE0,
XREST}, { “sub”, 1,3,3,0xA200, 0xFE00, {OP_Xmem,OP_Ymem,OP_DST}, 0,
REST}, { “sub”, 2,2,4,0xF010, 0xFCF0,
{OP_lk,OPT|OP_SHFT,OP_SRC,OPT|OP_DST}, 0, REST}, { “sub”, 2,3,4,0xF061,
0xFCFF, {OP_lk,OP_16,OP_SRC,OPT|OP_DST}, 0, REST}, { “subb”,
1,2,2,0x0E00, 0xFE00, {OP_Smem,OP_SRC1}, FL_SMR, REST}, { “subc”,
1,2,2,0x1E00, 0xFE00, {OP_Smem,OP_SRC1}, FL_SMR, REST}, { “subs”,
1,2,2,0x0A00, 0xFE00, {OP_Smem,OP_SRC1}, FL_SMR, REST}, { “trap”,
1,1,1,0xF4C0, 0xFFE0, {OP_031}, B_BRANCH|FL_NR, REST}, { “writa”,
1,1,1,0x7F00, 0xFF00, {OP_Smem}, FL_SMR, REST}, { “xc”, 1,2,4,0xFD00,
0xFD00, {OP_12,OP_CC,OPT|OP_CC,OPT|OP_CC}, FL_NR, REST}, { “xor”,
1,1,3,0xF0C0, 0xFCE0, {OP_SRC,OPT|OP_SHIFT,OPT|OP_DST}, 0,
REST},/SRC*/ { “xor”, 1,2,2,0x1C00, 0xFE00, {OP_Smem,OP_SRC1},
FL_SMR, REST}, { “xor”, 2,2,4,0xF050, 0xFCF0,
{OP_lku,OPT|OP_SHFT,OP_SRC,OPT|OP_DST}, 0, REST}, { “xor”, 2,3,4,0xF065,
0xFCFF, {OP_lku,OP_16,OP_SRC,OPT|OP_DST}, 0, REST}, { “xorm”,
2,2,2,0x6A00, 0xFF00, {OP_lku,OP_Smem}, FL_NR|FL_SMR, REST}, { NULL,
0,0,0,0,0, {}, 0, REST}, };
/* assume all parallel instructions have at least three operands */
const insn_template tic54x_paroptab[] = { { “ld”,1,1,2,0xA800, 0xFE00,
{OP_Xmem,OP_DST}, FL_PAR,0,0, “mac”, {OP_Ymem,OPT|OP_RND},}, {
“ld”,1,1,2,0xAA00, 0xFE00, {OP_Xmem,OP_DST}, FL_PAR,0,0, “macr”,
{OP_Ymem,OPT|OP_RND},}, { “ld”,1,1,2,0xAC00, 0xFE00, {OP_Xmem,OP_DST},
FL_PAR,0,0, “mas”, {OP_Ymem,OPT|OP_RND},}, { “ld”,1,1,2,0xAE00, 0xFE00,
{OP_Xmem,OP_DST}, FL_PAR,0,0, “masr”, {OP_Ymem,OPT|OP_RND},}, {
“st”,1,2,2,0xC000, 0xFC00, {OP_SRC,OP_Ymem}, FL_PAR,0,0, “add”,
{OP_Xmem,OP_DST}, }, { “st”,1,2,2,0xC800, 0xFC00, {OP_SRC,OP_Ymem},
FL_PAR,0,0, “ld”, {OP_Xmem,OP_DST}, }, { “st”,1,2,2,0xE400, 0xFC00,
{OP_SRC,OP_Ymem}, FL_PAR,0,0, “ld”, {OP_Xmem,OP_T}, }, {
“st”,1,2,2,0xD000, 0xFC00, {OP_SRC,OP_Ymem}, FL_PAR,0,0, “mac”,
{OP_Xmem,OP_DST}, }, { “st”,1,2,2,0xD400, 0xFC00, {OP_SRC,OP_Ymem},
FL_PAR,0,0, “macr”, {OP_Xmem,OP_DST}, }, { “st”,1,2,2,0xD800, 0xFC00,
{OP_SRC,OP_Ymem}, FL_PAR,0,0, “mas”, {OP_Xmem,OP_DST}, }, {
“st”,1,2,2,0xDC00, 0xFC00, {OP_SRC,OP_Ymem}, FL_PAR,0,0, “masr”,
{OP_Xmem,OP_DST}, }, { “st”,1,2,2,0xCC00, 0xFC00, {OP_SRC,OP_Ymem},
FL_PAR,0,0, “mpy”, {OP_Xmem,OP_DST}, }, { “st”,1,2,2,0xC400, 0xFC00,
{OP_SRC,OP_Ymem}, FL_PAR,0,0, “sub”, {OP_Xmem,OP_DST}, }, { NULL, 0, 0,
0, 0, 0, {0,0,0,0}, 0, REST }, };
================================================================================
FILE: hw/arm/calypso/fw_console.c SIZE: 2738 bytes, 81 lines
================================================================================
/ 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] == ‘’ || buf[len-1] == ’)) len–; if
(len == 0) return; if (fw_log_fp) { fprintf(fw_log_fp, “[fw] %.s”,
(int)len, buf); fflush(fw_log_fp); } fprintf(stderr, ”[fw-console]
%.s”, (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”, 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);
}
================================================================================
FILE: hw/arm/calypso/l1ctl_sock.c SIZE: 19712 bytes, 522 lines
================================================================================
/ l1ctl_sock.c — L1CTL unix socket server (legacy
QEMU-internal path) État runtime actuel (2026-05-25) : ce
socket est INACTIF dans le run * orchestré par scripts/run.sh.
run.sh:458 override l’env L1CTL_SOCK vers * /tmp/qemu_l1ctl_disabled
pour le child QEMU, donc ce module crée son * socket à une
adresse-poubelle et personne ne s’y connecte. Le VRAI * socket
/tmp/osmocom_l2 que le mobile osmocom-bb utilise est créé par * osmocon
(-m romload -s /tmp/osmocom_l2), pas par QEMU. Le path
historique « Replaces the Python bridge » reste possible si on * lance
QEMU sans override env — utile pour des tests sans osmocon, mais * pas
le mode de fonctionnement principal. Voir doc/L1CTL_SOCK_FLOW.md * et le
commentaire à run.sh:458. Quand actif : provides a unix
socket at /tmp/osmocom_l2 that speaks * L1CTL (length-prefixed messages)
to OsmocomBB mobile. Internally translates between: * -
sercomm framing (FLAG/ESCAPE/DLCI) on the firmware UART side * - L1CTL
length-prefix on the mobile socket side
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 “”, ##VA_ARGS)
/* Nom lisible des types L1CTL (l1ctl_proto.h) — diagnostic pur pour
suivre la * conversation firmware↔︎mobile à l’œil. NB : ce mobile cause
par osmocon/hdlc * (serial), pas par ce socket unix ; ce log ne voit que
le sens firmware→mobile * via sercomm. Le vrai flux mobile↔︎firmware se
lit dans 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;
/* FN-FIX : le FN que le firmware envoie au mobile dans
L1CTL_RACH_CONF (msg type * 0x0c), capture ICI au moment EXACT ou le
mobile le recoit (= ce qu’il memorise * pour matcher la req-ref de l’IMM
ASSIGN, gsm48_rr.c:3372). Lu par le shunt * (calypso_dsp_shunt.c) pour
reecrire la req-ref. Source race-free : pas de lecture * paresseuse de
last_rach.fn @0x836500 (qui est asynchrone
vs l’IMM ASS du BTS). / volatile uint32_t g_last_rach_conf_fn = 0;
volatile uint32_t g_rach_conf_fn[256] = {0}; / per-ra FN-FIX :
RACH_CONF fn keye par g_last_recorded_ra / extern volatile uint8_t
g_last_recorded_ra; / defini dans calypso_dsp_shunt.c (record_rach)
*/
/* —- 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;
}
}
/* Hop 5 : injection directe DL SI -> mobile en L1CTL DATA_IND
(court-circuite * a_cd->ARM->UART qui perd des octets). Appele par
le shunt GSMTAP listener. / void l1ctl_inject_dl_si(const uint8_t
l2, int l2len, uint32_t fn) { if (g_l1ctl.cli_fd < 0 || !l2 ||
l2len <= 0) return; if (l2len > 23) l2len = 23; uint8_t pl[16 +
23]; memset(pl, 0, sizeof(pl)); pl[0] = 0x03; /* L1CTL_DATA_IND /
pl[4] = 0x80; / chan_nr = BCCH / pl[6] = (uint8_t)(514 >>
8); pl[7] = (uint8_t)(514 & 0xFF); / band_arfcn 514 /
pl[8]=(uint8_t)(fn>>24); pl[9]=(uint8_t)(fn>>16);
pl[10]=(uint8_t)(fn>>8); pl[11]=(uint8_t)fn; / frame_nr (BE)
/ pl[12] = 40; / rx_level / pl[13] = 30; / snr /
/ pl[14]=num_biterr=0, pl[15]=fire_crc=0 (CRC OK) */ memcpy(pl +
16, l2, l2len); l1ctl_send_to_mobile(&g_l1ctl, pl, 16 + l2len);
L1CTL_LOG(“INJECT DL DATA_IND BCCH fn=%u l2len=%d -> mobile”, fn,
l2len); }
/* —- 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) {
/* ===== GATES de déblocage (oracle FORCE_TOA, gate-par-gate) =====
* Le mobile reçoit par CE socket (mobile.cfg: layer2-socket
* /tmp/osmocom_l2). Deux gates bridgent les trous du demod DSP, pour
* prouver que tout l'aval (camp/SI/IMM-ASS) marche quand le DSP fournit
* son résultat. Purement oracle — à retirer quand le DSP demod marche.
* CALYPSO_FORCE_FBSB=1 : bridge blocker #1 (Channel sync error) =
* FBSB_CONF(0x02) result@[18] → 0=SUCCESS.
* CALYPSO_FORCE_AGCH=1 : bridge blocker #2 (pas de sysinfo) sur les
* DATA_IND(0x03) : BCCH(chan 0x80) rote le type
* SI ; AGCH/PCH(chan 0x90) injecte IMM ASS.
* Port exact du GDB mutate_agch.
* Layout payload : l1ctl_hdr(4) + l1ctl_info_dl(12) + corps ;
* → FBSB result @18 ; DATA_IND chan_nr @4, L3 @16. */
/* @BEQUILLE — FORCE_FBSB / FORCE_AGCH (CALYPSO_FORCE_FBSB, CALYPSO_FORCE_AGCH,
* EQ1, defaut 0 ; VERROUILLES a 0 par run.sh en mode full-grgsm)
* masque : le resultat du demod DSP vu par le mobile. FBSB : force le resultat
* de FBSB_CONF a SUCCESS. AGCH : rote le type SI du BCCH et ECRASE le
* L3 du PCH par un IMM ASSIGNMENT en dur.
* retirer : quand le demod DSP publie un a_cd valide (SI reels decodes).
* NB : assignation dure ("=0" puis export) en full-grgsm -> les poser en
* ligne de commande n'a AUCUN effet dans le mode par defaut.
*/
static int g_fbsb = -1, g_agch = -1;
if (g_fbsb < 0) {
const char *a = getenv("CALYPSO_FORCE_FBSB");
const char *b = getenv("CALYPSO_FORCE_AGCH");
g_fbsb = (a && *a == '1') ? 1 : 0;
g_agch = (b && *b == '1') ? 1 : 0;
}
if (g_fbsb && payload[0] == 0x02 && plen >= 19 && payload[18] != 0) {
L1CTL_LOG("GATE-FBSB #1: FBSB_CONF result 0x%02x → 0", payload[18]);
payload[18] = 0;
}
if (g_agch && payload[0] == 0x03 && plen >= 16 + 3) {
uint8_t chan_nr = payload[4];
uint8_t *l3 = &payload[16];
if (chan_nr == 0x80) {
static const uint8_t si[4] = { 0x19, 0x1a, 0x1b, 0x1c };
static int r = 0;
l3[2] = si[r]; r = (r + 1) & 3;
L1CTL_LOG("GATE-AGCH #2 bcch: SI type → 0x%02x", l3[2]);
} else if (chan_nr == 0x90 && plen >= 16 + 23) {
static const uint8_t imm[23] = {
0x2d, 0x06, 0x3f, 0x00, 0x20, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b,
0x2b, 0x2b, 0x2b };
memcpy(l3, imm, sizeof(imm));
L1CTL_LOG("GATE-AGCH #2 pch: IMM ASSIGNMENT injecté");
}
}
/* FN-FIX : capture le FN du RACH_CONF (0x0c) = le FN que le mobile memorise.
* Layout : l1ctl_hdr(4) + l1ctl_info_dl ; frame_nr (BE) @ payload[8..11]. */
if (payload[0] == 0x0c && plen >= 12) {
g_last_rach_conf_fn = ((uint32_t)payload[8] << 24) | ((uint32_t)payload[9] << 16) |
((uint32_t)payload[10] << 8) | (uint32_t)payload[11];
g_rach_conf_fn[g_last_recorded_ra] = g_last_rach_conf_fn; /* per-ra : keye par le ra de la derniere RACH */
L1CTL_LOG("FN-FIX: RACH_CONF fn=%u capture (memo mobile, ra=0x%02x)", g_last_rach_conf_fn, g_last_recorded_ra);
}
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];
/* === CAPTURE Kc (chiffrement A5) : 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. On ecrit /dev/shm/calypso_kc (seq,algo,
* key_len,Kc) -> l'ipc-device chiffre l'UL (osmo_a5) et si_bridge relance
* grgsm -k pour dechiffrer le DL. Le Kc capture = celui derive par le
* mobile (A8) = exactement celui du reseau. */
if (payload[0] == 0x15 && msglen >= 10) {
uint8_t algo = payload[8];
uint8_t klen = payload[9];
if (klen > 16) klen = 16;
if (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);
}
}
/* Reset cipher a l'etablissement/liberation du canal dedie : chaque
* nouveau canal demarre EN CLAIR jusqu'a son propre CIPHER MODE COMMAND
* (sinon un Kc perime chiffrerait la SABM du canal suivant). */
if (payload[0] == 0x05 || payload[0] == 0x12) { /* DM_EST_REQ / DM_REL_REQ */
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; }
================================================================================
FILE: hw/arm/calypso/sercomm_gate.c SIZE: 9828 bytes, 268 lines
================================================================================
/ sercomm_gate.c — Sercomm DLCI router (PTY) + CLK UDP
listener Two separate roles, matching the current QEMU split:
1. PTY (UART modem) — sercomm HDLC stream from host
(mobile/ccch_scan). * DLCIs are re-wrapped and pushed to the UART RX
FIFO so the firmware’s * sercomm driver parses them via the real code
path. L1CTL = DLCI 5, * TRXC = DLCI 4 (intercepted here, stub responses
wrapped back out). * No DLCI on the PTY ever carries radio bursts.
2. UDP CLK listener — just drains “IND CLOCK ” on the baseband
* side and logs it. calypso_trx owns its own FN counter; the CLK *
packets are purely informational here. TRXC traffic is
stubbed locally by calypso-ipc-device on UDP 5701 — QEMU * never sees
TRXC on UDP. TRXD (burst) transport is owned by calypso_bsp.c
via calypso_orch. 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”
/* TRXC handling is NOT done by QEMU. calypso-ipc-device answers TRXC
commands * locally (stub) on UDP 5701 — QEMU never sees them. The
L1CTL/L23 * path on PTY DLCI 5 is the only thing the modem UART carries.
*/
#define GATE_LOG(fmt, …)
fprintf(stderr, “[gate]” fmt “”, ##VA_ARGS)
/* UART pointer captured on the first sercomm_gate_feed() call. The
TRXC * UDP callback uses it to push received sercomm-wrapped frames
straight * into the firmware UART RX FIFO. / 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)”, 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)”, 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]
== “”[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 mobile (via bridge).
* Trace, then push to firmware FIFO so the real
* sercomm parser dispatches it. All other DLCIs
* (console, debug, …) go straight to 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.
UDP CLK listener — informational only *
============================================================
TRXC is stubbed by calypso-ipc-device on UDP 5701; QEMU never sees it. *
TRXD (bursts) is owned by calypso_bsp.c via calypso_orch. */
/* CLK UDP listener removed — QEMU sends ticks to bridge directly. */
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;
/* CLK UDP listener disabled — QEMU is now the clock master and sends
* ticks directly to the bridge via calypso_trx.c (port 6700).
* The gate no longer needs to receive CLK IND. */
(void)clk_port;
g_clk_fd = -1;
GATE_LOG("TRXD: owned by calypso_bsp.c via calypso_orch");
}
================================================================================
FILE: hw/char/calypso_uart.c SIZE: 29840 bytes, 925 lines
================================================================================
/ calypso_uart.c — Calypso UART Pragmatic emulation
for the Compal/Calypso loader path: * - strict 8-bit MMIO accesses * -
banked registers via LCR[7] / LCR==0xBF * - SCR / SSR implemented * - RX
FIFO with verbose debug * - raw RX/TX dumps to /tmp/qemu-.raw
* 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” /*
current_cpu, mem_io_pc — for RBR-READ-PROBE / #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_full_pcb.h” / calypso_async_log :
tick log off main thread */ #include “hw/arm/calypso/sercomm_gate.h”
/* Register offsets */ #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
/* IER bits */ #define IER_RX_DATA (1 << 0) #define
IER_TX_EMPTY (1 << 1) #define IER_RX_LINE (1 << 2)
/* IIR values */ #define IIR_NO_INT 0x01 #define IIR_RX_LINE 0x06
#define IIR_RX_DATA 0x04 #define IIR_TX_EMPTY 0x02
/* LCR bits */ #define LCR_DLAB (1 << 7) #define LCR_CONF_BF
0xBF
/* LSR bits */ #define LSR_DR (1 << 0) #define LSR_OE (1
<< 1) #define LSR_THRE (1 << 5) #define LSR_TEMT (1 <<
6)
/* MSR bits */ #define MSR_CTS (1 << 4) #define MSR_DSR (1
<< 5) #define MSR_DCD (1 << 7)
/* FCR bits */ #define FCR_FIFO_EN (1 << 0) #define
FCR_RX_RESET (1 << 1) #define FCR_TX_RESET (1 << 2)
/* SSR bits (minimal model) */ #define SSR_TX_FIFO_FULL (1 <<
0)
/** * uart_log_raw - Log raw UART data to a file * @path: Path to the log file * @buf: Buffer containing the data * @len: Length of the data Appends
binary data to the specified file. Used for debugging * modem and IrDA
traffic. Silently ignores errors. / static void uart_log_raw(const
char path, const uint8_t buf, size_t len) { FILE f =
fopen(path, “ab”); if (!f) { return; }
fwrite(buf, 1, len, f);
fclose(f);
}
/* —- FIFO helpers —- */
/** * fifo_reset - Reset the RX FIFO state * @s: UART device state / static void
fifo_reset(CalypsoUARTState s) { s->rx_head = 0; s->rx_tail =
0; s->rx_count = 0; }
/** * fifo_push - Push a byte into the RX FIFO * @s: UART device state * @data: Byte to push Sets overrun
error flag if FIFO is full. / 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”, 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++;
}
/** * fifo_pop - Pop a byte from the RX FIFO * @s: UART device state Returns: The
popped byte, or 0 if FIFO is empty. / 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--;
/* RBR-POP-PROBE (2026-05-27, c web review) : count fifo_pop calls to
* discriminate icount-rerun vs access-size-decomposition vs other
* byte-loss mechanisms. One romload run shows the ratio. */
{
static uint64_t pop_total;
pop_total++;
if (s->label && !strcmp(s->label, "modem") && pop_total <= 200) {
fprintf(stderr,
"[UART-POP-PROBE] #%llu byte=0x%02x rx_count_after=%u\n",
(unsigned long long)pop_total,
(unsigned)data,
(unsigned)s->rx_count);
}
}
return data;
}
/* —- IRQ —- */
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;
}
s->iir = iir;
/* Force edge transition so INTH always sees the change.
* After IRQ_CTRL ack clears levels[n], a steady-high line
* needs a low→high pulse to re-register in the INTH. */
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)) { /
Force IRQ re-evaluation by pulsing the IRQ line */
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) { / Re-check
TX interrupt state — if THR is empty and IER TX enabled, * fire the
interrupt so firmware can write next byte. */
calypso_uart_update_irq(s); }
void calypso_uart_inject_raw(CalypsoUARTState s, const uint8_t
buf, int len) { if (!s) return; for (int i = 0; i < len; i++) {
fifo_push(s, buf[i]); } if (s->rx_count > 0) { s->lsr |=
LSR_DR; calypso_uart_update_irq(s); } }
void calypso_uart_force_init(CalypsoUARTState s) { / Force
UART into operational state for firmware that gets stuck * before
completing its own UART init (e.g. trx.highram.elf). * Sets
MDR1=UART16x, enables RX+TX interrupts. / if (s->mdr1 != 0x00) {
s->mdr1 = 0x00; / UART 16x mode / s->scr = 0x01; }
s->ier = 0x03; / RX + TX interrupts enabled */
calypso_uart_update_irq(s); }
/* —- RX poll timer —- * QEMU’s chardev backend (PTY) only delivers
data during the main event * loop. If the ARM CPU runs in a tight loop
without yielding, incoming * bytes accumulate in the PTY buffer and
never reach calypso_uart_receive. * This periodic timer forces QEMU to
check for pending chardev input. */
#define UART_RX_POLL_NS (10 * 1000 * 1000) /* 10 ms */
static void calypso_uart_rx_poll(void opaque) { CalypsoUARTState
s = (CalypsoUARTState *)opaque;
/* AUDIT FIX 2026-05-08 night : `main_loop_wait(false)` removed.
*
* In QEMU API, the parameter is named `nonblocking`. `false` means
* BLOCKING — the prior comment "non-blocking poll" was wrong.
* Worse, calling main_loop_wait from a timer callback creates
* arbitrary recursion : the very loop that dispatched this REALTIME
* timer is re-entered from within itself.
*
* Under -icount, this breaks the invariant
* virtual_time = icount * (1 << shift)
* because nested TCG bursts update icount non-monotonically relative
* to the outer loop's scheduling decisions ; the auto-tune algorithm
* drifts and VIRTUAL-clock timers (tdma/firq) miss their deadlines
* for seconds at a time. Symptom : bridge UDP path frozen under any
* `icount != off`.
*
* `qemu_chr_fe_accept_input` alone is what's needed : it signals the
* chardev backend that more bytes can be delivered. The main loop
* resumes naturally at the end of this callback.
*
* Diagnosed by Claude web event-loop audit 2026-05-08. The user
* wants `CALYPSO_ICOUNT != off` to work end-to-end. */
qemu_chr_fe_accept_input(&s->chr);
/* Re-arm (realtime, 5ms — tightened from 50ms 2026-05-26 night).
*
* Sous icount=auto, le main_loop QEMU itère moins fréquemment
* pendant les TCG bursts ARM, ce qui creuse une latence wall-clock
* entre l'arrivée de bytes osmocon sur la PTY et leur livraison à
* calypso_uart_receive. À 50ms la romload upload (64 KiB / blocks
* 1024) prenait > 60s wall et osmocon timeout. À 5ms ça matche la
* cadence émission osmocon (~1KB tous les 5ms). */
timer_mod(s->rx_poll_timer,
qemu_clock_get_ms(QEMU_CLOCK_REALTIME) + 5);
}
/* —- Control PTY callbacks —- */
/* —- Calypso romloader stub ——————————————– On real
hardware the Compal/Calypso boots into a small ROM-resident *
“romloader” that speaks a simple framed protocol over UART:
<i (0x3c 0x69) ident → ack >i + param >p … * <w (0x3c 0x77)
hdr(8B) data(N) write block → >w (or >W on err) * <c (0x3c
0x63) chk(1B) checksum → >c (or >C) * <b (0x3c 0x62) addr(4B
BE) branch → >b → run firmware osmocon performs this
handshake before it switches to bridging the * mobile↔︎firmware sercomm
channel. Our QEMU loads the firmware via -kernel * and never runs the
bootloader, so without this stub osmocon loops on * “Waiting for
handshake”. The stub eats every modem-UART RX byte until the
branch ack is sent, * fakes the protocol responses (no actual download —
the firmware is * already in RAM), then enables passthrough so
subsequent traffic flows * to the firmware sercomm parser as usual.
The param ack advertises a payload size of 1024 bytes, so each
<w * block is 8 (header continuation) + 1024 (data) bytes after the
0x77. */
typedef enum { ROM_IDLE, /* waiting for 0x3c lead-in /
ROM_AFTER_3C, / saw 0x3c, expecting cmd char / ROM_BLOCK_DATA,
/ consuming write block payload / ROM_CHK_DATA, /
consuming 1-byte checksum / ROM_BR_DATA, / consuming 4-byte
branch address / ROM_PASSTHROUGH, / handshake complete — bytes
go to sercomm */ } RomloadState;
static struct { RomloadState state; int needed; /* bytes still
expected for current block / uint16_t payload_size; / what we
advertised in param ack */ } romload = { .state = ROM_IDLE,
.payload_size = 1024, };
/* Returns true when the byte was consumed by the stub (must NOT be *
forwarded to sercomm). Returns false when passthrough is active. /
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;
}
/* discard pre-handshake noise */
return true;
case ROM_AFTER_3C: {
if (b == 0x69) {
/* <i ident → reply >i then >p (param ack with payload size LE) */
uint8_t ack[6] = {
0x3e, 0x69, /* >i */
0x3e, 0x70, /* >p */
(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));
fprintf(stderr, "[UART:modem] ROMLOAD STUB: ident → ack+param "
"(payload_size=%u)\n", romload.payload_size);
romload.state = ROM_IDLE;
} else if (b == 0x77) {
/* <w block: 8 hdr-cont (idx, num+1, sz_msb, sz_lsb, addr×4)
* + payload_size data bytes still to read */
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 {
/* unknown command — discard and resync */
romload.state = ROM_IDLE;
}
return true;
}
case ROM_BLOCK_DATA:
if (--romload.needed == 0) {
uint8_t ack[2] = { 0x3e, 0x77 }; /* >w */
qemu_chr_fe_write_all(&s->chr, ack, sizeof(ack));
romload.state = ROM_IDLE;
}
return true;
case ROM_CHK_DATA:
if (--romload.needed == 0) {
/* osmocon's handle_read_romload sets buf_used_len=3 in
* WAITING_CHECKSUM_ACK: it waits for ">c" + a trailing byte
* (used for the nack diagnostic). The 2-byte ack alone
* keeps it blocked. Echo the checksum byte we just got. */
uint8_t ack[3] = { 0x3e, 0x63, b }; /* >c <chk> */
qemu_chr_fe_write_all(&s->chr, ack, sizeof(ack));
fprintf(stderr, "[UART:modem] ROMLOAD STUB: checksum 0x%02x → ack\n",
b);
romload.state = ROM_IDLE;
}
return true;
case ROM_BR_DATA:
if (--romload.needed == 0) {
uint8_t ack[2] = { 0x3e, 0x62 }; /* >b */
qemu_chr_fe_write_all(&s->chr, ack, sizeof(ack));
fprintf(stderr, "[UART:modem] ROMLOAD STUB: branch → ack — "
"switching to sercomm passthrough\n");
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;
/* RX = host → firmware. Modem UART tagged [PTY-MODEM-RX]
* (generic — actual DLCI dispatch is logged downstream by the
* sercomm parser, e.g. [gate] TRXC RX from PTY for DLCI 4). */
{
const char *tag = (s->label && !strcmp(s->label, "modem"))
? "PTY-MODEM-RX" : "UART";
const char *lbl = (s->label && !strcmp(s->label, "modem"))
? "" : s->label ? s->label : "?";
fprintf(stderr,
"[%s%s%s] <<<RX %d bytes (rx_count=%u free=%u):",
tag, *lbl ? ":" : "", lbl,
size,
(unsigned)s->rx_count,
(unsigned)(CALYPSO_UART_RX_FIFO_SIZE - s->rx_count));
for (int i = 0; i < size && i < 64; i++)
fprintf(stderr, " %02x", buf[i]);
if (size > 64) fprintf(stderr, " ...");
fprintf(stderr, "\n");
}
if (s->label && !strcmp(s->label, "modem")) {
uart_log_raw("/tmp/qemu-modem-rx.raw", buf, size);
} else if (s->label && !strcmp(s->label, "irda")) {
uart_log_raw("/tmp/qemu-irda-rx.raw", buf, size);
}
/* IrDA UART: raw debug / IrDA-stack channel. The firmware irphy SIR
* de-framer reads these bytes via the RX FIFO (uart_getchar_nb), so push
* them straight into the FIFO and raise LSR_DR + the RX IRQ (same as
* calypso_uart_inject_raw). Legacy burst-over-UART routing removed: GSM
* DL bursts arrive via UDP/BSP, never this UART. */
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;
}
/* Modem UART: filter through the romloader stub first. While the
* stub is in handshake mode it eats every byte and replies on the
* same chardev to satisfy osmocon. Once branch ack has fired, the
* stub goes passthrough and the remaining bytes flow into the
* sercomm parser as before. */
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) {
sercomm_gate_feed(s, passthrough, pt_len);
}
/* Réamorce le chardev backend pour la batch suivante.
* Sous icount=auto le main_loop itère moins souvent → sans cet
* appel les bytes osmocon attendent jusqu'au prochain tick du
* rx_poll_timer (5ms). osmocon timeout sur ses blocs romload. */
qemu_chr_fe_accept_input(&s->chr);
return;
}
/* Non-modem UARTs (irda is already handled above): pass directly. */
sercomm_gate_feed(s, buf, size);
if (s->rx_count > 0) {
s->lsr |= LSR_DR;
}
calypso_uart_update_irq(s);
}
/* —- MMIO —- */
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 {
/* RBR-READ-PROBE (2026-05-27) : log access size + offset + ARM
* mem_io_pc (host retaddr, but stable within a single insn).
* Combine with [UART-POP-PROBE] : if N pops per N reads with same
* mem_io_pc → access-size decomposition. If N pops per N reads
* with distinct mem_io_pc → real distinct LDRs (no decomposition,
* no rerun). Different counts → other byte-loss mechanism. */
if (s->label && !strcmp(s->label, "modem")) {
static int read_log = 0;
if (read_log < 200) {
uintptr_t pc = current_cpu ? current_cpu->mem_io_pc : 0;
fprintf(stderr,
"[UART-RBR-READ] #%d off=0x%02x size=%u "
"mem_io_pc=0x%lx rx_count_before=%u\n",
read_log, (unsigned)offset, size,
(unsigned long)pc, (unsigned)s->rx_count);
read_log++;
}
}
val = fifo_pop(s);
if (s->rx_count > 0) {
s->lsr |= LSR_DR;
} else {
s->lsr &= ~LSR_DR;
}
/* RBR debug: log bytes read by firmware from modem UART */
if (s->label && !strcmp(s->label, "modem")) {
static int rbr_log = 0;
if (rbr_log < 200) {
fprintf(stderr, "[UART-RBR] pop=0x%02x rx_count=%u\n",
(unsigned)(val & 0xFF), (unsigned)s->rx_count);
rbr_log++;
}
}
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) {
/* TX burst drain: don't clear pending on the first read.
* This lets the firmware ISR loop and drain multiple bytes.
* Clear only after 2 consecutive reads without a THR write
* (meaning the ISR has no more data to send). */
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;
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;
/* TX trace: tag modem UART as L1CTL-PTY.
* Per-byte log is volume-heavy (>140k lines per minute under
* fw-console "LOST N!" flood). Gated on env CALYPSO_UART_TRACE=1
* (default OFF) to keep host I/O free for QEMU emulation —
* heavy stderr writes were causing BTS to die from "No more
* clock from transceiver" because bridge couldn't get scheduled. */
{
static int trace_enabled = -1;
if (trace_enabled < 0) {
const char *e = getenv("CALYPSO_UART_TRACE");
trace_enabled = (e && *e == '1') ? 1 : 0;
}
if (trace_enabled) {
const char *tag = (s->label && !strcmp(s->label, "modem"))
? "L1CTL-PTY" : "UART";
const char *lbl = (s->label && !strcmp(s->label, "modem"))
? "" : s->label ? s->label : "?";
fprintf(stderr, "[%s%s%s] >>>TX %02x\n",
tag, *lbl ? ":" : "", lbl, ch);
}
}
if (s->label && !strcmp(s->label, "modem")) {
uart_log_raw("/tmp/qemu-modem-tx.raw", &ch, 1);
} else if (s->label && !strcmp(s->label, "irda")) {
uart_log_raw("/tmp/qemu-irda-tx.raw", &ch, 1);
}
/* Non-blocking TX (2026-05-29 fix — observer-effect kill).
*
* `qemu_chr_fe_write_all` est synchrone : bloque ARM jusqu'à ce
* que tout soit écrit dans le chardev backend (PTY). Sous LOST
* cascade firmware (20-char × 217/sec = 37% du wall en sercomm
* print à 115200 bps), ARM saturait → frame_irq pas servicé →
* next check_lost_frame voit pire diff → encore LOST → boucle
* self-amplifiante. La mesure était la maladie.
*
* Maintenant : write non-blocking. Si le backend ne peut pas
* absorber tout immédiatement, on DROP (= la trace diagnostique
* a priorité moindre que l'avancement firmware). Firmware ne
* voit jamais le backpressure → frame_irq toujours servicé à
* temps → boucle LOST cassée. */
(void)qemu_chr_fe_write(&s->chr, &ch, 1);
/* Feed TX byte to L1CTL socket (sercomm parser).
* Cette voie n'a pas le problème de backpressure (= unix socket
* non-bloquant by default), on la laisse synchrone. */
if (s->label && !strcmp(s->label, "modem")) {
l1ctl_sock_uart_tx_byte(ch);
}
s->lsr |= LSR_THRE | LSR_TEMT;
s->thr_empty_pending = true;
s->tx_empty_reads = 0; /* reset burst counter — ISR wrote a byte */
calypso_uart_update_irq(s);
}
break;
case REG_IER:
if (s->lcr & LCR_DLAB) {
s->dlh = value;
} else {
uint8_t old = s->ier;
s->ier = value & 0x0F;
if (old != s->ier && s->label && strcmp(s->label, "modem") != 0) {
/* Off-main-thread : ARM TCG ne bloque pas sur stdio.
* Avant : fprintf inline → IER toggle 1.25 Hz × ~50µs
* stdio lock = jitter cumulé sur ARM. */
calypso_async_log("[UART:%s] IER=0x%02x (RX=%d TX=%d)\n",
s->label ? s->label : "?",
s->ier,
!!(s->ier & IER_RX_DATA),
!!(s->ier & IER_TX_EMPTY));
}
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) {
if (s->rx_count > 0) {
fprintf(stderr, "[UART:%s] FCR_RX_RESET with %u bytes in FIFO!\n",
s->label ? s->label : "?", (unsigned)s->rx_count);
}
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;
fprintf(stderr, "[UART:%s] MDR1=0x%02x\n",
s->label ? s->label : "?",
(unsigned)value);
break;
case REG_SCR:
s->scr = value;
fprintf(stderr, "[UART:%s] SCR=0x%02x\n",
s->label ? s->label : "?",
(unsigned)value);
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 }, };
/* —- QOM —- */
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);
fprintf(stderr, "### UART PATCH ACTIVE ###\n");
fprintf(stderr, "[UART:%s] realize: chardev %s\n",
s->label ? s->label : "?",
connected ? "CONNECTED" : "NONE");
if (connected) {
qemu_chr_fe_set_handlers(&s->chr,
calypso_uart_can_receive,
calypso_uart_receive,
NULL, NULL,
s,
NULL, true);
fprintf(stderr, "[UART:%s] handlers installed, opaque=%p\n",
s->label ? s->label : "?",
(void *)s);
/* Start RX poll timer using REALTIME clock to force the CPU to
* yield and process chardev I/O from the PTY backend. */
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)
================================================================================
FILE: hw/intc/calypso_inth.c SIZE: 11231 bytes, 312 lines
================================================================================
/ calypso_inth.c — Calypso INTH (Interrupt Handler)
Level-sensitive interrupt controller at 0xFFFFFA00. * 32 IRQ inputs,
priority-based arbitration, IRQ/FIQ routing via ILR. The
Calypso INTH is LEVEL-SENSITIVE: it tracks the current level of * each
input line. When a peripheral deasserts its IRQ (e.g. UART clears *
TX_EMPTY by reading IIR), the INTH immediately sees the change.
Simplified model: no nesting, no irq_in_service blocking. The ARM
CPU’s * own CPSR I-bit prevents re-entry. We just present the
highest-priority * active IRQ at all times. Edge-triggered sources
(TPU_FRAME=4, TPU_PAGE=5) * are cleared on IRQ_NUM read.
SPDX-License-Identifier: GPL-2.0-or-later */
#include “qemu/osdep.h” #include “hw/irq.h” #include “hw/sysbus.h”
#include “qemu/log.h” #include “hw/arm/calypso/calypso_inth.h”
/* —- Priority arbitration —- */
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;
/* AUDIT FIX 2026-05-08 night : was a single-best arbitration that
* conflated IRQ and FIQ channels. When both an IRQ-routed and an
* FIQ-routed source were active simultaneously, the higher-priority
* winner would raise its parent line AND lower the other, killing
* any pending interrupt on the losing channel.
*
* In ARM, FIQ and IRQ are two independent CPU lines with separate
* vectors, separate disable bits (CPSR.F vs CPSR.I), and separate
* acknowledgement (FIQ_NUM vs IRQ_NUM registers). They MUST be
* arbitrated independently.
*
* Concrete failure observed under -icount auto :
* SIM (line 6, ILR[6]=0x1ffc → FIQ bit set) raised the FIQ line.
* UART_MODEM (line 7, IRQ-routed) was also active.
* Single-best arbitration picked UART (lower prio value), raised
* parent_irq, LOWERED parent_fiq → ARM never got FIQ → sim_irq_handler
* never ran → rxDoneFlag never set → ARM busy-loop forever at 0x822b90.
*
* Round-robin scan within each channel separately. */
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; }
}
}
/* Drive parent_irq line independently */
if (best_irq >= 0) {
s->ith_v = best_irq; /* IRQ_NUM read returns this */
qemu_irq_raise(s->parent_irq);
} else {
if (best_fiq < 0) s->ith_v = 0;
qemu_irq_lower(s->parent_irq);
}
/* Drive parent_fiq line independently */
if (best_fiq >= 0) {
s->fiq_v = best_fiq; /* FIQ_NUM read returns this */
qemu_irq_raise(s->parent_fiq);
} else {
qemu_irq_lower(s->parent_fiq);
}
}
/* —- GPIO input handler (one per IRQ line) —- */
static void calypso_inth_set_irq(void opaque, int irq, int level)
{ CalypsoINTHState s = CALYPSO_INTH(opaque);
/* AUDIT INSTRUMENTATION 2026-05-08 night : trace SIM (irq 6) raises
* with current mask state — disambiguates whether SIM IRQ propagates
* to ARM or is blocked by mask. Cap log to avoid flood. */
if (irq == 6 /* SIM */) {
static unsigned sim_log;
if (sim_log++ < 60)
fprintf(stderr,
"[INTH] LINE-SET sim(6) level=%d mask=0x%08x "
"bit6_masked=%d prev_levels=0x%08x ilr[6]=0x%04x\n",
level, s->mask,
!!(s->mask & (1u<<6)), s->levels, s->ilr[6]);
}
if (level) {
s->levels |= (1u << irq);
} else {
s->levels &= ~(1u << irq);
}
calypso_inth_update(s);
}
/* —- MMIO read/write —- */
static uint64_t calypso_inth_read(void opaque, hwaddr offset,
unsigned size) { CalypsoINTHState s = CALYPSO_INTH(opaque);
switch (offset) {
case 0x00: /* IT_REG1 — active bits [15:0] */
return s->levels & 0xFFFF;
case 0x02: /* IT_REG2 — active bits [31:16] */
return (s->levels >> 16) & 0xFFFF;
case 0x08: /* MASK_IT_REG1 */
return s->mask & 0xFFFF;
case 0x0a: /* MASK_IT_REG2 */
return (s->mask >> 16) & 0xFFFF;
case 0x10: /* IRQ_NUM — read returns current highest-priority IRQ */
case 0x80: /* IRQ_NUM (legacy) */
{
uint16_t num = s->ith_v;
/* Clear level for edge-like sources (TPU_FRAME=4, TPU_PAGE=5, API=15).
* These pulse once per event; clearing here prevents re-trigger
* until the next event raises the line again. */
if (num == 4 || num == 5 || num == 15) {
s->levels &= ~(1u << num);
}
/* Re-evaluate immediately: if other active IRQs remain,
* keep CPU IRQ line high so the firmware can chain ISRs
* without returning to the main loop. */
calypso_inth_update(s);
{
static uint32_t total = 0;
static uint32_t irq7_count = 0;
total++;
if (num == 7) {
irq7_count++;
if (irq7_count <= 50 || (irq7_count % 100) == 0)
fprintf(stderr, "[INTH] IRQ7 dispatch #%u (total=%u) levels=0x%08x mask=0x%08x\n",
irq7_count, total, s->levels, s->mask);
}
if (total <= 20 || total == 100 || total == 500 || total == 1000)
fprintf(stderr, "[INTH] IRQ_NUM=%u (#%u) levels=0x%08x mask=0x%08x\n",
num, total, s->levels, s->mask);
}
return num;
}
case 0x12: /* FIQ_NUM */
case 0x82: /* FIQ_NUM (legacy) */
{
/* AUDIT FIX 2026-05-08 night : returns separately-arbitrated FIQ
* source number (was returning ith_v, the IRQ winner — wrong for
* FIQ acknowledgement). Edge-clear for FIQ-routed edge sources too. */
uint16_t num = s->fiq_v;
if (num == 4 || num == 5 || num == 15) {
s->levels &= ~(1u << num);
}
calypso_inth_update(s);
static unsigned fiq_log;
if (fiq_log++ < 30)
fprintf(stderr, "[INTH] FIQ_NUM=%u read levels=0x%08x mask=0x%08x\n",
num, s->levels, s->mask);
return num;
}
case 0x14: /* IRQ_CTRL */
case 0x84: /* IRQ_CTRL (legacy) */
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 0x08: /* MASK_IT_REG1 */
{
uint32_t old = s->mask;
s->mask = (s->mask & 0xFFFF0000) | (value & 0xFFFF);
/* AUDIT INSTRUMENTATION 2026-05-08 night : trace mask writes to
* disambiguate icount-vs-mask race for SIM IRQ (bit 6). */
static unsigned mask_log;
if (mask_log++ < 50)
fprintf(stderr,
"[INTH] MASK-W LO val=0x%04x full 0x%08x → 0x%08x "
"bit6(SIM)=%d bit7(UART)=%d levels=0x%08x\n",
(unsigned)value, old, s->mask,
!!(s->mask & (1u<<6)), !!(s->mask & (1u<<7)),
s->levels);
calypso_inth_update(s);
break;
}
case 0x0a: /* MASK_IT_REG2 */
{
uint32_t old = s->mask;
s->mask = (s->mask & 0x0000FFFF) | ((value & 0xFFFF) << 16);
static unsigned mask_log_hi;
if (mask_log_hi++ < 50)
fprintf(stderr,
"[INTH] MASK-W HI val=0x%04x full 0x%08x → 0x%08x\n",
(unsigned)value, old, s->mask);
calypso_inth_update(s);
break;
}
case 0x14: /* IRQ_CTRL — end-of-service acknowledge */
case 0x84:
{
/* Advance round-robin past the IRQ just serviced.
* Only advance if the serviced IRQ was actually active
* (not a spurious read of ith_v=0 when nothing was pending). */
uint16_t svc = s->ith_v;
if (svc > 0 || (s->levels & 1)) {
/* Real IRQ was serviced — advance past it */
s->rr_start = (svc + 1) % CALYPSO_INTH_NUM_IRQS;
}
calypso_inth_update(s);
break;
}
default:
if (offset >= 0x20 && offset < 0x60) {
int idx = (offset - 0x20) / 2;
s->ilr[idx] = value & 0x1FFF;
/* Force UART (IRQ7) to same priority as TPU_FRAME (IRQ4).
* Firmware sets IRQ7 to prio 31 which causes starvation. */
if (idx == 7) {
s->ilr[7] = (s->ilr[7] & ~0x1F) | (s->ilr[4] & 0x1F);
}
/* Same fix for UART_IRDA (IRQ18) — under -icount the IRDA RX
* IRQ is starved by IRQ7 if left at firmware's default prio 31.
* IrDA is the firmware logging channel ; without it, fw-irda.log
* stays empty and the operator loses runtime visibility. */
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 }, };
/* —- QOM lifecycle —- */
static void calypso_inth_realize(DeviceState *dev, Error **errp) {
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->irq_in_service = -1;
s->rr_start = 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)
================================================================================
FILE: hw/ssi/calypso_i2c.c SIZE: 1796 bytes, 69 lines
================================================================================
/ Calypso I2C Controller - Minimal stub * Returns “ready”
immediately to avoid firmware blocking */
#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: / STATUS - always
ready / return 0x04; / ARDY (access ready) */ default: return
0; } }
static void calypso_i2c_write(void opaque, hwaddr offset,
uint64_t value, unsigned size) { / Accept all writes silently */
}
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)
================================================================================
FILE: hw/ssi/calypso_spi.c SIZE: 5558 bytes, 205 lines
================================================================================
/ calypso_spi.c — Calypso SPI + TWL3025 ABB
REWRITE: Correct register map + poweroff blocking. The
OsmocomBB loader calls twl3025_power_off() (writes TOGBR1 bit 0) *
whenever flash_init() fails. In QEMU we block this to keep the * loader
alive so osmoload can still inject firmware.
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”
/* Register offsets */ #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
/* CTRL bits */ #define SPI_CTRL_START (1 << 0)
/* —- TWL3025 ABB SPI transaction —- */
static uint16_t twl3025_spi_xfer(CalypsoSPIState *s, uint16_t tx) {
int read = (tx >> 15) & 1; int addr = (tx >> 6) &
0x1FF; int wdata = tx & 0x3F;
if (addr >= 256) {
addr = 0;
}
if (read) {
fprintf(stderr, "[SPI] ABB read addr=0x%02x → 0x%04x\n",
addr, s->abb_regs[addr]);
return s->abb_regs[addr];
} else {
fprintf(stderr, "[SPI] ABB write addr=0x%02x data=0x%02x", addr, wdata);
/* ---- TOGBR1 (0x09): power control toggle ----
* Bit 0 (TOGB) = power off the phone.
* The loader calls twl3025_power_off() which writes 1 here
* whenever flash_init() fails.
* We BLOCK this to keep the loader alive in QEMU.
*/
if (addr == ABB_TOGBR1 && (wdata & 0x01)) {
fprintf(stderr, " *** POWEROFF BLOCKED (TOGBR1 bit 0) ***\n");
return 0; /* Don't store, don't poweroff */
}
/* ---- TOGBR2 (0x0A): other toggles ---- */
if (addr == ABB_TOGBR2) {
fprintf(stderr, " (TOGBR2)\n");
s->abb_regs[addr] = wdata;
return 0;
}
fprintf(stderr, "\n");
s->abb_regs[addr] = wdata;
if (addr == ABB_VRPCDEV) {
s->abb_regs[ABB_VRPCSTS] = 0x1F;
}
return 0;
}
}
/* —- MMIO read —- */
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:
return SPI_STATUS_RE;
case SPI_REG_TX_LSB:
return s->tx_data & 0xFF;
case SPI_REG_TX_MSB:
return (s->tx_data >> 8) & 0xFF;
case SPI_REG_RX_LSB:
return s->rx_data & 0xFF;
case SPI_REG_RX_MSB:
return (s->rx_data >> 8) & 0xFF;
default:
qemu_log_mask(LOG_UNIMP, "calypso-spi: read at 0x%02x\n",
(unsigned)offset);
return 0;
}
}
/* —- MMIO write —- */
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_START) {
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_data = (s->tx_data & 0xFF00) | (value & 0xFF);
break;
case SPI_REG_TX_MSB:
s->tx_data = (s->tx_data & 0x00FF) | ((value & 0xFF) << 8);
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 }, };
/* —- QOM lifecycle —- */
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;
s->tx_data = 0;
s->rx_data = 0;
memset(s->abb_regs, 0, sizeof(s->abb_regs));
s->abb_regs[ABB_VRPCSTS] = 0x1F;
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)
================================================================================
FILE: hw/timer/calypso_timer.c SIZE: 12368 bytes, 331 lines
================================================================================
/ calypso_timer.c — Calypso GP/Watchdog Timer
16-bit down-counter with auto-reload, prescaler, and IRQ. * Calypso base
clock: 13 MHz. The silicon has a fixed /32 hardware * prescaler ahead of
the user-visible PRESCALER field, so: tick_freq = 13 MHz /
(32 << PRESCALER) With PRESCALER=0 the timer ticks at
13e6/32 ≈ 406.25 kHz, which is * what osmocom-bb’s check_lost_frame()
expects (1875 ticks ≈ 4615 µs * = one TDMA frame). Without the fixed /32
the firmware sees thousands * of “LOST N!” because the timer wraps
multiple times per frame. Register map (firmware uses byte
access on CNTL, word access on LOAD/READ): * 0x00 CNTL bit 0 = START *
bit 1 = AUTO_RELOAD * bits 4:2 = PRESCALER (0..7) → user divider * bit 5
= CLOCK_ENABLE (timer ticks only when also START) * 0x02 LOAD Reload
value (16-bit) * 0x04 READ Current count (16-bit, read-only)
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”
/* Layout matches osmocom-bb firmware (calypso/timer.c). The timer
only * ticks when both START and CLOCK_ENABLE are set; hwtimer_read()
polls * those exact bits before returning the count, so they MUST
round-trip * through readb/writeb intact — otherwise the firmware sees
the timer * as stopped and returns 0xFFFF, producing a constant “LOST
0!” stream * every TDMA frame because diff = 0xFFFF - 0xFFFF = 0. */
#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 /* 13 MHz */
/* Domaine de temps du hwtimer. DOIT être le MÊME que le
tdma_tick/frame-IRQ * (calypso_trx.c calypso_tdma_clock()), sinon le
firmware mesure les ticks * hwtimer (une horloge) entre deux frame-IRQ
(une AUTRE horloge) -> le ratio * 1875 ticks/trame ne tient pas ->
“LOST N!” en continu, et icount ne peut RIEN * (le hwtimer sur REALTIME
est immunisé à icount). Défaut VIRTUAL (single-domain, * verrouillé sous
icount=auto) ; opt-in wall via la MÊME gate que le tick. / static
QEMUClockType calypso_timer_clock(void) { static int cached = -1; if
(cached < 0) { const char e = getenv(“CALYPSO_TDMA_REALTIME”);
cached = (e && *e == ‘1’) ? 1 : 0; } return cached ?
QEMU_CLOCK_REALTIME : QEMU_CLOCK_VIRTUAL; }
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; / Silicon has a fixed /32 hardware
prescaler in front of PRESCALER. */ int64_t divider = 32LL <<
prescaler; int64_t freq = CALYPSO_BASE_CLK / divider; if (freq <= 0)
freq = 1; s->tick_ns = NANOSECONDS_PER_SECOND / freq; }
/* Compute current count by interpolating virtual time elapsed since
the * timer was (re)started — avoids scheduling one QEMU event per
decrement, * which would coalesce on the QEMU virtual clock granularity
and make the * effective tick rate roughly half of what the firmware
expects. / 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) {
/ Schedule the next IRQ at the moment count would reach 0 from the
* current virtual time. / 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) {
/* Reanchor epoch to "now" so the next read sees count=load. */
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); }
/* —- Frame-locked lost-frame latch (timer #1 only) —- * Firmware
check_lost_frame() (layer1/sync.c) reads timer #1 once per TDMA *
frame-IRQ and expects the count to advance by exactly 1875 ticks between
two * IRQs (tolérance ±1). Sous CALYPSO_TDMA_REALTIME=1 + -icount auto,
l’instant * d’échantillonnage jitte (±~13 ticks) -> “LOST N!” en
continu. On fige la * valeur READ de timer #1 sur une grille dérivée du
FN : count(fn) = load - * (fn mod 4)1875 ∈ {7499,5624,3749,1874}.
Deux frames consécutives diffèrent de 1875 pile -> zéro LOST ;
un vrai saut (fn +k) donne k1875 -> LOST légitime conservé.
timer #1 est le SEUL hwtimer lu par check_lost_frame(). */ #define
LOST_TICKS_PER_TDMA 1875
static CalypsoTimerState g_lost_timer; / timer #1 @
0xFFFE3800 */
static bool calypso_lost_latch_enabled(void) { static int cached =
-1; /* défaut ON ; opt-out CALYPSO_LOST_LATCH=0 / if (cached < 0)
{ const char e = getenv(“CALYPSO_LOST_LATCH”); cached = (e
&& *e == ‘0’) ? 0 : 1; } return cached; }
/* Read-driven lost-frame grid (timer #1 only). * check_lost_frame()
(layer1/sync.c:189) est le SEUL lecteur de timer #1 dans * tout le
firmware — hwtimer_read() n’a pas d’autre appelant, et les delay_
sont des busy-loops, pas des lectures hwtimer. Il lit 0x04
exactement 1× par * l1_sync (= 1× par frame-IRQ reçue) et attend un
décrément de 1875 pile entre * deux lectures. La dérive trx_fn/sch_fn
(delta 1..6 qui snap-back) fait sauter * la grille fn-dérivée -> le
firmware voit k1875 -> “LOST k1875!”. En calant la
grille sur la LECTURE (décrément de 1875 pile par read, wrap mod period)
on * garantit diff==1875 à chaque l1_sync -> zéro LOST, sans aucun
effet de bord * (aucun autre code ne lit timer #1). Gate
CALYPSO_LOST_READ_DRIVEN, défaut ON. / static bool
calypso_lost_read_driven(void) { static int cached = -1; if (cached <
0) { const char e = getenv(”CALYPSO_LOST_READ_DRIVEN”); cached = (e
&& e == ‘0’) ? 0 : 1; } return cached; }
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 ||
!calypso_lost_latch_enabled()) { return; } uint32_t period =
(uint32_t)s->load + 1; / 7500 / uint32_t step =
((uint64_t)fn LOST_TICKS_PER_TDMA) % period;
s->lost_latch_count = (uint16_t)(s->load - step); /* down-counter
*/ s->lost_latch_active = true; }
/* —- MMIO —- */
static uint64_t calypso_timer_read(void opaque, hwaddr offset,
unsigned size) { CalypsoTimerState s = CALYPSO_TIMER(opaque);
{
static int rd_count = 0;
static int64_t prev_t_virt = 0;
if (rd_count < 0) { /* DISABLED — re-enable by setting >0 */
uint16_t live = calypso_timer_current_count(s);
int64_t now = qemu_clock_get_ns(calypso_timer_clock());
int64_t dt = prev_t_virt ? (now - prev_t_virt) : 0;
fprintf(stderr, "[timer] RD ts=%p off=0x%02x live=%u stored=%u "
"running=%d tick_ns=%" PRId64 " epoch=%" PRId64
" t_virt=%" PRId64 " dt=%" PRId64 " rd#=%d\n",
(void *)s, (unsigned)offset, live, s->count, s->running,
s->tick_ns, s->epoch_ns, now, dt, rd_count);
prev_t_virt = now;
rd_count++;
}
}
switch (offset) {
case 0x00: return s->ctrl;
case 0x02: return s->load;
case 0x04:
if (s->lost_latch_active) { /* frame-locked value for check_lost_frame() */
if (s == g_lost_timer && calypso_lost_read_driven()) {
/* Grille calée sur la lecture : chaque read décroît de 1875 pile
* (wrap mod period) -> diff==1875 systématique -> zéro LOST. */
uint32_t period = (uint32_t)s->load + 1; /* 7500 */
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: { /* CNTL — preserve all 8 bits the firmware writes */
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)) {
/* prescaler changed mid-run — re-anchor at current count */
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: /* LOAD */
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 }, };
/* —- QOM lifecycle —- */
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)
================================================================================
FILE: tools/calypso-ipc-device/_ref_ipc-driver-test.c SIZE: 13310 bytes,
492 lines
================================================================================
/ Copyright 2020 sysmocom - s.f.m.c. GmbH info@sysmocom.de *
Author: Pau Espin Pedrol pespin@sysmocom.de SPDX-License-Identifier:
0BSD Permission to use, copy, modify, and/or distribute this
software for any purpose * with or without fee is hereby granted.THE
SOFTWARE IS PROVIDED “AS IS” AND THE * AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL * IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR * BE LIABLE
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF * CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE * USE OR PERFORMANCE OF THIS
SOFTWARE. */
#define _GNU_SOURCE #include <pthread.h>
#include <debug.h>
#include <stdio.h> #include <stdlib.h> #include
<unistd.h> #include <string.h> #include <errno.h>
#include <assert.h> #include <sys/socket.h> #include
<sys/un.h> #include <inttypes.h> #include <sys/mman.h>
#include <sys/stat.h> /* For mode constants / #include
<fcntl.h> / For O_* constants */ #include
<getopt.h>
#include <osmocom/core/application.h> #include
<osmocom/core/talloc.h> #include <osmocom/core/select.h>
#include <osmocom/core/socket.h> #include
<osmocom/core/logging.h> #include <osmocom/core/utils.h>
#include <osmocom/core/msgb.h> #include
<osmocom/core/select.h> #include <osmocom/core/timer.h>
#include “shm.h” #include “ipc_shm.h” #include “ipc_chan.h” #include
“ipc_sock.h”
#define DEFAULT_SHM_NAME “/osmo-trx-ipc-driver-shm2” #define
IPC_SOCK_PATH_PREFIX “/tmp”
static void tall_ctx; struct ipc_sock_state
global_ipc_sock_state;
/* 8 channels are plenty / struct ipc_sock_state
global_ctrl_socks[8]; struct ipc_shm_io ios_tx_to_device[8];
struct ipc_shm_io ios_rx_from_device[8];
void shm; void global_dev;
static struct ipc_shm_region *decoded_region;
static struct { int msocknum; char *ud_prefix_dir; } cmdline_cfg;
static const struct log_info_cat default_categories[] = { [DMAIN] = {
.name = “DMAIN”, .color = NULL, .description = “Main generic category”,
.loglevel = LOGL_DEBUG,.enabled = 1, }, [DDEV] = { .name = “DDEV”,
.description = “Device/Driver specific code”, .color = NULL, .enabled =
1, .loglevel = LOGL_DEBUG, }, };
const struct log_info log_infox = { .cat = default_categories,
.num_cat = ARRAY_SIZE(default_categories), };
#include “uhdwrap.h”
volatile int ipc_exit_requested = 0;
static int ipc_shm_setup(const char *shm_name, size_t shm_len) { int
fd; int rc;
LOGP(DMAIN, LOGL_NOTICE, "Opening shm path %s\n", shm_name);
if ((fd = shm_open(shm_name, O_CREAT | O_RDWR | O_TRUNC, S_IRUSR | S_IWUSR)) < 0) {
LOGP(DMAIN, LOGL_ERROR, "shm_open %d: %s\n", errno, strerror(errno));
rc = -errno;
goto err_shm_open;
}
LOGP(DMAIN, LOGL_NOTICE, "Truncating %d to size %zu\n", fd, shm_len);
if (ftruncate(fd, shm_len) < 0) {
LOGP(DMAIN, LOGL_ERROR, "ftruncate %d: %s\n", errno, strerror(errno));
rc = -errno;
goto err_mmap;
}
LOGP(DMAIN, LOGL_NOTICE, "mmaping shared memory fd %d\n", fd);
if ((shm = mmap(NULL, shm_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) {
LOGP(DMAIN, LOGL_ERROR, "mmap %d: %s\n", errno, strerror(errno));
rc = -errno;
goto err_mmap;
}
LOGP(DMAIN, LOGL_NOTICE, "mmap'ed shared memory at addr %p\n", shm);
/* After a call to mmap(2) the file descriptor may be closed without affecting the memory mapping. */
close(fd);
return 0;
err_mmap: shm_unlink(shm_name); close(fd); err_shm_open: return rc;
}
struct msgb ipc_msgb_alloc(uint8_t msg_type) { struct msgb
msg; struct ipc_sk_if *ipc_prim;
msg = msgb_alloc(sizeof(struct ipc_sk_if) + 1000, "ipc_sock_tx");
if (!msg)
return NULL;
msgb_put(msg, sizeof(struct ipc_sk_if) + 1000);
ipc_prim = (struct ipc_sk_if *)msg->data;
ipc_prim->msg_type = msg_type;
return msg;
}
static int ipc_tx_greeting_cnf(uint8_t req_version) { struct msgb
msg; struct ipc_sk_if ipc_prim;
msg = ipc_msgb_alloc(IPC_IF_MSG_GREETING_CNF);
if (!msg)
return -ENOMEM;
ipc_prim = (struct ipc_sk_if *)msg->data;
ipc_prim->u.greeting_cnf.req_version = req_version;
return ipc_sock_send(msg);
}
static int ipc_tx_info_cnf() { struct msgb msg; struct ipc_sk_if
ipc_prim;
msg = ipc_msgb_alloc(IPC_IF_MSG_INFO_CNF);
if (!msg)
return -ENOMEM;
ipc_prim = (struct ipc_sk_if *)msg->data;
uhdwrap_fill_info_cnf(ipc_prim);
return ipc_sock_send(msg);
}
static int ipc_tx_open_cnf(int rc, uint32_t num_chans, int32_t
timingoffset) { struct msgb msg; struct ipc_sk_if ipc_prim;
struct ipc_sk_if_open_cnf_chan *chan_info; unsigned int i;
msg = ipc_msgb_alloc(IPC_IF_MSG_OPEN_CNF);
if (!msg)
return -ENOMEM;
ipc_prim = (struct ipc_sk_if *)msg->data;
ipc_prim->u.open_cnf.return_code = rc;
ipc_prim->u.open_cnf.path_delay = timingoffset; // 6.18462e-5 * 1625e3 / 6;
OSMO_STRLCPY_ARRAY(ipc_prim->u.open_cnf.shm_name, DEFAULT_SHM_NAME);
chan_info = ipc_prim->u.open_cnf.chan_info;
for (i = 0; i < num_chans; i++) {
snprintf(chan_info->chan_ipc_sk_path, sizeof(chan_info->chan_ipc_sk_path),"%s/ipc_sock%d_%d",
cmdline_cfg.ud_prefix_dir, cmdline_cfg.msocknum, i);
/* FIXME: dynamc chan limit, currently 8 */
if (i < 8)
ipc_sock_init(chan_info->chan_ipc_sk_path, &global_ctrl_socks[i], ipc_chan_sock_accept, i);
chan_info++;
}
return ipc_sock_send(msg);
}
int ipc_rx_greeting_req(struct ipc_sk_if_greeting *greeting_req) { if
(greeting_req->req_version == IPC_SOCK_API_VERSION)
ipc_tx_greeting_cnf(IPC_SOCK_API_VERSION); else ipc_tx_greeting_cnf(0);
return 0; }
int ipc_rx_info_req(struct ipc_sk_if_info_req *info_req) {
ipc_tx_info_cnf(); return 0; }
int ipc_rx_open_req(struct ipc_sk_if_open_req open_req) { /
calculate size needed */ unsigned int len; unsigned int i;
global_dev = uhdwrap_open(open_req);
/* b210 packet size is 2040, but our tx size is 2500, so just do *2 */
int shmbuflen = uhdwrap_get_bufsizerx(global_dev) * 2;
len = ipc_shm_encode_region(NULL, open_req->num_chans, 4, shmbuflen);
/* Here we verify num_chans, rx_path, tx_path, clockref, etc. */
int rc = ipc_shm_setup(DEFAULT_SHM_NAME, len);
len = ipc_shm_encode_region((struct ipc_shm_raw_region *)shm, open_req->num_chans, 4, shmbuflen);
// LOGP(DMAIN, LOGL_NOTICE, "%s\n", osmo_hexdump((const unsigned char *)shm, 80));
/* set up our own copy of the decoded area, we have to do it here,
* since the uhd wrapper does not allow starting single channels
* additionally go for the producer init for both, so only we are responsible for the init, instead
* of splitting it with the client and causing potential races if one side uses it too early */
decoded_region = ipc_shm_decode_region(0, (struct ipc_shm_raw_region *)shm);
for (i = 0; i < open_req->num_chans; i++) {
// ios_tx_to_device[i] = ipc_shm_init_consumer(decoded_region->channels[i]->dl_stream);
ios_tx_to_device[i] = ipc_shm_init_producer(decoded_region->channels[i]->dl_stream);
ios_rx_from_device[i] = ipc_shm_init_producer(decoded_region->channels[i]->ul_stream);
}
ipc_tx_open_cnf(-rc, open_req->num_chans, uhdwrap_get_timingoffset(global_dev));
return 0;
}
volatile bool ul_running = false; volatile bool dl_running =
false;
void uplink_thread(void x_void_ptr) { uint32_t chann =
decoded_region->num_chans; ul_running = true;
pthread_setname_np(pthread_self(), “uplink_rx”);
while (!ipc_exit_requested) {
int32_t read = uhdwrap_read(global_dev, chann);
if (read < 0)
return 0;
}
return 0;
}
void downlink_thread(void x_void_ptr) { int chann =
decoded_region->num_chans; dl_running = true;
pthread_setname_np(pthread_self(), “downlink_tx”);
while (!ipc_exit_requested) {
bool underrun;
uhdwrap_write(global_dev, chann, &underrun);
}
return 0;
}
int ipc_rx_chan_start_req(struct ipc_sk_chan_if_op_void req,
uint8_t chan_nr) { struct msgb msg; struct ipc_sk_chan_if
*ipc_prim; int rc = 0;
rc = uhdwrap_start(global_dev, chan_nr);
/* no per-chan start/stop */
if (!dl_running || !ul_running) {
/* chan != first chan start will "fail", which is fine, usrp can't start/stop chans independently */
if (rc) {
LOGP(DMAIN, LOGL_INFO, "starting rx/tx threads.. req for chan:%d\n", chan_nr);
pthread_t rx, tx;
pthread_create(&rx, NULL, uplink_thread, 0);
pthread_create(&tx, NULL, downlink_thread, 0);
}
} else
LOGP(DMAIN, LOGL_INFO, "starting rx/tx threads request ignored.. req for chan:%d\n", chan_nr);
msg = ipc_msgb_alloc(IPC_IF_MSG_START_CNF);
if (!msg)
return -ENOMEM;
ipc_prim = (struct ipc_sk_chan_if *)msg->data;
ipc_prim->u.start_cnf.return_code = rc ? 0 : -1;
return ipc_chan_sock_send(msg, chan_nr);
} int ipc_rx_chan_stop_req(struct ipc_sk_chan_if_op_void req,
uint8_t chan_nr) { struct msgb msg; struct ipc_sk_chan_if
*ipc_prim; int rc;
/* no per-chan start/stop */
rc = uhdwrap_stop(global_dev, chan_nr);
msg = ipc_msgb_alloc(IPC_IF_MSG_STOP_CNF);
if (!msg)
return -ENOMEM;
ipc_prim = (struct ipc_sk_chan_if *)msg->data;
ipc_prim->u.stop_cnf.return_code = rc ? 0 : -1;
return ipc_chan_sock_send(msg, chan_nr);
} int ipc_rx_chan_setgain_req(struct ipc_sk_chan_if_gain req,
uint8_t chan_nr) { struct msgb msg; struct ipc_sk_chan_if
*ipc_prim; double rv;
rv = uhdwrap_set_gain(global_dev, req->gain, chan_nr, req->is_tx);
msg = ipc_msgb_alloc(IPC_IF_MSG_SETGAIN_CNF);
if (!msg)
return -ENOMEM;
ipc_prim = (struct ipc_sk_chan_if *)msg->data;
ipc_prim->u.set_gain_cnf.is_tx = req->is_tx;
ipc_prim->u.set_gain_cnf.gain = rv;
return ipc_chan_sock_send(msg, chan_nr);
}
int ipc_rx_chan_setfreq_req(struct ipc_sk_chan_if_freq_req req,
uint8_t chan_nr) { struct msgb msg; struct ipc_sk_chan_if
*ipc_prim; bool rv;
rv = uhdwrap_set_freq(global_dev, req->freq, chan_nr, req->is_tx);
msg = ipc_msgb_alloc(IPC_IF_MSG_SETFREQ_CNF);
if (!msg)
return -ENOMEM;
ipc_prim = (struct ipc_sk_chan_if *)msg->data;
ipc_prim->u.set_freq_cnf.return_code = rv ? 0 : 1;
return ipc_chan_sock_send(msg, chan_nr);
}
int ipc_rx_chan_settxatten_req(struct ipc_sk_chan_if_tx_attenuation
req, uint8_t chan_nr) { struct msgb msg; struct ipc_sk_chan_if
*ipc_prim; double rv;
rv = uhdwrap_set_txatt(global_dev, req->attenuation, chan_nr);
msg = ipc_msgb_alloc(IPC_IF_MSG_SETTXATTN_CNF);
if (!msg)
return -ENOMEM;
ipc_prim = (struct ipc_sk_chan_if *)msg->data;
ipc_prim->u.txatten_cnf.attenuation = rv;
return ipc_chan_sock_send(msg, chan_nr);
}
int ipc_sock_init(const char *path, struct ipc_sock_state
**global_state_var, int (sock_callback_fn)(struct osmo_fd fd,
unsigned int what), int n) { struct ipc_sock_state state; struct
osmo_fd bfd; int rc;
state = talloc_zero(NULL, struct ipc_sock_state);
if (!state)
return -ENOMEM;
*global_state_var = state;
INIT_LLIST_HEAD(&state->upqueue);
state->conn_bfd.fd = -1;
bfd = &state->listen_bfd;
bfd->fd = osmo_sock_unix_init(SOCK_SEQPACKET, 0, path, OSMO_SOCK_F_BIND);
if (bfd->fd < 0) {
LOGP(DMAIN, LOGL_ERROR, "Could not create %s unix socket: %s\n", path, strerror(errno));
talloc_free(state);
return -1;
}
osmo_fd_setup(bfd, bfd->fd, OSMO_FD_READ, sock_callback_fn, state, n);
rc = osmo_fd_register(bfd);
if (rc < 0) {
LOGP(DMAIN, LOGL_ERROR, "Could not register listen fd: %d\n", rc);
close(bfd->fd);
talloc_free(state);
return rc;
}
LOGP(DMAIN, LOGL_INFO, "Started listening on IPC socket: %s\n", path);
return 0;
}
static void print_help(void) { printf(“ipc-driver-test Usage:” ” -h
–help This message” ” -u –unix-sk-dir DIR Existing directory where to
create the Master socket” ” -n –sock-num NR Master socket suffix number
NR“); }
static void handle_options(int argc, char **argv) { while (1) { int
option_index = 0, c; const struct option long_options[] = { { “help”, 0,
0, ‘h’ }, { “unix-sk-dir”, 1, 0, ‘u’ }, { “sock-num”, 1, 0, ‘n’ }, { 0,
0, 0, 0 } };
c = getopt_long(argc, argv, "hu:n:", long_options, &option_index);
if (c == -1)
break;
switch (c) {
case 'h':
print_help();
exit(0);
break;
case 'u':
cmdline_cfg.ud_prefix_dir = talloc_strdup(tall_ctx, optarg);
break;
case 'n':
cmdline_cfg.msocknum = atoi(optarg);
break;
default:
exit(2);
break;
}
}
if (argc > optind) {
fprintf(stderr, "Unsupported positional arguments on command line\n");
exit(2);
}
}
int main(int argc, char **argv) { char ipc_msock_path[128]; tall_ctx
= talloc_named_const(NULL, 0, “OsmoTRX”); msgb_talloc_ctx_init(tall_ctx,
0); osmo_init_logging2(tall_ctx, &log_infox);
log_enable_multithread();
handle_options(argc, argv);
if (!cmdline_cfg.ud_prefix_dir)
cmdline_cfg.ud_prefix_dir = talloc_strdup(tall_ctx, IPC_SOCK_PATH_PREFIX);
snprintf(ipc_msock_path, sizeof(ipc_msock_path), "%s/ipc_sock%d", cmdline_cfg.ud_prefix_dir, cmdline_cfg.msocknum);
LOGP(DMAIN, LOGL_INFO, "Starting %s\n", argv[0]);
ipc_sock_init(ipc_msock_path, &global_ipc_sock_state, ipc_sock_accept, 0);
while (!ipc_exit_requested)
osmo_select_main(0);
if (global_dev) {
unsigned int i;
for (i = 0; i < decoded_region->num_chans; i++)
uhdwrap_stop(global_dev, i);
}
ipc_sock_close(global_ipc_sock_state);
return 0;
}
================================================================================
FILE: tools/calypso-ipc-device/_ref_uhdwrap.cpp SIZE: 7618 bytes, 255
lines
================================================================================
/ Copyright 2020 sysmocom - s.f.m.c. GmbH info@sysmocom.de *
Author: Eric Wild ewild@sysmocom.de SPDX-License-Identifier:
0BSD Permission to use, copy, modify, and/or distribute this
software for any purpose * with or without fee is hereby granted.THE
SOFTWARE IS PROVIDED “AS IS” AND THE * AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL * IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR * BE LIABLE
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF * CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE * USE OR PERFORMANCE OF THIS
SOFTWARE. */ extern “C” { #include <osmocom/core/application.h>
#include <osmocom/core/talloc.h> #include
<osmocom/core/select.h> #include <osmocom/core/socket.h>
#include <osmocom/core/logging.h> #include
<osmocom/core/utils.h> #include <osmocom/core/msgb.h>
#include <osmocom/core/select.h> #include
<osmocom/core/timer.h>
#include “shm.h” #include “ipc_shm.h” #include “ipc-driver-test.h” }
#include “../uhd/UHDDevice.h” #include “uhdwrap.h”
#include “Logger.h” #include “Threads.h” #include “Utils.h”
// no vty source for cfg params here, so we have to build our own
static struct trx_cfg actual_cfg = {};
int uhd_wrap::open() { int rv = uhd_device::open(); samps_per_buff_rx
= rx_stream->get_max_num_samps(); samps_per_buff_tx =
tx_stream->get_max_num_samps(); channel_count =
usrp_dev->get_rx_num_channels();
wrap_rx_buffs = std::vector<std::vector<short> >(channel_count, std::vector<short>(2 * samps_per_buff_rx));
for (size_t i = 0; i < wrap_rx_buffs.size(); i++)
wrap_rx_buf_ptrs.push_back(&wrap_rx_buffs[i].front());
wrap_tx_buffs = std::vector<std::vector<short> >(channel_count, std::vector<short>(2 * 5000));
for (size_t i = 0; i < wrap_tx_buffs.size(); i++)
wrap_tx_buf_ptrs.push_back(&wrap_tx_buffs[i].front());
return rv;
}
uhd_wrap::~uhd_wrap() { // drvtest::gshutdown = 1; //t->join();
}
size_t uhd_wrap::bufsizerx() { return samps_per_buff_rx; }
size_t uhd_wrap::bufsizetx() { return samps_per_buff_tx; }
int uhd_wrap::chancount() { return channel_count; }
int uhd_wrap::wrap_read(TIMESTAMP timestamp) { uhd::rx_metadata_t
md; size_t num_rx_samps = rx_stream->recv(wrap_rx_buf_ptrs,
samps_per_buff_rx, md, 0.1, true); timestamp =
md.time_spec.to_ticks(rx_rate); return num_rx_samps;
//uhd_device::readSamples(bufs, len, overrun, timestamp, underrun);
}
extern “C” void uhdwrap_open(struct ipc_sk_if_open_req
open_req) { actual_cfg.num_chans = open_req->num_chans;
actual_cfg.swap_channels = false; /* FIXME: this is actually the sps
value, not the sample rate! * sample rate is looked up according to the
sps rate by uhd backend */ actual_cfg.rx_sps =
open_req->rx_sample_freq_num / open_req->rx_sample_freq_den;
actual_cfg.tx_sps = open_req->tx_sample_freq_num /
open_req->tx_sample_freq_den;
/* FIXME: dev arg string* */
/* FIXME: rx frontend bw? */
/* FIXME: tx frontend bw? */
switch (open_req->clockref) {
case FEATURE_MASK_CLOCKREF_EXTERNAL:
actual_cfg.clock_ref = ReferenceType::REF_EXTERNAL;
break;
case FEATURE_MASK_CLOCKREF_INTERNAL:
default:
actual_cfg.clock_ref = ReferenceType::REF_INTERNAL;
break;
}
for (unsigned int i = 0; i < open_req->num_chans; i++) {
actual_cfg.chans[i].rx_path = open_req->chan_info[i].tx_path;
actual_cfg.chans[i].tx_path = open_req->chan_info[i].rx_path;
}
uhd_wrap *uhd_wrap_dev = new uhd_wrap(RadioDevice::NORMAL, &actual_cfg);
uhd_wrap_dev->open();
return uhd_wrap_dev;
} extern “C” int32_t uhdwrap_get_bufsizerx(void dev) { uhd_wrap
d = (uhd_wrap *)dev; return d->bufsizerx(); }
extern “C” int32_t uhdwrap_get_timingoffset(void dev) { uhd_wrap
d = (uhd_wrap *)dev; return d->getTimingOffset(); }
extern “C” int32_t uhdwrap_read(void dev, uint32_t num_chans) {
TIMESTAMP t; uhd_wrap d = (uhd_wrap *)dev;
if (num_chans != d->wrap_rx_buf_ptrs.size()) {
perror("omg chans?!");
}
int32_t read = d->wrap_read(&t);
/* multi channel rx on b210 will return 0 due to alignment adventures, do not put 0 samples into a ipc buffer... */
if (read <= 0)
return read;
for (uint32_t i = 0; i < num_chans; i++) {
ipc_shm_enqueue(ios_rx_from_device[i], t, read, (uint16_t *)&d->wrap_rx_buffs[i].front());
}
return read;
}
extern “C” int32_t uhdwrap_write(void dev, uint32_t num_chans,
bool underrun) { uhd_wrap d = (uhd_wrap )dev;
uint64_t timestamp;
int32_t len = -1;
for (uint32_t i = 0; i < num_chans; i++) {
len = ipc_shm_read(ios_tx_to_device[i], (uint16_t *)&d->wrap_tx_buffs[i].front(), 5000, ×tamp, 1);
if (len < 0)
return 0;
}
return d->writeSamples(d->wrap_tx_buf_ptrs, len, underrun, timestamp);
}
extern “C” double uhdwrap_set_freq(void dev, double f, size_t
chan, bool for_tx) { uhd_wrap d = (uhd_wrap *)dev; if (for_tx)
return d->setTxFreq(f, chan); else return d->setRxFreq(f, chan);
}
extern “C” double uhdwrap_set_gain(void dev, double f, size_t
chan, bool for_tx) { uhd_wrap d = (uhd_wrap *)dev; // if (for_tx)
// return d->setTxGain(f, chan); // else return d->setRxGain(f,
chan); }
extern “C” double uhdwrap_set_txatt(void dev, double a, size_t
chan) { uhd_wrap d = (uhd_wrap *)dev; return
d->setPowerAttenuation(a, chan); }
extern “C” int32_t uhdwrap_start(void dev, int chan) { uhd_wrap
d = (uhd_wrap *)dev; return d->start(); }
extern “C” int32_t uhdwrap_stop(void dev, int chan) { uhd_wrap
d = (uhd_wrap *)dev; return d->stop(); }
extern “C” void uhdwrap_fill_info_cnf(struct ipc_sk_if ipc_prim)
{ struct ipc_sk_if_info_chan chan_info;
uhd::device_addr_t args("");
uhd::device_addrs_t devs_found = uhd::device::find(args);
if (devs_found.size() < 1) {
std::cout << "\n No device found!";
exit(0);
}
uhd::usrp::multi_usrp::sptr usrp = uhd::usrp::multi_usrp::make(devs_found[0]);
auto rxchans = usrp->get_rx_num_channels();
auto txchans = usrp->get_tx_num_channels();
auto rx_range = usrp->get_rx_gain_range();
auto tx_range = usrp->get_tx_gain_range();
//auto nboards = usrp->get_num_mboards();
auto refs = usrp->get_clock_sources(0);
auto devname = usrp->get_mboard_name(0);
ipc_prim->u.info_cnf.feature_mask = 0;
if (std::find(refs.begin(), refs.end(), "internal") != refs.end())
ipc_prim->u.info_cnf.feature_mask |= FEATURE_MASK_CLOCKREF_INTERNAL;
if (std::find(refs.begin(), refs.end(), "external") != refs.end())
ipc_prim->u.info_cnf.feature_mask |= FEATURE_MASK_CLOCKREF_EXTERNAL;
// at least one duplex channel
auto num_chans = rxchans == txchans ? txchans : 1;
ipc_prim->u.info_cnf.iq_scaling_val_rx = 0.3;
ipc_prim->u.info_cnf.iq_scaling_val_tx = 1;
ipc_prim->u.info_cnf.max_num_chans = num_chans;
OSMO_STRLCPY_ARRAY(ipc_prim->u.info_cnf.dev_desc, devname.c_str());
chan_info = ipc_prim->u.info_cnf.chan_info;
for (unsigned int i = 0; i < ipc_prim->u.info_cnf.max_num_chans; i++) {
auto rxant = usrp->get_rx_antennas(i);
auto txant = usrp->get_tx_antennas(i);
for (unsigned int j = 0; j < txant.size(); j++) {
OSMO_STRLCPY_ARRAY(chan_info->tx_path[j], txant[j].c_str());
}
for (unsigned int j = 0; j < rxant.size(); j++) {
OSMO_STRLCPY_ARRAY(chan_info->rx_path[j], rxant[j].c_str());
}
chan_info->min_rx_gain = rx_range.start();
chan_info->max_rx_gain = rx_range.stop();
chan_info->min_tx_gain = tx_range.start();
chan_info->max_tx_gain = tx_range.stop();
chan_info->nominal_tx_power = 7.5; // FIXME: would require uhd dev + freq info
chan_info++;
}
}
================================================================================
FILE: tools/calypso-ipc-device/calypso_ipc_device.c SIZE: 13919 bytes,
505 lines
================================================================================
/ calypso-ipc-device — fork de ipc-driver-test.c (osmo-trx).
Bridge QEMU Calypso BSP (UDP 6702) ↔︎ osmo-trx-ipc (shm +
master Unix sock). * Garde l’infra IPC d’osmo-trx telle quelle
(greeting/info/open/start * handshake, shm rings, per-chan sockets) —
remplace seulement le backend * device : à la place d’UHD (B210 etc.),
c’est notre QEMU émulé qui * produit/consomme les samples via UDP 6702.
Implémentation des hooks uhdwrap_() : qemu_wrap.c (pas
uhdwrap.cpp). L’API publique de uhdwrap reste identique pour
réutiliser le framework IPC. Original copyright préservé
ci-dessous. Copyright 2020 sysmocom - s.f.m.c. GmbH info@sysmocom.de *
Author: Pau Espin Pedrol pespin@sysmocom.de SPDX-License-Identifier:
0BSD Permission to use, copy, modify, and/or distribute this
software for any purpose * with or without fee is hereby granted.THE
SOFTWARE IS PROVIDED “AS IS” AND THE * AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL * IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR * BE LIABLE
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF * CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE * USE OR PERFORMANCE OF THIS
SOFTWARE. */
#define _GNU_SOURCE #include <pthread.h>
#include “debug.h”
#include <stdio.h> #include <stdlib.h> #include
<unistd.h> #include <string.h> #include <errno.h>
#include <assert.h> #include <sys/socket.h> #include
<sys/un.h> #include <inttypes.h> #include <sys/mman.h>
#include <sys/stat.h> /* For mode constants / #include
<fcntl.h> / For O_* constants */ #include
<getopt.h>
#include <osmocom/core/application.h> #include
<osmocom/core/talloc.h> #include <osmocom/core/select.h>
#include <osmocom/core/socket.h> #include
<osmocom/core/logging.h> #include <osmocom/core/utils.h>
#include <osmocom/core/msgb.h> #include
<osmocom/core/select.h> #include <osmocom/core/timer.h>
#include “shm.h” #include “ipc_shm.h” #include “ipc_chan.h” #include
“ipc_sock.h”
#define DEFAULT_SHM_NAME “/osmo-trx-ipc-driver-shm2” #define
IPC_SOCK_PATH_PREFIX “/tmp”
static void tall_ctx; struct ipc_sock_state
global_ipc_sock_state;
/* 8 channels are plenty / struct ipc_sock_state
global_ctrl_socks[8]; struct ipc_shm_io ios_tx_to_device[8];
struct ipc_shm_io ios_rx_from_device[8];
void shm; void global_dev;
static struct ipc_shm_region *decoded_region;
static struct { int msocknum; char *ud_prefix_dir; } cmdline_cfg;
static const struct log_info_cat default_categories[] = { [DMAIN] = {
.name = “DMAIN”, .color = NULL, .description = “Main generic category”,
.loglevel = LOGL_DEBUG,.enabled = 1, }, [DDEV] = { .name = “DDEV”,
.description = “Device/Driver specific code”, .color = NULL, .enabled =
1, .loglevel = LOGL_DEBUG, }, };
const struct log_info log_infox = { .cat = default_categories,
.num_cat = ARRAY_SIZE(default_categories), };
#include “uhdwrap.h”
volatile int ipc_exit_requested = 0;
static int ipc_shm_setup(const char *shm_name, size_t shm_len) { int
fd; int rc;
LOGP(DMAIN, LOGL_NOTICE, "Opening shm path %s\n", shm_name);
if ((fd = shm_open(shm_name, O_CREAT | O_RDWR | O_TRUNC, S_IRUSR | S_IWUSR)) < 0) {
LOGP(DMAIN, LOGL_ERROR, "shm_open %d: %s\n", errno, strerror(errno));
rc = -errno;
goto err_shm_open;
}
LOGP(DMAIN, LOGL_NOTICE, "Truncating %d to size %zu\n", fd, shm_len);
if (ftruncate(fd, shm_len) < 0) {
LOGP(DMAIN, LOGL_ERROR, "ftruncate %d: %s\n", errno, strerror(errno));
rc = -errno;
goto err_mmap;
}
LOGP(DMAIN, LOGL_NOTICE, "mmaping shared memory fd %d\n", fd);
if ((shm = mmap(NULL, shm_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) {
LOGP(DMAIN, LOGL_ERROR, "mmap %d: %s\n", errno, strerror(errno));
rc = -errno;
goto err_mmap;
}
LOGP(DMAIN, LOGL_NOTICE, "mmap'ed shared memory at addr %p\n", shm);
/* After a call to mmap(2) the file descriptor may be closed without affecting the memory mapping. */
close(fd);
return 0;
err_mmap: shm_unlink(shm_name); close(fd); err_shm_open: return rc;
}
struct msgb ipc_msgb_alloc(uint8_t msg_type) { struct msgb
msg; struct ipc_sk_if *ipc_prim;
msg = msgb_alloc(sizeof(struct ipc_sk_if) + 1000, "ipc_sock_tx");
if (!msg)
return NULL;
msgb_put(msg, sizeof(struct ipc_sk_if) + 1000);
ipc_prim = (struct ipc_sk_if *)msg->data;
ipc_prim->msg_type = msg_type;
return msg;
}
static int ipc_tx_greeting_cnf(uint8_t req_version) { struct msgb
msg; struct ipc_sk_if ipc_prim;
msg = ipc_msgb_alloc(IPC_IF_MSG_GREETING_CNF);
if (!msg)
return -ENOMEM;
ipc_prim = (struct ipc_sk_if *)msg->data;
ipc_prim->u.greeting_cnf.req_version = req_version;
return ipc_sock_send(msg);
}
static int ipc_tx_info_cnf() { struct msgb msg; struct ipc_sk_if
ipc_prim;
msg = ipc_msgb_alloc(IPC_IF_MSG_INFO_CNF);
if (!msg)
return -ENOMEM;
ipc_prim = (struct ipc_sk_if *)msg->data;
uhdwrap_fill_info_cnf(ipc_prim);
return ipc_sock_send(msg);
}
static int ipc_tx_open_cnf(int rc, uint32_t num_chans, int32_t
timingoffset) { struct msgb msg; struct ipc_sk_if ipc_prim;
struct ipc_sk_if_open_cnf_chan *chan_info; unsigned int i;
msg = ipc_msgb_alloc(IPC_IF_MSG_OPEN_CNF);
if (!msg)
return -ENOMEM;
ipc_prim = (struct ipc_sk_if *)msg->data;
ipc_prim->u.open_cnf.return_code = rc;
ipc_prim->u.open_cnf.path_delay = timingoffset; // 6.18462e-5 * 1625e3 / 6;
OSMO_STRLCPY_ARRAY(ipc_prim->u.open_cnf.shm_name, DEFAULT_SHM_NAME);
chan_info = ipc_prim->u.open_cnf.chan_info;
for (i = 0; i < num_chans; i++) {
snprintf(chan_info->chan_ipc_sk_path, sizeof(chan_info->chan_ipc_sk_path),"%s/ipc_sock%d_%d",
cmdline_cfg.ud_prefix_dir, cmdline_cfg.msocknum, i);
/* FIXME: dynamc chan limit, currently 8 */
if (i < 8)
ipc_sock_init(chan_info->chan_ipc_sk_path, &global_ctrl_socks[i], ipc_chan_sock_accept, i);
chan_info++;
}
return ipc_sock_send(msg);
}
int ipc_rx_greeting_req(struct ipc_sk_if_greeting *greeting_req) { if
(greeting_req->req_version == IPC_SOCK_API_VERSION)
ipc_tx_greeting_cnf(IPC_SOCK_API_VERSION); else ipc_tx_greeting_cnf(0);
return 0; }
int ipc_rx_info_req(struct ipc_sk_if_info_req *info_req) {
ipc_tx_info_cnf(); return 0; }
int ipc_rx_open_req(struct ipc_sk_if_open_req open_req) { /
calculate size needed */ unsigned int len; unsigned int i;
global_dev = uhdwrap_open(open_req);
/* b210 packet size is 2040, but our tx size is 2500, so just do *2 */
int shmbuflen = uhdwrap_get_bufsizerx(global_dev) * 2;
len = ipc_shm_encode_region(NULL, open_req->num_chans, 4, shmbuflen);
/* Here we verify num_chans, rx_path, tx_path, clockref, etc. */
int rc = ipc_shm_setup(DEFAULT_SHM_NAME, len);
len = ipc_shm_encode_region((struct ipc_shm_raw_region *)shm, open_req->num_chans, 4, shmbuflen);
// LOGP(DMAIN, LOGL_NOTICE, "%s\n", osmo_hexdump((const unsigned char *)shm, 80));
/* set up our own copy of the decoded area, we have to do it here,
* since the uhd wrapper does not allow starting single channels
* additionally go for the producer init for both, so only we are responsible for the init, instead
* of splitting it with the client and causing potential races if one side uses it too early */
decoded_region = ipc_shm_decode_region(0, (struct ipc_shm_raw_region *)shm);
for (i = 0; i < open_req->num_chans; i++) {
// ios_tx_to_device[i] = ipc_shm_init_consumer(decoded_region->channels[i]->dl_stream);
ios_tx_to_device[i] = ipc_shm_init_producer(decoded_region->channels[i]->dl_stream);
ios_rx_from_device[i] = ipc_shm_init_producer(decoded_region->channels[i]->ul_stream);
}
ipc_tx_open_cnf(-rc, open_req->num_chans, uhdwrap_get_timingoffset(global_dev));
return 0;
}
volatile bool ul_running = false; volatile bool dl_running =
false;
void uplink_thread(void x_void_ptr) { uint32_t chann =
decoded_region->num_chans; ul_running = true;
pthread_setname_np(pthread_self(), “uplink_rx”);
while (!ipc_exit_requested) {
int32_t read = uhdwrap_read(global_dev, chann);
if (read < 0)
return 0;
}
return 0;
}
void downlink_thread(void x_void_ptr) { int chann =
decoded_region->num_chans; dl_running = true;
pthread_setname_np(pthread_self(), “downlink_tx”);
while (!ipc_exit_requested) {
bool underrun;
uhdwrap_write(global_dev, chann, &underrun);
}
return 0;
}
int ipc_rx_chan_start_req(struct ipc_sk_chan_if_op_void req,
uint8_t chan_nr) { struct msgb msg; struct ipc_sk_chan_if
*ipc_prim; int rc = 0;
rc = uhdwrap_start(global_dev, chan_nr);
/* no per-chan start/stop */
if (!dl_running || !ul_running) {
/* chan != first chan start will "fail", which is fine, usrp can't start/stop chans independently */
if (rc) {
LOGP(DMAIN, LOGL_INFO, "starting rx/tx threads.. req for chan:%d\n", chan_nr);
pthread_t rx, tx;
pthread_create(&rx, NULL, uplink_thread, 0);
pthread_create(&tx, NULL, downlink_thread, 0);
}
} else
LOGP(DMAIN, LOGL_INFO, "starting rx/tx threads request ignored.. req for chan:%d\n", chan_nr);
msg = ipc_msgb_alloc(IPC_IF_MSG_START_CNF);
if (!msg)
return -ENOMEM;
ipc_prim = (struct ipc_sk_chan_if *)msg->data;
ipc_prim->u.start_cnf.return_code = rc ? 0 : -1;
return ipc_chan_sock_send(msg, chan_nr);
} int ipc_rx_chan_stop_req(struct ipc_sk_chan_if_op_void req,
uint8_t chan_nr) { struct msgb msg; struct ipc_sk_chan_if
*ipc_prim; int rc;
/* no per-chan start/stop */
rc = uhdwrap_stop(global_dev, chan_nr);
msg = ipc_msgb_alloc(IPC_IF_MSG_STOP_CNF);
if (!msg)
return -ENOMEM;
ipc_prim = (struct ipc_sk_chan_if *)msg->data;
ipc_prim->u.stop_cnf.return_code = rc ? 0 : -1;
return ipc_chan_sock_send(msg, chan_nr);
} int ipc_rx_chan_setgain_req(struct ipc_sk_chan_if_gain req,
uint8_t chan_nr) { struct msgb msg; struct ipc_sk_chan_if
*ipc_prim; double rv;
rv = uhdwrap_set_gain(global_dev, req->gain, chan_nr, req->is_tx);
msg = ipc_msgb_alloc(IPC_IF_MSG_SETGAIN_CNF);
if (!msg)
return -ENOMEM;
ipc_prim = (struct ipc_sk_chan_if *)msg->data;
ipc_prim->u.set_gain_cnf.is_tx = req->is_tx;
ipc_prim->u.set_gain_cnf.gain = rv;
return ipc_chan_sock_send(msg, chan_nr);
}
int ipc_rx_chan_setfreq_req(struct ipc_sk_chan_if_freq_req req,
uint8_t chan_nr) { struct msgb msg; struct ipc_sk_chan_if
*ipc_prim; bool rv;
rv = uhdwrap_set_freq(global_dev, req->freq, chan_nr, req->is_tx);
msg = ipc_msgb_alloc(IPC_IF_MSG_SETFREQ_CNF);
if (!msg)
return -ENOMEM;
ipc_prim = (struct ipc_sk_chan_if *)msg->data;
ipc_prim->u.set_freq_cnf.return_code = rv ? 0 : 1;
return ipc_chan_sock_send(msg, chan_nr);
}
int ipc_rx_chan_settxatten_req(struct ipc_sk_chan_if_tx_attenuation
req, uint8_t chan_nr) { struct msgb msg; struct ipc_sk_chan_if
*ipc_prim; double rv;
rv = uhdwrap_set_txatt(global_dev, req->attenuation, chan_nr);
msg = ipc_msgb_alloc(IPC_IF_MSG_SETTXATTN_CNF);
if (!msg)
return -ENOMEM;
ipc_prim = (struct ipc_sk_chan_if *)msg->data;
ipc_prim->u.txatten_cnf.attenuation = rv;
return ipc_chan_sock_send(msg, chan_nr);
}
int ipc_sock_init(const char *path, struct ipc_sock_state
**global_state_var, int (sock_callback_fn)(struct osmo_fd fd,
unsigned int what), int n) { struct ipc_sock_state state; struct
osmo_fd bfd; int rc;
state = talloc_zero(NULL, struct ipc_sock_state);
if (!state)
return -ENOMEM;
*global_state_var = state;
INIT_LLIST_HEAD(&state->upqueue);
state->conn_bfd.fd = -1;
bfd = &state->listen_bfd;
bfd->fd = osmo_sock_unix_init(SOCK_SEQPACKET, 0, path, OSMO_SOCK_F_BIND);
if (bfd->fd < 0) {
LOGP(DMAIN, LOGL_ERROR, "Could not create %s unix socket: %s\n", path, strerror(errno));
talloc_free(state);
return -1;
}
osmo_fd_setup(bfd, bfd->fd, OSMO_FD_READ, sock_callback_fn, state, n);
rc = osmo_fd_register(bfd);
if (rc < 0) {
LOGP(DMAIN, LOGL_ERROR, "Could not register listen fd: %d\n", rc);
close(bfd->fd);
talloc_free(state);
return rc;
}
LOGP(DMAIN, LOGL_INFO, "Started listening on IPC socket: %s\n", path);
return 0;
}
static void print_help(void) { printf(“ipc-driver-test Usage:” ” -h
–help This message” ” -u –unix-sk-dir DIR Existing directory where to
create the Master socket” ” -n –sock-num NR Master socket suffix number
NR“); }
static void handle_options(int argc, char **argv) { while (1) { int
option_index = 0, c; const struct option long_options[] = { { “help”, 0,
0, ‘h’ }, { “unix-sk-dir”, 1, 0, ‘u’ }, { “sock-num”, 1, 0, ‘n’ }, { 0,
0, 0, 0 } };
c = getopt_long(argc, argv, "hu:n:", long_options, &option_index);
if (c == -1)
break;
switch (c) {
case 'h':
print_help();
exit(0);
break;
case 'u':
cmdline_cfg.ud_prefix_dir = talloc_strdup(tall_ctx, optarg);
break;
case 'n':
cmdline_cfg.msocknum = atoi(optarg);
break;
default:
exit(2);
break;
}
}
if (argc > optind) {
fprintf(stderr, "Unsupported positional arguments on command line\n");
exit(2);
}
}
int main(int argc, char **argv) { char ipc_msock_path[128]; tall_ctx
= talloc_named_const(NULL, 0, “OsmoTRX”); msgb_talloc_ctx_init(tall_ctx,
0); osmo_init_logging2(tall_ctx, &log_infox);
log_enable_multithread();
handle_options(argc, argv);
if (!cmdline_cfg.ud_prefix_dir)
cmdline_cfg.ud_prefix_dir = talloc_strdup(tall_ctx, IPC_SOCK_PATH_PREFIX);
snprintf(ipc_msock_path, sizeof(ipc_msock_path), "%s/ipc_sock%d", cmdline_cfg.ud_prefix_dir, cmdline_cfg.msocknum);
LOGP(DMAIN, LOGL_INFO, "Starting %s\n", argv[0]);
ipc_sock_init(ipc_msock_path, &global_ipc_sock_state, ipc_sock_accept, 0);
while (!ipc_exit_requested)
osmo_select_main(0);
if (global_dev) {
unsigned int i;
for (i = 0; i < decoded_region->num_chans; i++)
uhdwrap_stop(global_dev, i);
}
ipc_sock_close(global_ipc_sock_state);
return 0;
}
================================================================================
FILE: tools/calypso-ipc-device/ipc_chan.c SIZE: 6309 bytes, 254 lines
================================================================================
/ Copyright 2020 sysmocom - s.f.m.c. GmbH info@sysmocom.de *
Author: Pau Espin Pedrol pespin@sysmocom.de SPDX-License-Identifier:
0BSD Permission to use, copy, modify, and/or distribute this
software for any purpose * with or without fee is hereby granted.THE
SOFTWARE IS PROVIDED “AS IS” AND THE * AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL * IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR * BE LIABLE
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF * CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE * USE OR PERFORMANCE OF THIS
SOFTWARE. / #include <stdio.h> #include <stdlib.h>
#include <unistd.h> #include <string.h> #include
<errno.h> #include <assert.h> #include <sys/socket.h>
#include <sys/un.h> #include <inttypes.h> #include
<sys/mman.h> #include <sys/stat.h> / For mode constants
/ #include <fcntl.h> / For O_* constants */
#include <debug.h> #include <osmocom/core/application.h>
#include <osmocom/core/talloc.h> #include
<osmocom/core/select.h> #include <osmocom/core/socket.h>
#include <osmocom/core/logging.h> #include
<osmocom/core/utils.h> #include <osmocom/core/msgb.h>
#include <osmocom/core/select.h> #include
<osmocom/core/timer.h>
#include “shm.h” #include “ipc-driver-test.h” #include “ipc_chan.h”
#include “ipc_sock.h”
static int ipc_chan_rx(uint8_t msg_type, struct ipc_sk_chan_if
*ipc_prim, uint8_t chan_nr) { int rc = 0;
switch (msg_type) {
case IPC_IF_MSG_START_REQ:
rc = ipc_rx_chan_start_req(&ipc_prim->u.start_req, chan_nr);
break;
case IPC_IF_MSG_STOP_REQ:
rc = ipc_rx_chan_stop_req(&ipc_prim->u.stop_req, chan_nr);
break;
case IPC_IF_MSG_SETGAIN_REQ:
rc = ipc_rx_chan_setgain_req(&ipc_prim->u.set_gain_req, chan_nr);
break;
case IPC_IF_MSG_SETFREQ_REQ:
rc = ipc_rx_chan_setfreq_req(&ipc_prim->u.set_freq_req, chan_nr);
break;
case IPC_IF_MSG_SETTXATTN_REQ:
rc = ipc_rx_chan_settxatten_req(&ipc_prim->u.txatten_req, chan_nr);
break;
default:
LOGP(DDEV, LOGL_ERROR, "Received unknown IPC msg type 0x%02x on chan %d\n", msg_type, chan_nr);
rc = -EINVAL;
}
return rc;
}
static int ipc_chan_sock_read(struct osmo_fd bfd) { struct
ipc_sock_state state = (struct ipc_sock_state )bfd->data;
struct ipc_sk_chan_if ipc_prim; struct msgb *msg; int rc;
msg = msgb_alloc(sizeof(*ipc_prim) + 1000, "ipc_chan_sock_rx");
if (!msg)
return -ENOMEM;
ipc_prim = (struct ipc_sk_chan_if *)msg->tail;
rc = recv(bfd->fd, msg->tail, msgb_tailroom(msg), 0);
if (rc == 0)
goto close;
if (rc < 0) {
if (errno == EAGAIN) {
msgb_free(msg);
return 0;
}
goto close;
}
if (rc < (int)sizeof(*ipc_prim)) {
LOGP(DDEV, LOGL_ERROR,
"Received %d bytes on Unix Socket, but primitive size "
"is %zu, discarding\n",
rc, sizeof(*ipc_prim));
msgb_free(msg);
return 0;
}
rc = ipc_chan_rx(ipc_prim->msg_type, ipc_prim, bfd->priv_nr);
/* as we always synchronously process the message in IPC_rx() and
* its callbacks, we can free the message here. */
msgb_free(msg);
return rc;
close: msgb_free(msg); ipc_sock_close(state); return -1; }
int ipc_chan_sock_send(struct msgb msg, uint8_t chan_nr) { struct
ipc_sock_state state = global_ctrl_socks[chan_nr]; struct osmo_fd
*conn_bfd;
if (!state)
return -EINVAL;
if (!state) {
LOGP(DDEV, LOGL_INFO,
"IPC socket not created, "
"dropping message\n");
msgb_free(msg);
return -EINVAL;
}
conn_bfd = &state->conn_bfd;
if (conn_bfd->fd <= 0) {
LOGP(DDEV, LOGL_NOTICE,
"IPC socket not connected, "
"dropping message\n");
msgb_free(msg);
return -EIO;
}
msgb_enqueue(&state->upqueue, msg);
osmo_fd_write_enable(conn_bfd);
return 0;
}
static int ipc_chan_sock_write(struct osmo_fd bfd) { struct
ipc_sock_state state = (struct ipc_sock_state *)bfd->data; int
rc;
while (!llist_empty(&state->upqueue)) {
struct msgb *msg, *msg2;
struct ipc_sk_chan_if *ipc_prim;
/* peek at the beginning of the queue */
msg = llist_entry(state->upqueue.next, struct msgb, list);
ipc_prim = (struct ipc_sk_chan_if *)msg->data;
osmo_fd_write_disable(bfd);
/* bug hunter 8-): maybe someone forgot msgb_put(...) ? */
if (!msgb_length(msg)) {
LOGP(DDEV, LOGL_ERROR,
"message type (%d) with ZERO "
"bytes!\n",
ipc_prim->msg_type);
goto dontsend;
}
/* try to send it over the socket */
rc = write(bfd->fd, msgb_data(msg), msgb_length(msg));
if (rc == 0)
goto close;
if (rc < 0) {
if (errno == EAGAIN) {
osmo_fd_write_enable(bfd);
break;
}
goto close;
}
dontsend:
/* _after_ we send it, we can dequeue */
msg2 = msgb_dequeue(&state->upqueue);
assert(msg == msg2);
msgb_free(msg);
}
return 0;
close: ipc_sock_close(state); return -1; }
static int ipc_chan_sock_cb(struct osmo_fd *bfd, unsigned int flags)
{ int rc = 0;
if (flags & OSMO_FD_READ)
rc = ipc_chan_sock_read(bfd);
if (rc < 0)
return rc;
if (flags & OSMO_FD_WRITE)
rc = ipc_chan_sock_write(bfd);
return rc;
}
int ipc_chan_sock_accept(struct osmo_fd bfd, unsigned int flags)
{ struct ipc_sock_state state = (struct ipc_sock_state
)bfd->data; struct osmo_fd conn_bfd =
&state->conn_bfd; struct sockaddr_un un_addr; socklen_t len; int
rc;
len = sizeof(un_addr);
rc = accept(bfd->fd, (struct sockaddr *)&un_addr, &len);
if (rc < 0) {
LOGP(DDEV, LOGL_ERROR, "Failed to accept a new connection\n");
return -1;
}
if (conn_bfd->fd >= 0) {
LOGP(DDEV, LOGL_NOTICE,
"osmo-trx connects but we already have "
"another active connection ?!?\n");
/* We already have one IPC connected, this is all we support */
osmo_fd_read_disable(&state->listen_bfd);
close(rc);
return 0;
}
/* copy chan nr, required for proper bfd<->chan # mapping */
osmo_fd_setup(conn_bfd, rc, OSMO_FD_READ, ipc_chan_sock_cb, state, bfd->priv_nr);
if (osmo_fd_register(conn_bfd) != 0) {
LOGP(DDEV, LOGL_ERROR,
"Failed to register new connection "
"fd\n");
close(conn_bfd->fd);
conn_bfd->fd = -1;
return -1;
}
LOGP(DDEV, LOGL_NOTICE, "Unix socket connected to external osmo-trx\n");
return 0;
}
================================================================================
FILE: tools/calypso-ipc-device/ipc_shm.c SIZE: 5734 bytes, 200 lines
================================================================================
/ Copyright 2020 sysmocom - s.f.m.c. GmbH info@sysmocom.de *
Author: Eric Wild ewild@sysmocom.de SPDX-License-Identifier:
0BSD Permission to use, copy, modify, and/or distribute this
software for any purpose * with or without fee is hereby granted.THE
SOFTWARE IS PROVIDED “AS IS” AND THE * AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL * IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR * BE LIABLE
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF * CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE * USE OR PERFORMANCE OF THIS
SOFTWARE. */ #ifdef __cplusplus extern “C” { #endif
#include <shm.h> #include “ipc_shm.h” #include
<pthread.h> #include <semaphore.h> #include <stdint.h>
#include <stdlib.h> #include <string.h> #include
<stdio.h> #include <errno.h> #include
<osmocom/core/panic.h>
#include <debug.h>
#ifdef __cplusplus } #endif
#define SAMPLE_SIZE_BYTE (sizeof(uint16_t) * 2)
struct ipc_shm_io ipc_shm_init_consumer(struct ipc_shm_stream
s) { unsigned int i;
struct ipc_shm_io *r = (struct ipc_shm_io *)malloc(sizeof(struct ipc_shm_io));
r->this_stream = s->raw;
r->buf_ptrs =
(volatile struct ipc_shm_raw_smpl_buf **)malloc(sizeof(struct ipc_shm_raw_smpl_buf *) * s->num_buffers);
/* save actual ptrs */
for (i = 0; i < s->num_buffers; i++)
r->buf_ptrs[i] = s->buffers[i];
r->partial_read_begin_ptr = 0;
return r;
}
struct ipc_shm_io ipc_shm_init_producer(struct ipc_shm_stream
s) { int rv; pthread_mutexattr_t att; pthread_condattr_t t1, t2;
struct ipc_shm_io *r = ipc_shm_init_consumer(s); rv =
pthread_mutexattr_init(&att); if (rv != 0) { osmo_panic(“%s:%d
rv:%d”, FILE, LINE, rv); }
rv = pthread_mutexattr_setrobust(&att, PTHREAD_MUTEX_ROBUST);
if (rv != 0) {
osmo_panic("%s:%d rv:%d", __FILE__, __LINE__, rv);
}
rv = pthread_mutexattr_setpshared(&att, PTHREAD_PROCESS_SHARED);
if (rv != 0) {
osmo_panic("%s:%d rv:%d", __FILE__, __LINE__, rv);
}
rv = pthread_mutex_init((pthread_mutex_t *)&r->this_stream->lock, &att);
if (rv != 0) {
osmo_panic("%s:%d rv:%d", __FILE__, __LINE__, rv);
}
pthread_mutexattr_destroy(&att);
rv = pthread_condattr_setpshared(&t1, PTHREAD_PROCESS_SHARED);
if (rv != 0) {
osmo_panic("%s:%d rv:%d", __FILE__, __LINE__, rv);
}
rv = pthread_condattr_setpshared(&t2, PTHREAD_PROCESS_SHARED);
if (rv != 0) {
osmo_panic("%s:%d rv:%d", __FILE__, __LINE__, rv);
}
rv = pthread_cond_init((pthread_cond_t *)&r->this_stream->cf, &t1);
if (rv != 0) {
osmo_panic("%s:%d rv:%d", __FILE__, __LINE__, rv);
}
rv = pthread_cond_init((pthread_cond_t *)&r->this_stream->ce, &t2);
if (rv != 0) {
osmo_panic("%s:%d rv:%d", __FILE__, __LINE__, rv);
}
pthread_condattr_destroy(&t1);
pthread_condattr_destroy(&t2);
r->this_stream->read_next = 0;
r->this_stream->write_next = 0;
return r;
}
void ipc_shm_close(struct ipc_shm_io *r) { if (r) {
free(r->buf_ptrs); free(r); } }
int32_t ipc_shm_enqueue(struct ipc_shm_io r, uint64_t timestamp,
uint32_t len_in_sps, uint16_t data) { volatile struct
ipc_shm_raw_smpl_buf *buf; int32_t rv; struct timespec tv;
clock_gettime(CLOCK_REALTIME, &tv); tv.tv_sec += 1;
rv = pthread_mutex_timedlock((pthread_mutex_t *)&r->this_stream->lock, &tv);
if (rv != 0)
return -rv;
while (((r->this_stream->write_next + 1) & (r->this_stream->num_buffers - 1)) == r->this_stream->read_next &&
rv == 0)
rv = pthread_cond_timedwait((pthread_cond_t *)&r->this_stream->ce,
(pthread_mutex_t *)&r->this_stream->lock, &tv);
if (rv != 0)
return -rv;
buf = r->buf_ptrs[r->this_stream->write_next];
buf->timestamp = timestamp;
rv = len_in_sps <= r->this_stream->buffer_size ? len_in_sps : r->this_stream->buffer_size;
memcpy((void *)buf->samples, data, SAMPLE_SIZE_BYTE * rv);
buf->data_len = rv;
r->this_stream->write_next = (r->this_stream->write_next + 1) & (r->this_stream->num_buffers - 1);
pthread_cond_signal((pthread_cond_t *)&r->this_stream->cf);
pthread_mutex_unlock((pthread_mutex_t *)&r->this_stream->lock);
return rv;
}
int32_t ipc_shm_read(struct ipc_shm_io r, uint16_t out_buf,
uint32_t num_samples, uint64_t timestamp, uint32_t timeout_seconds)
{ volatile struct ipc_shm_raw_smpl_buf buf; int32_t rv; uint8_t
freeflag = 0; struct timespec tv; clock_gettime(CLOCK_REALTIME,
&tv); tv.tv_sec += timeout_seconds;
rv = pthread_mutex_timedlock((pthread_mutex_t *)&r->this_stream->lock, &tv);
if (rv != 0)
return -rv;
while (r->this_stream->write_next == r->this_stream->read_next && rv == 0)
rv = pthread_cond_timedwait((pthread_cond_t *)&r->this_stream->cf,
(pthread_mutex_t *)&r->this_stream->lock, &tv);
if (rv != 0)
return -rv;
buf = r->buf_ptrs[r->this_stream->read_next];
if (buf->data_len <= num_samples) {
memcpy(out_buf, (void *)&buf->samples[r->partial_read_begin_ptr * 2], SAMPLE_SIZE_BYTE * buf->data_len);
r->partial_read_begin_ptr = 0;
rv = buf->data_len;
buf->data_len = 0;
r->this_stream->read_next = (r->this_stream->read_next + 1) & (r->this_stream->num_buffers - 1);
freeflag = 1;
} else /*if (buf->data_len > num_samples)*/ {
memcpy(out_buf, (void *)&buf->samples[r->partial_read_begin_ptr * 2], SAMPLE_SIZE_BYTE * num_samples);
r->partial_read_begin_ptr += num_samples;
buf->data_len -= num_samples;
rv = num_samples;
}
*timestamp = buf->timestamp;
buf->timestamp += rv;
if (freeflag)
pthread_cond_signal((pthread_cond_t *)&r->this_stream->ce);
pthread_mutex_unlock((pthread_mutex_t *)&r->this_stream->lock);
return rv;
}
================================================================================
FILE: tools/calypso-ipc-device/ipc_sock.c SIZE: 6305 bytes, 266 lines
================================================================================
/ Copyright 2020 sysmocom - s.f.m.c. GmbH info@sysmocom.de *
Author: Pau Espin Pedrol pespin@sysmocom.de SPDX-License-Identifier:
0BSD Permission to use, copy, modify, and/or distribute this
software for any purpose * with or without fee is hereby granted.THE
SOFTWARE IS PROVIDED “AS IS” AND THE * AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL * IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR * BE LIABLE
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF * CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE * USE OR PERFORMANCE OF THIS
SOFTWARE. / #include <stdio.h> #include <stdlib.h>
#include <unistd.h> #include <string.h> #include
<errno.h> #include <assert.h> #include <sys/socket.h>
#include <sys/un.h> #include <inttypes.h> #include
<sys/mman.h> #include <sys/stat.h> / For mode constants
/ #include <fcntl.h> / For O_* constants */
#include <debug.h> #include <osmocom/core/application.h>
#include <osmocom/core/talloc.h> #include
<osmocom/core/select.h> #include <osmocom/core/socket.h>
#include <osmocom/core/logging.h> #include
<osmocom/core/utils.h> #include <osmocom/core/msgb.h>
#include <osmocom/core/select.h> #include
<osmocom/core/timer.h>
#include “shm.h” #include “ipc-driver-test.h”
extern volatile int ipc_exit_requested; static int ipc_rx(uint8_t
msg_type, struct ipc_sk_if *ipc_prim) { int rc = 0;
switch (msg_type) {
case IPC_IF_MSG_GREETING_REQ:
rc = ipc_rx_greeting_req(&ipc_prim->u.greeting_req);
break;
case IPC_IF_MSG_INFO_REQ:
rc = ipc_rx_info_req(&ipc_prim->u.info_req);
break;
case IPC_IF_MSG_OPEN_REQ:
rc = ipc_rx_open_req(&ipc_prim->u.open_req);
break;
default:
LOGP(DDEV, LOGL_ERROR, "Received unknown IPC msg type 0x%02x\n", msg_type);
rc = -EINVAL;
}
return rc;
}
int ipc_sock_send(struct msgb msg) { struct ipc_sock_state
state = global_ipc_sock_state; struct osmo_fd *conn_bfd;
if (!state) {
LOGP(DDEV, LOGL_INFO,
"IPC socket not created, "
"dropping message\n");
msgb_free(msg);
return -EINVAL;
}
conn_bfd = &state->conn_bfd;
if (conn_bfd->fd <= 0) {
LOGP(DDEV, LOGL_NOTICE,
"IPC socket not connected, "
"dropping message\n");
msgb_free(msg);
return -EIO;
}
msgb_enqueue(&state->upqueue, msg);
osmo_fd_write_enable(conn_bfd);
return 0;
}
void ipc_sock_close(struct ipc_sock_state state) { struct osmo_fd
bfd = &state->conn_bfd;
LOGP(DDEV, LOGL_NOTICE, "IPC socket has LOST connection\n");
ipc_exit_requested = 1;
osmo_fd_unregister(bfd);
close(bfd->fd);
bfd->fd = -1;
/* re-enable the generation of ACCEPT for new connections */
osmo_fd_read_enable(&state->listen_bfd);
/* flush the queue */
while (!llist_empty(&state->upqueue)) {
struct msgb *msg = msgb_dequeue(&state->upqueue);
msgb_free(msg);
}
}
int ipc_sock_read(struct osmo_fd bfd) { struct ipc_sock_state
state = (struct ipc_sock_state )bfd->data; struct ipc_sk_if
ipc_prim; struct msgb *msg; int rc;
msg = msgb_alloc(sizeof(*ipc_prim) + 1000, "ipc_sock_rx");
if (!msg)
return -ENOMEM;
ipc_prim = (struct ipc_sk_if *)msg->tail;
rc = recv(bfd->fd, msg->tail, msgb_tailroom(msg), 0);
if (rc == 0)
goto close;
if (rc < 0) {
if (errno == EAGAIN) {
msgb_free(msg);
return 0;
}
goto close;
}
if (rc < (int)sizeof(*ipc_prim)) {
LOGP(DDEV, LOGL_ERROR,
"Received %d bytes on Unix Socket, but primitive size "
"is %zu, discarding\n",
rc, sizeof(*ipc_prim));
msgb_free(msg);
return 0;
}
rc = ipc_rx(ipc_prim->msg_type, ipc_prim);
/* as we always synchronously process the message in IPC_rx() and
* its callbacks, we can free the message here. */
msgb_free(msg);
return rc;
close: msgb_free(msg); ipc_sock_close(state); return -1; }
static int ipc_sock_write(struct osmo_fd bfd) { struct
ipc_sock_state state = (struct ipc_sock_state *)bfd->data; int
rc;
while (!llist_empty(&state->upqueue)) {
struct msgb *msg, *msg2;
struct ipc_sk_if *ipc_prim;
/* peek at the beginning of the queue */
msg = llist_entry(state->upqueue.next, struct msgb, list);
ipc_prim = (struct ipc_sk_if *)msg->data;
osmo_fd_write_disable(bfd);
/* bug hunter 8-): maybe someone forgot msgb_put(...) ? */
if (!msgb_length(msg)) {
LOGP(DDEV, LOGL_ERROR,
"message type (%d) with ZERO "
"bytes!\n",
ipc_prim->msg_type);
goto dontsend;
}
/* try to send it over the socket */
rc = write(bfd->fd, msgb_data(msg), msgb_length(msg));
if (rc == 0)
goto close;
if (rc < 0) {
if (errno == EAGAIN) {
osmo_fd_write_enable(bfd);
break;
}
goto close;
}
dontsend:
/* _after_ we send it, we can deueue */
msg2 = msgb_dequeue(&state->upqueue);
assert(msg == msg2);
msgb_free(msg);
}
return 0;
close: ipc_sock_close(state); return -1; }
static int ipc_sock_cb(struct osmo_fd *bfd, unsigned int flags) { int
rc = 0;
if (flags & OSMO_FD_READ)
rc = ipc_sock_read(bfd);
if (rc < 0)
return rc;
if (flags & OSMO_FD_WRITE)
rc = ipc_sock_write(bfd);
return rc;
}
/* accept connection coming from IPC / int ipc_sock_accept(struct
osmo_fd bfd, unsigned int flags) { struct ipc_sock_state state
= (struct ipc_sock_state )bfd->data; struct osmo_fd *conn_bfd =
&state->conn_bfd; struct sockaddr_un un_addr; socklen_t len; int
rc;
len = sizeof(un_addr);
rc = accept(bfd->fd, (struct sockaddr *)&un_addr, &len);
if (rc < 0) {
LOGP(DDEV, LOGL_ERROR, "Failed to accept a new connection\n");
return -1;
}
if (conn_bfd->fd >= 0) {
LOGP(DDEV, LOGL_NOTICE,
"ip clent connects but we already have "
"another active connection ?!?\n");
/* We already have one IPC connected, this is all we support */
osmo_fd_read_disable(&state->listen_bfd);
close(rc);
return 0;
}
osmo_fd_setup(conn_bfd, rc, OSMO_FD_READ, ipc_sock_cb, state, 0);
if (osmo_fd_register(conn_bfd) != 0) {
LOGP(DDEV, LOGL_ERROR,
"Failed to register new connection "
"fd\n");
close(conn_bfd->fd);
conn_bfd->fd = -1;
return -1;
}
LOGP(DDEV, LOGL_NOTICE, "Unix socket connected to external osmo-trx\n");
return 0;
}
================================================================================
FILE: tools/calypso-ipc-device/qemu_wrap.c SIZE: 95007 bytes, 1823 lines
================================================================================
/ qemu_wrap.c — backend QEMU pour calypso-ipc-device.
Remplace osmo-trx/…/ipc/uhdwrap.cpp : à la place d’un device UHD
physique, * notre source de samples est le BSP QEMU émulé (UDP 6702).
Phase 1 — Proof of Life (ce fichier dans son état actuel) : *
- Accepte le handshake greeting/info/open/start d’osmo-trx-ipc. * -
uhdwrap_read produit un heartbeat continu de zéros cs16 → ul_stream. *
Cadence l’horloge osmo-trx (qui lit les timestamps UL comme master
clock). * - uhdwrap_write consomme silencieusement les bursts DL shm *
(à câbler vers UDP 6702 en Phase 1.5 / Task #6). * - Les autres hooks
(gain, freq, txatt, start, stop) sont no-op success. Specs
Calypso : * 1 channel, fs = 270 833 Hz (= 13e6/48), 1 SPS, cs16 I/Q
entrelacé. * 148 samples par burst (matches BSP encoder window côté
QEMU). SPDX-License-Identifier: 0BSD */
#define _GNU_SOURCE #include <arpa/inet.h> #include
<errno.h> #include <math.h> #include <signal.h>
#include <netinet/in.h> #include <pthread.h> #include
<stdbool.h> #include <stdint.h> #include <stdio.h>
#include <stdlib.h> #include <string.h> #include
<sys/socket.h> #include <sys/stat.h> #include
<fcntl.h> #include <unistd.h>
#include <osmocom/core/logging.h> #include
<osmocom/core/bits.h> #include
<osmocom/coding/gsm0503_coding.h> #include
<osmocom/gsm/a5.h> /* osmo_a5() : chiffrement A5/1 UL */
#include “debug.h” #include “ipc_shm.h” #include “shm.h” #include
“uhdwrap.h”
/* Specs Calypso baseband GSM. / #define CALYPSO_FS_NUM 13000000u
/ 13 MHz GSM master clock / #define CALYPSO_FS_DEN 48u /
/48 → 270 833.33 Hz */
/* osmo-trx-ipc has a hard-coded CHUNK=625 (radioInterface.cpp:36).
It always * commits buffers of CHUNKtx_sps samples to the device shm
— at 1 SPS = 625 samples per write = 4 GSM timeslots = half TDMA
frame. So our shm buffer * must be sized for that. We accept the 625
samples and extract only the * first 148 (TS=0) before forwarding to
QEMU BSP (which expects 148-sample * bursts in its TRXD UDP datagram).
The remaining 477 samples (TS 1..3 of * the half-frame) are dropped —
FBSB only listens on C0 TN=0. / #define CALYPSO_SHM_BUFSIZE 2500
/ samples per shm commit (matches osmo-trx CHUNK at 1 SPS) /
#define CALYPSO_TRX_OSR 4 / 4 SPS natif / #define
CALYPSO_DL_BURSTLEN (CALYPSO_BSP_BURSTLEN CALYPSO_TRX_OSR) /* 592
I/Q @ 4 SPS / #define CALYPSO_FRAME_SAMPLES (1250
CALYPSO_TRX_OSR) /* 5000 samples/frame @ 4 SPS / #define
CALYPSO_BSP_BURSTLEN 148 / samples per UDP datagram to QEMU BSP (=
correlator window) / / FIX LU 2026-06-05 : guard de tete
(complex samples) AVANT les bits actifs de * la RACH UL. Le correlateur
RACH osmo-trx (sigProcLib.cpp:1683 TOA gate <3sps, :1788
target ~sym48) rejette un burst place a l’offset 0 du slot (pic en bord
* -> rejete -> NOPE/-110). Un vrai access-burst a ~68 sym de guard
avant la sync. * ~32 sym @ OSR4 = 128 samples placent la sync dans la
fenetre du correlateur. */ #define CALYPSO_UL_SLOT_OFFSET 128
/* —- Timing frame CANONIQUE (logique GSM, robuste) —- * 1 frame TDMA
= CALYPSO_FRAME_QBITS qbits (1250 symboles x 4) = CALYPSO_FRAME_NS. * Le
budget DSP n’est PAS hardcode : le gating se fait sur le qfn du firmware
* (g_qemu_qfn), qui avance quand le firmware a fini sa frame = budget
DSP consomme * implicitement. On suit la frame REELLE du firmware, pas
une constante devinee. / #define CALYPSO_FRAME_QBITS 5000 #define
CALYPSO_FRAME_NS 4615384L / 5000 qbits / 1083333.33 qbits/s = 60/13
ms / #define CALYPSO_NUM_CHANS 1 #define CALYPSO_PATH_NAME “TX”
/ placeholder ; matches osmo-trx-ipc.cfg */ #define
CALYPSO_RX_PATH_NAME “RX”
/* QEMU BSP UDP endpoint. Matches the legacy calypso-ipc-device
target — QEMU’s * calypso_bsp.c binds on this. Override via env if
needed. */ #define QEMU_BSP_HOST_DEFAULT “127.0.0.1” #define
QEMU_BSP_PORT_DEFAULT 6702
/* GSM TDMA timing at 1 SPS. 1 TS ≈ 156.25 samples, 8 TS per frame. *
SAMPLES_PER_FRAME = 1250 = 8 × 156.25 (= 156.25 × 8). * Hyperframe =
2715648 frames (GSM 05.02 §3.1). */ #define SAMPLES_PER_FRAME 1250u
#define GSM_HYPERFRAME 2715648u
/* TRXDv0 datagram header = 8 bytes : * [0] version(4) | TN(4) —
calypso-ipc-device reads tn = data[0] & 7 * [1-4] FN,
big-endian (4 bytes) * [5] RSSI (uint8 dBm-ish) — not consumed by
Calypso BSP for DL * [6-7] ToA q4 (int16, optional) — not consumed by
Calypso BSP for DL * Payload = 4 × num_samples bytes (cs16 I,Q
interleaved). */ #define TRXD_HDR_LEN 8
/* Heartbeat pacing. 148 samples × (CALYPSO_FS_DEN / CALYPSO_FS_NUM)
sec * = 148 × 48 / 13e6 = 546.5 µs. usleep ≥ 1 ms granularity in
pratique, * so we pace at 500 µs and let osmo-trx absorb the ~9 %
overproduction * (it will read at its native rate and discard / buffer
accordingly). */ #define READ_PACE_US 500
/* Shared with calypso_ipc_device.c : these are populated in
ipc_rx_open_req * after ipc_shm_init_producer() / consumer(). /
extern struct ipc_shm_io ios_tx_to_device[8]; /* DL stream :
osmo-trx writes, we read / extern struct ipc_shm_io
ios_rx_from_device[8]; /* UL stream : we write, osmo-trx reads
*/
struct qemu_dev { uint32_t num_chans; uint64_t rx_ts; /* cumulative
sample timestamp for UL writes */ bool started[8]; };
/* UDP socket to QEMU BSP. Lazy-init on first qemu_wrap_write call so
we don’t * need to thread it through open(). */ static int g_bsp_fd =
-1; static struct sockaddr_in g_bsp_peer; static pthread_mutex_t
g_bsp_mutex = PTHREAD_MUTEX_INITIALIZER;
/* —- Fix D : DL FIFO qfn-paced —- Without this, the
device read shm at osmo-trx wall pace (~209 chunks/s) * and forwarded
each one to UDP 6702. QEMU (under icount=auto) consumed only * ~10 fn/s
→ 21 bursts tagged with the same qfn → 95 % dropped → FCCH * (5/51
frames) almost never reached the DSP correlator. Strategy :
ordered FIFO, 1 burst per qfn, no phase match. * - qemu_wrap_write :
append TS=0 burst to FIFO tail (on-air order). * - clk_listener : on
each qfn tick, pop FIFO head, tag fn=qfn, * sendto 6702. One burst per
qfn → cadence calé sur QEMU. Why no qfn↔︎on-air phase match :
during cold acquisition the MS does * not yet know on-air FN ; qfn is an
arbitrary internal counter. The * mapping qfn↔︎on-air is exactly what
FCCH+SCH establish. Phase-matching * before that requires data we don’t
have. The FIFO instead preserves * on-air order ; FB correlator scans
tone-only (FN-agnostic) and locks * in ~1-2s ; once SCH is decoded, the
MS adopts the on-air FN encoded * in it, and from then on its qfn
matches the tag we’re applying → * BCCH lecture devient cohérente
automatiquement. Scope : ce fix donne FBSB_CONF + BCCH. PAS
la LU — comme le device * lit à 20× le débit de consommation QEMU, la
FIFO accumule un lag de * plusieurs secondes ; pour UL RACH ce lag est
fatal (BTS rejette les * RACH au FN périmé). LU = autre combat, exige
horloges réelles. / #define DL_FIFO_SIZE 4096 / Coussin de
pré-fill (fix 2026-05-30) : on ne sert pas le 1er burst tant que * la
FIFO DL n’a pas atteint DL_PREFILL entrées. Établit un buffer qui
absorbe * les spikes de jitter entre l’horloge QEMU (clk_listener) et le
heartbeat * device (uhdwrap_read) — deux horloges libres. Sans ça, la
profondeur ~2 se * vide au moindre spike → “FIFO empty” → osmo-trx RX
error → IPC LOST → le BSP * n’est jamais nourri (D_BURST_D vide, snr=0).
32 frames ≈ 148 ms de marge. / #define DL_PREFILL 32 struct
dl_fifo_entry { bool is_fcch; / for diag log only / uint64_t
ts; / internal osmo-trx ts (for diag) / / Pre-built TRXDv0
packet, header rewritten at send time with qfn. / uint8_t
pkt[TRXD_HDR_LEN + CALYPSO_DL_BURSTLEN * 4]; }; static struct
dl_fifo_entry g_dl_fifo[DL_FIFO_SIZE]; static volatile size_t
g_dl_fifo_head = 0; / next pop index / static volatile size_t
g_dl_fifo_tail = 0; / next push index */ static pthread_mutex_t
g_dl_fifo_mutex = PTHREAD_MUTEX_INITIALIZER; static volatile uint32_t
g_last_qfn_sent = UINT32_MAX;
/* GMSK signature : a FCCH burst (148 zero bits) has dphi = +π/2
every * sample at 1 SPS. We measure the fraction of positive dphi
samples ; * ≥ 95 % positive = FCCH. Same logic as
tools/dump_chunks_pattern.py. / static bool is_fcch_burst_iq(const
int16_t iq, int n_samples) { if (n_samples < 16) return false;
int positives = 0; float prev_a = atan2f((float)iq[1], (float)iq[0]);
for (int i = 1; i < n_samples; i++) { float a = atan2f((float)iq[2 *
i + 1], (float)iq[2 * i]); float d = a - prev_a; while (d >
(float)M_PI) d -= 2.0f * (float)M_PI; while (d < -(float)M_PI) d +=
2.0f * (float)M_PI; if (d > 0.0f) positives++; prev_a = a; } return
positives >= (n_samples - 1) * 95 / 100; }
/* —- QEMU clock sync (Option A) —- * QEMU sends a 4-byte BE FN to
127.0.0.1:6700 on every TDMA tick * (calypso_trx.c:1434+). We bind that
port in a listener thread and use the * resulting FN to (1) pace the UL
heartbeat so osmo-trx clock advances at * QEMU’s effective rate (not
wall-clock), and (2) tag outbound DL datagrams * with the QEMU current
FN so the BSP queue accepts them (within its * 64-frame match window).
Without this, under icount=auto QEMU runs ~25× slower than
wall — our * heartbeat advanced rx_ts at 217 fn/s while QEMU was at ~8.4
fn/s. Result: * osmo-bts-trx bursts arrived with stale fn (delta
thousands), all dropped, * and the scheduler spammed STALE log lines
that caused the visible hang. */ #define QEMU_CLK_PORT 6700 static
volatile uint32_t g_qemu_qfn = 0; static volatile int g_qfn_seen = 0;
static int g_clk_fd = -1; static pthread_t g_clk_thread; extern volatile
int ipc_exit_requested;
static void clk_listener(void arg) { (void)arg;
pthread_setname_np(pthread_self(), “qemu_clk_rx”);
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) {
LOGP(DDEV, LOGL_ERROR, "clk_listener: socket() failed: %s\n", strerror(errno));
return NULL;
}
int reuse = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(QEMU_CLK_PORT);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
LOGP(DDEV, LOGL_ERROR, "clk_listener: bind 6700 failed: %s\n", strerror(errno));
close(fd);
return NULL;
}
g_clk_fd = fd;
LOGP(DDEV, LOGL_NOTICE, "clk_listener: bound 127.0.0.1:%d, waiting QEMU ticks\n",
QEMU_CLK_PORT);
uint8_t pkt[64];
while (!ipc_exit_requested) {
ssize_t n = recvfrom(fd, pkt, sizeof(pkt), 0, NULL, NULL);
if (n < 4) continue;
uint32_t fn = ((uint32_t)pkt[0] << 24) | ((uint32_t)pkt[1] << 16)
| ((uint32_t)pkt[2] << 8) | (uint32_t)pkt[3];
__atomic_store_n(&g_qemu_qfn, fn, __ATOMIC_RELEASE);
if (!g_qfn_seen) {
__atomic_store_n(&g_qfn_seen, 1, __ATOMIC_RELEASE);
LOGP(DDEV, LOGL_NOTICE,
"clk_listener: first QEMU tick received, qfn=%u\n", fn);
/* [2026-07-24] DL-FIFO-CATCHUP (gate CALYPSO_DL_FIFO_CATCHUP_OFF,
* default ON) : le FIFO DL se remplit en continu depuis
* osmo-trx-ipc (~209 burst/s) DES LE DEMARRAGE du device, bien
* AVANT que QEMU ait fini de booter (ROM DSP, etc.) et emis son
* premier tick CLK. Sans rattrapage, on sert ce backlog un burst
* par qfn-tick POUR TOUJOURS (Fix D pace bien le DEBIT, mais ne
* corrige jamais l'OFFSET initial) -> delta constant observe
* (ex: -1020 trames = ~4.7s de backlog, zero derive sur 160s+,
* confirme que c'est un decalage fige au demarrage, pas un
* probleme de cadence). On rattrape UNE SEULE FOIS ici, au tout
* premier tick : on ne garde que les DL_PREFILL entrees les plus
* fraiches, on jette le reste. Le coussin DL_PREFILL (jitter)
* est preserve, seul le backlog de demarrage disparait. */
{
static int catchup_off = -1;
if (catchup_off < 0)
catchup_off = getenv("CALYPSO_DL_FIFO_CATCHUP_OFF") &&
atoi(getenv("CALYPSO_DL_FIFO_CATCHUP_OFF"));
if (!catchup_off) {
pthread_mutex_lock(&g_dl_fifo_mutex);
size_t head0 = g_dl_fifo_head;
size_t tail0 = g_dl_fifo_tail;
size_t depth0 = tail0 - head0;
if (depth0 > DL_PREFILL) {
size_t dropped = depth0 - DL_PREFILL;
g_dl_fifo_head = tail0 - DL_PREFILL;
LOGP(DDEV, LOGL_NOTICE,
"DL-FIFO-CATCHUP: dropping %zu stale backlog "
"entries (depth %zu -> %u) at first qfn=%u\n",
dropped, depth0, DL_PREFILL, fn);
}
pthread_mutex_unlock(&g_dl_fifo_mutex);
}
}
}
/* ---- Fix D : pop FIFO head, tag with qfn, send ----
* 1 burst per qfn tick from QEMU → cadence matches QEMU's
* effective rate ; no overflow, no drop, no phase reasoning.
* On-air order is preserved by the FIFO ; the MS will adopt the
* encoded FN once it decodes SCH, locking the tag↔content. */
if (g_bsp_fd < 0)
continue;
uint32_t last = __atomic_load_n(&g_last_qfn_sent, __ATOMIC_ACQUIRE);
if (fn == last) continue; /* dedup duplicate qfn ticks */
__atomic_store_n(&g_last_qfn_sent, fn, __ATOMIC_RELEASE);
pthread_mutex_lock(&g_dl_fifo_mutex);
size_t head = g_dl_fifo_head;
size_t tail = g_dl_fifo_tail;
/* Pré-fill : attendre un coussin DL_PREFILL avant de servir le 1er
* burst (puis on sert normalement 1/tick). Le coussin absorbe ensuite
* les spikes de jitter sans jamais retomber à 0. */
static int s_prefilled = 0;
if (!s_prefilled) {
if (tail - head < DL_PREFILL) {
pthread_mutex_unlock(&g_dl_fifo_mutex);
continue; /* laisse la FIFO se remplir, ne consomme pas le tick */
}
s_prefilled = 1;
LOGP(DDEV, LOGL_NOTICE,
"DL FIFO pre-filled to %d, starting to serve at qfn=%u\n",
DL_PREFILL, fn);
}
if (head == tail) {
/* Empty FIFO — nothing to serve this tick. */
pthread_mutex_unlock(&g_dl_fifo_mutex);
static uint64_t empty_count = 0;
if (empty_count++ < 5)
LOGP(DDEV, LOGL_INFO, "FIFO empty at qfn=%u\n", fn);
continue;
}
struct dl_fifo_entry *e = &g_dl_fifo[head % DL_FIFO_SIZE];
/* Patch fn into header : la VRAIE FN du burst (depuis e->ts), PAS le qfn
* courant. Sinon la latence FIFO (DL_PREFILL=32) decale la FN de ~32
* frames -> fn%51 faux -> blocs BCCH mal assembles -> decode foire.
* LA derniere piece : fifo_depth=32 scramblait la FN. */
uint32_t bfn = (uint32_t)(e->ts / ((uint64_t)CALYPSO_FRAME_SAMPLES));
e->pkt[0] = 0; /* tn=0 */
e->pkt[1] = (uint8_t)(bfn >> 24);
e->pkt[2] = (uint8_t)(bfn >> 16);
e->pkt[3] = (uint8_t)(bfn >> 8);
e->pkt[4] = (uint8_t)(bfn);
ssize_t sent = sendto(g_bsp_fd, e->pkt,
TRXD_HDR_LEN + CALYPSO_DL_BURSTLEN * 4, 0,
(struct sockaddr *)&g_bsp_peer,
sizeof(g_bsp_peer));
bool was_fcch = e->is_fcch;
uint64_t ets = e->ts;
g_dl_fifo_head = head + 1;
size_t depth = tail - g_dl_fifo_head;
pthread_mutex_unlock(&g_dl_fifo_mutex);
static uint64_t qsend_count = 0;
if (qsend_count < 10 || (qsend_count % 500) == 0 || was_fcch) {
LOGP(DDEV, LOGL_INFO,
"qfn-serve #%llu qfn=%u ts=%llu%s fifo_depth=%zu sent=%zd\n",
(unsigned long long)qsend_count, fn,
(unsigned long long)ets,
was_fcch ? " *FCCH*" : "", depth, sent);
}
qsend_count++;
}
close(fd);
g_clk_fd = -1;
return NULL;
}
static int bsp_udp_init(void) { pthread_mutex_lock(&g_bsp_mutex);
if (g_bsp_fd >= 0) { pthread_mutex_unlock(&g_bsp_mutex); return
0; }
const char *host = getenv("CALYPSO_BSP_HOST");
const char *port_s = getenv("CALYPSO_BSP_PORT");
if (!host || !*host) host = QEMU_BSP_HOST_DEFAULT;
uint16_t port = (port_s && *port_s) ? (uint16_t)atoi(port_s) : QEMU_BSP_PORT_DEFAULT;
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) {
LOGP(DDEV, LOGL_ERROR, "bsp_udp_init: socket() failed: %s\n", strerror(errno));
pthread_mutex_unlock(&g_bsp_mutex);
return -1;
}
memset(&g_bsp_peer, 0, sizeof(g_bsp_peer));
g_bsp_peer.sin_family = AF_INET;
g_bsp_peer.sin_port = htons(port);
if (inet_aton(host, &g_bsp_peer.sin_addr) == 0) {
LOGP(DDEV, LOGL_ERROR, "bsp_udp_init: invalid host '%s'\n", host);
close(fd);
pthread_mutex_unlock(&g_bsp_mutex);
return -1;
}
g_bsp_fd = fd;
LOGP(DDEV, LOGL_NOTICE, "bsp_udp_init: TRXDv0 → %s:%u (fd=%d)\n", host, port, fd);
pthread_mutex_unlock(&g_bsp_mutex);
return 0;
}
/* Compute (FN, TN) from a sample timestamp. FBSB only listens on C0
TN=0 so * we tag all bursts with TN=0 — sufficient until SDCCH/RACH
phase. * Currently unused (Phase 1 uses live g_qemu_qfn instead), kept
for Phase 2 * slot-rewrite that needs bts_fn % 51. /
attribute((unused)) static void ts_to_fn_tn(uint64_t
ts, uint32_t fn_out, uint8_t tn_out) { uint64_t frame = ts /
SAMPLES_PER_FRAME; fn_out = (uint32_t)(frame % GSM_HYPERFRAME);
*tn_out = 0; }
/* Build the 8-byte TRXDv0 header into out[0..7]. / static void
trxd_build_hdr(uint8_t out[TRXD_HDR_LEN], uint32_t fn, uint8_t tn) {
out[0] = (tn & 0x07); / version=0 in high nibble, TN in low 3
/ out[1] = (uint8_t)(fn >> 24); out[2] = (uint8_t)(fn >>
16); out[3] = (uint8_t)(fn >> 8); out[4] = (uint8_t)(fn); out[5] =
0; / RSSI placeholder / out[6] = 0; / ToA hi / out[7]
= 0; / ToA lo */ }
/* —- open / close —- */
void uhdwrap_open(struct ipc_sk_if_open_req open_req) {
struct qemu_dev d = calloc(1, sizeof(d)); if (!d) { LOGP(DDEV,
LOGL_ERROR, “qemu_wrap_open: calloc failed”); return NULL; }
d->num_chans = open_req->num_chans; d->rx_ts = 0;
LOGP(DDEV, LOGL_NOTICE,
"qemu_wrap_open: num_chans=%u clockref=0x%x rx_fs=%u/%u tx_fs=%u/%u bw=%u\n",
open_req->num_chans, open_req->clockref,
open_req->rx_sample_freq_num, open_req->rx_sample_freq_den,
open_req->tx_sample_freq_num, open_req->tx_sample_freq_den,
open_req->bandwidth);
/* Start the QEMU clock listener (binds UDP 6700, receives 4 B BE FN
* on every QEMU tdma tick). Idempotent : skip if already running. */
static bool clk_started = false;
if (!clk_started) {
if (pthread_create(&g_clk_thread, NULL, clk_listener, NULL) == 0) {
clk_started = true;
} else {
LOGP(DDEV, LOGL_ERROR,
"qemu_wrap_open: pthread_create(clk_listener) failed\n");
}
}
return d;
}
/* —- info_cnf : reply to osmo-trx-ipc capability query —- */
void uhdwrap_fill_info_cnf(struct ipc_sk_if ipc_prim) { struct
ipc_sk_if_info_cnf info = &ipc_prim->u.info_cnf;
memset(info, 0, sizeof(*info));
info->feature_mask = FEATURE_MASK_CLOCKREF_EXTERNAL;
/* iq_scaling : cs16 full range 1.0 — we don't scale ourselves */
info->iq_scaling_val_rx = 1.0;
info->iq_scaling_val_tx = 1.0;
info->max_num_chans = CALYPSO_NUM_CHANS;
snprintf(info->dev_desc, sizeof(info->dev_desc),
"calypso-ipc-device (QEMU UDP 6702 bridge), GSM %d SPS %.0f Hz",
CALYPSO_TRX_OSR,
(double)CALYPSO_FS_NUM / (double)CALYPSO_FS_DEN * CALYPSO_TRX_OSR);
for (size_t i = 0; i < CALYPSO_NUM_CHANS; i++) {
struct ipc_sk_if_info_chan *ci = &info->chan_info[i];
snprintf(ci->tx_path[0], RF_PATH_NAME_SIZE, "%s", CALYPSO_PATH_NAME);
snprintf(ci->rx_path[0], RF_PATH_NAME_SIZE, "%s", CALYPSO_RX_PATH_NAME);
ci->min_rx_gain = 0.0;
ci->max_rx_gain = 100.0;
ci->min_tx_gain = 0.0;
ci->max_tx_gain = 100.0;
ci->nominal_tx_power = 0.0; /* dBm — placeholder */
}
LOGP(DDEV, LOGL_INFO, "qemu_wrap_fill_info_cnf: 1 chan, fs=%.0f Hz, %d SPS\n",
(double)CALYPSO_FS_NUM / (double)CALYPSO_FS_DEN * CALYPSO_TRX_OSR, CALYPSO_TRX_OSR);
}
/* —- buffer sizing + timing —- */
int32_t uhdwrap_get_bufsizerx(void *dev) { (void)dev; return
CALYPSO_SHM_BUFSIZE; }
int32_t uhdwrap_get_timingoffset(void dev) { (void)dev; return 0;
/ no analog pipeline → no path delay to compensate */ }
/* —- start / stop —- */
int32_t uhdwrap_start(void dev, int chan) { struct qemu_dev
d = dev; if (!d || chan < 0 || chan >= 8) return 0;
bool was_started = d->started[chan];
d->started[chan] = true;
LOGP(DDEV, LOGL_NOTICE, "qemu_wrap_start chan=%d (first=%d)\n",
chan, !was_started);
/* Convention ipc-driver-test (cf. ipc_rx_chan_start_req in our fork) :
* a non-zero return on the FIRST chan_start triggers the global RX/TX
* thread creation (uplink_thread + downlink_thread). Subsequent chan
* starts return 0 so we don't spawn duplicate threads. */
return was_started ? 0 : 1;
}
int32_t uhdwrap_stop(void dev, int chan) { struct qemu_dev d
= dev; if (!d || chan < 0 || chan >= 8) return 0;
d->started[chan] = false; LOGP(DDEV, LOGL_NOTICE, “qemu_wrap_stop
chan=%d”, chan); return 1; }
/* —- gain / freq / txatt : no-op echoes —- */
double uhdwrap_set_gain(void *dev, double g, size_t chan, bool
for_tx) { (void)dev; LOGP(DDEV, LOGL_INFO, “qemu_wrap_set_gain chan=%zu
%s=%.1f (no-op)”, chan, for_tx ? “tx” : “rx”, g); return g; }
double uhdwrap_set_freq(void dev, double f, size_t chan, bool
for_tx) { (void)dev; LOGP(DDEV, LOGL_INFO, “qemu_wrap_set_freq chan=%zu
%s=%.0f Hz (no-op)”, chan, for_tx ? “tx” : “rx”, f); /
ipc_rx_chan_setfreq_req does return_code = rv ? 0 : 1. So
returning * 1.0 here (non-zero / true) yields return_code=0 →
osmo-trx-ipc sees * success. Returning 0.0 would mean failure. */ return
1.0; }
double uhdwrap_set_txatt(void *dev, double a, size_t chan) {
(void)dev; LOGP(DDEV, LOGL_INFO, “qemu_wrap_set_txatt chan=%zu att=%.1f
(no-op)”, chan, a); return a; }
/*
============================================================================
* UL (IPC TX) : le BSP qemu envoie les bursts UL du mobile en TRXDv0 (8
hdr + * 148 soft-bits ±127) vers 127.0.0.1:5702. On les reçoit, on les
MODULE en * GMSK I/Q (osmo-trx attend de l’I/Q), et on les injecte dans
le slot TS0 du * chunk UL au lieu des zéros. Opt-in CALYPSO_IPC_UL=1
(défaut off → heartbeat). * Sync : best-effort — on place le dernier
burst reçu sur le prochain chunk TS0. * L’alignement FN fin se règle
quand le mobile TX réellement (post-camp). *
============================================================================
/ #include <math.h> #define UL_TRXD_HDR 8 static int g_ul_on =
-1; / CALYPSO_IPC_UL / / FIX OSR 2026-06-04 : osmo-trx
tourne a CALYPSO_TRX_OSR=4 SPS. Le modulateur * DOIT produire 148
symboles * OSR samples (= 592 @ 4 SPS), sinon les 148 * samples 1-SPS
sont lus comme ~37 symboles de charabia -> aucune correlation *
d’access-burst cote osmo-trx -> NOPE -> RACH jamais detectee.
/ static int16_t g_ul_iq[CALYPSO_BSP_BURSTLEN * CALYPSO_TRX_OSR *
2]; / dernier burst modulé @ OSR / static volatile int
g_ul_pending = 0; / 1 = un burst à injecter / static volatile
uint32_t g_ul_real_fn = 0; / FN firmware (sideband) du dernier RACH
-> FN-lock / / === RACH waveform DEDIE (FIX MT-SMS
2026-06-09) ============================ * g_ul_iq est ECRASE a CHAQUE
frame par le chemin SDCCH-idle (ul_mod_laurent -> * g_ul_iq,
~119x/run). Pour le LU c’etait masque : le firmware re-livrait la RACH *
30x sur g_bsp_fd, donc g_ul_iq etait re-rempli juste avant un slot
eligible. * La paging-response (RA=0x98) n’est encodee QU’UNE fois ->
entre l’encode et le * 1er vrai slot RACH (max 51 frames), le SDCCH-idle
clobbe g_ul_iq -> meme avec la * gate corrigee, le burst inject
serait du SDCCH, pas la RACH. On latch donc la * waveform RACH dans un
buffer SEPARE (g_rach_iq) et on l’arme STICKY pour quelques * slots
RACH-eligibles, re-injectee sur le 1er fn_ok. / static int16_t
g_rach_iq[CALYPSO_BSP_BURSTLEN * CALYPSO_TRX_OSR * 2]; / waveform
RACH latchee / static volatile int g_rach_pending = 0; / nb de
slots RACH-eligibles restants a tenter / static volatile uint32_t
g_rach_arm_seq = 0; / incremente a chaque nouvel arm (debug) */
/* Record .cfile de l’I/Q UL synthetise (fc32), symetrique du DL
dsp_iq. Ecrit * UNIQUEMENT depuis le thread uhdwrap_read (ul_drain +
uhdwrap_read) -> pas de * race, lazy-open suffisant. / static
FILE g_ul_rec = NULL; static int g_ul_rec_init = 0;
/* MSK phase-continue a OSR samples/symbole : 148 soft-bits (±127)
-> 148OSR cs16 I/Q. Increment de phase ±(π/2)/OSR par
SAMPLE (convention osmo-trx : * bit 1 → +π/2 par symbole). Amplitude
~0.6 full-scale (override CALYPSO_UL_AMP). / static void
ul_gmsk_mod(const int8_t bits, int16_t iq) { static double AMP
= -1.0; static int ACT = -2; if (AMP < 0.0) { const char e =
getenv(“CALYPSO_UL_AMP”); AMP = (e && e) ? atof(e) :
20000.0; } if (ACT == -2) { const char e =
getenv(“CALYPSO_UL_ACTIVE_SYMS”); ACT = (e && e) ? atoi(e) :
-1; } / ACCESS BURST (RACH) : seulement 88 symboles ACTIFS (8 tail
+ 41 sync etendu * + 36 data + 3 tail), puis 60 symboles de GUARD =
SILENCE (IQ=0, PAS du GMSK : * un 0 GMSK-module est un tone fc/4, le
correlateur RACH veut un gap d’energie). * 88OSR=352 GMSK +
60OSR=240 zeros = 592 = burst. Auto-detection access-vs- * normal :
tail[0..7]==0 ET guard[88..147]==0 -> access burst. Override *
CALYPSO_UL_ACTIVE_SYMS (>0 force, -1/unset = auto). / static int
INV = -1, USEG = -1; if (INV < 0) { const char e =
getenv(“CALYPSO_UL_INVERT”); INV = (e && e == ‘1’) ? 1 : 0;
} if (USEG < 0) { const char e = getenv(“CALYPSO_UL_GMSK”); USEG
= (!e || e != ‘0’); } / defaut GMSK / const int N =
CALYPSO_BSP_BURSTLEN, OSR = CALYPSO_TRX_OSR, NS = N OSR; int
active = N; if (ACT > 0) active = ACT; /* FIX RACH FANTÔMES : plus
d’auto-détection access-burst depuis le motif de bits. * Le repli
non-RACH (bits BSP idle, souvent tail0+guard0) était modulé en *
access-burst -> osmo-trx détectait des RACH RA=3 FANTÔMES (mesuré :
90 CHAN RQD * pour 6 vrais RACH) -> canaux SDCCH alloués sans SABM
-> WAIT_RLL timeout -> * fuite -> épuisement du pool -> le
SMS MO n’obtient plus de canal. Le VRAI RACH * passe par ul_mod_laurent
(waveform osmo-trx exacte), JAMAIS par ul_gmsk_mod ; * le self-test
aussi. Donc ici = toujours burst normal 148 sym (override possible * via
CALYPSO_UL_ACTIVE_SYMS pour debug). */ if (active > N) active =
N;
if (!USEG) {
/* MSK fallback (CALYPSO_UL_GMSK=0) */
double ph = 0.0; int idx = 0;
for (int i = 0; i < N; i++) {
if (i >= active) { for (int s=0;s<OSR;s++){iq[2*idx]=0;iq[2*idx+1]=0;idx++;} continue; }
int b = ((bits[i] > 0) ? 1 : 0) ^ INV;
double step = (b ? 1.0 : -1.0) * (M_PI/2.0)/(double)OSR;
for (int s=0;s<OSR;s++){iq[2*idx]=(int16_t)(cos(ph)*AMP);iq[2*idx+1]=(int16_t)(sin(ph)*AMP);ph+=step;idx++;}
}
return;
}
/* GMSK BT=0.3 : pulse de frequence gaussien (osmo-trx correle du GMSK, pas du MSK).
* freq[n] = Sum_k alpha[k]*g(n-k*OSR) ; phi = cumsum(freq)*pi/2 ; I/Q=AMP*(cos,sin). */
#define GMSK_L 4
static double g_pulse[GMSK_L * CALYPSO_TRX_OSR];
static int g_init = 0;
if (!g_init) {
const double BT = 0.3, ln2 = 0.6931471805599453, kk = 2.0*M_PI*BT/sqrt(ln2);
int Lo = GMSK_L*OSR; double sum = 0;
for (int m = 0; m < Lo; m++) {
double t = ((double)m - Lo/2.0 + 0.5)/OSR; /* symboles, centre */
double q1 = 0.5*erfc(kk*(t-0.5)/sqrt(2.0));
double q2 = 0.5*erfc(kk*(t+0.5)/sqrt(2.0));
g_pulse[m] = q1 - q2; sum += g_pulse[m];
}
if (sum != 0.0) for (int m = 0; m < Lo; m++) g_pulse[m] /= sum; /* Sigma=1 -> pi/2 par symbole */
g_init = 1;
}
static double freq[CALYPSO_BSP_BURSTLEN * CALYPSO_TRX_OSR];
for (int n = 0; n < NS; n++) freq[n] = 0.0;
int Lo = GMSK_L*OSR;
for (int k = 0; k < active; k++) {
double al = (((bits[k] > 0) ? 1 : 0) ^ INV) ? 1.0 : -1.0;
int base = k*OSR - Lo/2;
for (int m = 0; m < Lo; m++) { int pos = base+m; if (pos >= 0 && pos < NS) freq[pos] += al*g_pulse[m]; }
}
/* ROTATION GMSK (osmo-trx GMSKRotate) : +pi/2 par SYMBOLE = +pi/(2*OSR) par sample.
* Sans elle, le signal est decale de fs/(2*OSR) -> le demod osmo-trx (qui dé-rote
* de la meme quantite) recoit du garbage -> BER 456/456. C'etait LA piece manquante
* (le RACH echouait pareil). Gate CALYPSO_UL_ROT (def 1), signe CALYPSO_UL_ROT_SGN. */
static int ROT = -2; static double ROTSGN = 1.0;
if (ROT == -2) {
const char *e = getenv("CALYPSO_UL_ROT"); ROT = (!e || *e != '0') ? 1 : 0;
const char *s = getenv("CALYPSO_UL_ROT_SGN"); ROTSGN = (s && atoi(s) < 0) ? -1.0 : 1.0;
}
const double ROTSTEP = ROTSGN * (M_PI / 2.0) / (double)OSR; /* pi/8 @ OSR=4 */
double phi = 0.0; int active_end = active*OSR + Lo; if (active_end > NS) active_end = NS;
for (int n = 0; n < NS; n++) {
phi += (M_PI/2.0)*freq[n];
double pt = ROT ? (phi + ROTSTEP * (double)n) : phi;
if (n < active_end) { iq[2*n] = (int16_t)(cos(pt)*AMP); iq[2*n+1] = (int16_t)(sin(pt)*AMP); }
else { iq[2*n] = 0; iq[2*n+1] = 0; } /* guard silence */
}
}
/* === Modulateur GMSK Laurent EXACT d’osmo-trx (port C de
sigProcLib::modulateBurstLaurent). * Le GMSK maison ne correle pas le
detecteur osmo-trx (BER 456/456) ; CE modulateur si * (la table RACH =
son dump -> rc=3). bits[nbits] soft +/-1 -> symboles +/-1 @ sps=4,
* rotation pi/(2sps)/sample, convolution pulse Laurent c0 (+ c1 =
jXOR(b[i-1],b[i-2])), * somme. Sortie cs16 (scale CALYPSO_UL_AMP,
def 20000) sur CALYPSO_BSP_BURSTLENOSR samples. convolve
START_ONLY : out[n] = sum_{j=0}^{H-1} in[n-(H-1)+j]h[j] (in=0 si
index<0). / static void ul_mod_laurent(const int8_t bits,
int nbits, int16_t iq) { const int sps = CALYPSO_TRX_OSR; /* 4
/ const int BL = 625; / burst_len osmo-trx / static const
double C0[16] = { 0.0, 4.46348606e-03, 2.84385729e-02, 1.03184855e-01,
2.56065552e-01, 4.76375085e-01, 7.05961177e-01, 8.71291644e-01,
9.29453645e-01, 8.71291644e-01, 7.05961177e-01, 4.76375085e-01,
2.56065552e-01, 1.03184855e-01, 2.84385729e-02, 4.46348606e-03 }; static
const double C1[8] = { 0.0, 8.16373112e-03, 2.84385729e-02,
5.64158904e-02, 7.05463553e-02, 5.64158904e-02, 2.84385729e-02,
8.16373112e-03 }; static double AMP = -1.0; if (AMP < 0.0) { const
char e = getenv(“CALYPSO_UL_AMP”); AMP = (e && *e) ?
atof(e) : 20000.0; } if (nbits > 156) nbits = 156;
static double sym[625], c0r[625], c0i[625], c1r[625], c1i[625];
for (int n = 0; n < BL; n++) { sym[n]=0; c0r[n]=0; c0i[n]=0; c1r[n]=0; c1i[n]=0; }
int b[160]; for (int i = 0; i < nbits; i++) b[i] = (bits[i] > 0) ? 1 : 0;
/* symboles +/-1 : index 0 = padding tail(-1), sps,2sps.. = 2*bit-1, puis padding tail. */
int idx = 0;
sym[idx] = -1.0; idx += sps;
for (int i = 0; i < nbits; i++) { sym[idx] = 2.0*b[i]-1.0; idx += sps; }
if (idx < BL) sym[idx] = -1.0;
/* rotation GMSK : c0[n] = sym[n] * e^(j n pi/(2*sps)) */
const double rstep = (M_PI/2.0)/(double)sps;
for (int n = 0; n < BL; n++) { double ph = rstep*(double)n; c0r[n] = sym[n]*cos(ph); c0i[n] = sym[n]*sin(ph); }
/* c1[k] = c0[k] * (j*phase) = -phase*c0i + j*phase*c0r ; phase=2*(b[i-1]^b[i-2])-1.
* start magic (k=sps*2, phase=-1), i=2..nbits-1, end magic (i=nbits). */
if (nbits >= 2) {
int k = sps*2; double phase = -1.0;
c1r[k] = -phase*c0i[k]; c1i[k] = phase*c0r[k]; k += sps;
for (int i = 2; i < nbits; i++) {
phase = 2.0*(double)(b[i-1]^b[i-2]) - 1.0;
if (k < BL) { c1r[k] = -phase*c0i[k]; c1i[k] = phase*c0r[k]; }
k += sps;
}
phase = 2.0*(double)(b[nbits-1]^b[nbits-2]) - 1.0;
if (k < BL) { c1r[k] = -phase*c0i[k]; c1i[k] = phase*c0r[k]; }
}
int NS = CALYPSO_BSP_BURSTLEN * sps; /* 592 */
for (int n = 0; n < NS; n++) {
double or_ = 0.0, oi_ = 0.0;
for (int j = 0; j < 16; j++) { int s = n-15+j; if (s>=0 && s<BL) { or_ += c0r[s]*C0[j]; oi_ += c0i[s]*C0[j]; } }
for (int j = 0; j < 8; j++) { int s = n-7+j; if (s>=0 && s<BL) { or_ += c1r[s]*C1[j]; oi_ += c1i[s]*C1[j]; } }
double I = or_*AMP, Q = oi_*AMP;
if (I>32767.0) I=32767.0; else if (I<-32768.0) I=-32768.0;
if (Q>32767.0) Q=32767.0; else if (Q<-32768.0) Q=-32768.0;
iq[2*n] = (int16_t)I; iq[2*n+1] = (int16_t)Q;
}
}
/* Enregistre l’I/Q UL synthetise (cs16 -> fc32 normalise -1..1)
dans un .cfile, * exactement comme le DL dsp_iq (gr_complex, 4 SPS =
1083333 Hz). Bursts * concatenes (pas de framing TDMA) : decodable comme
le dsp_iq DL. Lazy-open ; * CALYPSO_UL_IQ_RECORD= (defaut
/dev/shm/dsp_ul_iq.cfile), vide=off. * nsamp = nb de samples COMPLEXES
(592 = CALYPSO_BSP_BURSTLENOSR par burst). / static void
ul_iq_record(const int16_t iq, int nsamp) { if (!g_ul_rec_init) {
g_ul_rec_init = 1; const char p = getenv(“CALYPSO_UL_IQ_RECORD”);
if (!p) p = “/dev/shm/dsp_ul_iq.cfile”; if (p) { g_ul_rec = fopen(p,
“wb”); if (g_ul_rec) LOGP(DDEV, LOGL_NOTICE, “[ul-rec] record I/Q UL
-> %s (cfile fc32 @ 4 SPS)”, p); else LOGP(DDEV, LOGL_ERROR,
“[ul-rec] fopen(%s): %s”, p, strerror(errno)); } } if (!g_ul_rec ||
nsamp <= 0) return; static float fbuf[CALYPSO_BSP_BURSTLEN *
CALYPSO_TRX_OSR * 2]; int nf = nsamp 2; if (nf >
(int)(sizeof(fbuf)/sizeof(fbuf[0]))) nf =
(int)(sizeof(fbuf)/sizeof(fbuf[0])); for (int i = 0; i < nf; i++)
fbuf[i] = (float)iq[i] / 32768.0f; fwrite(fbuf, sizeof(float),
(size_t)nf, g_ul_rec); }
/* RACH access-burst complet en soft-bits +/-1 pour ul_gmsk_mod : *
[8 tail][41 sync TS0][36 bits codes gsm0503_rach_ext_encode][3 tail],
reste guard. * La sync = GSM::gRACHSynchSequenceTS0 (exactement ce que
correle osmo-trx). Le DSP * Calypso fait normalement ce codage+sync ;
shunte, on le refait ici. RA/BSIC env : * CALYPSO_UL_RA (defaut 3, fixe
pour prouver rc>0), CALYPSO_UL_BSIC (defaut 7 = BSIC * reel ; colore
la parite -> requis pour CHAN RQD cote osmo-bts, pas pour rc). /
static void ul_build_rach_ra(int8_t ab, int ra_arg, int bsic_arg) {
static const char SYNC[] = “01001011011111111001100110101010001111000”;
/* 41 / static int RA_env = -1, BSIC_env = -1; if (RA_env < 0) {
const char e = getenv(“CALYPSO_UL_RA”); RA_env = (e &&
e) ? (int)strtol(e, 0, 0) : 3; } if (BSIC_env < 0) { const char
e = getenv(“CALYPSO_UL_BSIC”); BSIC_env = (e && e) ?
atoi(e) : 7; } int RA = (ra_arg >= 0) ? ra_arg : RA_env; / RA
reelle du mobile (paging: >=0x80) / int BSIC = (bsic_arg >= 0)
? bsic_arg : BSIC_env; ubit_t coded[40]; memset(coded, 0,
sizeof(coded)); gsm0503_rach_ext_encode(coded, (uint16_t)RA,
(uint8_t)BSIC, false); / 36 bits codes / for (int i = 0; i <
CALYPSO_BSP_BURSTLEN; i++) ab[i] = -1; / tail/guard par defaut
/ int p = 0; for (int i = 0; i < 8; i++) ab[p++] = -1; /
extended tail / for (int i = 0; i < 41; i++) ab[p++] = (SYNC[i]
== ‘1’) ? 1 : -1; / synch sequence / for (int i = 0; i < 36;
i++) ab[p++] = coded[i] ? 1 : -1; / RA codee (BSIC color) / for
(int i = 0; i < 3; i++) ab[p++] = -1; / tail / / p==88
; [88..147]=-1 -> ul_gmsk_mod auto-detecte active=88 + guard silence
*/ }
/* compat : RA/BSIC depuis l’env (CALYPSO_UL_RA / _BSIC). /
static void ul_build_rach(int8_t ab) { ul_build_rach_ra(ab, -1,
-1); }
/* Construit le burst NORMAL #bid (0..3) du bloc SDCCH/SACCH depuis
la L2 (23o) : * gsm0503_xcch_encode -> 4116 bits e[] (GSM 05.03
conv+FIRE+interleave). Burst normal = [3 tail][58 e (57 data +
steal)][26 TSC7][58 e][3 tail] en soft-bits +/-1. Tout actif * (148)
-> ul_gmsk_mod fait du GMSK plein (le motif != access-burst -> pas
de guard). / static void ul_build_sdcch_burst(int8_t ab, const
uint8_t l2, int bid) { static const uint8_t TSC7[26] = {
1,1,1,0,1,1,1,1,0,0,0,1,0,0,1,0,1,1,1,0,1,1,1,1,0,0 }; ubit_t e[4 *
116]; memset(e, 0, sizeof(e)); gsm0503_xcch_encode(e, l2); const ubit_t
cB = e + (bid & 3) * 116; int p = 0; for (int i = 0; i < 3;
i++) ab[p++] = -1; /* tail / for (int i = 0; i < 58; i++) ab[p++]
= cB[i] ? 1 : -1; / data 1 (57 + steal) / for (int i = 0; i
< 26; i++) ab[p++] = TSC7[i] ? 1 : -1; / midamble TSC7 / for
(int i = 0; i < 58; i++) ab[p++] = cB[58 + i] ? 1 : -1; / data 2
/ for (int i = 0; i < 3; i++) ab[p++] = -1; / tail /
/ p==148, tout actif -> GMSK plein */ }
/* SELF-TEST (#12) : module l’access-burst (le MÊME que rach_ref.cs16
= dump du vrai * modulateBurst osmo-trx) avec ul_mod_laurent et compare.
maxdiff~0 => port correct. * Le pattern du diff isole le bug :
echelle (AMP), decalage (convolution/TOA), * conjugue (signe rotation),
renverse (sens convol). Appelé 1x. / static void
ul_laurent_selftest(void) { int8_t ab[CALYPSO_BSP_BURSTLEN];
ul_build_rach(ab); / 88 bits actifs = ceux de rach_ref / static
int16_t my[CALYPSO_BSP_BURSTLEN * CALYPSO_TRX_OSR * 2];
ul_mod_laurent(ab, 88, my); FILE f = fopen(“/root/rach_ref.cs16”,
“rb”); if (!f) { LOGP(DDEV, LOGL_NOTICE, “LAURENT-SELFTEST: pas de
/root/rach_ref.cs16”); return; } static int16_t ref[CALYPSO_BSP_BURSTLEN
* CALYPSO_TRX_OSR * 2]; size_t got = fread(ref, 2 * sizeof(int16_t),
CALYPSO_BSP_BURSTLEN * CALYPSO_TRX_OSR, f); fclose(f); int N = (int)got;
if (N > CALYPSO_BSP_BURSTLEN * CALYPSO_TRX_OSR) N =
CALYPSO_BSP_BURSTLEN * CALYPSO_TRX_OSR; long maxd = 0, sumd = 0; for
(int i = 0; i < N * 2; i++) { long d = labs((long)my[i] -
(long)ref[i]); if (d > maxd) maxd = d; sumd += d; } LOGP(DDEV,
LOGL_NOTICE, “LAURENT-SELFTEST: N=%d maxdiff=%ld avgdiff=%ld |
mine[0..7]=%d,%d %d,%d %d,%d %d,%d” “ref[0..7]=%d,%d %d,%d %d,%d %d,%d”,
N, maxd, sumd / (N * 2 + 1),
my[0],my[1],my[2],my[3],my[4],my[5],my[6],my[7],
ref[0],ref[1],ref[2],ref[3],ref[4],ref[5],ref[6],ref[7]); }
/* Sideband RACH (NO-HARDCODE) : lit la VRAIE RA+BSIC+FN publiee par
QEMU * (calypso_trx.c calypso_rach_publish) dans /dev/shm/calypso_rach.
Fichier * REGULIER (pas un FIFO -> jamais bloquant). Layout 16o fige,
partage avec QEMU : * [0..3]=seq(u32 LE) [4]=ra [5]=bsic [8..11]=fn(u32
LE). Retourne 1 si seq>0. / static int calypso_rach_read(uint8_t
ra, uint8_t bsic, uint32_t fn) { static int fd = -1; if
(fd < 0) fd = open(“/dev/shm/calypso_rach”, O_RDONLY); /* retry tant
que QEMU ne l’a pas cree / if (fd < 0) return 0; uint8_t buf[16];
if (pread(fd, buf, sizeof(buf), 0) != (ssize_t)sizeof(buf)) return 0;
uint32_t seq; memcpy(&seq, buf + 0, sizeof(seq)); if (seq == 0)
return 0; if (ra) ra = buf[4]; if (bsic) bsic = buf[5]; if (fn)
memcpy(fn, buf + 8, sizeof(fn)); return 1; }
/* Lit /dev/shm/calypso_kc (ecrit par QEMU l1ctl_sock sur
L1CTL_CRYPTO_REQ) : * [0..3]seq(LE) [4]algo [5]key_len [6..21]Kc.
Retourne le seq (0 = pas de cipher * actif : aucun CIPHER MODE COMMAND,
ou canal reset via DM_EST/DM_REL). / static uint32_t
calypso_kc_read(uint8_t algo, uint8_t kc, uint8_t klen) {
static int fd = -1; if (fd < 0) fd = open(“/dev/shm/calypso_kc”,
O_RDONLY); if (fd < 0) return 0; uint8_t buf[32]; if (pread(fd, buf,
sizeof(buf), 0) != (ssize_t)sizeof(buf)) return 0; uint32_t seq;
memcpy(&seq, buf, 4); if (seq == 0) return 0; if (algo) algo =
buf[4]; if (klen) klen = buf[5]; if (kc) memcpy(kc, buf + 6, 16);
return seq; }
/* SDCCH/SACCH UL sideband (#12 PIÈCE 2) : lit la L2 montante (a_cu)
publiée par QEMU * (calypso_dsp_shunt) dans /dev/shm/calypso_sdcch_ul.
Layout 48o : seq@0(u32) * l1s_fn@4(u32) fn@8(u32) task_u@12(u16) l1s%51@14(u8) l2[23]@16. Retourne 1 si seq>0. / static int
calypso_sdcch_ul_read(uint8_t l2, uint8_t l1s_mod51, uint32_t
l1s_fn, uint32_t seq_out) { static int fd = -1; if (fd < 0)
fd = open(“/dev/shm/calypso_sdcch_ul”, O_RDONLY); if (fd < 0) return
0; uint8_t buf[48]; if (pread(fd, buf, sizeof(buf), 0) !=
(ssize_t)sizeof(buf)) return 0; uint32_t seq; memcpy(&seq, buf + 0,
sizeof(seq)); if (seq == 0) return 0; if (seq_out) seq_out = seq;
if (l1s_fn) memcpy(l1s_fn, buf + 4, sizeof(l1s_fn)); if (l1s_mod51)
l1s_mod51 = buf[14]; if (l2) memcpy(l2, buf + 16, 23); return 1;
}
/* Draine l’UL sur g_bsp_fd (le BSP renvoie l’UL à la source du DL =
nous, * cf. calypso_bsp.c:381), module le dernier burst dispo.
Non-bloquant. */ static void ul_drain(void) { static int _stdone = 0; if
(!_stdone) { _stdone = 1; ul_laurent_selftest(); } /* #12 : valide le
port Laurent 1x / if (g_bsp_fd < 0) return; uint8_t
pkt[UL_TRXD_HDR + CALYPSO_BSP_BURSTLEN + 16]; int got = 0, got_rach = 0;
/ got_rach : un VRAI access-burst RACH a ete draine / for (;;)
{ ssize_t n = recvfrom(g_bsp_fd, pkt, sizeof(pkt), MSG_DONTWAIT, NULL,
NULL); if (n < (ssize_t)(UL_TRXD_HDR + CALYPSO_BSP_BURSTLEN)) break;
const int8_t bits = (const int8_t )(pkt + UL_TRXD_HDR); /
RACH ENC (defaut ON) : reconstruit l’access-burst code+sync (le DSP
shunte * ne le fait plus), au lieu de moduler les bits firmware (sans
sync). / static int rach_enc = -1; if (rach_enc < 0) { const char
e = getenv(“CALYPSO_UL_RACH_ENC”); rach_enc = (!e || *e != ‘0’);
}
/* === NO-HARDCODE : TABLE de modulation per-RA ===========================
* La VRAIE RA du mobile (d_rach@0x0474, plombee via /dev/shm/calypso_rach)
* varie a chaque burst. osmo-trx a pre-genere /root/rach_ref_RA<nn>.cs16
* (sa modulation Laurent EXACTE, qui correle son detecteur) pour chaque RA.
* On selectionne le ref de la VRAIE RA -> le BTS voit la bonne RA, plus le
* RA=3 fixe. Repli : ancien rach_ref.cs16 (RA fixe), puis GMSK maison. */
static int16_t ref_tab[16][CALYPSO_BSP_BURSTLEN * CALYPSO_TRX_OSR * 2];
static int ref_n[16]; /* 0=pas charge, <0=absent, >0=samples */
static int ref_init = 0;
if (!ref_init) { for (int i = 0; i < 16; i++) ref_n[i] = 0; ref_init = 1; }
int used_tab = 0;
if (rach_enc) {
uint8_t real_ra = 0xff, real_bsic = 0; uint32_t real_fn = 0;
/* #SMS/paging : encode l'access-burst pour la VRAIE RA a la volee (tout RA
* 0x00-0xff). La paging response a une RA >= 0x80 (cause "answer to paging") ;
* l'ancien gate real_ra<16 + table per-RA 0x00-0x0f la ratait -> repli RA=3 ->
* IMM ASS reqref mismatch -> echec. ul_build_rach_ra + ul_mod_laurent = forme
* d'onde EXACTE osmo-trx (cf LAURENT-SELFTEST) ; la detection osmo-trx correle
* la sync (RA-indep) puis decode RA+BSIC -> reqref correcte -> le mobile matche. */
if (calypso_rach_read(&real_ra, &real_bsic, &real_fn)) {
g_ul_real_fn = real_fn; /* stash pour le FN-lock (uhdwrap_read) */
int8_t ab_rach[CALYPSO_BSP_BURSTLEN];
ul_build_rach_ra(ab_rach, (int)real_ra, (int)real_bsic);
ul_mod_laurent(ab_rach, 88, g_ul_iq); /* 88 bits actifs = access-burst */
ul_iq_record(g_ul_iq, CALYPSO_BSP_BURSTLEN * CALYPSO_TRX_OSR); /* record I/Q UL (RACH paging-resp) */
/* FIX MT-SMS : latch la waveform RACH dans son buffer dedie et arme
* STICKY sur N slots RACH-eligibles. Le chemin SDCCH-idle ecrase g_ul_iq
* a chaque frame ; g_rach_iq lui reste intact -> la paging-response (un
* seul encode) survit jusqu'a un vrai slot RACH. CALYPSO_UL_RACH_STICKY =
* nb de slots eligibles a tenter (def 8 ~ couvre >1 multiframe-51). */
memcpy(g_rach_iq, g_ul_iq, sizeof(g_rach_iq));
{ static int rsticky = -1;
if (rsticky < 0) { const char *e = getenv("CALYPSO_UL_RACH_STICKY"); rsticky = (e && *e) ? atoi(e) : 0; /* defaut OFF (gate vide) : sticky retire */ }
g_rach_pending = rsticky; g_rach_arm_seq++; }
used_tab = 1;
static int last_ra = -1;
if ((int)real_ra != last_ra) {
last_ra = real_ra;
LOGP(DDEV, LOGL_NOTICE,
"UL RACH RA REELLE=0x%02x bsic=0x%02x (encode a la volee, fn=%u)\n",
real_ra, real_bsic, real_fn);
}
}
}
/* (DECANNE 2026-06-07 : ancien repli rach_ref.cs16 RA=3 supprime) */
if (used_tab) {
/* g_ul_iq rempli par la VRAIE RA encodee a la volee */
} else {
/* DECANNE (2026-06-07) : plus de repli RA=3 canne (rach_ref.cs16 / maison).
* Sans vraie RA publiee dans /dev/shm/calypso_rach, on module les bits BSP
* bruts (sans sync RACH reconstruite) -> osmo-trx ne les detecte PAS comme un
* Channel Request. Fin des RACH RA=3 fantomes qui inondaient le BSC en CHAN
* RQD et saturaient le pool SDCCH. */
ul_gmsk_mod(bits, g_ul_iq);
ul_iq_record(g_ul_iq, CALYPSO_BSP_BURSTLEN * CALYPSO_TRX_OSR); /* record I/Q UL (RACH brute BSP) */
}
got = 1;
if (used_tab) got_rach = 1; /* seul un VRAI RACH (RA publiee) arme g_ul_pending */
/* INSTR 2026-06-04 : dump one-shot des 1ers bursts UL recus du BSP pour
* VOIR si c'est un vrai access-burst (sync RACH 41b) ou autre chose, et
* confirmer la sortie OSR=4. Couper via CALYPSO_UL_DEBUG=0. */
static int ul_dbg = -1, ul_seen = 0;
if (ul_dbg < 0) { const char *e = getenv("CALYPSO_UL_DEBUG"); ul_dbg = (!e || *e!='0'); }
if (ul_dbg && ul_seen < 8) {
ul_seen++;
char bs[CALYPSO_BSP_BURSTLEN + 1];
int nz = 0;
for (int i = 0; i < CALYPSO_BSP_BURSTLEN; i++) { bs[i] = bits[i] > 0 ? '1':'0'; if (bits[i]) nz++; }
bs[CALYPSO_BSP_BURSTLEN] = 0;
int s0 = g_ul_iq[0], s1 = g_ul_iq[1], smid = g_ul_iq[CALYPSO_BSP_BURSTLEN*CALYPSO_TRX_OSR];
LOGP(DDEV, LOGL_NOTICE,
"UL-DBG #%d in=%db(nz=%d) out=%d samp [%d,%d..mid=%d] bits=%s\n",
ul_seen, CALYPSO_BSP_BURSTLEN, nz,
CALYPSO_BSP_BURSTLEN*CALYPSO_TRX_OSR, s0, s1, smid, bs);
}
}
/* FIX PHANTOM RACH : g_ul_pending (= déclenche la réinjection de g_rach_iq +
* la calibration FN) ne doit s'armer que pour un VRAI RACH, pas pour chaque burst
* SDCCH/idle drainé du BSP. Avant : tout burst -> g_ul_pending=1 -> réinjection du
* dernier RACH à chaque frame -> osmo-trx corrèle en boucle (200 RACH-DET / 9 vrais)
* -> CHAN RQD fantômes -> fuite SDCCH -> épuisement -> SMS sans canal. Le vrai RACH
* du mobile (used_tab=1, RA publiée) arme toujours g_ul_pending -> LU/RACH intacts. */
if (got_rach) g_ul_pending = 1;
}
/* === RELAIS I/Q CONTINU (mode full-grgsm) === * CALYPSO_IPC_RELAY=1
: au lieu d’extraire un burst TS0 → TRXDv0 → BSP Calypso, * on RELAIE
l’I/Q CONTINU (fc32) entre osmo-trx et le transceiver gr-gsm du * mobile
(radio_if_udp). DL : chunk osmo-trx (cs16) → fc32 → UDP RX_PORT. * UL :
UDP TX_PORT (fc32) → cs16 → ios_rx_from_device → osmo-trx. * Plus de DSP
Calypso → plus de congestion. / static int g_relay_on = -1; static
int g_relay_dl_fd = -1; / send DL fc32 → radio_if_udp RX (5810)
/ static struct sockaddr_in g_relay_dl_dst; static int g_relay_ul_fd
= -1; / recv UL fc32 ← radio_if_udp TX (5811) */ static float
g_relay_fbuf[CALYPSO_SHM_BUFSIZE * 2];
static void relay_init(void) { if (g_relay_on >= 0) return; const
char e = getenv(“CALYPSO_IPC_RELAY”); g_relay_on = (e &&
e == ‘1’) ? 1 : 0; if (!g_relay_on) return; const char host =
getenv(“CALYPSO_TRX_IQ_HOST”); if (!host || !host) host =
“127.0.0.1”; const char rxp = getenv(“CALYPSO_TRX_IQ_RX_PORT”);
const char txp = getenv(“CALYPSO_TRX_IQ_TX_PORT”); int rx_port =
(rxp && rxp) ? atoi(rxp) : 5810; int tx_port = (txp
&& txp) ? atoi(txp) : 5811; g_relay_dl_fd = socket(AF_INET,
SOCK_DGRAM, 0); memset(&g_relay_dl_dst, 0, sizeof(g_relay_dl_dst));
g_relay_dl_dst.sin_family = AF_INET; g_relay_dl_dst.sin_port =
htons(rx_port); g_relay_dl_dst.sin_addr.s_addr = inet_addr(host);
g_relay_ul_fd = socket(AF_INET, SOCK_DGRAM, 0); int one = 1;
setsockopt(g_relay_ul_fd, SOL_SOCKET, SO_REUSEADDR, &one,
sizeof(one)); struct sockaddr_in a; memset(&a, 0, sizeof(a));
a.sin_family = AF_INET; a.sin_port = htons(tx_port); a.sin_addr.s_addr =
htonl(INADDR_ANY); if (bind(g_relay_ul_fd, (struct sockaddr *)&a,
sizeof(a)) < 0) LOGP(DDEV, LOGL_ERROR, “RELAY UL bind(:%d) failed”,
tx_port); LOGP(DDEV, LOGL_NOTICE, “IPC RELAY ON : DL fc32 → %s:%d, UL
fc32 ← :%d (full-grgsm)”, host, rx_port, tx_port); }
/* === FIFO writer FRAME-ATOMIQUE, decouple du hot-path (fix SACCH
grgsm) ======= * BUG corrige (construction IQ “mauvaise a partir de la
fifo”) : l’ancien * write(O_NONBLOCK) direct sur le pipe (a) laissait
passer des writes PARTIELS * (0<w<fbytes) -> desalignement byte
PERMANENT du flux fc32 -> grgsm en garbage ; * (b) DROPpait des
trames sur EAGAIN -> trous temporels -> grgsm perd la *
51-multitrame -> SDCCH/4 SACCH (SI5/SI6) jamais decodee (le BCCH/CCCH
resync * lui via FCCH/SCH, d’ou “ca marche a moitie”). * FIX : 1 thread
writer DEDIE par FIFO + ring de TRAMES. Le hot-path DL pousse * une
trame (memcpy sous lock court ~20KB) ou la DROP ENTIERE si le ring est *
plein ; le writer fait des write() BLOQUANTS COMPLETS (jamais partiels)
-> * alignement byte toujours correct, jamais d’underrun cote QEMU.
/ enum { RELAY_NFIFO_MAX = 8, RELAY_RING = 64, / 8 :
fft+grgsm+record+asciifft + grgsm_ciph (decipher DL) /
RELAY_FRAME_FLOATS = CALYPSO_SHM_BUFSIZE 2 }; typedef struct {
char path[128]; int fd; /* writer-thread-owned / pthread_t th;
pthread_mutex_t mtx; pthread_cond_t cv; float
ring[RELAY_RING][RELAY_FRAME_FLOATS]; size_t rlen[RELAY_RING]; unsigned
head, tail; / SPSC : producer=hot-path, consumer=thread */ unsigned
long dropped, written; } relay_fifo_t; static relay_fifo_t
g_rfifo[RELAY_NFIFO_MAX]; static int g_rfifo_n = -1;
static void relay_fifo_writer(void arg) { relay_fifo_t *rf =
arg; static __thread float local[RELAY_FRAME_FLOATS]; for (;;) { size_t
nfloats; pthread_mutex_lock(&rf->mtx); while (rf->head ==
rf->tail) pthread_cond_wait(&rf->cv, &rf->mtx);
unsigned h = rf->head % RELAY_RING; nfloats = rf->rlen[h];
memcpy(local, rf->ring[h], nfloats * sizeof(float)); rf->head++;
pthread_mutex_unlock(&rf->mtx);
if (rf->fd < 0) {
/* OUVERTURE NON-BLOQUANTE : pas de lecteur (ENXIO) -> on DROP cette
* trame et on retentera a la suivante. CRUCIAL : l'ancien open(O_WRONLY)
* BLOQUANT figeait ce thread tant qu'aucun grgsm n'etait lecteur ; or
* quand si_bridge TUE grgsm pour le respawn cipher, le lecteur
* disparait -> ce writer + la cascade relay gelaient l'ipc-device
* (DL FIFO plein -> osmo-trx SETPOWER no-response -> feed_iq gele ->
* pas de LU accept). Avec O_NONBLOCK le churn de lecteur (kill/respawn
* grgsm) est INOFFENSIF. Une fois ouvert, on RETIRE O_NONBLOCK pour
* garder des write() BLOQUANTS COMPLETS (frame-atomique preserve). */
int fd = open(rf->path, O_WRONLY | O_NONBLOCK);
if (fd < 0) { rf->dropped++; continue; } /* ENXIO : aucun lecteur */
int fl = fcntl(fd, F_GETFL, 0);
if (fl >= 0) fcntl(fd, F_SETFL, fl & ~O_NONBLOCK); /* writes bloquants */
fcntl(fd, F_SETPIPE_SZ, 1 << 20);
rf->fd = fd;
}
const char *p = (const char *)local;
size_t left = nfloats * sizeof(float);
while (left) { /* write COMPLET, jamais partiel */
ssize_t w = write(rf->fd, p, left);
if (w > 0) { p += (size_t)w; left -= (size_t)w; continue; }
if (w < 0 && errno == EINTR) continue;
close(rf->fd); rf->fd = -1; break; /* EPIPE/EBADF : reader parti */
}
if (!left) rf->written++;
}
return NULL;
}
static void relay_fifo_init(void) { if (g_rfifo_n >= 0) return; /*
SIGPIPE ignore : un write() sur une FIFO dont le lecteur (grgsm) vient
de * disparaitre doit renvoyer EPIPE (gere par la boucle write ->
reopen), PAS * tuer le process. / signal(SIGPIPE, SIG_IGN);
g_rfifo_n = 0; const char e = getenv(“CALYPSO_RELAY_FIFOS”); const
char list = (e && e) ? e :
“/tmp/iq_fft.fifo:/tmp/iq_grgsm.fifo:/tmp/iq_record.fifo”; char
buf[512]; strncpy(buf, list, sizeof(buf) - 1); buf[sizeof(buf) - 1] = 0;
char sav = NULL, tok = strtok_r(buf, “:”, &sav); while (tok
&& g_rfifo_n < RELAY_NFIFO_MAX) { relay_fifo_t rf =
&g_rfifo[g_rfifo_n]; strncpy(rf->path, tok, 127);
rf->path[127] = 0; rf->fd = -1; rf->head = rf->tail = 0;
rf->dropped = rf->written = 0; pthread_mutex_init(&rf->mtx,
NULL); pthread_cond_init(&rf->cv, NULL); mkfifo(rf->path,
0666); / ignore EEXIST */ pthread_create(&rf->th, NULL,
relay_fifo_writer, rf); g_rfifo_n++; tok = strtok_r(NULL, “:”,
&sav); } LOGP(DDEV, LOGL_NOTICE, “RELAY FIFO: %d writer-threads
(frame-atomic, ring=%d trames)”, g_rfifo_n, RELAY_RING); }
/* hot-path : pousse 1 trame dans chaque FIFO ; DROP entiere si ring
plein. / static void relay_fifo_push(const float frame, size_t
nfloats) { if (g_rfifo_n < 0) relay_fifo_init(); if (nfloats >
(size_t)RELAY_FRAME_FLOATS) nfloats = RELAY_FRAME_FLOATS; for (int f =
0; f < g_rfifo_n; f++) { relay_fifo_t rf = &g_rfifo[f];
pthread_mutex_lock(&rf->mtx); if (rf->tail - rf->head >=
RELAY_RING) { rf->dropped++; / ring plein -> drop TRAME
entiere / if ((rf->dropped % 500) == 1) LOGP(DDEV, LOGL_INFO,
“RELAY FIFO %s drop=%lu written=%lu”, rf->path, rf->dropped,
rf->written); } else { unsigned t = rf->tail % RELAY_RING;
memcpy(rf->ring[t], frame, nfloats sizeof(float));
rf->rlen[t] = nfloats; rf->tail++;
pthread_cond_signal(&rf->cv); }
pthread_mutex_unlock(&rf->mtx); } }
/* —- RX (uplink_thread loop) : produces UL heartbeat zeros to
osmo-trx —- */
int32_t uhdwrap_read(void dev, uint32_t num_chans) { struct
qemu_dev d = dev; if (!d) return -1;
static int16_t zeros_iq[CALYPSO_SHM_BUFSIZE * 2];
static bool zeros_init = false;
if (!zeros_init) {
memset(zeros_iq, 0, sizeof(zeros_iq));
zeros_init = true;
}
/* UL (IPC TX) init : pas de bind — l'UL arrive sur g_bsp_fd (le BSP renvoie
* l'UL à la source du DL). On se contente du flag + drain. */
if (g_ul_on < 0) {
const char *e = getenv("CALYPSO_IPC_UL");
g_ul_on = (e && *e == '1') ? 1 : 0;
if (g_ul_on)
LOGP(DDEV, LOGL_NOTICE,
"UL (IPC TX) ON : g_bsp_fd → mod GMSK → ios_rx_from_device\n");
}
if (g_ul_on) ul_drain();
/* Chunk UL : zéros par défaut ; si un burst UL est dispo et qu'on est sur
* un chunk TS0 (ts%1250==0), on l'injecte dans le slot TS0 (samples 0..147). */
static int16_t ul_chunk[CALYPSO_SHM_BUFSIZE * 2];
int16_t *ul_src = zeros_iq;
/* --- reglages UL (sweepables sans rebuild) ---
* CALYPSO_UL_FN_OFFSET : decalage FN device->osmo-trx (observe = 31).
* CALYPSO_UL_FN_GATE : 1 = n'injecter que sur un FN RACH-eligible (combined
* CCCH+SDCCH4 : osmo_fn%51 in {4,5,14..36,45,46}).
* CALYPSO_UL_SLOT_OFFSET : offset intra-slot (samples) du burst (TOA). */
static int ul_fnoff = -99999, ul_fngate = -1, ul_slotoff = -1;
if (ul_fnoff == -99999) { const char *e = getenv("CALYPSO_UL_FN_OFFSET"); ul_fnoff = (e && *e) ? atoi(e) : 36; /* hardcode : offset FN device->osmo-trx (gate vide=36) */ }
if (ul_fngate < 0) { const char *e = getenv("CALYPSO_UL_FN_GATE"); ul_fngate = (!e || *e != '0'); }
if (ul_slotoff < 0) { const char *e = getenv("CALYPSO_UL_SLOT_OFFSET"); ul_slotoff = (e && *e) ? atoi(e) : 1875; /* hardcode : TOA intra-slot RACH (gate vide=1875) */ }
uint32_t internal_fn = (uint32_t)(d->rx_ts / (uint64_t)CALYPSO_FRAME_SAMPLES);
uint32_t osmo_fn = internal_fn + (uint32_t)ul_fnoff; /* FN tel que vu par osmo-trx (SDCCH only) */
/* FN-GATE RACH (FIX MT-SMS 2026-06-09) : osmo-trx TAMPONNE+CORRELE le burst injecte
* sur SON fn == internal_fn (PROUVE : la seule RACH-DET du run est a osmo-trx fn=4058,
* exactement = inject#1 internal_fn=4058, que le gate avait etiquete osmo_fn=4094).
* L'ancien gate testait osmo_fn%51 = (internal_fn+36)%51 -> il autorisait des slots
* (internal%51 in {0,9,10,37..44,47..50}) ou osmo-trx fait tourner le correlateur
* NORMAL-BURST, JAMAIS le correlateur RACH -> burst jamais detecte comme access-burst.
* Le LU n'a marche QUE par coincidence (inject#1 tombait sur internal%51=29, un vrai
* slot RACH). La paging-response (RA=0x98) n'a JAMAIS touche un vrai slot RACH -> aucune
* 2e RACH-DET -> pas de CHAN RQD -> pas d'IMM ASS -> SMS jamais livre.
* FIX : evaluer l'eligibilite RACH sur internal_fn (== osmo-trx fn), set combination-V
* {4,5,14..36,45,46} = exactement osmo-trx Transceiver::expectedCorrType() case V.
* NB : le bloc SDCCH (ligne ~1059) garde son propre osmo_fn+eff_ofs (calibre
* independamment, SABM/UA OK) -> on NE touche QUE la gate RACH. */
uint32_t m51 = internal_fn % 51;
int fn_ok = !ul_fngate || (m51 == 4 || m51 == 5 || (m51 >= 14 && m51 <= 36) || m51 == 45 || m51 == 46);
/* === FN-LOCK (NO-HARDCODE, env CALYPSO_UL_FN_LOCK=1 ; OFF par defaut) =======
* Le mobile matche la request-reference de l'IMM ASSIGN sur (ra, T1/T2/T3) =
* FN mod 42432 (=32*26*51). Il a memorise (real_fn-1) [prim_rach.c:94] ; osmo-trx
* tamponne le burst injecte avec SA FN (= internal_fn + K_trx). Les 3 horloges
* sont rate-lockees 1:1 (offset constant verifie ~2016926). On auto-mesure UNE
* FOIS la congruence cible cal_off au 1er RACH (ZERO FN hardcode), puis on
* n'injecte que sur le slot ou (internal_fn+cal_off)%42432 == (real_fn-1)%42432.
* CALYPSO_UL_FN_ADJ = sweep +/- frames (le -1 prim_rach + SB2_LATENCY peut
* decaler de 1-2). Invisible tant que l'IMM ASSIGN AGCH n'atteint pas le mobile. */
static int ul_fnlock = -1, fn_adj = -99999;
if (ul_fnlock < 0) { const char *e = getenv("CALYPSO_UL_FN_LOCK"); ul_fnlock = (e && *e == '1') ? 1 : 0; }
if (fn_adj == -99999) { const char *e = getenv("CALYPSO_UL_FN_ADJ"); fn_adj = e ? atoi(e) : 0; }
int fnlock_ok = 1;
if (ul_fnlock) {
uint32_t real_fn = g_ul_real_fn;
static int cal_done = 0; static uint32_t cal_off = 0;
if (!cal_done && real_fn && g_ul_pending) {
cal_off = ((real_fn - 1u) - internal_fn) % 42432u; /* live, magic-free */
cal_done = 1;
LOGP(DDEV, LOGL_NOTICE, "UL FN-LOCK cal_off=%u (internal_fn=%u real_fn=%u)\n",
cal_off, internal_fn, real_fn);
}
if (cal_done && real_fn) {
int64_t w = ((int64_t)real_fn - 1 + fn_adj) % 42432; if (w < 0) w += 42432;
uint32_t have = (internal_fn + cal_off) % 42432u;
fnlock_ok = ((uint32_t)w == have);
} else {
fnlock_ok = 0; /* pas encore calibre -> attendre un RACH */
}
}
/* RACH a injecter : soit fraichement livre (g_ul_pending, comportement LU 30x),
* soit latche STICKY pour la paging-response (g_rach_pending, un seul encode).
* On prefere g_rach_iq (intact) a g_ul_iq (clobbe par le SDCCH-idle). */
int rach_inject = (g_ul_pending || g_rach_pending > 0);
if (g_ul_on && rach_inject && fn_ok && fnlock_ok && (d->rx_ts % ((uint64_t)CALYPSO_FRAME_SAMPLES)) == 0) {
memset(ul_chunk, 0, sizeof(ul_chunk));
int off = ul_slotoff < 0 ? 0 : ul_slotoff;
if (2 * off + (int)sizeof(g_ul_iq) > (int)sizeof(ul_chunk)) off = 0; /* borne */
/* g_rach_iq survit au clobber SDCCH-idle -> source preferentielle */
memcpy(ul_chunk + 2 * off, g_rach_iq, sizeof(g_rach_iq));
ul_src = ul_chunk;
g_ul_pending = 0;
if (g_rach_pending > 0) g_rach_pending--; /* consomme un slot eligible */
static unsigned ul_inj = 0;
if (ul_inj++ < 30 || (ul_inj % 100) == 0)
LOGP(DDEV, LOGL_NOTICE,
"UL inject #%u → internal_fn=%u osmo_fn=%u (%%51=%u) slotoff=%d ts=%llu rach_pend=%d seq=%u\n",
ul_inj, internal_fn, osmo_fn, m51, off, (unsigned long long)d->rx_ts,
g_rach_pending, g_rach_arm_seq);
}
/* === SDCCH/SACCH UL (#12 PIÈCE 2) : burst NORMAL encodé sur le slot dédié =======
* Le firmware met la L2 montante (SABM/SACCH/idle) dans a_cu -> sideband. On
* l'encode (gsm0503_xcch + TSC7) et on l'injecte sur le slot SDCCH/4 SS0 UL
* (osmo_fn%51 ∈ {37..40}, burst bid = osmo_fn%51-37). Priorité sur le relay
* (ul_src=ul_chunk -> le relay 5811 skip via `ul_src != ul_chunk`). N'écrase PAS
* le RACH (gate ul_src != ul_chunk). Tunables CALYPSO_UL_SDCCH(=1), _SDCCH_OFS. */
static int ul_sdcch = -1, sd_ofs = -99999;
if (ul_sdcch < 0) { const char *e = getenv("CALYPSO_UL_SDCCH"); ul_sdcch = (!e || *e != '0') ? 1 : 0; }
if (sd_ofs == -99999){ const char *e = getenv("CALYPSO_UL_SDCCH_OFS"); sd_ofs = e ? atoi(e) : 0; }
if (ul_sdcch &&
(d->rx_ts % ((uint64_t)CALYPSO_FRAME_SAMPLES)) == 0) {
/* POLL le sideband a CHAQUE frame (pas seulement aux slots inject). La SABM
* (ctrl 0x3f) est publiee a l1s%51={36-39} mais l'offset l1s<->osmo_fn faisait
* que les reads gates sur les slots SDCCH tombaient sur de l'idle -> SABM
* jamais vue. Poller chaque frame la capture des qu'elle est publiee -> cache
* sticky, tenu CALYPSO_UL_SABM_TTL blocs, prefere a l'idle au latch bid 0 ->
* elle part sur un bloc complet aligne -> osmo-bts Rx SABM -> UA.
* CALYPSO_UL_SABM_STICKY=0 desactive. */
static uint8_t pend_l2[23]; static int pend_valid = 0; /* trame UL en attente (1 bloc) */
static uint32_t last_seq = 0; static int sticky = -1;
static uint8_t l1s51 = 0xff;
static int sd_autoofs = -99999; /* offset auto-calibre l1s%51 -> osmo s51 */
if (sticky < 0) { const char *e = getenv("CALYPSO_UL_SABM_STICKY"); sticky = (!e || *e != '0') ? 1 : 0; }
{ uint8_t l2[23]; uint32_t lfn = 0, seq = 0;
if (calypso_sdcch_ul_read(l2, &l1s51, &lfn, &seq)) {
/* #2 UL DCCH, CONSUME-ONCE PAR SEQ : chaque transmission firmware = un seq
* nouveau. On capture la trame une fois par seq nouveau (SAPI0 signalisation
* OU SAPI3 SMS), injectee sur UN bloc SDCCH puis effacee (cf injection bid 0).
* Remplace le buffer sticky a TTL partage qui faisait SAPI3 ecraser SAPI0
* pendant le SMS -> lien principal down. Le firmware multiplexe deja SAPI0/
* SAPI3 par bloc ; un miss -> retransmission T200 (nouveau seq) -> recapture.
* Filtre : idle (UI 0x03) et SACCH SAPI1 (sapi=(a0>>2)&7) ecartes.
* NB : depuis le fix PUBLISH-NO-IDLE cote QEMU, l'idle n'est PLUS publie ->
* tout seq nouveau est porteur ; le filtre is_fill reste (defensif). */
int sapi = (l2[0] >> 2) & 0x07;
int is_fill = (l2[1] == 0x03);
if (sticky && seq != last_seq && (sapi == 0 || sapi == 3) && !is_fill) {
last_seq = seq;
memcpy(pend_l2, l2, sizeof(pend_l2)); pend_valid = 1;
/* #2 v3 ALIGNEMENT DETERMINISTE (2026-06-09) : l'ancien auto-calib
* (sd_autoofs = 37 - osmo_fn%51) derivait l'offset de l'INSTANT ou le
* firmware publie la SABM, dicte par l1s.current_time.fn -> seede par
* le SCH FN que gr-gsm decode au sync -> RUN-VARIANT. SUPPRIME.
* Avec osmo-trx START_FN=0 ET IPCDevice ts_initial snappe a 102*5000,
* osmo_trx_fn == internal_fn (mod 102) ; ul_fnoff=36 -> eff_ofs FIXE=15
* place bid0..3 sur osmo_trx_fn%51 {37,38,39,40} = SDCCH/4 SS0 UL.
* CALYPSO_UL_SDCCH_OFS surcharge pour sweeper si besoin. */
}
} }
int eff_ofs = (sd_ofs != 0) ? sd_ofs : 15;
uint32_t s51 = (uint32_t)((((long)osmo_fn + eff_ofs) % 51 + 51) % 51);
if (ul_src != ul_chunk && s51 >= 37 && s51 <= 40) { /* SDCCH/4 SS0 UL block */
int bid = (int)s51 - 37;
/* COHÉRENCE DE BLOC (#2 v2) : osmo-bts desentrelace les 4 bursts osmo%51
* {37,38,39,40} EN UN bloc L2 -> les 4 DOIVENT porter le MEME L2. On TIENT
* donc la derniere trame signalisante captee (held_l2) de facon persistante
* et on la snapshot -> blk_l2 UNIQUEMENT a bid 0, reutilisee pour bid 1..3.
* Plus de latch mid-bloc (qui rendait le bloc incoherent : bid0=idle +
* bid1-3=SABM -> CRC fail). held tient jusqu'a remplacement par une nouvelle
* capture OU expiration (CALYPSO_UL_SABM_HOLD_TTL blocs, def 30 ~7s ; rafraichi
* a chaque capture). La SABM etant retransmise ~1/s (T200) et injectee sur
* ~4 blocs/s, osmo-bts voit plusieurs blocs SABM COHERENTS -> decode -> UA.
* CALYPSO_UL_SABM_HOLD=0 = legacy (pas de hold persistant). */
static uint8_t held_l2[23]; static int held_valid = 0; static int held_ttl = 0;
static uint8_t blk_l2[23]; static int blk_valid = 0;
static int hold_on = -1, hold_ttl_max = -1;
if (hold_on < 0) { const char *e = getenv("CALYPSO_UL_SABM_HOLD"); hold_on = (!e || *e != '0') ? 1 : 0; }
if (hold_ttl_max < 0) { const char *e = getenv("CALYPSO_UL_SABM_HOLD_TTL"); hold_ttl_max = (e && *e) ? atoi(e) : 30; }
/* capture persistante : toute nouvelle trame signalisante remplace held_l2 */
if (sticky && pend_valid) {
memcpy(held_l2, pend_l2, sizeof(held_l2)); held_valid = 1;
held_ttl = hold_on ? hold_ttl_max : 1; pend_valid = 0;
}
if (bid == 0) { /* snapshot 1×/bloc -> 4 bursts coherents */
if (held_valid && held_ttl > 0) {
memcpy(blk_l2, held_l2, sizeof(blk_l2)); blk_valid = 1;
if (--held_ttl == 0) held_valid = 0;
} else {
blk_l2[0] = 0x01; blk_l2[1] = 0x03; blk_l2[2] = 0x01; /* idle UI SDCCH FIXE SAPI0 */
memset(blk_l2 + 3, 0x2b, sizeof(blk_l2) - 3); blk_valid = 1;
}
}
if (blk_valid) {
int8_t ab[CALYPSO_BSP_BURSTLEN];
ul_build_sdcch_burst(ab, blk_l2, bid);
/* === CHIFFREMENT A5 UL ============================================
* Si un Kc a ete capture (CIPHER MODE COMMAND -> /dev/shm/calypso_kc),
* on chiffre les 114 bits data du burst (mapping IDENTIQUE a osmo-bts
* scheduler.c:1614 : negation soft-bit ab[i+3] / ab[i+88]). osmo-bts
* dechiffre l'UL avec le FN du burst -> A5 est FN-keye : FN = osmo_fn
* (+ CALYPSO_CIPH_FN_ADJ, sweepable car l'alignement FN device<->bts
* est la variable critique). CALYPSO_CIPH_A5 force le n (debug). */
{
static int ci_init = -1, ci_fn_adj = 0, ci_force_n = -1;
static uint8_t kc[16]; static int n_a5 = 0;
if (ci_init < 0) {
const char *e = getenv("CALYPSO_CIPH_FN_ADJ"); ci_fn_adj = e ? atoi(e) : 0;
const char *f = getenv("CALYPSO_CIPH_A5"); ci_force_n = (f && *f) ? atoi(f) : -1;
ci_init = 1;
}
uint8_t cgalgo = 0, cklen = 0;
if (calypso_kc_read(&cgalgo, kc, &cklen))
n_a5 = (ci_force_n >= 0) ? ci_force_n
: ((cgalgo >= 1 && cgalgo <= 3) ? cgalgo : 0);
else
n_a5 = 0; /* seq=0 -> cipher off */
if (n_a5 >= 1 && n_a5 <= 3) {
ubit_t ks[114];
/* FN du keystream = internal_fn (= horloge osmo-trx/osmo-bts,
* PROUVE par la RACH-DET a osmo-trx fn==internal_fn), PAS osmo_fn
* (=internal_fn+36, label legacy trompeur). osmo-bts dechiffre le
* burst UL recu avec SON FN = internal_fn ; on doit chiffrer au
* meme FN sinon keystream different (A5 FN-keye) -> CIPHER MODE
* COMPLETE illisible -> pas de LU ACCEPT. CALYPSO_CIPH_FN_ADJ=0. */
uint32_t fnc = (uint32_t)((long)internal_fn + ci_fn_adj);
osmo_a5(n_a5, kc, fnc, NULL, ks); /* keystream UL */
for (int i = 0; i < 57; i++) {
if (ks[i]) ab[i + 3] = (int8_t)-ab[i + 3];
if (ks[i + 57]) ab[i + 88] = (int8_t)-ab[i + 88];
}
static unsigned nci = 0;
if (nci++ < 30 || (nci % 100) == 0)
LOGP(DDEV, LOGL_NOTICE,
"UL CIPHER A5/%d bid=%d fn=%u (adj=%d) Kc=%02x%02x%02x%02x..\n",
n_a5, bid, fnc, ci_fn_adj, kc[0], kc[1], kc[2], kc[3]);
}
}
ul_mod_laurent(ab, CALYPSO_BSP_BURSTLEN, g_ul_iq); /* modulateur EXACT osmo-trx */
ul_iq_record(g_ul_iq, CALYPSO_BSP_BURSTLEN * CALYPSO_TRX_OSR); /* record I/Q UL (SDCCH/SACCH) */
memset(ul_chunk, 0, sizeof(ul_chunk));
/* offset ÉCHANTILLON dédié au burst NORMAL (≠ access-burst RACH) : le
* détecteur normal-burst d'osmo-trx corrèle le TSC à une position
* différente du détecteur RACH -> au même offset que le RACH, le TSC
* tombe hors fenêtre. CALYPSO_UL_SDCCH_SMP_OFS=N (échantillons, sweepable,
* peut être négatif) décale le burst normal pour caler le TSC. */
static int sd_smp = -999999;
if (sd_smp == -999999) { const char *e = getenv("CALYPSO_UL_SDCCH_SMP_OFS"); sd_smp = e ? atoi(e) : 0; }
int off = (ul_slotoff < 0 ? 0 : ul_slotoff) + sd_smp;
if (off < 0) off = 0;
if (2 * off + (int)sizeof(g_ul_iq) > (int)sizeof(ul_chunk)) off = 0;
memcpy(ul_chunk + 2 * off, g_ul_iq, sizeof(g_ul_iq));
ul_src = ul_chunk;
static unsigned sd_inj = 0;
int is_idle_inj = (blk_l2[0] == 0x01 && blk_l2[1] == 0x03 && blk_l2[2] == 0x01);
if (sd_inj++ < 40 || !is_idle_inj || (sd_inj % 200) == 0)
LOGP(DDEV, LOGL_NOTICE,
"UL SDCCH inject #%u%s bid=%d osmo%%51=%u l1s%%51=%u eff_ofs=%d L2=%02x %02x %02x\n",
sd_inj, is_idle_inj ? "" : " *SABM/SIG*", bid, s51, l1s51,
eff_ofs, blk_l2[0], blk_l2[1], blk_l2[2]);
}
}
}
/* ---- WALL-PACED UL heartbeat (clock_nanosleep ABSTIME) ----
*
* Avant : `usleep(2300)` → wall-paced ~2.3ms mais usleep délivre
* 2.4ms en moyenne sous charge → osmo-trx ts advance ~4% slow →
* BTS reçoit CLK_IND à 208 FN/sec wall (drift -4.2%).
*
* Première tentative : qfn-paced spin-wait (sync sur QEMU FN ticks).
* Échec : le spin time-out de 10ms quand QEMU lag → osmo-trx-ipc
* starve → IPC socket disconnect → crash (vérifié dans run +94s).
*
* Cette version : clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME)
* sur deadline absolue. Précision sub-µs, pas de spin, pas de starve.
* Même horloge que le clk_master_thread côté QEMU (calypso_trx.c)
* → les deux pacing restent alignés tant que le host kernel est
* stable (= toujours, sauf charge extrême). */
static struct timespec next_deadline = { .tv_sec = 0, .tv_nsec = 0 };
/* 625 samples / 270833 sps = 2307.692 µs exact = 2307692 ns (= WALL_TDMA_NS/2,
* le heartbeat est une demi-frame). Configurable via CALYPSO_TDMA_NS (la
* MÊME var que le clk_master QEMU) pour ralentir la timeline uniformément
* si l'émulation ne tient pas le temps réel → cohérence osmo-trx ↔ QEMU. */
static long PERIOD_NS = 0, QFN_LEAD = 0, QFN_FLOOR_NS = 0;
static int QFN_FORCE = -1;
static uint64_t local_half = 0;
if (PERIOD_NS == 0) {
PERIOD_NS = CALYPSO_FRAME_NS / 2; /* demi-frame, budget firmware 4908 qbits */
const char *e = getenv("CALYPSO_TDMA_NS");
if (e && *e) { long long v = atoll(e); if (v >= CALYPSO_FRAME_NS) PERIOD_NS = (long)(v / 2); }
const char *f = getenv("CALYPSO_QFN_FORCE"); QFN_FORCE = (f && *f == '1') ? 1 : 0;
const char *l = getenv("CALYPSO_QFN_LEAD"); QFN_LEAD = (l && *l) ? atol(l) : 32;
const char *g = getenv("CALYPSO_QFN_FLOOR_NS"); QFN_FLOOR_NS = (g && *g) ? atol(g) : 50000000L;
}
/* ---- LOCK SUR L'HORLOGE QEMU (CALYPSO_QFN_FORCE=1) : budget constant ----
* Le device se cale sur le qfn de qemu (g_qemu_qfn, clk_listener port 6700).
* osmo-trx (master clock = nos UL) ET le relay->gr-gsm verrouillent sur le
* firmware. Budget = 148 cplx/frame (DARAM 0x2a00) ; heartbeat = demi-frame
* -> 2/qfn. local_half <= qfn*2 + QFN_LEAD (sinon attend qemu, poll-sleep).
* QFN_FLOOR_NS = anti-starve (pas de hard-timeout qui crashait). Defaut
* (QFN_FORCE=0) = wall historique. */
if (QFN_FORCE && __atomic_load_n(&g_qfn_seen, __ATOMIC_ACQUIRE)) {
struct timespec t0; clock_gettime(CLOCK_MONOTONIC, &t0);
for (;;) {
uint32_t qfn = __atomic_load_n(&g_qemu_qfn, __ATOMIC_ACQUIRE);
if (local_half <= (uint64_t)qfn * 2 + (uint64_t)QFN_LEAD) break;
struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now);
long el = (now.tv_sec - t0.tv_sec) * 1000000000L + (now.tv_nsec - t0.tv_nsec);
if (el >= QFN_FLOOR_NS) break;
usleep(100);
}
local_half++;
} else {
if (next_deadline.tv_sec == 0) {
clock_gettime(CLOCK_MONOTONIC, &next_deadline);
}
next_deadline.tv_nsec += PERIOD_NS;
while (next_deadline.tv_nsec >= 1000000000L) {
next_deadline.tv_nsec -= 1000000000L;
next_deadline.tv_sec += 1;
}
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_deadline, NULL);
}
/* RELAIS UL : I/Q fc32 du transceiver gr-gsm → cs16 → osmo-trx. Si rien
* ce tick, zéros (le clock doit avancer). Buffer local (thread DL séparé). */
relay_init();
int16_t relay_ul[CALYPSO_SHM_BUFSIZE * 2];
/* FIX LU 2026-06-05 : PRIORITE ABSOLUE a la RACH injectee. Si ul_src==ul_chunk
* (IPC_UL a injecte une RACH ce tick), on SAUTE entierement le bloc relais —
* sinon recvfrom(5811) (le flowgraph gr-gsm emet sur 5811 en full-grgsm) renvoie
* n>0 et `ul_src = relay_ul` ECRASAIT la RACH -> enqueue de zeros -> NOPE/-110.
* (l'ancien garde ne protegeait que n<=0, pas n>0.) */
if (g_relay_on && g_relay_ul_fd >= 0 && ul_src != ul_chunk) {
float ulf[CALYPSO_SHM_BUFSIZE * 2];
ssize_t n = recvfrom(g_relay_ul_fd, ulf, sizeof(ulf), MSG_DONTWAIT, NULL, NULL);
if (n > 0) {
/* Le relais (transceiver gr-gsm, 5811) a des donnees -> prioritaire. */
memset(relay_ul, 0, sizeof(relay_ul));
int ns = (int)(n / (2 * sizeof(float)));
if (ns > CALYPSO_SHM_BUFSIZE) ns = CALYPSO_SHM_BUFSIZE;
for (int i = 0; i < ns * 2; i++) {
float v = ulf[i] * 32768.0f;
if (v > 32767.0f) v = 32767.0f; else if (v < -32768.0f) v = -32768.0f;
relay_ul[i] = (int16_t)v;
}
ul_src = relay_ul;
} else if (ul_src != ul_chunk) {
/* Pas de RACH injectee par IPC_UL ce tick -> zeros (l'horloge avance). */
memset(relay_ul, 0, sizeof(relay_ul));
ul_src = relay_ul;
}
/* FIX LU 2026-06-04 : sinon (relais vide MAIS IPC_UL a injecte une RACH)
* on GARDE ul_chunk. Avant, ce bloc ecrasait INCONDITIONNELLEMENT ul_src
* par relay_ul (vide en full-grgsm : transceiver 5811 absent) -> la RACH
* du mobile (BSP -> ul_gmsk_mod -> ul_chunk) etait jetee -> osmo-trx
* envoyait des NOPE -> BTS jamais de CHAN RQD -> Location Update echouait. */
}
/* INSTR : a l'enqueue, quand le burst RACH est dans ul_src (==ul_chunk),
* prouver qu'il porte de l'energie ET qu'il part vers un stream RX valide. */
{
static int eqdbg = -1; if (eqdbg < 0) { const char *e = getenv("CALYPSO_UL_DEBUG"); eqdbg = (!e || *e != '0'); }
if (eqdbg && ul_src == ul_chunk) {
static unsigned eqn = 0;
int nz = 0; for (int i = 0; i < CALYPSO_SHM_BUFSIZE * 2; i++) if (ul_src[i]) nz++;
if (eqn++ < 40)
LOGP(DDEV, LOGL_NOTICE,
"ENQ-RACH #%u ts=%llu num_chans=%u rx0=%p nz=%d s0=[%d,%d]\n",
eqn, (unsigned long long)d->rx_ts, num_chans,
(void *)ios_rx_from_device[0], nz, ul_src[0], ul_src[1]);
}
}
for (uint32_t c = 0; c < num_chans && c < 8; c++) {
if (!ios_rx_from_device[c]) continue;
int32_t rc = ipc_shm_enqueue(ios_rx_from_device[c],
d->rx_ts,
CALYPSO_SHM_BUFSIZE,
(uint16_t *)ul_src);
if (rc < 0) {
static unsigned overruns = 0;
if (overruns++ < 5)
LOGP(DDEV, LOGL_NOTICE,
"ul_stream enqueue rc=%d chan=%u ts=%llu\n",
rc, c, (unsigned long long)d->rx_ts);
}
}
d->rx_ts += CALYPSO_SHM_BUFSIZE;
return CALYPSO_SHM_BUFSIZE;
}
/* —- TX (downlink_thread loop) : consumes DL bursts from osmo-trx —-
* POL = drain silently. Phase 1.5 will sendto() to UDP 127.0.0.1:6702.
/ / DL read buffer : osmo-trx commits CHUNKtx_sps = 625
samples per write at 1 SPS. We read up to that. The first
CALYPSO_BSP_BURSTLEN samples = TS=0 * burst, forwarded to BSP. Rest is
discarded for FBSB phase. / #define DL_READ_SAMPLES
CALYPSO_SHM_BUFSIZE static uint16_t dl_read_buf[DL_READ_SAMPLES * 2];
/ cs16 I,Q interleaved */ static uint8_t dl_send_pkt[TRXD_HDR_LEN +
CALYPSO_DL_BURSTLEN * 4];
int32_t uhdwrap_write(void dev, uint32_t num_chans, bool
underrun) { struct qemu_dev d = dev; if (!d || !underrun)
return -1; underrun = false; bool any = false;
if (g_bsp_fd < 0) bsp_udp_init();
for (uint32_t c = 0; c < num_chans && c < 8; c++) {
if (!ios_tx_to_device[c]) continue;
uint64_t ts = 0;
/* timeout_seconds = 0 → wait briefly (cond_timedwait clamps to wall now);
* we don't want the downlink thread to spin if osmo-trx has no DL ready. */
int32_t rv = ipc_shm_read(ios_tx_to_device[c], dl_read_buf,
DL_READ_SAMPLES, &ts, 0);
if (rv <= 0) {
*underrun = true;
continue;
}
any = true;
/* RELAIS : I/Q continu (TOUS les samples du chunk, tous TS) → fc32 →
* UDP vers le transceiver gr-gsm. gsm.receiver trouve lui-même le bon
* timeslot/timing. On NE fait PAS l'extraction per-burst TRXDv0. */
relay_init();
if (g_relay_on) {
int ns = (rv < DL_READ_SAMPLES) ? rv : DL_READ_SAMPLES;
for (int i = 0; i < ns * 2; i++)
g_relay_fbuf[i] = (float)((int16_t)dl_read_buf[i]) / 32768.0f;
if (g_relay_dl_fd >= 0)
sendto(g_relay_dl_fd, g_relay_fbuf, (size_t)ns * 2 * sizeof(float),
MSG_DONTWAIT, (struct sockaddr *)&g_relay_dl_dst,
sizeof(g_relay_dl_dst));
/* FIFOs LIVE frame-par-frame -> writer-thread FRAME-ATOMIQUE
* (cf relay_fifo_push / relay_fifo_writer ci-dessus). Plus de write
* partiel ni de desalignement byte -> grgsm garde la 51-multitrame
* -> SDCCH/4 SACCH (SI5/SI6) decodee. Drop = TRAME entiere si ring
* plein (continuite byte preservee). CALYPSO_RELAY_FIFOS (':'-sep).*/
relay_fifo_push(g_relay_fbuf, (size_t)ns * 2);
/* RANK2 : forward I/Q continu -> BSP UDP:6702 (TRXDv0 passthrough),
* meme cs16 dense que la FIFO gr-gsm. Gate CALYPSO_BSP_CONT_FORWARD
* DEFAUT OFF (inerte). Requiert BSP_IQ_PASSTHROUGH=1 ; mettre
* RELAY_ALSO_BSP=0 pour ne pas doubler avec le ring TS0. */
{
static int cont_fwd = -1;
if (cont_fwd < 0) { const char *e = getenv("CALYPSO_BSP_CONT_FORWARD");
cont_fwd = (e && *e == '1') ? 1 : 0; } /* DEFAUT OFF */
if (cont_fwd && g_bsp_fd >= 0) {
int nc = (ns < CALYPSO_DL_BURSTLEN) ? ns : CALYPSO_DL_BURSTLEN;
uint32_t bfn = (uint32_t)(ts / ((uint64_t)CALYPSO_FRAME_SAMPLES));
static uint8_t cbsp_pkt[TRXD_HDR_LEN + CALYPSO_DL_BURSTLEN * 4];
cbsp_pkt[0] = 0;
cbsp_pkt[1] = (uint8_t)(bfn >> 24);
cbsp_pkt[2] = (uint8_t)(bfn >> 16);
cbsp_pkt[3] = (uint8_t)(bfn >> 8);
cbsp_pkt[4] = (uint8_t)(bfn);
cbsp_pkt[5] = 0;
cbsp_pkt[6] = 0; cbsp_pkt[7] = 0;
memcpy(cbsp_pkt + TRXD_HDR_LEN, dl_read_buf, (size_t)nc * 4u);
sendto(g_bsp_fd, cbsp_pkt, TRXD_HDR_LEN + (size_t)nc * 4u,
MSG_DONTWAIT, (struct sockaddr *)&g_bsp_peer,
sizeof(g_bsp_peer));
}
}
/* RELAY+BSP (#3 cfile) : si CALYPSO_RELAY_ALSO_BSP=1, on NE
* `continue` PAS — on tombe dans l'extraction TS0→TRXDv0→BSP pour
* alimenter feed_iq (cfile + shm ring grgsm↔BSP). Defaut: relais pur. */
static int also_bsp = -1;
if (also_bsp < 0) { const char *e = getenv("CALYPSO_RELAY_ALSO_BSP");
also_bsp = (e && *e=='1') ? 1 : 0; }
if (!also_bsp) continue; /* relais pur */
}
/* TS=0 slice : SAMPLES_PER_FRAME=1250 at 1 SPS = 8 × 156.25.
* osmo-trx commits half-frames (625 samples) → chunks pair at
* ts%1250==0 carry TS0..3, chunks impair (ts%1250==625) carry
* TS4..7. We only forward TS=0 (first 148 of pair chunks). */
uint32_t ts_in_frame = (uint32_t)(ts % ((uint64_t)CALYPSO_FRAME_SAMPLES));
int has_ts0 = (ts_in_frame == 0);
if (!has_ts0) {
static uint64_t skip_count = 0;
if (skip_count < 5 || (skip_count % 5000) == 0) {
LOGP(DDEV, LOGL_INFO,
"skip non-TS0 chunk #%llu ts=%llu ts_in_frame=%u\n",
(unsigned long long)skip_count, (unsigned long long)ts,
ts_in_frame);
}
skip_count++;
continue;
}
/* Offset d'extraction du burst dans le chunk de 625 samples : le burst
* actif TS0 n'est pas forcément à l'offset 0 du slot (156.25 samples).
* Le démod gr-gsm a montré un décalage (TSC@62 au lieu de @61) → un
* mauvais offset désaligne le FCCH/midambule pour le corrélateur FB-det
* du DSP (d_fb_det reste 0 sur de vrais samples). Réglable via
* CALYPSO_DL_BURST_OFFSET (samples, défaut 0) pour sweeper l'alignement. */
static int burst_off = -1;
static int iq_conj = -1;
if (burst_off < 0) {
const char *e = getenv("CALYPSO_DL_BURST_OFFSET");
burst_off = (e && *e) ? atoi(e) : 0;
if (burst_off < 0) burst_off = 0;
const char *c = getenv("CALYPSO_DL_IQ_CONJ");
iq_conj = (c && *c == '1') ? 1 : 0;
LOGP(DDEV, LOGL_NOTICE,
"DL burst extraction offset = %d samples, iq_conj = %d "
"(CALYPSO_DL_BURST_OFFSET / CALYPSO_DL_IQ_CONJ)\n",
burst_off, iq_conj);
}
int avail = (int)rv - burst_off;
int n_samples = (avail < CALYPSO_DL_BURSTLEN) ? avail : CALYPSO_DL_BURSTLEN;
if (n_samples < 0) n_samples = 0;
const int16_t *burst_src = (const int16_t *)dl_read_buf + 2 * burst_off;
size_t payload_len = (size_t)n_samples * 4u;
uint32_t internal_fn = (uint32_t)(ts / ((uint64_t)CALYPSO_FRAME_SAMPLES));
/* Detect FCCH inline — purely for diag log (helps spot when
* we serve an FCCH vs other bursts). Not used for routing. */
bool is_fcch = is_fcch_burst_iq(burst_src, n_samples);
/* Push TS=0 burst to FIFO tail. clk_listener will pop it and
* tag with qfn when QEMU is ready. */
pthread_mutex_lock(&g_dl_fifo_mutex);
size_t tail = g_dl_fifo_tail;
size_t depth = tail - g_dl_fifo_head;
if (depth >= DL_FIFO_SIZE - 1) {
/* FIFO full — drop oldest by advancing head. Backpressure
* preferable to OOM. In steady state this shouldn't fire :
* device reads ~209 burst/s, QEMU consumes ~10 fn/s, but
* we only read+push when ipc_shm_read returns data, which
* itself is paced by the consumer. */
g_dl_fifo_head++;
static uint64_t drop_count = 0;
if (drop_count++ < 5)
LOGP(DDEV, LOGL_NOTICE,
"DL FIFO full (size=%d), dropping oldest. #%llu\n",
DL_FIFO_SIZE, (unsigned long long)drop_count);
}
struct dl_fifo_entry *fe = &g_dl_fifo[tail % DL_FIFO_SIZE];
fe->is_fcch = is_fcch;
fe->ts = ts;
/* Header placeholder (fn rewritten at send time in clk_listener). */
fe->pkt[0] = 0;
fe->pkt[1] = 0; fe->pkt[2] = 0; fe->pkt[3] = 0; fe->pkt[4] = 0;
fe->pkt[5] = 0; fe->pkt[6] = 0; fe->pkt[7] = 0;
memcpy(fe->pkt + TRXD_HDR_LEN, burst_src, payload_len);
if (iq_conj) {
/* Conjugaison I/Q (-Q) : le démod gr-gsm a montré rot=-1 (tone FCCH
* de signe opposé à la réf du corrélateur DSP). Flip le signe de Q
* remet le tone à la bonne fréquence pour le FB-det. */
int16_t *p = (int16_t *)(fe->pkt + TRXD_HDR_LEN);
for (int k = 0; k < n_samples; k++)
p[2 * k + 1] = (int16_t)(-p[2 * k + 1]);
}
g_dl_fifo_tail = tail + 1;
size_t new_depth = g_dl_fifo_tail - g_dl_fifo_head;
pthread_mutex_unlock(&g_dl_fifo_mutex);
/* ---- α : sweep 51 raw chunks (offset-agnostic FCCH search) ----
* Capture N=51 consecutive RAW chunks (pre-slice) into per-chunk
* files indexed by internal_fn = ts / SAMPLES_PER_FRAME. The
* analyzer (tools/fcch_sweep.py) computes dphi_std per chunk and
* sorts ascending : FCCH bursts (tone @ +π/2) have std≈0 and float
* to the top. The internal_fn % 51 of these top hits gives X =
* on-air ↔ internal frame offset (used by Phase 1.5 slot rewrite).
*
* Skip the first SKIP chunks to let osmo-bts-trx exit POWERUP
* fillers and start real DL. Default CALYPSO_FCCH_DUMP_SKIP=2000
* ≈ 5 s of TS0 chunks at wall pace.
*
* One meta file with the full index for fast lookup. */
if (getenv("CALYPSO_FCCH_DUMP") && getenv("CALYPSO_FCCH_DUMP")[0] == '1') {
static int dump_skipped = 0;
static int dump_count = 0;
static int dump_done = 0;
static FILE *idx_file = NULL;
static int skip_target = -1;
static int capture_target = -1;
if (skip_target < 0) {
const char *s = getenv("CALYPSO_FCCH_DUMP_SKIP");
skip_target = (s && *s) ? atoi(s) : 2000;
const char *c = getenv("CALYPSO_FCCH_DUMP_N");
capture_target = (c && *c) ? atoi(c) : 51;
}
if (!dump_done) {
if (dump_skipped < skip_target) {
dump_skipped++;
} else {
if (!idx_file) {
idx_file = fopen("/tmp/fcch_sweep_index.txt", "w");
if (idx_file) {
fprintf(idx_file,
"# alpha sweep : %d raw chunks (pre-slice, 625 cs16 samples each)\n"
"# fields: idx ts internal_fn internal_fn_mod51 ts_in_frame qfn_tagged\n",
capture_target);
}
LOGP(DDEV, LOGL_NOTICE,
"alpha sweep START (skipped %d, will capture %d chunks)\n",
dump_skipped, capture_target);
}
uint64_t internal_fn = ts / ((uint64_t)CALYPSO_FRAME_SAMPLES);
char path[128];
snprintf(path, sizeof(path),
"/tmp/fcch_sweep_%03d.bin", dump_count);
FILE *f = fopen(path, "wb");
if (f) {
/* Raw chunk : 2 * rv cs16 samples = up to 1250 uint16 */
fwrite(dl_read_buf, sizeof(int16_t), 2 * rv, f);
fclose(f);
}
if (idx_file) {
uint32_t qfn_now = __atomic_load_n(&g_qemu_qfn, __ATOMIC_ACQUIRE);
fprintf(idx_file, "%03d %llu %llu %llu %u %u\n",
dump_count,
(unsigned long long)ts,
(unsigned long long)internal_fn,
(unsigned long long)(internal_fn % 51),
ts_in_frame, qfn_now);
fflush(idx_file);
}
dump_count++;
if (dump_count >= capture_target) {
if (idx_file) { fclose(idx_file); idx_file = NULL; }
dump_done = 1;
LOGP(DDEV, LOGL_NOTICE,
"alpha sweep DONE : %d raw chunks in /tmp/fcch_sweep_*.bin "
"+ index /tmp/fcch_sweep_index.txt\n",
dump_count);
}
}
}
}
/* Fix D : NO direct sendto here — clk_listener dispatches one
* burst per qfn tick from the ring above. Direct send would
* re-introduce the 209/s wall-paced flood that drowned QEMU. */
static uint64_t dl_count = 0;
if (dl_count < 5 || (dl_count % 1000) == 0) {
LOGP(DDEV, LOGL_INFO,
"DL-push #%llu chan=%u int_fn=%u%s fifo_depth=%zu rv=%d\n",
(unsigned long long)dl_count, c, internal_fn,
is_fcch ? " *FCCH*" : "", new_depth, rv);
}
dl_count++;
}
/* If no DL was ready on any chan, brief sleep to avoid hot-spin. */
if (!any) usleep(READ_PACE_US);
return 0;
}
================================================================================
FILE: tools/calypso-ipc-device/shm.c SIZE: 6044 bytes, 149 lines
================================================================================
/ Copyright 2020 sysmocom - s.f.m.c. GmbH info@sysmocom.de *
Author: Pau Espin Pedrol pespin@sysmocom.de SPDX-License-Identifier:
0BSD Permission to use, copy, modify, and/or distribute this
software for any purpose * with or without fee is hereby granted.THE
SOFTWARE IS PROVIDED “AS IS” AND THE * AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL * IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR * BE LIABLE
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF * CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE * USE OR PERFORMANCE OF THIS
SOFTWARE. */
#include <stdint.h> #include <stddef.h> #include
<osmocom/core/talloc.h>
#include “shm.h”
#define ENCDECDEBUG(…) //fprintf(stderr,
VA_ARGS)
/* Convert offsets to pointers / struct ipc_shm_stream
ipc_shm_decode_stream(void tall_ctx, struct ipc_shm_raw_region
root_raw, struct ipc_shm_raw_stream stream_raw) { unsigned int
i; struct ipc_shm_stream stream; stream = talloc_zero(tall_ctx,
struct ipc_shm_stream); stream = talloc_zero_size(tall_ctx,
sizeof(struct ipc_shm_stream) + sizeof(struct ipc_shm_raw_smpl_buf )
stream_raw->num_buffers); if (!stream) return NULL;
stream->num_buffers = stream_raw->num_buffers;
stream->buffer_size = stream_raw->buffer_size; stream->raw =
stream_raw; for (i = 0; i < stream->num_buffers; i++) {
ENCDECDEBUG(“decode: smpl_buf %d at offset %u”, i,
stream_raw->buffer_offset[i]); stream->buffers[i] = (struct
ipc_shm_raw_smpl_buf )(((uint8_t )root_raw) +
stream_raw->buffer_offset[i]); } return stream; }
struct ipc_shm_channel ipc_shm_decode_channel(void tall_ctx,
struct ipc_shm_raw_region root_raw, struct ipc_shm_raw_channel
chan_raw) { struct ipc_shm_channel chan; chan =
talloc_zero(tall_ctx, struct ipc_shm_channel); if (!chan) return NULL;
ENCDECDEBUG(“decode: streams at offset %u and %u”,
chan_raw->dl_buf_offset, chan_raw->ul_buf_offset);
chan->dl_stream = ipc_shm_decode_stream( chan, root_raw, (struct
ipc_shm_raw_stream )(((uint8_t )root_raw) +
chan_raw->dl_buf_offset)); chan->ul_stream =
ipc_shm_decode_stream( chan, root_raw, (struct ipc_shm_raw_stream
)(((uint8_t )root_raw) + chan_raw->ul_buf_offset)); return
chan; } struct ipc_shm_region ipc_shm_decode_region(void
tall_ctx, struct ipc_shm_raw_region root_raw) { unsigned int i;
struct ipc_shm_region root; root = talloc_zero_size(tall_ctx,
sizeof(struct ipc_shm_region) + sizeof(struct ipc_shm_channel ) *
root_raw->num_chans); if (!root) return NULL;
root->num_chans = root_raw->num_chans;
for (i = 0; i < root->num_chans; i++) {
ENCDECDEBUG("decode: channel %d at offset %u\n", i, root_raw->chan_offset[i]);
root->channels[i] = ipc_shm_decode_channel(
root, root_raw,
(struct ipc_shm_raw_channel *)(((uint8_t *)root_raw) + root_raw->chan_offset[i]));
}
return root;
}
unsigned int ipc_shm_encode_smpl_buf(struct ipc_shm_raw_region
root_raw, struct ipc_shm_raw_smpl_buf smpl_buf_raw, uint32_t
buffer_size) { unsigned int offset = sizeof(struct
ipc_shm_raw_smpl_buf); offset = (((uintptr_t)offset + 7) &
~0x07ULL); ENCDECDEBUG(“encode: smpl_buf at offset %u”, offset); offset
+= buffer_size * sizeof(uint16_t) * 2; /* samples */ return offset;
}
unsigned int ipc_shm_encode_stream(struct ipc_shm_raw_region
root_raw, struct ipc_shm_raw_stream stream_raw, uint32_t
num_buffers, uint32_t buffer_size) { unsigned int i; ptrdiff_t start =
(ptrdiff_t)stream_raw; unsigned int offset = sizeof(struct
ipc_shm_raw_stream) + sizeof(uint32_t) * num_buffers; offset =
(((uintptr_t)offset + 7) & ~0x07ULL); ENCDECDEBUG(“encode: stream at
offset %lu”, (start - (ptrdiff_t)root_raw)); if (root_raw) {
stream_raw->num_buffers = num_buffers; stream_raw->buffer_size =
buffer_size; stream_raw->read_next = 0; stream_raw->write_next =
0; } for (i = 0; i < num_buffers; i++) { if (root_raw)
stream_raw->buffer_offset[i] = (start + offset -
(ptrdiff_t)root_raw); offset += ipc_shm_encode_smpl_buf(root_raw,
(struct ipc_shm_raw_smpl_buf )(start + offset), buffer_size); }
return offset; } unsigned int ipc_shm_encode_channel(struct
ipc_shm_raw_region root_raw, struct ipc_shm_raw_channel
chan_raw, uint32_t num_buffers, uint32_t buffer_size) { uint8_t
start = (uint8_t )chan_raw; unsigned int offset = sizeof(struct
ipc_shm_raw_channel); offset = (((uintptr_t)offset + 7) & ~0x07ULL);
ENCDECDEBUG(“encode: channel at offset %lu”, (start - (uint8_t
)root_raw)); if (root_raw) chan_raw->dl_buf_offset = (start +
offset - (uint8_t )root_raw); offset +=
ipc_shm_encode_stream(root_raw, (struct ipc_shm_raw_stream )(start
+ offset), num_buffers, buffer_size); if (root_raw)
chan_raw->ul_buf_offset = (start + offset - (uint8_t )root_raw);
offset += ipc_shm_encode_stream(root_raw, (struct ipc_shm_raw_stream
)(start + offset), num_buffers, buffer_size); return offset; } /*
if root_raw is NULL, then do a dry run, aka only calculate final offset
/ unsigned int ipc_shm_encode_region(struct ipc_shm_raw_region
root_raw, uint32_t num_chans, uint32_t num_buffers, uint32_t
buffer_size) { unsigned i; uintptr_t start = (uintptr_t)root_raw;
unsigned int offset = sizeof(struct ipc_shm_raw_region) +
sizeof(uint32_t) * num_chans; offset = (((uintptr_t)offset + 7) &
~0x07ULL);
if (root_raw)
root_raw->num_chans = num_chans;
for (i = 0; i < num_chans; i++) {
if (root_raw)
root_raw->chan_offset[i] = (start + offset - (uintptr_t)root_raw);
ENCDECDEBUG("encode: channel %d chan_offset[i]=%lu\n", i, start + offset - (uintptr_t)root_raw);
offset += ipc_shm_encode_channel(root_raw, (struct ipc_shm_raw_channel *)(start + offset), num_buffers,
buffer_size);
}
//TODO: pass maximum size and verify we didn't go through
return offset;
}
Comment lire les logs : direct (si on tourne dans le container) ou via docker exec (si host)