Il modem software#

Il componente esp32idf_radioamateur_modem (integrato sotto components/, GPL-3.0) è il cuore del progetto: un modem software AFSK/FSK completo che demodula e modula audio APRS interamente sull’ESP32, usando solo il SAR-ADC, il DAC e un GPTimer. Questo capitolo copre il modem come capacità — i suoi profili, la sua API pubblica e la sua configurazione a runtime. Per gli interni del DSP e il ragionamento dietro le scelte di frequenza di campionamento e core, vedi La catena di segnale DSP.

Profili del modem#

I profili selezionabili (modem_mode_t) sono numerati in modo identico al menu a tendina di modulazione dell’amministrazione web, così che l’applicazione possa convertire il valore salvato direttamente nell’enum:

Valore

Profilo

Baud

Toni

0

AFSK300

300

1600 / 1800 Hz

1

Bell 202 (predefinito, APRS standard)

1200

1200 / 2200 Hz

2

ITU V.23

1200

1300 / 2100 Hz

3

G3RUH FSK

9600

—

I profili a 1200 Bd eseguono fino a dodici demodulatori in parallelo (MODEM_RX_MAX_DEMODULATORS = 12). Un demodulatore è un comparatore sull’uscita di un correlatore (un prefiltro passa-banda e i correlatori di marca e spazio che lo seguono): o ognuno ha il proprio prefiltro, con inclinazione diversa tra i toni, oppure più comparatori condividono un prefiltro e si distinguono per il peso dato al tono di spazio. In entrambi i casi il set copre uno sbilanciamento dei toni più ampio di qualsiasi demodulatore da solo; la trama la consegna il primo che la completa e le copie vengono scartate confrontando l’FCS. Il set di demodulatori, la banda del prefiltro, la soglia di ricezione, un passa-alto per il CTCSS, il controllo del guadagno e una riparazione opzionale dei bit dell’FCS sono impostazioni a runtime (modem_config_t.rx, vedi Radiomodem).

Correzione d’errore in avanti FX.25#

FX.25 incapsula AX.25 in un codice Reed–Solomon, permettendo al ricevitore di correggere errori di bit che altrimenti fallirebbero il CRC. È totalmente retrocompatibile: un frame FX.25 porta un normale frame AX.25 dentro un blocco RS con tag di correlazione, quindi i ricevitori di puro AX.25 decodificano comunque il frame interno. La modalità è selezionabile: 0 = disattivato, 1 = solo RX, 2 = RX+TX. Il codec viene sempre compilato — il CMakeLists.txt del componente stesso definisce ENABLE_FX25 pubblicamente — quindi cambiare modalità non richiede una ricompilazione. L’implementazione RS vive in lwfec/ (rs.c, gf.c).

Il codec lavora sul posto su un intero blocco Reed–Solomon di 255 byte in ogni modalità, comprese quelle il cui payload K è di soli 32 byte: la parità viene spostata in coda al blocco e lo spazio intermedio viene azzerato. Il buffer del chiamante deve quindi essere lungo 255 byte qualunque sia la K passata. Per questo Fx25Encode()/Fx25Decode() e RsEncode()/RsDecode() ricevono la capacità del buffer come argomento esplicito: viene verificata con assert nelle build di debug e fa fallire la chiamata in modo sicuro altrimenti, e ax25.c la sostiene con un controllo a tempo di compilazione sui due buffer che consegna.

API pubblica#

L’header pubblico del componente (esp32idf_radioamateur_modem.h) espone:

Funzione

Scopo

modem_init(cfg)

Avvia l’hardware e i task di servizio interni. Si blocca ~5 s una volta per avvio calibrando il clock reale dell’ADC.

modem_set_modem(cfg)

Cambiare il profilo attivo e impostazioni correlate a runtime.

modem_set_rx_callback(cb, ctx)

Installare il callback invocato per ogni frame decodificato.

modem_send_raw(frame, len)

Accodare un frame AX.25 grezzo (senza flag/stuffing/FCS — tutto aggiunto automaticamente).

modem_build_frame_tnc2(tnc2, out, out_len)

Costruire un frame grezzo da una stringa monitor TNC2.

modem_send_tnc2(tnc2)

Costruire + accodare in una singola chiamata.

modem_format_tnc2(msg, out, out_len)

Rendere un frame decodificato di nuovo in una stringa TNC2.

modem_tx_queue_depth()

Numero di frame ancora in coda/in volo su TX RF (0 = inattivo). È lo stato dell’anello TX che legge il tetto di arretrato TX RF.

modem_persistence_missed_count()

Quante volte il pavimento anti-starvation di CSMA ha forzato una trasmissione dopo otto slot consecutivi che hanno trovato il canale libero e mancato il sorteggio di persistenza. Uno slot occupato fa ripartire il conteggio, quindi misura soltanto il persist configurato: con il valore predefinito di 63 circa una portante su dieci finisce così. Nulla viene scartato, quindi è una statistica di accesso al canale e non uno scarto.

modem_channel_busy_count()

Quante volte un frame è stato trasmesso perché il rilevamento di portante era ancora attivo dopo csma_busy_max_ms dal primo slot occupato. È un rapporto sulla frequenza — un canale occupato molto più a lungo di quanto spieghi il normale traffico a pacchetti — e il frame esce sopra al segnale già presente. Non si muove finché quel limite vale 0. Ogni trasmissione forzata viene addebitata a esattamente uno dei due contatori.

modem_measure_adc_rate(ms)

Misurare la frequenza reale di campionamento dell’ADC; si blocca per la finestra richiesta.

L’header porta inoltre MODEM_DEFAULT_CONFIG() (un inizializzatore di modem_config_t), l’helper MODEM_DELAY_TICKS(ms), modem_rx_frame_t e il tipo di callback modem_rx_cb_t. Si noti che non esiste un punto di ingresso di smontaggio: il modem viene avviato una volta per boot e riconfigurato sul posto con modem_set_modem().

I tre punti di ingresso di trasmissione — modem_send_raw(), modem_build_frame_tnc2() e modem_send_tnc2() — si possono chiamare da qualsiasi task. Condividono un mutex interno, perché condividono l’accumulatore di CRC in uscita, l’anello di trasmissione a produttore singolo e la macchina a stati che va in trasmissione a partire da esso. modem_send_tnc2() mantiene quel mutex per tutta la costruzione e l’accodamento, quindi una trama arriva sempre all’anello con la somma di controllo accumulata per essa, anche quando un beacon parte nello stesso istante in cui l’IGate rilancia una riga da APRS-IS. Un chiamante che non ottiene il percorso entro un secondo riceve ESP_ERR_TIMEOUT (o 0 dal costruttore) invece di restare in attesa dietro di esso a tempo indeterminato.

Configurazione a runtime (modem_config_t)#

Costruita in esattamente un posto — aprs_service_build_modem_config() — condivisa dall’avvio, dal Salva della pagina Radio (riapplicazione in tempo reale, nessun riavvio) e dal test di loop:

Campo

Origine

Note

modem

afsk_modem_type

conversione diretta; la pagina fissa 0–3

flat_audio

audio_lpf

ingresso piatto/da discriminatore: attivo per una presa dati o dal discriminatore, spento per un’uscita altoparlante; sceglie la tabella delle inclinazioni dei prefiltri dei preset

full_duplex

false normalmente

LOOP TEST passa true (un cavo DAC→ADC significa che CSMA non vede mai il canale libero)

allow_non_aprs

false

accettare Control/PID diversi da 0x03/0xF0?

preamble_ms

preamble (300)

TXDelay

tx_tail_ms

tx_tail (20)

TXTail: flag inviati dopo il flag di chiusura prima di rilasciare il PTT, arrotondati per eccesso a flag interi

slot_time_ms

tx_timeslot (2000)

tempo di silenzio CSMA: quanto attende un frame accodato prima che l’accesso al canale cominci del tutto. L’intervallo fra i sorteggi di persistenza che seguono è csma_slot_ms, non questo valore. Ignorato in full duplex.

csma_slot_ms

csma_slot_ms (100)

slot temporale CSMA (lo SlotTime standard AX.25/KISS): l’intervallo fra i sorteggi di persistenza a canale libero e fra i controlli di un canale occupato. Ignorato in full duplex.

csma_busy_max_ms

csma_busy_max_s × 1000 (30000)

attesa massima che un canale occupato si liberi, contata dal primo slot occupato, prima di trasmettere comunque il frame accodato; 0 attende finché il canale resta occupato, come fa un TNC KISS standard. Ignorato in full duplex.

persist

csma_persist (63)

p-persistenza CSMA (il Persist standard AX.25/KISS): una volta che il canale è sentito libero, il modem trasmette con probabilità persist/256 per slot e altrimenti attende un altro slot prima di rilanciare. 255 = trasmette sempre al primo slot libero; valori più bassi distanziano le stazioni in contesa. Otto sorteggi mancati consecutivi a canale libero trasmettono comunque, così il sorteggio da solo non trattiene mai un frame indefinitamente; uno slot occupato fa ripartire il conteggio. Ignorato in full duplex.

fx25_mode

fx25_mode

0=off, 1=solo RX, 2=RX+TX

ptt_active_high

MODEM_PTT_ACTIVE_HIGH

cablaggio di scheda in compilazione, non un campo di configurazione

min_unkey_ms

ptt_min_unkey_ms

tempo minimo extra di PTT-disattivato tra le trasmissioni

adc_self_bias

adc_self_bias (spento)

polarizza il pad dell’ADC con il proprio pull-up e pull-down in serie, per un ingresso accoppiato tramite condensatore senza rete di polarizzazione esterna. Applicato dopo che il driver continuo ha configurato il pad, cosa che scollega entrambe le resistenze. Solo GPIO32/33

rx_clip_warn

rx_clip_warn (spento)

registra un avviso a frequenza limitata quando un blocco elaborato raggiunge gli estremi della gamma di conversione

dac_amplitude_pct

dac_amplitude_pct (MODEM_DAC_AMPLITUDE_PCT)

ampiezza di uscita, applicata per campione. Con un minimo del 20 %: il DAC è a 8 bit, quindi l’attenuazione richiesta da un ingresso microfonico spetta a un attenuatore esterno

dac_samplerate

dac_samplerate (MODEM_DAC_SAMPLERATE)

38400 o 76800 Hz. L’unico campo che modem_set_modem() non applica: il periodo del clock di campionamento e ogni passo di fase da esso derivato sono programmati a hardware fermo, quindi lo applica modem_init() e la modifica ha effetto al riavvio successivo

tx_max_keyed_ms

tx_max_keyed_ms (0)

tempo massimo di trasmissione, 0 = disattivato. Oltre tale durata il task di servizio del modem rilascia il PTT, ferma il modulatore e scarta la trasmissione

rx

rx_tuning (MODEM_RX_TUNING_DEFAULT())

catena di ricezione: preset dei demodulatori e inclinazioni personalizzate, banda e lunghezza del prefiltro, soglia di ricezione, passa-alto CTCSS, guadagno automatico o fisso, riparazione dei bit; limitata da modem_rx_tuning_sanitize()

Nota

Il GPIO del PTT non è un campo di modem_config_t — è una scelta di cablaggio di scheda fissata in compilazione (MODEM_PTT_GPIO), come i pin ADC/DAC. Solo il livello attivo è passato a runtime, e viene anch’esso direttamente dalla macro di compilazione. Esplicitamente non mappati a runtime (senza equivalente nel componente): pin e attenuazione ADC/DAC, squelch hardware, interruttore di potenza RF, volume RX e il tetto dell’AGC. La soglia di ricezione software e un guadagno fisso di ricezione fanno parte di rx.

LIVELLO RX e TEST TX#

Il loop test più sotto richiede un cavo fra DAC e ADC, quindi smette di essere utilizzabile non appena un apparato sostituisce quel ponticello: non c’è nulla che restituisca il frame. Due pulsanti accanto coprono lo stesso terreno con un apparato collegato, uno per direzione.

LIVELLO RX (aprs_rx_level_sample(), POST /radio/level) osserva lo stadio di ricezione per circa un secondo e riporta il livello della banda dei toni (afskGetBandRms()) e il livello RMS a banda larga con i loro picchi, un verdetto in una parola (saturato, nessun segnale, basso sotto 20 mV RMS di toni, buono), l’offset di continua dell’ingresso, il guadagno dell’AGC, gli estremi grezzi di conversione su tutta la finestra e lo stato del rilevamento di portante, seguiti dalle statistiche di ricezione di modem_get_rx_stats() (trame decodificate ed esclusive di ogni demodulatore, trame consegnate e riparate, campioni persi) e il carico del DSP di ricezione di afskGetDspLoad() (quota media e di picco del tempo reale nella finestra, e il blocco più alto dall’ultimo azzeramento delle statistiche). Non trasmette nulla e non cambia lo stato del modem, quindi può girare mentre viene decodificato traffico reale. È ciò contro cui si regola il trimmer di ricezione — puntare a toni di almeno 20 mV RMS con la gamma grezza lontana da 0 e 4095 — ed è ciò che distingue un ingresso polarizzato da adc_self_bias (1200-2000 mV) da uno senza alcuna polarizzazione.

TEST TX (aprs_tx_test_run(), POST /radio/txtest) manda in trasmissione e modula un breve frame di stato attraverso il consueto percorso di trasmissione non critico, per cui valgono sia l’accesso al canale in semiduplex sia il tetto di duty cycle. Non aspetta nulla di ritorno: la deviazione prodotta si legge su altra strumentazione e si regola a 2,5-3,5 kHz.

Entrambi condividono il flag di prenotazione del loop test, quindi ne gira uno solo dei tre alla volta.

Il LOOP TEST#

Lo strumento di messa in funzione più utile del progetto. Cabla GPIO25 → GPIO33, apri Radio / Modem, premi LOOP TEST. aprs_loop_test_run():

  1. Costruisce un piccolo pacchetto APRS che porta un token casuale monouso (>LOOPTEST <token>), con il nominativo della stazione come sorgente (quello dell’IGate, o del digipeater); senza di esso si rifiuta di partire.

  2. Prende il trasmettitore: i frame già in coda escono prima normalmente, poi ogni altro produttore viene rifiutato (DROP_TX_SELF_TEST) fino alla fine del test.

  3. Inibisce il PTT (modem_set_ptt_inhibit()): il frame viene modulato sul DAC, ma un ricetrasmettitore collegato non viene mai attivato.

  4. Devia i frame decodificati al proprio hook così che il frame di test non venga mai digipetato, caricato, né registrato come traffico reale.

  5. Commuta il modem a full duplex — un cavo DAC→ADC significa che il nodo sente sempre la propria portante e CSMA non attiverebbe mai la radio.

  6. Attende che il rilevamento di portante del demodulatore si liberi prima di attivare il PTT, al massimo per LOOP_TEST_CHANNEL_WAIT_MS (3000 ms), per non trasmettere il tono di autotest sopra una stazione che è in onda in quel momento — la lettura è indipendente dal flag di duplex appena impostato, che condiziona solo il CSMA. Un canale ancora occupato al raggiungimento del limite viene registrato e il test trasmette comunque.

  7. Trasmette, poi attende fino a LOOP_TEST_TIMEOUT_MS (4000 ms) che la catena ADC → demodulatore → HDLC → AX.25 restituisca lo stesso frame.

  8. Ripristina sempre l’hook reale e la modalità duplex configurata, poi toglie l’inibizione del PTT e rilascia il trasmettitore, prima di tornare.

Nel frattempo un task di monitor cattura diagnostici che il componente espone solo istantaneamente: uno snapshot dell’ADC grezzo passivo a metà preambolo, RMS di picco, guadagno AGC di picco, una mappa di bit di DCD, e la fase RX HDLC più lontana raggiunta per demodulatore. Il messaggio di risultato distingue:

Sintomo

Diagnosi

ADC grezzo min ≈ max

ADC morto / non cablato

il grezzo oscilla, RMS ~0

nessun tono raggiunge l’ADC

RMS ok, DCD mai attivo

il PLL non ha mai agganciato → mismatch baud/tipo modem o audio cattivo

DCD attivo, fase < FRAME

flag visti ma nessun frame iniziato — problema di recupero bit, non rumore

DCD attivo, fase = FRAME, nessun frame

frame assemblati ma falliti al CRC — livello/SNR marginale

frame di ritorno, token non corrisponde

distorsione, clipping, o cablaggio di loop sbagliato

PASS

riporta il livello RX in mV RMS