SSB-Sender per DSP-Software
Mein FSK-Modulationsverfahren
kann es schon fast, aber nicht richtig. Ich habe mal ein Sprachsignal
an den Eingang gelegt und das Ausgangssignal mit einem SSB-Empfänger
abgehört. Man kann erahnen, dass es Sprache sein soll, aber die
Frequenzwechsel sind zu langsam, und alles hat dieselbe Amplitude. Das
hört sich dann eher an wie misslungene Musik.
Vor Jahren habe ich mal gelesen, dass jemand SSB wie ein FM-Signal
aufbereitet hat und am Ende erst in der Endstufe die Amplituden wieder
hergestellt hat. Das hat mich immer schon fasziniert. Dass so etwas
auch mit ausgefeilter DSP-Software gehen kann, war mir klar. Aber ich
hätte nie gedacht, dass so etwas sogar auf einem ATmega laufen kann.
Die Lösung kam von Guido, PE1NNZ http://pe1nnz.nl.eu.org/, der den bekannten QRP-CW-Transceiver QCX http://www.qrp-labs.com/qcx.html für SSB aufgebohrt hat. Das gesamte Projekt findet man unter https://github.com/threeme3/QCX-SSB.
Die Software besteht aus einem großen File, weil alles, was man sonst
aus Bibliotheken verwenden würde, extrem optimiert wurde. Das finde ich
sehr sympathisch, aber einfach ist es trotzdem nicht. Ich wollte den
Kern der Software verstehen und erstmal nur das verwenden, was man für
einen SSB-Sender braucht. Die Software enthält auch alles, was im
Empfänger gebraucht wird, also die IQ-Verarbeitung, Filter, ALC
usw. Ich ziehe meinen Hut vor Guido, der einen vollständigen
Transceiver mit einem ATmega328 realisiert hat.
Hier erstmal einen laienhafte Kurzbeschreibung, was da im SSB-Sender
abläuft: Das Mikrofonsignal wird direkt an AD0 eingelesen. Der
AD-Wandler läuft mit der internen Referenz von 1,1 V, die am AREF-Pin
erscheint, wenn sie per Software eingeschaltet wird. Ein
Spannungsteiler mit zwei gleichen Widerständen macht daraus 0,55 V als
Ruhespannung des Eingangs. Das Audiosignal wird in der Funktion SSB
verarbeitet. Mit einer Hilbert-Transformation wird es um 90 Grad
gedreht, sodass man ein I/Q-Signal bekommt. Aus diesem wird dann die
momentane Amplitude und die aktuelle Phase bestimmt. Die Phase wird in
eine Frequenzänderung umgerechnet, die dann als USB- oder LSB-Frequenz
an den SI5351 gesendet wird. Die Amplitude wird über einen PWM-Kanal
ausgegeben und steuert die Leistung des Endverstärkers.
Im ersten Versuch habe ich es ganz ohne Amplitudensteuerung
versucht. Das Signal erscheint dann an CLK2, also am Ausgang B des
SDR-Shields. Ganz erstaunlich: Man kann so bereits Sprache verstehen.
Die Software erzeugt eine Art FM-Signal, dessen Frequenzen dem
Mikrofonsignal entsprechen. Als Audioquelle habe ich ein UKW-Radio
verwendet und einen Sprachbeitrag gesucht.
Im zweiten Versuch habe ich auch die Amplitudensteuerung verwendet.
Nach dem Schaltungsvorschlag von Guido wird ein passendes
DC-Steuersignal als Vorspannung für den Endstufen-FET verwendet. Ich
hätte gedacht, dass man die Betriebsspannung steuern muss, aber bei
sorgfältiger Justierung der Übertragungskennlinie reicht auch eine
Steuerung am Gate. Das SSB-Signal erscheint nun am Drain des
Transistors. Ich habe für den ersten Test einen Arbeitswiderstand mit
100 Ohm verwendet.
Ein kurzes Stück Draht als Antenne koppelt das Signal auf den
Empfänger. Nun entsteht bereits ein vollwertiges SSB-Signal mit
hervorragender Träger- und Seitenbandunterdrückung. Der Klang ist
teilweise etwas verzerrt, was möglicherweise daran liegt, dass das
Signal vom Radio auch Frequenzen oberhalb der Bandgrenze enthält. Ein
Tiefpassfilter sollte helfen.
Das auf das nötigste reduzierte Programm SDR-SSB.ino steht hier zum Download: SDR-SSB.zip
void loop(){
int16_t adc; // current ADC sample 10-bits analog input, NOTE: first ADCL, then ADCH
int16_t _adc;
ADCSRA |= (1 << ADSC);
si5351.SendPLLBRegisterBulk(); // submit frequency registers to SI5351 over 731kbit/s I2C (transfer takes 64/731 = 88us, then PLL-loopfilter probably needs 50us to stabalize)
OCR1BL = amp; // submit amplitude to PWM register (takes about 1/32125 = 31us+/-31us to propagate) -> amplitude-phase-alignment error is about 30-50us
adc = ADC;
int16_t df = ssb(adc); // convert analog input into phase-shifts (carrier out by periodic frequency shifts)
adc += ADC;
ADCSRA |= (1 << ADSC);
si5351.freq_calc_fast(df); // calculate SI5351 registers based on frequency shift and carrier frequency
_adc = (adc/2 - 512);
// OCR1BL = (_adc/4 );
}
Im
Hauptprogramm werden zweimal Messungen am AD-Wandler ausgeführt, aus
denen dann das gemittelte Signal an die SSB-Funktion übergeben wird.
Diese berechnet die Frequenz df und die Amplitude amp. Die
entsprechenden Registereinstellungen des SI5351 werden mit
si5351.SendPLLBRegisterBulk() übertragen, Dabei werden nicht alle,
sondern nur sehr wenige Register neu beschreiben. Der AD-Wandler wird
jeweils zwischen diesen beiden Funktionen ausgelesen, damit er keine
zusätzliche Zeit braucht. Aus diesem Grund sind die einzelnen Aufrufe
in einer ungewöhnlichen Reihenfolge angeordnet
#define F_SAMP_TX 4476
...
inline int16_t ssb(int16_t in)
{
static int16_t dc;
int16_t i, q;
uint8_t j;
static int16_t v[16];
for(j = 0; j != 15; j++) v[j] = v[j + 1];
dc += (in - dc) / 2;
v[15] = in - dc; // DC decoupling
i = v[7];
q = ((v[0] - v[14]) * 2 + (v[2] - v[12]) * 8 + (v[4] - v[10]) * 21 + (v[6] - v[8]) * 15) / 128 + (v[6] - v[8]) / 2;
// Hilbert transform, 40dB side-band rejection in 400..1900Hz (@4kSPS) when used in image-rejection scenario; (Hilbert transform require 5 additional bits)
uint16_t _amp = magn(i, q);
_amp = _amp << (drive);
amp = lut[_amp];
static int16_t prev_phase;
int16_t phase = arctan3(q, i);
int16_t dp = phase - prev_phase; // phase difference and restriction
prev_phase = phase;
if(dp < 0) dp = dp + _UA; // make negative phase shifts positive: prevents negative frequencies and will reduce spurs on other sideband
if(mode == USB)
return dp * ( F_SAMP_TX / _UA); // calculate frequency-difference based on phase-difference
else
return dp * (-F_SAMP_TX / _UA);
}
Die
Funktion ssb enthält die eigentliche Signalverarbeitung. Ich musste die
Konstante #define F_SAMP_TX 4476 anpassen. sie enthält die
Frequenz, mit der die Funktion ssb aufgerufen wird. Weil die Software
für einen Takt von 20 MHz geschrieben wurde, ich aber derzeit nur mit
den 16 MHz meines Arduino arbeite, läuft alles etwas langsamer. Wenn
ich einen Ton von 1000 Hz auf den Eingang gebe, könnte ein zu hoher
oder zu niedriger SSB-Ton herauskommen. Man kann also mit dieser
Einstellung von F_SAMP_TX erreichen, dass man eine höhere oder tieferen
Stimme bekommt. Bei einer korrekten Einstellung wird genau die richte
Frequenz erzeugt, was z.B. für die Verwendung mit FT8 wichtig ist.
Die Amplitude wird über die Lookup-Tabelle lut angepasst. die Tabelle
wird in setup() vorbereitet: Dazu dient eine Geradengleichung, die die
unterste und die oberste Spannung enthält. Die optimalen Einstellungen
hängen von der Kennlinie des Power-FET und von der HF- Gate-Spannung
ab. Bei der unteren Grenze (80) soll der Transistor gerade vollständig
gesperrt sein. An der oberen Grenze der Vorspannung (220) soll die
maximale Leistung am Ausgang stehen. Ob die Übertragungskennlinie
dazwischen wirklich gerade wird, spielt keine so große Rolle. Kleine
Fehler erzeugen kaum zusätzliche Verzerrungen.
static uint8_t pwm_min = 80; // PWM value for which PA reaches its minimum: 29 when C31 installed; 0 when C31 removed; 0 for biasing BS170 directly
static uint8_t pwm_max = 220; // PWM value for which PA reaches its maximum: 96 when C31 installed; 255 when C31 removed; 220 for biasing BS170 directly
for(uint16_t i = 0; i != 256; i++) // refresh LUT based on pwm_min, pwm_max
lut[i] = (float)i / ((float)255 / ((float)pwm_max - (float)pwm_min)) + pwm_min;
Soweit
funktioniert es nun. Ich habe ein USB-Testsignal auf 3610 kHz erzeugt
und mit einem zweiten SDR abgehört. Das Spektrum zeigt eine
vollständige Unterdrückung des Träges und des unteren Seitenbands. Das
Signal ist gut verständlich. Allerdings ist die Bandbreite etwas
reduziert. Die höchsten übertragenden Frequenzen liegen bei 2,2 kHz.
Was darüber liegt, wird nach unten geklappt und erzeugt entsprechende
Verzerrungen. Für ernsthafte Anwendungen sollte der ATmega also einen
Quarz mit 20 MHz bekommen.
SSB-Versuche und Sample Rate
Inzwischen habe ich versucht, meinen QRP-FT8-Transceiver als SSB-Sender
umzurüsten. Der Arduino-Nano hat dazu eine Quarzfassung bekommen. So
kann ich den Arduino mit 16 MHz laden und dann wahlweise auch mit 20
MHz betreiben. Allerdings waren meine Versuche nicht sehr erfolgreich.
Der Knackpunkt war die I2C-Datenübertraghung zum SI5351. Ich musste
beim Betrieb mit 20 MHz mit der Einstellung #define
I2C_DELAY 8 die Übertragung langsamer machen, damit es
überhaupt funktioniert. Aber auch dann kam es noch zu starken
Verzerrungen, die sich letztlich auf I2C-Übertragungsfehler
zurückführen ließen.
Die Ursache des Problems liegt auf dem SI5351-Board von Adafruit. Es
verwendet Pegelwandler zwischen 5 V und 3,3 V, die aber nicht schnell
genug sind. Auf dem Elektor-SDR-Shield verwende ich dagegen eine
direkte Verbindung ohne Pegelwandler, weil der I2C-Bus ohnehin schon
mit unterschiedlichen Pegeln klarkommt. Im Schaltungsvorschlag von
Guido wird der SI5351 von 5 V aus mit zwei Si-Dioden in Serie
verwendet, also mit ca. 3,6 V bis 3,8 V. Und die I2C-Pullups sind mit 1
k ungewöhnlich klein, was der Geschwindigkeit zugutekommt. Der
QRP-Transceiver mit seinem Adafrquit Board macht also Probleme. Und am
Schluss ist mir bei den Versuchen auch noch die Endstufe durchgebrannt.
Deshalb bin ich erst einmal zu meinem ersten Aufbau zurückgehkehrt, Ein
Versuch hat gezeigt, dass ich die I2C-Übertragung mit dem
Elektor-SDR-Shield sogar noch schneller machen kann. Bei 16 MHz
funktioniert noch die Einstellung #define I2C_DELAY 2; Damit wird
eine Umsetzrate von ca. 5 kHz erreicht: #define F_SAMP_TX 5000
Als weitere Verbesserung hat es sich bewährt, das Modulationssignal aus
vier Einzelmessungen zu mitteln. Genauer gesagt wird nur dreimal
gemessen, aber viermal ausgelesen. Das ermöglicht den zeitlichen
Versatz zwischen Start einer Messung und dem Auslesen. Die AD-Wandlung
läuft dann parallel zu den anderen drei zeitaufwendigen
Arbeitsschritten. Das Mitteln arbeitet wie ein Oversampling mit
Tiefpassfilter, sodass Störungen durch Signale außerhalb der
Übertragungsbandbreite reduziert werden.
void loop(){
int16_t adc; // current ADC sample 10-bits analog input, NOTE: first ADCL, then ADCH
int16_t _adc;
ADCSRA |= (1 << ADSC);
si5351.SendPLLBRegisterBulk(); // submit frequency registers to SI5351 over 731kbit/s I2C (transfer takes 64/731 = 88us, then PLL-loopfilter probably needs 50us to stabalize)
OCR1BL = amp; // submit amplitude to PWM register (takes about 1/32125 = 31us+/-31us to propagate) -> amplitude-phase-alignment error is about 30-50us
adc = ADC;
int16_t df = ssb(adc); // convert analog input into phase-shifts (carrier out by periodic frequency shifts)
adc += ADC;
adc += ADC;
ADCSRA |= (1 << ADSC);
si5351.freq_calc_fast(df); // calculate SI5351 registers based on frequency shift and carrier frequency
adc += ADC; // 4-mal Messen und Mitteln
_adc = (adc/4 - 512);
}
Mit diesen Änderungen wurde ein deutlich besseres SSB-Signal erzeugt.
Weil ich den Deutschlandfunk über UKW als Modulationsquelle verwendet
habe, konnte ich hören, wie das Verfahren mit unterschiedlichen Stimmen
klarkommt. Bei sehr tiefen Stimmen gab es Probleme mit zusätzlichen
Verzerrungen. Hohe Frauenstimmen kamen ebenfalls undeutlicher rüber. Am
besten schnitt eine relativ hohe, männliche Stimme ab. Dabei spielte es
keine große Rolle, ob die Sample-Rate genau angegeben wurde. Es schien
mir sogar, dass es sinnvoll sein kann, die Stimme im Interesse einer
deutlichen Übertragung künstlich etwas höher zu machen, was ja bei
diesem Verfahren durch Angabe einer größeren Konstante F_SAMP_TX leicht
machbar ist. Im Spektrum sieht man nun ein brauchbares Signal
ohne große Nebenausstrahlungen.
Eine Hörprobe
DL2MAN zeigt mit seinem uSDX SSB transmit demo, welcher Klang mit dem Verfahren erreicht werden kann.