La catena di segnale DSP#
Questo capitolo spiega come il modem converte l’audio radio in frame e viceversa, e — altrettanto importante — perché i numeri sono quelli che sono. L’header di configurazione del modem è insolitamente ben documentato, e il ragionamento conta se mai lo tocchi.
La catena, fase per fase#
Fase |
Frequenza |
Dove |
|---|---|---|
SAR-ADC1 continuo/DMA, frame di conversione da 128 campioni |
76 800 Hz |
ISR del driver su core 0 |
ingest: de-interleave coppie, soppressore di impulsi
( |
76 800 Hz |
|
FIR di decimazione (48 coefficienti, rapporto 8:1), passa-alto
del quarto ordine per CTCSS e bassi ( |
→ 9 600 Hz |
|
per correlatore (fino a quattro): prefiltro passa-banda, correlatori mark/space, magnitudini dei toni, passa-basso, stima dello sbilanciamento dei toni |
9 600 Hz |
|
per demodulatore (fino a dodici): comparatore sulle magnitudini di un correlatore, DCD, DPLL, decodifica NRZI |
9 600 Hz |
|
de-framing HDLC, de-stuffing di bit, controllo FCS (riparazione dei bit opzionale), decodifica RS FX.25, soppressione dei duplicati tra demodulatori |
— |
|
⟵ TX ⟶ codifica AX.25, FCS, bit stuff, NRZI, accumulatore di fase a 32 bit, LUT seno da 512 voci |
38 400 Hz |
|
Cosa dà per buono il decodificatore di trame#
ax25_decode() è un punto di ingresso pubblico del componente modem, quindi
convalida il proprio input invece di fidarsi del produttore che riempie il
buffer. Oltre alla lunghezza minima dell’intestazione (destinazione + sorgente +
controllo + PID = 16 byte) non legge nulla senza prima misurarlo rispetto a
len: il campo indirizzi viene percorso un indirizzo alla volta, e sia
l’ottetto di estensione sia i sette byte del ripetitore che dichiara devono
essere ancora dentro la trama. Una ricezione troncata o corrotta i cui bit di
estensione non terminano mai viene respinta, invece di decodificare byte che
stanno oltre la trama — nel percorso RX quei byte sono la coda della trama
ricevuta in precedenza, che altrimenti diventerebbero nominativi di ripetitore
del tutto plausibili in una decodifica per il resto valida.
Cosa garantisce il codificatore di trame#
In trasmissione, Ax25GetTxBit() applica il bit stuffing alla trama e alla sua
FCS come un unico campo HDLC continuo: dopo ogni sequenza di cinque bit a 1 viene
inserito uno 0, e il contatore della sequenza attraversa il confine tra l’ultimo
byte di dati e la FCS. Quando una sequenza di cinque 1 termina esattamente
sull’ultimo bit della FCS, lo 0 di riempimento viene comunque inviato, prima
del flag di chiusura. Se mancasse, il ricevitore prenderebbe lo 0 iniziale del
flag per il bit di riempimento e il flag di chiusura si completerebbe con solo sei
bit di dati dall’ultimo confine di byte invece di sette. Il decodificatore interno
non conta quei bit, ma quelli più rigorosi sì — hdlc_rec.c di Direwolf scarta
una trama del genere — e con contenuto casuale il caso capita circa a una trama
su sessanta. I blocchi FX.25 non sono coinvolti: writeFx25Frame() riempie la
FCS da sé prima della codifica Reed–Solomon.
Il campo indirizzi termina sempre sull’ultimo indirizzo in uso. ax25_encode()
e hdlcFrame() impostano il bit di fine indirizzo (bit 0 dell’ottetto SSID)
su quell’indirizzo e lo azzerano su tutti gli altri, compreso il caso completo di
destinazione, sorgente e otto digipeater, in cui dopo l’ultimo non resta alcuno
slot vuoto. Una trama senza quel bit viene scartata da Direwolf e dai TNC KISS,
e il ricevitore interno prenderebbe il byte di controllo per la fine del campo
indirizzi; poiché il digipeater ricodifica da testo TNC2 ogni trama che ripete,
una trama legale con otto digipeater riparte legale come è arrivata.
Ogni trama si chiude con un flag seguito dalla coda TX (Ax25TxTail(),
predefinita 20 ms, arrotondata per eccesso a flag interi), e solo allora la ISR
rilascia il PTT. Lo spegnimento pubblica il proprio stato in un ordine fisso -
smontaggio in sospeso, poi trasmettitore inattivo e PTT rilasciato, poi lo
stadio di attivazione di nuovo a riposo - e Ax25TransmitCheck() non attiva
la trama successiva finché il modulatore è in funzione o lo smontaggio è ancora
dovuto. Una nuova attivazione non può quindi mai iniziare dentro lo spegnimento
precedente, cosa che in full duplex (senza tempo di silenzio) lascerebbe il
trasmettitore bloccato fino al riavvio.
Ax25GetTxBit() gira nella ISR del timer del DAC, che è sicura rispetto alla
cache (CONFIG_GPTIMER_ISR_CACHE_SAFE) e quindi continua a girare mentre una
scrittura LittleFS o un aggiornamento OTA tengono disabilitata la cache della
flash. Tutto ciò che quella ISR legge sta in IRAM o DRAM. In particolare non
tocca mai Fx25ModeList, la tabella dei modi FX.25 in flash:
writeFx25Frame() copia l’etichetta di correlazione, K e T del modo
scelto nel descrittore stesso della trama, in contesto di task, e la ISR invia
l’etichetta da quella copia. La stessa regola vale per il resto della catena di
chiamate (MODEM_BAUDRATE_TIMER_HANDLER(), txRetireFrame(),
ModemTransmitStop(), setTransmit(), setPtt(), LED_Status2()), le
cui funzioni sono IRAM_ATTR e le cui tabelle e costanti (sin_table, il
numero di pin del PTT) sono DRAM_ATTR.
Questo è fissato da un test di regressione sull’host,
components/esp32idf_radioamateur_modem/test/host/. Compila i sorgenti reali
ax25.c, crc_ccit.c e quelli di FX.25 con piccoli header sostitutivi, sotto
AddressSanitizer e UndefinedBehaviorSanitizer, e invia 20 000 trame UI casuali
(30–180 byte) attraverso Ax25WriteTxFrame() / Ax25TransmitCheck() /
Ax25GetTxBit(). Ogni trasmissione viene confrontata bit per bit con un
codificatore HDLC di riferimento e passata sia al ricevitore interno sia a un
ricevitore che segue le regole di Direwolf; un secondo passaggio invia 2 000
trame come blocchi FX.25 attraverso il ricevitore interno, con le pagine che
contengono Fx25ModeList non mappate mentre i bit vengono emessi, così che
qualunque lettura della tabella dal percorso dei bit in trasmissione vada in
errore come farebbe con la cache disabilitata. Un terzo passaggio costruisce
trame con 0-8 digipeater da testo TNC2 e verifica il bit di fine indirizzo,
ax25_decode() e una trasmissione completa attraverso il ricevitore interno;
un quarto gira in half duplex e verifica che uno slot temporale 0 trasmetta ogni
trama subito, mentre uno diverso da zero trattenga ogni trama almeno per quel
tempo; un quinto verifica che ogni lunghezza di coda dia esattamente un flag di
chiusura più i flag della coda, e che una trama accodata non si attivi finché il
modulatore è in funzione o il suo smontaggio è ancora dovuto. L’esecuzione riporta anche quante trame hanno chiuso la FCS su cinque 1
(qualche centinaio), quindi un PASS copre sempre il caso.
cd components/esp32idf_radioamateur_modem/test/host
make # seme predefinito
make SEED=0x2a # qualunque altro seme
La regolazione della ricezione#
Tutto quanto segue si imposta a runtime tramite modem_config_t.rx (il
gruppo Demodulatore di ricezione di Radiomodem) ed è applicato da
afskSetModem() con il task di ricezione fermo, quindi nessun blocco viene
mai elaborato da una catena costruita a metà.
- Correlatori e comparatori.
Ogni correlatore misura la magnitudine vera
sqrt(I² + Q²)del tono di marca e di quello di spazio;|I| + |Q|oscillerebbe fra 1 e 1,41 volte quel valore con la fase del tono, cioè 3 dB di rumore sulla decisione. Un comparatore confronta poi la magnitudine di marca con quella di spazio moltiplicata per il proprio peso. In aria lo sbilanciamento fra i toni va da nulla (un trasmettitore con ingresso dati piatto su un’uscita discriminatore) a 5–12 dB a favore del tono di spazio (un trasmettitore con pre-enfasi su un’uscita discriminatore), e un’uscita altoparlante sposta entrambi i casi per la de-enfasi del ricevitore, quindi una sola decisione senza peso fallisce oltre circa ±12 dB di sbilanciamento.Le due metà lo compensano. I prefiltri sono progettati in
ModemInit()(campionamento in frequenza, finestra di Hamming, fase lineare, picco della banda passante scalato all’unità perché i percorsi int16 e int32 restino nell’intervallo) a partire dai bordi di banda, dalla lunghezza e da un”inclinazione; l’inclinazione realmente raggiunta, misurata sui coefficienti, viene registrata e usata dalla stima dello sbilanciamento. Il peso del comparatore è esatto e quasi gratuito, perché il passa-basso dopo la rivelazione è lineare e può agire sulle due magnitudini prima della pesatura — più comparatori condividono un correlatore al costo di una moltiplicazione e una sottrazione ciascuno. Ciò che un comparatore non può fare è tenere un tono forte fuori dal correlatore dell’altro tono: il correlatore dura un simbolo, quindi la sua risposta è abbastanza larga da lasciar entrare l’altro tono, e solo un filtro davanti lo toglie. Per questo il set predefinitoMODEM_RX_EQ_MULTISLICEusaMODEM_RX_SLICER_PREFILTERS= 4 prefiltri, inclinati di +13, +4, −5 e −13 dB, con tre comparatori ciascuno; tenere ogni peso entro circa 3,5 dB dall’unità conta sui segnali deboli, dove un peso lontano costa sensibilità. La tabella inmodem.ccontiene la compensazione con cui ogni comparatore deve finire — inclinazione del prefiltro più peso — e ogni peso è derivato dall’inclinazione che il suo prefiltro ha realmente raggiunto, così i dodici demodulatori avanzano a passi di 3 dB fra +16,5 e −16,5 dB qualunque sia la lunghezza del prefiltro. Quell’unico intervallo contiene lo sbilanciamento dell’audio piatto e di quello de-enfatizzato, quindi i multicomparatori ignoranoflat_audio; solo i set di filtri e il classico scelgono le loro inclinazioni in base ad esso.ModemLogConfig()elenca il risultato appena il task di ricezione riparte.MODEM_RX_EQ_MULTISLICE2dispone gli stessi dodici comparatori su due prefiltri, sei ciascuno (+9/−9 dB), per confrontare le due disposizioni su una stazione in funzione. I set di filtri danno a ogni demodulatore il proprio prefiltro e un comparatore senza peso, e il set classico mantiene le tabelle fisse a 8 coefficienti. La mappa di bit del rilevamento di portante e la maschera dei demodulatori della finestra dei duplicati sono a 16 bit, il che limitaMODEM_RX_MAX_DEMODULATORSa 16.- Soppressione dei duplicati e statistiche.
Una trama con FCS valido apre una finestra di 32 periodi di bit × il numero di demodulatori attivi; le copie con lo stesso CRC degli altri demodulatori al suo interno vengono scartate (trame semplici e FX.25 allo stesso modo). La finestra registra quali demodulatori hanno prodotto la trama, quindi alla chiusura attribuisce una trama ottenuta da uno solo a quel demodulatore — il contatore
esclusiviche dice ciò che ogni prefiltro aggiunge.- La soglia di ricezione conserva ciò che trattiene.
Un blocco raggiunge i demodulatori mentre il suo livello nella banda dei toni (
afskGetBandRms(): due passa-banda in cascata attorno a 900–2600 Hz sul segnale decimato e filtrato dal passa-alto, in mV al pin) ha superatorx.gate_mvper più di tre blocchi, e finché non scende sotto la metà. Ronzio, CTCSS e bassi non la aprono né la tengono aperta. Il decimatore e il passa-alto girano su ogni blocco, e gli ultimi tre blocchi trattenuti vengono conservati (decimati, 3 × 192 float); quando la soglia si apre vengono demodulati per primi, così il preambolo speso per decidere l’apertura non va perso.gate_mv = 0alimenta ogni blocco.- Controllo del guadagno sul segnale in banda.
L’AGC misura il blocco decimato e non il flusso a 76,8 kHz, quindi il rumore del discriminatore sopra i 5 kHz non determina il guadagno, e il nuovo guadagno si applica al blocco su cui è stato misurato. Attacco 0,25 e rilascio 0,002 per blocco da 20 ms, con il passo per blocco limitato a ×2 / ÷2 e il guadagno tra 0,1 e 8. Il livello è preso su tutta la banda decimata, bassi compresi, così un ingresso ricco di bassi mantiene il guadagno sotto la limitazione dell’ingresso del demodulatore. Un blocco il cui ingresso, misurato prima del guadagno, è sotto 0,002 della semiampiezza dell’ADC (circa 3,3 mV RMS al pin, il rumore proprio del convertitore) lascia fermo il guadagno; la prova è sull’ingresso stesso, quindi ogni segnale reale viene portato al livello di lavoro qualunque fosse il guadagno. La legge sta nell’header
rx_agc.h, solo intestazione, che il replay su PC diaudio_test/rx_replaycompila senza modifiche. Un guadagno fisso lo sostituisce quandorx.agc_modelo richiede.- Riparazione dei bit tramite sindrome del CRC.
CRC-16/X.25 è affine su GF(2): il registro dopo una trama e il suo FCS è
0xF0B8XOR una sindrome che dipende solo dallo schema d’errore. La sindrome di un singolo bit errato in posizione p è un passo di CRC a ingresso zero della sindrome in p + 1, quindi ogni bit singolo e ogni coppia adiacente (lo schema di un simbolo errato dopo NRZI) vengono verificati in una sola passata. Una correzione è accettata solo quando esattamente un candidato corrisponde e la trama supera un rigoroso test di plausibilità APRS, e mai mentre la copia intatta di un altro demodulatore è nella finestra dei duplicati.- Il componente modem è compilato con ``-O2``.
Il DSP di ricezione gira su ogni campione di ogni demodulatore; il
CMakeLists.txtdel componente lo compila ottimizzato per la velocità qualunque sia il livello usato dal resto del progetto.
Perché i numeri sono quelli che sono#
- ADC a 76 800 Hz, non 38 400.
38 400 dà al profilo a 9600 Bd esattamente quattro campioni ADC per simbolo. L’istante di campionamento del DPLL è allora quantizzato al 25 % di un simbolo e il voto a maggioranza di tre campioni copre il 75 % di un simbolo — la finestra di voto raggiunge sempre una transizione. La simulazione su host del
modem.creale, con clock reali e senza rumore, ha prodotto errori di bit netti in ogni fase in cui gli istanti dell’ADC si allineano con gli istanti di aggiornamento del DAC; i due clock differiscono di ~0,05 %, quindi l’allineamento percorre quelle fasi ogni ~55 ms. A 76 800 la stessa simulazione dà zero errori di bit in ogni fase e con fino a 30 µs di jitter di fronte TX. Ai profili AFSK non è mai importato (si demodulano a 9600 Hz attraverso un correlatore dopo la decimazione) e misurano identico a qualsiasi frequenza. Costo: il doppio del lavoro RX DSP, eMODEM_RESAMPLE_RATIOdiventa 8, ciò che richiede il FIR di decimazione più lungo — un filtro a 8 tap tagliato per 4:1 non fa antialias di 8:1.- Il DAC resta a 38 400 Hz (= 32 × 1200, un multiplo esatto di ogni frequenza
di baud supportata). I profili AFSK costruiscono i passi dei toni e un accumulatore di fase di simbolo Q32 dalla frequenza reale di allarme del DAC, quindi toni e velocità sono esatti anche se il periodo di allarme si arrotonda a tick interi del timer; un fronte di simbolo cade sul campione del DAC più vicino, al massimo il 3 % di un simbolo a 1200 Bd e nascosto dal tono a fase continua. G3RUH mantiene invece ogni simbolo per un numero intero di campioni del DAC, con tutti i fronti sulla stessa griglia: un accumulatore frazionario a quattro campioni per simbolo sposterebbe ogni tanto un fronte di un quarto di simbolo. Era il ricevitore ad aver bisogno di risoluzione.
- ``MODEM_ADC_CONV_FRAME = 128``, non la dimensione di blocco.
L’ISR dell’ADC di IDF stessa chiama
xRingbufferSendFromISR(), che fa tutto ilmemcpydentroportENTER_CRITICAL_ISR(). Su Xtensa ciò alzaPS.INTLEVELa 3 — e il clock di campionamento del DAC è un’interruzione di livello 3. Quindi l’ISR del DAC è mascherata durante la copia: 768 campioni ≈ 11 µs (10 % di un simbolo a 9600 Bd — fatale); 128 campioni ≈ 2 µs (2 % — entro il budget). Nessuna quantità diIRAM_ATTRdal nostro lato aiuta: il codice bloccante è del driver, già in IRAM, e semplicemente lungo. A 1200 Bd, 11 µs è l’1,3 % di un simbolo e invisibile — che è esattamente perché ogni profilo AFSK passava mentre G3RUH perdeva frame.- ``MODEM_DAC_TIMER_CORE (1) ≠ MODEM_ADC_ISR_CORE (0)``.
portENTER_CRITICAL_ISR()maschera il livello ≤ 3 solo sul core locale. Metti il clock del DAC sull’altro core e l’ISR dell’ADC gira solo aspettando il lock invece di mascherarlo. Imposto con#error. Le due correzioni (frame piccoli, core separati) sono indipendenti ed entrambe applicate.- ``ModemCalibrateSampleRate()``.
modem_init()si blocca ~5 s all’avvio misurando la frequenza reale dell’ADC (modem_measure_adc_rate()), perché il passo del PLL di ogni profilo presuppone la frequenza nominale dell’ADC e la differenza è altrimenti un errore di stato stazionario che il DPLL deve inseguire per un’intera trasmissione. Le stazioni in aria trasmettono alla propria velocità nominale, quindi la correzione in ricezione è il solo errore dell’ADC. La frequenza reale di allarme del DAC, nota esattamente dalla configurazione del timer, viene registrata anch’essa ma usata solo quando un ricevitore G3RUH sente il trasmettitore del nodo stesso (full duplex, l’autotest con loop via filo). Entrambi i clock derivano dallo stesso cristallo, quindi i rapporti sono proprietà fisse della scheda: misurati una volta per avvio, riapplicati a ogni cambio di profilo.- Il FIR di decimazione filtra sul posto.
Il campione di uscita i viene scritto in
buf[i]mentre i coefficienti leggono la finestra che termina inbuf[i × MODEM_RESAMPLE_RATIO], quindi con qualunque rapporto ≥ 2 il puntatore di scrittura resta dietro alla finestra di lettura tranne che nelle primeFILTER_TAPS − 1posizioni. Quei pochi campioni iniziali, insieme alla coda del blocco precedente, sono preparati in un breve array statico prima che il ciclo inizi; il resto del blocco è letto grezzo dabuf[]stesso. È tutta qui la ragione per cui il percorso di RX non tiene una seconda copia del blocco da 20 ms.- ``MODEM_RX_FIFO_SIZE = 4096`` campioni.
Dimensionato in campioni, quindi si è ristretto nel tempo quando la frequenza è raddoppiata (2048 erano 53 ms a 38,4 k, solo 26,7 ms a 76,8 k — appena un blocco da 20 ms). 4096 ripristina il margine; deve contenere ≥ 2 blocchi, poiché
AFSK_Poll()consuma solo blocchi interi.
Guardie di compilazione#
Guardie #error di compilazione impongono: pin del DAC ∈ {25, 26}; pin
dell’ADC ∈ 32–39; MODEM_ADC_SAMPLERATE % 9600 == 0; FIFO ≥ 2 blocchi;
MODEM_ADC_CONV_FRAME pari, che divida MODEM_BLOCK_SIZE, e allineato ai
byte a SOC_ADC_DIGI_DATA_BYTES_PER_CONV; core del timer DAC ≠ core dell’ISR
ADC; priorità del timer DAC ∈ 1..3. Due _Static_assert in afsk.c fissano
l’invariante di lavoro sul posto del decimatore: MODEM_RESAMPLE_RATIO ≥ 2 a
meno che il filtro non abbia un solo coefficiente, e MODEM_BLOCK_SIZE ≥ 2 ×
(FILTER_TAPS − 1) perché i tratti di storia iniziale e finale non possano
sovrapporsi.
Riferimento di configurazione in compilazione#
Tutto in
components/esp32idf_radioamateur_modem/include/esp32idf_radioamateur_modem_config.h,
ogni macro protetta con #ifndef così che il sistema di build possa
sovrascriverla.
Macro |
Predefinito |
Significato |
|---|---|---|
|
25 |
uscita audio; solo 25 o 26 |
|
33 |
ingresso audio; solo 32–39 |
|
−1 |
pin di PTT (cablaggio di scheda). Il valore predefinito dell’header è −1
(disabilitato); il |
|
1 |
polarità di PTT |
|
−1 |
LED di stato |
|
38400 |
= 32 × 1200 |
|
76800 |
= 8 × 9600 |
|
60 |
oscillazione del DAC, % di 0–3,3 V |
|
|
finestra ≈ 0–3,1 V |
|
4096 |
campioni, potenza di due |
|
128 |
campioni per frame DMA |
|
10 / 4096 / 0 |
task RX DSP |
|
0 |
core dell’ISR DMA dell’ADC |
|
1 |
deve differire dal core dell’ISR dell’ADC |
|
3 |
1..3 |
|
12 |
demodulatori (comparatori) a 1200 Bd in parallelo,
|
(derivato) |
9600 |
fisso |
(derivato) |
8 |
ADC ÷ demod |
(derivato) |
1536 |
20 ms a 76,8 kHz |
I file sorgente del modem#
File |
Ruolo |
|---|---|
|
ingest DMA dell’ADC, soglia di ricezione e anello di trattenuta, FIR di decimazione, passa-alto, AGC, ISR del DAC, PTT |
|
progetto dei prefiltri e set di demodulatori, correlatori, DPLL, tabelle di toni, DCD, stima dello sbilanciamento, calibrazione |
|
framer HDLC, NRZI, bit-stuffing, soppressione dei duplicati, riparazione dei bit, codec AX.25, coda TX |
|
l’API pubblica del componente: |
|
FEC Reed–Solomon FX.25 |
|
FCS (sequenza di controllo del frame) |
|
stadi del front-end di ricezione solo intestazione (soppressore di
impulsi, inseguitore della DC, passa-alto, controllo del guadagno), condivisi con il replay su PC di
|