Questo è l’articolo su cui tutto il tracking generator poggia. Nei pezzi precedenti vi ho mostrato la mappa delle porte e la HAL; qui arrivo al punto: come si dice davvero al CMU200 “genera a questa frequenza, a questo livello, e accendi l’uscita”. La funzione che controlla il sintetizzatore, la C331_C_CTRL, era il mio blocco duro: in rf.dl2 compariva solo come stub EXTERNAL, e una ricerca su tutti e 33 i binari importati non trovava nessuna implementazione. Era il classico muro contro cui si sbatte per giorni.
Come l’ho trovata
Non l’ho trovata cercando la funzione, l’ho trovata cercando le dipendenze. Ho parsato la import table di rf.dl2 (PowerShell, formato PE) e ne è uscita la lista delle DLL da cui dipende:
DLL: audio.dl2 (thunk RVA 0x0005EAAC)
DLL: cmu.dl1 (thunk RVA 0x0005EAC0)
DLL: cmudrv.dl0 (thunk RVA 0x0005EC94) ← IL DRIVER
DLL: base.dll (thunk RVA 0x0005F008)
DLL: base.exe (thunk RVA 0x0005F1B4)
Filtrando i nomi importati per C331|RFL|AUXTX|set_rf è emerso che era cmudrv.dl0 a fornire tutta l’astrazione hardware, set_rf_frequency e set_rf_level comprese. Il file è baseV5.22\extracted\BASE\fsw\1\cmudrv.dl0, 2.400.256 byte, con 2.347 export nominati: la superficie API più ampia che abbia incontrato. Image base 0x00400000, la stessa di rf.dl2. La patch per Ghidra è il solito rituale: MZ ripristinata a offset 0 e PE\0\0 scritto a 0x80, dove il Phar Lap tiene la firma PL.
I setter di frequenza e livello
Le due funzioni che mi interessano, con RVA e VA (VA = RVA + 0x00400000):
| Metodo | RVA | VA |
|---|---|---|
set_rf_frequency | 0x0002E482 | 0x0042E482 |
set_rf_level | 0x0002E5CF | 0x0042E5CF |
set_active_channel | 0x0002E338 | 0x0042E338 |
set_operation_mode | 0x0002EF02 | 0x0042EF02 |
set_commit_mode | 0x0002D71C | 0x0042D71C |
transfer_channels_to_cor | 0x00036526 | 0x00436526 |
transfer_par_to_cor | 0x00036641 | 0x00436641 |
La catena completa del generatore, ora chiusa, parte da RFGT_C_GT::adjust_c331 @ 0x0043CB6B in rf.dl2 e arriva a questi setter in cmudrv.dl0. La adjust_c331 fa da trampolino: legge livello e frequenza, seleziona il canale, chiama set_rf_frequency, set_rf_level, set_operation_mode, set_active_channel, e chiude con RFL_C_BASE::adjust_hardware_now.
Un’omissione che mi è costata cara e che vi segnalo subito: di adjust_c331 esistono due funzioni, con lo stesso identico nome e in due binari diversi. Quella qui sopra, in rf.dl2, è la catena del generatore TX. In cmu.dl1 c’è NPM_C_TASK::adjust_c331 @ 0x004BDA90, che è il setter del NBL in ricezione manda set_rf_level(RX, NBL) al COR e non ha nulla a che vedere con la trasmissione. Se cercate il simbolo per nome senza guardare il modulo, prima o poi prendete quello sbagliato.
Sul lato sintetizzatore conta la coppia di costruttori di parola di sintonia, entrambi decompilati: AUXTX_C_AUXTX::set_rf_B95 @ 0x0041E2E9, che assembla una frame da 7 byte [NCL0_low, NCL0_high, Acounter, Bcounter, switch, IOffset, QOffset], e set_rf_B96 @ 0x0041E6DD, più laboriosa, che sceglie fra due tabelle di calibrazione e costruisce una frame di registri da 8 byte (RD0-RD2, DN0-DN2, NN0-NN2). Entrambe sono cammini di messaggi COR, non sequenze di outp(): send_data accoda con opcode AUXTX 1, read_data con opcode 2.
Una cosa che ho capito solo decompilando: set_rf_frequency da sola non basta. Scrive la parola di frequenza nella tabella di config per-canale, ma il valore raggiunge il DSP solo quando gira transfer_channels_to_cor. Il che mi porta al pezzo che conta.
Il COR e la sua mailbox DPRAM
Il C331_C_CTRL non scrive registri hardware direttamente: parla con il COR (Correction Processor), il DSP sulla board RXTX, attraverso un protocollo di messaggi in una mailbox DPRAM. Gli indirizzi fisici sono 0xD4000/0xD5000 (CMU_RXTX1_DPRAM/CMU_RXTX2_DPRAM). La funzione che fa il passaggio hardware, transfer_block_from_tc_queue @ 0x00436ED8, usa il selettore di device ISA 0x34 (il caso “l’offset è già un indirizzo fisico”) e scrive su questi offset:
| Offset | Size | Campo |
|---|---|---|
+0x00 | 1 | status/trigger — va letto prima (deve essere 0 = idle); scrivere 0xFF avvia il transfer |
+0x02 | 2 | sequence, big-endian; bit15 = altri chunk seguono |
+0x06 | 2 | payload length per questo chunk, big-endian |
+0x0A.. | ≤1000 | payload (il record vero) |
Prima di attivare il trigger il driver legge lo status byte della mailbox: un valore non-zero provoca un errore invece di sovrascrivere una mailbox non pulita. Il payload viene copiato per primo, poi lunghezza e sequence, e solo alla fine si asserisce il byte di trigger a +0x00. C’è anche un byte a +0x7FF che viene azzerato prima del trigger (presumo un flag ACK-ready) e uno a +0x7FC di cui non ho chiarito lo scopo.
Il formato del record di canale
transfer_channel_change @ 0x00433AD4 costruisce il record per la sintonia. Non c’è un prefisso di dimensione universale: ogni operazione COR provvede il proprio formato. Il record di canale:
| Offset | Size | Campo | Encoding |
|---|---|---|---|
+0x00 | 2 | command word | big-endian; 0x8000 | (rxtx_index?0x4000:0) | (immediate?0x3E9:0x3E8) |
+0x02 | 2 | record length − 4 | big-endian |
+0x04 | 2 | start channel index | big-endian |
+0x06 | 2 | (count & 0x7FF) | (field_bitmap << 11) | bitmap: bit0=Freq,1=NBL,2=WBL,3=Alt,4=Band |
+0x08.. | varia | campi presenti, in ordine Freq→NBL→WBL→Alt→Band | solo se il loro bit di bitmap è settato |
La parola di frequenza è 8 byte big-endian Q32.32 kHz. La formula di decode da c331_uint64_dbl @ 0x0042C342 è freq_hz = high32*1000.0 + low32*1000.0/2^32, con le due costanti di scala lette direttamente dalla memoria del firmware (_DAT_005B7A91 = 1000.0, _DAT_005B7A99 = 4294967296.0). Per codificare: khz = freq_hz/1000.0, high32 = floor(khz), low32 = round(frac(khz) * 2^32).
I campi NBL/WBL/Alt sono 2 byte fix16d2: raw = round(value * 100.0) byte-swappato. Un valore in dB con due decimali, per capirci: −20.00 dBm diventa −2000. Il campo Band è 2 byte raw, un piccolo indice di banda/range (il check nel firmware è < 13).
La formula del wire ID e il bit TX
Questa mi ha fatto perdere una giornata intera e la scrivo perché è il genere di dettaglio che non si può indovinare. Ogni record par e ogni record di canale porta un wire ID costruito così:
wire_id = 0x8000 | (rxtx_index ? 0x4000 : 0) | par_id
Con rxtx_index=0 siete sulla direzione RX (0x8000), con rxtx_index=1 siete sulla TX (0xC000). Tutti i record della catena del generatore richiedono rxtx_index=1. Io inizialmente mandavo rxtx_index=0, e il COR ignorava silenziosamente i record RX per operazioni TX: per questo i miei aggiornamenti di frequenza e livello sembravano avere successo — la scrittura in mailbox passava — ma l’RF non si muoveva.
I par record e la sequenza di accensione RF
I par record, quelli per connettore e modalità, sono più piatti dei record di canale: sono solo [ID (2 byte, big-endian)][valore] concatenati, senza header da 8 byte, e passano da transfer_par_to_cor @ 0x00436641, non da transfer_channels_to_cor. I par che servono per abilitare davvero l’uscita RF, tutti con rxtx_index=1:
| par_id | wire ID (TX) | valore | scopo |
|---|---|---|---|
| 100 | 0xC064 | connettore | selezione connettore TX |
| 124 | 0xC07C | 0x0000 | channel cursor (sempre 0) |
| 108 | 0xC06C | 0x000F | config modalità CW, va mandato per primo |
| 131 | 0xC083 | 0x0001 on / 0x0000 off | attivo-canali TX, 1 = RF attivo |
Attenzione a un errore che ho fatto e corretto: avevo derivato par 116 / wire 0xC074 dividendo per 1 invece che per 2 (le entry sono a 16 bit). Il valore giusto è par 124 / 0xC07C, e quel par è il cursore di canale, non l’enable RF. L’enable vero sono i par 108 e 131.
La sequenza completa di accensione (mode=4), tutta via transfer_par_to_cor, in ordine:
- Par 124 = 0 (channel cursor, wire
0xC07C) - Par 116 = 1, par 121 = 0, par 122 = 0 (measurement mode TX)
- Par 108 =
0x000F(config CW, wire0xC06C) - Par 128 = 0, par 129 = 0, par 130 = 0
- Par 131 = 1 (RF enable, wire
0xC083)
Lo spegnimento (mode=0) manda par 131 = 0 (0xC083) e par 128 = 0 (0xC080). Il par 108 non si rimanda in modalità spenta.
Per i connettori, la mappa verificata dalle tabelle di conversione cvt_conn_usb_to_tx_cor @ 0x005102DC (che indicizza una tabella a 3 entry, mappa identità 00 01 02): RF1→0, RF2→1, RF3→2, mandati come valore a 2 byte nel par record del connettore. RF4 bit 8 risolve a is_auxtx=1 e passa da AUXTX_C_AUXTX, un subsystem separato: non si usa come connettore TX da questa path.
Le par digital-level 101, 312 e 700
Sul versante dell’analizzatore contano tre par digital-level, trovate per ricerca di stringa in cmudrv.dl0. Il nome le fa sembrare letture di livello; due sono setter e la terza non la legge nessuno.
| Par ID | Hex | Tipo | Decodifica | Ruolo |
|---|---|---|---|---|
| 101 | 0x065 | UINT16 | 4.0 − int16(raw) / 100.0 (approssimata) | setter: set_digital_level_port, host→COR |
| 312 | 0x138 | FIX16D2 | int16(raw) / 100.0 (solo scala) | setter: set_digital_level, host→COR |
| 700 | 0x2BC | FIX16D2 | int16(raw) / 100.0 (solo scala) | archiviata e basta, nessun getter |
Un valore FIX16D2 si decodifica int16(raw) / 100.0, come i campi NBL/WBL dei record di canale. La forma 4.0 − raw/100.0 della par 101 viene da IQIF_C_IQIF::set_correction_to_iqif @ 0x004afbe4: letta lì, la par 101 è il conteggio ADC grezzo.
Le par 101 e 312 sono setter, cioè scrivono in direzione host→COR: non restituiscono niente a nessuno. Le par 700-737 finiscono in una tabella FIX16D2 all’offset ctrl+0x2b819, scritta da initialize_result_pointers a 0x004382E1, e da nessuna parte in cmudrv, rf_pe, base, BASE o audio c’è un codice che le legga. L’etichetta “COR-calibrated” non è sostenuta da una riga di codice. Su hardware, una configurazione RX normale fa tornare ACK FC vuoti: il COR non restituisce alcun livello per una lettura di potenza RF.
La lettura che uso per la spazzolata viene dal DSP RX a banda stretta, il wattmetro NPM: hw/cmu_dsp.c scarica RF\DRUNIPOW.DLD più RF\G10K0_RF.WDD, manda il comando di avvio, aspetta il messaggio 0x05, legge un blocco di 23 parole e prende la terza, con dBm = int24(parola)/32768 + NBL, dove NBL è l’ultimo livello richiesto al COR. Il meccanismo l’ho ricostruito dall’analisi statica del percorso di download DSP. La lettura via mailbox COR, quella di native_measure_power(), non è usata dalla spazzolata: sopravvive solo sulla schermata di test dell’analizzatore, per verifica diretta.
Due dettagli statici che spiegano come nasce l’ambiguità. Il primo è la mappa di dispatch dello stream FC, ricavata da copy_message_to_parameter_block a 0x00435b9f: le classi di par finiscono in tabelle diverse a seconda del tipo, 100-136 UINT16 su ctrl+0x2b7f1, 300-314 FIX16D2 su ctrl+0x2b801, 400-463 INT16, 500-505 double, 600-633 UINT16, 700-737 FIX16D2 su ctrl+0x2b819, 900-905 double. Il secondo sono gli indirizzi delle stringhe dei nomi, 0x005b987a, 0x005b9e25 e 0x005ba1e2: è da lì che la ricerca di stringa consegna le tre par, belle ordinate e con un nome plausibile addosso.
Nella stream FC i wire ID per RX sono 0x82BC (par 700) e 0x8065 (par 101); si maschera con 0x7FF per recuperare il par ID nudo.
Una distinzione che mi è costata chiarire: la lettura per-frequenza non è una singola funzione piatta. Il wattmetro dell’analizzatore RF vero è C_PRC_MEAS_AMPSMS_RFANAMOD, la cui implementazione sta in audio.dl2 @ 0x00436CBA, non in rf.dl2 dove è solo un thunk import. E il detector è lineare, non log-coded: la conversione lineare→log usa log10(x) * 20.0, quindi la normalizzazione DUT/reference va fatta come 20*log10(raw_DUT / raw_ref), non come sottrazione.

Cosa resta aperto
Non vi lascio con l’impressione che sia tutto chiuso, perché non lo è.
La semantica applicativa dei singoli frame COR: i nomi dei campi del COR_CHANNEL_DESCRIPTION completo e i payload ACK specifici per frame l’ho ricostruita solo come grammatica generica del transport; i significati nome per nome richiedono il mapping dei descrittori ai chiamanti o una cattura runtime.
La formula raw-to-dBm della Route B non è presente nel codice recuperato: get_uint16_result @ 0x004321AA fa solo validazione e endian-swap, nessuna moltiplicazione, offset o interpolazione. Il modello più plausibile che propongo è una tabella di calibrazione piecewise per frequenza/canale, ma è una forma candidata, non una formula recuperata. La buona notizia è che per il tracking basta la Route A (framework), che restituisce il dBm già calibrato dal DSP.
La validazione hardware resta empirica: Ghidra conferma selettore 0x34, la mailbox, sequence e trigger byte, ma non può confermare che quegli offset corrispondano alla mappa ISA/PCI attiva sullo strumento né che la board faccia ACK. E transfer_block salta del tutto la scrittura DPRAM se simulation_mode_is_on(), quindi né la simulation mode né DOSBox-X, che non ha un COR vero, possono esercitare questo path. È il confine finale da validare sull’hardware, che non ho ancora fatto.
Post Disclaimer

Informazioni Importanti
Note generali: Le informazioni contenute in questo articolo sono di carattere informativo e rispecchiano le opinioni personali del autore. Tutte le guide nel sito vengono proposte "as it is" puramente a scopo didattico. L'utente che accetta di seguirne i passaggi si assume ogni responsabilità in caso di guasto/malfunzionamento o altro problema come conseguenza della modifica.
Link di affiliazione e contenuti promozionali: Nei post di iu2frl.it posso includere collegamenti a rivenditori. Posso ricevere una piccola commissione dal rivenditore se effettui un acquisto dopo aver fatto clic su uno di questi link. I post non sono espressamente sponsorizzati da rivenditori, editori, promotori o produttori, se non diversamente specificato chiaramente e tutte le decisioni editoriali sono prese esclusivamente dagli autori dei singoli articoli.
Contenuti riguardanti regolamenti e/o normative: Eventuali riferimenti a normative non costituiscono valore legale, si tratta di libere interpretazioni ed estratti di circolari corredate da eventuali commenti, si invitano tutti gli utenti a verificare la veridicità di tali informazioni sugli organi ufficiali di riferimento, nessuna colpa o responsabilità può essere data agli autori degli articoli.
Licenza di utilizzo: Tutti i contenuti vengono condivisi con licenza CC BY-NC-SA 4.0: Questa licenza consente ad altri di remixare, adattare e sviluppare i contenuti in modo non commerciale, purché accreditino l'autore originale e concedano in licenza le loro nuove creazioni con gli stessi termini. Per maggiori informazioni visitare il sito Creative Commons.
Important Informations
General notes: The information contained in this article is of an informative nature and reflects the personal opinions of the author. All the guides/tutorial on the website are offered "as it is" for educational purposes only. The user who agrees to follow the steps assumes all responsibility in case of failure/malfunctioning or other problem as a result of the modification.
Affiliate links and promotional products: In some posts of iu2frl.it I can include links to resellers. I can get a small commission from the reseller if you make a purchase after clicking on one of these links. The posts are not expressly sponsored by resellers, publishers, promoters or producers, unless otherwise clearly specified and all editorial decisions are made solely by the authors of the individual articles.
Contents about rules and laws: Some articles may contain portions or whole laws or rules regarding specific areas, these kind of content are meant to be indicative only, no responsability can be given to the authors. Always refer to official sources when looking for rules or laws.
Contents licenses: All the contents are published as CC BY-NC-SA 4.0: This license lets others remix, adapt, and build upon your work non-commercially, as long as they credit you and license their new creations under the identical terms.. For more informations please visit Creative Commons website.
