SSB-Sender per DSP-Software   

Elektronik-Labor  Lernpakete  Projekte  HF  



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


https://youtu.be/PzubNCuGvEQ

DL2MAN zeigt mit seinem uSDX SSB transmit demo, welcher Klang mit dem Verfahren erreicht werden kann.





Elektronik-Labor  Lernpakete  Projekte  HF