diff --git a/.gitignore b/.gitignore index 30753e7d..3590c162 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ tests/build/ utils/regression_test.js node_modules/ +venv/ .doctrees/ figure-generating-scripts/fm_rds_250k_1Msamples.iq?raw=true @@ -41,4 +42,4 @@ figure-generating-scripts/fm_rds_250k_1Msamples.iq figure-generating-scripts/fm.wav _templates/patrons.html _spelling/ -Radar-2025-Labs/ \ No newline at end of file +Radar-2025-Labs/ diff --git a/Makefile b/Makefile index cb639aa2..482163f2 100644 --- a/Makefile +++ b/Makefile @@ -74,6 +74,13 @@ html: sed -i 's/value="Go"/value="Search"/g' $(BUILDDIR)/*/*.html @echo file://wsl.localhost/Ubuntu-22.04-New/home/marc/PySDR/_build/index.html +.PHONY: html-de +html-de: + $(SPHINXBUILD) -b html -D project="Ein Guide zu SDR und DSV mit Python" -D exclude_patterns=_build,index.rst,content/*,index-nl.rst,content-nl/*,index-fr.rst,content-fr/*,index-ukraine.rst,content-ukraine/*,index-zh.rst,content-zh/*,index-ja.rst,content-ja/* -D master_doc=index-de $(EXTENSIONS) . $(BUILDDIR)/de/ + @echo + @echo "German Build finished. The HTML pages are in $(BUILDDIR)/de/html." + + .PHONY: html-es html-es: $(SPHINXBUILD) -b html -D project="PySDR: Guia de uso para SDR/DSP con Python" -D exclude_patterns=_build,index.rst,content/*,index-nl.rst,content-nl/*,index-fr.rst,content-fr/*,index-ukraine.rst,content-ukraine/*,index-zh.rst,content-zh/*,index-ja.rst,content-ja/* -D master_doc=index-es $(EXTENSIONS) . $(BUILDDIR)/es/ diff --git a/README.md b/README.md index a7130685..9e43f341 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,19 @@

-[PySDR: A Guide to SDR and DSP using Python](https://pysdr.org) is a guide to software-defined radio (SDR) and RF signal processing using Python code examples, live at https://pysdr.org. It is a free online textbook that provides a gentle introduction to wireless communications and SDR using an abundance of diagrams, animations, and code examples. From FFTs to filters to digital modulation to receiving and transmitting from SDRs in Python, PySDR has you covered! This repo specifically contains the source content used to generate the textbook, including the body text and Python scripts to generate the figures. For questions/comments/suggestions feel free to submit an issue at the top of this page, or if you want to propose a change to the textbook (e.g. fix or improvement), you can use a Pull Request. Those who submit valuable feedback/fixes be permanently added to the acknowledgments section. Not good at Git but have changes to suggest? Feel free to email Marc at marc@pysdr.org. +[PySDR: Ein Leitfaden zu SDR und DSP mit Python](https://pysdr.org) ist ein Leitfaden zur softwaredefinierten Funktechnik (SDR) und HF-Signalverarbeitung mit Python-Codebeispielen, verfügbar unter https://pysdr.org. Es handelt sich um ein kostenloses Online-Lehrbuch, das eine sanfte Einführung in drahtlose Kommunikation und SDR bietet – mit zahlreichen Diagrammen, Animationen und Codebeispielen. Von FFTs über Filter bis hin zu digitaler Modulation sowie dem Empfangen und Senden über SDRs in Python – PySDR hat alles abgedeckt! Dieses Repository enthält speziell den Quellinhalt zur Generierung des Lehrbuchs, einschließlich des Haupttexts und der Python-Skripte zur Erzeugung der Abbildungen. Bei Fragen, Anmerkungen oder Vorschlägen können Sie oben auf dieser Seite ein Issue einreichen. Wenn Sie eine Änderung am Lehrbuch vorschlagen möchten (z. B. eine Korrektur oder Verbesserung), können Sie einen Pull Request verwenden. Wer wertvolles Feedback oder Korrekturen einreicht, wird dauerhaft im Danksagungsabschnitt aufgeführt. Nicht vertraut mit Git, aber Änderungen vorschlagen? Schreiben Sie Marc gerne eine E-Mail an marc@pysdr.org. -You can also support PySDR through the [PySDR Patreon page](https://www.patreon.com/c/PySDR) or a [one-time donation](https://www.paypal.com/donate/?hosted_button_id=FH3LQCJRUVPWL). +Sie können PySDR auch über die [PySDR Patreon-Seite](https://www.patreon.com/c/PySDR) oder eine [Einmalspende](https://www.paypal.com/donate/?hosted_button_id=FH3LQCJRUVPWL) unterstützen. -## Building +## Erstellen -Note that the website is now automatically built and deployed with each push/merge into master branch, using the GitHub action [build-and-deploy.yml](https://github.com/777arc/PySDR/blob/master/.github/workflows/build-and-deploy.yml) and the GitHub pages system for hosting the actual textbook. +Die Website wird nun bei jedem Push/Merge in den Master-Branch automatisch erstellt und bereitgestellt, mithilfe der GitHub-Aktion [build-and-deploy.yml](https://github.com/777arc/PySDR/blob/master/.github/workflows/build-and-deploy.yml) und dem GitHub Pages-System zum Hosten des eigentlichen Lehrbuchs. -For testing changes to the textbook locally, you can build using the following steps: +Um Änderungen am Lehrbuch lokal zu testen, kann mit folgenden Schritten gebaut werden: ### Ubuntu/Debian -Look at `.github/workflows/build-and-deploy.yml` and run the apt/pip installs, then: +Schauen Sie sich `.github/workflows/build-and-deploy.yml` an und führen Sie die apt/pip-Installationen aus, dann: ```bash make html @@ -27,35 +27,36 @@ make html-zh make html-ja ``` -In _build there should be an index.html that represents the main page of the English site +In `_build` sollte eine `index.html` vorhanden sein, die die Hauptseite der englischen Website darstellt. -Note: on one machine I had to add `~/.local/bin` to PATH +Hinweis: Auf einem Rechner musste ich `~/.local/bin` zum PATH hinzufügen. ### Windows -Install pre-requisite software with: +Installieren Sie die erforderliche Software wie folgt: -1. From the Microsoft Store install Python 3.10 (3.8-3.12 is fine too if you already have it installed). -1. In a PowerShell terminal (click start menu then type powershell, or open a terminal in VSCode) run `pip install sphinx sphinxcontrib-tikz patreon setuptools` -1. `cd` to the directory you cloned PySDR +1. Installieren Sie Python 3.10 aus dem Microsoft Store (3.8–3.12 ist ebenfalls in Ordnung, falls bereits installiert). +1. Führen Sie in einem PowerShell-Terminal (Startmenü öffnen, dann „powershell" eingeben, oder ein Terminal in VSCode öffnen) den Befehl `pip install sphinx sphinxcontrib-tikz patreon setuptools` aus. +1. Wechseln Sie mit `cd` in das Verzeichnis, in das Sie PySDR geklont haben. -Build the English version only using: +Erstellen Sie nur die englische Version mit: ``` python -m sphinx.cmd.build -b html . _build ``` -The first time running this it might take a while because it has to download LaTeX packages. +Beim ersten Ausführen kann dies etwas länger dauern, da LaTeX-Pakete heruntergeladen werden müssen. + +Testen Sie den JavaScript-Teil mit folgendem Befehl, um CORS-Fehler zu vermeiden: -Test the javascript part with the following to avoid CORS errors: ``` cd _build python -m http.server ``` -## Creating a PDF Export +## PDF-Export erstellen -Not fully working yet due to animated gifs, they all need to be removed for this to not error out: +Noch nicht vollständig funktionsfähig aufgrund animierter GIFs – diese müssen alle entfernt werden, damit kein Fehler auftritt: ``` sudo apt-get install -y latexmk @@ -63,15 +64,15 @@ sphinx-build -b latex . _build/latex make latexpdf ``` -## Misc +## Sonstiges -Ideas for future chapters: +Ideen für zukünftige Kapitel: -* Equalization, would be the last step needed to finish the end-to-end communications link -* OFDM, simulating OFDM and CP, show via Python how it turns freq selective fading into flat fading -* How to create real-time SDR apps with GUIs in Python using pyqt and pyqtgraph, or even just matplotlib with updating -* Python code that lets the Pluto (or RTL-SDR) act as an FM receiver, like with sound output -* End-to-end example that shows how to detect start of packet and other concepts not covered in RDS chapter -* Intro to radar +* Entzerrung – wäre der letzte fehlende Schritt zum Abschluss der Ende-zu-Ende-Kommunikationsstrecke +* OFDM – Simulation von OFDM und CP, Darstellung per Python, wie frequenzselektives Fading in flaches Fading umgewandelt wird +* Erstellung von Echtzeit-SDR-Anwendungen mit GUIs in Python mithilfe von pyqt und pyqtgraph oder sogar mit aktualisierendem matplotlib +* Python-Code, der den Pluto (oder RTL-SDR) als UKW-Empfänger mit Audioausgabe betreibt +* End-to-End-Beispiel, das zeigt, wie der Paketanfang erkannt wird und andere Konzepte, die im RDS-Kapitel nicht behandelt werden +* Einführung in Radar -Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 Unported License. +Creative Commons Lizenz
Dieses Werk ist lizenziert unter einer Creative Commons Namensnennung-NichtKommerziell-WeitergebeUnterGleichenBedingungen 4.0 International Lizenz. diff --git a/_images_de/2d_array_2d_doa_plot.svg b/_images_de/2d_array_2d_doa_plot.svg new file mode 100644 index 00000000..55a72744 --- /dev/null +++ b/_images_de/2d_array_2d_doa_plot.svg @@ -0,0 +1,1005 @@ + + + + + + + + 2025-06-20T02:09:40.562096 + image/svg+xml + + + Matplotlib v3.10.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Theta (Azimut, Grad) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Phi (Elevation, Grad) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leistung [linear] + + + + + + + + + + + + diff --git a/_images_de/2d_array_3d_doa_plot.png b/_images_de/2d_array_3d_doa_plot.png new file mode 100644 index 00000000..b964c6f6 Binary files /dev/null and b/_images_de/2d_array_3d_doa_plot.png differ diff --git a/_images_de/2d_array_eigenvalues.svg b/_images_de/2d_array_eigenvalues.svg new file mode 100644 index 00000000..b5d092e0 --- /dev/null +++ b/_images_de/2d_array_eigenvalues.svg @@ -0,0 +1,766 @@ + + + + + + + + 2025-06-20T02:09:33.187766 + image/svg+xml + + + Matplotlib v3.10.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Eigenwert [dB] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/2d_array_element_positions.svg b/_images_de/2d_array_element_positions.svg new file mode 100644 index 00000000..d2c8f6c1 --- /dev/null +++ b/_images_de/2d_array_element_positions.svg @@ -0,0 +1,1187 @@ + + + + + + + + 2025-06-20T02:09:31.822119 + image/svg+xml + + + Matplotlib v3.10.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + X-Position [m] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Y-Position [m] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Z-Position [m] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/2d_beamforming_2dplot.svg b/_images_de/2d_beamforming_2dplot.svg new file mode 100644 index 00000000..55f94964 --- /dev/null +++ b/_images_de/2d_beamforming_2dplot.svg @@ -0,0 +1,688 @@ + + + + + + + + 2025-09-29T11:46:51.866441 + image/svg+xml + + + Matplotlib v3.10.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Azimutwinkel [Grad] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Elevationswinkel [Grad] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leistung [linear] + + + + + + + + + + + + + diff --git a/_images_de/2d_beamforming_element_pos.svg b/_images_de/2d_beamforming_element_pos.svg new file mode 100644 index 00000000..a6533b64 --- /dev/null +++ b/_images_de/2d_beamforming_element_pos.svg @@ -0,0 +1,534 @@ + + + + + + + + 2025-06-20T03:29:09.142490 + image/svg+xml + + + Matplotlib v3.10.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + X-Position [m] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Z-Position [m] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/2d_beamforming_ula.svg b/_images_de/2d_beamforming_ula.svg new file mode 100644 index 00000000..0eee40dc --- /dev/null +++ b/_images_de/2d_beamforming_ula.svg @@ -0,0 +1,520 @@ + + + + + + + + 2025-06-20T03:22:53.742152 + image/svg+xml + + + Matplotlib v3.10.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + X-Position [m] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Y-Position [m] + + + + + + + + + + + + + + + + + + + + + + + + + Theta (20 Grad) + + + + + + + + diff --git a/_images_de/64qam.png b/_images_de/64qam.png new file mode 100644 index 00000000..f731ba55 Binary files /dev/null and b/_images_de/64qam.png differ diff --git a/_images_de/ASK.svg b/_images_de/ASK.svg new file mode 100644 index 00000000..ba9b9c06 --- /dev/null +++ b/_images_de/ASK.svg @@ -0,0 +1,2007 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Unsere Daten + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Drahtloses Signal + + + + + + + + + + + diff --git a/_images_de/AntSDR.png b/_images_de/AntSDR.png new file mode 100644 index 00000000..8bbaa6f6 Binary files /dev/null and b/_images_de/AntSDR.png differ diff --git a/_images_de/AntSDR_Comparison.jpg b/_images_de/AntSDR_Comparison.jpg new file mode 100644 index 00000000..54a2b9c6 Binary files /dev/null and b/_images_de/AntSDR_Comparison.jpg differ diff --git a/_images_de/AntSDR_E200_block_diagram.png b/_images_de/AntSDR_E200_block_diagram.png new file mode 100644 index 00000000..9c79bfa7 Binary files /dev/null and b/_images_de/AntSDR_E200_block_diagram.png differ diff --git a/_images_de/AntSDR_E310.png b/_images_de/AntSDR_E310.png new file mode 100644 index 00000000..1f0fc1a8 Binary files /dev/null and b/_images_de/AntSDR_E310.png differ diff --git a/_images_de/Costas_loop_model.svg b/_images_de/Costas_loop_model.svg new file mode 100644 index 00000000..d1e986f6 --- /dev/null +++ b/_images_de/Costas_loop_model.svg @@ -0,0 +1,425 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + φ(t) + + +u(t) = m(t)sin(ωt) +data m(t) + + + + + + + + + + + + +cos( ωt) +sin( ωt) +g(t) +m +(t) +2 + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_images_de/DOA_with_training.svg b/_images_de/DOA_with_training.svg new file mode 100644 index 00000000..b3f8fa08 --- /dev/null +++ b/_images_de/DOA_with_training.svg @@ -0,0 +1,1013 @@ + + + + + + + + 2024-05-03T21:52:08.046720 + image/svg+xml + + + Matplotlib v3.8.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Winkel [Grad] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Betrag [dB] + + + + + + + + + + + + + + + + + + + + + + + Strahlmuster und DOA-Ergebnisse, mit Training + + + + + + + + diff --git a/_images_de/DOA_without_training.svg b/_images_de/DOA_without_training.svg new file mode 100644 index 00000000..3251a4bd --- /dev/null +++ b/_images_de/DOA_without_training.svg @@ -0,0 +1,724 @@ + + + + + + + + 2024-05-03T21:33:04.215987 + image/svg+xml + + + Matplotlib v3.8.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Winkel [Grad] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Betrag [dB] + + + + + + + + + + + + + + + + + + + + + + + A + B + C + diff --git a/_images_de/DOA_without_training_pattern.svg b/_images_de/DOA_without_training_pattern.svg new file mode 100644 index 00000000..9727dfc7 --- /dev/null +++ b/_images_de/DOA_without_training_pattern.svg @@ -0,0 +1,1022 @@ + + + + + + + + 2024-05-03T21:41:46.411092 + image/svg+xml + + + Matplotlib v3.8.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Winkel [Grad] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Betrag [dB] + + + + + + + + + + + + + + + + + + + + + + + Strahlmuster und DOA-Ergebnisse, ohne Training + + + + + + + + diff --git a/_images_de/FIR_IIR.png b/_images_de/FIR_IIR.png new file mode 100644 index 00000000..a4060ac3 Binary files /dev/null and b/_images_de/FIR_IIR.png differ diff --git a/_images_de/IQ3.gif b/_images_de/IQ3.gif new file mode 100644 index 00000000..ca44078f Binary files /dev/null and b/_images_de/IQ3.gif differ diff --git a/_images_de/IQEngine_from_Maia.png b/_images_de/IQEngine_from_Maia.png new file mode 100644 index 00000000..0afa1057 Binary files /dev/null and b/_images_de/IQEngine_from_Maia.png differ diff --git a/_images_de/IQ_diagram.png b/_images_de/IQ_diagram.png new file mode 100644 index 00000000..378fb551 Binary files /dev/null and b/_images_de/IQ_diagram.png differ diff --git a/_images_de/IQ_diagram_rx.png b/_images_de/IQ_diagram_rx.png new file mode 100644 index 00000000..1fbc15cc Binary files /dev/null and b/_images_de/IQ_diagram_rx.png differ diff --git a/_images_de/IQ_wave.png b/_images_de/IQ_wave.png new file mode 100644 index 00000000..9db7eb19 Binary files /dev/null and b/_images_de/IQ_wave.png differ diff --git a/_images_de/Maia.png b/_images_de/Maia.png new file mode 100644 index 00000000..7602f359 Binary files /dev/null and b/_images_de/Maia.png differ diff --git a/_images_de/SNR.png b/_images_de/SNR.png new file mode 100644 index 00000000..a18ca638 Binary files /dev/null and b/_images_de/SNR.png differ diff --git a/_images_de/SNR2.png b/_images_de/SNR2.png new file mode 100644 index 00000000..2ce11535 Binary files /dev/null and b/_images_de/SNR2.png differ diff --git a/_images_de/Spherical_Coordinates.svg b/_images_de/Spherical_Coordinates.svg new file mode 100644 index 00000000..870f4295 --- /dev/null +++ b/_images_de/Spherical_Coordinates.svg @@ -0,0 +1,421 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + θ + (Azimut) + ϕ + (Elevation) + + x + + + x + + + y + + - + + + + -90° + + + - + + + + + +90° + + +90° + + z + + + + + + + + + + diff --git a/_images_de/ad9361.svg b/_images_de/ad9361.svg new file mode 100644 index 00000000..ebdc7f53 --- /dev/null +++ b/_images_de/ad9361.svg @@ -0,0 +1,5138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RxA + RxB + RxC + + + + + + + + Basisband + + GPO + Empfangskanal 1 + Empfangskanal 2 + + 12-bit + + + + + + + + + + Sendekanal 1 + Sendekanal 2 + + SPI + + Reset + + CTRL + + + + DIV + + + + + + TxMon + + + + Sheet.256 + + + + + + + + + + Kalibrierung undKorrektur + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 70MHz - 6GHz + + Tx + Rx + + + + + Ch1 I/Q + Ch2 I/Q + Ch1 I/Q + Ch2 I/Q + + + TemperaturSensor + + + Sheet.256 + + + + + + + + + + + + + + + + + Sheet.256 + + + + + + + 70MHz - 6GHz + Rx + Tx + + DIV + + DIV + + + + + + + + + + + Rx 61.44 MSPS + + Zustandsmaschine + + + + + + + + TxA + + + + + TxB + + + + + + + + AD9361 + + + DIV + + + + + GND + + + + + + + + + + + + Dual10-bit + + + + + + AutomatischeVerstärkungControl + □ Manual□ Slow□ Fast + + + + + + + + + + 25 - 640 MSPS + + + FIR + + HB2 + + HB1 + + + GAIN + + HB3 + I + + + + + + + + + + + + + ADC + + + + + + + HF-Kanalbandbreite + 200kHz - 56MHz (I/Q) + ÷1÷2÷3 + ÷1÷2 + ÷1÷2 + ÷1÷2÷4 + + + + + + + + + + + + + FIR + + HB2 + + HB1 + + + GAIN + + HB3 + Q + + + + + + + + + + + ADC + + + PhaseTeiler + + Sheet.256 + + + + + + + Rx + Tx + + + + + Empfänger-DezimationDigitale Filterung und Entzerrung + + + + + + + + + + + + + + + + + + HF-Kanalbandbreite + Sender-InterpolationDigitale Filterung und Entzerrung + 200kHz - 56MHz (I/Q) + 1x2x3x + 1x2x + 1x2x + 1x2x4 x + I + Q + + + FIR + + HB1 + + HB2 + + HB3 + + DAC + + + + + + + + + + + + + + + + + + + PhaseTeiler + + + + + + + + + + + + + + + + + + 320 MSPS + + + FIR + + HB1 + + HB2 + + HB3 + + DAC + + + + Sheet.256 + + + + + + + + Eingangs-Mux + + AUX DAC + + AUX ADC + + LNA + TIA + TIA + ATTN + + Sheet.256 + + + + + + + Ausgangs-Mux + + + + + + + + + + Sheet.256 + + + + + + + + + CMOS / LVDS INTERFACE + + + + + + Tx 61.44 MSPS + + + LOOPBACK + + PN &BIST + + Sheet.256 + + + + + + + + + + + + + + 715 MHz - 1430 MHz + + DCXO + + + VDD_GPO + VDD_INTERFACE + VDD_MAIN + RX2A_P,RX2A_NRX1A_P,RX1A_N + RX2B_P,RX2B_NRX1B_P,RX1B_N + RX2C_P,RX2C_NRX1C_P,RX1C_N + TXMON2 + TXMON1 + RXLO + TXLO + SPI + CTRL + AUXDAC1AUXDAC2 + TX2A_P,TX2A_NTX1A_P,TX1A_N + TX2B_P,TX2B_NTX1B_P,TX1B_N + AUXADC + XTALP + XTALN + RADIOSWITCHING + RESETB + P0_[D11:D0]/TX_[D5:D0]P1_[D11:D0]/RX_[D5:D0] + GND + 1.8 - 3.3V + 1.2V - 2.5V + 1.3 V + + diff --git a/_images_de/adaptive_mcs.svg b/_images_de/adaptive_mcs.svg new file mode 100644 index 00000000..d34f44f7 --- /dev/null +++ b/_images_de/adaptive_mcs.svg @@ -0,0 +1 @@ +64QAM16QAMQPSK \ No newline at end of file diff --git a/_images_de/adaptive_mcs2.svg b/_images_de/adaptive_mcs2.svg new file mode 100644 index 00000000..4a6457c1 --- /dev/null +++ b/_images_de/adaptive_mcs2.svg @@ -0,0 +1,219 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + Durchsatt + + + SNRQPSK +8PSK +16QAM +64QAM + + + + + + Verwendetes Modulationsschema + + + + + + + + + 64QAM16QAM +8PSK +QPSK + + + + diff --git a/_images_de/adi-adalm-pluto-diagram-large.jpg b/_images_de/adi-adalm-pluto-diagram-large.jpg new file mode 100644 index 00000000..77bdf94d Binary files /dev/null and b/_images_de/adi-adalm-pluto-diagram-large.jpg differ diff --git a/_images_de/adsb.jpg b/_images_de/adsb.jpg new file mode 100644 index 00000000..22e107a1 Binary files /dev/null and b/_images_de/adsb.jpg differ diff --git a/_images_de/am_fm_animation.gif b/_images_de/am_fm_animation.gif new file mode 100644 index 00000000..f97e07e5 Binary files /dev/null and b/_images_de/am_fm_animation.gif differ diff --git a/_images_de/amplitude_phase_period.svg b/_images_de/amplitude_phase_period.svg new file mode 100644 index 00000000..b901b6de --- /dev/null +++ b/_images_de/amplitude_phase_period.svg @@ -0,0 +1,596 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Periode (1/Frequenz) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/analog_digital_filter.png b/_images_de/analog_digital_filter.png new file mode 100644 index 00000000..a93616df Binary files /dev/null and b/_images_de/analog_digital_filter.png differ diff --git a/_images_de/analog_digital_filter_nolabel.png b/_images_de/analog_digital_filter_nolabel.png new file mode 100644 index 00000000..684e34c9 Binary files /dev/null and b/_images_de/analog_digital_filter_nolabel.png differ diff --git a/_images_de/antenna_gain_patterns.png b/_images_de/antenna_gain_patterns.png new file mode 100644 index 00000000..22cda829 Binary files /dev/null and b/_images_de/antenna_gain_patterns.png differ diff --git a/_images_de/antenna_steering.png b/_images_de/antenna_steering.png new file mode 100644 index 00000000..a1fa085b Binary files /dev/null and b/_images_de/antenna_steering.png differ diff --git a/_images_de/ask2.svg b/_images_de/ask2.svg new file mode 100644 index 00000000..d5ea534e --- /dev/null +++ b/_images_de/ask2.svg @@ -0,0 +1,1416 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/ask3.svg b/_images_de/ask3.svg new file mode 100644 index 00000000..aaaefb16 --- /dev/null +++ b/_images_de/ask3.svg @@ -0,0 +1,1616 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Unsere Daten + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Drahtloses Signal + + + + + + + + + + + diff --git a/_images_de/ask_set.png b/_images_de/ask_set.png new file mode 100644 index 00000000..322baef8 Binary files /dev/null and b/_images_de/ask_set.png differ diff --git a/_images_de/atmospheric_attenuation.svg b/_images_de/atmospheric_attenuation.svg new file mode 100644 index 00000000..6761ebf1 --- /dev/null +++ b/_images_de/atmospheric_attenuation.svg @@ -0,0 +1,1498 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + 10 + + + 100 + + + Frequenz [GHz] + + + + + + + + + + + + + + + + + + + + + 0.001 + + + 0.01 + + + 0.1 + + + 1 + + + 10 + + + 100 + + + 1000 + + + Dämpfung [dB / km] + + + + + H + + + 2 + + + O + + + + + + + + + + + + + O + + + 2 + + + + + + + + + + + + + O + + + 2 + + + + + + + + + + + + + H + + + 2 + + + O + + + + + + + + + + + + + H + + + 2 + + + O + + + + + + + + + + 2 + + + 3 + + + 4 + + + 5 + + + 6 + + + 7 + + 20 + 30 + 40 + 50 + 200 + 300 + diff --git a/_images_de/audio_equalizer.webp b/_images_de/audio_equalizer.webp new file mode 100644 index 00000000..7a30df01 Binary files /dev/null and b/_images_de/audio_equalizer.webp differ diff --git a/_images_de/bandpass_filter_freq.svg b/_images_de/bandpass_filter_freq.svg new file mode 100644 index 00000000..df4bdc6d --- /dev/null +++ b/_images_de/bandpass_filter_freq.svg @@ -0,0 +1,522 @@ + + + + + + + + 2022-02-22T01:21:16.475065 + image/svg+xml + + + Matplotlib v3.5.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [kHz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenzgang des Filters + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/bandpass_filter_taps.svg b/_images_de/bandpass_filter_taps.svg new file mode 100644 index 00000000..4cf6c410 --- /dev/null +++ b/_images_de/bandpass_filter_taps.svg @@ -0,0 +1,4536 @@ + + + + + + + + 2022-02-22T01:20:57.728303 + image/svg+xml + + + Matplotlib v3.5.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sample + Koeffizienten + diff --git a/_images_de/barker-code.svg b/_images_de/barker-code.svg new file mode 100644 index 00000000..99061d29 --- /dev/null +++ b/_images_de/barker-code.svg @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/baseband_bandpass.png b/_images_de/baseband_bandpass.png new file mode 100644 index 00000000..9c3354b9 Binary files /dev/null and b/_images_de/baseband_bandpass.png differ diff --git a/_images_de/beamforming_examples.svg b/_images_de/beamforming_examples.svg new file mode 100644 index 00000000..8070eb72 --- /dev/null +++ b/_images_de/beamforming_examples.svg @@ -0,0 +1 @@ +ELM-2084Israeli Multi-Mission Radar(AESA/Fully Digital Array)~2005Raytheon’s MIM-104 Patriot Radar(PESA/Analog/Traditional Array)~1969Starlink User Terminal, aka “Dishy”(Hybrid Array)~2022Subarray \ No newline at end of file diff --git a/_images_de/beamforming_taxonomy.svg b/_images_de/beamforming_taxonomy.svg new file mode 100644 index 00000000..d9c6fadf --- /dev/null +++ b/_images_de/beamforming_taxonomy.svg @@ -0,0 +1 @@ +Konventioneller Strahlformer(auch: Delay-and-Sum)MVDRCaponDMI/SMIFrostRLSLMSOLSBSSTechniquesICA basedConstantModulusGeschaltete Strahlung(aka Blass/Butler Matrix,primarily for analog arrays)Nebenkeulen-Unterdrücker(Howell Array)Mehrfach-Nebenkeulen-Unterdrücker(Applebaum Array)LCMVDILFASTSCORERoanokeDopplerMUSICESPIRITPiserenkoZerlegung akaNullsteuerungMax SNRMax SINRDynamischer Mehrfach-Nebenkeule-Unterdrücker(Applebaum Array)Mustersynth.Woodward-LawsonTechnikMaxLikelihoodStrahl-formerRäumliches MultiplexingEinfallsrichtung (DOA)Unterraum-basiertBlindTraditionell(Datenunabhängig/Deterministisch)AdaptivBlock-basiert(Snapshot/Update-basiert)(Snapshot/Update-basiert)Fensterfunktionen(Optionale Erweiterung)ChebychevHammingTaylorBinomialEingang beinhaltet(erwarteten) Winkelder NutzquelleBraucht Pilots/exaktesSignalStrahlformungs-TaxonomieRaum--Raum-Zeit-adaptive Verarbeitung (STAP)Die meisten Techniken unterStrahlformungkönnendirekt zur DOA-Bestimmung verwendet werdenDOA \ No newline at end of file diff --git a/_images_de/binary_file.png b/_images_de/binary_file.png new file mode 100644 index 00000000..f56b79e0 Binary files /dev/null and b/_images_de/binary_file.png differ diff --git a/_images_de/bladeRF-2.0-micro-Block-Diagram-4-oscillator.png b/_images_de/bladeRF-2.0-micro-Block-Diagram-4-oscillator.png new file mode 100644 index 00000000..7b41e74f Binary files /dev/null and b/_images_de/bladeRF-2.0-micro-Block-Diagram-4-oscillator.png differ diff --git a/_images_de/bladeRF-2.0-micro-Block-Diagram-4.png b/_images_de/bladeRF-2.0-micro-Block-Diagram-4.png new file mode 100644 index 00000000..b6426139 Binary files /dev/null and b/_images_de/bladeRF-2.0-micro-Block-Diagram-4.png differ diff --git a/_images_de/bladeRF_micro.png b/_images_de/bladeRF_micro.png new file mode 100644 index 00000000..263cd328 Binary files /dev/null and b/_images_de/bladeRF_micro.png differ diff --git a/_images_de/bladerf-waterfall.svg b/_images_de/bladerf-waterfall.svg new file mode 100644 index 00000000..afc77f6c --- /dev/null +++ b/_images_de/bladerf-waterfall.svg @@ -0,0 +1,452 @@ + + + + + + + + 2024-03-14T05:36:44.113286 + image/svg+xml + + + Matplotlib v3.8.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [MHz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeit [s] + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/bpsk.svg b/_images_de/bpsk.svg new file mode 100644 index 00000000..de187127 --- /dev/null +++ b/_images_de/bpsk.svg @@ -0,0 +1,251 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ZeitZeitEingangBitsBPSKmoduliertAusgang + + + diff --git a/_images_de/bpsk2.svg b/_images_de/bpsk2.svg new file mode 100644 index 00000000..dc5f0338 --- /dev/null +++ b/_images_de/bpsk2.svg @@ -0,0 +1 @@ +Zeit \ No newline at end of file diff --git a/_images_de/bpsk3.png b/_images_de/bpsk3.png new file mode 100644 index 00000000..55a6bbbd Binary files /dev/null and b/_images_de/bpsk3.png differ diff --git a/_images_de/bpsk_iq.png b/_images_de/bpsk_iq.png new file mode 100644 index 00000000..c6f2e33b Binary files /dev/null and b/_images_de/bpsk_iq.png differ diff --git a/_images_de/butterfly.svg b/_images_de/butterfly.svg new file mode 100644 index 00000000..db9a5ed2 --- /dev/null +++ b/_images_de/butterfly.svg @@ -0,0 +1 @@ +𝑥0𝑥1𝑦0𝑦1𝑤𝑛𝑘-1 \ No newline at end of file diff --git a/_images_de/butterfly2.svg b/_images_de/butterfly2.svg new file mode 100644 index 00000000..2da2923b --- /dev/null +++ b/_images_de/butterfly2.svg @@ -0,0 +1 @@ +𝑥0𝑥1𝑥2𝑥3𝑥4𝑥5𝑥6𝑥7𝑦0𝑦4𝑦2𝑦6𝑦1𝑦5𝑦3𝑦7𝑤40𝑤41𝑤42𝑤43𝑤20𝑤21𝑤20𝑤21 \ No newline at end of file diff --git a/_images_de/caf_at_correct_alpha.svg b/_images_de/caf_at_correct_alpha.svg new file mode 100644 index 00000000..5c954307 --- /dev/null +++ b/_images_de/caf_at_correct_alpha.svg @@ -0,0 +1,576 @@ + + + + + + + + 2025-06-09T12:57:56.492157 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CAF (Realteil) + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/caf_at_incorrect_alpha.svg b/_images_de/caf_at_incorrect_alpha.svg new file mode 100644 index 00000000..67c1531a --- /dev/null +++ b/_images_de/caf_at_incorrect_alpha.svg @@ -0,0 +1,483 @@ + + + + + + + + 2025-06-09T12:58:07.447113 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CAF (Realteil) + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/caf_avg_over_alpha.svg b/_images_de/caf_avg_over_alpha.svg new file mode 100644 index 00000000..9bb7dae2 --- /dev/null +++ b/_images_de/caf_avg_over_alpha.svg @@ -0,0 +1,700 @@ + + + + + + + + 2025-06-09T13:08:10.415110 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CAF-Leistung + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/carrier-offset.png b/_images_de/carrier-offset.png new file mode 100644 index 00000000..5a50d4d8 Binary files /dev/null and b/_images_de/carrier-offset.png differ diff --git a/_images_de/carrier.png b/_images_de/carrier.png new file mode 100644 index 00000000..08b05934 Binary files /dev/null and b/_images_de/carrier.png differ diff --git a/_images_de/cdma.png b/_images_de/cdma.png new file mode 100644 index 00000000..ba4c07e0 Binary files /dev/null and b/_images_de/cdma.png differ diff --git a/_images_de/central_limit_theorem.svg b/_images_de/central_limit_theorem.svg new file mode 100644 index 00000000..b1e4f4be --- /dev/null +++ b/_images_de/central_limit_theorem.svg @@ -0,0 +1,9670 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/coarse-freq-sync-before.svg b/_images_de/coarse-freq-sync-before.svg new file mode 100644 index 00000000..c69f007b --- /dev/null +++ b/_images_de/coarse-freq-sync-before.svg @@ -0,0 +1,1420 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/coarse-freq-sync.svg b/_images_de/coarse-freq-sync.svg new file mode 100644 index 00000000..0a2aca98 --- /dev/null +++ b/_images_de/coarse-freq-sync.svg @@ -0,0 +1,1514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Vergrößert unten + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/complex_plane_1.png b/_images_de/complex_plane_1.png new file mode 100644 index 00000000..734e59a8 Binary files /dev/null and b/_images_de/complex_plane_1.png differ diff --git a/_images_de/complex_plane_2.png b/_images_de/complex_plane_2.png new file mode 100644 index 00000000..2b6df453 Binary files /dev/null and b/_images_de/complex_plane_2.png differ diff --git a/_images_de/complex_plane_3.png b/_images_de/complex_plane_3.png new file mode 100644 index 00000000..dfa3c3ee Binary files /dev/null and b/_images_de/complex_plane_3.png differ diff --git a/_images_de/complex_taps.png b/_images_de/complex_taps.png new file mode 100644 index 00000000..7e846749 Binary files /dev/null and b/_images_de/complex_taps.png differ diff --git a/_images_de/constellation-animated-postcostas.gif b/_images_de/constellation-animated-postcostas.gif new file mode 100644 index 00000000..72624085 Binary files /dev/null and b/_images_de/constellation-animated-postcostas.gif differ diff --git a/_images_de/constellation-animated.gif b/_images_de/constellation-animated.gif new file mode 100644 index 00000000..b1bc4afa Binary files /dev/null and b/_images_de/constellation-animated.gif differ diff --git a/_images_de/convolution.gif b/_images_de/convolution.gif new file mode 100644 index 00000000..b1c47bb8 Binary files /dev/null and b/_images_de/convolution.gif differ diff --git a/_images_de/convolve_comparison_1000.svg b/_images_de/convolve_comparison_1000.svg new file mode 100644 index 00000000..53a1efb6 --- /dev/null +++ b/_images_de/convolve_comparison_1000.svg @@ -0,0 +1,1563 @@ + + + + + + + + 2024-04-19T18:11:37.382213 + image/svg+xml + + + Matplotlib v3.8.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Anzahl der Koeffizienten + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeit pro Aufruf (ms) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Eingangssignallänge: 1000 Samples + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/convolve_comparison_100000.svg b/_images_de/convolve_comparison_100000.svg new file mode 100644 index 00000000..9a86fcbd --- /dev/null +++ b/_images_de/convolve_comparison_100000.svg @@ -0,0 +1,1572 @@ + + + + + + + + 2024-04-19T18:12:55.749063 + image/svg+xml + + + Matplotlib v3.8.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Anzahl der Koeffizienten + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeit pro Aufruf (ms) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Eingangssignallänge: 100000 Samples + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/costas-loop-freq-tracking.svg b/_images_de/costas-loop-freq-tracking.svg new file mode 100644 index 00000000..aa6d68b9 --- /dev/null +++ b/_images_de/costas-loop-freq-tracking.svg @@ -0,0 +1,1434 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenzversatz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [Hz] + diff --git a/_images_de/costas-loop-output.svg b/_images_de/costas-loop-output.svg new file mode 100644 index 00000000..36df513d --- /dev/null +++ b/_images_de/costas-loop-output.svg @@ -0,0 +1,1631 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Vor Costas-Schleife + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Nach Costas-Schleife + + + + + + + + + + + diff --git a/_images_de/costas-loop.svg b/_images_de/costas-loop.svg new file mode 100644 index 00000000..b0161d2e --- /dev/null +++ b/_images_de/costas-loop.svg @@ -0,0 +1,1865 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/costas_animation.gif b/_images_de/costas_animation.gif new file mode 100644 index 00000000..0212035b Binary files /dev/null and b/_images_de/costas_animation.gif differ diff --git a/_images_de/db.png b/_images_de/db.png new file mode 100644 index 00000000..5d09adfb Binary files /dev/null and b/_images_de/db.png differ diff --git a/_images_de/dc-signal.png b/_images_de/dc-signal.png new file mode 100644 index 00000000..40071294 Binary files /dev/null and b/_images_de/dc-signal.png differ diff --git a/_images_de/dc-signal1.png b/_images_de/dc-signal1.png new file mode 100644 index 00000000..40071294 Binary files /dev/null and b/_images_de/dc-signal1.png differ diff --git a/_images_de/dc_spike.png b/_images_de/dc_spike.png new file mode 100644 index 00000000..b6a950e5 Binary files /dev/null and b/_images_de/dc_spike.png differ diff --git a/_images_de/delay_and_sum.gif b/_images_de/delay_and_sum.gif new file mode 100644 index 00000000..f263640e Binary files /dev/null and b/_images_de/delay_and_sum.gif differ diff --git a/_images_de/destructive_interference.svg b/_images_de/destructive_interference.svg new file mode 100644 index 00000000..8befb2ed --- /dev/null +++ b/_images_de/destructive_interference.svg @@ -0,0 +1,206 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + += + + + diff --git a/_images_de/detection_basic_1.svg b/_images_de/detection_basic_1.svg new file mode 100644 index 00000000..5e1cf585 --- /dev/null +++ b/_images_de/detection_basic_1.svg @@ -0,0 +1,6266 @@ + + + + + + + + 2026-02-03T19:39:10.038436 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sample-Index + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Signalbetrag [linear] + + + + + + + + + + + + + + + + + + + + Wahrer Versatz + + + + + + + + diff --git a/_images_de/detection_basic_2.svg b/_images_de/detection_basic_2.svg new file mode 100644 index 00000000..fc1ad617 --- /dev/null +++ b/_images_de/detection_basic_2.svg @@ -0,0 +1,6112 @@ + + + + + + + + 2026-02-03T19:39:10.127980 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sample-Index + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Korrelationsbetrag [linear] + + + + + + + + + + + + + + + + + + + + Wahrer Versatz + + + + + + + + diff --git a/_images_de/detection_cfar.svg b/_images_de/detection_cfar.svg new file mode 100644 index 00000000..86a81b23 --- /dev/null +++ b/_images_de/detection_cfar.svg @@ -0,0 +1,13442 @@ + + + + + + + + 2026-01-25T21:46:24.426568 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leistung + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Empfangssignalleistung + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sample-Index + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Korrelationsleistung + + + + + + + + + + + + + + + + + + + + Präambel-Korrelationsausgang mit adaptiver CFAR-Schwelle + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CFAR-Adaptivschwell + + + + + + Erkennungen (Präambel gefunden) + + + + + + + + + + + + diff --git a/_images_de/detection_cfar2.svg b/_images_de/detection_cfar2.svg new file mode 100644 index 00000000..6a05522e --- /dev/null +++ b/_images_de/detection_cfar2.svg @@ -0,0 +1,13644 @@ + + + + + + + + 2026-01-25T21:14:25.065560 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leistung + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Empfangssignalleistung + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sample-Index + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Korrelationsleistung + + + + + + + + + + + + + + + + + + + + Präambel-Korrelationsausgang mit adaptiver CFAR-Schwelle + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CFAR-Adaptivschwell + + + + + + Erkennungen (Präambel gefunden) + + + + + + + + + + + + diff --git a/_images_de/detection_dsss.svg b/_images_de/detection_dsss.svg new file mode 100644 index 00000000..5e868647 --- /dev/null +++ b/_images_de/detection_dsss.svg @@ -0,0 +1,979 @@ + + + + + + + + 2026-01-25T21:18:47.096850 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Versatz (Chip-Bruchteil) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Normierter Korrelations-Spitzenwert + + + + + + + + + + + + + + + + + + + + DSSS-Korrelationspeak vs. fraktionaler Chip-Timing-Versatz + + + + + + + + Normierte Korrelation + + + + Perfekte Ausrichtung + + + + + + + + + diff --git a/_images_de/detection_freq_offset.svg b/_images_de/detection_freq_offset.svg new file mode 100644 index 00000000..f1c4c7bc --- /dev/null +++ b/_images_de/detection_freq_offset.svg @@ -0,0 +1,605 @@ + + + + + + + + 2026-01-25T21:16:20.059400 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenzversatz (kHz) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Normierter Korrelationspeak [dB] + + + + + + + + + + + + + + + + + + + + + + + Korrelationsdegradation vs. Frequenzversatz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/detection_freq_offset2.svg b/_images_de/detection_freq_offset2.svg new file mode 100644 index 00000000..0c7566c0 --- /dev/null +++ b/_images_de/detection_freq_offset2.svg @@ -0,0 +1,777 @@ + + + + + + + + 2026-01-25T21:16:26.171610 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Erkennungswahrscheinlichkeit + + + + + + + + + + + + + + + + + + + + + + + Erkennungswahrscheinlichkeit vs. SNR für verschiedene Frequenzversätze + + + + + + + + Versatz=0,0 kHz + + + + Versatz=2,0 kHz + + + + Versatz=5,0 kHz + + + + + + + + + diff --git a/_images_de/detection_gps_2d_map.png b/_images_de/detection_gps_2d_map.png new file mode 100644 index 00000000..ad9ef9d8 Binary files /dev/null and b/_images_de/detection_gps_2d_map.png differ diff --git a/_images_de/detection_gps_code_phase_slice.svg b/_images_de/detection_gps_code_phase_slice.svg new file mode 100644 index 00000000..39028698 --- /dev/null +++ b/_images_de/detection_gps_code_phase_slice.svg @@ -0,0 +1,3351 @@ + + + + + + + + 2026-03-21T02:47:59.745089 + image/svg+xml + + + Matplotlib v3.10.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Codephase (Chips) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Korrelationsleistung + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/detection_gps_spectrogram.svg b/_images_de/detection_gps_spectrogram.svg new file mode 100644 index 00000000..661dbbdb --- /dev/null +++ b/_images_de/detection_gps_spectrogram.svg @@ -0,0 +1,524 @@ + + + + + + + + 2026-03-21T02:24:00.638974 + image/svg+xml + + + Matplotlib v3.10.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [MHz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeit [s] + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/detection_pd_vs_snr.svg b/_images_de/detection_pd_vs_snr.svg new file mode 100644 index 00000000..06fc160a --- /dev/null +++ b/_images_de/detection_pd_vs_snr.svg @@ -0,0 +1,1706 @@ + + + + + + + + 2026-02-03T21:08:16.073111 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ROC-Kurven + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Pd vs. SNR (Pfa=0,01) + + + + + + + + + + + diff --git a/_images_de/detection_realtime.png b/_images_de/detection_realtime.png new file mode 100644 index 00000000..fd8541a5 Binary files /dev/null and b/_images_de/detection_realtime.png differ diff --git a/_images_de/differential_coding.svg b/_images_de/differential_coding.svg new file mode 100644 index 00000000..83474afc --- /dev/null +++ b/_images_de/differential_coding.svg @@ -0,0 +1,1292 @@ + + + + + + + + 2022-08-24T10:26:15.651172 + image/svg+xml + + + Matplotlib v3.5.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Symbole + Kodiert + Dekodiert + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Beginnend mit 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Symbole + Kodiert + Dekodiert + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Beginnend mit 0 + + + + + + + + + + + diff --git a/_images_de/differential_coding2.svg b/_images_de/differential_coding2.svg new file mode 100644 index 00000000..4005f5ec --- /dev/null +++ b/_images_de/differential_coding2.svg @@ -0,0 +1,321 @@ + + + + + + + + + + + Verzögerung + Eingang + Ausgang + + + + + + + + Ausgang + + + Eingang 2 + + + Eingang 1 + + 0 + 0 + 0 + 1 + 1 + 0 + 1 + 0 + 1 + 0 + 1 + 1 + + + + XOR + + + diff --git a/_images_de/discrete_convolution.png b/_images_de/discrete_convolution.png new file mode 100644 index 00000000..902a9651 Binary files /dev/null and b/_images_de/discrete_convolution.png differ diff --git a/_images_de/doa.svg b/_images_de/doa.svg new file mode 100644 index 00000000..244bbd6a --- /dev/null +++ b/_images_de/doa.svg @@ -0,0 +1 @@ +dEinfallsrichtungθHauptstrahlrichtung \ No newline at end of file diff --git a/_images_de/doa_capons.svg b/_images_de/doa_capons.svg new file mode 100644 index 00000000..0dbcfbed --- /dev/null +++ b/_images_de/doa_capons.svg @@ -0,0 +1,554 @@ + + + + + + + + 2024-04-17T16:45:05.947654 + image/svg+xml + + + Matplotlib v3.8.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/doa_capons2.svg b/_images_de/doa_capons2.svg new file mode 100644 index 00000000..253490f2 --- /dev/null +++ b/_images_de/doa_capons2.svg @@ -0,0 +1,589 @@ + + + + + + + + 2024-04-17T16:53:00.949548 + image/svg+xml + + + Matplotlib v3.8.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/doa_complex_scenario.svg b/_images_de/doa_complex_scenario.svg new file mode 100644 index 00000000..7b2bc031 --- /dev/null +++ b/_images_de/doa_complex_scenario.svg @@ -0,0 +1,526 @@ + + + + + + + + 2024-04-17T16:57:46.215398 + image/svg+xml + + + Matplotlib v3.8.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/doa_conventional_beamformer.svg b/_images_de/doa_conventional_beamformer.svg new file mode 100644 index 00000000..0c3b768a --- /dev/null +++ b/_images_de/doa_conventional_beamformer.svg @@ -0,0 +1,722 @@ + + + + + + + + 2024-01-22T23:20:00.151337 + image/svg+xml + + + Matplotlib v3.8.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Theta [Grad] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DOA-Metrik + + + + + + + + + + + + + + + + + + + + + + + + + 20 Grad + + + + + + + + diff --git a/_images_de/doa_conventional_beamformer_polar.svg b/_images_de/doa_conventional_beamformer_polar.svg new file mode 100644 index 00000000..9f324277 --- /dev/null +++ b/_images_de/doa_conventional_beamformer_polar.svg @@ -0,0 +1,1029 @@ + + + + + + + + 2024-01-22T23:20:04.154182 + image/svg+xml + + + Matplotlib v3.8.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/doa_covariance_method_1.svg b/_images_de/doa_covariance_method_1.svg new file mode 100644 index 00000000..1b72d043 --- /dev/null +++ b/_images_de/doa_covariance_method_1.svg @@ -0,0 +1,1569 @@ + + + + + + + + 2025-04-12T01:01:17.276957 + image/svg+xml + + + Matplotlib v3.10.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Azimutwinkel (Grad) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leistung [dB] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Konventionelles Muster + + + + MVDR-Muster + + + + + + + + + diff --git a/_images_de/doa_covariance_method_2.svg b/_images_de/doa_covariance_method_2.svg new file mode 100644 index 00000000..bc10dfe3 --- /dev/null +++ b/_images_de/doa_covariance_method_2.svg @@ -0,0 +1,1538 @@ + + + + + + + + 2025-04-12T01:01:08.512119 + image/svg+xml + + + Matplotlib v3.10.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Azimutwinkel (Grad) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leistung [dB] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Konventionelles Muster + + + + MVDR-Muster + + + + + + + + + diff --git a/_images_de/doa_d_is_large_animation.gif b/_images_de/doa_d_is_large_animation.gif new file mode 100644 index 00000000..416f5537 Binary files /dev/null and b/_images_de/doa_d_is_large_animation.gif differ diff --git a/_images_de/doa_d_is_small_animation.gif b/_images_de/doa_d_is_small_animation.gif new file mode 100644 index 00000000..1ef62987 Binary files /dev/null and b/_images_de/doa_d_is_small_animation.gif differ diff --git a/_images_de/doa_d_is_small_animation2.gif b/_images_de/doa_d_is_small_animation2.gif new file mode 100644 index 00000000..a63ea027 Binary files /dev/null and b/_images_de/doa_d_is_small_animation2.gif differ diff --git a/_images_de/doa_eigenvalues.svg b/_images_de/doa_eigenvalues.svg new file mode 100644 index 00000000..93ee52e9 --- /dev/null +++ b/_images_de/doa_eigenvalues.svg @@ -0,0 +1,571 @@ + + + + + + + + 2023-04-05T01:42:47.826139 + image/svg+xml + + + Matplotlib v3.5.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Eigenwert [dB] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/doa_from_behind.svg b/_images_de/doa_from_behind.svg new file mode 100644 index 00000000..1fe3390f --- /dev/null +++ b/_images_de/doa_from_behind.svg @@ -0,0 +1,585 @@ + + + + + + + + 2023-04-03T01:26:02.032378 + image/svg+xml + + + Matplotlib v3.5.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/doa_lms_animation.gif b/_images_de/doa_lms_animation.gif new file mode 100644 index 00000000..60b9aaa7 Binary files /dev/null and b/_images_de/doa_lms_animation.gif differ diff --git a/_images_de/doa_music.svg b/_images_de/doa_music.svg new file mode 100644 index 00000000..4678b24e --- /dev/null +++ b/_images_de/doa_music.svg @@ -0,0 +1,637 @@ + + + + + + + + 2024-04-17T17:14:02.762415 + image/svg+xml + + + Matplotlib v3.8.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/doa_music_animation.gif b/_images_de/doa_music_animation.gif new file mode 100644 index 00000000..6e69a017 Binary files /dev/null and b/_images_de/doa_music_animation.gif differ diff --git a/_images_de/doa_quiescent.svg b/_images_de/doa_quiescent.svg new file mode 100644 index 00000000..cd3cbd2f --- /dev/null +++ b/_images_de/doa_quiescent.svg @@ -0,0 +1,639 @@ + + + + + + + + 2024-01-27T03:01:17.896605 + image/svg+xml + + + Matplotlib v3.8.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/doa_quiescent_beamwidth.svg b/_images_de/doa_quiescent_beamwidth.svg new file mode 100644 index 00000000..f4401516 --- /dev/null +++ b/_images_de/doa_quiescent_beamwidth.svg @@ -0,0 +1,1078 @@ + + + + + + + + 2024-05-10T23:13:07.577953 + image/svg+xml + + + Matplotlib v3.8.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Theta [Grad] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Strahlmuster [dB] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/doa_radar_scenario.svg b/_images_de/doa_radar_scenario.svg new file mode 100644 index 00000000..b938ed77 --- /dev/null +++ b/_images_de/doa_radar_scenario.svg @@ -0,0 +1,916 @@ + + + + + + + + 2024-01-27T03:22:24.303597 + image/svg+xml + + + Matplotlib v3.8.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/doa_sweeping_angle_animation.gif b/_images_de/doa_sweeping_angle_animation.gif new file mode 100644 index 00000000..7b9904db Binary files /dev/null and b/_images_de/doa_sweeping_angle_animation.gif differ diff --git a/_images_de/doa_time_domain.svg b/_images_de/doa_time_domain.svg new file mode 100644 index 00000000..9d429d7a --- /dev/null +++ b/_images_de/doa_time_domain.svg @@ -0,0 +1,1163 @@ + + + + + + + + 2026-01-06T17:18:14.811243 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/doa_time_domain_with_noise.svg b/_images_de/doa_time_domain_with_noise.svg new file mode 100644 index 00000000..af049ecd --- /dev/null +++ b/_images_de/doa_time_domain_with_noise.svg @@ -0,0 +1,1274 @@ + + + + + + + + 2026-01-06T17:18:16.561340 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/doa_trig.svg b/_images_de/doa_trig.svg new file mode 100644 index 00000000..67910ff0 --- /dev/null +++ b/_images_de/doa_trig.svg @@ -0,0 +1 @@ +90o-θθdθ? \ No newline at end of file diff --git a/_images_de/downconversion.png b/_images_de/downconversion.png new file mode 100644 index 00000000..6043e902 Binary files /dev/null and b/_images_de/downconversion.png differ diff --git a/_images_de/ethernet.svg b/_images_de/ethernet.svg new file mode 100644 index 00000000..2066d61a --- /dev/null +++ b/_images_de/ethernet.svg @@ -0,0 +1,321 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + Zeit + + + + + + +1V = 008 ns+0.5V = 01-0.5V = 10-1V = 11 + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/face_template.png b/_images_de/face_template.png new file mode 100644 index 00000000..17136c11 Binary files /dev/null and b/_images_de/face_template.png differ diff --git a/_images_de/fading_example.jpg b/_images_de/fading_example.jpg new file mode 100644 index 00000000..d023c47a Binary files /dev/null and b/_images_de/fading_example.jpg differ diff --git a/_images_de/fft-block-diagram.svg b/_images_de/fft-block-diagram.svg new file mode 100644 index 00000000..b8ed1ba8 --- /dev/null +++ b/_images_de/fft-block-diagram.svg @@ -0,0 +1 @@ +SignalZeitbereichSignal imFrequenzbereichFFT diff --git a/_images_de/fft-io.svg b/_images_de/fft-io.svg new file mode 100644 index 00000000..61d8bf90 --- /dev/null +++ b/_images_de/fft-io.svg @@ -0,0 +1 @@ +Sample:N-10123EingangsvektorFFTIndex:N-10123AusgangT Sekunden∆tB Bandbreite [Hz](B = fs) \ No newline at end of file diff --git a/_images_de/fft-python1.png b/_images_de/fft-python1.png new file mode 100644 index 00000000..e6a27c8c Binary files /dev/null and b/_images_de/fft-python1.png differ diff --git a/_images_de/fft-python1.svg b/_images_de/fft-python1.svg new file mode 100644 index 00000000..f782cea6 --- /dev/null +++ b/_images_de/fft-python1.svg @@ -0,0 +1,719 @@ + + + + + + + + 2024-11-26T00:57:45.461941 + image/svg+xml + + + Matplotlib v3.9.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sample-Index + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Signalamplitude + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/fft-python2.png b/_images_de/fft-python2.png new file mode 100644 index 00000000..d53f15be Binary files /dev/null and b/_images_de/fft-python2.png differ diff --git a/_images_de/fft-python2.svg b/_images_de/fft-python2.svg new file mode 100644 index 00000000..adce3cbe --- /dev/null +++ b/_images_de/fft-python2.svg @@ -0,0 +1,1169 @@ + + + + + + + + 2024-11-26T00:57:45.571054 + image/svg+xml + + + Matplotlib v3.9.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FFT-Betrag + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FFT-Index + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FFT-Phase [Radianten] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/fft-python3.svg b/_images_de/fft-python3.svg new file mode 100644 index 00000000..49cd8778 --- /dev/null +++ b/_images_de/fft-python3.svg @@ -0,0 +1,238 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0(DC)N-1N/2+ frequencies-frequencies + + diff --git a/_images_de/fft-python4.svg b/_images_de/fft-python4.svg new file mode 100644 index 00000000..4e6db0db --- /dev/null +++ b/_images_de/fft-python4.svg @@ -0,0 +1 @@ +0(DC)N-1N/2+ frequencies-frequenciesFFT-Verschiebung0(DC)+ frequencies-frequencies \ No newline at end of file diff --git a/_images_de/fft-python5.png b/_images_de/fft-python5.png new file mode 100644 index 00000000..2f20c610 Binary files /dev/null and b/_images_de/fft-python5.png differ diff --git a/_images_de/fft-python5.svg b/_images_de/fft-python5.svg new file mode 100644 index 00000000..279c9d53 --- /dev/null +++ b/_images_de/fft-python5.svg @@ -0,0 +1,1091 @@ + + + + + + + + 2024-11-26T00:57:45.692211 + image/svg+xml + + + Matplotlib v3.9.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FFT-Betrag + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0,15 Hz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FFT-Phase [Radianten] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/fft_example1.svg b/_images_de/fft_example1.svg new file mode 100644 index 00000000..b2492331 --- /dev/null +++ b/_images_de/fft_example1.svg @@ -0,0 +1,2308 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Betrag [dB] + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/fft_in_python.svg b/_images_de/fft_in_python.svg new file mode 100644 index 00000000..c93fcec7 --- /dev/null +++ b/_images_de/fft_in_python.svg @@ -0,0 +1,1469 @@ + + + + + + + + 2023-03-03T22:20:29.654162 + image/svg+xml + + + Matplotlib v3.5.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [MHz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Betrag [dB] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/fft_logo_wide.gif b/_images_de/fft_logo_wide.gif new file mode 100644 index 00000000..b72cf9c3 Binary files /dev/null and b/_images_de/fft_logo_wide.gif differ diff --git a/_images_de/fft_of_caf.svg b/_images_de/fft_of_caf.svg new file mode 100644 index 00000000..863a068f --- /dev/null +++ b/_images_de/fft_of_caf.svg @@ -0,0 +1,899 @@ + + + + + + + + 2025-06-09T12:57:56.572566 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SCF-Betrag + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/fft_signal_order.png b/_images_de/fft_signal_order.png new file mode 100644 index 00000000..4909d607 Binary files /dev/null and b/_images_de/fft_signal_order.png differ diff --git a/_images_de/fftconvolve.svg b/_images_de/fftconvolve.svg new file mode 100644 index 00000000..dfee2322 --- /dev/null +++ b/_images_de/fftconvolve.svg @@ -0,0 +1,21145 @@ + + + + + + + + 2024-04-19T18:35:51.527801 + image/svg+xml + + + Matplotlib v3.8.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [MHz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LDS [dB] + + + + + + + + + + + + + + + + + + + + + + + + + + + Eingang + + + + Ausgang + + + + + + + + + diff --git a/_images_de/filter.png b/_images_de/filter.png new file mode 100644 index 00000000..75902727 Binary files /dev/null and b/_images_de/filter.png differ diff --git a/_images_de/filter_convolve.png b/_images_de/filter_convolve.png new file mode 100644 index 00000000..415f45ca Binary files /dev/null and b/_images_de/filter_convolve.png differ diff --git a/_images_de/filter_design1.png b/_images_de/filter_design1.png new file mode 100644 index 00000000..d8106c99 Binary files /dev/null and b/_images_de/filter_design1.png differ diff --git a/_images_de/filter_design2.png b/_images_de/filter_design2.png new file mode 100644 index 00000000..95749a8f Binary files /dev/null and b/_images_de/filter_design2.png differ diff --git a/_images_de/filter_design3.png b/_images_de/filter_design3.png new file mode 100644 index 00000000..64ab1a51 Binary files /dev/null and b/_images_de/filter_design3.png differ diff --git a/_images_de/filter_design4.png b/_images_de/filter_design4.png new file mode 100644 index 00000000..dfe65de9 Binary files /dev/null and b/_images_de/filter_design4.png differ diff --git a/_images_de/filter_design5.png b/_images_de/filter_design5.png new file mode 100644 index 00000000..e1439914 Binary files /dev/null and b/_images_de/filter_design5.png differ diff --git a/_images_de/filter_design6.png b/_images_de/filter_design6.png new file mode 100644 index 00000000..f31ff86c Binary files /dev/null and b/_images_de/filter_design6.png differ diff --git a/_images_de/filter_design7.png b/_images_de/filter_design7.png new file mode 100644 index 00000000..b569bc3c Binary files /dev/null and b/_images_de/filter_design7.png differ diff --git a/_images_de/filter_designer1.png b/_images_de/filter_designer1.png new file mode 100644 index 00000000..f06e5053 Binary files /dev/null and b/_images_de/filter_designer1.png differ diff --git a/_images_de/filter_designer2.png b/_images_de/filter_designer2.png new file mode 100644 index 00000000..99c0dabe Binary files /dev/null and b/_images_de/filter_designer2.png differ diff --git a/_images_de/filter_designer3.png b/_images_de/filter_designer3.png new file mode 100644 index 00000000..1b1b2034 Binary files /dev/null and b/_images_de/filter_designer3.png differ diff --git a/_images_de/filter_types.png b/_images_de/filter_types.png new file mode 100644 index 00000000..40141554 Binary files /dev/null and b/_images_de/filter_types.png differ diff --git a/_images_de/filter_use_case.png b/_images_de/filter_use_case.png new file mode 100644 index 00000000..922f1a0a Binary files /dev/null and b/_images_de/filter_use_case.png differ diff --git a/_images_de/filter_use_case2.png b/_images_de/filter_use_case2.png new file mode 100644 index 00000000..c586f746 Binary files /dev/null and b/_images_de/filter_use_case2.png differ diff --git a/_images_de/filter_use_case3.png b/_images_de/filter_use_case3.png new file mode 100644 index 00000000..dbc3a874 Binary files /dev/null and b/_images_de/filter_use_case3.png differ diff --git a/_images_de/filter_use_case4.png b/_images_de/filter_use_case4.png new file mode 100644 index 00000000..53be80bb Binary files /dev/null and b/_images_de/filter_use_case4.png differ diff --git a/_images_de/filter_use_case5.png b/_images_de/filter_use_case5.png new file mode 100644 index 00000000..b28d05c6 Binary files /dev/null and b/_images_de/filter_use_case5.png differ diff --git a/_images_de/filter_use_case6.png b/_images_de/filter_use_case6.png new file mode 100644 index 00000000..5b5c130c Binary files /dev/null and b/_images_de/filter_use_case6.png differ diff --git a/_images_de/filter_use_case_nolabel.png b/_images_de/filter_use_case_nolabel.png new file mode 100644 index 00000000..f69518e4 Binary files /dev/null and b/_images_de/filter_use_case_nolabel.png differ diff --git a/_images_de/flat_vs_freq_selective.png b/_images_de/flat_vs_freq_selective.png new file mode 100644 index 00000000..2459cdea Binary files /dev/null and b/_images_de/flat_vs_freq_selective.png differ diff --git a/_images_de/flux.png b/_images_de/flux.png new file mode 100644 index 00000000..e7728262 Binary files /dev/null and b/_images_de/flux.png differ diff --git a/_images_de/fm_band_psd.png b/_images_de/fm_band_psd.png new file mode 100644 index 00000000..e7834be9 Binary files /dev/null and b/_images_de/fm_band_psd.png differ diff --git a/_images_de/fm_before_demod.svg b/_images_de/fm_before_demod.svg new file mode 100644 index 00000000..c1c54be0 --- /dev/null +++ b/_images_de/fm_before_demod.svg @@ -0,0 +1,825 @@ + + + + + + + + 2022-02-21T23:11:22.598625 + image/svg+xml + + + Matplotlib v3.5.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [kHz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LDS vor FM-Demod [dB] + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/fm_demph_filter_freq_response.svg b/_images_de/fm_demph_filter_freq_response.svg new file mode 100644 index 00000000..7b499fa0 --- /dev/null +++ b/_images_de/fm_demph_filter_freq_response.svg @@ -0,0 +1,892 @@ + + + + + + + + 2022-02-25T23:39:31.191744 + image/svg+xml + + + Matplotlib v3.5.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [kHz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LDS [dB] + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/fm_psd.svg b/_images_de/fm_psd.svg new file mode 100644 index 00000000..c1eb4809 --- /dev/null +++ b/_images_de/fm_psd.svg @@ -0,0 +1,1264 @@ + + + + + + + + 2022-02-21T22:46:16.320092 + image/svg+xml + + + Matplotlib v3.5.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [kHz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 67 kHz + 57 kHz + 19 kHz + 38 kHz + diff --git a/_images_de/fm_psd_labeled.svg b/_images_de/fm_psd_labeled.svg new file mode 100644 index 00000000..bdfccf73 --- /dev/null +++ b/_images_de/fm_psd_labeled.svg @@ -0,0 +1,1032 @@ + + + + + + + + 2022-02-21T22:46:16.320092 + image/svg+xml + + + Matplotlib v3.5.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [kHz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 67 + 57 + 19 + 0 + Pilot + RDS + DirectBand + Mono + Audio + (L+R) + Stereo-Audio (L-R) + Ton + 38 + diff --git a/_images_de/fourier_series_arbitrary_function.gif b/_images_de/fourier_series_arbitrary_function.gif new file mode 100644 index 00000000..de479f38 Binary files /dev/null and b/_images_de/fourier_series_arbitrary_function.gif differ diff --git a/_images_de/fourier_series_triangle.gif b/_images_de/fourier_series_triangle.gif new file mode 100644 index 00000000..ca6f0381 Binary files /dev/null and b/_images_de/fourier_series_triangle.gif differ diff --git a/_images_de/fractional-delay-filter.svg b/_images_de/fractional-delay-filter.svg new file mode 100644 index 00000000..bdd2f7ce --- /dev/null +++ b/_images_de/fractional-delay-filter.svg @@ -0,0 +1,818 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Realteil des Signals + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/freq-shift-diagram.svg b/_images_de/freq-shift-diagram.svg new file mode 100644 index 00000000..332008b9 --- /dev/null +++ b/_images_de/freq-shift-diagram.svg @@ -0,0 +1,154 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + SignalfreqverschobenesSignal + + Sinusschwingung + + diff --git a/_images_de/freq-shift.svg b/_images_de/freq-shift.svg new file mode 100644 index 00000000..16857a2e --- /dev/null +++ b/_images_de/freq-shift.svg @@ -0,0 +1 @@ +fFrequenzf + f0 \ No newline at end of file diff --git a/_images_de/freq-sync-after-square.png b/_images_de/freq-sync-after-square.png new file mode 100644 index 00000000..325e248e Binary files /dev/null and b/_images_de/freq-sync-after-square.png differ diff --git a/_images_de/freq_error.png b/_images_de/freq_error.png new file mode 100644 index 00000000..565ed083 Binary files /dev/null and b/_images_de/freq_error.png differ diff --git a/_images_de/fsk.svg b/_images_de/fsk.svg new file mode 100644 index 00000000..ca3d416b --- /dev/null +++ b/_images_de/fsk.svg @@ -0,0 +1,8879 @@ + + + + + + + + 2025-03-28T22:33:03.108677 + image/svg+xml + + + Matplotlib v3.10.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [MHz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Leistungsdichtespektrum [dB] + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/fsk2.svg b/_images_de/fsk2.svg new file mode 100644 index 00000000..c27750f8 --- /dev/null +++ b/_images_de/fsk2.svg @@ -0,0 +1 @@ +10Zeit1-10000111f0f1 \ No newline at end of file diff --git a/_images_de/gaussian_IQ.png b/_images_de/gaussian_IQ.png new file mode 100644 index 00000000..74147280 Binary files /dev/null and b/_images_de/gaussian_IQ.png differ diff --git a/_images_de/gaussian_gaussian_conv.gif b/_images_de/gaussian_gaussian_conv.gif new file mode 100644 index 00000000..21dfe31b Binary files /dev/null and b/_images_de/gaussian_gaussian_conv.gif differ diff --git a/_images_de/gaussian_histogram.png b/_images_de/gaussian_histogram.png new file mode 100644 index 00000000..9544a26f Binary files /dev/null and b/_images_de/gaussian_histogram.png differ diff --git a/_images_de/gaussian_transformed.png b/_images_de/gaussian_transformed.png new file mode 100644 index 00000000..3b56ceb4 Binary files /dev/null and b/_images_de/gaussian_transformed.png differ diff --git a/_images_de/hackrf1.jpeg b/_images_de/hackrf1.jpeg new file mode 100644 index 00000000..6c71965d Binary files /dev/null and b/_images_de/hackrf1.jpeg differ diff --git a/_images_de/hackrf2.jpeg b/_images_de/hackrf2.jpeg new file mode 100644 index 00000000..b1968e94 Binary files /dev/null and b/_images_de/hackrf2.jpeg differ diff --git a/_images_de/hackrf_block_diagram.webp b/_images_de/hackrf_block_diagram.webp new file mode 100644 index 00000000..fd14936e Binary files /dev/null and b/_images_de/hackrf_block_diagram.webp differ diff --git a/_images_de/hackrf_freq_screenshot.png b/_images_de/hackrf_freq_screenshot.png new file mode 100644 index 00000000..34dcd5e1 Binary files /dev/null and b/_images_de/hackrf_freq_screenshot.png differ diff --git a/_images_de/hackrf_time_screenshot.png b/_images_de/hackrf_time_screenshot.png new file mode 100644 index 00000000..aaaec4a3 Binary files /dev/null and b/_images_de/hackrf_time_screenshot.png differ diff --git a/_images_de/hamming.svg b/_images_de/hamming.svg new file mode 100644 index 00000000..5184a677 --- /dev/null +++ b/_images_de/hamming.svg @@ -0,0 +1 @@ +Bitposition1234567891011121314151617181920Kodierte Bitsp1p2d1p4d2d3d4p8d5d6d7d8d9d10d11p16d12d13d14d15ParitätBit Abdeckungp1xxxxxxxxxxp2xxxxxxxxxxp4xxxxxxxxxp8xxxxxxxxp16xxxxx \ No newline at end of file diff --git a/_images_de/hamming2.svg b/_images_de/hamming2.svg new file mode 100644 index 00000000..dfd2b73e --- /dev/null +++ b/_images_de/hamming2.svg @@ -0,0 +1 @@ +Bitposition1234567Kodierte Bitsp1p2d1p4d2d3d4ParitätBit Abdeckungp1xxxxp2xxxxp4xxxx \ No newline at end of file diff --git a/_images_de/hamming3.png b/_images_de/hamming3.png new file mode 100644 index 00000000..828d930a Binary files /dev/null and b/_images_de/hamming3.png differ diff --git a/_images_de/impulse.png b/_images_de/impulse.png new file mode 100644 index 00000000..f0c62c8d Binary files /dev/null and b/_images_de/impulse.png differ diff --git a/_images_de/impulse1.png b/_images_de/impulse1.png new file mode 100644 index 00000000..3f34d90d Binary files /dev/null and b/_images_de/impulse1.png differ diff --git a/_images_de/impulse_response.png b/_images_de/impulse_response.png new file mode 100644 index 00000000..2ea1d4df Binary files /dev/null and b/_images_de/impulse_response.png differ diff --git a/_images_de/inspectrum.jpg b/_images_de/inspectrum.jpg new file mode 100644 index 00000000..06bfd304 Binary files /dev/null and b/_images_de/inspectrum.jpg differ diff --git a/_images_de/krakensdr.jpg b/_images_de/krakensdr.jpg new file mode 100644 index 00000000..b1727cb1 Binary files /dev/null and b/_images_de/krakensdr.jpg differ diff --git a/_images_de/lcmv_beam_pattern.svg b/_images_de/lcmv_beam_pattern.svg new file mode 100644 index 00000000..fecddf24 --- /dev/null +++ b/_images_de/lcmv_beam_pattern.svg @@ -0,0 +1,887 @@ + + + + + + + + 2024-04-24T14:08:27.351989 + image/svg+xml + + + Matplotlib v3.8.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/lcmv_beam_pattern_spread.svg b/_images_de/lcmv_beam_pattern_spread.svg new file mode 100644 index 00000000..67ea703c --- /dev/null +++ b/_images_de/lcmv_beam_pattern_spread.svg @@ -0,0 +1,795 @@ + + + + + + + + 2024-05-10T23:43:18.651127 + image/svg+xml + + + Matplotlib v3.8.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/linear_vs_log.png b/_images_de/linear_vs_log.png new file mode 100644 index 00000000..fed6d0dd Binary files /dev/null and b/_images_de/linear_vs_log.png differ diff --git a/_images_de/manchester_freq_response.svg b/_images_de/manchester_freq_response.svg new file mode 100644 index 00000000..040d84fa --- /dev/null +++ b/_images_de/manchester_freq_response.svg @@ -0,0 +1,793 @@ + + + + + + + + 2022-02-21T23:55:42.655859 + image/svg+xml + + + Matplotlib v3.5.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [kHz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenzgang des Filters + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/manchester_taps.svg b/_images_de/manchester_taps.svg new file mode 100644 index 00000000..4015e666 --- /dev/null +++ b/_images_de/manchester_taps.svg @@ -0,0 +1,2233 @@ + + + + + + + + 2022-02-21T23:56:27.971744 + image/svg+xml + + + Matplotlib v3.5.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/masking-equation.png b/_images_de/masking-equation.png new file mode 100644 index 00000000..acbcc1bc Binary files /dev/null and b/_images_de/masking-equation.png differ diff --git a/_images_de/masking.svg b/_images_de/masking.svg new file mode 100644 index 00000000..fa937754 --- /dev/null +++ b/_images_de/masking.svg @@ -0,0 +1 @@ +Frequenz10 \ No newline at end of file diff --git a/_images_de/max_freq.svg b/_images_de/max_freq.svg new file mode 100644 index 00000000..eea6ba27 --- /dev/null +++ b/_images_de/max_freq.svg @@ -0,0 +1,411 @@ + + + + + + + + 2022-02-25T23:07:07.691107 + image/svg+xml + + + Matplotlib v3.5.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fmax + Rauschpegel + Signal(e) + diff --git a/_images_de/monopulse.svg b/_images_de/monopulse.svg new file mode 100644 index 00000000..0ce6395a --- /dev/null +++ b/_images_de/monopulse.svg @@ -0,0 +1,143 @@ + + + + + + + + + Ziel + Sender + Array + Strahl 1 + Strahl 2 + + + “Sum” + Strahl + diff --git a/_images_de/monopulse_tracking.svg b/_images_de/monopulse_tracking.svg new file mode 100644 index 00000000..0bdb7c6b --- /dev/null +++ b/_images_de/monopulse_tracking.svg @@ -0,0 +1,1326 @@ + + + + + + + + + 2023-12-25T09:03:58.835927 + image/svg+xml + + + Matplotlib v3.3.4, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Phasenschätzung [Grad] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Fehler + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/monopulse_waving.svg b/_images_de/monopulse_waving.svg new file mode 100644 index 00000000..9d1fa310 --- /dev/null +++ b/_images_de/monopulse_waving.svg @@ -0,0 +1,1018 @@ + + + + + + + + + 2023-12-25T08:50:41.547308 + image/svg+xml + + + Matplotlib v3.3.4, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Fehler + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/multipath.svg b/_images_de/multipath.svg new file mode 100644 index 00000000..78b446dd --- /dev/null +++ b/_images_de/multipath.svg @@ -0,0 +1,143 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + SendeantenneReceive Antenna +Direktweg + + + + + Mehrwegausbreitung + + diff --git a/_images_de/multipath2.svg b/_images_de/multipath2.svg new file mode 100644 index 00000000..7c7035fe --- /dev/null +++ b/_images_de/multipath2.svg @@ -0,0 +1 @@ +SenderEmpfängerDirektwegMehrwegausbreitungh(t)τ0τN-1τ1t \ No newline at end of file diff --git a/_images_de/multipath_fading.png b/_images_de/multipath_fading.png new file mode 100644 index 00000000..88c801ff Binary files /dev/null and b/_images_de/multipath_fading.png differ diff --git a/_images_de/negative-frequencies.svg b/_images_de/negative-frequencies.svg new file mode 100644 index 00000000..6a55adbc --- /dev/null +++ b/_images_de/negative-frequencies.svg @@ -0,0 +1,332 @@ + + + + + + image/svg+xml + + + + + + + + + + + + Frequenz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Index:N +- +10 +1 +2 + +3 +0 + + + + + + + + + + + + diff --git a/_images_de/negative-frequencies2.svg b/_images_de/negative-frequencies2.svg new file mode 100644 index 00000000..8e7053c3 --- /dev/null +++ b/_images_de/negative-frequencies2.svg @@ -0,0 +1 @@ +Frequenz100 MHz105 MHz95 MHz \ No newline at end of file diff --git a/_images_de/negative-frequencies3.svg b/_images_de/negative-frequencies3.svg new file mode 100644 index 00000000..0edd51b8 --- /dev/null +++ b/_images_de/negative-frequencies3.svg @@ -0,0 +1 @@ +Frequenz05 MHz-5 MHzAbtastrate2 \ No newline at end of file diff --git a/_images_de/negative_freq_animation.gif b/_images_de/negative_freq_animation.gif new file mode 100644 index 00000000..f46afb37 Binary files /dev/null and b/_images_de/negative_freq_animation.gif differ diff --git a/_images_de/noise.png b/_images_de/noise.png new file mode 100644 index 00000000..3ad7f60e Binary files /dev/null and b/_images_de/noise.png differ diff --git a/_images_de/noise3.png b/_images_de/noise3.png new file mode 100644 index 00000000..813416d6 Binary files /dev/null and b/_images_de/noise3.png differ diff --git a/_images_de/noise_freq.png b/_images_de/noise_freq.png new file mode 100644 index 00000000..ff8a49c9 Binary files /dev/null and b/_images_de/noise_freq.png differ diff --git a/_images_de/noise_iq.png b/_images_de/noise_iq.png new file mode 100644 index 00000000..aecf2365 Binary files /dev/null and b/_images_de/noise_iq.png differ diff --git a/_images_de/noise_python.png b/_images_de/noise_python.png new file mode 100644 index 00000000..0f8f01c6 Binary files /dev/null and b/_images_de/noise_python.png differ diff --git a/_images_de/noisey_qpsk.png b/_images_de/noisey_qpsk.png new file mode 100644 index 00000000..4daba981 Binary files /dev/null and b/_images_de/noisey_qpsk.png differ diff --git a/_images_de/noisey_qpsk2.png b/_images_de/noisey_qpsk2.png new file mode 100644 index 00000000..0c94fc47 Binary files /dev/null and b/_images_de/noisey_qpsk2.png differ diff --git a/_images_de/non_csp_metric.svg b/_images_de/non_csp_metric.svg new file mode 100644 index 00000000..b00428e0 --- /dev/null +++ b/_images_de/non_csp_metric.svg @@ -0,0 +1,28985 @@ + + + + + + + + 2024-06-24T00:29:16.700216 + image/svg+xml + + + Matplotlib v3.9.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Kombiniert + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/null_steering.svg b/_images_de/null_steering.svg new file mode 100644 index 00000000..2063d397 --- /dev/null +++ b/_images_de/null_steering.svg @@ -0,0 +1,832 @@ + + + + + + + + 2024-05-03T20:15:26.369633 + image/svg+xml + + + Matplotlib v3.8.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/nyquist_rate.png b/_images_de/nyquist_rate.png new file mode 100644 index 00000000..495b4dfa Binary files /dev/null and b/_images_de/nyquist_rate.png differ diff --git a/_images_de/offtuning.png b/_images_de/offtuning.png new file mode 100644 index 00000000..92d3affb Binary files /dev/null and b/_images_de/offtuning.png differ diff --git a/_images_de/phase_jitter.svg b/_images_de/phase_jitter.svg new file mode 100644 index 00000000..7a5f6a1e --- /dev/null +++ b/_images_de/phase_jitter.svg @@ -0,0 +1,1541 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/phase_jitter_awgn.svg b/_images_de/phase_jitter_awgn.svg new file mode 100644 index 00000000..baa1c7ab --- /dev/null +++ b/_images_de/phase_jitter_awgn.svg @@ -0,0 +1,1413 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/phaser_animation_hamming.gif b/_images_de/phaser_animation_hamming.gif new file mode 100644 index 00000000..9aea74dc Binary files /dev/null and b/_images_de/phaser_animation_hamming.gif differ diff --git a/_images_de/phaser_animation_rect.gif b/_images_de/phaser_animation_rect.gif new file mode 100644 index 00000000..8a999156 Binary files /dev/null and b/_images_de/phaser_animation_rect.gif differ diff --git a/_images_de/phaser_components.png b/_images_de/phaser_components.png new file mode 100644 index 00000000..fcae9a25 Binary files /dev/null and b/_images_de/phaser_components.png differ diff --git a/_images_de/phaser_detailed_block_diagram.png b/_images_de/phaser_detailed_block_diagram.png new file mode 100644 index 00000000..f7500d29 Binary files /dev/null and b/_images_de/phaser_detailed_block_diagram.png differ diff --git a/_images_de/phaser_front_and_back.png b/_images_de/phaser_front_and_back.png new file mode 100644 index 00000000..1c5fcc48 Binary files /dev/null and b/_images_de/phaser_front_and_back.png differ diff --git a/_images_de/phaser_gui.png b/_images_de/phaser_gui.png new file mode 100644 index 00000000..f6dd3bfa Binary files /dev/null and b/_images_de/phaser_gui.png differ diff --git a/_images_de/phaser_hb100.png b/_images_de/phaser_hb100.png new file mode 100644 index 00000000..5bb624df Binary files /dev/null and b/_images_de/phaser_hb100.png differ diff --git a/_images_de/phaser_on_tripod.png b/_images_de/phaser_on_tripod.png new file mode 100644 index 00000000..42604287 Binary files /dev/null and b/_images_de/phaser_on_tripod.png differ diff --git a/_images_de/phaser_rx_psd.png b/_images_de/phaser_rx_psd.png new file mode 100644 index 00000000..53accb6f Binary files /dev/null and b/_images_de/phaser_rx_psd.png differ diff --git a/_images_de/phaser_sweep.png b/_images_de/phaser_sweep.png new file mode 100644 index 00000000..3b71d5b6 Binary files /dev/null and b/_images_de/phaser_sweep.png differ diff --git a/_images_de/phaser_sweep_polar.png b/_images_de/phaser_sweep_polar.png new file mode 100644 index 00000000..addc0a86 Binary files /dev/null and b/_images_de/phaser_sweep_polar.png differ diff --git a/_images_de/pluto.png b/_images_de/pluto.png new file mode 100644 index 00000000..e806cb4b Binary files /dev/null and b/_images_de/pluto.png differ diff --git a/_images_de/pluto_plus.png b/_images_de/pluto_plus.png new file mode 100644 index 00000000..662ca0ed Binary files /dev/null and b/_images_de/pluto_plus.png differ diff --git a/_images_de/pluto_plus_pcb.jpg b/_images_de/pluto_plus_pcb.jpg new file mode 100644 index 00000000..675b0601 Binary files /dev/null and b/_images_de/pluto_plus_pcb.jpg differ diff --git a/_images_de/pluto_tx_rx.svg b/_images_de/pluto_tx_rx.svg new file mode 100644 index 00000000..ce786431 --- /dev/null +++ b/_images_de/pluto_tx_rx.svg @@ -0,0 +1,777 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [MHz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LDS + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/psd_of_bpsk_used_for_caf.svg b/_images_de/psd_of_bpsk_used_for_caf.svg new file mode 100644 index 00000000..1354dac4 --- /dev/null +++ b/_images_de/psd_of_bpsk_used_for_caf.svg @@ -0,0 +1,9288 @@ + + + + + + + + 2024-05-30T00:29:24.525765 + image/svg+xml + + + Matplotlib v3.8.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LDS + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/psd_of_multiple_signals.svg b/_images_de/psd_of_multiple_signals.svg new file mode 100644 index 00000000..6649648d --- /dev/null +++ b/_images_de/psd_of_multiple_signals.svg @@ -0,0 +1,858 @@ + + + + + + + + 2024-06-23T16:51:42.928832 + image/svg+xml + + + Matplotlib v3.9.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LDS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/psk_set.png b/_images_de/psk_set.png new file mode 100644 index 00000000..a0ab49f2 Binary files /dev/null and b/_images_de/psk_set.png differ diff --git a/_images_de/pulse_shaped_BSPK.svg b/_images_de/pulse_shaped_BSPK.svg new file mode 100644 index 00000000..8cf095bf --- /dev/null +++ b/_images_de/pulse_shaped_BSPK.svg @@ -0,0 +1,791 @@ + + + + + + + + 2024-06-23T15:54:55.949298 + image/svg+xml + + + Matplotlib v3.9.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sample-Index + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Samplewert (I) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/pulse_shaping.png b/_images_de/pulse_shaping.png new file mode 100644 index 00000000..8586070d Binary files /dev/null and b/_images_de/pulse_shaping.png differ diff --git a/_images_de/pulse_shaping_freq.png b/_images_de/pulse_shaping_freq.png new file mode 100644 index 00000000..e4985029 Binary files /dev/null and b/_images_de/pulse_shaping_freq.png differ diff --git a/_images_de/pulse_shaping_python1.png b/_images_de/pulse_shaping_python1.png new file mode 100644 index 00000000..1b662715 Binary files /dev/null and b/_images_de/pulse_shaping_python1.png differ diff --git a/_images_de/pulse_shaping_python2.png b/_images_de/pulse_shaping_python2.png new file mode 100644 index 00000000..1a2015ab Binary files /dev/null and b/_images_de/pulse_shaping_python2.png differ diff --git a/_images_de/pulse_shaping_python3.png b/_images_de/pulse_shaping_python3.png new file mode 100644 index 00000000..8fa30043 Binary files /dev/null and b/_images_de/pulse_shaping_python3.png differ diff --git a/_images_de/pulse_shaping_python3.svg b/_images_de/pulse_shaping_python3.svg new file mode 100644 index 00000000..7ea1cb73 --- /dev/null +++ b/_images_de/pulse_shaping_python3.svg @@ -0,0 +1,853 @@ + + + + + + + + 2022-02-28T22:27:04.733637 + image/svg+xml + + + Matplotlib v3.5.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/pulse_shaping_rolloff.png b/_images_de/pulse_shaping_rolloff.png new file mode 100644 index 00000000..551c9426 Binary files /dev/null and b/_images_de/pulse_shaping_rolloff.png differ diff --git a/_images_de/pulse_train.svg b/_images_de/pulse_train.svg new file mode 100644 index 00000000..7504b4ab --- /dev/null +++ b/_images_de/pulse_train.svg @@ -0,0 +1,1568 @@ + + + + + + + + 2022-02-28T22:14:59.268925 + image/svg+xml + + + Matplotlib v3.5.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + T + T + T + T + T + T + T + T + T + T + T + T + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Impulse (vor Kombination) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/pyqt_animation.gif b/_images_de/pyqt_animation.gif new file mode 100644 index 00000000..3b1db9bb Binary files /dev/null and b/_images_de/pyqt_animation.gif differ diff --git a/_images_de/pyqt_spectrogram.png b/_images_de/pyqt_spectrogram.png new file mode 100644 index 00000000..d5d19fa8 Binary files /dev/null and b/_images_de/pyqt_spectrogram.png differ diff --git a/_images_de/pyqt_time_plot.png b/_images_de/pyqt_time_plot.png new file mode 100644 index 00000000..bee3da0e Binary files /dev/null and b/_images_de/pyqt_time_plot.png differ diff --git a/_images_de/pyqtgraph_example.png b/_images_de/pyqtgraph_example.png new file mode 100644 index 00000000..d5d72c8f Binary files /dev/null and b/_images_de/pyqtgraph_example.png differ diff --git a/_images_de/qam.png b/_images_de/qam.png new file mode 100644 index 00000000..ff0d1aaa Binary files /dev/null and b/_images_de/qam.png differ diff --git a/_images_de/qam_time_domain.png b/_images_de/qam_time_domain.png new file mode 100644 index 00000000..736f9314 Binary files /dev/null and b/_images_de/qam_time_domain.png differ diff --git a/_images_de/qpsk.png b/_images_de/qpsk.png new file mode 100644 index 00000000..008c2c46 Binary files /dev/null and b/_images_de/qpsk.png differ diff --git a/_images_de/qpsk_list.png b/_images_de/qpsk_list.png new file mode 100644 index 00000000..fba14cd5 Binary files /dev/null and b/_images_de/qpsk_list.png differ diff --git a/_images_de/qpsk_python.svg b/_images_de/qpsk_python.svg new file mode 100644 index 00000000..fc9c1ea2 --- /dev/null +++ b/_images_de/qpsk_python.svg @@ -0,0 +1,1541 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/qpsk_python2.svg b/_images_de/qpsk_python2.svg new file mode 100644 index 00000000..c6d50af8 --- /dev/null +++ b/_images_de/qpsk_python2.svg @@ -0,0 +1,1541 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/qpsk_vs_16qam.png b/_images_de/qpsk_vs_16qam.png new file mode 100644 index 00000000..3534c86f Binary files /dev/null and b/_images_de/qpsk_vs_16qam.png differ diff --git a/_images_de/qt_layouts.svg b/_images_de/qt_layouts.svg new file mode 100644 index 00000000..c2df4036 --- /dev/null +++ b/_images_de/qt_layouts.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/_images_de/raised_cosine.svg b/_images_de/raised_cosine.svg new file mode 100644 index 00000000..50ee4c65 --- /dev/null +++ b/_images_de/raised_cosine.svg @@ -0,0 +1,1134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/raised_cosine_freq.svg b/_images_de/raised_cosine_freq.svg new file mode 100644 index 00000000..3c3cb24a --- /dev/null +++ b/_images_de/raised_cosine_freq.svg @@ -0,0 +1,789 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/rayleigh.svg b/_images_de/rayleigh.svg new file mode 100644 index 00000000..55b01930 --- /dev/null +++ b/_images_de/rayleigh.svg @@ -0,0 +1,1005 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Rayleigh-Fading + + + + + Kein Fading + + + + + + + + Zeit [s] + Kanalbetrag [dB] + diff --git a/_images_de/realistic_filter.png b/_images_de/realistic_filter.png new file mode 100644 index 00000000..d9208e83 Binary files /dev/null and b/_images_de/realistic_filter.png differ diff --git a/_images_de/receiver_arch_diagram.svg b/_images_de/receiver_arch_diagram.svg new file mode 100644 index 00000000..fffaf117 --- /dev/null +++ b/_images_de/receiver_arch_diagram.svg @@ -0,0 +1,610 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Direktabtastung (auch: Direct RF)veryteurer ADC + + Direktmischung (auch: Zero IF)e.g. USRP B200,PlutoSDRÜberlagerungsempfängere.g.UKW-Radio im alten Auto + + + + + + + + + LNA + + TeuerADC + + LNA + + ADC + + ADC + + Filter + + Filter + + + ADCorAnalogDe-mod + + Filter + + + + + LNA + + Filter + + + + 90oVerschiebung + + + + + + + + + + + LOLOIQ + + diff --git a/_images_de/rect_exp_conv.gif b/_images_de/rect_exp_conv.gif new file mode 100644 index 00000000..d6616806 Binary files /dev/null and b/_images_de/rect_exp_conv.gif differ diff --git a/_images_de/rect_fat_rect_conv.gif b/_images_de/rect_fat_rect_conv.gif new file mode 100644 index 00000000..8b00ef2e Binary files /dev/null and b/_images_de/rect_fat_rect_conv.gif differ diff --git a/_images_de/rect_rect_conv.gif b/_images_de/rect_rect_conv.gif new file mode 100644 index 00000000..4f1fa684 Binary files /dev/null and b/_images_de/rect_rect_conv.gif differ diff --git a/_images_de/rrc_filter.png b/_images_de/rrc_filter.png new file mode 100644 index 00000000..6fec5a94 Binary files /dev/null and b/_images_de/rrc_filter.png differ diff --git a/_images_de/rrc_rolloff.svg b/_images_de/rrc_rolloff.svg new file mode 100644 index 00000000..a0e981d6 --- /dev/null +++ b/_images_de/rrc_rolloff.svg @@ -0,0 +1,1108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/rtlsdr-gain.svg b/_images_de/rtlsdr-gain.svg new file mode 100644 index 00000000..44246377 --- /dev/null +++ b/_images_de/rtlsdr-gain.svg @@ -0,0 +1,5163 @@ + + + + + + + + 2024-03-11T05:04:52.363447 + image/svg+xml + + + Matplotlib v3.7.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/rtlsdr-waterfall.svg b/_images_de/rtlsdr-waterfall.svg new file mode 100644 index 00000000..4b106e23 --- /dev/null +++ b/_images_de/rtlsdr-waterfall.svg @@ -0,0 +1,489 @@ + + + + + + + + 2024-03-11T05:08:57.202462 + image/svg+xml + + + Matplotlib v3.7.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [MHz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeit [s] + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/rtlsdrs.svg b/_images_de/rtlsdrs.svg new file mode 100644 index 00000000..01b2014f --- /dev/null +++ b/_images_de/rtlsdrs.svg @@ -0,0 +1,11653 @@ + + diff --git a/_images_de/sampling.svg b/_images_de/sampling.svg new file mode 100644 index 00000000..cd9ea0fc --- /dev/null +++ b/_images_de/sampling.svg @@ -0,0 +1,531 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/sampling_Fs_0.3.svg b/_images_de/sampling_Fs_0.3.svg new file mode 100644 index 00000000..487f2806 --- /dev/null +++ b/_images_de/sampling_Fs_0.3.svg @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/sampling_Fs_0.36.svg b/_images_de/sampling_Fs_0.36.svg new file mode 100644 index 00000000..f66b11c8 --- /dev/null +++ b/_images_de/sampling_Fs_0.36.svg @@ -0,0 +1,581 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/sampling_Fs_0.45.svg b/_images_de/sampling_Fs_0.45.svg new file mode 100644 index 00000000..5fe94a4b --- /dev/null +++ b/_images_de/sampling_Fs_0.45.svg @@ -0,0 +1,674 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/sampling_Fs_0.6.svg b/_images_de/sampling_Fs_0.6.svg new file mode 100644 index 00000000..d9882fd5 --- /dev/null +++ b/_images_de/sampling_Fs_0.6.svg @@ -0,0 +1,189 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_images_de/saturated_time.png b/_images_de/saturated_time.png new file mode 100644 index 00000000..72fa8e96 Binary files /dev/null and b/_images_de/saturated_time.png differ diff --git a/_images_de/scf_coherence.svg b/_images_de/scf_coherence.svg new file mode 100644 index 00000000..6837bb75 --- /dev/null +++ b/_images_de/scf_coherence.svg @@ -0,0 +1,602 @@ + + + + + + + + 2024-06-27T23:20:13.113719 + image/svg+xml + + + Matplotlib v3.9.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zyklische Frequenz [norm. Hz] + + + + + + + + + + + + + + Reguläres SCF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Spektrale Kohärenzfunktion (COH) + + + + + + + + + + + diff --git a/_images_de/scf_coherence_pulse_shaped.svg b/_images_de/scf_coherence_pulse_shaped.svg new file mode 100644 index 00000000..1c5a9fda --- /dev/null +++ b/_images_de/scf_coherence_pulse_shaped.svg @@ -0,0 +1,602 @@ + + + + + + + + 2024-06-27T23:22:57.790536 + image/svg+xml + + + Matplotlib v3.9.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zyklische Frequenz [norm. Hz] + + + + + + + + + + + + + + Reguläres SCF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Spektrale Kohärenzfunktion (COH) + + + + + + + + + + + diff --git a/_images_de/scf_conj_multiple_signals.svg b/_images_de/scf_conj_multiple_signals.svg new file mode 100644 index 00000000..0e51acd6 --- /dev/null +++ b/_images_de/scf_conj_multiple_signals.svg @@ -0,0 +1,765 @@ + + + + + + + + 2024-06-26T20:23:50.006241 + image/svg+xml + + + Matplotlib v3.9.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zyklische Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/scf_conj_pulseshaped_bpsk.svg b/_images_de/scf_conj_pulseshaped_bpsk.svg new file mode 100644 index 00000000..c908f61f --- /dev/null +++ b/_images_de/scf_conj_pulseshaped_bpsk.svg @@ -0,0 +1,649 @@ + + + + + + + + 2024-06-26T15:33:41.847842 + image/svg+xml + + + Matplotlib v3.9.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zyklische Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/scf_conj_rect_bpsk.svg b/_images_de/scf_conj_rect_bpsk.svg new file mode 100644 index 00000000..18a78d20 --- /dev/null +++ b/_images_de/scf_conj_rect_bpsk.svg @@ -0,0 +1,634 @@ + + + + + + + + 2024-06-26T15:33:59.110422 + image/svg+xml + + + Matplotlib v3.9.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zyklische Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/scf_conj_rect_qpsk.svg b/_images_de/scf_conj_rect_qpsk.svg new file mode 100644 index 00000000..c86b17b2 --- /dev/null +++ b/_images_de/scf_conj_rect_qpsk.svg @@ -0,0 +1,595 @@ + + + + + + + + 2024-06-26T15:32:45.711198 + image/svg+xml + + + Matplotlib v3.9.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zyklische Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/scf_conj_rect_qpsk_scaled.svg b/_images_de/scf_conj_rect_qpsk_scaled.svg new file mode 100644 index 00000000..d52002c5 --- /dev/null +++ b/_images_de/scf_conj_rect_qpsk_scaled.svg @@ -0,0 +1,649 @@ + + + + + + + + 2024-06-26T15:37:28.058812 + image/svg+xml + + + Matplotlib v3.9.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zyklische Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/scf_fam.svg b/_images_de/scf_fam.svg new file mode 100644 index 00000000..c8285ee3 --- /dev/null +++ b/_images_de/scf_fam.svg @@ -0,0 +1,425 @@ + + + + + + + + 2024-06-06T05:28:28.834463 + image/svg+xml + + + Matplotlib v3.9.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zyklische Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/scf_fam_1d.svg b/_images_de/scf_fam_1d.svg new file mode 100644 index 00000000..0f659540 --- /dev/null +++ b/_images_de/scf_fam_1d.svg @@ -0,0 +1,4201 @@ + + + + + + + + 2024-06-06T05:28:29.098718 + image/svg+xml + + + Matplotlib v3.9.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zyklische Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SCF-Leistung + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/scf_fam_zoomedin.svg b/_images_de/scf_fam_zoomedin.svg new file mode 100644 index 00000000..4e74a9f9 --- /dev/null +++ b/_images_de/scf_fam_zoomedin.svg @@ -0,0 +1,516 @@ + + + + + + + + 2024-06-06T05:28:29.007944 + image/svg+xml + + + Matplotlib v3.9.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zyklische Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/scf_freq_smoothing.svg b/_images_de/scf_freq_smoothing.svg new file mode 100644 index 00000000..ee91f70f --- /dev/null +++ b/_images_de/scf_freq_smoothing.svg @@ -0,0 +1,385 @@ + + + + + + + + 2024-06-06T23:37:23.699278 + image/svg+xml + + + Matplotlib v3.9.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zyklische Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/scf_freq_smoothing_ofdm.svg b/_images_de/scf_freq_smoothing_ofdm.svg new file mode 100644 index 00000000..9def4dfe --- /dev/null +++ b/_images_de/scf_freq_smoothing_ofdm.svg @@ -0,0 +1,413 @@ + + + + + + + + 2024-06-20T00:26:31.961436 + image/svg+xml + + + Matplotlib v3.9.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zyklische Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/scf_freq_smoothing_ofdm_zoomed_in.svg b/_images_de/scf_freq_smoothing_ofdm_zoomed_in.svg new file mode 100644 index 00000000..e151ead0 --- /dev/null +++ b/_images_de/scf_freq_smoothing_ofdm_zoomed_in.svg @@ -0,0 +1,463 @@ + + + + + + + + 2026-01-23T13:53:18.154826 + image/svg+xml + + + Matplotlib v3.10.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zyklische Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/scf_freq_smoothing_pulse_multiple_signals.svg b/_images_de/scf_freq_smoothing_pulse_multiple_signals.svg new file mode 100644 index 00000000..417a4d2b --- /dev/null +++ b/_images_de/scf_freq_smoothing_pulse_multiple_signals.svg @@ -0,0 +1,701 @@ + + + + + + + + 2024-06-23T16:53:52.648993 + image/svg+xml + + + Matplotlib v3.9.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zyklische Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/scf_freq_smoothing_pulse_shaped_bpsk.svg b/_images_de/scf_freq_smoothing_pulse_shaped_bpsk.svg new file mode 100644 index 00000000..c550a4e8 --- /dev/null +++ b/_images_de/scf_freq_smoothing_pulse_shaped_bpsk.svg @@ -0,0 +1,385 @@ + + + + + + + + 2024-06-23T15:57:54.674888 + image/svg+xml + + + Matplotlib v3.9.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zyklische Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/scf_freq_smoothing_pulse_shaped_bpsk2.svg b/_images_de/scf_freq_smoothing_pulse_shaped_bpsk2.svg new file mode 100644 index 00000000..3e0e507f --- /dev/null +++ b/_images_de/scf_freq_smoothing_pulse_shaped_bpsk2.svg @@ -0,0 +1,385 @@ + + + + + + + + 2024-06-23T18:03:34.093860 + image/svg+xml + + + Matplotlib v3.9.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zyklische Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/scf_freq_smoothing_pulse_shaped_bpsk3.svg b/_images_de/scf_freq_smoothing_pulse_shaped_bpsk3.svg new file mode 100644 index 00000000..490b7705 --- /dev/null +++ b/_images_de/scf_freq_smoothing_pulse_shaped_bpsk3.svg @@ -0,0 +1,385 @@ + + + + + + + + 2024-06-23T18:03:53.742221 + image/svg+xml + + + Matplotlib v3.9.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zyklische Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/scf_time_smoothing.svg b/_images_de/scf_time_smoothing.svg new file mode 100644 index 00000000..5eb911a3 --- /dev/null +++ b/_images_de/scf_time_smoothing.svg @@ -0,0 +1,385 @@ + + + + + + + + 2024-06-06T02:13:09.227435 + image/svg+xml + + + Matplotlib v3.9.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zyklische Frequenz [norm. Hz] + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/shannon_limit.svg b/_images_de/shannon_limit.svg new file mode 100644 index 00000000..814067ed --- /dev/null +++ b/_images_de/shannon_limit.svg @@ -0,0 +1,628 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Shannon-Grenze [Bits/s/Hz] + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/shannon_limit_proof.png b/_images_de/shannon_limit_proof.png new file mode 100644 index 00000000..4e8d2dcb Binary files /dev/null and b/_images_de/shannon_limit_proof.png differ diff --git a/_images_de/shifted_filter.png b/_images_de/shifted_filter.png new file mode 100644 index 00000000..c6095365 Binary files /dev/null and b/_images_de/shifted_filter.png differ diff --git a/_images_de/shifted_filter_nolabel.png b/_images_de/shifted_filter_nolabel.png new file mode 100644 index 00000000..73f41a08 Binary files /dev/null and b/_images_de/shifted_filter_nolabel.png differ diff --git a/_images_de/sigmf_logo.gif b/_images_de/sigmf_logo.gif new file mode 100644 index 00000000..ecf72699 Binary files /dev/null and b/_images_de/sigmf_logo.gif differ diff --git a/_images_de/silly_marc.jpg b/_images_de/silly_marc.jpg new file mode 100644 index 00000000..91ae92a8 Binary files /dev/null and b/_images_de/silly_marc.jpg differ diff --git a/_images_de/sine-wave.png b/_images_de/sine-wave.png new file mode 100644 index 00000000..e4989e37 Binary files /dev/null and b/_images_de/sine-wave.png differ diff --git a/_images_de/spatial_tapering_animation.gif b/_images_de/spatial_tapering_animation.gif new file mode 100644 index 00000000..9fdc698d Binary files /dev/null and b/_images_de/spatial_tapering_animation.gif differ diff --git a/_images_de/spatial_tapering_animation2.gif b/_images_de/spatial_tapering_animation2.gif new file mode 100644 index 00000000..9d07a6b4 Binary files /dev/null and b/_images_de/spatial_tapering_animation2.gif differ diff --git a/_images_de/spectrogram.svg b/_images_de/spectrogram.svg new file mode 100644 index 00000000..767593c3 --- /dev/null +++ b/_images_de/spectrogram.svg @@ -0,0 +1,425 @@ + + + + + + + + 2026-02-24T01:48:09.678494 + image/svg+xml + + + Matplotlib v3.10.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz [MHz] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeit [s] + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/spectrogram_diagram.svg b/_images_de/spectrogram_diagram.svg new file mode 100644 index 00000000..395a97c3 --- /dev/null +++ b/_images_de/spectrogram_diagram.svg @@ -0,0 +1 @@ +Frequenz-FS/2ZeitFFT von Abschnitt 1FFT von Abschnitt 2FFT von Abschnitt 3FFT von Abschnitt 4FFT von Abschnitt 5FFT von Abschnitt 6FS/20Abschnitt1Abschnitt 2Abschnitt 3Abschnitt 5Abschnitt 4Abschnitt 6Zeitbereich(IQ-Samples)0Spektrogramm0 \ No newline at end of file diff --git a/_images_de/spectrogram_time.svg b/_images_de/spectrogram_time.svg new file mode 100644 index 00000000..da938ea0 --- /dev/null +++ b/_images_de/spectrogram_time.svg @@ -0,0 +1,1003 @@ + + + + + + + + 2022-02-11T22:37:25.600196 + image/svg+xml + + + Matplotlib v3.5.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/splitting_rc_filter.svg b/_images_de/splitting_rc_filter.svg new file mode 100644 index 00000000..26a77727 --- /dev/null +++ b/_images_de/splitting_rc_filter.svg @@ -0,0 +1,291 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + Modulation + + + Impuls Shaping + + + + Drahtlos Kanal + + + + Bits + + + + + + + + + + Angepasst Filter + + + + Demodulation + + + Bits (hopefully) + + + + + + RC Filter + + + + + + + RC Filter + + + + + + + diff --git a/_images_de/square-wave.svg b/_images_de/square-wave.svg new file mode 100644 index 00000000..7099ab62 --- /dev/null +++ b/_images_de/square-wave.svg @@ -0,0 +1,1057 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Betrag + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/squaring-qpsk.gif b/_images_de/squaring-qpsk.gif new file mode 100644 index 00000000..8f6e8b5a Binary files /dev/null and b/_images_de/squaring-qpsk.gif differ diff --git a/_images_de/squaring-qpsk2.gif b/_images_de/squaring-qpsk2.gif new file mode 100644 index 00000000..d1c552af Binary files /dev/null and b/_images_de/squaring-qpsk2.gif differ diff --git a/_images_de/summing_sinusoids.svg b/_images_de/summing_sinusoids.svg new file mode 100644 index 00000000..a944bc62 --- /dev/null +++ b/_images_de/summing_sinusoids.svg @@ -0,0 +1,205 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + += + + diff --git a/_images_de/symbol_sync1.png b/_images_de/symbol_sync1.png new file mode 100644 index 00000000..a229f17b Binary files /dev/null and b/_images_de/symbol_sync1.png differ diff --git a/_images_de/symbol_sync2.png b/_images_de/symbol_sync2.png new file mode 100644 index 00000000..98f6a541 Binary files /dev/null and b/_images_de/symbol_sync2.png differ diff --git a/_images_de/symbol_sync3.png b/_images_de/symbol_sync3.png new file mode 100644 index 00000000..1dc16436 Binary files /dev/null and b/_images_de/symbol_sync3.png differ diff --git a/_images_de/symbol_sync4.png b/_images_de/symbol_sync4.png new file mode 100644 index 00000000..17370476 Binary files /dev/null and b/_images_de/symbol_sync4.png differ diff --git a/_images_de/symbols.png b/_images_de/symbols.png new file mode 100644 index 00000000..2921c36f Binary files /dev/null and b/_images_de/symbols.png differ diff --git a/_images_de/symbols1.png b/_images_de/symbols1.png new file mode 100644 index 00000000..f6ce5f79 Binary files /dev/null and b/_images_de/symbols1.png differ diff --git a/_images_de/sync-diagram.svg b/_images_de/sync-diagram.svg new file mode 100644 index 00000000..a61a6998 --- /dev/null +++ b/_images_de/sync-diagram.svg @@ -0,0 +1 @@ +Bits(from higher layer)KanalCodingModulationImpuls ShapingAngepasst FilterGrob Freq SyncZeitsynchronisationFeine Frequenz SyncRahmen Detect/SyncDemodulationKanal DecodingBits(hoffentlich)Drahtlos Kanal \ No newline at end of file diff --git a/_images_de/sync-freq-offset.svg b/_images_de/sync-freq-offset.svg new file mode 100644 index 00000000..2c922743 --- /dev/null +++ b/_images_de/sync-freq-offset.svg @@ -0,0 +1,6016 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Vor Frequenzversatz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Nach Frequenzversatz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/time-scaling.svg b/_images_de/time-scaling.svg new file mode 100644 index 00000000..93e3e63d --- /dev/null +++ b/_images_de/time-scaling.svg @@ -0,0 +1,673 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeit + Zeit + mehrBits proSekunde + + + + + + + Frequenz + + + + + Frequenz + mehrSpektrumbenötigt + diff --git a/_images_de/time-sync-constellation-animated.gif b/_images_de/time-sync-constellation-animated.gif new file mode 100644 index 00000000..fe0737e8 Binary files /dev/null and b/_images_de/time-sync-constellation-animated.gif differ diff --git a/_images_de/time-sync-constellation.svg b/_images_de/time-sync-constellation.svg new file mode 100644 index 00000000..7081f8c0 --- /dev/null +++ b/_images_de/time-sync-constellation.svg @@ -0,0 +1,1566 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Vor Zeitsynchronisation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Nach Zeitsynchronisation + + + + + + + + + + + diff --git a/_images_de/time-sync-interpolated-samples.svg b/_images_de/time-sync-interpolated-samples.svg new file mode 100644 index 00000000..398d6b65 --- /dev/null +++ b/_images_de/time-sync-interpolated-samples.svg @@ -0,0 +1,917 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Vor Interpolation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Nach Interpolation + + + + + + + + + + + diff --git a/_images_de/time-sync-original-data.svg b/_images_de/time-sync-original-data.svg new file mode 100644 index 00000000..db9398a6 --- /dev/null +++ b/_images_de/time-sync-original-data.svg @@ -0,0 +1,1710 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/time-sync-output.svg b/_images_de/time-sync-output.svg new file mode 100644 index 00000000..90ce2dff --- /dev/null +++ b/_images_de/time-sync-output.svg @@ -0,0 +1,3534 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/time-sync-output2.svg b/_images_de/time-sync-output2.svg new file mode 100644 index 00000000..0f74e3ff --- /dev/null +++ b/_images_de/time-sync-output2.svg @@ -0,0 +1,2193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/time-sync-pulse-shaped.svg b/_images_de/time-sync-pulse-shaped.svg new file mode 100644 index 00000000..96bc4d9b --- /dev/null +++ b/_images_de/time-sync-pulse-shaped.svg @@ -0,0 +1,835 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images_de/time_and_freq_domain_example_signals.png b/_images_de/time_and_freq_domain_example_signals.png new file mode 100644 index 00000000..58178529 Binary files /dev/null and b/_images_de/time_and_freq_domain_example_signals.png differ diff --git a/_images_de/translate_matplotlib_svgs.py b/_images_de/translate_matplotlib_svgs.py new file mode 100644 index 00000000..a369d117 --- /dev/null +++ b/_images_de/translate_matplotlib_svgs.py @@ -0,0 +1,380 @@ +""" +Post-process Matplotlib-generated SVGs to translate English axis labels to German. + +Matplotlib SVGs store text as glyph paths, not elements. This script: +1. Finds text groups identified by HTML comments () +2. Extracts the position from the transform attribute +3. Replaces the glyph group with an SVG element in German + +Supports three transform formats used by different matplotlib/SVG versions: + - translate(x y) scale(a b) + - translate(x y) rotate(angle) scale(a b) + - matrix(a,b,c,d,e,f) +""" +import re +import os +import glob + +# ========================================================================= +# Translation dictionary (English → German) +# ========================================================================= +TRANSLATIONS = { + # --- Axis labels --- + "Sample Index": "Sample-Index", + "Signal Amplitude": "Signalamplitude", + "FFT Magnitude": "FFT-Betrag", + "FFT Phase [radians]": "FFT-Phase [Radianten]", + "FFT Index": "FFT-Index", + "Frequency [Hz]": "Frequenz [Hz]", + "Frequency [MHz]": "Frequenz [MHz]", + "Frequency [kHz]": "Frequenz [kHz]", + "Frequency [GHz]": "Frequenz [GHz]", + "Frequency [Normalized Hz]": "Frequenz [norm. Hz]", + "Frequency Domain": "Frequenzbereich", + "Frequency Response of Filter": "Frequenzgang des Filters", + "Frequency offset (kHz)": "Frequenzversatz (kHz)", + "Frequency": "Frequenz", + "Time [s]": "Zeit [s]", + "Time Domain": "Zeitbereich", + "Time": "Zeit", + "Amplitude / Value": "Amplitude / Wert", + "Amplitude": "Amplitude", + "Magnitude [dB]": "Betrag [dB]", + "Magnitude": "Betrag", + "Power [linear]": "Leistung [linear]", + "Power Spectral Density [dB]": "Leistungsdichtespektrum [dB]", + "Power, dB": "Leistung [dB]", + "Power": "Leistung", + "Phase Estimate [degrees]": "Phasenschätzung [Grad]", + "Phase": "Phase", + "Sample Value (I)": "Samplewert (I)", + "Samples": "Samples", + "Sample": "Sample", + "Index": "Index", + "Angle [deg]": "Winkel [Grad]", + "Azimuth angle (deg)": "Azimutwinkel (Grad)", + "Azimuth angle [degrees]": "Azimutwinkel [Grad]", + "Elevation angle [degrees]": "Elevationswinkel [Grad]", + "Theta (azimuth, degrees)": "Theta (Azimut, Grad)", + "theta (20 deg)": "Theta (20 Grad)", + "Phi (elevation, degrees)": "Phi (Elevation, Grad)", + "Theta [Degrees]": "Theta [Grad]", + "Eigenvalue [dB]": "Eigenwert [dB]", + "Beam Pattern [dB]": "Strahlmuster [dB]", + "DOA Metric": "DOA-Metrik", + "Channel Magnitude [dB]": "Kanalbetrag [dB]", + "Signal Magnitude [Linear]": "Signalbetrag [linear]", + "Normalized Frequency": "Normierte Frequenz", + "Cyclic Frequency [Normalized Hz]": "Zyklische Frequenz [norm. Hz]", + "SCF Magnitude": "SCF-Betrag", + "SCF Power": "SCF-Leistung", + "CAF Power": "CAF-Leistung", + "CAF (real part)": "CAF (Realteil)", + "Tau": "Tau", + "Alpha": "Alpha", + "PSD [dB]": "LDS [dB]", + "PSD Before FM Demod [dB]": "LDS vor FM-Demod [dB]", + "PSD": "LDS", + "Shannon Limit [bits/s/Hz]": "Shannon-Grenze [Bits/s/Hz]", + "SNR [dB]": "SNR [dB]", + "SNR (dB)": "SNR (dB)", + "X Position [m]": "X-Position [m]", + "Y Position [m]": "Y-Position [m]", + "Z Position [m]": "Z-Position [m]", + "Correlation Magnitude [Linear]": "Korrelationsbetrag [linear]", + "Normalized Correlation Peak Magnitude": "Normierter Korrelations-Spitzenwert", + "Normalized correlation peak (dB)": "Normierter Korrelationspeak [dB]", + "Normalized Correlation": "Normierte Korrelation", + "Correlation Power": "Korrelationsleistung", + "Number of taps": "Anzahl der Koeffizienten", + "Time per call (ms)": "Zeit pro Aufruf (ms)", + "Code Phase (chips)": "Codephase (Chips)", + "Offset (Fraction of a Chip)": "Versatz (Chip-Bruchteil)", + "Freq Offset": "Frequenzversatz", + "Freq": "Frequenz", + "Pd": "Pd", + "Pfa": "Pfa", + "Period (1/Frequency)": "Periode (1/Frequenz)", + "20 degrees": "20 Grad", + + # --- Plot titles --- + "Beam Pattern and DOA Results, With Training": "Strahlmuster und DOA-Ergebnisse, mit Training", + "Beam Pattern and DOA Results, Without Training": "Strahlmuster und DOA-Ergebnisse, ohne Training", + "Conventional Pattern": "Konventionelles Muster", + "MVDR Pattern": "MVDR-Muster", + "Detection probability vs SNR for various frequency offsets": "Erkennungswahrscheinlichkeit vs. SNR", + "Correlation degradation vs frequency offset": "Korrelationsdegradation vs. Frequenzversatz", + "DSSS Correlation Peak vs. Fractional Chip Timing Offset": "DSSS-Korrelationspeak vs. Chip-Versatz", + "Preamble Correlator Output with Adaptive CFAR Threshold": "Präambel-Korrelationsausgang mit CFAR", + "ROC Curves": "ROC-Kurven", + "Probability of detection": "Erkennungswahrscheinlichkeit", + "Pd vs SNR (Pfa=0.01)": "Pd vs. SNR (Pfa=0,01)", + "SV 11 — Code-Phase Slice (Doppler = +2500 Hz)": "SV 11 — Codephase (Doppler = +2500 Hz)", + + # --- Legend / annotation labels --- + "Input Signal Length: 1000 samples": "Eingangssignallänge: 1000 Samples", + "Input Signal Length: 100000 samples": "Eingangssignallänge: 100000 Samples", + "Input": "Eingang", + "Output": "Ausgang", + "Error": "Fehler", + "Signal 1": "Signal 1", + "Signal 2": "Signal 2", + "Signal 3": "Signal 3", + "Real part of signal": "Realteil des Signals", + "Wireless Signal": "Drahtloses Signal", + "No Fading": "Kein Fading", + "Rayleigh Fading": "Rayleigh-Fading", + "Symbols": "Symbole", + "Decoded": "Dekodiert", + "Encoded": "Kodiert", + "Our Data": "Unsere Daten", + "Combined": "Kombiniert", + "Pulses (before being combined)": "Impulse (vor Kombination)", + "Starting With 0": "Beginnend mit 0", + "Starting With 1": "Beginnend mit 1", + "True Offset": "Wahrer Versatz", + "Perfect Alignment": "Perfekte Ausrichtung", + "After Costas Loop": "Nach Costas-Schleife", + "Before Costas Loop": "Vor Costas-Schleife", + "After Freq Offset": "Nach Frequenzversatz", + "Before Freq Offset": "Vor Frequenzversatz", + "After Interpolation": "Nach Interpolation", + "Before Interpolation": "Vor Interpolation", + "After Time Sync": "Nach Zeitsynchronisation", + "Before Time Sync": "Vor Zeitsynchronisation", + "Detections (Preamble Found)": "Erkennungen (Präambel gefunden)", + "CFAR Adaptive Threshold": "CFAR-Adaptivschwelle", + "0.15 Hz": "0,15 Hz", + "Zoomed in below": "Vergrößert unten", + "Regular SCF": "Reguläres SCF", + "Spectral Coherence Function (COH)": "Spektrale Kohärenzfunktion (COH)", + "Time-Domain Received Signal": "Empfangssignal im Zeitbereich", + "Correlator Output $|r(t) * p^*(-t)|^2$": "Korrelatorausgang", + "Rx Signal Power ($|r(t)|^2$)": "Empfangssignalleistung", + "Offset=0.0 kHz": "Versatz=0,0 kHz", + "Offset=2.0 kHz": "Versatz=2,0 kHz", + "Offset=5.0 kHz": "Versatz=5,0 kHz", +} + +# Window names are proper names, no translation needed: +# Hamming, Hanning, Blackman, Bartlett, Kaiser, Rectangular → Rechteck + + +def find_text_groups(content): + """Find matplotlib text groups by scanning for comment+transform patterns. + + Returns list of dicts with keys: label, tx, ty, rotate, scale, start, end, g_open + """ + results = [] + + # A text group looks like: + # + # + # [optional ...] + # + # ... glyph uses ... + # + # + # + # The transform can be: + # translate(x y) [rotate(angle)] scale(a b) + # matrix(a,b,c,d,e,f) where (e,f) = translate and (a,d) = scale + + # Build combined regex that handles both transform types + TRANSFORM_PAT = ( + r'(?:' + r'translate\(([^)]+)\)(?:\s*rotate\(([^)]+)\))?\s*scale\(([^)]+)\)' # variant 1 + r'|' + r'matrix\(([0-9.eE+\-]+),([0-9.eE+\-]+),([0-9.eE+\-]+),([0-9.eE+\-]+),([0-9.eE+\-]+),([0-9.eE+\-]+)\)' # variant 2 + r')' + ) + + pattern = re.compile( + r'(]*id="text_\d+"[^>]*>)\s*' # outer g with text id + r'\s*' # comment = label + r'(?:]*>.*?\s*)?' # optional inline defs + r']*transform="' + TRANSFORM_PAT + r'"', + re.DOTALL + ) + + for m in pattern.finditer(content): + label = m.group(2).strip() + g_open = m.group(1) + + # Groups 3-5: translate variant; groups 6-11: matrix variant + if m.group(3) is not None: # translate variant + pos_parts = m.group(3).split() + rotate_str = m.group(4) + scale_str = m.group(5) or "0.1 -0.1" + try: + tx, ty = float(pos_parts[0]), float(pos_parts[1]) + except (ValueError, IndexError): + continue + else: # matrix variant + try: + ma = float(m.group(6)) # scale_x + # mb = m.group(7) # 0 normally + # mc = m.group(8) # 0 normally + md = float(m.group(9)) # scale_y (negative = y-flip) + tx = float(m.group(10)) + ty = float(m.group(11)) + except (ValueError, TypeError): + continue + rotate_str = None + scale_str = f"{abs(ma)} {abs(md)}" + + # Find end of the outer group + start = m.start() + depth = 0 + group_end = -1 + for i in range(start, min(start + 50000, len(content))): + if content[i:i+2] == '': + depth -= 1 + if depth == 0: + group_end = i + 4 + break + if group_end == -1: + continue + + results.append({ + 'label': label, + 'tx': tx, + 'ty': ty, + 'rotate': rotate_str, + 'scale': scale_str, + 'start': start, + 'end': group_end, + 'g_open': g_open, + }) + + return results + + +def estimate_font_size(scale_str): + """Estimate font-size in SVG units from the scale string.""" + parts = scale_str.split() + try: + scale = abs(float(parts[0])) + return max(6.0, scale * 100) + except (ValueError, IndexError): + return 10.0 + + +def make_text_element(group_info, german_text, font_size): + """Build an SVG element to replace a glyph group.""" + tx = group_info['tx'] + ty = group_info['ty'] + rotate = group_info['rotate'] + + id_match = re.search(r'id="([^"]+)"', group_info['g_open']) + group_id = id_match.group(1) if id_match else 'text_x' + + safe_text = (german_text + .replace('&', '&') + .replace('<', '<') + .replace('>', '>')) + + if rotate is not None: + transform = f'translate({tx:.3f},{ty:.3f}) rotate({rotate})' + return ( + f'' + f'{safe_text}' + f'' + ) + else: + return ( + f'' + f'{safe_text}' + f'' + ) + + +def process_matplotlib_svg(filepath, translations): + """Translate text labels in a matplotlib SVG file. Returns True if changed.""" + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + + # Only process matplotlib SVGs (glyph-path based) + if 'DejaVuSans' not in content and 'DejaVu Sans' not in content: + return False + + groups = find_text_groups(content) + if not groups: + return False + + # Collect replacements (apply in reverse order to preserve offsets) + replacements = [] + for g in groups: + german = translations.get(g['label']) + if german and german != g['label']: + fs = estimate_font_size(g['scale']) + replacements.append((g['start'], g['end'], make_text_element(g, german, fs))) + + if not replacements: + return False + + # Apply in reverse order + replacements.sort(key=lambda x: x[0], reverse=True) + result = content + for start, end, elem in replacements: + result = result[:start] + elem + result[end:] + + with open(filepath, 'w', encoding='utf-8') as f: + f.write(result) + return True + + +# SVGs already handled by translate_svgs.py (real elements) +REAL_TEXT_FILES = { + 'Costas_loop_model.svg', 'Spherical_Coordinates.svg', 'ad9361.svg', + 'adaptive_mcs.svg', 'adaptive_mcs2.svg', 'atmospheric_attenuation.svg', + 'bandpass_filter_taps.svg', 'beamforming_examples.svg', 'beamforming_taxonomy.svg', + 'bpsk.svg', 'bpsk2.svg', 'costas-loop-freq-tracking.svg', 'differential_coding2.svg', + 'doa.svg', 'doa_trig.svg', 'ethernet.svg', 'fft-block-diagram.svg', 'fft-io.svg', + 'fft-python3.svg', 'fft-python4.svg', 'fm_psd.svg', 'fm_psd_labeled.svg', + 'freq-shift-diagram.svg', 'freq-shift.svg', 'fsk2.svg', 'hamming.svg', 'hamming2.svg', + 'masking.svg', 'max_freq.svg', 'monopulse.svg', 'multipath.svg', 'multipath2.svg', + 'negative-frequencies.svg', 'negative-frequencies2.svg', 'negative-frequencies3.svg', + 'rayleigh.svg', 'receiver_arch_diagram.svg', 'spectrogram_diagram.svg', + 'splitting_rc_filter.svg', 'sync-diagram.svg', 'time-scaling.svg', 'trellis.svg', + 'two-signals.svg', 'tx_rx_chain.svg', 'tx_rx_system.svg', 'tx_rx_system_params.svg', +} + + +if __name__ == '__main__': + images_dir = os.path.dirname(os.path.abspath(__file__)) + changed = 0 + unchanged = 0 + + # Files with ONLY real text (no matplotlib glyphs) — skip entirely + PURE_REAL_TEXT = { + 'Costas_loop_model.svg', 'Spherical_Coordinates.svg', 'ad9361.svg', + 'adaptive_mcs.svg', 'adaptive_mcs2.svg', 'atmospheric_attenuation.svg', + 'bandpass_filter_taps.svg', 'beamforming_examples.svg', 'beamforming_taxonomy.svg', + 'bpsk.svg', 'bpsk2.svg', 'differential_coding2.svg', 'doa.svg', 'doa_trig.svg', + 'ethernet.svg', 'fft-block-diagram.svg', 'fft-io.svg', 'fft-python3.svg', + 'fft-python4.svg', 'freq-shift-diagram.svg', 'freq-shift.svg', 'fsk2.svg', + 'hamming.svg', 'hamming2.svg', 'masking.svg', 'monopulse.svg', 'multipath.svg', + 'multipath2.svg', 'negative-frequencies.svg', 'negative-frequencies2.svg', + 'negative-frequencies3.svg', 'receiver_arch_diagram.svg', 'spectrogram_diagram.svg', + 'splitting_rc_filter.svg', 'sync-diagram.svg', 'time-scaling.svg', 'trellis.svg', + 'two-signals.svg', 'tx_rx_chain.svg', 'tx_rx_system.svg', 'tx_rx_system_params.svg', + } + + for fpath in sorted(glob.glob(os.path.join(images_dir, '*.svg'))): + fname = os.path.basename(fpath) + if fname in PURE_REAL_TEXT or fname.startswith('translate_'): + continue + if process_matplotlib_svg(fpath, TRANSLATIONS): + print(f' TRANSLATED: {fname}') + changed += 1 + else: + unchanged += 1 + + print(f'\nDone: {changed} matplotlib SVGs translated, {unchanged} unchanged.') diff --git a/_images_de/translate_svgs.py b/_images_de/translate_svgs.py new file mode 100644 index 00000000..48ac3269 --- /dev/null +++ b/_images_de/translate_svgs.py @@ -0,0 +1,366 @@ +""" +Translate English text in SVG files to German. +Handles two types: + 1. SVGs with real / elements (46 files) + 2. Matplotlib SVGs with comments - these require script regeneration +""" +import re +import os +import glob + +# Translation dictionary for SVG text labels +# Keys are exact English strings, values are German translations +# Order matters: longer/more specific strings first to avoid partial replacements +TRANSLATIONS = { + # === AXIS LABELS === + "Frequency [GHz]": "Frequenz [GHz]", + "Frequency [MHz]": "Frequenz [MHz]", + "Frequency [kHz]": "Frequenz [kHz]", + "Frequency [Hz]": "Frequenz [Hz]", + "Frequency [Normalized Hz]": "Frequenz [normiert Hz]", + "Frequency Domain": "Frequenzbereich", + "Frequency Response of Filter": "Frequenzgang des Filters", + "Attenuation [dB / km]": "Dämpfung [dB / km]", + "Channel Magnitude [dB]": "Kanalbetrag [dB]", + "Signal Amplitude": "Signalamplitude", + "Signal Magnitude [Linear]": "Signalbetrag [linear]", + "Power Spectral Density [dB]": "Leistungsdichtespektrum [dB]", + "Magnitude [dB]": "Betrag [dB]", + "Beam Pattern [dB]": "Strahlmuster [dB]", + "FFT Magnitude": "FFT-Betrag", + "FFT Phase [radians]": "FFT-Phase [Radianten]", + "FFT Index": "FFT-Index", + "Sample Index": "Sample-Index", + "Time [s]": "Zeit [s]", + "Time Domain": "Zeitbereich", + "Eigenvalue [dB]": "Eigenwert [dB]", + "Azimuth angle (deg)": "Azimutwinkel (Grad)", + "Azimuth angle [degrees]": "Azimutwinkel [Grad]", + "Elevation angle [degrees]": "Elevationswinkel [Grad]", + "Theta [Degrees]": "Theta [Grad]", + "Angle [deg]": "Winkel [Grad]", + "B bandwidth [Hz]": "B Bandbreite [Hz]", + "T seconds": "T Sekunden", + "X Position [m]": "X Position [m]", + "Y Position [m]": "Y Position [m]", + "Z Position [m]": "Z Position [m]", + "Shannon Limit [bits/s/Hz]": "Shannon-Grenze [Bits/s/Hz]", + "Correlation Magnitude [Linear]": "Korrelationsbetrag [linear]", + + # === PLOT TITLES / DESCRIPTIONS === + "Beamforming Taxonomy": "Strahlformungs-Taxonomie", + "Direction of Arrival (DOA)": "Einfallsrichtung (DOA)", + "Direction of Arrival": "Einfallsrichtung", + "Beam Pattern and DOA Results, With Training": "Strahlmuster und DOA-Ergebnisse, mit Training", + "Beam Pattern and DOA Results, Without Training": "Strahlmuster und DOA-Ergebnisse, ohne Training", + "Time Adaptive Processing (STAP)": "Raum-Zeit-adaptive Verarbeitung (STAP)", + "Spectrogram": "Spektrogramm", + "Time Domain": "Zeitbereich", + "Modulation Scheme Used": "Verwendetes Modulationsschema", + + # === SYSTEM COMPONENTS === + "Direct Sampling (a.k.a. Direct RF)": "Direktabtastung (auch: Direct RF)", + "Direct Conversion (a.k.a. Zero IF)": "Direktmischung (auch: Zero IF)", + "Superheterodyne": "Überlagerungsempfänger", + "FM radio in your old car": "UKW-Radio im alten Auto", + "Expensive": "Teuer", + "expensive ADC": "teurer ADC", + "Digital Filtering and Equalization": "Digitale Filterung und Entzerrung", + "RF Channel Bandwidth": "HF-Kanalbandbreite", + "Rx Decimation": "Empfänger-Dezimation", + "Tx Interpolation": "Sender-Interpolation", + "Calibration and": "Kalibrierung und", + "Correction": "Korrektur", + "Enable State": "Zustandsmaschine", + "Machine (ENSM)": "", + "Automatic": "Automatische", + "Phase\nSplitter": "Phasenteiler", + "Input Mux": "Eingangs-Mux", + "Output Mux": "Ausgangs-Mux", + "Rx Channel 1": "Empfangskanal 1", + "Rx Channel 2": "Empfangskanal 2", + "Tx Channel 1": "Sendekanal 1", + "Tx Channel 2": "Sendekanal 2", + "Temperature": "Temperatur", + "Baseband": "Basisband", + "Splitter": "Teiler", + "Dual": "Dual", + + # === SIGNAL FLOW === + "Transmit Antenna": "Sendeantenne", + "TX Antenna": "Sendeantenne", + "RX Antenna": "Empfangsantenne", + "Transmitter": "Sender", + "Receiver": "Empfänger", + "Transmit\nPower": "Sendeleistung", + "Transmit": "Senden", + "Received": "Empfangen", + "LOS Path": "Direktweg", + "Multipath": "Mehrwegausbreitung", + "Path Loss": "Pfadverlust", + "(Compression)": "(Kompression)", + "(Error correcting": "(Fehlerkorrektur", + "codes)": "Codes)", + "PSK, QAM)": "PSK, QAM)", + "RF Circuit": "HF-Schaltung", + "Up": "Aufwärts", + "Converter,": "Wandler,", + "Amplifiers)": "Verstärker)", + "Down": "Abwärts", + "Digital Converter)": "Digitalwandler)", + "Converter)": "Wandler)", + "Synchronization and": "Synchronisation und", + "processing": "Verarbeitung", + "often happens here": "oft hier durchgeführt", + "Source\nData": "Quelldaten", + "Source": "Quelle", + "Data": "Daten", + "(hopefully)": "(hoffentlich)", + "Wireless": "Drahtlos", + + # === BEAMFORMING === + "Conventional Beamformer": "Konventioneller Strahlformer", + "(aka Delay and Sum)": "(auch: Delay-and-Sum)", + "Null Steering": "Nullsteuerung", + "Switched Beam": "Geschaltete Strahlung", + "Spatial Multiplexing": "Räumliches Multiplexing", + "Pattern Synthesis": "Mustersynth.", + "Subspace": "Unterraum", + "Traditional": "Traditionell", + "(Data Independent/Deterministic)": "(Datenunabhängig/Deterministisch)", + "Adaptive": "Adaptiv", + "Iterative": "(Snapshot/Update-basiert)", + "(Snapshot/Update Based)": "(Snapshot/Update-basiert)", + "Block\nbased": "Blockbasiert", + "Tapering": "Fensterfunktionen", + "(Optional Addon)": "(Optionale Erweiterung)", + "Input includes": "Eingang beinhaltet", + "(expected) angle": "(erwarteten) Winkel", + "of SOI": "der Nutzquelle", + "Needs pilots/exact": "Braucht Pilots/exaktes", + "Most techniques with": "Die meisten Techniken unter", + "under": "", + "Beamforming": "Strahlformung", + "can": "können", + "be directly used to perform": "direkt zur DOA-Bestimmung verwendet werden", + "Space": "Raum-", + "Blind": "Blind", + "Sidelobe": "Nebenkeulen-", + "Canceller": "Unterdrücker", + "Multiple Sidelobe": "Mehrfach-Nebenkeulen-", + "Decomposition aka": "Zerlegung aka", + "Max SNR": "Max SNR", + "Max SINR": "Max SINR", + "Dynamic Multiple": "Dynamischer Mehrfach-", + "Sidelobe Canceller": "Nebenkeule-Unterdrücker", + "Woodward Lawson": "Woodward-Lawson", + "Technique": "Technik", + "Max": "Max", + "Likelihood": "Likelihood", + "Beamform": "Strahl-", + "ers": "former", + "based": "basiert", + + # === PLOT ELEMENTS === + "Noise Floor": "Rauschpegel", + "Signal(s)": "Signal(e)", + "Signal we don't want": "Unerwünschtes Signal", + "Signal in": "Eingangssignal", + "Signal 1": "Signal 1", + "Signal 2": "Signal 2", + "Signal 3": "Signal 3", + "FFT Shift": "FFT-Verschiebung", + "FFT of Slice 1": "FFT von Abschnitt 1", + "FFT of Slice 2": "FFT von Abschnitt 2", + "FFT of Slice 3": "FFT von Abschnitt 3", + "FFT of Slice 4": "FFT von Abschnitt 4", + "FFT of Slice 5": "FFT von Abschnitt 5", + "FFT of Slice 6": "FFT von Abschnitt 6", + "Slice\n1": "Abschnitt 1", + "Slice 2": "Abschnitt 2", + "Slice 3": "Abschnitt 3", + "Slice 4": "Abschnitt 4", + "Slice 5": "Abschnitt 5", + "Slice 6": "Abschnitt 6", + "Slice": "Abschnitt", + "(IQ Samples)": "(IQ-Samples)", + "Stereo Audio (L-R)": "Stereo-Audio (L-R)", + "Mono": "Mono", + "Audio": "Audio", + "Tone": "Ton", + "Bit Position": "Bitposition", + "Encoded Bits": "Kodierte Bits", + "Encoded": "Kodiert", + "Decoded": "Dekodiert", + "Parity": "Parität", + "Coverage": "Abdeckung", + "boresight": "Hauptstrahlrichtung", + "Target": "Ziel", + "Beam 1": "Strahl 1", + "Beam 2": "Strahl 2", + '"Sum"': '"Summe"', + "Beam": "Strahl", + "Taps": "Koeffizienten", + "Magnitude": "Betrag", + "Sample Rate": "Abtastrate", + "Sample:": "Sample:", + "Sample\n": "Sample\n", + "Input Vector": "Eingangsvektor", + "Input 1": "Eingang 1", + "Input 2": "Eingang 2", + "Input": "Eingang", + "Output": "Ausgang", + "Delay": "Verzögerung", + "Filter": "Filter", + "Channel": "Kanal", + "Modulation": "Modulation", + "Demodulation": "Demodulation", + "Pulse": "Impuls", + "Matched": "Angepasst", + "Coarse": "Grob", + "Time Sync": "Zeitsynchronisation", + "Fine Freq": "Feine Frequenz", + "Frame": "Rahmen", + "Gain": "Verstärkung", + "Power": "Leistung", + + # === PARTIAL WORDS (for axis labels split across elements) === + "requency": "requenz", + "ime": "eit", + "frequency": "Frequenz", + "time": "Zeit", + "spectrum": "Spektrum", + "signal": "Signal", + "shifted": "verschoben", + "sinusoid": "Sinusschwingung", + "shift": "Verschiebung", + "more\nbits per\nsecond": "mehr\nBits pro\nSekunde", + "more\nspectrum\nrequired": "mehr\nSpektrum\nbenötigt", + "bits per": "Bits pro", + "second": "Sekunde", + "more": "mehr", + "required": "benötigt", + "index:": "Index:", + "modulated\noutput": "modulierter\nAusgang", + "modulated": "moduliert", + "output": "Ausgang", + + # === ADAPTIVE MCS === + "Throughpu": "Durchsat", + "(L+R)": "(L+R)", + + # === SPHERICAL COORDINATES === + "(azimuth)": "(Azimut)", + "(elevation)": "(Elevation)", + + # === TRELLIS === + "Level j=": "Ebene j=", + + # === REMAINING LOWERCASE / SPECIFIC CASES === + "Received": "Empfangen", + "TX Antenna": "Sendeantenne", + "RX Antenna": "Empfangsantenne", + "Transmit\nPower": "Sendeleistung", + "Automatic\nGain\nControl": "Automatische\nVerstärkungs-\nRegelung", +} + +# Files where we should NOT translate "T" → "Z" standalone letter (it's part of Time axis) +# We handle spectrogram_diagram.svg specially +SPECTROGRAM_SPECIAL = { + ">T<": ">Z<", # Only the T that's part of "Time" axis +} + + +def translate_text_content(text, context=""): + """Translate English text to German.""" + # Try exact match first (longest first) + for en, de in sorted(TRANSLATIONS.items(), key=lambda x: -len(x[0])): + if text == en: + return de + return text + + +def translate_svg_text_elements(content, filename=""): + """Replace English text in SVG text nodes with German translations. + + Uses targeted replacement: for each (en, de) pair we replace all occurrences of + '>en<' and '>en ' (text node delimiters), which only matches actual XML text nodes, + not attribute values (which are enclosed in quotes) or path data. + """ + result = content + + # Apply translations ordered by length (longest first to avoid partial matches) + for en, de in sorted(TRANSLATIONS.items(), key=lambda x: -len(x[0])): + if not en or not de or en == de: + continue + + # Replace as exact text node: >en< or >en (followed by < or newline/space then <) + # This covers: en and en + result = result.replace(f'>{en}<', f'>{de}<') + result = result.replace(f'>{en} <', f'>{de} <') + result = result.replace(f'>{en}\n', f'>{de}\n') + result = result.replace(f'>{en}\r\n', f'>{de}\r\n') + + # Special handling for spectrogram_diagram.svg: "T" axis label → "Z" + if 'spectrogram_diagram' in filename: + result = result.replace( + 'matrix(-1.83697e-16 -1 1 -1.83697e-16 113.526 276)">T', + 'matrix(-1.83697e-16 -1 1 -1.83697e-16 113.526 276)">Z' + ) + + return result + + +def process_svg_file(filepath): + """Translate a single SVG file.""" + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + + original = content + filename = os.path.basename(filepath) + translated = translate_svg_text_elements(content, filename) + + if translated != original: + with open(filepath, 'w', encoding='utf-8') as f: + f.write(translated) + return True + return False + + +# List of SVGs with real text elements (directly translatable) +REAL_TEXT_FILES = [ + 'Costas_loop_model.svg', 'Spherical_Coordinates.svg', 'ad9361.svg', + 'adaptive_mcs.svg', 'adaptive_mcs2.svg', 'atmospheric_attenuation.svg', + 'bandpass_filter_taps.svg', 'beamforming_examples.svg', 'beamforming_taxonomy.svg', + 'bpsk.svg', 'bpsk2.svg', 'costas-loop-freq-tracking.svg', 'differential_coding2.svg', + 'doa.svg', 'doa_trig.svg', 'ethernet.svg', 'fft-block-diagram.svg', 'fft-io.svg', + 'fft-python3.svg', 'fft-python4.svg', 'fm_psd.svg', 'fm_psd_labeled.svg', + 'freq-shift-diagram.svg', 'freq-shift.svg', 'fsk2.svg', 'hamming.svg', 'hamming2.svg', + 'masking.svg', 'max_freq.svg', 'monopulse.svg', 'multipath.svg', 'multipath2.svg', + 'negative-frequencies.svg', 'negative-frequencies2.svg', 'negative-frequencies3.svg', + 'rayleigh.svg', 'receiver_arch_diagram.svg', 'spectrogram_diagram.svg', + 'splitting_rc_filter.svg', 'sync-diagram.svg', 'time-scaling.svg', 'trellis.svg', + 'two-signals.svg', 'tx_rx_chain.svg', 'tx_rx_system.svg', 'tx_rx_system_params.svg' +] + + +if __name__ == '__main__': + images_dir = os.path.dirname(os.path.abspath(__file__)) + + changed = 0 + unchanged = 0 + + for fname in REAL_TEXT_FILES: + fpath = os.path.join(images_dir, fname) + if not os.path.exists(fpath): + print(f' MISSING: {fname}') + continue + + if process_svg_file(fpath): + print(f' TRANSLATED: {fname}') + changed += 1 + else: + print(f' unchanged: {fname}') + unchanged += 1 + + print(f'\nDone: {changed} files translated, {unchanged} unchanged.') diff --git a/_images_de/trellis.svg b/_images_de/trellis.svg new file mode 100644 index 00000000..f0416fe6 --- /dev/null +++ b/_images_de/trellis.svg @@ -0,0 +1,1316 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 00 + + 00 + + 00 + + 00 + + 11 + + 11 + + 01 + + 01 + + 01 + + 01 + + 01 + + 01 + + 01 + + 01 + + 01 + + 01 + + 10 + + 10 + + 10 + + 10 + + 10 + + 10 + + 10 + + 10 + + 10 + + 11 + + 11 + + 11 + + 11 + + 11 + + 11 + + 11 + + 11 + + 11 + + 00 + + 00 + + 00 + + 00 + + 00 + + 00 + + 00 + + 00 + a + b + c + d + Ebene j=0 + 1 + 2 + 3 + 4 + 5 + L-1 + L + L+1 + L+2 + + diff --git a/_images_de/two-signals.svg b/_images_de/two-signals.svg new file mode 100644 index 00000000..6486a684 --- /dev/null +++ b/_images_de/two-signals.svg @@ -0,0 +1 @@ +FrequenzStörsignalNutzsignal \ No newline at end of file diff --git a/_images_de/tx_rx_chain.svg b/_images_de/tx_rx_chain.svg new file mode 100644 index 00000000..a8f449ec --- /dev/null +++ b/_images_de/tx_rx_chain.svg @@ -0,0 +1 @@ +QuelleEncoding(Kompression)KanalEncoding(FehlerkorrekturCodes)Modulation(e.g., PSK, QAM)HF-Schaltung(Filters,Aufwärts-Wandler,Verstärker)DatenDAC(Digital-to-Analog Wandler)HF-Schaltung(Filters,Abwärts-Wandler,Verstärker)ADC(Analog-to-Digitalwandler)DemodulationQuelle DecodingDaten(hoffentlich)KanalDecodingDrahtlosKanalSendeantenneSynchronisation und other Verarbeitungoft hier durchgeführtEmpfangsantenne \ No newline at end of file diff --git a/_images_de/tx_rx_system.svg b/_images_de/tx_rx_system.svg new file mode 100644 index 00000000..3c240e82 --- /dev/null +++ b/_images_de/tx_rx_system.svg @@ -0,0 +1,129 @@ + + + + + + image/svg+xml + + + + + + + + + + + + SenderEmpfänger + + + + + + + + + + + diff --git a/_images_de/tx_rx_system_params.svg b/_images_de/tx_rx_system_params.svg new file mode 100644 index 00000000..d1a4b668 --- /dev/null +++ b/_images_de/tx_rx_system_params.svg @@ -0,0 +1 @@ +Sendeantenne VerstärkungGtSenderEmpfängerEmpfangsantenneVerstärkungGrSendenLeistungPtPfadverlustLpEmpfangen LeistungPr \ No newline at end of file diff --git a/_images_de/usrp.png b/_images_de/usrp.png new file mode 100644 index 00000000..fcaf2bf9 Binary files /dev/null and b/_images_de/usrp.png differ diff --git a/_images_de/waterfall.png b/_images_de/waterfall.png new file mode 100644 index 00000000..c168d35d Binary files /dev/null and b/_images_de/waterfall.png differ diff --git a/_images_de/weird_psk.png b/_images_de/weird_psk.png new file mode 100644 index 00000000..98c434e6 Binary files /dev/null and b/_images_de/weird_psk.png differ diff --git a/_images_de/wifi-frame.png b/_images_de/wifi-frame.png new file mode 100644 index 00000000..f5fa9118 Binary files /dev/null and b/_images_de/wifi-frame.png differ diff --git a/_images_de/windows.svg b/_images_de/windows.svg new file mode 100644 index 00000000..479a4e53 --- /dev/null +++ b/_images_de/windows.svg @@ -0,0 +1,1862 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Amplitude / Wert + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zeitbereich + + + + + + + + + Rechteck + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Normierte Frequenz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Frequenzbereich + + + + + + + + + + + diff --git a/_static/de.svg b/_static/de.svg new file mode 100644 index 00000000..399dcced --- /dev/null +++ b/_static/de.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/_templates/homepage_de.html b/_templates/homepage_de.html new file mode 100644 index 00000000..f02ff7a0 --- /dev/null +++ b/_templates/homepage_de.html @@ -0,0 +1,59 @@ +

PySDR: Ein Leitfaden zu SDR und DSP mit Python

+ +

+ von + + Dr. Marc Lichtman + + - + + pysdr@vt.edu + +

+ +

+ Willkommen bei PySDR, einem kostenlosen Online-Lehrbuch (keine Python-Bibliothek!), das eine sanfte Einführung in die drahtlose Kommunikation und Software-Defined Radio (SDR) bietet – mit zahlreichen Diagrammen, Animationen und Python-Codebeispielen. Von FFTs über Filter bis hin zu digitaler Modulation sowie dem Empfangen und Senden mit SDRs in Python – PySDR hat alles abgedeckt! +

+ +

+ Das Ziel von PySDR ist es, den Zugang zu Themen zu erleichtern, die traditionell auf sehr mathematische Weise und nur an einer kleinen Anzahl von Universitäten gelehrt werden. Alle Inhalte, die zur Erstellung von PySDR verwendet werden, sind Open Source und können + hier gefunden werden. +

+ +

+ Siehe + Kapitel 1: Einführung + für den Zweck und die Zielgruppe des Lehrbuchs. +

+ +

+ Für einen ersten Einblick in die HF-Signalverarbeitung kann mit der folgenden Simulation gespielt werden, die den Frequenz- und Zeitbereich eines Signals zeigt, das aus einem Ton und weißem Gaußschem Rauschen besteht. +

+ +
+ + + +
+
+ + + +
+
+ +
+
+
+
+ +
+ + + +
diff --git a/_templates/layout.html b/_templates/layout.html index da78e0bd..c1211441 100644 --- a/_templates/layout.html +++ b/_templates/layout.html @@ -76,12 +76,14 @@  |  EnglishEnglish   - DutchDutch   + DutchDutch   FrenchFrench   UkrainianUkrainian   ChineseChinese   SpanishSpanish   - JapaneseJapanese + JapaneseJapanese   + GermanGerman + {% endblock %} diff --git a/conf.py b/conf.py index 9b2ba809..2a29f4b6 100644 --- a/conf.py +++ b/conf.py @@ -70,7 +70,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build', 'index-fr.rst', 'content-fr/*', 'index-nl.rst', 'content-nl/*', 'index-ukraine.rst', 'content-ukraine/*', 'index-zh.rst', 'content-zh/*', 'index-es.rst', 'content-es/*', 'index-ja.rst', 'content-ja/*'] +exclude_patterns = ['_build', 'venv', 'venv/**', 'index-de.rst', 'content-de/*', 'index-fr.rst', 'content-fr/*', 'index-nl.rst', 'content-nl/*', 'index-ukraine.rst', 'content-ukraine/*', 'index-zh.rst', 'content-zh/*', 'index-es.rst', 'content-es/*', 'index-ja.rst', 'content-ja/*'] # The reST default role (used for this markup: `text`) to use for all # documents. diff --git a/content-de/2d_beamforming.rst b/content-de/2d_beamforming.rst new file mode 100644 index 00000000..aaceac3a --- /dev/null +++ b/content-de/2d_beamforming.rst @@ -0,0 +1,564 @@ +.. _2d-beamforming-chapter: + +#################### +2D-Beamforming +#################### + +Dieses Kapitel erweitert das 1D-Beamforming/DOA-Kapitel auf 2D-Arrays. Wir beginnen mit einem einfachen Rechteck-Array und entwickeln die Steuervektorgleichung und den MVDR-Beamformer, dann arbeiten wir mit echten Daten von einem 3×5-Array. Abschließend verwenden wir das interaktive Werkzeug, um die Auswirkungen verschiedener Array-Geometrien und Elementabstände zu erkunden. + +************************************* +Rechteck-Arrays und 2D-Beamforming +************************************* + +Rechteck-Arrays (a.k.a. planare Arrays) bestehen aus einem 2D-Array von Elementen. Mit einer zusätzlichen Dimension erhalten wir etwas mehr Komplexität, aber dieselben Grundprinzipien gelten, und der schwierigste Teil ist die Visualisierung der Ergebnisse (z.B. keine einfachen Polarplots mehr, jetzt brauchen wir 3D-Oberflächenplots). Obwohl unser Array jetzt 2D ist, müssen wir nicht jede Datenstruktur um eine Dimension erweitern. Zum Beispiel behalten wir unsere Gewichte als 1D-Array komplexer Zahlen bei. Wir müssen jedoch die Positionen unserer Elemente in 2D darstellen. Wir verwenden weiterhin :code:`theta` für den Azimutwinkel, aber jetzt führen wir einen neuen Winkel :code:`phi` als Elevationswinkel ein. Es gibt viele sphärische Koordinatenkonventionen, aber wir verwenden folgende: + +.. image:: ../_images/Spherical_Coordinates.svg + :align: center + :target: ../_images/Spherical_Coordinates.svg + :alt: Sphärisches Koordinatensystem mit theta und phi + +Was entspricht: + +.. math:: + + x = \sin(\theta) \cos(\phi) + + y = \cos(\theta) \cos(\phi) + + z = \sin(\phi) + +Wir wechseln auch zu einer verallgemeinerten Steuervektorgleichung, die nicht spezifisch für eine Array-Geometrie ist: + +.. math:: + + s = e^{2j \pi \boldsymbol{p} u / \lambda} + +wobei :math:`\boldsymbol{p}` die Menge der x/y/z-Elementpositionen in Metern ist (Größe :code:`Nr` x 3) und :math:`u` der Richtungseinheitsvektor in x/y/z ist (Größe 3x1). In Python sieht das so aus: + +.. code-block:: python + + def steering_vector(pos, dir): + # Nrx3 3x1 + return np.exp(2j * np.pi * pos @ dir / wavelength) # gibt Nr x 1 aus (Spaltenvektor) + +Versuchen wir, diese verallgemeinerte Steuervektorgleichung mit einem einfachen ULA mit 4 Elementen zu verwenden, um die Verbindung zu dem herzustellen, was wir bisher gelernt haben. Wir stellen :code:`d` jetzt in Metern statt relativ zur Wellenlänge dar. Wir platzieren die Elemente entlang der y-Achse: + +.. code-block:: python + + Nr = 4 + fc = 5e9 + wavelength = 3e8 / fc + d = 0.5 * wavelength # in Metern + + # Elementpositionen als Liste von (x,y,z)-Koordinaten, auch wenn es nur ein ULA entlang der y-Achse ist + pos = np.zeros((Nr, 3)) # Elementpositionen als Liste von x,y,z-Koordinaten in Metern + for i in range(Nr): + pos[i,0] = 0 # x-Position + pos[i,1] = d * i # y-Position + pos[i,2] = 0 # z-Position + +Die folgende Grafik zeigt eine Draufsicht des ULA mit einem Beispiel-Theta von 20 Grad. + +.. image:: ../_images/2d_beamforming_ula.svg + :align: center + :target: ../_images/2d_beamforming_ula.svg + :alt: ULA mit Theta von 20 Grad + +Das Einzige, was noch fehlt, ist die Verbindung unseres alten :code:`theta` mit diesem neuen Einheitsvektoransatz. Wir können :code:`dir` basierend auf :code:`theta` leicht berechnen. Wir wissen, dass die x- und z-Komponente unseres Einheitsvektors 0 sein wird, da wir uns noch im 1D-Raum befinden, und basierend auf unserer sphärischen Koordinatenkonvention ist die y-Komponente :code:`np.cos(theta)`, also lautet der vollständige Code :code:`dir = np.asmatrix([0, np.cos(theta_i), 0]).T`. An diesem Punkt solltest du die Verbindung zwischen unserer verallgemeinerten Steuervektorgleichung und der ULA-Steuervektorgleichung herstellen können. Probiere diesen neuen Code aus, wähle ein :code:`theta` zwischen 0 und 360 Grad (vergiss die Umrechnung in Bogenmaß!), und der Steuervektor sollte ein 4x1-Array sein. + +Gehen wir jetzt zum 2D-Fall über. Wir platzieren unser Array in der X-Z-Ebene, mit Boresight horizontal in Richtung der positiven y-Achse zeigend (:math:`\theta = 0`, :math:`\phi = 0`). Wir verwenden denselben Elementabstand wie zuvor, haben jetzt aber insgesamt 16 Elemente: + +.. code-block:: python + + # Jetzt auf 2D umsteigen, mit einem 4x4-Array mit halbem Wellenlängenabstand, also 16 Elemente insgesamt + Nr = 16 + + # Elementpositionen als Liste von x,y,z-Koordinaten in Metern, Array in der X-Z-Ebene + pos = np.zeros((Nr,3)) + for i in range(Nr): + pos[i,0] = d * (i % 4) # x-Position + pos[i,1] = 0 # y-Position + pos[i,2] = d * (i // 4) # z-Position + +Die Draufsicht unseres rechteckigen 4×4-Arrays: + +.. image:: ../_images/2d_beamforming_element_pos.svg + :align: center + :target: ../_images/2d_beamforming_element_pos.svg + :alt: Rechteckige Array-Elementpositionen + +Um auf ein bestimmtes Theta und Phi zu zeigen, müssen wir diese Winkel in einen Einheitsvektor umrechnen. Wir können dieselbe verallgemeinerte Steuervektorgleichung wie zuvor verwenden, müssen aber jetzt den Einheitsvektor basierend auf Theta und Phi berechnen, mithilfe der Gleichungen am Anfang dieses Kapitels: + +.. code-block:: python + + # In eine beliebige Richtung zeigen + theta = np.deg2rad(60) # Azimutwinkel + phi = np.deg2rad(30) # Elevationswinkel + + # Mit unserer sphärischen Koordinatenkonvention den Einheitsvektor berechnen: + def get_unit_vector(theta, phi): # Winkel in Bogenmaß + return np.asmatrix([np.sin(theta) * np.cos(phi), # x-Komponente + np.cos(theta) * np.cos(phi), # y-Komponente + np.sin(phi)]).T # z-Komponente + + dir = get_unit_vector(theta, phi) + # dir ist 3x1 + # [[0.75 ] + # [0.4330127] + # [0.5 ]] + +Jetzt verwenden wir unsere verallgemeinerte Steuervektorfunktion, um den Steuervektor zu berechnen: + +.. code-block:: python + + s = steering_vector(pos, dir) + + # Konventionellen Beamformer verwenden (Gewichte gleich dem Steuervektor), Strahlmuster plotten + w = s # 16x1 Gewichtsvektor + +An dieser Stelle ist es erwähnenswert, dass wir beim Übergang von 1D zu 2D die Dimensionen von nichts geändert haben – wir haben nur nicht-null x/y/z-Komponenten, die Steuervektorgleichung ist dieselbe und die Gewichte sind immer noch ein 1D-Array. Es mag verlockend sein, die Gewichte als 2D-Array zusammenzustellen, damit sie visuell der Array-Geometrie entsprechen, aber das ist nicht notwendig und am besten als 1D zu belassen. Für jedes Element gibt es ein entsprechendes Gewicht, und die Liste der Gewichte ist in derselben Reihenfolge wie die Liste der Elementpositionen. + +Das Strahlmuster dieser Gewichte zu visualisieren ist etwas komplizierter, da wir einen 3D-Plot oder eine 2D-Heatmap benötigen. Wir scannen :code:`theta` und :code:`phi`, um ein 2D-Array von Leistungspegeln zu erhalten, und plotten das dann mit :code:`imshow()`. Der folgende Code macht genau das, und das Ergebnis wird in der Abbildung unten gezeigt, zusammen mit einem Punkt an dem zuvor eingegebenen Winkel: + +.. code-block:: python + + resolution = 100 # Anzahl der Punkte in jeder Richtung + theta_scan = np.linspace(-np.pi/2, np.pi/2, resolution) # Azimutwinkel + phi_scan = np.linspace(-np.pi/4, np.pi/4, resolution) # Elevationswinkel + results = np.zeros((resolution, resolution)) # 2D-Array zum Speichern der Ergebnisse + for i, theta_i in enumerate(theta_scan): + for j, phi_i in enumerate(phi_scan): + a = steering_vector(pos, get_unit_vector(theta_i, phi_i)) # Arrayfaktor + results[i, j] = np.abs(w.conj().T @ a)[0,0] # Leistung im Signal, linear sieht besser aus + plt.imshow(results.T, extent=(theta_scan[0]*180/np.pi, theta_scan[-1]*180/np.pi, phi_scan[0]*180/np.pi, phi_scan[-1]*180/np.pi), origin='lower', aspect='auto', cmap='viridis') + plt.colorbar(label='Leistung [linear]') + plt.scatter(theta*180/np.pi, phi*180/np.pi, color='red', s=50) # Punkt an richtigem Theta/Phi hinzufügen + plt.xlabel('Azimutwinkel [Grad]') + plt.ylabel('Elevationswinkel [Grad]') + plt.show() + +.. image:: ../_images/2d_beamforming_2dplot.svg + :align: center + :target: ../_images/2d_beamforming_2dplot.svg + :alt: 3D-Plot des Strahlmusters + +Simulieren wir jetzt einige echte Abtastwerte; wir fügen zwei Ton-Jammer hinzu, die aus verschiedenen Richtungen ankommen: + +.. code-block:: python + + N = 10000 # Anzahl der zu simulierenden Abtastwerte + + jammer1_theta = np.deg2rad(-30) + jammer1_phi = np.deg2rad(10) + jammer1_dir = get_unit_vector(jammer1_theta, jammer1_phi) + jammer1_s = steering_vector(pos, jammer1_dir) # Nr x 1 + jammer1_tone = np.exp(2j*np.pi*0.1*np.arange(N)).reshape(1,-1) # als Zeilenvektor + + jammer2_theta = np.deg2rad(10) + jammer2_phi = np.deg2rad(50) + jammer2_dir = get_unit_vector(jammer2_theta, jammer2_phi) + jammer2_s = steering_vector(pos, jammer2_dir) + jammer2_tone = np.exp(2j*np.pi*0.2*np.arange(N)).reshape(1,-1) # als Zeilenvektor + + noise = np.random.normal(0, 1, (Nr, N)) + 1j * np.random.normal(0, 1, (Nr, N)) # komplexes Gaußsches Rauschen + r = jammer1_s @ jammer1_tone + jammer2_s @ jammer2_tone + noise # erzeugt 16 x 10000 Matrix + +Spaßeshalber berechnen wir die MVDR-Beamformer-Gewichte in Richtung des zuvor verwendeten Theta und Phi (ein Einheitsvektor in diese Richtung ist noch als :code:`dir` gespeichert): + +.. code-block:: python + + s = steering_vector(pos, dir) # 16 x 1 + R = np.cov(r) # Kovarianzmatrix, 16 x 16 + Rinv = np.linalg.pinv(R) + w = (Rinv @ s)/(s.conj().T @ Rinv @ s) # MVDR/Capon-Gleichung + +Anstatt das Strahlmuster im wenig übersichtlichen 3D-Plot anzusehen, verwenden wir eine alternative Methode, um zu prüfen, ob diese Gewichte sinnvoll sind: Wir bewerten die Antwort der Gewichte in verschiedene Richtungen und berechnen die Leistung in dB. Beginnen wir mit der Richtung, in die wir zeigen: + +.. code-block:: python + + # Leistung in der Richtung, in die wir zeigen (theta=60, phi=30, noch als dir gespeichert): + a = steering_vector(pos, dir) # Arrayfaktor + resp = w.conj().T @ a # Skalar + print("Leistung in Zeige-Richtung:", 10*np.log10(np.abs(resp)[0,0]), 'dB') + +Dies gibt 0 dB aus, was wir erwarten, da MVDRs Ziel ist, in der gewünschten Richtung Einheitsleistung zu erzielen. Jetzt prüfen wir die Leistung in Richtung der zwei Jammer sowie in einer zufälligen Richtung und einer Richtung, die einen Grad von unserer gewünschten Richtung abweicht (derselbe Code, nur :code:`dir` aktualisieren). Die Ergebnisse sind in der folgenden Tabelle dargestellt: + +.. list-table:: + :widths: 70 30 + :header-rows: 1 + + * - Zeige-Richtung + - Gewinn + * - :code:`dir` (für MVDR-Gewichtsberechnung verwendete Richtung) + - 0 dB + * - Jammer 1 + - -17,488 dB + * - Jammer 2 + - -18,551 dB + * - 1 Grad weg von :code:`dir` in :math:`\theta` und :math:`\phi` + - -0,00683 dB + * - Eine zufällige Richtung + - -10,591 dB + +Deine Ergebnisse können aufgrund des zufälligen Rauschens variieren. Die wichtigste Erkenntnis ist: die Jammer befinden sich in einem Null und haben sehr niedrige Leistung, die 1-Grad-Abweichung von :code:`dir` liegt leicht unter 0 dB, befindet sich aber noch in der Hauptkeule, und eine zufällige Richtung liegt unter 0 dB, aber über den Jammern. Beachte, dass du mit MVDR einen Gewinn von 0 dB für die Hauptkeule erhältst, aber mit dem konventionellen Beamformer würdest du :math:`10 \log_{10}(Nr)` erhalten, also etwa 12 dB für unser 16-Element-Array – das zeigt einen der Kompromisse von MVDR. + +Den Code für diesen Abschnitt findest du `hier `_. + +********************************************** +Signale von einem echten 2D-Array verarbeiten +********************************************** + +In diesem Abschnitt arbeiten wir mit echten Daten, die von einem 3×5-Array aufgezeichnet wurden, das mit einer `QUAD-MxFE `_-Plattform von Analog Devices erstellt wurde, die bis zu 16 Sende- und Empfangskanäle unterstützt (wir haben nur 15 verwendet und nur im Empfangsmodus). Es werden zwei Aufzeichnungen bereitgestellt: Die erste enthält einen Sender am Boresight des Arrays, den wir zur Kalibrierung verwenden. Die zweite Aufzeichnung enthält zwei Sender in verschiedenen Richtungen, die wir für Beamforming- und DOA-Tests verwenden. + +- `IQ-Aufzeichnung von nur C `_ (zur Kalibrierung verwendet, da C am Boresight ist) +- `IQ-Aufzeichnung von B und D `_ (für Beamforming/DOA-Tests verwendet) + +Das QUAD-MxFE wurde auf 2,8 GHz abgestimmt und alle Sender verwendeten einen einfachen Ton innerhalb der Beobachtungsbandbreite. Interessant an dieser DSP-Verarbeitung ist, dass die Abtastrate tatsächlich keine Rolle spielt; keine der verwendeten Array-Verarbeitungstechniken hängt von der Abtastrate ab, sie setzen nur voraus, dass das Signal irgendwo im Basisbandsignal liegt. Die DSP hängt von der Mittenfrequenz ab, weil die Phasenverschiebung zwischen Elementen von der Frequenz und dem Ankunftswinkel abhängt. Das ist das Gegenteil der meisten anderen Signalverarbeitung, wo die Abtastrate wichtig ist, aber die Mittenfrequenz nicht. + +Wir können diese Aufzeichnungen mit folgendem Code in Python laden: + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + + r = np.load("DandB_capture1.npy")[0:15] # 16. Element nicht angeschlossen, aber trotzdem aufgezeichnet + r_cal = np.load("C_only_capture1.npy")[0:15] # nur das Kalibrierungssignal (am Boresight) + +Der Abstand zwischen Antennen betrug 0,051 Meter. Wir können die Elementpositionen als Liste von x,y,z-Koordinaten in Metern darstellen. Wir platzieren das Array in der X-Z-Ebene, da das Array vertikal montiert war (mit Boresight horizontal zeigend). + +.. code-block:: python + + fc = 2.8e9 # Mittenfrequenz in Hz + d = 0.051 # Abstand zwischen Antennen in Metern + wavelength = 3e8 / fc + Nr = 15 + rows = 3 + cols = 5 + + # Elementpositionen als Liste von x,y,z-Koordinaten in Metern + pos = np.zeros((Nr, 3)) + for i in range(Nr): + pos[i,0] = d * (i % cols) # x-Position + pos[i,1] = 0 # y-Position + pos[i,2] = d * (i // cols) # z-Position + + # Elementpositionen plotten und beschriften + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + ax.scatter(pos[:,0], pos[:,1], pos[:,2], 'o') + # Indizes beschriften + for i in range(Nr): + ax.text(pos[i,0], pos[i,1], pos[i,2], str(i), fontsize=10) + plt.xlabel("X-Position [m]") + plt.ylabel("Y-Position [m]") + ax.set_zlabel("Z-Position [m]") + plt.grid() + plt.show() + +Der Plot beschriftet jedes Element mit seinem Index, der der Reihenfolge der Elemente in den aufgezeichneten :code:`r`- und :code:`r_cal`-IQ-Abtastwerten entspricht. + +.. image:: ../_images/2d_array_element_positions.svg + :align: center + :target: ../_images/2d_array_element_positions.svg + :alt: 2D-Array-Elementpositionen + +Die Kalibrierung wird nur mit den :code:`r_cal`-Abtastwerten durchgeführt, die nur mit dem Sender am Boresight aufgezeichnet wurden. Das Ziel ist, die Phasen- und Amplitudenoffsets für jedes Element zu finden. Bei perfekter Kalibrierung und unter der Annahme, dass der Sender genau am Boresight war, sollten alle einzelnen Empfangselemente dasselbe Signal empfangen – alle in Phase miteinander und mit der gleichen Amplitude. Aufgrund von Unvollkommenheiten im Array/Kabeln/Antennen hat jedes Element jedoch einen anderen Phasen- und Amplitudenoffset. Der Kalibrierungsprozess besteht darin, diese Offsets zu finden, die wir später auf die :code:`r`-Abtastwerte anwenden, bevor wir versuchen, Array-Verarbeitung darauf durchzuführen. + +Es gibt viele Möglichkeiten, eine Kalibrierung durchzuführen, aber wir verwenden eine Methode, die die Eigenwertzersetzung der Kovarianzmatrix beinhaltet. Der Eigenvektor entsprechend dem größten Eigenwert ist derjenige, der hoffentlich das empfangene Signal repräsentiert, und wir verwenden ihn, um die Phasenoffsets für jedes Element zu finden, indem wir einfach die Phase jedes Elements des Eigenvektors nehmen und auf das erste Element normieren, das wir als Referenzelement behandeln. Die Amplitudenkalibrierung verwendet nicht den Eigenvektor, sondern die mittlere Amplitude des empfangenen Signals für jedes Element. + +.. code-block:: python + + # Kovarianzmatrix berechnen, Nr x Nr + R_cal = r_cal @ r_cal.conj().T + + # Eigenwertzersetzung, v[:,i] ist der Eigenvektor entsprechend dem Eigenwert w[i] + w, v = np.linalg.eig(R_cal) + + # Eigenwerte plotten, um sicherzustellen, dass es nur einen großen gibt + w_dB = 10*np.log10(np.abs(w)) + w_dB -= np.max(w_dB) # normalisieren + fig, (ax1) = plt.subplots(1, 1, figsize=(7, 3)) + ax1.plot(w_dB, '.-') + ax1.set_xlabel('Index') + ax1.set_ylabel('Eigenwert [dB]') + plt.show() + + # Maximalen Eigenvektor zur Kalibrierung verwenden + v_max = v[:, np.argmax(np.abs(w))] + mags = np.mean(np.abs(r_cal), axis=1) + mags = mags[0] / mags # auf erstes Element normieren + phases = np.angle(v_max) + phases = phases[0] - phases # auf erstes Element normieren + cal_table = mags * np.exp(1j * phases) + print("cal_table", cal_table) + +Unten ist der Plot der Eigenwertverteilung. Wir möchten sicherstellen, dass es nur einen großen Wert gibt und die anderen klein sind, was ein empfangenes Signal repräsentiert. Störer oder Mehrwege können den Kalibrierungsprozess beeinträchtigen. + +.. image:: ../_images/2d_array_eigenvalues.svg + :align: center + :target: ../_images/2d_array_eigenvalues.svg + :alt: 2D-Array-Eigenwertverteilung + +Die Kalibrierungstabelle ist eine Liste komplexer Zahlen, eine für jedes Element, die die Phasen- und Amplitudenoffsets darstellen. Das erste Element ist das Referenzelement und ist immer 1,0 + 0j. Die übrigen Elemente sind die Offsets für jedes Element in derselben Reihenfolge wie :code:`pos`. + +.. code-block:: python + + [1. +0.j 0.99526771+0.76149029j -0.91754588-0.66825262j + -0.96840297+0.37251012j 0.87866849+0.40446665j 0.56040169+1.50499875j + -0.80109196-1.29299264j -1.28464742-0.31133052j 1.26622038+0.46047599j + 2.01855809+9.77121302j -0.29249322-1.09413205j -1.0372309 -0.17983522j + -0.70614339+0.78682873j -0.75612972+5.67234809j 1.00032754-0.60824109j] + + +Wir können diese Offsets auf alle von dem Array aufgezeichneten Abtastwerte anwenden, indem wir einfach jedes Element der Abtastwerte mit dem entsprechenden Element der Kalibrierungstabelle multiplizieren: + +.. code-block:: python + + # Kalibrierungsoffsets auf r anwenden + for i in range(Nr): + r[i, :] *= cal_table[i] + +Als Randnotiz: Deshalb haben wir die Offsets mit :code:`mags[0] / mags` und :code:`phases[0] - phases` berechnet. Hätten wir die Reihenfolge umgekehrt, müssten wir eine Division zum Anwenden der Offsets durchführen, aber wir bevorzugen die Multiplikation. + +Als nächstes führen wir die DOA-Schätzung mit dem MUSIC-Algorithmus durch. Wir verwenden die zuvor definierten Funktionen :code:`steering_vector()` und :code:`get_unit_vector()`, um den Steuervektor für jedes Element des Arrays zu berechnen, und dann den MUSIC-Algorithmus, um die DOA der zwei Sender in den :code:`r`-Abtastwerten zu schätzen. Der MUSIC-Algorithmus wurde im vorherigen Kapitel besprochen. + +.. code-block:: python + + # DOA mit MUSIC + resolution = 400 # Anzahl der Punkte in jeder Richtung + theta_scan = np.linspace(-np.pi/2, np.pi/2, resolution) # Azimutwinkel + phi_scan = np.linspace(-np.pi/4, np.pi/4, resolution) # Elevationswinkel + results = np.zeros((resolution, resolution)) # 2D-Array für Ergebnisse + R = np.cov(r) # Kovarianzmatrix, 15 x 15 + Rinv = np.linalg.pinv(R) + expected_num_signals = 4 + w, v = np.linalg.eig(R) # Eigenwertzersetzung + eig_val_order = np.argsort(np.abs(w)) + v = v[:, eig_val_order] # Eigenvektoren sortieren + V = np.zeros((Nr, Nr - expected_num_signals), dtype=np.complex64) # Rauschunterraum + for i in range(Nr - expected_num_signals): + V[:, i] = v[:, i] + for i, theta_i in enumerate(theta_scan): + for j, phi_i in enumerate(phi_scan): + dir_i = get_unit_vector(-1*theta_i, phi_i) # TODO: -1* war nötig, um der Realität zu entsprechen + s = steering_vector(pos, dir_i) # 15 x 1 + music_metric = 1 / (s.conj().T @ V @ V.conj().T @ s) + music_metric = np.abs(music_metric).squeeze() + music_metric = np.clip(music_metric, 0, 2) # Nützlich für ABCD + results[i, j] = music_metric + +Unsere Ergebnisse sind 2D, da das Array 2D ist. Wir können entweder einen 3D-Plot oder eine 2D-Heatmap verwenden. Zuerst ein 3D-Plot mit Elevation auf einer Achse und Azimut auf der anderen: + +.. code-block:: python + + # 3D-Az-El-DOA-Ergebnisse + results = 10*np.log10(results) # in dB umrechnen + results[results < -20] = -20 # z-Achse auf bestimmten dB-Pegel beschneiden + fig, ax = plt.subplots(subplot_kw={"projection": "3d", "computed_zorder": False}) + surf = ax.plot_surface(np.rad2deg(theta_scan[:,None]), + np.rad2deg(phi_scan[None,:]), + results, + cmap='viridis') + ax.set_xlabel('Azimut (theta)') + ax.set_ylabel('Elevation (phi)') + ax.set_zlabel('Leistung [dB]') + fig.savefig('../_images/2d_array_3d_doa_plot.svg', bbox_inches='tight') + plt.show() + +.. image:: ../_images/2d_array_3d_doa_plot.png + :align: center + :scale: 30% + :target: ../_images/2d_array_3d_doa_plot.png + :alt: 3D-DOA-Plot + +Je nach Situation kann es schwierig sein, Zahlen aus einem 3D-Plot abzulesen, daher können wir auch eine 2D-Heatmap mit :code:`imshow()` erstellen: + +.. code-block:: python + + # 2D-Az-El-Heatmap + extent=(np.min(theta_scan)*180/np.pi, + np.max(theta_scan)*180/np.pi, + np.min(phi_scan)*180/np.pi, + np.max(phi_scan)*180/np.pi) + plt.imshow(results.T, extent=extent, origin='lower', aspect='auto', cmap='viridis') + plt.colorbar(label='Leistung [linear]') + plt.xlabel('Theta (Azimut, Grad)') + plt.ylabel('Phi (Elevation, Grad)') + plt.savefig('../_images/2d_array_2d_doa_plot.svg', bbox_inches='tight') + plt.show() + +.. image:: ../_images/2d_array_2d_doa_plot.svg + :align: center + :target: ../_images/2d_array_2d_doa_plot.svg + :alt: 2D-DOA-Plot + +Anhand dieses 2D-Plots können wir den geschätzten Azimut und die Elevation der beiden Sender leicht ablesen (und sehen, dass es nur zwei waren). Basierend auf dem Testaufbau, der zur Erstellung dieser Aufzeichnung verwendet wurde, stimmen diese Ergebnisse mit der Realität überein. Der genaue Azimut und die Elevation der Sender wurden nie tatsächlich gemessen, da dafür spezialisierte Ausrüstung erforderlich wäre. + +Als Übung versuche den konventionellen Beamformer sowie MVDR zu verwenden und vergleiche die Ergebnisse mit MUSIC. + +Den vollständigen Code für diesen Abschnitt findest du `hier `_. + +*********************** +Interaktives Entwurfswerkzeug +*********************** + +Das folgende interaktive Werkzeug wurde von `Jason Durbin `_ erstellt, einem freiberuflichen Phased-Array-Ingenieur, der freundlicherweise seine Einbettung in PySDR erlaubt hat. Besuche gerne das `vollständige Projekt `_ oder sein `Beratungsunternehmen `_. Dieses Werkzeug ermöglicht das Ändern der Geometrie eines Phased-Arrays, des Elementabstands, der Steuerposition, das Hinzufügen von Nebenkeulen-Tapering und weitere Funktionen. + +Einige Details zu diesem Werkzeug: Antennenelemente werden als isotrop angenommen. Die Richtwirkungsberechnung nimmt jedoch Halbhemisphären-Strahlung an (z.B. keine Rückzipfel). Daher wird die berechnete Richtwirkung 3 dBi höher sein als bei reiner isotroper Verwendung. Das Gitter kann durch Erhöhung von Theta/Phi, U/V oder Azimut/Elevation-Punkten feiner gemacht werden. Klicken (oder langes Drücken) auf Elemente in den Phasen-/Dämpfungsplots ermöglicht das manuelle Einstellen von Phase/Dämpfung. Außerdem ermöglicht das Dämpfungs-Popup das Deaktivieren von Elementen. + +.. raw:: html + + + +
+
+
+

Geometry

+
+
+

Steering

+ +
+ + +
+
+ + +
+
+
+

Taper(s)

+
+ + +
+
+
+
+
+

Quantization

+
+ + +
+
+ + +
+
+ + +
+
+ 0 bits would be no quantization. +
+
+
+
+
+ +
Loading...
+
+
+
+
+

Element
Phase

 
+
+ +
+ +
+
+

Element Attenuation

 
+
+ +
+ +
+
+

2-D Radiation Pattern

 
+
+ +
+ +
+
+
+
+

1-D Pattern Cuts

+
+ +
+ +
+
+
+
+

Taper

+
+ +
+ +
+
diff --git a/content-de/about_author.rst b/content-de/about_author.rst new file mode 100644 index 00000000..2466c7bc --- /dev/null +++ b/content-de/about_author.rst @@ -0,0 +1,20 @@ +.. _author-chapter: + +################## +Über den Autor +################## + +Dr. Marc Lichtman ist ein Forscher im Bereich drahtloser Kommunikation, der sich auf SDR, maschinelles Lernen, LTE/5G-NR und Spektrumserkennung spezialisiert hat. Er ist außerordentlicher Professor (adj. Prof.) an der University of Maryland, wo er einen Kurs entwickelt und unterrichtet hat, der als Grundlage für dieses Lehrbuch diente. Sein Kurs war ein Wahlfach im letzten Studienjahr, das sich an Informatikstudenten richtet, die sich für SDR/DSP interessieren. Dieser Kurs hat ihm geholfen, besser zu verstehen, wie man sehr schweres Material für Studenten zugänglich und ansprechend gestaltet, die zwar gute Programmierer waren, aber wenig bis keine Kenntnisse in drahtloser Kommunikation und Signalverarbeitung hatten. Es war nicht ungewöhnlich, die Stunde mit einem Mini-Hackathon zu beginnen, bei dem die Studenten versteckte Signale (die von Marc übertragen wurden) mithilfe des kürzlich Gelernten finden oder dekodieren mussten. + +Marc ist außerdem einer der Leiter des `GNU Radio-Projekts `_, eines Open-Source-SDR-Frameworks, das in der Wissenschaft und in der verteidigungsbezogenen Forschung weit verbreitet ist. GNU Radio kann zur Implementierung fortgeschrittener DSP-Anwendungen verwendet werden, und eine GNU Radio-App oder ein einzelner Block lässt sich sehr einfach mit anderen teilen. + +Marc lebt derzeit in der Gegend von Washington D.C. mit seiner Frau und ihren vielen Katzen und Hunden. Zu seinen Hobbys gehören Holzbearbeitung, Metallbearbeitung, Laserschneiden, Klarinette/Saxofon, Segeln, Gartenarbeit und Flipper. + +E-Mail: marc@pysdr.org + +Fakultätsseite der University of Maryland: `cs.umd.edu/people/sdr `_ + +.. image:: ../_images_de/silly_marc.jpg + :scale: 100 % + :align: center + :alt: Foto von Marc Lichtman, dem Autor von PySDR, mit seiner Katze in einer lustigen Pose diff --git a/content-de/bladerf.rst b/content-de/bladerf.rst new file mode 100644 index 00000000..ca00564b --- /dev/null +++ b/content-de/bladerf.rst @@ -0,0 +1,410 @@ +.. _bladerf-chapter: + +################## +BladeRF in Python +################## + +Das bladeRF 2.0 (auch bekannt als bladeRF 2.0 micro) des Unternehmens `Nuand `_ ist ein USB-3.0-basiertes SDR mit zwei Empfangskanälen, zwei Sendekanälen, einem abstimmbaren Bereich von 47 MHz bis 6 GHz und der Möglichkeit, mit bis zu 61 MHz oder sogar 122 MHz abzutasten, wenn man es entsprechend modifiziert. Es verwendet denselben AD9361 HF-integrierten Schaltkreis (RFIC) wie das USRP B210 und das PlutoSDR, sodass die HF-Leistung vergleichbar ist. Das bladeRF 2.0 wurde 2021 veröffentlicht, hat einen kompakten Formfaktor von 2,5" x 4,5" und ist in zwei verschiedenen FPGA-Größen erhältlich (xA4 und xA9). Obwohl sich dieses Kapitel auf das bladeRF 2.0 konzentriert, gilt ein Großteil des Codes auch für das ursprüngliche bladeRF, das `2013 erschien `_. + +.. image:: ../_images/bladeRF_micro.png + :scale: 35 % + :align: center + :alt: bladeRF 2.0 Produktfoto + +******************************** +bladeRF-Architektur +******************************** + +Das bladeRF 2.0 basiert auf dem AD9361 RFIC, kombiniert mit einem Cyclone-V-FPGA (entweder dem 49 kLE :code:`5CEA4` oder dem 301 kLE :code:`5CEA9`), sowie einem Cypress FX3 USB-3.0-Controller mit einem 200 MHz ARM9-Kern, der mit einer angepassten Firmware bespielt ist. Das Blockdiagramm des bladeRF 2.0 ist unten dargestellt: + +.. image:: ../_images/bladeRF-2.0-micro-Block-Diagram-4.png + :scale: 80 % + :align: center + :alt: bladeRF 2.0 Blockdiagramm + +Das FPGA steuert den RFIC, führt digitale Filterung durch und verpackt Pakete für die Übertragung über USB (unter anderem). Der `Quellcode `_ für das FPGA-Image ist in VHDL geschrieben und erfordert die kostenlose Quartus Prime Lite Design-Software, um benutzerdefinierte Images zu kompilieren. Vorkompilierte Images sind `hier `_ verfügbar. + +Der `Quellcode `_ für die Cypress FX3 Firmware ist Open-Source und enthält Code zum: + +1. Laden des FPGA-Images +2. Übertragen von IQ-Samples zwischen dem FPGA und dem Host über USB 3.0 +3. Steuern der GPIO des FPGAs über UART + +Aus der Perspektive des Signalflusses gibt es zwei Empfangskanäle und zwei Sendekanäle, wobei jeder Kanal je nach verwendetem Frequenzband einen Nieder- und Hochfrequenzeingang/-ausgang zum RFIC hat. Aus diesem Grund wird zwischen dem RFIC und den SMA-Anschlüssen ein elektronischer HF-Schalter mit einem Pol und zwei Ausgängen (SPDT) benötigt. Das Bias-T ist eine integrierte Schaltung auf der Platine, die etwa 4,5 V Gleichstrom am SMA-Anschluss bereitstellt und dazu dient, einen externen Verstärker oder andere HF-Komponenten bequem zu versorgen. Dieser zusätzliche Gleichstromoffset befindet sich auf der HF-Seite des SDR und stört daher den grundlegenden Sende-/Empfangsbetrieb nicht. + +JTAG ist eine Art Debug-Schnittstelle, die das Testen und Verifizieren von Designs während des Entwicklungsprozesses ermöglicht. + +Am Ende dieses Kapitels besprechen wir den VCTCXO-Oszillator, den PLL und den Erweiterungsport. + +******************************** +Software- und Hardware-Einrichtung +******************************** + +Ubuntu (oder Ubuntu innerhalb von WSL) +####################################### + +Unter Ubuntu und anderen Debian-basierten Systemen kannst du die bladeRF-Software mit folgenden Befehlen installieren: + +.. code-block:: bash + + sudo apt update + sudo apt install cmake python3-pip libusb-1.0-0 + cd ~ + git clone --depth 1 https://github.com/Nuand/bladeRF.git + cd bladeRF/host + mkdir build && cd build + cmake .. + make -j8 + sudo make install + sudo ldconfig + cd ../libraries/libbladeRF_bindings/python + sudo python3 setup.py install + +Damit werden die libbladerf-Bibliothek, Python-Bindungen, bladeRF-Kommandozeilenwerkzeuge, der Firmware-Downloader und der FPGA-Bitstream-Downloader installiert. Um zu prüfen, welche Version der Bibliothek du installiert hast, verwende :code:`bladerf-tool version` (dieser Leitfaden wurde mit libbladeRF Version v2.5.0 geschrieben). + +Wenn du Ubuntu über WSL verwendest, musst du auf der Windows-Seite das bladeRF-USB-Gerät an WSL weiterleiten. Installiere dazu zunächst das neueste `usbipd-Dienstprogramm als MSI `_ (dieser Leitfaden setzt usbipd-win 4.0.0 oder höher voraus), öffne dann PowerShell im Administratormodus und führe folgendes aus: + +.. code-block:: bash + + usbipd list + # (finde die BUSID mit der Bezeichnung bladeRF 2.0 und setze sie im folgenden Befehl ein) + usbipd bind --busid 1-23 + usbipd attach --wsl --busid 1-23 + +Auf der WSL-Seite solltest du :code:`lsusb` ausführen und einen neuen Eintrag namens :code:`Nuand LLC bladeRF 2.0 micro` sehen können. Beachte, dass du das Flag :code:`--auto-attach` zum Befehl :code:`usbipd attach` hinzufügen kannst, wenn es sich automatisch neu verbinden soll. + +(Möglicherweise nicht erforderlich) Sowohl für natives Linux als auch für WSL müssen wir die udev-Regeln installieren, damit wir keine Berechtigungsfehler erhalten: + +.. code-block:: + + sudo nano /etc/udev/rules.d/88-nuand.rules + +und folgende Zeile einfügen: + +.. code-block:: + + ATTRS{idVendor}=="2cf0", ATTRS{idProduct}=="5250", MODE="0666" + +Zum Speichern und Beenden von nano: Strg+O, dann Enter, dann Strg+X. Um udev neu zu laden, führe aus: + +.. code-block:: bash + + sudo udevadm control --reload-rules && sudo udevadm trigger + +Wenn du WSL verwendest und die Meldung :code:`Failed to send reload request: No such file or directory` erscheint, bedeutet das, dass der udev-Dienst nicht läuft. Du musst dann :code:`sudo nano /etc/wsl.conf` öffnen und folgende Zeilen hinzufügen: + +.. code-block:: bash + + [boot] + command="service udev start" + +Starte dann WSL neu mit folgendem Befehl in PowerShell als Administrator: :code:`wsl.exe --shutdown`. + +Trenne und verbinde dein bladeRF erneut (WSL-Nutzer müssen es erneut anhängen) und teste die Berechtigungen mit: + +.. code-block:: bash + + bladerf-tool probe + bladerf-tool info + +Es hat funktioniert, wenn du dein bladeRF 2.0 aufgelistet siehst und **nicht** die Meldung :code:`Found a bladeRF via VID/PID, but could not open it due to insufficient permissions` erscheint. Wenn es geklappt hat, notiere die angezeigte FPGA-Version und Firmware-Version. + +(Optional) Installiere die neueste Firmware und FPGA-Images (zum Zeitpunkt der Erstellung dieses Leitfadens v2.4.0 bzw. v0.15.0) mit: + +.. code-block:: bash + + cd ~/Downloads + wget https://www.nuand.com/fx3/bladeRF_fw_latest.img + bladerf-tool flash_fw bladeRF_fw_latest.img + + # für xA4 verwende: + wget https://www.nuand.com/fpga/hostedxA4-latest.rbf + bladerf-tool flash_fpga hostedxA4-latest.rbf + + # für xA9 verwende: + wget https://www.nuand.com/fpga/hostedxA9-latest.rbf + bladerf-tool flash_fpga hostedxA9-latest.rbf + +Trenne und verbinde dein bladeRF erneut, um es neu zu starten. + +Nun testen wir die Funktionalität, indem wir 1 Million Samples im FM-Radioband bei 10 MHz Abtastrate in die Datei /tmp/samples.sc16 aufnehmen: + +.. code-block:: bash + + bladerf-tool rx --num-samples 1000000 /tmp/samples.sc16 100e6 10e6 + +Ein paar :code:`Hit stall for buffer`-Meldungen sind zu erwarten, aber es hat funktioniert, wenn du eine 4 MB große Datei /tmp/samples.sc16 siehst. + +Abschließend testen wir die Python-API mit: + +.. code-block:: bash + + python3 + import bladerf + bladerf.BladeRF() + exit() + +Es hat funktioniert, wenn du etwas wie :code:`)>` und keine Warnungen oder Fehler siehst. + +Windows und macOS +################## + +Für Windows-Nutzer (die WSL nicht bevorzugen) siehe https://github.com/Nuand/bladeRF/wiki/Getting-Started%3A-Windows, und für macOS-Nutzer siehe https://github.com/Nuand/bladeRF/wiki/Getting-started:-Mac-OSX. + +******************************** +bladeRF Python API Grundlagen +******************************** + +Zunächst fragen wir das bladeRF nach einigen nützlichen Informationen ab, mit folgendem Skript. **Benenne dein Skript nicht bladerf.py**, da es sonst mit dem bladeRF Python-Modul selbst in Konflikt gerät! + +.. code-block:: python + + from bladerf import _bladerf + import numpy as np + import matplotlib.pyplot as plt + + sdr = _bladerf.BladeRF() + + print("Device info:", _bladerf.get_device_list()[0]) + print("libbladeRF version:", _bladerf.version()) # v2.5.0 + print("Firmware version:", sdr.get_fw_version()) # v2.4.0 + print("FPGA version:", sdr.get_fpga_version()) # v0.15.0 + + rx_ch = sdr.Channel(_bladerf.CHANNEL_RX(0)) # 0 oder 1 übergeben + print("sample_rate_range:", rx_ch.sample_rate_range) + print("bandwidth_range:", rx_ch.bandwidth_range) + print("frequency_range:", rx_ch.frequency_range) + print("gain_modes:", rx_ch.gain_modes) + print("manual gain range:", sdr.get_gain_range(_bladerf.CHANNEL_RX(0))) # Kanal 0 oder 1 + +Für das bladeRF 2.0 xA9 sollte die Ausgabe in etwa so aussehen: + +.. code-block:: python + + Device info: Device Information + backend libusb + serial f80a27b1010448dfb7a003ef7fa98a59 + usb_bus 2 + usb_addr 5 + instance 0 + libbladeRF version: v2.5.0 ("2.5.0-git-624994d") + Firmware version: v2.4.0 ("2.4.0-git-a3d5c55f") + FPGA version: v0.15.0 ("0.15.0") + sample_rate_range: Range + min 520834 + max 61440000 + step 2 + scale 1.0 + + bandwidth_range: Range + min 200000 + max 56000000 + step 1 + scale 1.0 + + frequency_range: Range + min 70000000 + max 6000000000 + step 2 + scale 1.0 + + gain_modes: [, , , , ] + + manual gain range: Range + min -15 + max 60 + step 1 + scale 1.0 + +Der Bandwidth-Parameter legt den Filter fest, den das SDR beim Empfang verwendet. Daher setzen wir ihn typischerweise gleich oder leicht unterhalb von sample_rate/2. Die Gain-Modi sind wichtig zu verstehen: Das SDR verwendet entweder einen manuellen Verstärkungsmodus, bei dem du die Verstärkung in dB vorgibst, oder eine automatische Verstärkungsregelung (AGC), die drei verschiedene Einstellungen hat (schnell, langsam, hybrid). Für Anwendungen wie die Spektrumüberwachung wird ein manueller Gain empfohlen, damit du erkennen kannst, wann Signale auftauchen und verschwinden. Für Anwendungen, bei denen du ein bestimmtes Signal empfangen möchtest, das du erwartest, ist AGC nützlicher, da es den Gain automatisch anpasst, damit das Signal den Analog-Digital-Wandler (ADC) optimal ausnutzt. + +Um die wichtigsten Parameter des SDR einzustellen, können wir folgenden Code hinzufügen: + +.. code-block:: python + + sample_rate = 10e6 + center_freq = 100e6 + gain = 50 # -15 bis 60 dB + num_samples = int(1e6) + + rx_ch.frequency = center_freq + rx_ch.sample_rate = sample_rate + rx_ch.bandwidth = sample_rate/2 + rx_ch.gain_mode = _bladerf.GainMode.Manual + rx_ch.gain = gain + +******************************** +Samples empfangen in Python +******************************** + +Als nächstes bauen wir auf dem vorherigen Codeblock auf und empfangen 1 Million Samples im FM-Radioband bei 10 MHz Abtastrate – genauso wie zuvor. Jede Antenne am RX1-Port sollte FM empfangen können, da die Signale sehr stark sind. Der folgende Code zeigt, wie die synchrone Stream-API des bladeRF funktioniert: Sie muss konfiguriert und ein Empfangspuffer muss erstellt werden, bevor der Empfang beginnt. Die :code:`while True:`-Schleife empfängt so lange Samples, bis die angeforderte Anzahl erreicht ist. Die empfangenen Samples werden in einem separaten NumPy-Array gespeichert, damit wir sie nach der Schleife verarbeiten können. + +.. code-block:: python + + # Synchronen Stream konfigurieren + sdr.sync_config(layout = _bladerf.ChannelLayout.RX_X1, # oder RX_X2 + fmt = _bladerf.Format.SC16_Q11, # int16s + num_buffers = 16, + buffer_size = 8192, + num_transfers = 8, + stream_timeout = 3500) + + # Empfangspuffer erstellen + bytes_per_sample = 4 # nicht ändern, es werden immer int16s verwendet + buf = bytearray(1024 * bytes_per_sample) + + # Modul aktivieren + print("Starte Empfang") + rx_ch.enable = True + + # Empfangsschleife + x = np.zeros(num_samples, dtype=np.complex64) # Speicher für IQ-Samples + num_samples_read = 0 + while True: + if num_samples > 0 and num_samples_read == num_samples: + break + elif num_samples > 0: + num = min(len(buf) // bytes_per_sample, num_samples - num_samples_read) + else: + num = len(buf) // bytes_per_sample + sdr.sync_rx(buf, num) # In Puffer einlesen + samples = np.frombuffer(buf, dtype=np.int16) + samples = samples[0::2] + 1j * samples[1::2] # In komplexen Typ umwandeln + samples /= 2048.0 # Auf -1 bis 1 skalieren (12-Bit-ADC) + x[num_samples_read:num_samples_read+num] = samples[0:num] # Puffer im Samples-Array speichern + num_samples_read += num + + print("Stoppe") + rx_ch.enable = False + print(x[0:10]) # erste 10 IQ-Samples ansehen + print(np.max(x)) # wenn dieser Wert nahe 1 ist, überlädst du den ADC und solltest den Gain reduzieren + +Ein paar :code:`Hit stall for buffer`-Meldungen am Ende sind zu erwarten. Die letzte ausgegebene Zahl zeigt den maximalen empfangenen Samplewert. Du solltest deinen Gain so einstellen, dass dieser Wert ungefähr zwischen 0,5 und 0,8 liegt. Wenn er 0,999 beträgt, ist dein Empfänger überlastet/gesättigt und das Signal wird verzerrt (es erscheint im Frequenzbereich verschmiert). + +Um das empfangene Signal zu visualisieren, zeigen wir die IQ-Samples als Spektrogramm an (siehe :ref:`spectrogram-section` für weitere Details zur Funktionsweise von Spektrogrammen). Füge folgendes am Ende des vorherigen Codeblocks hinzu: + +.. code-block:: python + + # Spektrogramm erstellen + fft_size = 2048 + num_rows = len(x) // fft_size # // ist eine ganzzahlige Division, die abrundet + spectrogram = np.zeros((num_rows, fft_size)) + for i in range(num_rows): + spectrogram[i,:] = 10*np.log10(np.abs(np.fft.fftshift(np.fft.fft(x[i*fft_size:(i+1)*fft_size])))**2) + extent = [(center_freq + sample_rate/-2)/1e6, (center_freq + sample_rate/2)/1e6, len(x)/sample_rate, 0] + plt.imshow(spectrogram, aspect='auto', extent=extent) + plt.xlabel("Frequenz [MHz]") + plt.ylabel("Zeit [s]") + plt.show() + +.. image:: ../_images/bladerf-waterfall.svg + :align: center + :target: ../_images/bladerf-waterfall.svg + :alt: bladeRF Spektrogramm-Beispiel + +Jede vertikale gewellte Linie ist ein FM-Radiosignal. Was das pulsierende Signal auf der rechten Seite verursacht, ist unklar – eine Reduzierung des Gains ließ es nicht verschwinden. + + +******************************** +Samples senden in Python +******************************** + +Das Senden von Samples mit dem bladeRF ist dem Empfangen sehr ähnlich. Der wesentliche Unterschied besteht darin, dass wir die zu sendenden Samples generieren und sie dann mit der Methode :code:`sync_tx` an das bladeRF schreiben müssen, die unseren gesamten Batch an Samples auf einmal verarbeiten kann (bis zu ca. 4 Milliarden Samples). Der folgende Code zeigt, wie man einen einfachen Ton sendet und ihn 30 Mal wiederholt. Der Ton wird mit NumPy generiert und dann auf den Bereich -2048 bis 2048 skaliert, um in den 12-Bit-Digital-Analog-Wandler (DAC) zu passen. Anschließend wird der Ton in Bytes umgewandelt, die int16-Werte repräsentieren, und als Sendepuffer verwendet. Die synchrone Stream-API wird zum Senden der Samples verwendet, und die :code:`while True:`-Schleife sendet so lange, bis die gewünschte Anzahl an Wiederholungen erreicht ist. Wenn du stattdessen Samples aus einer Datei senden möchtest, verwende :code:`samples = np.fromfile('deinedatei.iq', dtype=np.int16)` (oder welchen Datentyp sie auch haben), um die Samples einzulesen, und konvertiere sie dann mit :code:`samples.tobytes()` in Bytes. Beachte dabei den Wertebereich des DAC von -2048 bis 2048. + +.. code-block:: python + + from bladerf import _bladerf + import numpy as np + + sdr = _bladerf.BladeRF() + tx_ch = sdr.Channel(_bladerf.CHANNEL_TX(0)) # 0 oder 1 übergeben + + sample_rate = 10e6 + center_freq = 100e6 + gain = 0 # -15 bis 60 dB. Beim Senden klein anfangen und langsam erhöhen; Antenne anschließen! + num_samples = int(1e6) + repeat = 30 # Anzahl der Wiederholungen des Signals + print('Sendedauer:', num_samples/sample_rate*repeat, 'Sekunden') + + # IQ-Samples zum Senden generieren (hier ein einfacher Ton) + t = np.arange(num_samples) / sample_rate + f_tone = 1e6 + samples = np.exp(1j * 2 * np.pi * f_tone * t) # liegt zwischen -1 und +1 + samples = samples.astype(np.complex64) + samples *= 2048.0 # Auf -2048 bis 2048 skalieren (12-Bit-DAC) + samples = samples.view(np.int16) + buf = samples.tobytes() # Samples in Bytes umwandeln und als Sendepuffer verwenden + + tx_ch.frequency = center_freq + tx_ch.sample_rate = sample_rate + tx_ch.bandwidth = sample_rate/2 + tx_ch.gain = gain + + # Synchronen Stream konfigurieren + sdr.sync_config(layout=_bladerf.ChannelLayout.TX_X1, # oder TX_X2 + fmt=_bladerf.Format.SC16_Q11, # int16s + num_buffers=16, + buffer_size=8192, + num_transfers=8, + stream_timeout=3500) + + print("Starte Senden!") + repeats_remaining = repeat - 1 + tx_ch.enable = True + while True: + sdr.sync_tx(buf, num_samples) # an bladeRF schreiben + print(repeats_remaining) + if repeats_remaining > 0: + repeats_remaining -= 1 + else: + break + + print("Stoppe Senden") + tx_ch.enable = False + +Ein paar :code:`Hit stall for buffer`-Meldungen am Ende sind zu erwarten. + +Um gleichzeitig zu senden und zu empfangen, müssen Threads verwendet werden. Am besten nutzt du dafür Nuands Beispiel `txrx.py `_, das genau das tut. + +*********************************** +Oszillatoren, PLLs und Kalibrierung +*********************************** + +Alle Direktkonversions-SDRs (einschließlich aller AD9361-basierten SDRs wie dem USRP B2X0, Analog Devices Pluto und bladeRF) sind auf einen einzelnen Oszillator angewiesen, der einen stabilen Takt für den HF-Transceiver bereitstellt. Jeder Versatz oder Jitter in der von diesem Oszillator erzeugten Frequenz überträgt sich als Frequenzversatz und Frequenzjitter auf das empfangene oder gesendete Signal. Dieser Oszillator befindet sich an Bord, kann aber optional durch ein separates Rechteck- oder Sinussignal „diszipliniert" werden, das über einen U.FL-Steckverbinder auf der Platine in das bladeRF eingespeist wird. + +An Bord des bladeRF befindet sich ein `Abracon VCTCXO `_ (spannungsgesteuerter, temperaturkompensierter Oszillator) mit einer Frequenz von 38,4 MHz. Der „temperaturkompensierte" Aspekt bedeutet, dass er so ausgelegt ist, dass er über einen weiten Temperaturbereich stabil bleibt. Der spannungsgesteuerte Aspekt bedeutet, dass ein Spannungspegel verwendet wird, um leichte Anpassungen an der Oszillatorfrequenz vorzunehmen. Beim bladeRF wird diese Spannung von einem separaten 10-Bit-Digital-Analog-Wandler (DAC) bereitgestellt, wie im Blockdiagramm unten in Grün dargestellt. Das bedeutet, dass wir über Software feine Anpassungen an der Frequenz des Oszillators vornehmen können – so wird das VCTCXO des bladeRF kalibriert (auch als „Trimmen" bezeichnet). Glücklicherweise werden die bladeRFs bereits im Werk kalibriert, wie wir später in diesem Abschnitt besprechen. Wenn du jedoch entsprechende Messgeräte zur Verfügung hast, kannst du diesen Wert jederzeit feinabstimmen, besonders wenn die Frequenz des Oszillators im Laufe der Jahre driftet. + +.. image:: ../_images/bladeRF-2.0-micro-Block-Diagram-4-oscillator.png + :scale: 80 % + :align: center + :alt: bladeRF 2.0 Blockdiagramm mit Oszillator + +Bei Verwendung einer externen Frequenzreferenz (die nahezu jede Frequenz bis 300 MHz haben kann) wird das Referenzsignal direkt in den `Analog Devices ADF4002 `_ PLL auf dem bladeRF eingespeist. Dieser PLL synchronisiert sich mit dem Referenzsignal und sendet ein Signal an den VCTCXO (wie oben in Blau dargestellt), das proportional zur Frequenz- und Phasendifferenz zwischen dem (skalierten) Referenzeingang und dem VCTCXO-Ausgang ist. Sobald der PLL eingerastet ist, ist dieses Signal zwischen PLL und VCTCXO eine stationäre Gleichspannung, die den VCTCXO-Ausgang bei „genau" 38,4 MHz hält (vorausgesetzt, die Referenz war korrekt) und phasensynchron mit dem Referenzeingang ist. Bei Verwendung einer externen Referenz muss :code:`clock_ref` aktiviert werden (entweder über Python oder die CLI) und die Eingangsreferenzfrequenz (auch als :code:`refin_freq` bezeichnet, standardmäßig 10 MHz) eingestellt werden. Gründe für die Verwendung einer externen Referenz sind eine bessere Frequenzgenauigkeit und die Möglichkeit, mehrere SDRs mit derselben Referenz zu synchronisieren. + +Jeder VCTCXO-DAC-Trimwert des bladeRF wird im Werk auf innerhalb von 1 Hz bei 38,4 MHz bei Raumtemperatur kalibriert. Du kannst deine Seriennummer auf `dieser Seite `_ eingeben, um den werksseitig kalibrierten Wert abzufragen (die Seriennummer findest du auf der Platine oder mit :code:`bladerf-tool probe`). Laut Nuand sollte eine neue Platine gut innerhalb von 0,5 ppm und wahrscheinlich näher an 0,1 ppm liegen. Wenn du Messgeräte zur Überprüfung der Frequenzgenauigkeit hast oder den Wert auf den Werkswert setzen möchtest, kannst du folgende Befehle verwenden: + +.. code-block:: bash + + $ bladeRF-cli -i + bladeRF> flash_init_cal 301 0x2049 + +Ersetze dabei :code:`301` durch deine bladeRF-Größe und :code:`0x2049` durch den Hex-Wert deines VCTCXO-DAC-Trimwerts. Ein Neustart ist erforderlich, damit die Änderung wirksam wird. + +*********************************** +Abtastung bei 122 MHz +*********************************** + +Kommt bald! + +*********************************** +Erweiterungsports +*********************************** + +Das bladeRF 2.0 verfügt über einen Erweiterungsport mit einem BSH-030-Steckverbinder. Weitere Informationen zur Verwendung dieses Ports folgen bald! + +******************************** +Weiterführende Literatur +******************************** + +#. `bladeRF Wiki `_ +#. `Nuands txrx.py Beispiel `_ diff --git a/content-de/channel_coding.rst b/content-de/channel_coding.rst new file mode 100644 index 00000000..b9df3ed6 --- /dev/null +++ b/content-de/channel_coding.rst @@ -0,0 +1,186 @@ +.. _channel-coding-chapter: + +##################### +Kanalcodierung +##################### + +In diesem Kapitel stellen wir die Grundlagen der Kanalcodierung vor, auch bekannt als Vorwärtsfehlerkorrektur (FEC), die Shannon-Grenze, Hamming-Codes, Turbo-Codes und LDPC-Codes. Kanalcodierung ist ein riesiges Gebiet innerhalb der drahtlosen Kommunikation und ein Zweig der „Informationstheorie", die sich mit der Quantifizierung, Speicherung und Übertragung von Informationen befasst. + +*************************** +Warum wir Kanalcodierung brauchen +*************************** + +Wie wir im Kapitel :ref:`noise-chapter` gelernt haben, sind drahtlose Kanäle verrauscht, und unsere digitalen Symbole erreichen den Empfänger nicht perfekt. Wenn du einen Netzwerkkurs belegt hast, weißt du vielleicht bereits von zyklischen Redundanzprüfungen (CRCs), die Fehler auf der Empfangsseite **erkennen**. Der Zweck der Kanalcodierung ist es, Fehler am Empfänger zu erkennen **und zu korrigieren**. Wenn wir etwas Spielraum für Fehler zulassen, können wir zum Beispiel mit einem höherwertigen Modulationsschema senden, ohne eine unterbrochene Verbindung zu haben. Als visuelles Beispiel betrachte die folgenden Konstellationen, die QPSK (links) und 16QAM (rechts) bei gleichem Rauschen zeigen. QPSK liefert 2 Bits pro Symbol, während 16QAM mit 4 Bits pro Symbol die doppelte Datenrate bietet. Beachte jedoch, wie in der QPSK-Konstellation die Symbole tendenziell die Symbolentscheidungsgrenze (die x- und y-Achse) nicht überschreiten, was bedeutet, dass die Symbole korrekt empfangen werden. In der 16QAM-Darstellung hingegen gibt es Überlappungen in den Clustern, und infolgedessen werden viele Symbole falsch empfangen. + +.. image:: ../_images_de/qpsk_vs_16qam.png + :scale: 90 % + :align: center + :alt: Vergleich von verrauschtem QPSK und 16QAM zur Demonstration der Notwendigkeit von Vorwärtsfehlerkorrektur (Kanalcodierung) + +Ein fehlgeschlagener CRC führt normalerweise zu einer erneuten Übertragung, zumindest bei Protokollen wie TCP. Wenn Alice eine Nachricht an Bob sendet, wäre es vorzuziehen, Bob nicht dazu bringen zu müssen, eine Nachricht an Alice zurückzusenden, um die Informationen erneut anzufordern. Der Zweck der Kanalcodierung ist es, **redundante** Informationen zu übertragen. Die Redundanz ist eine Ausfallsicherung, die die Anzahl fehlerhafter Pakete, Neuübertragungen oder verlorener Daten reduziert. + +Wir haben besprochen, warum wir Kanalcodierung brauchen. Schauen wir uns an, wo sie in der Sende-Empfangs-Kette vorkommt: + +.. image:: ../_images_de/tx_rx_chain.svg + :align: center + :target: ../_images_de/tx_rx_chain.svg + :alt: Die Sende-Empfangs-Kette der drahtlosen Kommunikation, die beide Seiten eines Transceivers zeigt + +Beachte, dass es mehrere Codierungsschritte in der Sende-Empfangs-Kette gibt. Quellencodierung, unser erster Schritt, ist nicht dasselbe wie Kanalcodierung; Quellencodierung soll die zu übertragenden Daten so weit wie möglich komprimieren, ähnlich wie beim Zippen von Dateien zur Platzeinsparung. Die Ausgabe des Quellencodierungsblocks sollte nämlich **kleiner** als die Dateneingabe sein, aber die Ausgabe der Kanalcodierung wird größer als ihre Eingabe sein, da Redundanz hinzugefügt wird. + +*************************** +Arten von Codes +*************************** + +Zur Kanalcodierung verwenden wir einen „Fehlerkorrekturcode". Dieser Code sagt uns, welche Bits wir angesichts der zu übertragenden Bits tatsächlich senden? Der grundlegendste Code wird als „Wiederholungscodierung" bezeichnet, bei der man einfach ein Bit N-mal hintereinander wiederholt. Bei Wiederholungs-3-Code würde man jedes Bit dreimal übertragen: + +.. role:: raw-html(raw) + :format: html + +- 0 :raw-html:`→` 000 +- 1 :raw-html:`→` 111 + +Die Nachricht 10010110 wird nach der Kanalcodierung als 111000000111000111111000 übertragen. + +Einige Codes arbeiten auf „Blöcken" von Eingangsbits, während andere einen Streamansatz verwenden. Codes, die auf Blöcken mit fester Länge arbeiten, werden als „Blockcodes" bezeichnet, während Codes, die auf einem Bitstrom beliebiger Länge arbeiten, als „Faltungscodes" bezeichnet werden. Dies sind die zwei primären Arten von Codes. Unser Wiederholungs-3-Code ist ein Blockcode, bei dem jeder Block drei Bits umfasst. + +Als Anmerkung: Diese Fehlerkorrekturcodes werden nicht nur bei der Kanalcodierung für drahtlose Verbindungen verwendet. Hast du schon einmal Informationen auf einer Festplatte oder SSD gespeichert und dich gewundert, warum es beim Zurücklesen nie zu Bitfehlern kommt? Das Schreiben und Lesen von Speicher ist ähnlich wie ein Kommunikationssystem. Festplatten-/SSD-Controller haben eingebaute Fehlerkorrektur. Sie ist für das Betriebssystem transparent und kann proprietär sein, da sie sich vollständig auf der Festplatte/SSD befindet. Für tragbare Medien wie CDs muss die Fehlerkorrektur standardisiert sein. Reed-Solomon-Codes waren bei CD-ROMs üblich. + +*************************** +Coderate +*************************** + +Alle Fehlerkorrekturen beinhalten eine Form von Redundanz. Das bedeutet, wenn wir 100 Bits an Informationen übertragen möchten, müssen wir tatsächlich **mehr als** 100 Bits senden. Die „Coderate" ist das Verhältnis zwischen der Anzahl der Informationsbits und der Gesamtzahl der gesendeten Bits (d.h. Informations- plus Redundanzbits). Zurück zum Wiederholungs-3-Codierungsbeispiel: Wenn ich 100 Bits an Informationen habe, können wir Folgendes bestimmen: + +- 300 Bits werden gesendet +- Nur 100 Bits repräsentieren Informationen +- Coderate = 100/300 = 1/3 + +Die Coderate wird immer kleiner als 1 sein, da es einen Kompromiss zwischen Redundanz und Durchsatz gibt. Eine niedrigere Coderate bedeutet mehr Redundanz und weniger Durchsatz. + +*************************** +Modulation und Codierung +*************************** + +Im Kapitel :ref:`modulation-chapter` haben wir uns mit Rauschen in Modulationsschemas befasst. Bei einem niedrigen SNR benötigst du ein Modulationsschema niedrigerer Ordnung (z.B. QPSK), um mit dem Rauschen umzugehen, und bei einem hohen SNR kannst du Modulation wie 256QAM verwenden, um mehr Bits pro Sekunde zu erhalten. Kanalcodierung ist ähnlich; du möchtest bei niedrigen SNRs niedrigere Coderaten, und bei hohen SNRs kannst du eine Coderate von fast 1 verwenden. Moderne Kommunikationssysteme haben eine Reihe von kombinierten Modulations- und Codierungsschemas, sogenannte MCS (Modulation and Coding Scheme). Jedes MCS spezifiziert ein Modulationsschema und ein Codierungsschema für bestimmte SNR-Pegel. + +Moderne Kommunikationssysteme ändern das MCS adaptiv in Echtzeit basierend auf den Drahtlosnalkanalzuständen. Der Empfänger sendet Feedback über die Kanalqualität an den Sender. Das Feedback muss ausgetauscht werden, bevor sich die Qualität des drahtlosen Kanals ändert, was in der Größenordnung von Millisekunden liegen kann. Dieser adaptive Prozess führt zu höchstmöglichem Durchsatz und wird von modernen Technologien wie LTE, 5G und WLAN verwendet. Unten ist eine Visualisierung eines Zellturms, der während der Übertragung das MCS ändert, während sich die Entfernung eines Benutzers zur Zelle ändert. + +.. image:: ../_images_de/adaptive_mcs.svg + :align: center + :target: ../_images_de/adaptive_mcs.svg + :alt: Modulations- und Codierungsschema (MCS) visualisiert mit einer Mobilfunkbasisstation, bei der jeder Ring die Grenze eines MCS-Schemas darstellt + +Wenn du bei adaptivem MCS den Durchsatz über dem SNR aufträgst, erhältst du eine treppenförmige Kurve wie im Graphen unten. Protokolle wie LTE haben oft eine Tabelle, die angibt, welches MCS bei welchem SNR verwendet werden soll. + +.. image:: ../_images_de/adaptive_mcs2.svg + :align: center + :target: ../_images_de/adaptive_mcs2.svg + :alt: Diagramm des Durchsatzes über dem SNR für verschiedene Modulations- und Codierungsschemas (MCS), das eine Treppen- oder Stufenform ergibt + +*************************** +Hamming-Code +*************************** + +Schauen wir uns einen einfachen Fehlerkorrekturcode an. Der Hamming-Code war der erste nicht-triviale entwickelte Code. In den späten 1940er Jahren arbeitete Richard Hamming bei Bell Labs und verwendete einen elektromechanischen Computer, der gestanztes Papierband verwendete. Wenn Fehler in der Maschine erkannt wurden, hielt sie an und Bediener mussten sie beheben. Hamming wurde frustriert davon, seine Programme aufgrund erkannter Fehler von vorne starten zu müssen. Er sagte: „Verdammt nochmal, wenn die Maschine einen Fehler erkennen kann, warum kann sie dann nicht die Position des Fehlers lokalisieren und ihn korrigieren?" Er verbrachte die nächsten Jahre damit, den Hamming-Code zu entwickeln, damit der Computer genau das tun konnte. + +Bei Hamming-Codes werden zusätzliche Bits, sogenannte Paritätsbits oder Prüfbits, zur Information für Redundanz hinzugefügt. Alle Bitpositionen, die Zweierpotenzen sind, sind Paritätsbits: 1, 2, 4, 8 usw. Die anderen Bitpositionen sind für Informationen. Die Tabelle unter diesem Absatz hebt Paritätsbits in Grün hervor. Jedes Paritätsbit „deckt" alle Bits ab, bei denen das bitweise UND des Paritätsbits und der Bitposition ungleich null ist, unten mit einem roten X markiert. Wenn wir ein Datenbit verwenden möchten, brauchen wir die Paritätsbits, die es abdecken. Um bis zum Datenbit d9 gehen zu können, benötigen wir Paritätsbit p8 und alle Paritätsbits, die davor kommen, also sagt uns diese Tabelle, wie viele Paritätsbits wir für eine bestimmte Anzahl von Bits benötigen. Dieses Muster setzt sich unbegrenzt fort. + +.. image:: ../_images_de/hamming.svg + :align: center + :target: ../_images_de/hamming.svg + :alt: Hamming-Code-Muster, das zeigt, wie die Paritätsbit-Abdeckung funktioniert + +Hamming-Codes sind Blockcodes, also arbeiten sie auf N Datenbits auf einmal. Mit drei Paritätsbits können wir also auf Blöcken von vier Datenbits auf einmal arbeiten. Wir stellen dieses Fehlercodierungsschema als Hamming(7,4) dar, wobei das erste Argument die insgesamt übertragenen Bits und das zweite Argument die Datenbits sind. + +.. image:: ../_images_de/hamming2.svg + :align: center + :target: ../_images_de/hamming2.svg + :alt: Beispiel für Hamming 7,4, das drei Paritätsbits hat + +Im Folgenden sind drei wichtige Eigenschaften von Hamming-Codes aufgeführt: + +- Die minimale Anzahl von Bitänderungen, die benötigt wird, um von einem beliebigen Codewort zu einem anderen zu gelangen, ist drei +- Es kann Einzelbit-Fehler korrigieren +- Es kann Zweibit-Fehler erkennen, aber nicht korrigieren + +Algorithmisch kann der Codierungsprozess durch eine einfache Matrixmultiplikation durchgeführt werden, unter Verwendung der sogenannten „Generatormatrix". Im folgenden Beispiel ist der Vektor 1011 die zu codierende Dateneingabe, d.h. die Information, die wir an den Empfänger senden möchten. Die 2D-Matrix ist die Generatormatrix und definiert das Codierungsschema. Das Ergebnis der Multiplikation liefert das zu übertragende Codewort. + +.. image:: ../_images_de/hamming3.png + :scale: 60 % + :align: center + :alt: Matrixmultiplikation zur Codierung von Bits mit einer Generatormatrix unter Verwendung von Hamming-Codes + +Der Sinn des Eintauchens in Hamming-Codes war es, einen Eindruck davon zu geben, wie Fehlercodierung funktioniert. Blockcodes folgen tendenziell diesem Muster. Faltungscodes funktionieren anders, aber wir gehen hier nicht darauf ein; sie verwenden oft die Trellis-Dekodierung, die in einem Diagramm dargestellt werden kann, das so aussieht: + +.. image:: ../_images_de/trellis.svg + :align: center + :scale: 80% + :alt: Ein Trellis-Diagramm oder -Graph wird bei der Faltungscodierung verwendet, um Verbindungen zwischen Knoten darzustellen + +*************************** +Weiche vs. harte Dekodierung +*************************** + +Erinnere dich, dass beim Empfänger die Demodulation vor der Dekodierung erfolgt. Der Demodulator kann uns seine beste Schätzung darüber mitteilen, welches Symbol gesendet wurde, oder er kann den „weichen" Wert ausgeben. Bei BPSK kann der Demodulator anstatt uns 1 oder 0 zu sagen, 0,3423 oder -1,1234 sagen, was auch immer der „weiche" Wert des Symbols war. Typischerweise ist die Dekodierung so ausgelegt, dass sie harte oder weiche Werte verwendet. + +- **Weiche Entscheidungsdekodierung** – verwendet die weichen Werte +- **Harte Entscheidungsdekodierung** – verwendet nur die 1en und 0en + +Weich ist robuster, weil du alle dir zur Verfügung stehenden Informationen verwendest, aber weich ist auch viel komplizierter zu implementieren. Die Hamming-Codes, über die wir gesprochen haben, verwenden harte Entscheidungen, während Faltungscodes dazu neigen, weiche zu verwenden. + +*************************** +Shannon-Grenze +*************************** + +Die Shannon-Grenze oder Shannon-Kapazität ist ein unglaubliches Stück Theorie, das uns sagt, wie viele Bits pro Sekunde an fehlerfreien Informationen wir senden können: + +.. math:: + C = B \cdot log_2 \left( 1 + \frac{S}{N} \right) + +- C – Kanalkapazität [Bits/Sek] +- B – Kanalbandbreite [Hz] +- S – Mittlere empfangene Signalleistung [Watt] +- N – Mittlere Rauschleistung [Watt] + +Diese Gleichung stellt das Beste dar, was ein MCS erreichen kann, wenn es bei einem ausreichend hohen SNR fehlerfrei arbeitet. Es macht mehr Sinn, die Grenze in Bits/Sek/Hz darzustellen, d.h. Bits/Sek pro Spektrumsmenge: + +.. math:: + \frac{C}{B} = log_2 \left( 1 + \mathrm{SNR} \right) + +mit SNR in linearen Einheiten (nicht dB). Beim Aufzeichnen stellen wir SNR jedoch normalerweise in dB dar: + +.. image:: ../_images_de/shannon_limit.svg + :align: center + :target: ../_images_de/shannon_limit.svg + :alt: Diagramm der Shannon-Grenze in Bits pro Sekunde pro Hz über SNR in dB + +Wenn du Shannon-Limit-Diagramme anderswo siehst, die etwas anders aussehen, verwenden sie wahrscheinlich eine x-Achse von „Energie pro Bit" oder :math:`E_b/N_0`, was nur eine Alternative zur Arbeit mit SNR ist. + +Es könnte helfen, die Dinge zu vereinfachen, indem man erkennt, dass bei relativ hohem SNR (z.B. 10 dB oder höher) die Shannon-Grenze als :math:`log_2 \left( \mathrm{SNR} \right)` angenähert werden kann, was ungefähr :math:`\mathrm{SNR_{dB}}/3` entspricht (`hier erklärt `_). Zum Beispiel bei 24 dB SNR erhältst du 8 Bits/Sek/Hz; wenn du also 1 MHz zur Verfügung hast, sind das 8 Mbps. Du denkst vielleicht: „Na ja, das ist nur die theoretische Grenze", aber moderne Kommunikation kommt dieser Grenze sehr nahe, sodass sie zumindest einen groben Anhaltspunkt liefert. Du kannst diese Zahl immer halbieren, um Paket-/Frame-Overhead und nicht-ideale MCS zu berücksichtigen. + +Der maximale Durchsatz von 802.11n WLAN im 2,4-GHz-Band (das 20 MHz breite Kanäle verwendet) beträgt laut Spezifikation 300 Mbps. Natürlich könntest du direkt neben deinem Router sitzen und ein extrem hohes SNR erhalten, vielleicht 60 dB, aber um zuverlässig/praktisch zu sein, wird das MCS mit maximalem Durchsatz (erinnere dich an die Treppenkurve von oben) wahrscheinlich kein so hohes SNR erfordern. Du kannst sogar einen Blick auf die `MCS-Liste für 802.11n `_ werfen. 802.11n geht bis zu 64-QAM, und kombiniert mit Kanalcodierung erfordert es laut `dieser Tabelle `_ ein SNR von etwa 25 dB. Das bedeutet, dass dein WLAN selbst bei 60 dB SNR noch 64-QAM verwenden wird. Also bei 25 dB liegt die Shannon-Grenze bei etwa 8,3 Bits/Sek/Hz, was bei 20 MHz Spektrum 166 Mbps ergibt. Wenn du jedoch MIMO berücksichtigst (das wir in einem zukünftigen Kapitel behandeln werden), kannst du vier dieser Streams parallel laufen lassen, was 664 Mbps ergibt. Teile diese Zahl durch zwei und du erhältst etwas, das der beworbenen Maximalgeschwindigkeit von 300 Mbps für 802.11n WLAN im 2,4-GHz-Band sehr nahekommt. + +Der Beweis hinter der Shannon-Grenze ist ziemlich verrückt; er beinhaltet Mathematik, die so aussieht: + +.. image:: ../_images_de/shannon_limit_proof.png + :scale: 70 % + :align: center + :alt: Beispiel der Mathematik, die beim Shannon-Grenze-Beweis involviert ist + +Für weitere Informationen siehe `hier `_. + +*************************** +Modernste Codes +*************************** + +Derzeit sind die besten Kanalcodierungsschemas: + +1. Turbo-Codes, verwendet in 3G, 4G und NASA-Raumfahrzeugen. +2. LDPC-Codes, verwendet in DVB-S2, WiMAX, IEEE 802.11n. + +Beide Codes nähern sich der Shannon-Grenze (d.h. sie erreichen sie fast unter bestimmten SNRs). Hamming-Codes und andere einfachere Codes kommen der Shannon-Grenze bei weitem nicht nahe. Aus Forschungssicht bleibt in Bezug auf die Codes selbst nicht viel Verbesserungspotenzial. Die aktuelle Forschung konzentriert sich mehr darauf, die Dekodierung recheneffizienter und adaptiver gegenüber Kanal-Feedback zu machen. + +LDPC-Codes (Low-Density Parity-Check) sind eine Klasse hocheffizienter linearer Blockcodes. Sie wurden erstmals von Robert G. Gallager in seiner Doktorarbeit im Jahr 1960 am MIT vorgestellt. Aufgrund der Rechenkomplexität ihrer Implementierung wurden sie bis in die 1990er Jahre ignoriert! Er war 89 Jahre alt zum Zeitpunkt dieses Schreibens (2020), lebt noch und hat viele Preise für seine Arbeit gewonnen (Jahrzehnte nachdem er sie geleistet hatte). LDPC ist nicht patentiert und daher frei verwendbar (im Gegensatz zu Turbo-Codes), weshalb es in vielen offenen Protokollen verwendet wurde. + +Turbo-Codes basieren auf Faltungscodes. Es ist eine Klasse von Codes, die zwei oder mehr einfachere Faltungscodes und einen Interleaver kombiniert. Der grundlegende Patentantrag für Turbo-Codes wurde am 23. April 1991 eingereicht. Die Erfinder waren Franzosen; als Qualcomm Turbo-Codes in CDMA für 3G verwenden wollte, musste es eine gebührenpflichtige Patentlizenzvereinbarung mit France Télécom abschließen. Das primäre Patent lief am 29. August 2013 ab. diff --git a/content-de/cyclostationary.rst b/content-de/cyclostationary.rst new file mode 100644 index 00000000..09f85770 --- /dev/null +++ b/content-de/cyclostationary.rst @@ -0,0 +1,872 @@ +.. _freq-domain-chapter: + +########################## +Zyklostationäre Verarbeitung +########################## + +.. raw:: html + + Mitautorin: Sam Brown + +In diesem Kapitel entmystifizieren wir die zyklostationäre Signalverarbeitung (auch CSP genannt), ein relativ spezialisiertes Gebiet der HF-Signalverarbeitung, das zur Analyse oder Erkennung (oft bei sehr niedrigem SNR!) von Signalen mit zyklostationären Eigenschaften eingesetzt wird, wie z. B. die meisten modernen digitalen Modulationsverfahren. Wir behandeln die zyklische Autokorrelationsfunktion (CAF), die Spektrale Korrelationsfunktion (SCF), die Spektrale Kohärenzfunktion (COH), deren konjugierte Versionen und wie sie angewendet werden können. Dieses Kapitel enthält mehrere vollständige Python-Implementierungen mit Beispielen für BPSK, QPSK, OFDM und mehrere kombinierte Signale. + +**************** +Einführung +**************** + +Zyklostationäre Signalverarbeitung (CSP) ist eine Sammlung von Techniken zur Ausnutzung der zyklostationären Eigenschaft, die in vielen realen Kommunikationssignalen vorkommt. Dazu gehören modulierte Signale wie AM/FM/TV-Rundfunk, Mobilfunk und WLAN sowie Radarsignale und andere Signale, die Periodizität in ihrer Statistik aufweisen. Ein großer Teil der traditionellen Signalverarbeitungstechniken basiert auf der Annahme, dass das Signal stationär ist, d. h. die Statistiken des Signals wie Mittelwert, Varianz und Momente höherer Ordnung ändern sich nicht mit der Zeit. Die meisten realen HF-Signale sind jedoch zyklostationär, d. h. die Statistiken des Signals ändern sich *periodisch* mit der Zeit. CSP-Techniken nutzen diese zyklostationäre Eigenschaft und können zur Erkennung von Signalen im Rauschen, zur Modulationserkennung und zur Trennung von Signalen verwendet werden, die sich sowohl in Zeit als auch in Frequenz überlappen. + +Wenn du nach dem Durchlesen dieses Kapitels und dem Spielen in Python tiefer in CSP eintauchen möchtest, schau dir William Gardners Lehrbuch von 1994 `Cyclostationarity in Communications and Signal Processing `_ an, sein Lehrbuch von 1987 `Statistical Spectral Analysis `_ oder Chad Spooners `Sammlung von Blog-Beiträgen `_. + +Am Ende des SCF-Abschnitts findest du eine interaktive JavaScript-App, mit der du die SCF eines Beispielsignals untersuchen kannst, um zu sehen, wie sich die SCF mit verschiedenen Signal- und SCF-Parametern ändert – alles in deinem Browser! + +************************* +Wiederholung der Autokorrelation +************************* + +Auch wenn du mit der Autokorrelationsfunktion vertraut bist, lohnt es sich, sie kurz zu wiederholen, da sie die Grundlage von CSP ist. Die Autokorrelationsfunktion ist ein Maß für die Ähnlichkeit (auch Korrelation genannt) zwischen einem Signal und seiner zeitverschobenen Version. Sie gibt an, in welchem Maße ein Signal repetitives Verhalten aufweist. Die Autokorrelation des Signals :math:`x(t)` ist definiert als: + +.. math:: + R_x(\tau) = E[x(t)x^*(t-\tau)] + +wobei :math:`E` der Erwartungswertoperator, :math:`\tau` die Zeitverzögerung und :math:`*` das Symbol für die komplexe Konjugation ist. In diskreter Zeit mit einer begrenzten Anzahl von Samples wird dies zu: + +.. math:: + R_x(\tau) = \frac{1}{N} \sum_{n=-N/2}^{N/2} x\left[ n+\frac{\tau}{2} \right] x^*\left[ n-\frac{\tau}{2} \right] + +wobei :math:`N` die Anzahl der Samples im Signal ist. + +Wenn das Signal in irgendeiner Weise periodisch ist, wie z. B. die sich wiederholende Symbolform eines QPSK-Signals, dann wird auch die Autokorrelation über einen Bereich von Tau periodisch sein. Wenn ein QPSK-Signal beispielsweise 8 Samples pro Symbol hat, gibt es bei Tau als ganzzahligem Vielfachen von 8 ein viel stärkeres „Ähnlichkeitsmaß" als bei anderen Tau-Werten. + +************************************************ +Die Zyklische Autokorrelationsfunktion (CAF) +************************************************ + +Wir wollen herausfinden, wann in unserer Autokorrelation Periodizität vorhanden ist. Erinnere dich an die Fourier-Transformationsgleichung: Wenn wir testen wollen, wie stark eine bestimmte Frequenz :math:`f` in einem beliebigen Signal :math:`x(t)` vorhanden ist, können wir dies mit: + +.. math:: + X(f) = \int x(t) e^{-j2\pi ft} dt + +Um also Periodizität in unserer Autokorrelation zu finden, berechnen wir: + +.. math:: + R_x(\tau, \alpha) = \lim_{T\rightarrow\infty} \frac{1}{T} \int_{-T/2}^{T/2} x(t + \tau/2)x^*(t - \tau/2)e^{-j2\pi \alpha t}dt. + +oder in diskreter Zeit: + +.. math:: + R_x(\tau, \alpha) = \frac{1}{N} \sum_{n=-N/2}^{N/2} x\left[ n+\frac{\tau}{2} \right] x^*\left[ n-\frac{\tau}{2} \right] e^{-j2\pi \alpha n} + +Dies testet, wie stark die Frequenz :math:`\alpha` ist. Wir nennen die obige Gleichung die Zyklische Autokorrelationsfunktion (CAF). Eine andere Möglichkeit, die CAF zu betrachten, ist als Satz von Fourier-Reihenkoeffizienten, die diese Periodizität beschreiben. Wir verwenden den Begriff „zyklostationär" für Signale, die eine periodische oder fast periodische Autokorrelation besitzen. + +In Python kann die CAF des Basisbands :code:`samples` bei einem gegebenen :code:`alpha`- und :code:`tau`-Wert mit dem folgenden Code-Schnipsel berechnet werden: + +.. code-block:: python + + CAF = (np.exp(1j * np.pi * alpha * tau) * + np.sum(samples * np.conj(np.roll(samples, tau)) * + np.exp(-2j * np.pi * alpha * np.arange(N)))) + +Wir verwenden :code:`np.roll()`, um einen der Sample-Sätze um tau zu verschieben, da die Verschiebung um eine ganzzahlige Anzahl von Samples erfolgen muss. + +Um mit der CAF in Python zu spielen, simulieren wir zunächst ein Beispielsignal: ein rechteckiges BPSK-Signal (d. h. BPSK ohne Impulsformung) mit 20 Samples pro Symbol, weißem Gaußschen Rauschen (AWGN) und einem Frequenzversatz von 0,2 Hz: + +.. code-block:: python + + N = 100000 # Anzahl der zu simulierenden Samples + f_offset = 0.2 # Hz normiert + sps = 20 # zyklische Freq (alpha) wird 1/sps oder 0,05 Hz normiert sein + + symbols = np.random.randint(0, 2, int(np.ceil(N/sps))) * 2 - 1 # zufällige 1en und -1en + bpsk = np.repeat(symbols, sps) # jedes Symbol sps-mal wiederholen + bpsk = bpsk[:N] # auf N Samples kürzen + bpsk = bpsk * np.exp(2j * np.pi * f_offset * np.arange(N)) # BPSK nach oben verschieben (macht es auch komplex) + noise = np.random.randn(N) + 1j*np.random.randn(N) # komplexes weißes Gaußsches Rauschen + samples = bpsk + 0.1*noise # Rauschen zum Signal hinzufügen + +Da die absolute Abtastrate und Symbolrate in diesem Kapitel keine Rolle spielen, verwenden wir normierte Frequenz (entspricht Abtastrate = 1 Hz). Das bedeutet, das Signal muss zwischen -0,5 und +0,5 Hz liegen. + +Zur Veranschaulichung zeigen wir die Leistungsspektraldichte (d. h. FFT) des Signals *vor* jeder CSP-Verarbeitung: + +.. image:: ../_images/psd_of_bpsk_used_for_caf.svg + :align: center + :target: ../_images/psd_of_bpsk_used_for_caf.svg + :alt: PSD des für CAF verwendeten BPSK + +Wir berechnen nun die CAF bei dem richtigen Alpha (1/20 = 0,05 Hz) über einen Bereich von Tau-Werten: + +.. code-block:: python + + # CAF nur beim richtigen Alpha + alpha_of_interest = 1/sps # entspricht 0,05 Hz + taus = np.arange(-50, 51) + CAF = np.zeros(len(taus), dtype=complex) + for i in range(len(taus)): + CAF[i] = (np.exp(1j * np.pi * alpha_of_interest * taus[i]) * + np.sum(samples * np.conj(np.roll(samples, taus[i])) * + np.exp(-2j * np.pi * alpha_of_interest * np.arange(N)))) + +.. image:: ../_images/caf_at_correct_alpha.svg + :align: center + :target: ../_images/caf_at_correct_alpha.svg + :alt: CAF beim richtigen Alpha + +Zum Vergleich die CAF bei einem falschen Alpha (0,08 Hz): + +.. image:: ../_images/caf_at_incorrect_alpha.svg + :align: center + :target: ../_images/caf_at_incorrect_alpha.svg + :alt: CAF beim falschen Alpha + +Beachte die y-Achse – viel weniger Energie in der CAF diesmal. Wir können die CAF über einen Bereich von Alphas berechnen und bei jedem Alpha die Leistung in der CAF bestimmen: + +.. code-block:: python + + alphas = np.arange(0, 0.5, 0.005) + CAF = np.zeros((len(alphas), len(taus)), dtype=complex) + for j in range(len(alphas)): + for i in range(len(taus)): + CAF[j, i] = (np.exp(1j * np.pi * alphas[j] * taus[i]) * + np.sum(samples * np.conj(np.roll(samples, taus[i])) * + np.exp(-2j * np.pi * alphas[j] * np.arange(N)))) + CAF_magnitudes = np.average(np.abs(CAF), axis=1) # bei jedem Alpha Leistung berechnen + plt.plot(alphas, CAF_magnitudes) + plt.xlabel('Alpha') + plt.ylabel('CAF-Leistung') + +.. image:: ../_images/caf_avg_over_alpha.svg + :align: center + :target: ../_images/caf_avg_over_alpha.svg + :alt: CAF-Durchschnitt über Alpha + +Wir sehen nicht nur den erwarteten Spike bei 0,05 Hz, sondern auch Spikes bei ganzzahligen Vielfachen davon. Dies liegt daran, dass die CAF eine Fourier-Reihe ist und die Harmonischen der Grundfrequenz in der CAF vorhanden sind. + +************************************************ +Die Spektrale Korrelationsfunktion (SCF) +************************************************ + +So wie die CAF die Periodizität in der Autokorrelation eines Signals zeigt, zeigt die SCF die Periodizität in der PSD eines Signals. Autokorrelation und PSD sind ein Fourier-Transformationspaar, und daher sollte es keine Überraschung sein, dass CAF und SCF ebenfalls ein Fourier-Transformationspaar sind. Diese Beziehung ist als *Zyklische Wiener-Beziehung* bekannt. + +Man kann einfach die Fourier-Transformierte der CAF nehmen, um die SCF zu erhalten. Zurück zu unserem BPSK-Signal mit 20 Samples pro Symbol – schauen wir uns die SCF beim richtigen Alpha (0,05 Hz) an. Alles was wir tun müssen, ist die FFT der CAF zu nehmen und den Betrag zu plotten: + +.. code-block:: python + + f = np.linspace(-0.5, 0.5, len(taus)) + SCF = np.fft.fftshift(np.fft.fft(CAF)) + plt.plot(f, np.abs(SCF)) + plt.xlabel('Frequenz') + plt.ylabel('SCF') + +.. image:: ../_images/fft_of_caf.svg + :align: center + :target: ../_images/fft_of_caf.svg + :alt: FFT der CAF + +Wir sehen den 0,2-Hz-Frequenzversatz, den wir bei der Simulation des BPSK-Signals angewendet haben. Unten ist eine interaktive JavaScript-App, die eine SCF implementiert, damit du mit verschiedenen Signal- und SCF-Parametern spielen kannst: + +.. raw:: html + +
+ + +
+ + + 0.2 +
+ + + 20 +
+ + + + +
+ + +
+ + +
+ + +
+ + +
+ +
+
+ +
+ + + + + +******************************** +Frequenzglättungsmethode (FSM) +******************************** + +Nun betrachten wir, wie wir die SCF effizient berechnen können. Betrachte zunächst das Periodogramm, das einfach der quadratische Betrag der Fourier-Transformierten eines Signals ist: + +.. math:: + + I(u,f) = \frac{1}{N}\left|X(u,f)\right|^2 + +Wir können das zyklische Periodogramm durch das Produkt zweier frequenzverschobener Fourier-Transformierter erhalten: + +.. math:: + + I(u,f,\alpha) = \frac{1}{N}X(u,f + \alpha/2) X^*(u,f - \alpha/2) + +Beide stellen Schätzungen der PSD und der SCF dar; zur Mittelung über die Frequenz verwenden wir die Frequenzglättungsmethode (FSM): + +.. math:: + S_X(f, \alpha) = \lim_{\Delta\rightarrow 0} \lim_{T\rightarrow \infty} \frac{1}{T} g_{\Delta}(f) \otimes \left[X(t,f + \alpha/2) X^*(t,f - \alpha/2)\right] + +Unten ist eine minimale Python-Implementierung der FSM. Zuerst berechnet sie das zyklische Periodogramm durch Multiplikation zweier verschobener FFT-Versionen, dann wird jede Scheibe mit einer Fensterfunktion gefiltert, deren Länge die Auflösung der resultierenden SCF-Schätzung bestimmt: + +.. code-block:: python + + alphas = np.arange(0, 0.3, 0.001) + Nw = 256 # Fensterlänge + N = len(samples) # Signallänge + window = np.hanning(Nw) + + X = np.fft.fftshift(np.fft.fft(samples)) # FFT des gesamten Signals + + num_freqs = int(np.ceil(N/Nw)) # Frequenzauflösung nach Dezimierung + SCF = np.zeros((len(alphas), num_freqs), dtype=complex) + for i in range(len(alphas)): + shift = int(alphas[i] * N/2) + SCF_slice = np.roll(X, -shift) * np.conj(np.roll(X, shift)) + SCF[i, :] = np.convolve(SCF_slice, window, mode='same')[::Nw] # Fenster anwenden und um Nw dezimieren + SCF = np.abs(SCF) + SCF[0, :] = 0 # alpha=0 auf Null setzen, da es nur die PSD des Signals ist + + extent = (-0.5, 0.5, float(np.max(alphas)), float(np.min(alphas))) + plt.imshow(SCF, aspect='auto', extent=extent, vmax=np.max(SCF)/2) + plt.xlabel('Frequenz [normiert Hz]') + plt.ylabel('Zyklische Frequenz [normiert Hz]') + plt.show() + +Beachte, dass aufgrund der Art und Weise, wie die Verschiebung berechnet und auf eine ganzzahlige Anzahl von Samples gerundet wird, es hilft, mindestens :code:`2 / alpha_resolution` Samples auf einmal zu verarbeiten. + +SCF für das rechteckige BPSK-Signal mit 20 Samples pro Symbol: + +.. image:: ../_images/scf_freq_smoothing.svg + :align: center + :target: ../_images/scf_freq_smoothing.svg + :alt: SCF mit der Frequenzglättungsmethode (FSM) + +*************************** +Zeitglättungsmethode (TSM) +*************************** + +Nun betrachten wir eine Implementierung der TSM in Python. Der folgende Code-Schnipsel teilt das Signal in *num_windows* Blöcke auf, jeder der Länge *Nw* mit einer Überlappung von *Noverlap*: + +.. code-block:: python + + alphas = np.arange(0, 0.3, 0.001) + Nw = 256 # Fensterlänge + N = len(samples) # Signallänge + Noverlap = int(2/3*Nw) # Blocküberlappung + num_windows = int((N - Noverlap) / (Nw - Noverlap)) # Anzahl der Fenster + window = np.hanning(Nw) + + SCF = np.zeros((len(alphas), Nw), dtype=complex) + for ii in range(len(alphas)): # Schleife über zyklische Frequenzen + neg = samples * np.exp(-1j*np.pi*alphas[ii]*np.arange(N)) + pos = samples * np.exp( 1j*np.pi*alphas[ii]*np.arange(N)) + for i in range(num_windows): + pos_slice = window * pos[i*(Nw-Noverlap):i*(Nw-Noverlap)+Nw] + neg_slice = window * neg[i*(Nw-Noverlap):i*(Nw-Noverlap)+Nw] + SCF[ii, :] += np.fft.fft(neg_slice) * np.conj(np.fft.fft(pos_slice)) # Kreuz-Zyklisches Leistungsspektrum + SCF = np.fft.fftshift(SCF, axes=1) # HF-Frequenzachse verschieben + SCF = np.abs(SCF) + SCF[0, :] = 0 # alpha=0 auf Null setzen + + extent = (-0.5, 0.5, float(np.max(alphas)), float(np.min(alphas))) + plt.imshow(SCF, aspect='auto', extent=extent, vmax=np.max(SCF)/2) + plt.xlabel('Frequenz [normiert Hz]') + plt.ylabel('Zyklische Frequenz [normiert Hz]') + plt.show() + +.. image:: ../_images/scf_time_smoothing.svg + :align: center + :target: ../_images/scf_time_smoothing.svg + :alt: SCF mit der Zeitglättungsmethode (TSM) + +Sieht ungefähr genauso aus wie die FSM! + +***************** +Impulsgeformtes BPSK +***************** + +Bisher haben wir nur CSP eines *rechteckigen* BPSK-Signals untersucht. In realen HF-Systemen sehen wir jedoch fast nie rechteckige Pulse. Schauen wir uns jetzt ein BPSK-Signal mit einer Raised-Cosine (RC)-Impulsform an, die eine gängige Impulsform in der digitalen Kommunikation ist: + +.. math:: + h(t) = \mathrm{sinc}\left( \frac{t}{T} \right) \frac{\cos\left(\frac{\pi\beta t}{T}\right)}{1 - \left( \frac{2 \beta t}{T} \right)^2} + +Der Parameter :math:`\beta` bestimmt, wie schnell der Filter im Zeitbereich abklingt: + +.. image:: ../_images/raised_cosine_freq.svg + :align: center + :target: ../_images/raised_cosine_freq.svg + :alt: Der Raised-Cosine-Filter im Frequenzbereich mit verschiedenen Roll-Off-Werten + +Simulation eines BPSK-Signals mit Raised-Cosine-Impulsformung: + +.. code-block:: python + + N = 100000 # Anzahl der zu simulierenden Samples + f_offset = 0.2 # Hz normiert + sps = 20 + num_symbols = int(np.ceil(N/sps)) + symbols = np.random.randint(0, 2, num_symbols) * 2 - 1 # zufällige 1en und -1en + + pulse_train = np.zeros(num_symbols * sps) + pulse_train[::sps] = symbols + + # Raised-Cosine-Filter für Impulsformung + beta = 0.3 # Roll-Off-Parameter + num_taps = 101 + t = np.arange(num_taps) - (num_taps-1)//2 + h = np.sinc(t/sps) * np.cos(np.pi*beta*t/sps) / (1 - (2*beta*t/sps)**2) # RC-Gleichung + bpsk = np.convolve(pulse_train, h, 'same') # Impulsformung anwenden + + bpsk = bpsk[:N] + bpsk = bpsk * np.exp(2j * np.pi * f_offset * np.arange(N)) + noise = np.random.randn(N) + 1j*np.random.randn(N) + samples = bpsk + 0.1*noise + +.. image:: ../_images/pulse_shaped_BSPK.svg + :align: center + :target: ../_images/pulse_shaped_BSPK.svg + :alt: Impulsgeformtes BPSK-Signal mit Raised-Cosine-Impulsform + +SCF des impulsgeformten BPSK mit verschiedenen Roll-Off-Werten: + +:code:`beta = 0.3`: + +.. image:: ../_images/scf_freq_smoothing_pulse_shaped_bpsk.svg + :align: center + :target: ../_images/scf_freq_smoothing_pulse_shaped_bpsk.svg + :alt: SCF des impulsgeformten BPSK (FSM) beta 0,3 + +:code:`beta = 0.6`: + +.. image:: ../_images/scf_freq_smoothing_pulse_shaped_bpsk2.svg + :align: center + :target: ../_images/scf_freq_smoothing_pulse_shaped_bpsk2.svg + :alt: SCF des impulsgeformten BPSK (FSM) beta 0,6 + +:code:`beta = 0.9`: + +.. image:: ../_images/scf_freq_smoothing_pulse_shaped_bpsk3.svg + :align: center + :target: ../_images/scf_freq_smoothing_pulse_shaped_bpsk3.svg + :alt: SCF des impulsgeformten BPSK (FSM) beta 0,9 + +In allen drei erhalten wir keine Nebenkeulen in der Frequenzachse mehr, und in der zyklischen Frequenzachse bekommen wir nicht mehr die gleichen starken Harmonischen. Impulsgeformte Signale haben tendenziell eine viel „sauberere" SCF als rechteckige Signale, die einem einzelnen Spike mit einer Verschmierung darüber ähnelt. + +******************************** +SNR und Anzahl der Symbole +******************************** + +Demnächst! Wir werden behandeln, wie ab einem bestimmten Punkt ein höheres SNR nicht hilft und stattdessen mehr Symbole benötigt werden, und wie paketbasierte Wellenformen zu einer begrenzten Anzahl von Symbolen pro Übertragung führen. + +******************************** +QPSK und Modulation höherer Ordnung +******************************** + +Demnächst! Es werden QPSK, PSK höherer Ordnung, QAM und eine kurze Einführung in zyklische Momente und Kumulanten höherer Ordnung enthalten sein. + +******************************** +Mehrere überlappende Signale +******************************** + +Bisher haben wir uns immer nur ein Signal auf einmal angeschaut, aber was wenn unser empfangenes Signal mehrere einzelne Signale enthält, die sich in Frequenz, Zeit und sogar zyklischer Frequenz überlappen? In CSP sind wir oft damit beschäftigt, das Vorhandensein von Signalen bei verschiedenen zyklischen Frequenzen zu erkennen, die sich in Zeit und Frequenz überlappen. + +Wir simulieren drei Signale mit unterschiedlichen Eigenschaften: + +* Signal 1: Rechteckiges BPSK mit 20 Samples pro Symbol und 0,2 Hz Frequenzversatz +* Signal 2: Impulsgeformtes BPSK mit 20 Samples pro Symbol, -0,1 Hz Frequenzversatz und Roll-Off 0,35 +* Signal 3: Impulsgeformtes QPSK mit 4 Samples pro Symbol, 0,2 Hz Frequenzversatz und Roll-Off 0,21 + +Zwei Signale haben dieselbe zyklische Frequenz, und zwei haben dieselbe HF-Frequenz, was uns verschiedene Überlappungsgrade zum Experimentieren ermöglicht. + +.. raw:: html + +
+ Python-Code zur Simulation der drei Signale aufklappen + +.. code-block:: python + + N = 1000000 # Anzahl der zu simulierenden Samples + + def fractional_delay(x, delay): + N = 21 # Anzahl der Koeffizienten + n = np.arange(-N//2, N//2) # ...-3,-2,-1,0,1,2,3... + h = np.sinc(n - delay) # Filterkoeffizienten berechnen + h *= np.hamming(N) # Filter fensterieren + h /= np.sum(h) # normalisieren + return np.convolve(x, h, 'same') # Filter anwenden + + # Signal 1, Rechteck-BPSK + sps = 20 + f_offset = 0.2 + signal1 = np.repeat(np.random.randint(0, 2, int(np.ceil(N/sps))) * 2 - 1, sps) + signal1 = signal1[:N] * np.exp(2j * np.pi * f_offset * np.arange(N)) + signal1 = fractional_delay(signal1, 0.12345) + + # Signal 2, impulsgeformtes BPSK + sps = 20 + f_offset = -0.1 + beta = 0.35 + symbols = np.random.randint(0, 2, int(np.ceil(N/sps))) * 2 - 1 + pulse_train = np.zeros(int(np.ceil(N/sps)) * sps) + pulse_train[::sps] = symbols + t = np.arange(101) - (101-1)//2 + h = np.sinc(t/sps) * np.cos(np.pi*beta*t/sps) / (1 - (2*beta*t/sps)**2) + signal2 = np.convolve(pulse_train, h, 'same') + signal2 = signal2[:N] * np.exp(2j * np.pi * f_offset * np.arange(N)) + signal2 = fractional_delay(signal2, 0.52634) + + # Signal 3, impulsgeformtes QPSK + sps = 4 + f_offset = 0.2 + beta = 0.21 + data = x_int = np.random.randint(0, 4, int(np.ceil(N/sps))) # 0 bis 3 + data_degrees = data*360/4.0 + 45 # 45, 135, 225, 315 Grad + symbols = np.cos(data_degrees*np.pi/180.0) + 1j*np.sin(data_degrees*np.pi/180.0) + pulse_train = np.zeros(int(np.ceil(N/sps)) * sps, dtype=complex) + pulse_train[::sps] = symbols + t = np.arange(101) - (101-1)//2 + h = np.sinc(t/sps) * np.cos(np.pi*beta*t/sps) / (1 - (2*beta*t/sps)**2) + signal3 = np.convolve(pulse_train, h, 'same') + signal3 = signal3[:N] * np.exp(2j * np.pi * f_offset * np.arange(N)) + signal3 = fractional_delay(signal3, 0.3526) + + # Rauschen hinzufügen + noise = np.random.randn(N) + 1j*np.random.randn(N) + samples = 0.5*signal1 + signal2 + 1.5*signal3 + 0.1*noise + +.. raw:: html + +
+ +PSD dieser kombinierten Signale: + +.. image:: ../_images/psd_of_multiple_signals.svg + :align: center + :target: ../_images/psd_of_multiple_signals.svg + :alt: PSD von drei verschiedenen Signalen + +SCF dieser kombinierten Signale mit der FSM: + +.. image:: ../_images/scf_freq_smoothing_pulse_multiple_signals.svg + :align: center + :target: ../_images/scf_freq_smoothing_pulse_multiple_signals.svg + :alt: SCF von drei verschiedenen Signalen (FSM) + +Beachte, dass Signal 1 trotz rechteckiger Impulsform seine Harmonischen im Kegel über Signal 3 verborgen hat. Mittels CSP können wir erkennen, dass Signal 1 vorhanden ist, und eine gute Näherung seiner zyklischen Frequenz erhalten, die dann zur Synchronisation genutzt werden kann. Das ist die Stärke der zyklostationären Signalverarbeitung! + +************************ +Alternative CSP-Merkmale +************************ + +Die SCF ist nicht die einzige Möglichkeit, Zyklostationarität in einem Signal zu erkennen. Eine einfache Methode (sowohl konzeptionell als auch rechnerisch) beinhaltet das **Nehmen der FFT des Betrags** des Signals und die Suche nach Spikes. In Python ist das extrem einfach: + +.. code-block:: python + + samples_mag = np.abs(samples) + magnitude_metric = np.abs(np.fft.fft(samples_mag)) + +Bevor wir das Ergebnis plotten, nullen wir die DC-Komponente aus und nehmen nur die Hälfte der FFT-Ausgabe (da der Eingang reell ist): + +.. code-block:: python + + magnitude_metric = magnitude_metric[:len(magnitude_metric)//2] # nur Hälfte benötigt, da Eingang reell ist + magnitude_metric[0] = 0 # DC-Komponente auf Null setzen + f = np.linspace(-0.5, 0.5, len(samples)) + plt.plot(f, magnitude_metric) + +.. image:: ../_images/non_csp_metric.svg + :align: center + :target: ../_images/non_csp_metric.svg + :alt: Metrik zur Erkennung von Zyklostationarität ohne CAF oder SCF + +Für BPSK-Signale kann auch die FFT des quadrierten Signals berechnet werden; es zeigt einen Spike beim Trägerfrequenzversatz multipliziert mit zwei. Für QPSK die FFT des Signals in der 4. Potenz: + +.. code-block:: python + + samples_squared = samples**2 + squared_metric = np.abs(np.fft.fftshift(np.fft.fft(samples_squared)))/len(samples) + squared_metric[len(squared_metric)//2] = 0 # DC-Komponente auf Null setzen + + samples_quartic = samples**4 + quartic_metric = np.abs(np.fft.fftshift(np.fft.fft(samples_quartic)))/len(samples) + quartic_metric[len(quartic_metric)//2] = 0 # DC-Komponente auf Null setzen + +********************************* +Spektrale Kohärenzfunktion (COH) +********************************* + +*Kurzfassung: Die spektrale Kohärenzfunktion ist eine normierte Version der SCF, die in manchen Situationen anstelle der regulären SCF verwendet werden sollte.* + +Ein weiteres Maß für Zyklostationarität, das in vielen Fällen aufschlussreicher als die rohe SCF sein kann, ist die Spektrale Kohärenzfunktion (COH). Die COH nimmt die SCF und normiert sie so, dass das Ergebnis zwischen -1 und 1 liegt (wobei wir auf den Betrag schauen, der zwischen 0 und 1 liegt). Dies ist nützlich, da es die Informationen über die Zyklostationarität des Signals von Informationen über das Leistungsspektrum des Signals isoliert. + +Um die COH zu berechnen, berechnen wir zunächst die SCF und normieren dann durch das Produkt zweier verschobener PSD-Terme, analog zur Normierung durch das Produkt der Standardabweichungen: + +.. math:: + \rho = C_x(f, \alpha) = \frac{S_X(f,\alpha)}{\sqrt{C_x^0(f + \alpha/2) C_x^0(f - \alpha/2)}} + +Wir wenden das auf unseren Python-Code an (spezifisch die SCF mit der FSM). Die COH-Scheibe wird innerhalb der for-Schleife berechnet: + +.. code-block:: python + + COH_slice = SCF_slice / np.sqrt(np.roll(X, -shift) * np.roll(X, shift)) + +Dann wird dieselbe Faltung und Dezimierung wie für die SCF angewendet: + +.. code-block:: python + + COH[i, :] = np.convolve(COH_slice, window, mode='same')[::Nw] + +.. raw:: html + +
+ Vollständigen Code zur Erzeugung und Darstellung von SCF und COH aufklappen + +.. code-block:: python + + alphas = np.arange(0, 0.3, 0.001) + Nw = 256 # Fensterlänge + N = len(samples) # Signallänge + window = np.hanning(Nw) + + X = np.fft.fftshift(np.fft.fft(samples)) # FFT des gesamten Signals + + num_freqs = int(np.ceil(N/Nw)) + SCF = np.zeros((len(alphas), num_freqs), dtype=complex) + COH = np.zeros((len(alphas), num_freqs), dtype=complex) + for i in range(len(alphas)): + shift = int(alphas[i] * N/2) + SCF_slice = np.roll(X, -shift) * np.conj(np.roll(X, shift)) + SCF[i, :] = np.convolve(SCF_slice, window, mode='same')[::Nw] + COH_slice = SCF_slice / np.sqrt(np.roll(X, -shift) * np.roll(X, shift)) + COH[i, :] = np.convolve(COH_slice, window, mode='same')[::Nw] + SCF = np.abs(SCF) + COH = np.abs(COH) + + # alpha=0 für beide auf Null setzen + SCF[np.argmin(np.abs(alphas)), :] = 0 + COH[np.argmin(np.abs(alphas)), :] = 0 + + extent = (-0.5, 0.5, float(np.max(alphas)), float(np.min(alphas))) + fig, [ax0, ax1] = plt.subplots(1, 2, figsize=(10, 5)) + ax0.imshow(SCF, aspect='auto', extent=extent, vmax=np.max(SCF)/2) + ax0.set_xlabel('Frequenz [normiert Hz]') + ax0.set_ylabel('Zyklische Frequenz [normiert Hz]') + ax0.set_title('Reguläre SCF') + ax1.imshow(COH, aspect='auto', extent=extent, vmax=np.max(COH)/2) + ax1.set_xlabel('Frequenz [normiert Hz]') + ax1.set_title('Spektrale Kohärenzfunktion (COH)') + plt.show() + +.. raw:: html + +
+ +SCF und COH für ein rechteckiges BPSK-Signal mit 20 Samples pro Symbol und 0,2 Hz Frequenzversatz: + +.. image:: ../_images/scf_coherence.svg + :align: center + :target: ../_images/scf_coherence.svg + :alt: SCF und COH eines rechteckigen BPSK-Signals + +Wie zu sehen, sind die höheren Alphas in der COH viel ausgeprägter als in der SCF. Für das impulsgeformte BPSK-Signal: + +.. image:: ../_images/scf_coherence_pulse_shaped.svg + :align: center + :target: ../_images/scf_coherence_pulse_shaped.svg + :alt: SCF und COH eines impulsgeformten BPSK-Signals + +Versuche, sowohl SCF als auch COH für deine Anwendung zu generieren, um zu sehen, welche besser funktioniert! + +********** +Konjugierte +********** + +Bisher haben wir die folgenden Formeln für die CAF und die SCF verwendet, bei denen die komplexe Konjugation (:math:`*` Symbol) des Signals im zweiten Term verwendet wird: + +.. math:: + R_x(\tau,\alpha) = \lim_{T\rightarrow\infty} \frac{1}{T} \int_{-T/2}^{T/2} x(t + \tau/2)x^*(t - \tau/2)e^{-j2\pi \alpha t}dt \\ + S_X(f,\alpha) = \lim_{T\rightarrow\infty} \frac{1}{T} \lim_{U\rightarrow\infty} \frac{1}{U} \int_{-U/2}^{U/2} X(t,f + \alpha/2) X^*(t,f - \alpha/2) dt + +Es gibt jedoch eine alternative Form für CAF und SCF, bei der keine Konjugation enthalten ist. Diese Formen werden als *konjugierte CAF* und *konjugierte SCF* bezeichnet. Die konjugierten CAF und SCF sind wie folgt definiert: + +.. math:: + R_{x^*}(\tau,\alpha) = \lim_{T\rightarrow\infty} \frac{1}{T} \int_{-T/2}^{T/2} x(t + \tau/2)x(t - \tau/2)e^{-j2\pi \alpha t}dt \\ + S_{x^*}(f,\alpha) = \lim_{T\rightarrow\infty} \frac{1}{T} \lim_{U\rightarrow\infty} \frac{1}{U} \int_{-U/2}^{U/2} X(t,f + \alpha/2) X(t,f - \alpha/2) dt + +Zur Implementierung der konjugierten SCF mit der FSM gibt es neben dem Entfernen des :code:`conj()` noch einen zusätzlichen Schritt. Es gibt eine Eigenschaft der Fourier-Transformierten, die besagt, dass eine komplexe Konjugation im Zeitbereich einer Spiegelung und Konjugation im Frequenzbereich entspricht: + +.. math:: + x^*(t) \leftrightarrow X^*(-f) + +Daher ist der Code wie folgt: + +.. code-block:: python + + SCF_slice = np.roll(X, -shift) * np.flip(np.roll(X, -shift - 1)) + +Beachte das hinzugefügte :code:`np.flip()`, und :code:`roll()` muss in die umgekehrte Richtung erfolgen. Die vollständige FSM-Implementierung der konjugierten SCF: + +.. code-block:: python + + alphas = np.arange(-1, 1, 0.01) # Konj. SCF sollte von -1 bis +1 berechnet werden + Nw = 256 # Fensterlänge + N = len(samples) # Signallänge + window = np.hanning(Nw) + + X = np.fft.fftshift(np.fft.fft(samples)) # FFT des gesamten Signals + + num_freqs = int(np.ceil(N/Nw)) + SCF = np.zeros((len(alphas), num_freqs), dtype=complex) + for i in range(len(alphas)): + shift = int(np.round(alphas[i] * N/2)) + SCF_slice = np.roll(X, -shift) * np.flip(np.roll(X, -shift - 1)) # DIESER UNTERSCHIED + SCF[i, :] = np.convolve(SCF_slice, window, mode='same')[::Nw] + SCF = np.abs(SCF) + + extent = (-0.5, 0.5, float(np.min(alphas)), float(np.max(alphas))) + plt.imshow(SCF, aspect='auto', extent=extent, vmax=np.max(SCF)/2, origin='lower') + plt.xlabel('Frequenz [normiert Hz]') + plt.ylabel('Zyklische Frequenz [normiert Hz]') + plt.show() + +Ein weiterer wichtiger Unterschied bei der konjugierten SCF ist, dass wir Alphas zwischen -1 und +1 berechnen wollen, während wir bei der normalen SCF aufgrund der Symmetrie nur 0,0 bis 0,5 gemacht haben. + +Konjugierte SCF des rechteckigen BPSK-Signals mit 20 Samples pro Symbol und 0,2 Hz Frequenzversatz: + +.. image:: ../_images/scf_conj_rect_bpsk.svg + :align: center + :target: ../_images/scf_conj_rect_bpsk.svg + :alt: Konjugierte SCF des rechteckigen BPSK (FSM) + +Das Wichtigste aus diesem Abschnitt: Was du in der konjugierten SCF erhältst, sind Spikes bei der zyklischen Frequenz +/- **zweimal** dem Trägerfrequenzversatz, den wir als :math:`f_c` bezeichnen. In der Frequenzachse liegt er bei 0 Hz statt bei :math:`f_c`. Unser Frequenzversatz war 0,2 Hz, also erhalten wir Spikes bei 0,4 Hz +/- der zyklischen Frequenz 0,05 Hz. Merke dir: + +.. math:: + 2f_c \pm \alpha + +Konjugierte SCF des impulsgeformten BPSK: + +.. image:: ../_images/scf_conj_pulseshaped_bpsk.svg + :align: center + :target: ../_images/scf_conj_pulseshaped_bpsk.svg + :alt: Konjugierte SCF des impulsgeformten BPSK (FSM) + +Konjugierte SCF des rechteckigen QPSK: + +.. image:: ../_images/scf_conj_rect_qpsk.svg + :align: center + :target: ../_images/scf_conj_rect_qpsk.svg + :alt: Konjugierte SCF des rechteckigen QPSK (FSM) + +Beachte die Farbskala – die gesamte Ausgabe ist relativ niedrig, denn es gibt *keine Spikes in der konjugierten SCF bei QPSK*. Hier ist dieselbe Ausgabe mit Skalierung entsprechend unseren BPSK-Beispielen: + +.. image:: ../_images/scf_conj_rect_qpsk_scaled.svg + :align: center + :target: ../_images/scf_conj_rect_qpsk_scaled.svg + :alt: Konjugierte SCF des rechteckigen QPSK (FSM) mit Skalierung + +Die konjugierte SCF für QPSK, sowie für PSK und QAM höherer Ordnung, ist im Wesentlichen null/Rauschen. Das bedeutet, wir können die konjugierte SCF verwenden, um das Vorhandensein von BPSK zu erkennen, selbst wenn viele QPSK/QAM-Signale damit überlappen! + +Konjugierte SCF des Drei-Signal-Szenarios: + +.. image:: ../_images/scf_conj_multiple_signals.svg + :align: center + :target: ../_images/scf_conj_multiple_signals.svg + :alt: Konjugierte SCF von drei verschiedenen Signalen (FSM) + +Wir können die beiden BPSK-Signale sehen, aber das QPSK-Signal taucht nicht auf. + +******************************** +FFT-Akkumulationsmethode (FAM) +******************************** + +FSM und TSM funktionieren gut, besonders wenn du einen bestimmten Satz von zyklischen Frequenzen berechnen möchtest. Es gibt jedoch eine noch effizientere SCF-Implementierung, die als FFT-Akkumulationsmethode (FAM) bekannt ist, die inhärent den vollständigen Satz von zyklischen Frequenzen berechnet. Es gibt auch eine ähnliche Technik namens `Strip Spectral Correlation Analyzer (SSCA) `_. + +.. mermaid:: + + flowchart TD + A[Eingangssamples] --> B[In überlappende Fenster aufteilen] + B --> C[Hanning-Fenster anwenden] + C --> D[Erste FFT über jedes Fenster] + D --> E[Frequenzverschiebung] + E --> F[Zweite FFT] + F --> G[Betragsquadrat] + G --> H[SCF-Schätzung] + +.. code-block:: python + + N = 2**14 + x = samples[0:N] + Np = 512 # Anzahl der Eingangskanäle, sollte Potenz von 2 sein + L = Np//4 # Versatz zwischen Punkten in derselben Spalte + num_windows = (len(x) - Np) // L + 1 + Pe = int(np.floor(int(np.log(num_windows)/np.log(2)))) + P = 2**Pe + N = L*P + + # Kanalbildung + xs = np.zeros((num_windows, Np), dtype=complex) + for i in range(num_windows): + xs[i,:] = x[i*L:i*L+Np] + xs2 = xs[0:P,:] + + # Fensterung + xw = xs2 * np.tile(np.hanning(Np), (P,1)) + + # erste FFT + XF1 = np.fft.fftshift(np.fft.fft(xw)) + + # Frequenzverschiebung nach unten + f = np.arange(Np)/float(Np) - 0.5 + f = np.tile(f, (P, 1)) + t = np.arange(P)*L + t = t.reshape(-1,1) # als Spaltenvektor + t = np.tile(t, (1, Np)) + XD = XF1 * np.exp(-2j*np.pi*f*t) + + # Hauptberechnungen + SCF = np.zeros((2*N, Np)) + Mp = N//Np//2 + for k in range(Np): + for l in range(Np): + XF2 = np.fft.fftshift(np.fft.fft(XD[:,k]*np.conj(XD[:,l]))) # zweite FFT + i = (k + l) // 2 + a = int(((k - l) / Np + 1) * N) + SCF[a-Mp:a+Mp, i] = np.abs(XF2[(P//2-Mp):(P//2+Mp)])**2 + +.. image:: ../_images/scf_fam.svg + :align: center + :target: ../_images/scf_fam.svg + :alt: SCF mit der FFT-Akkumulationsmethode (FAM) + +Zoom in den interessanten Bereich um 0,2 Hz: + +.. image:: ../_images/scf_fam_zoomedin.svg + :align: center + :target: ../_images/scf_fam_zoomedin.svg + :alt: Vergrößerte SCF mit der FAM + +1D-Darstellung der SCF: + +.. image:: ../_images/scf_fam_1d.svg + :align: center + :target: ../_images/scf_fam_1d.svg + :alt: Zyklische Frequenz mit der FAM + +Bei der FAM wird eine enorme Anzahl von Pixeln erzeugt. Um die Anzahl der Pixel in der zyklischen Frequenzachse zu reduzieren, kann Max-Pooling verwendet werden (erfordert ggf. :code:`pip install scikit-image`): + +.. code-block:: python + + # Max-Pooling im zyklischen Bereich + import skimage.measure + print("Alte Form der SCF:", SCF.shape) + SCF = skimage.measure.block_reduce(SCF, block_size=(16, 1), func=np.max) + print("Neue Form der SCF:", SCF.shape) + +Externe Ressourcen zur FAM: + +* R.S. Roberts, W. A. Brown, and H. H. Loomis, Jr., "Computationally Efficient Algorithms for Cyclic Spectral Analysis," IEEE Signal Processing Magazine, April 1991, pp. 38-49. `Verfügbar hier `_ +* Da Costa, Evandro Luiz. Detection and identification of cyclostationary signals. Diss. Naval Postgraduate School, 1996. `Verfügbar hier `_ +* Chads Blog-Beitrag zur FAM: https://cyclostationary.blog/2018/06/01/csp-estimators-the-fft-accumulation-method/ + +******************************** +OFDM +******************************** + +Zyklostationarität ist bei OFDM-Signalen besonders stark aufgrund des zyklischen Präfix (CP) von OFDM, bei dem die letzten Samples jedes OFDM-Symbols kopiert und an den Anfang des OFDM-Symbols angehängt werden. Dies führt zu einer starken zyklischen Frequenz, die der OFDM-Symbollänge entspricht. + +Simulation eines OFDM-Signals mit CP, 64 Subträgern, 25% CP und QPSK-Modulation auf jedem Subträger. Wir interpolieren um den Faktor 2, sodass die OFDM-Symbollänge in Samples (64 + (64*0,25)) * 2 = 160 Samples beträgt. Das bedeutet, wir sollten Spikes bei ganzzahligen Vielfachen von 1/160 erhalten: + +.. code-block:: python + + from scipy.signal import resample + N = 200000 # Anzahl der zu simulierenden Samples + num_subcarriers = 64 + cp_len = num_subcarriers // 4 # Länge des zyklischen Präfix in Symbolen (25%) + print("CP-Länge in Samples", cp_len*2) # 2x Interpolation + print("OFDM-Symbollänge in Samples", (num_subcarriers+cp_len)*2) + num_symbols = int(np.floor(N/(num_subcarriers+cp_len))) // 2 + print("Anzahl der OFDM-Symbole:", num_symbols) + + qpsk_mapping = { + (0,0) : 1+1j, + (0,1) : 1-1j, + (1,0) : -1+1j, + (1,1) : -1-1j, + } + bits_per_symbol = 2 + + samples = np.empty(0, dtype=np.complex64) + for _ in range(num_symbols): + data = np.random.binomial(1, 0.5, num_subcarriers*bits_per_symbol) # 1en und 0en + data = data.reshape((num_subcarriers, bits_per_symbol)) # nach Subträgern gruppieren + symbol_freq = np.array([qpsk_mapping[tuple(b)] for b in data]) # OFDM beginnt im Frequenzbereich + symbol_time = np.fft.ifft(symbol_freq) + symbol_time = np.hstack([symbol_time[-cp_len:], symbol_time]) # CP anhängen + samples = np.concatenate((samples, symbol_time)) + + samples = resample(samples, len(samples)*2) # 2x interpolieren + samples = samples[:N] + + # Rauschen hinzufügen + SNR_dB = 5 + n = np.sqrt(np.var(samples) * 10**(-SNR_dB/10) / 2) * (np.random.randn(N) + 1j*np.random.randn(N)) + samples = samples + n + +Ergebnisse mit :code:`alphas = np.arange(0, 0.02, 1e-5)`: + +.. image:: ../_images/scf_freq_smoothing_ofdm_zoomed_in.svg + :align: center + :target: ../_images/scf_freq_smoothing_ofdm_zoomed_in.svg + :alt: SCF von OFDM (FSM) + +Drei Spikes sind klar erkennbar. + +Externe Ressourcen zu OFDM im CSP-Kontext: + +#. Sutton, Paul D., Keith E. Nolan, and Linda E. Doyle. "Cyclostationary signatures in practical cognitive radio applications." IEEE Journal on selected areas in Communications 26.1 (2008): 13-24. `Verfügbar hier `_ + +******************************************** +Signalerkennung mit bekannter zyklischer Frequenz +******************************************** + +In manchen Anwendungen möchtest du CSP verwenden, um ein bereits bekanntes Signal/Wellenform zu erkennen, wie z. B. Varianten von 802.11, LTE, 5G usw. Wenn du die zyklische Frequenz des Signals kennst und deine Abtastrate weißt, musst du wirklich nur ein einziges Alpha und ein einziges Tau berechnen. Demnächst wird ein Beispiel für diese Art von Problem mit einer HF-Aufzeichnung von WLAN folgen. + +****************** +Externe Ressourcen +****************** + +#. Antonio Napolitanos Lehrbuch `Cyclostationary Processes and Time Series: Theory, Applications, and Generalizations `_ +#. R.S. Roberts, W. A. Brown, and H. H. Loomis, Jr., "Computationally Efficient Algorithms for Cyclic Spectral Analysis," IEEE Signal Processing Magazine, April 1991, pp. 38-49. `Verfügbar hier `_ +#. Da Costa, Evandro Luiz. Detection and identification of cyclostationary signals. Diss. Naval Postgraduate School, 1996. `Verfügbar hier `_ +#. `Chad Spooners zyklostationäres Blog/Website `_ +#. Sutton, Paul D., Keith E. Nolan, and Linda E. Doyle. "Cyclostationary signatures in practical cognitive radio applications." IEEE Journal on selected areas in Communications 26.1 (2008): 13-24. `Verfügbar hier `_ diff --git a/content-de/detection.rst b/content-de/detection.rst new file mode 100644 index 00000000..320c0671 --- /dev/null +++ b/content-de/detection.rst @@ -0,0 +1,1104 @@ +.. _detection-chapter: + +##################################################### +Detektion mittels Korrelation +##################################################### + +.. raw:: html + + Co-authored by Sam Brown + +In diesem Kapitel lernen wir, wie wir das Vorhandensein von Signalen erkennen und ihre Zeitlage durch Kreuzkorrelation der empfangenen Samples mit einem bekannten Teil des Signals – beispielsweise einer Paket-Präambel – wiederherstellen können. Das führt natürlich zu einer einfachen Form der Klassifikation mithilfe einer Bank von Korrelatoren. Wir stellen die Grundideen der Signaldetektion vor und konzentrieren uns darauf, wie man entscheidet, ob ein bestimmtes Signal in einem verrauschten Umfeld vorhanden oder nicht vorhanden ist. Dabei behandeln wir Theorie und praktische Verfahren, um unter Unsicherheit gute Entscheidungen zu treffen. + +**************************************************** +Signaldetektion und Korrelator-Grundlagen +**************************************************** + +Signaldetektion ist die Aufgabe zu entscheiden, ob ein beobachteter Energiepeak ein bedeutungsvolles Signal oder nur Hintergrundrauschen ist. + +Die Herausforderung: In Systemen wie Radar oder Sonar ist Rauschen allgegenwärtig. Ist der Detektor zu empfindlich, entstehen Fehlalarme. Ist er nicht empfindlich genug, werden echte Ziele übersehen. + +Die Lösung beginnt mit dem Neyman-Pearson-Detektor, der einen mathematischen Mittelweg bietet, indem er die Wahrscheinlichkeit, ein Signal zu finden, maximiert und gleichzeitig Fehlalarme unter einem definierten Grenzwert hält. CFAR-Detektoren bauen auf dieser Idee auf, indem sie sich an Änderungen des Rauschpegels anpassen. Sie sind besonders nützlich, wenn die Rauschstatistik nicht stationär ist – d. h. wenn Rauschboden und Rauschverteilung durch Interferenz oder sich verändernde Kanalbedingungen variieren. Ziel ist es, die Detektionsschwelle automatisch anzupassen, wenn das Hintergrundrauschen schwankt, während eine gewählte Fehlalarmrate aufrechterhalten wird. Dazu muss der Rauschboden über die Zeit geschätzt werden. + +Sobald ein System weiß, dass etwas vorhanden ist, muss es noch herausfinden, wo die Daten genau beginnen. Digitale Pakete in LTE, 5G oder WLAN beginnen mit einer Präambel – einem bekannten, sich wiederholenden digitalen Muster. Ein Präambel-Korrelator funktioniert wie ein Schloss-Schlüssel-Mechanismus: Der Schlüssel ist eine dem Empfänger bekannte Symbolfolge, die eindeutig für das empfangene Signal ist. Indem eine Kopie der Präambel über das eingehende Signal geschoben und bei jeder Verzögerung ein Skalarprodukt gebildet wird, misst der Empfänger, wie ähnlich die Vorlage den empfangenen Samples an jeder Position ist. Wenn beide gut übereinstimmen, erscheint ein scharfer Spike und teilt dem Empfänger mit, wo er die Daten zu lesen beginnen soll. Fortgeschrittenere Versionen berücksichtigen auch Frequenzversätze, die durch kleine Abstimmunterschiede zwischen Handy und Basisstation oder durch Doppler-Verschiebungen entstehen. + +Wenn ein bekanntes Signal – oder eine Präambel – über einen nur durch additives weißes Gaußsches Rauschen (AWGN) gestörten Kanal übertragen wird, besteht die Aufgabe darin zu entscheiden, ob das Signal vorhanden ist. Dies ist das einfachste und grundlegendste Detektionsproblem. + +Die Kreuzkorrelationsfunktion +############################### + +Ein Korrelator ist in seiner einfachsten Form eine Kreuzkorrelation zwischen einem empfangenen Signal und einer Vorlage. Kreuzkorrelation ist lediglich ein Skalarprodukt zwischen zwei Vektoren, während einer der Vektoren über den anderen gleitet. Falls du Faltung kennst: Es ist fast dieselbe Operation, außer dass der zweite Vektor nicht gespiegelt wird – also etwas einfacher. Bei komplexen Signalen, mit denen wir es zu tun haben werden, muss einer der Eingänge zusätzlich konjugiert werden. In Python lässt sich das wie folgt implementieren: + +.. code-block:: python + + def correlate(a, v): + n = len(a) + m = len(v) + result = [] + for i in range(n - m + 1): + s = 0 + for j in range(m): + s += a[i + j] * v[j].conjugate() + result.append(s) + return result + + # Beispielverwendung: + a = [1+2j, 2+1j, 3+0j, 4-1j, 5-2j] + v = [0+1j, 1+0j, 0.5-0.5j] + correlate(a, v) + +Beachte, wie wir :code:`a` verschieben und dabei :code:`v` konjugieren, und wie die Schleife mit :code:`j` und :code:`s` im Grunde nur ein Vektor-Skalarprodukt ist. Glücklicherweise müssen wir Kreuzkorrelation nicht von Grund auf implementieren – in Python können wir NumPys :code:`correlate`-Funktion verwenden. Es gibt auch eine SciPy-Version, mit der man experimentieren kann. + +Python-Beispiel einer Kreuzkorrelation +######################################################## + +Um ein grundlegendes Python-Beispiel eines Korrelatorszu erstellen, benötigen wir zunächst ein Beispielsignal mit einer bekannten Präambel, die in Rauschen eingebettet ist. Wir verwenden eine Zadoff-Chu-Sequenz als Präambel, wegen ihrer hervorragenden Auto-Korrelationseigenschaften und ihrer verbreiteten Nutzung in Kommunikationssystemen. Wir verwenden keinen weiteren Datenteil des Signals, obwohl in den meisten Systemen nach der bekannten Präambel unbekannte Daten folgen würden. Eine Zadoff-Chu-Sequenz kann wie folgt erzeugt werden: + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + N = 839 # Länge der Zadoff-Chu-Sequenz + u = 25 # Wurzel der ZC-Sequenz + t = np.arange(N) + zadoff_chu = np.exp(-1j * np.pi * u * t * (t + 1) / N) + +Die resultierende Sequenz ist selbst ein Signal. Die IQ-Samples in :code:`zadoff_chu` repräsentieren ein Basisband-Komplexsignal, ähnlich vielen Signalen, die wir bereits in diesem Lehrbuch gesehen haben, aber es repräsentiert keine Bits. Wir können ein realistisches Szenario nachbilden, indem wir das Zadoff-Chu-Signal an einem zufälligen Offset in einen längeren AWGN-Strom einbetten: + +.. code-block:: python + + signal_length = 10 * N # Gesamtlänge des simulierten Signals + offset = np.random.randint(N, signal_length - N) + print(f"True offset: {offset}") + snr_db = -15 + noise_power = 1 / (2 * (10**(snr_db / 10))) + signal = np.sqrt(noise_power/2) * (np.random.randn(signal_length) + 1j * np.random.randn(signal_length)) + signal[offset:offset+N] += zadoff_chu # ZC-Signal an zufälligem Offset einfügen + +Beachte, dass wir ein sehr niedriges SNR verwenden. Es ist so niedrig, dass du die Zadoff-Chu-Sequenz im Zeitbereichssignal überhaupt nicht erkennen kannst. Unsere Sequenz ist 839 Samples lang, von insgesamt etwa 8.000 simulierten Samples, und ist so tief im Rauschen vergraben, dass man nicht einmal einen leichten Anstieg der Signalamplitude erkennen kann. + +.. image:: ../_images/detection_basic_1.svg + :align: center + :target: ../_images/detection_basic_1.svg + :alt: Time Domain Signal with Zadoff-Chu Sequence + +Jetzt können wir den Korrelator implementieren, indem wir das empfangene Signal gegen unsere bekannte Zadoff-Chu-Sequenz mit :code:`np.correlate()` kreuzkorrelieren. Dies setzt voraus, dass der Empfänger die genaue verwendete Präambel kennt. Im obigen Code wurde :code:`zadoff_chu` ursprünglich zur Simulation des Signals erstellt, repräsentiert nun aber auch die vom Empfänger verwendete Präambel-Vorlage. Der Korrelator lässt sich in einer einzigen Python-Zeile implementieren: + +.. code-block:: python + + correlation = np.correlate(signal, zadoff_chu, mode='valid') + +Der :code:`valid`-Modus wird gleich behandelt. Wir normalisieren die Ausgabe auch durch die Länge der Sequenz und berechnen das Betragsquadrat, um die Leistung zu erhalten; auch der bloße Betrag würde funktionieren. Das Wichtigste ist die :code:`np.correlate()`-Operation selbst. + +.. code-block:: python + + correlation = np.abs(correlation / N)**2 # durch N normalisieren und Betragsquadrat nehmen + +Unten plotten wir das Betragsquadrat und markieren die tatsächliche Startposition der Sequenz, um zu sehen, ob der Korrelator sie finden konnte: + +.. image:: ../_images/detection_basic_2.svg + :align: center + :target: ../_images/detection_basic_2.svg + :alt: Correlator Output + +Obwohl das SNR sehr niedrig ist, zeigt die Korrelatorausgabe einen deutlichen Spike genau dort, wo die Zadoff-Chu-Sequenz platziert wurde. Dieser Spike markiert den Beginn der Sequenz, sodass die 839 Samples ab dort die Präambel enthalten. Das ist die Kraft der korrelationsbasierten Detektion kombiniert mit einer langen Präambel. Bisher haben wir noch keine Schwelle festgelegt, um zu entscheiden, ob der Spike unser interessierendes Signal oder nur Rauschen ist – wir betrachten die Ausgabe nur visuell. Der Rest des Kapitels geht darum, diese Entscheidung zu automatisieren, insbesondere wenn Rauschboden und Hintergrundinterferenz sich ändern. + +Gültig, Gleich, Voll-Modi +####################################### + +Du hast vielleicht bemerkt, dass sowohl :code:`np.correlate()` als auch :code:`np.convolve()` drei Modi unterstützen: :code:`valid`, :code:`same` und :code:`full`. Diese Modi bestimmen die Länge des Ausgabe-Arrays relativ zu den Eingabe-Arrays. In unserem Fall haben wir :code:`valid` verwendet, was bedeutet, dass die Ausgabe nur Punkte enthält, bei denen die beiden Eingabe-Arrays vollständig überlappen. Dies ergibt eine Ausgabelänge von :code:`len(signal) - len(zadoff_chu) + 1`. Hätten wir :code:`same` verwendet, wäre die Ausgabe gleich lang wie das längere Eingabesignal. Bei :code:`full` erhält man die vollständige diskrete lineare Faltung mit einer etwas längeren Array-Länge von :code:`max(M, N) - min(M, N) + 1`, wobei :code:`M` und :code:`N` die Eingabelängen sind. In der HF-Signalverarbeitung wird Faltung oft zur Anwendung eines FIR-Filters verwendet, wobei Ein- und Ausgabe gleicher Länge praktisch ist, daher ist :code:`same` dort häufig. Für die korrelationsbasierte Detektion wollen wir jedoch üblicherweise :code:`valid`, da wir uns nur für Punkte interessieren, bei denen die Präambel vollständig mit dem empfangenen Signal überlappt, insbesondere wenn wir annehmen, dass das Signal nach dem Beginn des Empfangs startet. + +Der Neyman-Pearson-Detektor +############################ + +Der Goldstandard für die Wahl einer Schwelle für unsere Korrelatorausgabe ist der Neyman-Pearson-Detektor. Diese Theorie hilft uns, unter einer bestimmten Einschränkung eine optimale Entscheidung zu treffen: Sie findet die Schwelle, die die Detektionswahrscheinlichkeit :math:`P_{D}` für eine feste und akzeptable Fehlalarmwahrscheinlichkeit :math:`P_{FA}` maximiert. Einfach gesagt: Du entscheidest, wie viele Fehldetektionen du tolerieren kannst – z. B. ein Fehlalarm pro Stunde –, und der Neyman-Pearson-Detektor gibt dir die beste Schwelle, um möglichst viele echte Signale zu finden. Zur Erkennung einer bekannten Präambel in AWGN verwendet er einen unkomplizierten Ansatz: Er berechnet die Korrelation zwischen dem empfangenen Signal und dem bekannten Präambelmuster. Überschreitet dieser Wert eine vorher festgelegte Schwelle :math:`\tau`, wird das Signal als vorhanden deklariert; andernfalls wird angenommen, dass nur Rauschen vorhanden ist. + +Die Leistung dieses Detektors – gemessen durch :math:`P_{D}` und :math:`P_{FA}` – hängt von der Schwelle :math:`\tau`, dem SNR und der Präambellänge :math:`L` ab. Die Fehlalarmwahrscheinlichkeit hängt von der Schwelle und der Rauschvarianz :math:`\sigma_n^2` ab: + +:math:`P_{FA} = Q\left(\frac{\tau}{\sigma_n}\right)` + +Die Detektionswahrscheinlichkeit ist eine Funktion von Schwelle, Rauschvarianz und der Energie der Präambel (:math:`E_s = L \cdot S`, wobei :math:`S` die mittlere Symbolleistung ist): + +:math:`P_{D} = Q\left(\frac{\tau - \sqrt{E_s}}{\sigma_n}\right) = Q\left(\frac{\tau - \sqrt{L \cdot S}}{\sigma_n}\right)` + +Dabei ist :math:`Q(x)` die Q-Funktion (die Schwanzwahrscheinlichkeit der Standardnormalverteilung), die die Wahrscheinlichkeit angibt, dass eine standardnormalverteilte Zufallsvariable den Wert :math:`x` überschreitet. + +Leistungsanalyse: ROC-Kurven und Pd-vs.-SNR-Kurven +################################################################# + +Um die Leistung eines Korrelatordetektors im Rauschen zu quantifizieren, verwenden Ingenieure zwei hauptsächliche Visualisierungen: die Receiver Operating Characteristic (ROC)-Kurve und die Detektionswahrscheinlichkeit (:math:`P_{d}`) vs. SNR-Kurve. + +Die ROC-Kurve trägt die Detektionswahrscheinlichkeit (:math:`P_{D}`) gegen die Fehlalarmwahrscheinlichkeit (:math:`P_{FA}`) bei festem SNR auf. Durch Anpassung der Detektionsschwelle am Korrelatorausgang wählt man einen Punkt auf dieser Kurve – es ist also ein grundlegender Kompromiss. Eine niedrigere Schwelle erhöht :math:`P_{D}`, indem mehr echte Signale gefunden werden, erhöht aber auch :math:`P_{FA}`, indem häufiger auf Rauschen angeschlagen wird. Die Wölbung der Kurve in Richtung der oberen linken Ecke zeigt einen besseren Detektor an. Ein perfekter Detektor erreicht die obere linke Ecke mit 100% :math:`P_{D}` und 0% :math:`P_{FA}`; eine Diagonale repräsentiert zufälliges Raten. + +.. image:: ../_images/detection_pd_vs_snr.svg + :align: center + :target: ../_images/detection_pd_vs_snr.svg + :alt: Pd vs SNR Curve and ROC curve + +Zusammen zeigen die Gleichungen und die Intuition, dass die Präambellänge :math:`L` ein kritischer Entwurfsparameter ist, weil sie direkt den Verarbeitungsgewinn und damit die Detektionsleistung steuert. Bei fester Schwelle und festem SNR steigt :math:`P_{D}` mit :math:`L`. Eine längere Präambel erlaubt es, mehr Signalenergie zu sammeln, was es einfacher macht, das Signal vom Hintergrundrauschen zu unterscheiden. Diese Verbesserung wird Verarbeitungsgewinn genannt und üblicherweise in dB als :math:`10\log_{10}(L)` gemessen. Er ist entscheidend für die Erkennung schwacher Signale, die sonst übersehen würden. Durch Integration der Energie über mehr Samples können wir Signale aus dem Rauschen herausziehen, selbst wenn sie unterhalb des Rauschbodens liegen. GPS ist ein gutes Beispiel aus der Praxis, da der Empfänger sehr schwache Signale mit einer bekannten Codestruktur empfangen muss. + +**************************************************** +Beispiel: GPS-Signale unterhalb des Rauschbodens detektieren +**************************************************** + +Kurzeinführung in GPS-Signale +############################### + +Stand März 2026 gibt es 31 operationelle Satelliten in der U.S.-GPS-Konstellation, die auf mittlerer Erdumlaufbahn (MEO) fliegen und die Erde zweimal täglich umkreisen. Alle Satelliten senden ein Signal bei 1575,42 MHz, genannt L1, und verwenden alle dieselbe Trägerfrequenz. Wenn das Signal die Erdoberfläche erreicht, ist es extrem schwach und liegt deutlich unterhalb des Rauschbodens. Orthogonalität zwischen den Satelliten wird dadurch erreicht, dass jedem Satelliten ein eindeutiger 1023-Chip-Pseudozufallsrausch-(PRN-)Code, der sogenannte C/A-Code, zugewiesen wird – deshalb wird das Signal manchmal als L1 C/A bezeichnet. Diese C/A-Codes verwenden Gold-Codes und sind so ausgelegt, dass je zwei davon nahezu orthogonal sind: Wenn man die Codes zweier Satelliten miteinander korreliert, erhält man nahezu null. Der C/A-Code läuft mit 1,023 Millionen Chips pro Sekunde und ist nur 1023 Chips lang, wiederholt sich also alle 1 ms. Zusätzlich moduliert jeder Satellit langsam Navigationsdaten wie Orbitposition und Uhrenkorrekturen mit nur 50 bit/s, sodass ein Datenbit 20 vollständige Codewiederholungen überspannt. Diese Verwendung eines verschiedenen Codes pro Sender ist als CDMA (Code Division Multiple Access) bekannt – dasselbe Prinzip wie bei 3G-Mobiltelefonen. + +Auf der Empfängerseite bedeutet das Finden eines der 31 Satelliten, eine lokale Kopie der PRN-Sequenz dieses Satelliten zu erzeugen und einen Korrelator zu verwenden, um den Beginn der Codeperiode zu finden. Im GPS kann dieser Beginn wie der Anfang eines Pakets oder Rahmens behandelt werden, obwohl das System kontinuierlich sendet. Der genaue Korrelationspeak wird auch verwendet, um zu schätzen, wie weit das Signal gereist ist, bevor es den Empfänger erreicht hat; sobald das für vier oder mehr Satelliten bekannt ist, kann der Empfänger seinen Standort auf der Erde trilaterieren. Da sich die Satelliten mit etwa 4 km/s relativ zu dir bewegen, muss der Empfänger auch über ein Raster möglicher Frequenzversätze suchen, um den besten Korrelationspeak zu finden – ein 2D-Suchproblem. Der maximale Doppler beträgt etwa ±20 kHz (:code:`4e3 / 3e8 * 1.575e9`). Dieser Prozess wiederholt sich alle 1 ms, obwohl der Empfänger Verzögerung und Doppler verfolgt und keine vollständige Suche jedes Mal durchführen muss. Die anfängliche Suche nach jedem Satelliten wird als Akquisition bezeichnet, der anschließende Prozess als Tracking. Akquisition ist der rechenintensivere Teil und kann Minuten dauern, wenn der Empfänger ohne Vorinformationen über sichtbare Satelliten, Doppler-Verschiebungen oder seinen eigenen Standort startet. + +Korrelationsansatz +############################### + +Wir kreuzkorrelieren das eingehende Signal – in diesem Fall eine L1-Aufzeichnung – gegen eine lokal erzeugte Kopie des Codes jedes Satelliten. Ein großer Korrelationspeak bedeutet, dass der Satellit sichtbar ist und gibt uns den Beginn der 1-ms-Codeperiode. Um auch über Frequenz zu suchen, verwenden wir eine FFT-basierte Korrelation im Frequenzbereich, die es uns erlaubt, mehrere Frequenzversätze effizient zu testen, indem wir die FFT-Bins der lokalen Codereplika verschieben. Schließlich akkumulieren wir das Betragsquadrat der Korrelation über mehrere 1-ms-Blöcke, um das SNR zu verbessern. Das nennt man nicht-kohärente Integration, und sie hilft, GPS-Signale zu detektieren, die unterhalb des Rauschbodens empfangen werden. Wir schwellen das Ergebnis gegen die Korrelationsausgabe dividiert durch die mittlere Korrelationsleistung über alle Verzögerungen, was das Ergebnis normiert. + +Beispielaufzeichnung +############################### + +Wir verwenden eine Beispiel-GPS-Aufzeichnung von Daniel Estévez, die du `hier herunterladen `_ kannst. Es handelt sich um eine Complex-Float32-Datei, abgetastet mit 4 MHz und zentriert bei 1575,42 MHz. + +Unten ist das Spektrogramm der Aufzeichnung. Es gibt nicht viel zu sehen, und die vertikale Linie ist nicht das eigentliche GPS-Signal – sie ist wahrscheinlich schmalbandige Interferenz. Die eigentlichen GPS-L1-Signale verwenden eine Chiprate von 1,023 MHz mit einem sehr langsamen Datensignal oben drauf, sodass das Signal etwa 2 MHz breit wird, was wir im Spektrogramm einfach nicht sehen. Das ist ein gutes Beispiel dafür, wie GPS-Signale weit unterhalb des Rauschbodens empfangen werden, und warum wir korrelationsbasierte Detektion benötigen, um sie zu finden. + +.. image:: ../_images/detection_gps_spectrogram.svg + :align: center + :target: ../_images/detection_gps_spectrogram.svg + :alt: Spectrogram of GPS L1 Recording + +Für Interessierte: Diese Aufzeichnung ist ein kleiner Ausschnitt einer viel größeren Datei auf `IQEngine `_ unter :code:`estevez/GPS and other GNSS`; suche nach der Aufzeichnung :code:`GPS-L1-2022-03-27`. Auf IQEngine ist es eine Int16-Datei im SigMF-Format. + +Python-Beispiel +##################### + +Passe :code:`filename` an den Speicherort an, wohin du die IQ-Datei heruntergeladen hast. Beachte, dass :code:`num_integrations` bestimmt, wie viel von der Aufzeichnung eingelesen und verarbeitet wird; die Gesamtdauer ist einfach diese Zahl mal 1 ms, maximal 10 für die kürzere Aufzeichnung. + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + + filename = "GPS_L1_recording_10ms_4MHz_cf32.iq" + sample_rate = 4e6 + chip_rate = 1023000 # Chips/s (Teil der GPS-Spezifikation) + num_chips = 1023 # Chips pro C/A-Codeperiode + samples_per_code = int(round(sample_rate / chip_rate * num_chips)) # Genaue Sample-Anzahl in einer 1-ms-Codeperiode bei 4 MHz + doppler_min_hz = -5e3 # GPS-Doppler ≈ ±4 kHz für stationären Empfänger + doppler_max_hz = 5e3 + doppler_step_hz = 500 # gut genug für eine grobe Suche + num_integrations = 10 # nicht-kohärente Leistungsintegrationen (also 10 ms gesamt), bestimmt wie viel der IQ-Aufzeichnung eingelesen wird! + detection_thresh_dB = 14.0 # Peak-to-Mean-Ratio (PMR)-Schwelle in dB für eine Detektion, GPS C/A-Signale haben typischerweise 14-20 dB PMR bei 10 ms Integration + gps_svs = list(range(1, 33)) # 1–32 + + ##### C/A-Code-Erzeugung ##### + # Der GPS C/A-Code ist ein Gold-Code, gebildet durch XOR zweier 10-stufiger + # maximallängen Schieberegister (G1 und G2). G2 wird effektiv um eine + # satellitenspezifische Anzahl von Chips vor dem XOR verzögert. + # Referenz: IS-GPS-200, Tabelle 3-Ia + G2_DELAY = [ # G2-Phasenverzögerung (Chips) für gps_svs 1–32 + 5, 6, 7, 8, 17, 18, 139, 140, # 1– 8 + 141, 251, 252, 254, 255, 256, 257, 258, # 9–16 + 469, 470, 471, 472, 473, 474, 509, 512, # 17–24 + 513, 514, 515, 516, 859, 860, 861, 862, # 25–32 + ] + + """G1 LFSR: Polynom x^10 + x^3 + 1, Alles-Einsen-Init, Ausgabe bei Stufe 10.""" + reg = np.ones(10, dtype=np.int8) + G1 = np.empty(num_chips, dtype=np.int8) + for i in range(num_chips): + G1[i] = reg[9] + fb = reg[2] ^ reg[9] # Stufen 3 und 10 (0-indiziert: 2 und 9) + reg = np.roll(reg, 1) + reg[0] = fb + + """G2 LFSR: Polynom x^10+x^9+x^8+x^6+x^3+x^2+1, Alles-Einsen-Init.""" + reg = np.ones(10, dtype=np.int8) + G2 = np.empty(num_chips, dtype=np.int8) + for i in range(num_chips): + G2[i] = reg[9] + fb = reg[1]^reg[2]^reg[5]^reg[7]^reg[8]^reg[9] # Abgriffe 2,3,6,8,9,10 + reg = np.roll(reg, 1) + reg[0] = fb + + # 1023-Chip C/A PRN-Code für SV sv (1-32) als float32, 1 und -1, also BPSK + def make_prn(sv: int) -> np.ndarray: + g2_delayed = np.roll(G2, G2_DELAY[sv - 1]) + bits = G1 ^ g2_delayed # {0, 1} + return (1 - 2 * bits).astype(np.float32) # BPSK: {+1, −1} + + def upsample_prn(sv: int) -> np.ndarray: + """Nächster-Nachbar-Upsampling des 1023-Chip C/A-Codes → samples_per_code Samples.""" + code = make_prn(sv) + idx = (np.arange(samples_per_code) * num_chips / samples_per_code).astype(int) + return code[idx] + + # Vorberechnete Template-Signale – konjugierte FFTs aller upgesampleten PRN-Codes + template_signals = {sv: np.conj(np.fft.fft(upsample_prn(sv))) for sv in gps_svs} + + # IQ-Datei einlesen + n_needed = samples_per_code * num_integrations + iq = np.fromfile(filename, dtype=np.complex64, count=n_needed) + # Für die vollständige Version von IQEngine stattdessen folgendes verwenden: + #iq = np.fromfile(filename, dtype=np.int16, count=n_needed * 2) + #iq = (iq[0::2] + 1j * iq[1::2]).astype(np.complex64) + + # Jeden Satelliten über Doppler und Codephase durchsuchen + results = [] + detected = [] + print(f" {'SV':>3} {'Doppler (Hz)':>13} {'Phase (chips)':>14}" + f" {'Phase (samp)':>13} {'Delay (µs)':>11} {'PMR (dB)':>9}") + doppler_bins = np.arange(doppler_min_hz, doppler_max_hz + doppler_step_hz, doppler_step_hz) + for sv in gps_svs: + corr_map = np.zeros((len(doppler_bins), samples_per_code)) + n_total = samples_per_code * num_integrations + for di, f_d in enumerate(doppler_bins): + t = np.arange(n_total) / sample_rate # Zeitvektor + mixed = iq[:n_total] * np.exp(-2j*np.pi*float(f_d)*t) # Frequenzverschiebung anwenden + + # Betragsquadrat der Korrelation nicht-kohärent akkumulieren + for k in range(num_integrations): + blk = mixed[k * samples_per_code:(k + 1) * samples_per_code] + sig_fft = np.fft.fft(blk) + corr = np.fft.ifft(sig_fft * template_signals[sv]) # Korrelation im Frequenzbereich + corr_map[di] += np.abs(corr)**2 + + # Durch Mittelwert normalisieren und in dB umrechnen + peak_val = float(np.max(corr_map)) + mean_val = float(np.mean(corr_map)) + pmr_db = 10.0 * np.log10(peak_val / mean_val) + + peak_idx = np.unravel_index(np.argmax(corr_map), corr_map.shape) + best_doppler_hz = float(doppler_bins[peak_idx[0]]) + best_phase_samp = int(peak_idx[1]) + best_phase_chips = best_phase_samp * num_chips / samples_per_code + + r = { + "sv": sv, + "detected": pmr_db >= detection_thresh_dB, + "doppler_hz": best_doppler_hz, + "code_phase_samp": best_phase_samp, # Sample-Offset = "Paketbeginn" + "code_phase_chip": best_phase_chips, + "pmr_db": pmr_db, + "corr_map": corr_map, + "doppler_bins": doppler_bins, + } + results.append(r) + + # Ergebniszeile ausgeben + delay_us = r['code_phase_samp'] / sample_rate * 1e6 + flag = " ← DETECTED" if r['detected'] else "" + print(f" {sv:>3} {r['doppler_hz']:>+13.0f} {r['code_phase_chip']:>14.2f}" + f" {r['code_phase_samp']:>13d} {delay_us:>11.3f} {r['pmr_db']:>9.1f}{flag}") + +Dies sollte folgende Ausgabe erzeugen: + +.. code-block:: + + SV Doppler (Hz) Phase (chips) Phase (samp) Delay (µs) PMR (dB) + 1 -3000 757.79 2963 740.750 5.6 + 2 +1500 264.19 1033 258.250 9.1 + 3 -2000 316.62 1238 309.500 5.8 + 4 +5000 577.48 2258 564.500 5.0 + 5 +1000 64.96 254 63.500 5.3 + 6 +1500 511.76 2001 500.250 5.0 + 7 -4000 763.41 2985 746.250 5.0 + 8 +3500 961.62 3760 940.000 5.4 + 9 +3500 118.67 464 116.000 4.9 + 10 +0 890.52 3482 870.500 5.4 + 11 +2500 837.33 3274 818.500 14.6 ← DETECTED + 12 -500 871.60 3408 852.000 16.4 ← DETECTED + 13 +1000 137.85 539 134.750 5.9 + 14 +2500 287.72 1125 281.250 5.0 + 15 -5000 908.68 3553 888.250 5.3 + 16 +1500 292.58 1144 286.000 5.9 + 17 +500 994.61 3889 972.250 5.3 + 18 +4500 1005.61 3932 983.000 5.4 + 19 +5000 588.48 2301 575.250 5.0 + 20 +0 768.53 3005 751.250 5.4 + 21 -3000 749.60 2931 732.750 5.0 + 22 +2500 558.05 2182 545.500 14.4 ← DETECTED + 23 -5000 390.02 1525 381.250 5.3 + 24 +2500 955.48 3736 934.000 5.9 + 25 +1500 597.94 2338 584.500 15.5 ← DETECTED + 26 -1500 239.89 938 234.500 6.2 + 27 -2500 488.74 1911 477.750 4.7 + 28 +3000 858.81 3358 839.500 5.2 + 29 -4000 998.70 3905 976.250 5.2 + 30 -2000 937.58 3666 916.500 5.2 + 31 +5000 463.42 1812 453.000 15.9 ← DETECTED + 32 +1000 342.45 1339 334.750 16.2 ← DETECTED + +Wie man sieht, wurden 6 Satelliten detektiert. Obwohl unsere Schwelle 14,0 war, kann man aus dieser Liste leicht erkennen, dass die meisten anderen Satelliten nicht sichtbar waren, mit Ausnahme von SV-2, der wahrscheinlich sichtbar war, aber die Schwelle knapp nicht erreicht hat. Wer das verifizieren möchte: Die Aufzeichnung wurde am 27.03.2022T11:32:04 irgendwo in Spanien aufgenommen. + +Darstellung +########### + +Versuchen wir, die Ergebnisse für Satellit 11 – den ersten detektierten – darzustellen. Das erste Diagramm ist die 2D-Korrelationskarte über Doppler und Zeit/Verzögerung, das zweite ist ein Schnitt der Korrelationskarte beim besten Doppler-Bin, der die Korrelationsleistung über die Zeit zeigt, wie wir es im vorherigen Abschnitt gesehen haben. + +.. code-block:: python + + # Darstellung + sv = 11 # wir haben 11, 12, 22, 25, 31, 32 detektiert – versuche auch einen nicht detektierten! + r = results[sv - 1] # Dict mit Ergebnissen für diesen SV ausgeben + cmap = r['corr_map'] # 2D-Array der Korrelationsleistung vs. Doppler und Codephase + d_bins = r['doppler_bins'] # zugehörige Doppler-Bins + chips_axis = np.arange(samples_per_code) * num_chips / samples_per_code + + # 2D Doppler × Codephase-Karte + plt.figure(0, figsize=(10, 6)) + im = plt.pcolormesh(chips_axis, d_bins, cmap, shading='auto', cmap='viridis') + plt.xlabel("Code Phase (chips)") + plt.ylabel("Doppler (Hz)") + plt.title(f"SV {sv} — 2-D Acquisition Map (PMR = {r['pmr_db']:.1f} dB)") + plt.legend(fontsize=8, loc='upper right') + plt.colorbar(im, label="Correlation Power") + + # Codephase-Schnitt beim besten Doppler + best_di = int(np.argmin(np.abs(d_bins - r['doppler_hz']))) + plt.figure(1, figsize=(10, 6)) + plt.plot(chips_axis, cmap[best_di], lw=1, color='steelblue') + plt.xlabel("Code Phase (chips)") + plt.ylabel("Correlation Power") + plt.title(f"SV {sv} — Code-Phase Slice (Doppler = {r['doppler_hz']:+.0f} Hz)") + plt.legend(fontsize=8) + plt.grid(True, alpha=0.3) + + plt.show() + +.. image:: ../_images/detection_gps_2d_map.png + :align: center + :width: 700px + :alt: 2-D Acquisition Map + +.. image:: ../_images/detection_gps_code_phase_slice.svg + :align: center + :target: ../_images/detection_gps_code_phase_slice.svg + :alt: Code-Phase Slice + +Auf die Trilateration gehen wir hier nicht ein, aber die genaue Position dieses Spikes ist letztendlich das, was dem GPS-Empfänger ermöglicht, die Entfernung zum Satelliten zu bestimmen. Kombiniert mit denselben Informationen von vier oder mehr Satelliten kann er seinen Standort auf der Erde ermitteln. + +**************************************************** +CFAR-Detektoren: In dynamischen Umgebungen bestehen +**************************************************** + +Während der Neyman-Pearson-Detektor bei festem Rauschpegel optimal ist, sind reale Bedingungen selten so stabil. In einer dynamischen Umgebung – wie einem Radar, das ein Flugzeug durch Regen verfolgt, oder einem Funkempfänger in einer belebten Stadt – schwanken Hintergrundrauschen und Interferenzpegel ständig. Hier wird der CFAR-Detektor (Constant False Alarm Rate) unverzichtbar. + +CFAR-Detektoren sind die Arbeitspferde von Systemen, in denen ein unvorhersehbarer Hintergrund eine feste Schwelle unmöglich macht: + +- Radar und Sonar werden zur Erkennung von Zielen (Flugzeuge, U-Boote) gegen „Clutter" verwendet – Reflexionen von Wellen, Regen oder Land, die sich ändern, wenn sich der Sensor bewegt. +- Drahtlose Kommunikation, z. B. Cognitive Radio und LTE/5G-Systeme, nutzen CFAR, um verfügbares Spektrum zu identifizieren oder eingehende Pakete zu detektieren, wenn Interferenz von anderen Geräten stoßweise und unvorhersehbar ist. +- Medizinische Bildgebung setzt CFAR in automatisierter Ultraschall- oder MRT-Analyse ein, um echte Gewebemerkmale von variierenden elektronischen Rauschpegeln zu unterscheiden. + +Das „C" in CFAR steht für Constant, weil das Ziel darin besteht, die Fehlalarmwahrscheinlichkeit (:math:`P_{FA}`) auf einem stabilen, vorhersehbaren Niveau zu halten. + +Um eine Schwelle zu setzen, muss man ein statistisches Modell für das Rauschen annehmen – die Rauschverteilung. Bei einfachem AWGN folgt Rauschen einer Gauß-Verteilung. Bei Radar-Clutter könnte es jedoch einer Rayleigh- oder Weibull-Verteilung folgen. Wenn dein Modell falsch ist, „driftet" dein :math:`P_{FA}` – das System wird entweder blind oder von Fehlauslösungen überwältigt. + +Statt eines fest codierten Wertes schätzt ein CFAR-Detektor die Rauschleistung in der lokalen „Nachbarschaft" des Signals und multipliziert diese Schätzung mit einem Skalierungsfaktor (:math:`T`), der von deinem gewünschten :math:`P_{FA}` abgeleitet wird. Dadurch wird sichergestellt, dass die Schwelle mit dem Rauschboden steigt, wenn dieser ansteigt. + +Pro-Lag- vs. systemweite Fehlalarmraten +#################################################### + +Dies ist eine wichtige Unterscheidung, die Anfänger häufig übersehen. Wenn du nach einer Präambel suchst, führst du üblicherweise eine gleitende Korrelation durch und prüfst die Schwelle bei tausenden verschiedenen Zeitversätzen (oder „Lags") pro Sekunde. + +Pro-Lag-:math:`P_{FA}`: Das ist die Wahrscheinlichkeit, dass eine einzelne spezifische Korrelationsprüfung einen Fehlalarm ergibt. Wenn du deine Mathematik für ein :math:`P_{FA}` von 0,001 einstellst, hat jeder einzelne Lag eine 1-zu-1.000-Chance, ein „Geistersignal" zu sein. + +Systemweites (globales) :math:`P_{FA}`: Das ist die Wahrscheinlichkeit, dass das System mindestens einen Fehlalarm während eines gesamten Suchfensters auslöst (z. B. über 2.048 Lags). + +Mathematisch gesehen gilt: Wenn dein Pro-Lag-:math:`P_{FA}` :math:`p` ist, beträgt die Wahrscheinlichkeit mindestens eines Fehlalarms über :math:`N` Lags ungefähr :math:`1-(1-p)^{N}`. + +Folglich: Wenn du 1.000 Lags und ein Pro-Lag-:math:`P_{FA}` von 0,001 hast, wird dein System fast 63% der Zeit einen Fehlalarm melden! Um die systemweite Fehlalarmrate niedrig zu halten, muss das Pro-Lag-:math:`P_{FA}` auf einen extrem kleinen Wert gesetzt werden. + +Python-Beispiel +############### + +Um mit unserem eigenen CFAR-Detektor zu spielen, simulieren wir zunächst ein Szenario mit sich wiederholenden QPSK-Paketen mit einer bekannten Präambel über einen Kanal mit zeitlich variierendem Rauschboden. Dann implementieren wir einen einfachen Cell-Averaging CFAR (CA-CFAR)-Algorithmus zur Detektion der Präambeln im empfangenen Signal. Der folgende Python-Code erzeugt das empfangene Signal: + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + from scipy.signal import correlate + + def generate_qpsk_packets(num_packets, sps, preamble): + """Erzeugt sich wiederholende QPSK-Pakete mit Lücken und variablem Rauschen.""" + qpsk_map = np.array([1+1j, -1+1j, -1-1j, 1-1j]) / np.sqrt(2) + data_len = 200 + gap_len = 100 + full_signal = [] + + # Upgesampelte Präambel für Korrelation vorberechnen + upsampled_preamble = np.repeat(preamble, sps) + + for _ in range(num_packets): + data = qpsk_map[np.random.randint(0, 4, data_len)] + packet = np.concatenate([preamble, data]) + full_signal.extend(np.repeat(packet, sps)) + full_signal.extend(np.zeros(gap_len * sps)) + + return np.array(full_signal), upsampled_preamble + + # Simulationsparameter + sps = 4 + preamble_syms = np.array([1+1j, 1+1j, -1-1j, -1-1j, 1-1j, -1+1j]) / np.sqrt(2) + tx_signal, ref_preamble = generate_qpsk_packets(5, sps, preamble_syms) + + # Zeitlich variierender Rauschboden + t = np.arange(len(tx_signal)) + noise_env = 0.05 + 0.3 * np.sin(2 * np.pi * 0.0003 * t)**2 + noise = (np.random.randn(len(tx_signal)) + 1j*np.random.randn(len(tx_signal))) * noise_env + rx_signal = tx_signal + noise + +Der erste Schritt ist eine einzelne Korrelation des empfangenen Signals gegen die bekannte Präambel (in der Praxis wird das meist in Batches von Samples durchgeführt, aber wir machen es in einem Batch): + +.. code-block:: python + + # Korrelationsspike erscheint, wenn die Referenz mit dem empfangenen Segment übereinstimmt + corr_out = correlate(rx_signal, ref_preamble, mode='same') + corr_power = np.abs(corr_out)**2 + +TODO: nur die rohe Ausgabe dieses Schrittes betrachten + +Jetzt implementieren wir den CFAR-Detektor, wenden ihn auf die Korrelatorausgabe an und visualisieren die Ergebnisse: + +.. code-block:: python + + # CFAR-Detektion auf der Korrelatorausgabe + def ca_cfar_adaptive(data, num_train, num_guard, pfa): + num_cells = len(data) + thresholds = np.zeros(num_cells) + alpha = num_train * (pfa**(-1/num_train) - 1) # Skalierungsfaktor + half_window = (num_train + num_guard) // 2 + guard_half = num_guard // 2 + for i in range(half_window, num_cells - half_window): + # Trainingsbereich um die Testzelle aufbauen + lagging_win = data[i - half_window : i - guard_half] + leading_win = data[i + guard_half + 1 : i + half_window + 1] + noise_floor_est = np.mean(np.concatenate([lagging_win, leading_win])) + thresholds[i] = alpha * noise_floor_est + return thresholds + + # Peaks in Korrelationsleistung detektieren + cfar_thresholds = ca_cfar_adaptive(corr_power, num_train=60, num_guard=20, pfa=1e-5) + detections = np.where(corr_power > cfar_thresholds)[0] + # Randdetektionen entfernen, wo die Schwelle undefiniert ist + detections = detections[cfar_thresholds[detections] > 0] + + # Teilgrafik 1: empfangenes Signal und rohe Leistung + plt.figure(figsize=(14, 8)) + plt.subplot(2, 1, 1) + plt.plot(np.abs(rx_signal)**2, color='gray', alpha=0.4, label='Rx Signal Power ($|r(t)|^2$)') + plt.title("Time-Domain Received Signal") + plt.ylabel("Power") + plt.legend() + plt.grid(True, alpha=0.3) + + # Teilgrafik 2: Korrelatorausgabe vs. adaptive Schwelle + plt.subplot(2, 1, 2) + plt.plot(corr_power, label='Correlator Output $|r(t) * p^*(-t)|^2$', color='blue') + plt.plot(cfar_thresholds, label='CFAR Adaptive Threshold', color='red', linestyle='--', linewidth=1.5) + if len(detections) > 0: # Detektionen einblenden + plt.scatter(detections, corr_power[detections], color='lime', edgecolors='black', label='Detections (Preamble Found)', zorder=5) + plt.title("Preamble Correlator Output with Adaptive CFAR Threshold") + plt.xlabel("Sample Index") + plt.ylabel("Correlation Power") + plt.legend() + plt.grid(True, alpha=0.3) + plt.show() + +.. image:: ../_images/detection_cfar.svg + :align: center + :target: ../_images/detection_cfar.svg + :alt: CFAR Detector Output Example + + + +Frequenzversatz-robuste Präambel-Korrelatoren +#################################################### + +Die Detektion einer Präambel wird zu einem mehrdimensionalen Suchproblem, wenn die Mittenfrequenz unbekannt ist. In einem perfekt synchronisierten System wirkt ein kohärenter Korrelator als Matched Filter und maximiert das SNR. Frequenzversätze führen jedoch zu einer zeitlich variierenden Phasenrotation, die das Signal von der lokalen Vorlage dekorreliert und zu einer erheblichen Verschlechterung der Detektionsempfindlichkeit führt. + +Der Einfluss eines Frequenzversatzes :math:`\Delta f` hängt von seiner Größe relativ zur Präambeldauer (:math:`T_{p}`) ab: + +Leicht versetzte Signale, wie sie durch Doppler oder Frequenzdrift verursacht werden, entstehen typischerweise durch LO-ppm-Ungenauigkeiten oder langsame Bewegung. In diesem Fall gilt :math:`\Delta f \cdot T_{p} \ll 1`. Der Korrelationspeak wird leicht abgeschwächt, aber die Zeitlage kann noch wiederhergestellt werden. + +Wenn der Frequenzversatz völlig unbekannt ist – z. B. bei der Kaltstart-Satellitenakquisition oder Hochdynamik-UAV-Links –, kann die kohärente Summe auf null fallen, wenn die Phase sich um mehr als :math:`180^{\circ}` über die Präambel dreht (:math:`\Delta f > 1/(2T_{p})`). In diesem Fall wird Detektion unabhängig vom SNR unmöglich. + +Der Verlust in der Korrelationsamplitude durch einen Frequenzversatz wird durch den Dirichlet-Kernel (oder die periodische sinc-Funktion) beschrieben. Mit zunehmendem Frequenzversatz folgt die kohärente Summe rotierter Vektoren diesem sinc-artigen Abfall. + +Der Verlust in dB durch den Frequenzversatz lässt sich durch folgende Formel approximieren: + +:math:`L_{dB}(\Delta f) = 20 \log_{10} \left| \frac{\sin(\pi \Delta f N T_{s})}{N \sin(\pi \Delta f T_{s})} \right|` + +Dabei gilt: + + - :math:`N`: Anzahl der Symbole in der Präambel. + - :math:`T_{s}`: Symboldauer. + - :math:`\Delta f`: Frequenzversatz in Hz. + +Mit zunehmendem :math:`\Delta f` oszilliert der Zähler, während der Nenner wächst – das erzeugt Nullstellen in der Detektorempfindlichkeit. Für einen Standard-Korrelator liegt die erste Nullstelle bei :math:`\Delta f = 1/(N T_{s})`. Bei einem halben Bin-Versatz verlierst du ungefähr 3,9 dB, was dein effektives SNR und :math:`P_{d}` erheblich verschlechtert. + +Methoden zur Robustheit gegenüber Frequenzversätzen +########################################### + +A. Kohärenter segmentierter Korrelator + +Die Präambel der Länge :math:`N` wird in :math:`M` Segmente der Länge :math:`L = N/M` aufgeteilt. Jedes Segment wird kohärent korreliert, und die Ergebnisse werden durch Kompensation der Phasendrift zwischen den Segmenten kombiniert. + +:math:`Y_{coh} = \sum_{m=0}^{M-1} \left( \sum_{k=0}^{L-1} r[k+mL] \cdot p^{*}[k] \right) e^{-j \hat{\phi}_m}` + +Dabei ist :math:`\hat{\phi}_m` eine Schätzung der Phasenrotation für dieses Segment. Das bewahrt den SNR-Gewinn einer vollständigen Präambel, erfordert aber eine genaue Frequenzschätzung zur Phasenausrichtung. + +B. Nicht-kohärenter segmentierter Korrelator + +Segmente werden kohärent korreliert, aber ihre Beträge werden summiert – Phaseninformation wird verworfen. + +:math:`Y_{non-coh} = \sum_{m=0}^{M-1} \left| \sum_{k=0}^{L-1} r[k+mL] \cdot p^{*}[k] \right|^{2}` + +Dieser Ansatz ist extrem robust gegenüber Frequenzversätzen (bis zu :math:`1/(L T_{s})`). Er leidet jedoch unter dem nicht-kohärenten Integrationsverlust: Das Summieren von Beträgen statt komplexer Werte lässt Rauschen schneller akkumulieren als das Signal und reduziert effektiv das „Post-Detektions"-SNR. + +C. Brute-Force-Frequenzsuche + +Der Empfänger betreibt mehrere parallele Korrelatoren, jeweils um eine diskrete Frequenz :math:`\Delta f_{i}` verschoben. + +Diese Methode liefert die beste SNR-Leistung (voller kohärenter Gewinn), ist aber am rechenintensivsten. Der „Bin-Abstand" muss eng genug sein (basierend auf der Dirichlet-Formel), um sicherzustellen, dass der schlechteste Verlust zwischen den Bins akzeptabel ist (z. B. < 1 dB). + +Bei Zeitbereichs-Abtastung werden Samples mit einem festen Satz von Gewichten gefaltet. Bei einer Frequenzsuche erfordert das eine separate FIR-Bank für jedes Frequenzbin – effizient für kurze Präambeln auf FPGAs mit Xilinx-DSP48-Slices. +Frequenzbereichsverarbeitung (FFT): Zur Durchführung einer Suche nimmt man die FFT des eingehenden Signals und der Präambel. Multiplikation im Frequenzbereich entspricht Korrelation. +Der „Frequenzverschiebungs-Trick": Um verschiedene Frequenzversätze zu testen, benötigt man nicht mehrere FFTs – man kann einfach die FFT-Bins der Präambel relativ zum Signal zirkulär verschieben, bevor man die punktweise Multiplikation und IFFT durchführt. +Für kontinuierliche Datenströme werden Chunking-Methoden wie Overlap-Save oder Overlap-Add verwendet, um Daten in Blöcken zu verarbeiten, ohne Korrelationspeaks an den Rändern der FFT-Fenster zu verlieren. + +Robustheit gegenüber Frequenzversätzen ist ein Kompromiss zwischen Verarbeitungsgewinn und Rechenkomplexität. Nicht-kohärente segmentierte Korrelation ist die robusteste Wahl in Umgebungen mit hoher Unsicherheit, erfordert aber höhere Linkmargen. Kohärente segmentierte und Brute-Force-FFT-Suchen bieten bessere Empfindlichkeit, benötigen aber erheblich mehr Hardware-Ressourcen. Das Verständnis des Dirichlet-getriebenen Verlusts ist entscheidend bei der Wahl der Bin-Dichte für jeden frequenzsuchenden Empfänger. + +TODO: Diesen Plot erklären und einen Teil des Python-Codes dem Abschnitt hinzufügen + +.. image:: ../_images/detection_freq_offset.svg + :align: center + :target: ../_images/detection_freq_offset.svg + :alt: Frequency Offset Impact on Correlation + +***************************************************************** +Detektion von Direct Sequence Spread Spectrum (DSSS)-Signalen +***************************************************************** + +In einem DSSS-System (Direct Sequence Spread Spectrum) ist der Korrelationsdetektor das Bindeglied, das ein bedeutungsvolles Signal aus dem herausarbeitet, was zunächst wie zufälliges Rauschen aussieht. Durch Verwendung einer Chip-Sequenz mit hoher Rate – oder einem Chipping-Code – verteilt das System die Signalenergie über eine viel größere Bandbreite als das ursprüngliche Datensignal benötigt. Da die Gesamtleistung konstant bleibt, sinkt durch die Verteilung über einen breiteren Frequenzbereich die Leistungsspektraldichte (PSD). Dieser Spektralverdünnungseffekt kann den Signalpegel unter den thermischen Rauschboden treiben und ihn für konventionelle Schmalband-Empfänger nahezu unsichtbar machen. Für den vorgesehenen Empfänger kann jedoch dieselbe Chip-Sequenz angewendet werden, um das Signal zu de-spreizen – die Energie wird in die ursprüngliche schmale Bandbreite zurückkonzentriert, während gleichzeitig Schmalband-Interferenz gespreizt wird. Das ermöglicht eine zuverlässige Detektion selbst in sehr verrauschten Umgebungen. Der nächste Unterabschnitt befasst sich mit dem Timing-Aspekt dieses Problems. + +Die Rolle von Auto-Korrelationseigenschaften +######################################## + +Die Wahl der richtigen Sequenz ist entscheidend für Synchronisation und Mehrwegausbreitung-Unterdrückung. Idealerweise sollte eine Sequenz perfekte Auto-Korrelation haben: einen hohen Peak bei perfekter Ausrichtung und nahezu null Werte bei jedem anderen Zeitversatz. Scharfe Auto-Korrelationspeaks erlauben dem Empfänger, das Signal mit Sub-Chip-Timing-Genauigkeit zu erfassen. Wenn ein Signal von einem Gebäude reflektiert wird und verzögert ankommt, sorgen gute Auto-Korrelationseigenschaften dafür, dass der Empfänger die verzögerte Version als unkorreliertes Rauschen behandelt statt als destruktive Interferenz, was Mehrwege-Ausbreitung abmildert. + + +Häufige Spreizsequenzen +########################## + +Verschiedene Anwendungen erfordern unterschiedliche mathematische Eigenschaften in ihren Spreizsequenzen. Einige Beispiele: + +- Barker-Codes sind bekannt für die bestmöglichen Auto-Korrelationseigenschaften für kurze Längen (bis 13) und werden berühmtlich in 802.11b WLAN verwendet. +- M-Sequenzen (maximale Länge), erzeugt mit linearen Rückkopplungs-Schieberegistern (LFSRs), bieten ausgezeichnete Zufälligkeit und Auto-Korrelation über sehr lange Perioden. +- Gold-Codes, abgeleitet aus Paaren von M-Sequenzen, bieten eine große Menge von Sequenzen mit kontrollierter Kreuzkorrelation und sind der Standard für GPS und CDMA, wo mehrere Signale koexistieren müssen. +- Zadoff-Chu (ZC)-Sequenzen sind komplexwertige Sequenzen mit konstanter Amplitude und null Auto-Korrelation für alle Nicht-null-Verschiebungen und sind inzwischen ein Standard in LTE und 5G für Synchronisation. +- Kasami-Codes ähneln Gold-Codes, haben aber noch niedrigere Kreuzkorrelation für eine gegebene Sequenzlänge, was sie in dichten Umgebungen nützlich macht. + +Chip-Timing-Synchronisation in DSSS +#################################################### + +In einem DSSS-System hängt die Fähigkeit des Empfängers, Daten wiederherzustellen, vollständig von der Synchronisation mit der eingehenden Chip-Sequenz ab. Da Chips viel kürzer als Datenbits sind, kann selbst ein kleiner fraktionaler Timing-Fehler – bei dem der Empfänger zwischen Chips abtastet – den Korrelationspeak erheblich reduzieren. Wir können den Einfluss eines fraktionalen Timing-Versatzes untersuchen, indem wir ein einfaches DSSS-System simulieren und die Korrelationsausgabe darstellen, während der Timing-Versatz von 0 auf 1 Chip variiert. Beachte, dass wir hier keine vollständige Korrelation durchführen – wir nehmen nur ein Skalarprodukt bei Lag 0, weil wir bereits wissen, dass dort der Peak liegt. + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + + # Barker-11-Sequenz + barker11 = np.array([1, -1, 1, 1, -1, 1, 1, 1, -1, -1, -1]) + samples_per_chip = 100 + + # Sequenz upsampeln, um kontinuierliche Zeit zu simulieren + sig = np.repeat(barker11, samples_per_chip) + + offsets = np.linspace(-1.5, 1.5, 500) # Fraktionale Chip-Versätze + peaks = [] + + for offset in offsets: + # Signal um fraktionale Anzahl von Chips verschieben, umgerechnet in Samples + shift_samples = int(offset * samples_per_chip) + if shift_samples > 0: + shifted_sig = np.pad(sig, (shift_samples, 0))[:len(sig)] + elif shift_samples < 0: + shifted_sig = np.pad(sig, (0, abs(shift_samples)))[abs(shift_samples):] + else: + shifted_sig = sig + + # Normierte Korrelation bei Lag 0 für diesen Versatz berechnen + correlation = np.vdot(sig, shifted_sig) / np.vdot(sig, sig) + peaks.append(np.abs(correlation)) + + plt.figure(figsize=(10, 5)) + plt.plot(offsets, peaks, label='Normalized Correlation', color='blue', linewidth=2) + plt.axvline(0, color='red', linestyle='--', alpha=0.5, label='Perfect Alignment') + plt.title('DSSS Correlation Peak vs. Fractional Chip Timing Offset') + plt.xlabel('Offset (Fraction of a Chip)') + plt.ylabel('Normalized Correlation Peak Magnitude') + plt.grid(True, which='both', linestyle='--', alpha=0.6) + plt.legend() + plt.savefig('../_images/detection_dsss.svg', bbox_inches='tight') + plt.show() + +.. image:: ../_images/detection_dsss.svg + :align: center + :target: ../_images/detection_dsss.svg + :alt: DSSS + +Der Peak tritt erwartungsgemäß bei null Versatz auf und fällt linear ab, bis er bei einem halben Chip Versatz die Hälfte des Peakwertes erreicht. Nach mehr als einem Chip Versatz kann die Korrelation wieder anzusteigen scheinen, aber der eigentliche Peak ist niedrig, weil das Signal nicht mehr mit der Sequenz ausgerichtet ist. + +**************************************************** +Echtzeit-Paketdetektion in kontinuierlichen IQ-Strömen +**************************************************** + +Bisher haben wir die theoretischen Grundlagen der Signaldetektion erforscht – von Korrelatoren über CFAR-Detektoren bis zu Spread-Spectrum-Systemen. Jetzt kombinieren wir sie, um ein häufiges praktisches Problem zu lösen: **die Detektion intermittierender Pakete in einem kontinuierlichen Strom von IQ-Samples von einem SDR**. Betrachte folgendes Szenario: Ein Modem oder IoT-Gerät sendet einmal pro Sekunde oder in unregelmäßigen Abständen ein Datenpaket. Dein SDR empfängt kontinuierlich Samples bei z. B. 1 MHz. Die Pakete kommen zu unvorhersehbaren Zeiten an, eingebettet in Rauschen und Interferenz. Du musst: + +1. Erkennen, wann ein Paket eintrifft +2. Den genauen Sample-Index bestimmen, wo es beginnt +3. Das Paket zur weiteren Verarbeitung (Demodulation, Dekodierung usw.) extrahieren +4. Das alles in Echtzeit tun, ohne Pakete zu verpassen + +Das unterscheidet sich grundlegend von der Verarbeitung einer voraufgezeichneten IQ-Datei, wo du das gesamte Signal auf einmal analysieren kannst. Hier kommen Samples kontinuierlich an, und du musst in Echtzeit mit begrenzten Rechenressourcen Entscheidungen treffen. Wir kombinieren mehrere in diesem Kapitel behandelte Techniken: + +1. **Kreuzkorrelation**: Um das bekannte Präambelmuster zu finden +2. **CFAR-Detektion**: Um Schwellen trotz variablem Rauschen adaptiv zu setzen +3. **Pufferverwaltung**: Um kontinuierliche Streaming-Daten zu verwalten +4. **Peak-Detektion**: Um genaues Paket-Timing zu extrahieren + +Um in Echtzeit zu arbeiten, akkumulieren wir Samples in **Puffern** von z. B. 100.000 Samples, führen den Detektor auf jedem Puffer aus und halten Zustand über Puffergrenzen hinweg aufrecht, damit Pakete, die zwei Puffer überspannen, nicht übersehen werden. + +Implementierung +############## + +Unser Detektor folgt diesem Ablauf: + +.. mermaid:: + + flowchart TD + A("Kontinuierlicher IQ-Strom vom SDR
(1 MHz Abtastrate)") + B("Puffer-Akkumulation
(100k Samples = 0,1 s)") + C("Kreuzkorrelation mit bekannter Präambel") + D("CFAR-Schwellenberechnung") + E("Peak-Detektion
(Korrelation > Schwelle)") + F("Paketextraktion & Validierung") + A --> B --> C --> D --> E --> F + +Um Pakete zu vermeiden, die Puffergrenzen überqueren, verwenden wir einen **Overlap-Save**-Ansatz, bei dem jeder Puffer die letzten ``N_preamble`` Samples aus dem vorherigen Puffer enthält. Das stellt sicher, dass Pakete, die nahe dem Ende von Puffer ``i`` beginnen, vollständig in Puffer ``i+1`` enthalten sind. Es fügt einen kleinen rechnerischen Overhead hinzu, aber das ist dem Verpassen von Paketen an der Puffergrenze vorzuziehen. + +Bauen wir Schritt für Schritt einen vollständigen Paketdetektor in Python. Wir verwenden eine kürzere Zadoff-Chu-Präambel als zuvor und implementieren einen adaptiven CFAR-Detektor. + +Schritt 1: Präambel und Parameter definieren +******************************************* + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + from scipy.signal import correlate + + # Präambel: Zadoff-Chu-Sequenz (hervorragende Korrelationseigenschaften) + N_zc = 63 # ZC-Sequenzlänge (typischerweise prim oder 2er-Potenz minus 1) + u = 5 # ZC-Wurzel + t = np.arange(N_zc) + preamble = np.exp(-1j * np.pi * u * t * (t + 1) / N_zc) + + # Systemparameter + sample_rate = 1e6 + buffer_size = 100000 + overlap_size = len(preamble) # Überlappung für Grenzpakete + + # CFAR-Parameter + cfar_guard = 10 + cfar_train = 50 + pfa_target = 1e-6 + + # Paketparameter (für Simulation) + packet_length = 500 # Gesamtpaketlänge in Samples (Präambel + Daten) + snr_db = -5 + +Schritt 2: CFAR-Detektorfunktion +******************************* + +Wir verwenden den Cell-Averaging CFAR (CA-CFAR) von früher, leicht optimiert: + +.. code-block:: python + + def ca_cfar_1d(signal, num_train, num_guard, pfa): + """ + 1D Cell-Averaging CFAR-Detektor. + + Args: + signal: Eingangssignal (typischerweise Korrelationsbetrag) + num_train: Anzahl der Trainingszellen (auf jeder Seite) + num_guard: Anzahl der Schutzzellen (auf jeder Seite) + pfa: Ziel-Fehlalarmwahrscheinlichkeit + + Returns: + threshold: Adaptives Schwellen-Array + """ + n = len(signal) + threshold = np.zeros(n) + alpha = num_train * (pfa**(-1/num_train) - 1) + + for i in range(n): + # Trainingsbereich-Indizes definieren + train_start_left = max(0, i - num_guard - num_train) + train_end_left = max(0, i - num_guard) + train_start_right = min(n, i + num_guard + 1) + train_end_right = min(n, i + num_guard + num_train + 1) + + # Trainingszellen sammeln (Schutzzellen und CUT ausschließen) + train_cells = np.concatenate([ + signal[train_start_left:train_end_left], + signal[train_start_right:train_end_right] + ]) + + if len(train_cells) > 0: + noise_est = np.mean(train_cells) + threshold[i] = alpha * noise_est + + return threshold + +Schritt 3: Paketdetektionsfunktion +********************************** + +.. code-block:: python + + def detect_packets(buffer, preamble, cfar_guard, cfar_train, pfa, + min_spacing=None): + """ + Pakete in einem Puffer von IQ-Samples detektieren. + + Args: + buffer: Komplexe IQ-Samples + preamble: Bekannte Präambelsequenz + cfar_guard: CFAR-Schutzzellen + cfar_train: CFAR-Trainingszellen + pfa: Ziel-Fehlalarmwahrscheinlichkeit + min_spacing: Minimale Samples zwischen Detektionen (verhindert Duplikate) + + Returns: + detections: Liste von Sample-Indizes, wo Pakete beginnen + """ + # Puffer mit Präambel korrelieren + corr = correlate(buffer, preamble, mode='same') + corr_power = np.abs(corr)**2 + + # Adaptive Schwelle berechnen + threshold = ca_cfar_1d(corr_power, cfar_train, cfar_guard, pfa) + + # Peaks oberhalb der Schwelle finden + detections_raw = np.where(corr_power > threshold)[0] + + # Korrelationsversatz kompensieren (Peak tritt len(preamble)//2 nach dem echten Start auf) + half_preamble = len(preamble) // 2 + detections_raw = detections_raw - half_preamble + + # Randdetektionen entfernen (unzuverlässig) + half_preamble = len(preamble) // 2 + detections_raw = detections_raw[ + (detections_raw > half_preamble) & + (detections_raw < len(buffer) - half_preamble) + ] + + # Doppelte Detektionen entfernen (Peaks nah beieinander) + if min_spacing is None: + min_spacing = len(preamble) + + detections = [] + if len(detections_raw) > 0: + detections.append(detections_raw[0]) + for det in detections_raw[1:]: + if det - detections[-1] > min_spacing: + detections.append(det) + + return detections, corr_power, threshold + +Schritt 4: Simulation – Testsignal erzeugen +****************************************** + +.. code-block:: python + + def generate_packet_stream(preamble, packet_length, num_packets, + sample_rate, snr_db): + """ + Simulierten IQ-Strom mit intermittierenden Paketen erzeugen. + + Returns: + signal: Komplexe IQ-Samples + true_starts: Echte Paketstart-Indizes + """ + # Rauschleistung aus SNR berechnen + signal_power = 1.0 # Normierte Präambelleistung + noise_power = signal_power / (10**(snr_db/10)) + noise_std = np.sqrt(noise_power / 2) # Komplexes Rauschen + + # QPSK-Daten erzeugen (zufällige Nutzlast nach Präambel) + qpsk_map = np.array([1+1j, -1+1j, -1-1j, 1-1j]) / np.sqrt(2) + + # Zeit zwischen Paketen (1 Sekunde ±20% Jitter) + packets_per_sec = 1 + avg_gap = int(sample_rate / packets_per_sec) + + signal = [] + true_starts = [] + + for i in range(num_packets): + # Lücke hinzufügen (nur Rauschen) + if i == 0: + gap_length = np.random.randint(avg_gap//2, avg_gap) + else: + gap_length = np.random.randint(int(avg_gap*0.8), int(avg_gap*1.2)) + + noise = noise_std * (np.random.randn(gap_length) + + 1j*np.random.randn(gap_length)) + signal.extend(noise) + + # Echten Paketstart aufzeichnen + true_starts.append(len(signal)) + + # Paket hinzufügen (Präambel + Daten) + data_length = packet_length - len(preamble) + data = qpsk_map[np.random.randint(0, 4, data_length)] + packet = np.concatenate([preamble, data]) + + # Rauschen zum Paket hinzufügen + packet_noisy = packet + noise_std * (np.random.randn(len(packet)) + + 1j*np.random.randn(len(packet))) + signal.extend(packet_noisy) + + # Abschließende Lücke hinzufügen + gap_length = np.random.randint(avg_gap//2, avg_gap) + noise = noise_std * (np.random.randn(gap_length) + + 1j*np.random.randn(gap_length)) + signal.extend(noise) + + return np.array(signal), true_starts + + # 5 Sekunden Signal mit ~5 Paketen erzeugen + signal, true_starts = generate_packet_stream( + preamble, packet_length, num_packets=5, + sample_rate=sample_rate, snr_db=snr_db + ) + + print(f"Generated {len(signal)} samples ({len(signal)/sample_rate:.1f} sec)") + print(f"True packet starts: {true_starts}") + +Schritt 5: Detektion im Streaming-Modus ausführen +**************************************** + +Jetzt verarbeiten wir das Signal in Blöcken und simulieren Echtzeit-Streaming: + +.. code-block:: python + + def process_stream(signal, preamble, buffer_size, overlap_size, + cfar_guard, cfar_train, pfa): + """ + Kontinuierlichen IQ-Strom in Puffern verarbeiten (simuliert Echtzeit). + + Returns: + all_detections: Liste detektierter Paketstarts (globale Indizes) + """ + all_detections = [] + n_samples = len(signal) + current_pos = 0 + + while current_pos < n_samples: + # Puffer mit Überlappung definieren + buffer_start = max(0, current_pos - overlap_size) + buffer_end = min(n_samples, current_pos + buffer_size) + buffer = signal[buffer_start:buffer_end] + + # Pakete in diesem Puffer detektieren + detections, corr_power, threshold = detect_packets( + buffer, preamble, cfar_guard, cfar_train, pfa + ) + + # Puffer-relative Indizes in globale Indizes umrechnen + for det in detections: + global_idx = buffer_start + det + + # Doppelte Detektionen aus Überlappungsbereich vermeiden + if len(all_detections) == 0 or \ + global_idx - all_detections[-1] > len(preamble): + all_detections.append(global_idx) + + current_pos += buffer_size + + return all_detections + + + detected_starts = process_stream( + signal, preamble, buffer_size, overlap_size, + cfar_guard, cfar_train, pfa_target + ) + + print(f"\nDetection Results:") + print(f"True packets: {len(true_starts)}") + print(f"Detected packets: {len(detected_starts)}") + print(f"Detected starts: {detected_starts}") + +Schritt 6: Leistung bewerten +***************************** + +.. code-block:: python + + # Detektionsstatistiken berechnen + tolerance = len(preamble) + + matched_detections = [] + false_alarms = [] + + for det in detected_starts: + # Prüfen, ob Detektion mit einem echten Paket übereinstimmt + matched = False + for true_start in true_starts: + if abs(det - true_start) <= tolerance: + matched_detections.append(det) + matched = True + break + if not matched: + false_alarms.append(det) + + missed_packets = len(true_starts) - len(matched_detections) + + print(f"\nPerformance Metrics:") + print(f" Correct detections: {len(matched_detections)}/{len(true_starts)}") + print(f" Missed packets: {missed_packets}") + print(f" False alarms: {len(false_alarms)}") + + # Timing-Fehler berechnen + timing_errors = [] + for det in matched_detections: + errors = [abs(det - ts) for ts in true_starts] + timing_errors.append(min(errors)) + + if len(timing_errors) > 0: + print(f" Timing error (avg): {np.mean(timing_errors):.1f} samples") + print(f" Timing error (max): {np.max(timing_errors):.1f} samples") + +Schritt 7: Ergebnisse visualisieren +************************** + +.. code-block:: python + + # Einen Puffer für detaillierte Visualisierung verarbeiten + buffer_start = max(0, true_starts[0] - 5000) + buffer_end = min(len(signal), true_starts[0] + 20000) + viz_buffer = signal[buffer_start:buffer_end] + + detections_viz, corr_viz, thresh_viz = detect_packets( + viz_buffer, preamble, cfar_guard, cfar_train, pfa_target + ) + + # In globale Indizes für Darstellung umrechnen + detections_viz_global = [d + buffer_start for d in detections_viz] + + # Visualisierung erstellen + fig, axes = plt.subplots(3, 1, figsize=(14, 10)) + time_axis = (np.arange(len(viz_buffer)) + buffer_start) / sample_rate * 1000 # ms + + # Teilgrafik 1: Leistung des empfangenen Signals + axes[0].plot(time_axis, np.abs(viz_buffer)**2, 'gray', alpha=0.6, linewidth=0.5) + axes[0].set_ylabel('Power') + axes[0].set_title('Received IQ Signal Power') + axes[0].grid(True, alpha=0.3) + + # Echte Paketpositionen markieren + for ts in true_starts: + if buffer_start <= ts <= buffer_end: + t_ms = ts / sample_rate * 1000 + axes[0].axvline(t_ms, color='green', linestyle='--', alpha=0.7, + label='True Packet' if ts == true_starts[0] else '') + axes[0].legend() + + # Teilgrafik 2: Korrelationsausgabe + axes[1].plot(time_axis, corr_viz, 'blue', linewidth=1, label='Correlation') + axes[1].plot(time_axis, thresh_viz, 'red', linestyle='--', linewidth=1.5, + label='CFAR Threshold') + axes[1].set_ylabel('Correlation Power') + axes[1].set_title('Preamble Correlation with Adaptive CFAR Threshold') + axes[1].grid(True, alpha=0.3) + axes[1].legend() + + # Teilgrafik 3: Detektionen + detection_mask = np.zeros(len(viz_buffer)) + for det in detections_viz: + detection_mask[det] = corr_viz[det] + + axes[2].plot(time_axis, corr_viz, 'blue', alpha=0.4, linewidth=0.8) + axes[2].scatter(time_axis[detection_mask > 0], detection_mask[detection_mask > 0], + color='lime', edgecolors='black', s=100, zorder=5, + label='Detected Packets') + axes[2].set_xlabel('Time (ms)') + axes[2].set_ylabel('Correlation Power') + axes[2].set_title('Detected Packet Locations') + axes[2].grid(True, alpha=0.3) + axes[2].legend() + + plt.tight_layout() + plt.show() + +Die Visualisierung sollte zeigen: + +1. **Obere Grafik**: Rohe Signalleistung mit markierten echten Paketpositionen +2. **Mittlere Grafik**: Korrelationsausgabe mit adaptiver CFAR-Schwelle, die den Rauschboden verfolgt +3. **Untere Grafik**: Detektierte Pakete als hervorgehobene Peaks oberhalb der Schwelle + +.. image:: ../_images/detection_realtime.png + :align: center + :scale: 50 % + :alt: Real-time packet detection results + +Praktische Überlegungen und Parametereinstellung +#################################### + +Puffergröße: Kompromisse +*********************** + +**Größere Puffer**, z. B. 1M Samples: + +- ✅ Bessere CFAR-Rauschschätzung (mehr Trainingszellen) +- ✅ Geringerer Rechenoverhead (weniger Verarbeitungsaufrufe) +- ❌ Höhere Latenz (Puffer muss sich erst füllen) +- ❌ Mehr Speicher benötigt + +**Kleinere Puffer**, z. B. 10k Samples: + +- ✅ Niedrigere Latenz (schnellere Reaktion) +- ✅ Weniger Speicherverbrauch +- ❌ CFAR-Leistung verschlechtert sich (weniger Trainingszellen) +- ❌ Höhere CPU-Auslastung (häufigere Verarbeitung) + +**Empfehlung**: Starte mit einer Puffergröße von 10× bis 100× deiner Präambellänge. Für eine 63-Sample-Präambel bei 1 Msps probiere 10k bis 100k Samples. + +CFAR-Parametereinstellung +********************** + +Die drei CFAR-Parameter steuern das Detektorverhalten: + +**num_guard**: Schutzzellen + +- Verhindert Signalleckage in die Rauschschätzung +- Zu klein: Signal leckt in den Trainingsbereich, erhöht die Schwelle und verursacht verpasste Detektionen +- Zu groß: weniger Trainingszellen und schlechtere Rauschschätzung +- Faustregel: auf etwa 0,5 bis 1,0× die Präambellänge setzen + +**num_train**: Trainingszellen + +- Schätzt den lokalen Rauschboden +- Zu klein: verrauschte Schwelle und mehr Fehlalarme oder verpasste Detektionen +- Zu groß: Schwelle passt sich nicht schnell genug an Rauschänderungen an +- Faustregel: auf etwa 3 bis 5× die Präambellänge setzen + +**pfa**: Fehlalarmwahrscheinlichkeit + +- Steuert die Detektionsempfindlichkeit +- Zu hoch, z. B. 1e-2: viele Fehlalarme +- Zu niedrig, z. B. 1e-10: verpasste schwache Pakete +- Faustregel: mit 1e-5 für Pro-Lag-PFA beginnen, dann anhand der systemweiten Fehlalarmrate anpassen + +Erinnere dich an die Beziehung zwischen Pro-Lag- und systemweiter Fehlalarmrate aus dem früheren Abschnitt dieses Kapitels. diff --git a/content-de/digital_modulation.rst b/content-de/digital_modulation.rst new file mode 100644 index 00000000..29251f34 --- /dev/null +++ b/content-de/digital_modulation.rst @@ -0,0 +1,407 @@ +.. _modulation-chapter: + +################### +Digitale Modulation +################### + +In diesem Kapitel werden wir das *tatsächliche Übertragen von Daten* mittels digitaler Modulation und drahtlosen Symbolen besprechen! Wir werden Signale entwerfen, die „Informationen" übermitteln, z.B. Einsen und Nullen, unter Verwendung von Modulationsverfahren wie ASK, PSK, QAM und FSK. Wir werden auch IQ-Diagramme und Konstellationen besprechen und das Kapitel mit einigen Python-Beispielen abschließen. + +Das Hauptziel der Modulation ist es, so viele Daten wie möglich in so wenig Spektrum wie möglich zu pressen. Technisch gesehen wollen wir die „spektrale Effizienz" in Einheiten von Bit/s/Hz maximieren. Das schnellere Übertragen von Einsen und Nullen erhöht die Bandbreite unseres Signals (erinnere dich an Fourier-Eigenschaften), was bedeutet, dass mehr Spektrum genutzt wird. Wir werden auch andere Techniken als nur das schnellere Senden untersuchen. Es wird viele Kompromisse geben, wenn wir entscheiden, wie wir modulieren wollen, aber es wird auch Raum für Kreativität geben. + +******************* +Symbole +******************* +Neuer Begriff! Unser Sendesignal wird aus „Symbolen" bestehen. Jedes Symbol trägt eine bestimmte Anzahl von Informationsbits, und wir werden Symbole nacheinander übertragen, Tausende oder sogar Millionen hintereinander. + +Als vereinfachtes Beispiel, sagen wir, wir haben ein Kabel und senden Einsen und Nullen mit hohen und niedrigen Spannungspegeln. Ein Symbol ist eine dieser Einsen oder Nullen: + +.. image:: ../_images/symbols.png + :scale: 60 % + :align: center + :alt: Pulse train of ones and zeros depicting the concept of a digital symbol that carries information + +Im obigen Beispiel repräsentiert jedes Symbol ein Bit. Wie können wir mehr als ein Bit pro Symbol übermitteln? Lass uns die Signale untersuchen, die durch Ethernet-Kabel laufen, die in einem IEEE-Standard namens IEEE 802.3 1000BASE-T definiert sind. Der gängige Betriebsmodus von Ethernet verwendet eine 4-stufige Amplitudenmodulation (2 Bit pro Symbol) mit 8-ns-Symbolen. + +.. image:: ../_images/ethernet.svg + :align: center + :target: ../_images/ethernet.svg + :alt: Plot of IEEE 802.3 1000BASE-T Ethernet voltage signal showing 4-level amplitude shift keying (ASK) + +Nimm dir einen Moment, um diese Fragen zu beantworten: + +1. Wie viele Bits pro Sekunde werden im oben gezeigten Beispiel übertragen? +2. Wie viele Paare dieser Datenkabel wären nötig, um 1 Gigabit/s zu übertragen? +3. Wenn ein Modulationsverfahren 16 verschiedene Stufen hat, wie viele Bits pro Symbol sind das? +4. Mit 16 verschiedenen Stufen und 8-ns-Symbolen, wie viele Bits pro Sekunde sind das? + +.. raw:: html + +
+ Antworten + +1. 250 Mbps - (1/8e-9)*2 +2. Vier (was Ethernet-Kabel haben) +3. 4 Bits pro Symbol - log_2(16) +4. 0,5 Gbps - (1/8e-9)*4 + +.. raw:: html + +
+ +******************* +Drahtlose Symbole +******************* +Frage: Warum können wir das oben in der Abbildung gezeigte Ethernet-Signal nicht direkt übertragen? Es gibt viele Gründe, die zwei Größten sind: + +1. Niedrige Frequenzen erfordern *riesige* Antennen, und das obige Signal enthält Frequenzen bis hinunter zu DC (0 Hz). Wir können DC nicht übertragen. +2. Rechteckwellen belegen für die Bits pro Sekunde übermäßig viel Spektrum – erinnere dich aus dem Kapitel :ref:`freq-domain-chapter`, dass schnelle Änderungen im Zeitbereich eine große Menge an Bandbreite/Spektrum verbrauchen: + +.. image:: ../_images/square-wave.svg + :align: center + :target: ../_images/square-wave.svg + :alt: A square wave in time and frequency domain showing the large amount of bandwidth that a square wave uses + +Was wir bei drahtlosen Signalen tun, ist mit einem Träger zu beginnen, der nur ein Sinusoid ist. Z.B. verwendet UKW-Radio einen Träger wie 101,1 MHz oder 100,3 MHz. Wir modulieren diesen Träger auf irgendeine Weise (es gibt viele). Bei UKW-Radio ist es eine analoge Modulation, nicht digital, aber es ist dasselbe Konzept wie bei der digitalen Modulation. + +Auf welche Weisen können wir den Träger modulieren? Eine andere Weise, dieselbe Frage zu stellen: Was sind die verschiedenen Eigenschaften eines Sinusoids? + +1. Amplitude +2. Phase +3. Frequenz + +Wir können unsere Daten auf einen Träger modulieren, indem wir eine (oder mehrere) dieser drei Eigenschaften ändern. + +**************************** +Amplitudenumtastung (ASK) +**************************** + +Amplitudenumtastung (ASK, Amplitude Shift Keying) ist das erste digitale Modulationsverfahren, das wir besprechen werden, weil Amplitudenmodulation am einfachsten unter den drei Sinusoid-Eigenschaften zu visualisieren ist. Wir modulieren buchstäblich die **Amplitude** des Trägers. Hier ist ein Beispiel für 2-stufiges ASK, genannt 2-ASK: + +.. image:: ../_images/ASK.svg + :align: center + :target: ../_images/ASK.svg + :alt: Example of amplitude shift keying (ASK) in the time domain, specifically 2-ASK + +Beachte, dass der Durchschnittswert null ist; das bevorzugen wir immer, wenn möglich. + +Wir können mehr als zwei Stufen verwenden, was mehr Bits pro Symbol ermöglicht. Unten ist ein Beispiel für 4-ASK (Aus ist eine der vier Stufen). In diesem Fall trägt jedes Symbol 2 Bit Informationen. + +.. image:: ../_images/ask2.svg + :align: center + :target: ../_images/ask2.svg + :alt: Example of amplitude shift keying (ASK) in the time domain, specifically 4-ASK + +Frage: Wie viele Symbole sind im obigen Signalausschnitt gezeigt? Wie viele Bits werden insgesamt dargestellt? + +.. raw:: html + +
+ Antworten + +20 Symbole, also 40 Bits Informationen + +.. raw:: html + +
+ +Wie erstellen wir dieses Signal eigentlich digital, durch Code? Alles, was wir tun müssen, ist einen Vektor mit N Samples pro Symbol zu erstellen und diesen Vektor dann mit einem Sinusoid zu multiplizieren. Dies moduliert das Signal auf einen Träger (das Sinusoid fungiert als dieser Träger). Das folgende Beispiel zeigt 2-ASK mit 10 Samples pro Symbol. + +.. image:: ../_images/ask3.svg + :align: center + :target: ../_images/ask3.svg + :alt: Samples per symbol depiction using 2-ASK in the time domain, with 10 samples per symbol (sps) + +Das obere Diagramm zeigt die diskreten Samples als rote Punkte, d.h. unser digitales Signal. Das untere Diagramm zeigt, wie das resultierende modulierte Signal aussieht, das über die Luft übertragen werden könnte. In echten Systemen ist die Frequenz des Trägers normalerweise viel höher als die Rate, mit der sich die Symbole ändern. In diesem Beispiel gibt es nur 2,5 Zyklen des Sinusoids in jedem Symbol, aber in der Praxis könnte es Tausende geben, je nachdem, wie hoch im Spektrum das Signal übertragen wird. + +************************ +Phasenumtastung (PSK) +************************ + +Lass uns nun die Phase auf ähnliche Weise modulieren wie wir es mit der Amplitude getan haben. Die einfachste Form ist Binäre PSK, auch BPSK genannt, bei der es zwei Phasenstufen gibt: + +1. Keine Phasenänderung +2. 180-Grad-Phasenänderung + +Beispiel für BPSK (beachte die Phasenänderungen): + +.. image:: ../_images/bpsk.svg + :align: center + :target: ../_images/bpsk.svg + :alt: Simple example of binary phase shift keying (BPSK) in the time domain, showing a modulated carrier + +Es ist nicht sehr spaßig, Diagramme wie dieses anzusehen: + +.. image:: ../_images/bpsk2.svg + :align: center + :target: ../_images/bpsk2.svg + :alt: Phase shift keying like BPSK in the time domain is difficult to read, so we tend to use a constellation plot or complex plane + +Stattdessen stellen wir die Phase normalerweise in der komplexen Ebene dar. + +*********************** +IQ-Diagramme/Konstellationen +*********************** + +Du hast IQ-Diagramme bereits im Unterabschnitt komplexe Zahlen des Kapitels :ref:`sampling-chapter` gesehen, aber jetzt werden wir sie auf eine neue und interessante Weise verwenden. +Für ein gegebenes Symbol können wir die Amplitude und Phase in einem IQ-Diagramm zeigen. Für das BPSK-Beispiel sagten wir, dass wir Phasen von 0 und 180 Grad haben. Lass uns diese zwei Punkte im IQ-Diagramm darstellen. Wir nehmen eine Magnitude von 1 an. In der Praxis spielt es keine Rolle, welche Magnitude du verwendest; ein höherer Wert bedeutet ein stärkeres Signal, aber du kannst auch einfach den Verstärkungsgrad des Verstärkers erhöhen. + +.. image:: ../_images/bpsk_iq.png + :scale: 80 % + :align: center + :alt: IQ plot or constellation plot of BPSK + +Das obige IQ-Diagramm zeigt, was wir übertragen werden, oder genauer die Menge der Symbole, aus denen wir übertragen werden. Es zeigt nicht den Träger, also kannst du es dir als die Symbole im Basisband vorstellen. Wenn wir die Menge möglicher Symbole für ein gegebenes Modulationsverfahren zeigen, nennen wir das die „Konstellation". Viele Modulationsverfahren können durch ihre Konstellation definiert werden. + +Um BPSK zu empfangen und zu dekodieren, können wir IQ-Abtastung verwenden, wie wir im letzten Kapitel gelernt haben, und prüfen, wo die Punkte im IQ-Diagramm landen. Es wird jedoch eine zufällige Phasendrehung durch den drahtlosen Kanal geben, weil das Signal eine zufällige Verzögerung haben wird, wenn es durch die Luft zwischen Antennen geht. Die zufällige Phasendrehung kann mit verschiedenen Methoden, die wir später lernen werden, rückgängig gemacht werden. Hier ist ein Beispiel für einige verschiedene Arten, wie ein BPSK-Signal am Empfänger erscheinen könnte (ohne Rauschen): + +.. image:: ../_images/bpsk3.png + :scale: 60 % + :align: center + :alt: A random phase rotation of BPSK occurs as the wireless signal travels through the air + +Zurück zu PSK. Was wäre, wenn wir vier verschiedene Phasenstufen hätten? D.h. 0, 90, 180 und 270 Grad. In diesem Fall würde es im IQ-Diagramm so dargestellt werden, und es bildet ein Modulationsverfahren, das wir Quadraturphasenumtastung (QPSK) nennen: + +.. image:: ../_images/qpsk.png + :scale: 60 % + :align: center + :alt: Example of Quadrature Phase Shift Keying (QPSK) in the IQ plot or constellation plot + +Bei PSK haben wir immer N verschiedene Phasen, gleichmäßig um 360 Grad verteilt, für beste Ergebnisse. Wir zeigen oft den Einheitskreis, um zu betonen, dass alle Punkte dieselbe Magnitude haben: + +.. image:: ../_images/psk_set.png + :scale: 60 % + :align: center + :alt: Phase shift keying uses equally spaced constellation points on the IQ plot + +Frage: Was ist falsch an einem PSK-Verfahren wie dem im folgenden Bild? Ist es ein gültiges PSK-Modulationsverfahren? + +.. image:: ../_images/weird_psk.png + :scale: 60 % + :align: center + :alt: Example of non-uniformly spaced PSK constellation plot + +.. raw:: html + +
+ Antwort + +Es gibt nichts Ungültiges an diesem PSK-Verfahren. Du kannst es sicherlich verwenden, aber da die Symbole nicht gleichmäßig verteilt sind, ist dieses Verfahren nicht so effektiv wie es sein könnte. Die Verfahrenseffizienz wird klarer, sobald wir besprechen, wie Rauschen unsere Symbole beeinflusst. Die kurze Antwort ist, dass wir zwischen den Symbolen so viel Raum wie möglich lassen wollen, falls es Rauschen gibt, damit ein Symbol nicht vom Empfänger als eines der anderen (falschen) Symbole interpretiert wird. Wir wollen nicht, dass eine 0 als 1 empfangen wird. + +.. raw:: html + +
+ +Lass uns kurz zu ASK zurückkehren. Beachte, dass wir ASK genauso wie PSK im IQ-Diagramm darstellen können. Hier ist das IQ-Diagramm von 2-ASK, 4-ASK und 8-ASK in der bipolaren Konfiguration, sowie 2-ASK und 4-ASK in der unipolaren Konfiguration. In diesem Kontext bedeutet bipolar, dass das modulierte Signal sowohl positive als auch negative Amplitudenwerte annehmen kann, während unipolares ASK nur positive Amplituden verwendet. + +.. image:: ../_images/ask_set.png + :scale: 50 % + :align: center + :alt: Bipolar and unipolar amplitude shift keying (ASK) constellation or IQ plots + +Wie du vielleicht bemerkt hast, sind bipolares 2-ASK und BPSK dasselbe. Eine 180-Grad-Phasenverschiebung ist dasselbe wie das Multiplizieren des Sinusoids mit -1. Wir nennen es BPSK, wahrscheinlich weil PSK viel häufiger verwendet wird als ASK. + +************************************** +Quadraturamplitudenmodulation (QAM) +************************************** +Was, wenn wir ASK und PSK kombinieren? Wir nennen dieses Modulationsverfahren Quadraturamplitudenmodulation (QAM). QAM sieht normalerweise ungefähr so aus: + +.. image:: ../_images/64qam.png + :scale: 90 % + :align: center + :alt: Example of Quadrature Amplitude Modulation (QAM) on the IQ or constellation plot + +Hier sind einige weitere Beispiele für QAM: + +.. image:: ../_images/qam.png + :scale: 50 % + :align: center + :alt: Example of 16QAM, 32QAM, 64QAM, and 256QAM on the IQ or constellation plot + +Bei einem QAM-Modulationsverfahren können wir technisch gesehen Punkte überall im IQ-Diagramm platzieren, da sowohl Phase *als auch* Amplitude moduliert werden. Die „Parameter" eines gegebenen QAM-Verfahrens sind am besten durch die QAM-Konstellation definiert. Alternativ kannst du die I- und Q-Werte für jeden Punkt auflisten, wie unten für QPSK: + +.. image:: ../_images/qpsk_list.png + :scale: 80 % + :align: center + :alt: Constellation or IQ plots can also be represented using a table of symbols + +Beachte, dass die meisten Modulationsverfahren, außer den verschiedenen ASKs und BPSK, im Zeitbereich ziemlich schwer zu „sehen" sind. Um meinen Punkt zu beweisen, hier ist ein Beispiel von QAM im Zeitbereich. Kannst du die Phase jedes Symbols im folgenden Bild unterscheiden? Es ist schwierig. + +.. image:: ../_images/qam_time_domain.png + :scale: 50 % + :align: center + :alt: Looking at QAM in the time domain is difficult which is why we use constellation or IQ plots + +Angesichts der Schwierigkeit, Modulationsverfahren im Zeitbereich zu unterscheiden, bevorzugen wir IQ-Diagramme gegenüber der Anzeige des Zeitbereichssignals. Wir könnten dennoch das Zeitbereichssignal zeigen, wenn es eine bestimmte Paketstruktur gibt oder die Abfolge der Symbole wichtig ist. + +**************************** +Frequenzumtastung (FSK) +**************************** + +Das Letzte auf der Liste ist Frequenzumtastung (FSK, Frequency Shift Keying). FSK ist ziemlich einfach zu verstehen – wir wechseln einfach zwischen N Frequenzen, wobei jede Frequenz ein mögliches Symbol ist. Da wir jedoch einen Träger modulieren, sind es wirklich unsere Trägerfrequenz +/- diese N Frequenzen. Z.B. könnten wir bei einem Träger von 1,2 GHz zwischen diesen vier Frequenzen wechseln: + +1. 1,2001 GHz +2. 1,2003 GHz +3. 1,1999 GHz +4. 1,1997 GHz + +Das obige Beispiel wäre 4-FSK, also gäbe es zwei Bit pro Symbol. Der Frequenzabstand beträgt 200 kHz, und das Gesamtsignal würde etwas mehr als 600 kHz umspannen. Dieses 4-FSK-Signal im Frequenzbereich beim Basisband könnte beim Durchführen einer FFT über viele Symbole ungefähr so aussehen: + +.. image:: ../_images/fsk.svg + :align: center + :target: ../_images/fsk.svg + :alt: Example of Frequency Shift Keying (FSK), specifically 4FSK + +Wenn du FSK verwendest, musst du eine kritische Frage stellen: Wie groß soll der Abstand zwischen den Frequenzen sein? Wir bezeichnen diesen Abstand oft als :math:`\Delta f` in Hz. Wir wollen Überlappungen im Frequenzbereich vermeiden, damit der Empfänger weiß, welche Frequenz ein bestimmtes Symbol verwendet hat, daher muss :math:`\Delta f` groß genug sein. Die Breite jedes Trägers in der Frequenz ist eine Funktion unserer Symbolrate und eines angewendeten Pulsformungsfilters. Mehr Symbole pro Sekunde bedeutet kürzere Symbole, was breitere Bandbreite bedeutet (erinnere dich an die inverse Beziehung zwischen Zeit- und Frequenzskalierung). Je schneller wir Symbole übertragen, desto breiter wird jeder Träger, und folglich müssen wir :math:`\Delta f` größer machen, um überlappende Träger zu vermeiden. + +IQ-Diagramme können nicht verwendet werden, um verschiedene Frequenzen zu zeigen. Sie zeigen Magnitude und Phase. +Obwohl es möglich ist, FSK im Zeitbereich zu zeigen, macht es mehr als 2 Frequenzen schwierig, zwischen Symbolen zu unterscheiden: + +.. image:: ../_images/fsk2.svg + :align: center + :target: ../_images/fsk2.svg + :alt: Frequency Shift Keying (FSK) or 2FSK in the time domain + +Nebenbei bemerkt, beachte, dass UKW-Radio Frequenzmodulation (FM) verwendet, die wie eine analoge Version von FSK ist. Anstatt zwischen diskreten Frequenzen zu wechseln, verwendet UKW-Radio ein kontinuierliches Audiosignal, um die Frequenz des Trägers zu modulieren. Unten ist ein Beispiel für FM- und AM-Modulation, bei dem das „Signal" oben das Audiosignal ist, das auf den Träger moduliert wird. + +.. image:: ../_images/am_fm_animation.gif + :align: center + :scale: 75 % + :target: ../_images/am_fm_animation.gif + :alt: Animation of a carrier, amplitude modulation (AM), and frequency modulation (FM) in the time domain + +In diesem Lehrbuch beschäftigen wir uns hauptsächlich mit digitalen Formen der Modulation. + +******************* +Differenzialkodierung +******************* + +In vielen drahtlosen (und kabelgebundenen) Kommunikationsprotokollen auf Basis von PSK oder QAM wirst du wahrscheinlich auf einen Schritt stoßen, der direkt vor der Bitmodulation (oder direkt nach der Demodulation) stattfindet, genannt Differenzialkodierung. Um ihren Nutzen zu demonstrieren, betrachte den Empfang eines BPSK-Signals. Wenn das Signal durch die Luft fliegt, erfährt es eine gewisse zufällige Verzögerung zwischen Sender und Empfänger, was eine zufällige Rotation in der Konstellation verursacht, wie wir zuvor erwähnt haben. Wenn der Empfänger sich darauf synchronisiert und das BPSK auf die „I"-Achse (real) ausrichtet, hat er keine Möglichkeit zu wissen, ob es 180 Grad außer Phase ist oder nicht, weil die Konstellation symmetrisch ist. Eine Option ist, Symbole zu übertragen, deren Wert dem Empfänger im Voraus bekannt ist, die mit den Informationen gemischt werden, bekannt als Pilotsymbole. Der Empfänger kann diese bekannten Symbole verwenden, um zu bestimmen, welcher Cluster eine 1 oder 0 ist, im Fall von BPSK. Pilotsymbole müssen in einem bestimmten Zeitraum gesendet werden, der damit zusammenhängt, wie schnell sich der drahtlose Kanal ändert, was letztendlich die Datenrate reduziert. Anstatt Pilotsymbole in die übertragene Wellenform mischen zu müssen, können wir wählen, Differenzialkodierung zu verwenden. + +Der einfachste Fall der Differenzialkodierung ist bei der Verwendung neben BPSK, was ein Bit pro Symbol umfasst. Anstatt einfach eine 1 für binär 1 und eine -1 für binär 0 zu übertragen, beinhaltet BPSK-Differenzialkodierung das Übertragen einer 0, wenn das Eingangsbit dasselbe ist wie die **Kodierung** des vorherigen Bits (nicht das vorherige Eingangsbit selbst), und das Übertragen einer 1, wenn es sich unterscheidet. Wir übertragen immer noch dieselbe Anzahl von Bits, abgesehen von einem zusätzlichen Bit, das am Anfang benötigt wird, um die Ausgangssequenz zu starten, aber jetzt müssen wir uns keine Sorgen über die 180-Grad-Phasenmehrdeutigkeit machen. Dieses Kodierungsschema kann mit der folgenden Gleichung beschrieben werden, wobei :math:`x` die Eingangsbits und :math:`y` die Ausgangsbits sind, die mit BPSK moduliert werden: + +.. math:: + y_i = y_{i-1} \oplus x_i + +Da der Ausgang auf dem Ausgang des vorherigen Schritts basiert, müssen wir den Ausgang mit einer willkürlichen 1 oder 0 beginnen, und wie wir während des Dekodierungsprozesses zeigen werden, spielt es keine Rolle, welche wir wählen (wir müssen dieses Startsymbol trotzdem übertragen!). + +Für visuelle Lernende kann der Differenzialkodierungsprozess als Diagramm dargestellt werden, wobei der Verzögerungsblock eine Verzögerung-um-1-Operation ist: + +.. image:: ../_images/differential_coding2.svg + :align: center + :target: ../_images/differential_coding2.svg + :alt: Differential coding block diagram + +Als Kodierungsbeispiel betrachte das Übertragen der 10 Bits [1, 1, 0, 0, 1, 1, 1, 1, 1, 0] mit BPSK. Nehmen wir an, wir starten die Ausgangssequenz mit 1; es spielt tatsächlich keine Rolle, ob du 1 oder 0 verwendest. Es hilft, die Bits übereinander zu zeigen und dabei darauf zu achten, den Eingang zu verschieben, um Platz für das Startausgangsbit zu machen: + +.. code-block:: + + Eingang: 1 1 0 0 1 1 1 1 1 0 + Ausgang: 1 + +Als nächstes baust du den Ausgang auf, indem du das Eingangsbit mit dem vorherigen **Ausgangs**-Bit vergleichst und die XOR-Operation aus der obigen Tabelle anwendest. Das nächste Ausgangsbit ist daher eine 0, weil 1 und 1 übereinstimmen: + +.. code-block:: + + Eingang: 1 1 0 0 1 1 1 1 1 0 + Ausgang: 1 0 + +Wiederhole für den Rest und du erhältst: + +.. code-block:: + + Eingang: 1 1 0 0 1 1 1 1 1 0 + Ausgang: 1 0 1 1 1 0 1 0 1 0 0 + +Nach der Anwendung der Differenzialkodierung würden wir letztendlich [1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0] übertragen. Die Einsen und Nullen werden immer noch auf die positiven und negativen Symbole abgebildet, die wir zuvor besprochen haben. + +Der Dekodierungsprozess, der am Empfänger stattfindet, vergleicht das empfangene Bit mit dem vorherigen **empfangenen** Bit, was viel einfacher zu verstehen ist: + +.. math:: + x_i = y_i \oplus y_{i-1} + +Wenn du die BPSK-Symbole [1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0] empfangen würdest, würdest du von links beginnen und prüfen, ob die ersten beiden übereinstimmen; in diesem Fall tun sie es nicht, also ist das erste Bit eine 1. Wiederhole und du erhältst die Sequenz, mit der wir begonnen haben, [1, 1, 0, 0, 1, 1, 1, 1, 1, 0]. Es mag nicht offensichtlich sein, aber das Startbit, das wir hinzugefügt haben, hätte eine 1 oder eine 0 sein können und wir würden dasselbe Ergebnis erhalten. + +Der Kodierungs- und Dekodierungsprozess ist in der folgenden Grafik zusammengefasst: + +.. image:: ../_images/differential_coding.svg + :align: center + :target: ../_images/differential_coding.svg + :alt: Demonstration of differential coding using sequence of encoded and decoded bits + + +Der große Nachteil der Verwendung von Differenzialkodierung ist, dass ein Bitfehler zu zwei Bitfehlern führt. Die Alternative zur Verwendung von Differenzialkodierung für BPSK ist das periodische Hinzufügen von Pilotsymbolen, wie zuvor besprochen, die auch verwendet werden können, um Mehrwegausbreitung durch den Kanal umzukehren/zu invertieren. Aber ein Problem mit Pilotsymbolen ist, dass sich der drahtlose Kanal sehr schnell ändern kann, in der Größenordnung von Zehnten oder Hunderten von Symbolen, wenn es ein beweglicher Empfänger und/oder Sender ist, sodass du Pilotsymbole oft genug benötigen würdest, um den sich ändernden Kanal widerzuspiegeln. Wenn also ein drahtloses Protokoll großen Wert auf die Reduzierung der Komplexität des Empfängers legt, wie RDS, das wir im Kapitel :ref:`rds-chapter` studieren, kann es sich für die Verwendung von Differenzialkodierung entscheiden. + +Denk daran, dass das obige Differenzialkodierungsbeispiel spezifisch für BPSK war. Differenzialkodierung wird auf Symbolebene angewendet, also arbeitest du bei der Anwendung auf QPSK mit Paaren von Bits gleichzeitig, und so weiter für höhere QAM-Schemata. Differenzielles QPSK wird oft als DQPSK bezeichnet. + +******************* +Python-Beispiel +******************* + +Als kurzes Python-Beispiel lass uns QPSK im Basisband erzeugen und die Konstellation darstellen. + +Obwohl wir die komplexen Symbole direkt erzeugen könnten, fangen wir mit dem Wissen an, dass QPSK vier Symbole in 90-Grad-Abständen um den Einheitskreis hat. Wir werden 45, 135, 225 und 315 Grad für unsere Punkte verwenden. Zuerst erzeugen wir zufällige Zahlen zwischen 0 und 3 und führen Mathematik durch, um die gewünschten Grad zu erhalten, bevor wir in Radiant umrechnen. + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + + num_symbols = 1000 + + x_int = np.random.randint(0, 4, num_symbols) # 0 bis 3 + x_degrees = x_int*360/4.0 + 45 # 45, 135, 225, 315 Grad + x_radians = x_degrees*np.pi/180.0 # sin() und cos() nehmen Radiant + x_symbols = np.cos(x_radians) + 1j*np.sin(x_radians) # erzeugt unsere QPSK-Komplexsymbole + plt.plot(np.real(x_symbols), np.imag(x_symbols), '.') + plt.grid(True) + plt.show() + +.. image:: ../_images/qpsk_python.svg + :align: center + :target: ../_images/qpsk_python.svg + :alt: QPSK generated or simulated in Python + +Beachte, wie alle von uns erzeugten Symbole überlappen. Es gibt kein Rauschen, sodass alle Symbole denselben Wert haben. Lass uns etwas Rauschen hinzufügen: + +.. code-block:: python + + n = (np.random.randn(num_symbols) + 1j*np.random.randn(num_symbols))/np.sqrt(2) # AWGN mit Einheitsleistung + noise_power = 0.01 + r = x_symbols + n * np.sqrt(noise_power) + plt.plot(np.real(r), np.imag(r), '.') + plt.grid(True) + plt.show() + +.. image:: ../_images/qpsk_python2.svg + :align: center + :target: ../_images/qpsk_python2.svg + :alt: QPSK with AWGN noise generated or simulated in Python + +Beachte, wie additives weißes Gauß'sches Rauschen (AWGN) eine gleichmäßige Streuung um jeden Punkt in der Konstellation erzeugt. Wenn es zu viel Rauschen gibt, beginnen Symbole, die Grenze (die vier Quadranten) zu überschreiten, und werden vom Empfänger als falsches Symbol interpretiert. Versuche, :code:`noise_power` zu erhöhen, bis das passiert. + +Für diejenigen, die daran interessiert sind, Phasenrauschen zu simulieren, das aus Phasenjitter innerhalb des lokalen Oszillators (LO) resultieren könnte, ersetze :code:`r` durch: + +.. code-block:: python + + phase_noise = np.random.randn(len(x_symbols)) * 0.1 # Multiplikator für „Stärke" des Phasenrauschens anpassen + r = x_symbols * np.exp(1j*phase_noise) + +.. image:: ../_images/phase_jitter.svg + :align: center + :target: ../_images/phase_jitter.svg + :alt: QPSK with phase jitter generated or simulated in Python + +Du könntest sogar Phasenrauschen mit AWGN kombinieren, um die volle Erfahrung zu machen: + +.. image:: ../_images/phase_jitter_awgn.svg + :align: center + :target: ../_images/phase_jitter_awgn.svg + :alt: QPSK with AWGN noise and phase jitter generated or simulated in Python + +Wir hören an diesem Punkt auf. Wenn wir sehen wollten, wie das QPSK-Signal im Zeitbereich aussieht, müssten wir mehrere Samples pro Symbol erzeugen (in dieser Übung haben wir nur 1 Sample pro Symbol gemacht). Du wirst lernen, warum du mehrere Samples pro Symbol erzeugen musst, sobald wir die Pulsformung besprechen. Das Python-Beispiel im Kapitel :ref:`pulse-shaping-chapter` wird dort weitermachen, wo wir hier aufgehört haben. + +******************* +Weiterführende Literatur +******************* + +#. https://en.wikipedia.org/wiki/Differential_coding diff --git a/content-de/doa.rst b/content-de/doa.rst new file mode 100644 index 00000000..08ee5afd --- /dev/null +++ b/content-de/doa.rst @@ -0,0 +1,1298 @@ +.. _doa-chapter: + +################# +Beamforming & DOA +################# + +In diesem Kapitel behandeln wir die Konzepte Beamforming, Ankunftsrichtungsschätzung (DOA) und Phased Arrays im Allgemeinen. Wir vergleichen verschiedene Array-Typen und -Geometrien und zeigen, welche entscheidende Rolle der Elementabstand spielt. Techniken wie MVDR/Capon und MUSIC werden eingeführt und anhand von Python-Simulationsbeispielen demonstriert. + +********************* +Übersicht: Beamforming +********************* + +Ein Phased Array, auch als elektronisch geschwenktes Array bezeichnet, ist ein Array bzw. eine Sammlung von Antennen, das auf der Sende- oder Empfangsseite in Kommunikations- und Radarsystemen eingesetzt werden kann. Phased Arrays findet man in bodengestützten Systemen, in der Luft und auf Satelliten. Die einzelnen Antennen eines Arrays bezeichnen wir üblicherweise als Elemente, und manchmal wird das Array auch als „Sensor" bezeichnet. Diese Array-Elemente sind meist omnidirektionale Antennen, die gleichmäßig in einer Linie oder über zwei Dimensionen verteilt sind. + +Beamforming ist eine Signalverarbeitungsoperation, die mit Antennenarrays eingesetzt wird, um einen *räumlichen* Filter zu erzeugen – er filtert Signale aus allen Richtungen außer der gewünschten Richtung(en) heraus. Beamforming kann eingesetzt werden, um den SNR von Nutzsignalen zu erhöhen, Störsender zu unterdrücken, Strahlmuster zu formen oder sogar mehrere Datenströme gleichzeitig auf derselben Frequenz zu senden/empfangen. Als Teil des Beamformings verwenden wir Gewichte (auch Koeffizienten genannt), die auf jedes Element des Arrays angewendet werden, entweder digital oder in analoger Schaltung. Wir manipulieren die Gewichte, um die Strahlen des Arrays zu formen – daher der Name Beamforming! Wir können diese Strahlen (und Nullstellen) extrem schnell schwenken – viel schneller als mechanisch geschwenkte Antennen, die als Alternative zu Phased Arrays angesehen werden können. Beamforming wird typischerweise im Kontext einer Kommunikationsverbindung diskutiert, bei der der Empfänger versucht, ein oder mehrere Signale mit dem bestmöglichen SNR zu empfangen. Arrays spielen auch eine wichtige Rolle im Radar, wo das Ziel die Erkennung und Verfolgung von Zielen ist. + +.. image:: ../_images/doa_complex_scenario.svg + :align: center + :target: ../_images/doa_complex_scenario.svg + :alt: Diagramm eines komplexen Szenarios mit mehreren Signalen, die auf ein Array treffen + +Beamforming-Ansätze lassen sich in drei Kategorien unterteilen: konventionell, adaptiv und blind. Konventionelles Beamforming ist am nützlichsten, wenn die Ankunftsrichtung des Nutzsignals bereits bekannt ist; dabei werden Gewichte gewählt, die den Array-Gewinn in dieser Richtung maximieren. Dies kann sowohl auf der Empfangs- als auch auf der Sendeseite eines Kommunikationssystems eingesetzt werden. Adaptives Beamforming hingegen passt die Gewichte typischerweise anhand des Eingangssignals des Beamformers an, um ein bestimmtes Kriterium zu optimieren (z. B. einen Störsender zu unterdrücken, mehrere Hauptkeulen zu erzeugen usw.). Aufgrund seiner geschlossenen Regelschleife wird adaptives Beamforming typischerweise nur auf der Empfangsseite eingesetzt; das „Eingangssignal des Beamformers" ist dabei einfach das empfangene Signal, und adaptives Beamforming passt die Gewichte anhand der Statistiken dieser empfangenen Daten an. + +Die folgende Taxonomie versucht, die vielen Bereiche des Beamformings zu kategorisieren und zeigt Beispieltechniken: + +.. image:: ../_images/beamforming_taxonomy.svg + :align: center + :target: ../_images/beamforming_taxonomy.svg + :alt: Eine Beamforming-Taxonomie, die Beamforming in konventionell, adaptiv und blind kategorisiert sowie zeigt, wie DOA-Schätzung einzuordnen ist + +****************************** +Übersicht: Ankunftsrichtung (DOA) +****************************** + +Direction-of-Arrival (DOA) bezeichnet im DSP/SDR-Bereich den Prozess, ein Antennenarray zu verwenden, um die Ankunftsrichtungen eines oder mehrerer vom Array empfangener Signale zu erkennen und zu schätzen (im Gegensatz zum Beamforming, das sich auf den Empfang eines Signals bei gleichzeitiger Unterdrückung von Rauschen und Interferenz konzentriert). Obwohl DOA eindeutig unter das Thema Beamforming fällt, können die beiden Begriffe verwechselt werden. Einige Techniken wie konventionelles Beamforming und MVDR können sowohl für DOA als auch für Beamforming verwendet werden, da dieselbe Technik, die für Beamforming verwendet wird, auch für DOA eingesetzt wird: Man schwenkt den Winkel über alle Interessenwinkel und führt die Beamforming-Operation bei jedem Winkel durch, sucht dann nach Peaks im Ergebnis (jeder Peak ist ein Signal, aber wir wissen nicht, ob es das Nutzsignal, ein Störsender oder sogar eine Mehrwegreflexion des Nutzsignals ist). Man kann diese DOA-Techniken als einen Wrapper um einen bestimmten Beamformer betrachten. Andere Beamformer können nicht einfach in eine DOA-Routine eingebettet werden, z. B. aufgrund zusätzlicher Eingaben, die im DOA-Kontext nicht verfügbar sind. Es gibt auch DOA-Techniken wie MUSIC und ESPRIT, die ausschließlich für DOA vorgesehen sind und keine Beamformer sind. Da die meisten Beamforming-Techniken voraussetzen, dass der Ankunftswinkel des Nutzsignals bekannt ist, muss DOA kontinuierlich als Zwischenschritt durchgeführt werden, wenn sich das Ziel oder das Array bewegt – selbst wenn das primäre Ziel das Empfangen und Demodulieren des Nutzsignals ist. + +Phased Arrays und Beamforming/DOA finden Anwendung in verschiedensten Bereichen, am häufigsten jedoch in verschiedenen Formen des Radars, neueren WLAN-Standards, mmWave-Kommunikation in 5G, Satellitenkommunikation und Störsendung (Jamming). Im Allgemeinen eignen sich alle Anwendungen, die eine Antenne mit hohem Gewinn oder eine schnell bewegliche Hochgewinn-Antenne erfordern, gut für Phased Arrays. + +****************** +Array-Typen +****************** + +Phased Arrays lassen sich in drei Typen unterteilen: + +1. **Analog**, auch als passiv elektronisch geschwenktes Array (PESA) oder traditionelles Phased Array bezeichnet, wobei analoge Phasenschieber zur Strahlsteuerung verwendet werden. Auf der Empfangsseite werden alle Elemente nach der Phasenverschiebung (und optional einer einstellbaren Verstärkung) summiert und in ein einzelnes Signal umgewandelt, das abwärtsgemischt und empfangen wird. Auf der Sendeseite läuft der Prozess umgekehrt ab: Ein einzelnes digitales Signal wird ausgegeben, und auf der Analogseite werden Phasenschieber und Verstärkungsstufen verwendet, um das Signal für jede Antenne zu erzeugen. Diese digitalen Phasenschieber haben eine begrenzte Bit-Auflösung und eine Steuerlatenz. +2. **Digital**, auch als aktiv elektronisch geschwenktes Array (AESA) bezeichnet, bei dem jedes einzelne Element sein eigenes HF-Frontend hat und das Beamforming vollständig im digitalen Bereich erfolgt. Dies ist der teuerste Ansatz, da HF-Komponenten teuer sind, bietet jedoch viel mehr Flexibilität und Geschwindigkeit als PESAs. Digitale Arrays sind bei SDRs beliebt, obwohl die Anzahl der Empfangs- oder Sendekanäle des SDR die Anzahl der Elemente im Array begrenzt. +3. **Hybrid**, bei dem das Array aus vielen Subarrays besteht, die einzeln einem analogen Array ähneln, wobei jedes Subarray ein eigenes HF-Frontend hat, genau wie bei digitalen Arrays. Dies ist der häufigste Ansatz für moderne Phased Arrays, da er das Beste aus beiden Welten bietet. + +Beachte, dass die Begriffe PESA und AESA hauptsächlich im Radarkontext verwendet werden und es gewisse Unklarheiten darüber gibt, was genau ein PESA oder AESA ausmacht. Daher ist die Verwendung der Begriffe analog/digital/hybrid-Array klarer und kann auf jede Art von Anwendung angewendet werden. + +Ein reales Beispiel für jeden Typ ist unten dargestellt: + +.. image:: ../_images/beamforming_examples.svg + :align: center + :target: ../_images/beamforming_examples.svg + :alt: Beispiele für Phased Arrays, einschließlich PESA, AESA und Hybrid-Array + +Neben diesen drei Typen gibt es auch die Geometrie des Arrays. Die einfachste Geometrie ist das uniforme lineare Array (ULA), bei dem die Antennen in einer geraden Linie mit gleichem Abstand angeordnet sind (d. h. in 1 Dimension). ULAs haben eine 180-Grad-Mehrdeutigkeit, auf die wir später eingehen, und eine Lösung besteht darin, die Antennen in einem Kreis anzuordnen, was wir als uniformes kreisförmiges Array (UCA) bezeichnen. Für 2D-Strahlen verwenden wir üblicherweise ein uniformes rechteckiges Array (URA), bei dem die Antennen in einem Rastermuster angeordnet sind. + +In diesem Kapitel konzentrieren wir uns auf digitale Arrays, da sie besser für Simulation und DSP geeignet sind, aber die Konzepte übertragen sich auf analoge und hybride Arrays. Im nächsten Kapitel arbeiten wir praktisch mit dem „Phaser"-SDR von Analog Devices, das ein 10-GHz-8-Element-Analog-Array mit Phasen- und Verstärkungsstellern hat, das mit einem Pluto und einem Raspberry Pi verbunden ist. Wir konzentrieren uns auch auf die ULA-Geometrie, da sie die einfachste Mathematik und den einfachsten Code bietet, aber alle Konzepte übertragen sich auf andere Geometrien, und am Ende des Kapitels berühren wir kurz das UCA. + +******************* +SDR-Anforderungen +******************* + +Analoge Phased Arrays beinhalten einen Phasenschieber (und oft eine einstellbare Verstärkungsstufe) pro Kanal/Element, der in analoger HF-Schaltung implementiert ist. Das bedeutet, dass ein analoges Phased Array ein dediziertes Hardwarestück ist, das neben einem SDR verwendet werden muss oder für eine bestimmte Anwendung speziell gebaut wurde. Andererseits kann jedes SDR, das mehr als einen Kanal enthält, ohne zusätzliche Hardware als digitales Array verwendet werden, solange die Kanäle phasenkohärent sind und mit demselben Takt abgetastet werden, was typischerweise bei SDRs mit mehreren Empfangskanälen der Fall ist. Es gibt viele SDRs mit **zwei** Empfangskanälen, wie das Ettus USRP B210 und Analog Devices Pluto (der 2. Kanal ist über einen uFL-Stecker auf der Platine zugänglich). Leider erfordert das Überschreiten von zwei Kanälen den Einstieg in das SDR-Segment über 10.000 USD, zumindest Stand 2023, wie das Ettus USRP N310 oder das Analog Devices QuadMXFE (16 Kanäle). Die Hauptherausforderung besteht darin, dass kostengünstige SDRs typischerweise nicht „zusammengekettet" werden können, um die Anzahl der Kanäle zu skalieren. Ausnahmen sind das KerberosSDR (4 Kanäle) und das KrakenSDR (5 Kanäle), die mehrere RTL-SDRs mit einem gemeinsamen LO verwenden, um ein kostengünstiges digitales Array zu bilden; der Nachteil ist die sehr begrenzte Abtastrate (bis zu 2,56 MHz) und der Abstimmbereich (bis zu 1766 MHz). Die KrakenSDR-Platine und eine Beispiel-Antennenkonfiguration sind unten dargestellt. + +.. image:: ../_images/krakensdr.jpg + :align: center + :alt: Das KrakenSDR + :target: ../_images/krakensdr.jpg + +In diesem Kapitel verwenden wir keine spezifischen SDRs; stattdessen simulieren wir den Empfang von Signalen mit Python und gehen dann durch die DSP-Verarbeitung für Beamforming/DOA bei digitalen Arrays. + +************************************** +Einführung in Matrizenrechnung mit Python/NumPy +************************************** + +Python hat viele Vorteile gegenüber MATLAB, wie z. B. kostenlos und Open-Source zu sein, Vielfalt der Anwendungen, lebendige Community, Indizes beginnen bei 0 wie jede andere Sprache, Verwendung in KI/ML, und es scheint eine Bibliothek für alles zu geben, was man sich vorstellen kann. Wo es jedoch schwächer ist, ist die Art und Weise, wie Matrizenmanipulation kodiert/dargestellt wird (rechnerisch/geschwindigkeitsmäßig ist es durchaus schnell, wobei Funktionen intern effizient in C/C++ implementiert sind). Es hilft nicht, dass es mehrere Möglichkeiten gibt, Matrizen in Python darzustellen, wobei die :code:`np.matrix`-Methode zugunsten von :code:`np.ndarray` veraltet ist. In diesem Abschnitt geben wir eine kurze Einführung in Matrizenrechnung in Python mit NumPy, damit du bei den DOA-Beispielen komfortabler bist. + +Beginnen wir mit dem lästigsten Teil der Matrizenrechnung in NumPy: Vektoren werden als 1D-Arrays behandelt, sodass es keine Möglichkeit gibt, zwischen einem Zeilen- und einem Spaltenvektor zu unterscheiden (er wird standardmäßig als Zeilenvektor behandelt), während ein Vektor in MATLAB ein 2D-Objekt ist. In Python kannst du einen neuen Vektor mit :code:`a = np.array([2,3,4,5])` erstellen oder eine Liste in einen Vektor umwandeln mit :code:`mylist = [2, 3, 4, 5]` dann :code:`a = np.asarray(mylist)`, aber sobald du Matrizenrechnung durchführen möchtest, spielt die Ausrichtung eine Rolle, und diese werden als Zeilenvektoren interpretiert. Ein Transponieren dieses Vektors, z. B. mit :code:`a.T`, ändert ihn **nicht** in einen Spaltenvektor! Die Möglichkeit, aus einem normalen Vektor :code:`a` einen Spaltenvektor zu machen, ist :code:`a = a.reshape(-1,1)`. Die :code:`-1` weist NumPy an, die Größe dieser Dimension automatisch zu bestimmen, während die zweite Dimension die Länge 1 behält. Was dadurch entsteht, ist technisch gesehen ein 2D-Array, aber die zweite Dimension hat die Länge 1, sodass es aus mathematischer Sicht immer noch im Wesentlichen 1D ist. Es ist nur eine extra Zeile, aber sie kann den Fluss von Matrizenrechencode wirklich stören. + +Nun ein kurzes Beispiel für Matrizenrechnung in Python; wir multiplizieren eine :code:`3x10`-Matrix mit einer :code:`10x1`-Matrix. Erinnere daran, dass :code:`10x1` 10 Zeilen und 1 Spalte bedeutet, bekannt als Spaltenvektor, weil es nur eine Spalte ist. Aus unserer frühen Schulzeit wissen wir, dass dies eine gültige Matrizenmultiplikation ist, da die inneren Dimensionen übereinstimmen, und die resultierende Matrixgröße die äußeren Dimensionen sind, also :code:`3x1`. Wir verwenden :code:`np.random.randn()` zur Erstellung der :code:`3x10`-Matrix und :code:`np.arange()` zur Erstellung der :code:`10x1`-Matrix: + +.. code-block:: python + + A = np.random.randn(3,10) # 3x10 + B = np.arange(10) # 1D-Array der Länge 10 + B = B.reshape(-1,1) # 10x1 + C = A @ B # Matrizenmultiplikation + print(C.shape) # 3x1 + C = C.squeeze() # siehe nächsten Abschnitt + print(C.shape) # 1D-Array der Länge 3, einfacher zum Plotten und anderem Nicht-Matrix-Python-Code + +Nach der Matrizenrechnung kann das Ergebnis so aussehen: :code:`[[ 0. 0.125 0.251 -0.376 -0.251 ...]]`, was eindeutig nur eine Datendimension hat, aber wenn du es plotten möchtest, erhältst du entweder einen Fehler oder einen Plot, der nichts anzeigt. Das liegt daran, dass das Ergebnis technisch gesehen ein 2D-Array ist und du es mit :code:`a.squeeze()` in ein 1D-Array umwandeln musst. Die :code:`squeeze()`-Funktion entfernt alle Dimensionen der Länge 1 und ist praktisch bei der Matrizenrechnung in Python. Im oben genannten Beispiel wäre das Ergebnis :code:`[ 0. 0.125 0.251 -0.376 -0.251 ...]` (beachte die fehlenden zweiten Klammern), was geplottet oder in anderem Python-Code verwendet werden kann, der etwas 1D erwartet. + +Beim Kodieren von Matrizenrechnung ist die beste Plausibilitätsprüfung, die du durchführen kannst, die Ausgabe der Dimensionen (mit :code:`A.shape`), um zu überprüfen, ob sie deinen Erwartungen entsprechen. Erwäge, die Form in die Kommentare nach jeder Zeile einzufügen, als zukünftige Referenz und um sicherzustellen, dass Dimensionen bei Matrix- oder elementweiser Multiplikation übereinstimmen. + +Hier sind einige häufige Operationen in MATLAB und Python als eine Art Spickzettel: + +.. list-table:: + :widths: 35 25 40 + :header-rows: 1 + + * - Operation + - MATLAB + - Python/NumPy + * - Zeilenvektor erstellen, Größe :code:`1 x 4` + - :code:`a = [2 3 4 5];` + - :code:`a = np.array([2,3,4,5])` + * - Spaltenvektor erstellen, Größe :code:`4 x 1` + - :code:`a = [2; 3; 4; 5];` oder :code:`a = [2 3 4 5].'` + - :code:`a = np.array([[2],[3],[4],[5]])` oder |br| :code:`a = np.array([2,3,4,5])` dann |br| :code:`a = a.reshape(-1,1)` + * - 2D-Matrix erstellen + - :code:`A = [1 2; 3 4; 5 6];` + - :code:`A = np.array([[1,2],[3,4],[5,6]])` + * - Größe bestimmen + - :code:`size(A)` + - :code:`A.shape` + * - Transponieren, auch :math:`A^T` + - :code:`A.'` + - :code:`A.T` + * - Konjugiert komplexes Transponieren |br| auch Hermitesches Transponieren |br| auch :math:`A^H` + - :code:`A'` + - :code:`A.conj().T` |br| |br| (leider gibt es kein :code:`A.H` für ndarrays) + * - Elementweise Multiplikation + - :code:`A .* B` + - :code:`A * B` oder :code:`np.multiply(a,b)` + * - Matrizenmultiplikation + - :code:`A * B` + - :code:`A @ B` oder :code:`np.matmul(A,B)` + * - Skalarprodukt zweier Vektoren (1D) + - :code:`dot(a,b)` + - :code:`np.dot(a,b)` (np.dot nie für 2D verwenden) + * - Verketten + - :code:`[A A]` + - :code:`np.concatenate((A,A))` + +********************* +Steuervektor +********************* + +Um zum interessanten Teil zu kommen, müssen wir etwas Mathematik durcharbeiten, aber der folgende Abschnitt wurde so geschrieben, dass die Mathematik relativ unkompliziert ist und Diagramme dazu gibt; es werden nur grundlegendste Trigonometrie- und Exponentialeigenschaften verwendet. Es ist wichtig, die grundlegende Mathematik hinter dem zu verstehen, was wir in Python zur DOA-Berechnung tun werden. + +Betrachte ein eindimensionales, gleichmäßig verteiltes Array mit drei Elementen: + +.. image:: ../_images/doa.svg + :align: center + :target: ../_images/doa.svg + :alt: Diagramm zur Ankunftsrichtung (DOA) eines Signals auf einem gleichmäßig verteilten Antennenarray, mit Boresight-Winkel und Abstand zwischen Elementen + +In diesem Beispiel kommt ein Signal von der rechten Seite, trifft also zuerst das rechteste Element. Berechnen wir die Verzögerung zwischen dem Zeitpunkt, an dem das Signal dieses erste Element trifft, und wann es das nächste Element erreicht. Dazu formulieren wir das folgende trigonometrische Problem. Das rot markierte Segment stellt die Strecke dar, die das Signal zurücklegen muss, *nachdem* es das erste Element erreicht hat, bevor es das nächste trifft. + +.. image:: ../_images/doa_trig.svg + :align: center + :target: ../_images/doa_trig.svg + :alt: Trigonometrie zur Ankunftsrichtung (DOA) eines gleichmäßig verteilten Arrays + +Wenn wir SOH CAH TOA anwenden, interessiert uns hier die „Ankathete" und wir haben die Länge der Hypotenuse (:math:`d`), daher benötigen wir einen Kosinus: + +.. math:: + \cos(90 - \theta) = \frac{\mathrm{Ankathete}}{\mathrm{Hypotenuse}} + +Wir müssen die Ankathete bestimmen, da diese angibt, wie weit das Signal zwischen dem ersten und zweiten Element zurücklegen muss: Ankathete :math:`= d \cos(90 - \theta)`. Eine trigonometrische Identität erlaubt es uns, dies in Ankathete :math:`= d \sin(\theta)` umzuwandeln. Dies ist jedoch nur eine Strecke; wir müssen sie in eine Zeit umrechnen, indem wir die Lichtgeschwindigkeit verwenden: vergangene Zeit :math:`= d \sin(\theta) / c` Sekunden. Diese Gleichung gilt zwischen zwei beliebigen benachbarten Elementen unseres Arrays, obwohl wir das Ganze mit einer ganzen Zahl multiplizieren können, um die Zeit zwischen nicht benachbarten Elementen zu berechnen, da sie gleichmäßig verteilt sind (das machen wir später). + +Verbinden wir jetzt diese Trigonometrie und Lichtgeschwindigkeitsmathematik mit der Signalverarbeitungswelt. Sei unser Sendesignal im Basisband :math:`x(t)`, das auf einem Träger :math:`f_c` übertragen wird, sodass das Sendesignal :math:`x(t) e^{2j \pi f_c t}` ist. Wir verwenden :math:`d_m` für den Antennenabstand in Metern. Angenommen, dieses Signal trifft das erste Element zum Zeitpunkt :math:`t = 0`, was bedeutet, dass es das nächste Element nach :math:`d_m \sin(\theta) / c` Sekunden trifft. Damit empfängt das 2. Element: + +.. math:: + x(t - \Delta t) e^{2j \pi f_c (t - \Delta t)} + +.. math:: + \mathrm{wobei} \quad \Delta t = d_m \sin(\theta) / c + +Zur Erinnerung: Eine Zeitverschiebung wird vom Zeitargument subtrahiert. + +Wenn der Empfänger oder SDR die Abwärtskonversion durchführt, um das Signal zu empfangen, multipliziert er es im Wesentlichen mit dem Träger in umgekehrter Richtung; nach der Abwärtskonversion sieht der Empfänger: + +.. math:: + x(t - \Delta t) e^{2j \pi f_c (t - \Delta t)} e^{-2j \pi f_c t} + +.. math:: + = x(t - \Delta t) e^{-2j \pi f_c \Delta t} + +Jetzt können wir einen kleinen Trick anwenden, um dies weiter zu vereinfachen: Wenn wir ein Signal abtasten, kann es modelliert werden, indem wir :math:`t` durch :math:`nT` ersetzen, wobei :math:`T` die Abtastperiode und :math:`n` einfach 0, 1, 2, 3... ist. Eingesetzt ergibt das :math:`x(nT - \Delta t) e^{-2j \pi f_c \Delta t}`. Da :math:`nT` so viel größer als :math:`\Delta t` ist, können wir den ersten :math:`\Delta t`-Term weglassen und erhalten :math:`x(nT) e^{-2j \pi f_c \Delta t}`. Falls die Abtastrate jemals schnell genug wird, um sich der Lichtgeschwindigkeit über eine winzige Distanz anzunähern, können wir dies überdenken, aber erinnere daran, dass die Abtastrate nur etwas größer als die Bandbreite des Signals von Interesse sein muss. + +Fahren wir mit dieser Mathematik fort, aber beginnen wir nun, Dinge in diskreten Begriffen darzustellen, damit sie unserem Python-Code besser ähneln. Die letzte Gleichung kann wie folgt dargestellt werden; setzen wir :math:`\Delta t` wieder ein: + +.. math:: + x[n] e^{-2j \pi f_c \Delta t} + +.. math:: + = x[n] e^{-2j \pi f_c d_m \sin(\theta) / c} + +Fast fertig, aber glücklicherweise gibt es noch eine weitere Vereinfachung. Erinnere dich an die Beziehung zwischen Mittenfrequenz und Wellenlänge: :math:`\lambda = \frac{c}{f_c}`, oder umgekehrt :math:`f_c = \frac{c}{\lambda}`. Eingesetzt ergibt das: + +.. math:: + x[n] e^{-2j \pi d_m \sin(\theta) / \lambda} + +Im angewandten Beamforming und DOA stellen wir :math:`d`, den Abstand zwischen benachbarten Elementen, als Bruchteil der Wellenlänge dar (statt in Metern). Der gängigste Wert für :math:`d` beim Array-Design ist die halbe Wellenlänge. Unabhängig vom Wert von :math:`d` stellen wir :math:`d` von nun an als Bruchteil der Wellenlänge dar, was die Gleichungen und unseren Code vereinfacht. D. h. :math:`d` (ohne den Index :math:`m`) steht für den normierten Abstand und ist gleich :math:`d = d_m / \lambda`. Damit können wir die obige Gleichung vereinfachen zu: + +.. math:: + x[n] e^{-2j \pi d \sin(\theta)} + +Die obige Gleichung gilt für benachbarte Elemente; für das Signal, das das :math:`k`-te Element empfängt, multiplizieren wir :math:`d` einfach mit :math:`k`: + +.. math:: + x[n] e^{-2j \pi d k \sin(\theta)} + +Betrachten wir nun die Koordinatenkonvention, die wir verwenden wollen. In diesem Lehrbuch stellt 0 Grad tangential zur Array-Platzierung dar (d. h. die Linie, auf der die Elemente liegen), wie im obigen Diagramm gezeigt, und Theta nimmt im Uhrzeigersinn zu. Wir betrachten auch das erste/Referenzelement als das linkste, und jedes weitere Element ist um eine Distanz :math:`d_m` weiter rechts. Dies ist umgekehrt zu unserem obigen Diagramm, daher müssen wir die Richtung der Phasenverschiebung umkehren, was bedeutet, das negative Vorzeichen zu entfernen: + +.. math:: + x[n] e^{2j \pi d k \sin(\theta)} + +Wir können dies in Matrixform darstellen, indem wir einfach die obige Gleichung für alle :code:`Nr` Elemente im Array von :math:`k = 0, 1, ... , N-1` anordnen: + +.. math:: + + x + \begin{bmatrix} + e^{2j \pi d (0) \sin(\theta)} \\ + e^{2j \pi d (1) \sin(\theta)} \\ + e^{2j \pi d (2) \sin(\theta)} \\ + \vdots \\ + e^{2j \pi d (N_r - 1) \sin(\theta)} \\ + \end{bmatrix} + +wobei :math:`x` der 1D-Zeilenvektor mit dem Sendesignal ist und der ausgeschriebene Spaltenvektor der sogenannte „Steuervektor" (oft als :math:`s` bezeichnet und im Code :code:`s`) ist. Weil :math:`e^{0} = 1`, ist das erste Element des Steuervektors immer 1, und der Rest sind Phasenverschiebungen relativ zum ersten Element: + +.. math:: + + s = + \begin{bmatrix} + 1 \\ + e^{2j \pi d (1) \sin(\theta)} \\ + e^{2j \pi d (2) \sin(\theta)} \\ + \vdots \\ + e^{2j \pi d (N_r - 1) \sin(\theta)} \\ + \end{bmatrix} + +Fertig! Dieser Vektor ist das, was du in DOA-Papieren und ULA-Implementierungen überall sehen wirst! Du wirst ihn auch mit :math:`2\pi\sin(\theta)` ausgedrückt als Symbol wie :math:`\psi` sehen; in diesem Fall wäre der Steuervektor einfach :math:`e^{jd\psi}`, was die allgemeinere Form ist (wir werden diese Form jedoch nicht verwenden). In Python ist :code:`s`: + +.. code-block:: python + + s = [np.exp(2j*np.pi*d*0*np.sin(theta)), np.exp(2j*np.pi*d*1*np.sin(theta)), np.exp(2j*np.pi*d*2*np.sin(theta)), ...] # beachte das steigende k + # oder + s = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(theta)) # wobei Nr die Anzahl der Empfangsantennenelemente ist + +Beachte, dass Element 0 zu 1+0j führt (weil :math:`e^{0}=1`); das ergibt Sinn, da alles oben relativ zu diesem ersten Element war, sodass es das Signal ohne relative Phasenverschiebungen empfängt. :code:`d` hat die Einheit Wellenlängen, nicht Meter! + +******************* +Ein Signal empfangen +******************* + +Verwenden wir das Steuervektor-Konzept, um ein auf einem Array eintreffendes Signal zu simulieren. Als Sendesignal verwenden wir zunächst einen Ton: + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + + sample_rate = 1e6 + N = 10000 # Anzahl der zu simulierenden Samples + + # Erstelle einen Ton als Sendersignal + t = np.arange(N)/sample_rate # Zeitvektor + f_tone = 0.02e6 + tx = np.exp(2j * np.pi * f_tone * t) + +Simulieren wir nun ein Array aus drei omnidirektionalen Antennen in einer Linie mit Halbwellenlängenabstand. Wir simulieren das Signal des Senders, das bei einem bestimmten Winkel theta an diesem Array ankommt: + +.. code-block:: python + + d = 0.5 # halber Wellenlängenabstand + Nr = 3 + theta_degrees = 20 # Ankunftswinkel (kann frei verändert werden) + theta = theta_degrees / 180 * np.pi # in Radiant umrechnen + s = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(theta)) # Steuervektor + print(s) # beachte: 3 Elemente lang, komplex, erstes Element ist 1+0j + +Um den Steuervektor anzuwenden, müssen wir eine Matrizenmultiplikation von :code:`s` und :code:`tx` durchführen. Zuerst konvertieren wir beide in 2D, indem wir :code:`ourarray.reshape(-1,1)` verwenden: + +.. code-block:: python + + s = s.reshape(-1,1) # s als Spaltenvektor + print(s.shape) # 3x1 + tx = tx.reshape(1,-1) # tx als Zeilenvektor + print(tx.shape) # 1x10000 + + X = s @ tx # Empfangenes Signal X durch Matrizenmultiplikation simulieren + print(X.shape) # 3x10000. X ist jetzt ein 2D-Array, 1D für Zeit und 1D für die räumliche Dimension + +Zu diesem Zeitpunkt ist :code:`X` ein 2D-Array der Größe 3 x 10000, da wir drei Array-Elemente und 10000 simulierte Samples haben. Wir plotten die ersten 200 Samples (nur den Realteil): + +.. code-block:: python + + plt.plot(np.asarray(X[0,:]).squeeze().real[0:200]) + plt.plot(np.asarray(X[1,:]).squeeze().real[0:200]) + plt.plot(np.asarray(X[2,:]).squeeze().real[0:200]) + plt.show() + +.. image:: ../_images/doa_time_domain.svg + :align: center + :target: ../_images/doa_time_domain.svg + +Beachte die Phasenverschiebungen zwischen den Elementen, wie wir es erwartet haben (außer wenn das Signal in Hauptstrahlrichtung (Boresight) ankommt; setze theta auf 0, um dies zu sehen). Versuche, den Winkel zu ändern und schau, was passiert. + +Als letzten Schritt fügen wir dem empfangenen Signal Rauschen hinzu. Das Rauschen wird nach der Steuervektor-Anwendung hinzugefügt, da jedes Element ein unabhängiges Rauschsignal erfährt: + +.. code-block:: python + + n = np.random.randn(Nr, N) + 1j*np.random.randn(Nr, N) + X = X + 0.1*n # X und n sind beide 3x10000 + +.. image:: ../_images/doa_time_domain_with_noise.svg + :align: center + :target: ../_images/doa_time_domain_with_noise.svg + +****************************** +Konventionelles Beamforming & DOA +****************************** + +Wir werden nun diese Samples :code:`X` verarbeiten und so tun, als ob wir den Ankunftswinkel nicht kennen, und DOA durchführen, d. h. den/die Ankunftswinkel mit DSP und Python schätzen! Wie zu Beginn dieses Kapitels besprochen, sind Beamforming und DOA sehr ähnlich und bauen oft auf denselben Techniken auf. Im Rest dieses Kapitels untersuchen wir verschiedene „Beamformer" und beginnen jeweils mit der Beamformer-Mathematik/dem -Code zur Berechnung der Gewichte :math:`w`. Diese Gewichte können über die einfache Gleichung :math:`w^H X` oder in Python :code:`w.conj().T @ X` auf das eingehende Signal :code:`X` angewendet werden. Im obigen Beispiel ist :code:`X` eine :code:`3x10000`-Matrix, aber nach Anwenden der Gewichte verbleiben wir mit :code:`1x10000`, als hätte unser Empfänger nur eine Antenne. + +Wir beginnen mit dem „konventionellen" Beamforming-Ansatz, auch Delay-and-Sum-Beamforming genannt. Unser Gewichtsvektor :code:`w` muss ein 1D-Array für ein ULA sein; in unserem Beispiel mit drei Elementen ist :code:`w` ein :code:`3x1`-Array komplexer Gewichte. Beim konventionellen Beamforming lassen wir die Magnitude der Gewichte bei 1 und passen die Phasen so an, dass sich das Signal in der Richtung unseres gewünschten Signals konstruktiv addiert – das ist genau dieselbe Mathematik wie oben, d. h. unsere Gewichte sind unser Steuervektor! + +.. math:: + w_{conv} = e^{2j \pi d k \sin(\theta)} + +oder in Python: + +.. code-block:: python + + w = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(theta)) # konventioneller Beamformer (Delay-and-Sum) + X_weighted = w.conj().T @ X # Gewichte auf empfangenes Signal anwenden + print(X_weighted.shape) # 1x10000 + +Aber wie kennen wir den Interessenwinkel :code:`theta`? Wir müssen zunächst DOA durchführen, was das Abtasten aller Ankunftsrichtungen von -π bis +π (-180 bis +180 Grad) beinhaltet, z. B. in 1-Grad-Schritten. Bei jeder Richtung berechnen wir die Gewichte und berechnen dann die Signalleistung. Wir plotten die Ergebnisse und suchen nach Peaks. + +.. code-block:: python + + theta_scan = np.linspace(-1*np.pi, np.pi, 1000) # 1000 verschiedene Thetas zwischen -180 und +180 Grad + results = [] + for theta_i in theta_scan: + w = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(theta_i)) # konventioneller Beamformer + X_weighted = w.conj().T @ X # Gewichte anwenden + results.append(10*np.log10(np.var(X_weighted))) # Signalleistung in dB + results -= np.max(results) # normalisieren (optional) + + # Winkel mit maximalem Wert ausgeben + print(theta_scan[np.argmax(results)] * 180 / np.pi) # 19.99999999999998 + + plt.plot(theta_scan*180/np.pi, results) # Winkel in Grad plotten + plt.xlabel("Theta [Grad]") + plt.ylabel("DOA-Metrik") + plt.grid() + plt.show() + +.. image:: ../_images/doa_conventional_beamformer.svg + :align: center + :target: ../_images/doa_conventional_beamformer.svg + +Wir haben unser Signal gefunden! Für einen Polarplot der DOA-Ergebnisse: + +.. code-block:: python + + fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}) + ax.plot(theta_scan, results) # UNBEDINGT RADIANT FÜR POLAR VERWENDEN + ax.set_theta_zero_location('N') # 0 Grad nach oben + ax.set_theta_direction(-1) # im Uhrzeigersinn zunehmen + ax.set_rlabel_position(55) + plt.show() + +.. image:: ../_images/doa_conventional_beamformer_polar.svg + :align: center + :target: ../_images/doa_conventional_beamformer_polar.svg + :alt: Beispiel-Polarplot der DOA mit Strahlmuster und 180-Grad-Mehrdeutigkeit + +******************** +180-Grad-Mehrdeutigkeit +******************** + +Erklären wir, warum es einen zweiten Peak bei 160 Grad gibt; die simulierte DOA war 20 Grad, aber es ist kein Zufall, dass 180 - 20 = 160. Stell dir drei omnidirektionale Antennen in einer Linie vor. Das Array sieht denselben Effekt, unabhängig davon, ob das Signal von vorne oder von hinten ankommt – die Phasenverzögerung ist gleich. Deshalb gibt es bei der DOA-Berechnung immer eine solche 180-Grad-Mehrdeutigkeit; der einzige Ausweg ist ein 2D-Array oder ein zweites 1D-Array in einem anderen Winkel. + +.. image:: ../_images/doa_from_behind.svg + :align: center + :target: ../_images/doa_from_behind.svg + +Wenn der Ankunftswinkel (AoA) sich dem „Endfire" des Arrays nähert (d. h. wenn das Signal entlang der Array-Achse ankommt), nimmt die Leistung ab: Die Hauptkeule wird breiter und es entsteht Mehrdeutigkeit zwischen links und rechts. Von diesem Punkt an zeigen wir in unseren Polarplots nur noch -90 bis +90 Grad, da das Muster für 1D-lineare Arrays immer entlang der Array-Achse gespiegelt wird. + +******************** +Strahlmuster +******************** + +Die bisherigen Plots sind DOA-Ergebnisse – sie entsprechen der empfangenen Leistung bei jedem Winkel nach Anwenden des Beamformers. Wir können aber auch das Strahlmuster selbst betrachten, bevor wir ein Signal empfangen; dies wird manchmal als „quiescent antenna pattern" oder „Array-Antwort" bezeichnet. + +Unser Steuervektor + +.. code-block:: python + + np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(theta)) + +kapselt die ULA-Geometrie und hat als einzigen weiteren Parameter die gewünschte Steuerrichtung. Wir können das quieszente Antennendiagramm berechnen und plotten, wenn in eine bestimmte Richtung gelenkt wird. Dies kann durch die FFT der komplex-konjugierten Gewichte erledigt werden: + +.. code-block:: python + + Nr = 3 + d = 0.5 + N_fft = 512 + theta_degrees = 20 # nur die Richtung, auf die wir zeigen wollen + theta = theta_degrees / 180 * np.pi + w = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(theta)) # konventioneller Beamformer + w_padded = np.concatenate((w, np.zeros(N_fft - Nr))) # mit Nullen auf N_fft Elemente auffüllen + w_fft_dB = 10*np.log10(np.abs(np.fft.fftshift(np.fft.fft(w_padded)))**2) # Betrag der FFT in dB + w_fft_dB -= np.max(w_fft_dB) # auf 0 dB am Peak normalisieren + + # FFT-Bins auf Winkel in Radiant abbilden + theta_bins = np.arcsin(np.linspace(-1, 1, N_fft)) + + theta_max = theta_bins[np.argmax(w_fft_dB)] + + fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}) + ax.plot(theta_bins, w_fft_dB) + ax.plot([theta_max], [np.max(w_fft_dB)],'ro') + ax.text(theta_max - 0.1, np.max(w_fft_dB) - 4, np.round(theta_max * 180 / np.pi)) + ax.set_theta_zero_location('N') + ax.set_theta_direction(-1) + ax.set_rlabel_position(55) + ax.set_thetamin(-90) + ax.set_thetamax(90) + ax.set_ylim([-30, 1]) + plt.show() + +.. image:: ../_images/doa_quiescent.svg + :align: center + :target: ../_images/doa_quiescent.svg + +Die folgende Animation zeigt das Strahlmuster des konventionellen Beamformers für ein 8-Element-Array, das zwischen -90 und +90 Grad gesteuert wird: + +.. image:: ../_images/delay_and_sum.gif + :scale: 90 % + :align: center + :alt: Strahlmuster von Delay-and-Sum mit Gewichten in der komplexen Ebene + +Beachte, wie alle Gewichte Einheitsamplitude haben (sie bleiben auf dem Einheitskreis) und wie die höher nummerierten Elemente schneller „rotieren". + +******************** +Array-Strahlbreite +******************** + +Für Interessierte gibt es Gleichungen zur Näherung der Hauptkeulenbreite in Abhängigkeit von der Elementanzahl (sie funktionieren gut bei vielen Elementen, z. B. 8 oder mehr). Die Halbwertbreite (HPBW) ist 3 dB unterhalb des Hauptkeulengipfels und beträgt ungefähr :math:`\frac{0.9 \lambda}{N_rd\cos(\theta)}` [1], was beim Halbwellenlängenabstand vereinfacht zu: + +.. math:: + + \text{HPBW} \approx \frac{1.8}{N_r\cos(\theta)} \text{ [Radiant]} \qquad \text{wenn } d = \lambda/2 + +Die Breite der ersten Nullstellen (FNBW), die Breite der Hauptkeule von Null zu Null, beträgt ungefähr :math:`\frac{2\lambda}{N_rd}` [1], was beim Halbwellenlängenabstand vereinfacht zu: + +.. math:: + + \text{FNBW} \approx \frac{4}{N_r} \text{ [Radiant]} \qquad \text{wenn } d = \lambda/2 + +.. image:: ../_images/doa_quiescent_beamwidth.svg + :align: center + :target: ../_images/doa_quiescent_beamwidth.svg + +******************* +Wenn d nicht λ/2 ist +******************* + +Bisher haben wir einen Elementabstand d gleich einer halben Wellenlänge verwendet. Wenn d größer als λ/2 ist, entstehen sogenannte Gitterkeulen (Grating Lobes) – ein Ergebnis von „räumlichem Aliasing". Wie wir im Kapitel :ref:`sampling-chapter` gelernt haben, entsteht Aliasing, wenn wir nicht schnell genug abtasten. Dasselbe gilt im räumlichen Bereich: Wenn die Elemente nicht nahe genug beieinander liegen, erhalten wir fehlerhafte Ergebnisse. + +.. image:: ../_images/doa_d_is_large_animation.gif + :scale: 100 % + :align: center + :alt: Animation der DOA bei d >> λ/2 + +Das Nyquist-Kriterium gilt auch für den räumlichen Bereich: :math:`d \leq \lambda/2`. Solange dieser Abstand eingehalten wird, gibt es keine Gitterkeulen! + +Was passiert, wenn d kleiner als λ/2 ist? + +.. image:: ../_images/doa_d_is_small_animation.gif + :scale: 100 % + :align: center + :alt: Animation der DOA bei d << λ/2 + +Die Hauptkeule wird breiter, aber es gibt keine Gitterkeulen. Mit einem zweiten Signal von -40 Grad: + +.. image:: ../_images/doa_d_is_small_animation2.gif + :scale: 100 % + :align: center + :alt: Animation der DOA bei d << λ/2 mit zwei Signalen + +Unterhalb von λ/4 lassen sich die beiden Wege nicht mehr unterscheiden. d so nah wie möglich bei λ/2 zu halten bleibt ein wichtiges Thema. + +********************** +Räumliche Fensterung +********************** + +Räumliche Fensterung (Spatial Tapering) ist eine Technik, die zusammen mit dem konventionellen Beamformer verwendet wird, wobei die Magnitude der Gewichte angepasst wird, um bestimmte Eigenschaften zu erzielen. Beim konventionellen Beamformer haben alle Gewichte eine Magnitude von 1 (Einheitsamplitude). Mit räumlicher Fensterung multiplizieren wir die Gewichte mit Skalaren, um ihre Magnitude zu skalieren. + +.. code-block:: python + + tapering = np.random.uniform(0, 1, Nr) # zufällige Fensterung + w *= tapering + +.. image:: ../_images/spatial_tapering_animation.gif + :scale: 80 % + :align: center + :alt: Räumliche Fensterung mit zufälligen Werten + +Fensterung kann die Nebenkeulen reduzieren, indem die Magnitude der Gewichte an den **Rändern** des Arrays reduziert wird. Zum Beispiel kann eine Hamming-Fensterfunktion als Fensterungswerte verwendet werden: + +.. code-block:: python + + tapering = np.hamming(Nr) # Hamming-Fensterfunktion + w *= tapering + +.. image:: ../_images/spatial_tapering_animation2.gif + :scale: 80 % + :align: center + :alt: Räumliche Fensterung mit Hamming-Fenster + +Wir bemerken zwei Änderungen: Erstens wird die Hauptkeulenbreite von der Fensterfunktion beeinflusst (weniger Nebenkeulen führen typischerweise zu einer breiteren Hauptkeule). Ein rechteckiges Fenster (keine Fensterung) führt zur schmalsten Hauptkeule, aber zu den höchsten Nebenkeulen. Zweitens nimmt der Gewinn der Hauptkeule ab, wenn wir ein Fenster anwenden, da wir letztendlich weniger Signalenergie empfangen. + +****************************** +Gewichte manuell anpassen +****************************** + +Der konventionelle Beamformer liefert eine Gleichung zur Berechnung der Gewichte, aber für einen Moment wollen wir so tun, als hätten wir keine Methode zur Gewichtsberechnung, und stattdessen mit den Gewichten (sowohl Magnitude als auch Phase) manuell spielen, um zu sehen, was passiert. Unten ist eine kleine App in JavaScript, die das Strahlmuster eines 8-Element-Arrays simuliert, mit Schiebereglern zur Steuerung von Verstärkung und Phase jedes Elements: + +.. raw:: html + +
+
+ Element     Magnitude (Gain)                  Phase +
+ + + +********************* +Adaptives Beamforming +********************* + +Der konventionelle Beamformer ist eine einfache und effektive Methode für Beamforming, hat aber einige Einschränkungen. Zum Beispiel funktioniert er nicht gut, wenn mehrere Signale aus verschiedenen Richtungen ankommen oder wenn der Rauschpegel hoch ist. In diesen Fällen müssen wir fortschrittlichere Beamforming-Techniken verwenden, die oft als „adaptives" Beamforming bezeichnet werden. Die Idee hinter adaptivem Beamforming ist es, das empfangene Signal zur Berechnung der Gewichte zu verwenden, anstatt einen festen Satz von Gewichten wie beim konventionellen Beamformer zu verwenden. Dies ermöglicht es dem Beamformer, sich an die Umgebung anzupassen und eine bessere Leistung zu erbringen. + +Adaptive Beamforming-Techniken können weiter in reguläre und unterraumbasierte Methoden unterteilt werden. Unterraummethoden wie MUSIC und ESPRIT sind sehr leistungsfähig, erfordern aber eine Schätzung der Anzahl vorhandener Signale, und sie benötigen mindestens drei Elemente (mindestens vier werden empfohlen). + +Die erste adaptive Beamforming-Technik, die wir untersuchen werden, ist MVDR. + +********************** +MVDR/Capon-Beamformer +********************** + +Wir betrachten jetzt einen Beamformer, der etwas komplizierter als die konventionelle Delay-and-Sum-Technik ist, aber tendenziell viel besser abschneidet: den Minimum Variance Distortionless Response (MVDR)- oder Capon-Beamformer. Die Idee hinter MVDR ist es, das Signal aus dem Interessenwinkel bei einer festen Verstärkung von 1 (0 dB) zu halten, während die Gesamtleistung des resultierenden Beamformer-Signals minimiert wird. Er wird oft als „statistisch optimaler" Beamformer bezeichnet. + +Der MVDR/Capon-Beamformer lässt sich in folgender Gleichung zusammenfassen: + +.. math:: + + w_{mvdr} = \frac{R^{-1} s}{s^H R^{-1} s} + +Der Vektor :math:`s` ist der Steuervektor entsprechend der gewünschten Richtung. :math:`R` ist die räumliche Kovarianzmatrixschätzung auf Basis unserer empfangenen Samples, berechnet mit :code:`R = np.cov(X)` oder manuell durch :math:`R = X X^H`. Die räumliche Kovarianzmatrix ist eine :code:`Nr` x :code:`Nr`-Matrix (3x3 in den bisherigen Beispielen), die angibt, wie ähnlich die von den drei Elementen empfangenen Samples sind. Der Nenner dient hauptsächlich der Skalierung; der Zähler ist der wichtige Teil – die invertierte Kovarianzmatrix multipliziert mit dem Steuervektor. + +.. raw:: html + +
+ Für Interessierte: MVDR-Herleitung hier aufklappen + + +**Beamformer-Ausgang** – Der Ausgang des Beamformers mit dem Gewichtsvektor :math:`\mathbf{w}` ist: + +.. math:: + + y(t) = \mathbf{w}^H \mathbf{x}(t) + + +**Optimierungsproblem** – Das Ziel ist es, die Beamforming-Gewichte zu bestimmen, die die Ausgangsleistung unter der Nebenbedingung einer verzerrungsfreien Antwort in die gewünschte Richtung :math:`\theta_0` minimieren: + +.. math:: + + \min_{\mathbf{w}} \, \mathbf{w}^H \mathbf{R} \mathbf{w} \quad \text{s. t.} \quad \mathbf{w}^H \mathbf{s} = 1 + +wobei: + +* :math:`\mathbf{R} = E[\mathbf{X}\mathbf{X}^H]` die Kovarianzmatrix der empfangenen Signale ist +* :math:`\mathbf{s}` der Steuervektor in Richtung des gewünschten Signals :math:`\theta_0` ist + +**Lagrange-Methode** – Einführung eines Lagrange-Multiplikators :math:`\lambda`: + +.. math:: + + L(\mathbf{w}, \lambda) = \mathbf{w}^H \mathbf{R} \mathbf{w} - \lambda (\mathbf{w}^H \mathbf{s} - 1) + +**Lösung der Optimierung** – Durch Differenzierung nach :math:`\mathbf{w^H}` und Nullsetzen ergibt sich: + +.. math:: + + \frac{\partial L}{\partial \mathbf{w}^*} = 2\mathbf{R}\mathbf{w} - \lambda \mathbf{s} = 0 + + \mathbf{w} = \lambda \mathbf{s} \mathbf{{R^{-1}}} + + +Zur Lösung für :math:`\lambda` mit der Nebenbedingung :math:`\mathbf{w}^H \mathbf{s} = 1`: + +.. math:: + + \implies (\lambda \mathbf{s^{H}}\mathbf{{R^{-1}}})s = 1 + + \implies \lambda = \frac{1}{\mathbf{s}^{H}\mathbf{R}^{-1}\mathbf{s}} + + \mathbf{R}\mathbf{w} = \lambda \mathbf{s} + + \mathbf{w_{mvdr}} = \frac{\mathbf{R}^{-1} \mathbf{s}}{\mathbf{s}^H \mathbf{R}^{-1} \mathbf{s}} + +.. raw:: html + +
+ +In Python implementieren wir den MVDR/Capon-Beamformer wie folgt: + +.. code-block:: python + + # theta ist die Interessenrichtung in Radiant, X ist unser empfangenes Signal + def w_mvdr(theta, X): + s = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(theta)) # Steuervektor + s = s.reshape(-1,1) # als Spaltenvektor (3x1) + R = (X @ X.conj().T)/X.shape[1] # Kovarianzmatrix berechnen (Nr x Nr) + Rinv = np.linalg.pinv(R) # Pseudo-Inverse ist stabiler als echte Inverse + w = (Rinv @ s)/(s.conj().T @ Rinv @ s) # MVDR/Capon-Gleichung + return w + +Verwendung dieses MVDR-Beamformers für DOA: + +.. code-block:: python + + theta_scan = np.linspace(-1*np.pi, np.pi, 1000) + results = [] + for theta_i in theta_scan: + w = w_mvdr(theta_i, X) # 3x1 + X_weighted = w.conj().T @ X # Gewichte anwenden + power_dB = 10*np.log10(np.var(X_weighted)) + results.append(power_dB) + results -= np.max(results) # normalisieren + +Für ein komplexeres Szenario mit einem 8-Element-Array, das drei Signale aus verschiedenen Winkeln empfängt (20, 25 und 40 Grad, wobei das letzte viel schwächer ist): + +.. code-block:: python + + Nr = 8 # 8 Elemente + theta1 = 20 / 180 * np.pi + theta2 = 25 / 180 * np.pi + theta3 = -40 / 180 * np.pi + s1 = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(theta1)).reshape(-1,1) # 8x1 + s2 = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(theta2)).reshape(-1,1) + s3 = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(theta3)).reshape(-1,1) + # verschiedene Frequenzen verwenden. 1xN + tone1 = np.exp(2j*np.pi*0.01e6*t).reshape(1,-1) + tone2 = np.exp(2j*np.pi*0.02e6*t).reshape(1,-1) + tone3 = np.exp(2j*np.pi*0.03e6*t).reshape(1,-1) + X = s1 @ tone1 + s2 @ tone2 + 0.1 * s3 @ tone3 # letztes Signal hat 1/10 der Leistung + n = np.random.randn(Nr, N) + 1j*np.random.randn(Nr, N) + X = X + 0.05*n # 8xN + +MVDR-Ergebnis für dieses Szenario: + +.. image:: ../_images/doa_capons2.svg + :align: center + :target: ../_images/doa_capons2.svg + +Alle drei Signale sind erkennbar – auch die beiden nur 5 Grad voneinander entfernten und das schwächere bei -40 Grad. Zum Vergleich das konventionelle Beamforming-Ergebnis: + +.. image:: ../_images/doa_complex_scenario.svg + :align: center + :target: ../_images/doa_complex_scenario.svg + +Das konventionelle Beamforming findet nicht alle drei Signale – das zeigt den Vorteil eines adaptiven Beamformers. + +Als Optimierung kann für die DOA-Anwendung von MVDR die Leistungsberechnung ohne explizite Gewichtsanwendung erfolgen: + +.. math:: + + P_{mvdr} = \frac{1}{s^H R^{-1} s} + +.. code-block:: python + + def power_mvdr(theta, X): + s = np.exp(2j * np.pi * d * np.arange(X.shape[0]) * np.sin(theta)) + s = s.reshape(-1,1) + R = (X @ X.conj().T)/X.shape[1] + Rinv = np.linalg.pinv(R) + return 1/(s.conj().T @ Rinv @ s).squeeze() + +********************** +Kovarianzmatrix +********************** + +Nehmen wir uns kurz Zeit, die räumliche Kovarianzmatrix zu besprechen, ein Schlüsselkonzept im adaptiven Beamforming. Eine Kovarianzmatrix ist eine mathematische Darstellung der Ähnlichkeit zwischen Paaren von Elementen in einem zufälligen Vektor (in unserem Fall die Elemente unseres Arrays). Eine Kovarianzmatrix ist immer quadratisch, und die Werte entlang der Diagonale entsprechen der Kovarianz jedes Elements mit sich selbst. + +Im Allgemeinen ist die Kovarianzmatrix definiert als: + +:math:`\mathrm{cov}(X) = E \left[ (X - E[X])(X - E[X])^H \right]` + +Für drahtlose Signale im Basisband ist :math:`E[X]` typischerweise null oder sehr nahe null, sodass sich dies vereinfacht zu: + +:math:`\mathrm{cov}(X) = E[X X^H]` + +Mit einer begrenzten Anzahl von IQ-Samples können wir diese Kovarianz schätzen als :math:`\hat{R}`: + +.. math:: + + \hat{R} = \frac{\boldsymbol{X} \boldsymbol{X}^H}{N} + + = \frac{1}{N} \sum^N_{n=1} X_n X_n^H + +In Python: :code:`R = (X @ X.conj().T)/X.shape[1]` oder alternativ :code:`R = np.cov(X)`. + +Die Diagonalelemente sind reell und ungefähr gleich; sie geben im Wesentlichen die empfangene Signalleistung an jedem Element an. Die Nicht-Diagonalelemente sind die wichtigen Werte. Die Inverse der räumlichen Kovarianzmatrix (auch „Precision Matrix" oder in der Radartechnik „Whitening Matrix" genannt) gibt an, wie zwei Elemente miteinander verwandt sind, nachdem der Einfluss anderer Elemente entfernt wurde. + +********************** +LCMV-Beamformer +********************** + +Was, wenn wir mehr als ein Nutzsignal (SOI) haben? Mit einer kleinen Anpassung an MVDR implementieren wir den Linearly Constrained Minimum Variance (LCMV)-Beamformer, der mehrere SOIs verarbeiten kann. Der optimale Gewichtsvektor für den LCMV-Beamformer lautet: + +.. math:: + + w_{lcmv} = R^{-1} C [C^H R^{-1} C]^{-1} f + +wobei :math:`C` eine Matrix aus den Steuervektoren der entsprechenden SOIs und Störsender ist, und :math:`f` der gewünschte Antwortvektor ist. :math:`f` nimmt den Wert 0 an, wenn der entsprechende Steuervektor unterdrückt werden soll, und 1, wenn ein Strahl darauf gerichtet werden soll. Für zwei SOIs und zwei Störsender z. B. :code:`f = [1,1,0,0]`. Die Gesamtzahl der gleichzeitig formbaren Nullstellen und Strahlen ist durch die Arraygröße (Elementanzahl) begrenzt. + +Implementierung in Python für zwei SOIs (15 und 60 Grad) ohne explizit hardcodierte Nullstellen (MVDR übernimmt das automatisch anhand der Statistik): + +.. code-block:: python + + # SOI bei 15 Grad, weiteres mögliches SOI bei 60 Grad + soi1_theta = 15 / 180 * np.pi + soi2_theta = 60 / 180 * np.pi + + # LCMV-Gewichte + R_inv = np.linalg.pinv(np.cov(X)) # 8x8 + s1 = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(soi1_theta)).reshape(-1,1) # 8x1 + s2 = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(soi2_theta)).reshape(-1,1) # 8x1 + C = np.concatenate((s1, s2), axis=1) # 8x2 + f = np.ones(2).reshape(-1,1) # 2x1 + + # LCMV-Gleichung + # 8x8 8x2 2x8 8x8 8x2 2x1 + w = R_inv @ C @ np.linalg.pinv(C.conj().T @ R_inv @ C) @ f # Ausgabe ist 8x1 + +.. image:: ../_images/lcmv_beam_pattern.svg + :align: center + :target: ../_images/lcmv_beam_pattern.svg + :alt: Beispiel-Strahlmuster mit LCMV-Beamformer + +Wir haben Strahlen in die zwei Interessenrichtungen und Nullstellen bei den Störsendern (grüne und rote Punkte zeigen SOI- und Störsender-AoAs). + +.. raw:: html + +
+ Vollständigen Code aufklappen + +.. code-block:: python + + # Empfangenes Signal simulieren + Nr = 8 # 8 Elemente + theta1 = -60 / 180 * np.pi + theta2 = -30 / 180 * np.pi + theta3 = 0 / 180 * np.pi + theta4 = 30 / 180 * np.pi + s1 = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(theta1)).reshape(-1,1) # 8x1 + s2 = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(theta2)).reshape(-1,1) + s3 = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(theta3)).reshape(-1,1) + s4 = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(theta4)).reshape(-1,1) + # verschiedene Frequenzen. 1xN + tone1 = np.exp(2j*np.pi*0.01e6*t).reshape(1,-1) + tone2 = np.exp(2j*np.pi*0.02e6*t).reshape(1,-1) + tone3 = np.exp(2j*np.pi*0.03e6*t).reshape(1,-1) + tone4 = np.exp(2j*np.pi*0.04e6*t).reshape(1,-1) + X = s1 @ tone1 + s2 @ tone2 + s3 @ tone3 + s4 @ tone4 + n = np.random.randn(Nr, N) + 1j*np.random.randn(Nr, N) + X = X + 0.5*n # 8xN + + # SOI bei 15 Grad, weiteres mögliches SOI bei 60 Grad + soi1_theta = 15 / 180 * np.pi + soi2_theta = 60 / 180 * np.pi + + # LCMV-Gewichte + R_inv = np.linalg.pinv(np.cov(X)) # 8x8 + s1 = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(soi1_theta)).reshape(-1,1) # 8x1 + s2 = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(soi2_theta)).reshape(-1,1) # 8x1 + C = np.concatenate((s1, s2), axis=1) # 8x2 + f = np.ones(2).reshape(-1,1) # 2x1 + + # LCMV-Gleichung + w = R_inv @ C @ np.linalg.pinv(C.conj().T @ R_inv @ C) @ f # Ausgabe ist 8x1 + + # Strahlmuster plotten + w = w.squeeze() + N_fft = 1024 + w_padded = np.concatenate((w, np.zeros(N_fft - Nr))) + w_fft_dB = 10*np.log10(np.abs(np.fft.fftshift(np.fft.fft(w_padded)))**2) + w_fft_dB -= np.max(w_fft_dB) + theta_bins = np.arcsin(np.linspace(-1, 1, N_fft)) + + fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}) + ax.plot(theta_bins, w_fft_dB) + ax.plot([theta1], [0], 'or') + ax.plot([theta2], [0], 'or') + ax.plot([theta3], [0], 'or') + ax.plot([theta4], [0], 'or') + ax.plot([soi1_theta], [0], 'og') + ax.plot([soi2_theta], [0], 'og') + ax.set_theta_zero_location('N') + ax.set_theta_direction(-1) + ax.set_thetagrids(np.arange(-90, 105, 15)) + ax.set_rlabel_position(55) + ax.set_thetamin(-90) + ax.set_thetamax(90) + ax.set_ylim([-30, 1]) + plt.show() + +.. raw:: html + +
+ +Ein besonderer Anwendungsfall von LCMV ist die Erstellung breiterer Strahlen oder Nullstellen, indem :code:`f` für einen Bereich von Winkeln auf 1 oder 0 gesetzt wird. Beispiel mit einem 18-Element-Array: + +.. code-block:: python + + Nr = 18 + X = np.random.randn(Nr, N) + 1j*np.random.randn(Nr, N) # nur Rauschen + + # SOI von 15 bis 30 Grad mit 4 Winkeln + soi_thetas = np.linspace(15, 30, 4) / 180 * np.pi + + # Nullstelle von 45 bis 60 Grad mit 4 Winkeln + null_thetas = np.linspace(45, 60, 4) / 180 * np.pi + + # LCMV-Gewichte + R_inv = np.linalg.pinv(np.cov(X)) + s = [] + for soi_theta in soi_thetas: + s.append(np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(soi_theta)).reshape(-1,1)) + for null_theta in null_thetas: + s.append(np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(null_theta)).reshape(-1,1)) + C = np.concatenate(s, axis=1) + f = np.asarray([1]*len(soi_thetas) + [0]*len(null_thetas)).reshape(-1,1) + w = R_inv @ C @ np.linalg.pinv(C.conj().T @ R_inv @ C) @ f # LCMV-Gleichung + +.. image:: ../_images/lcmv_beam_pattern_spread.svg + :align: center + :target: ../_images/lcmv_beam_pattern_spread.svg + :alt: Beispiel-Strahlmuster mit LCMV und gespreiztem Strahl und Nullstelle + +******************* +Nullsteuerung +******************* + +Jetzt ist es sinnvoll, eine einfachere Technik zu untersuchen, die in analogen und digitalen Arrays eingesetzt werden kann: die Nullsteuerung (Null Steering). Sie ist wie eine Erweiterung des konventionellen Beamformers; zusätzlich zum Zeigen eines Strahls in die Interessenrichtung können wir Nullstellen bei bestimmten Winkeln platzieren. Diese Technik verwendet keine Gewichte basierend auf dem empfangenen Signal und wird daher nicht als adaptiv betrachtet. + +Die Gewichte für die Nullsteuerung werden berechnet, indem mit dem konventionellen Beamformer begonnen wird und dann die Sidelobe-Canceler-Gleichung angewendet wird, um Nullstellen hinzuzufügen: + +.. math:: + + w_{\text{neu}} = w_{\text{orig}} - \frac{w_{\text{null}}^H w_{\text{orig}}}{w_{\text{null}}^H w_{\text{null}}} w_{\text{null}} + +wobei :math:`w_{\text{null}}` der Steuervektor in Richtung der zu erzeugenden Nullstelle ist. Der vollständige Prozess: + +.. math:: + + \text{1:} \qquad w_{\text{orig}} = e^{2j \pi d k \sin(\theta_{SOI})} \qquad + + \text{2:} \qquad w_{\text{null}} = e^{2j \pi d k \sin(\theta_{null})} \qquad + + \text{3:} \qquad w_{\text{neu}} = w_{\text{orig}} - \frac{w_{\text{null}}^H w_{\text{orig}}}{w_{\text{null}}^H w_{\text{null}}} w_{\text{null}} + + \text{4:} \qquad w_{\text{orig}} = w_{\text{neu}} \qquad \qquad \qquad + + \text{5:} \qquad \text{GOTO 2 für nächste Nullstelle} + +Simulieren wir ein 8-Element-Array mit vier Nullstellen: + +.. code-block:: python + + d = 0.5 + Nr = 8 + + theta_soi = 30 / 180 * np.pi + nulls_deg = [-60, -30, 0, 60] # Grad + nulls_rad = np.asarray(nulls_deg) / 180 * np.pi + + # Mit konventionellem Beamformer auf theta_soi beginnen + w = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(theta_soi)).reshape(-1,1) + + # Nullstellen durchlaufen + for null_rad in nulls_rad: + # Gewichte gleich Steuervektor in Nullstellenrichtung + w_null = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(null_rad)).reshape(-1,1) + + # Skalierungsfaktor für w in der Nullstellenrichtung + scaling_factor = w_null.conj().T @ w / (w_null.conj().T @ w_null) + print("scaling_factor:", scaling_factor, scaling_factor.shape) + + # Gewichte aktualisieren + w = w - w_null @ scaling_factor # Sidelobe-Canceler-Gleichung + + # Strahlmuster plotten + N_fft = 1024 + w_padded = np.concatenate((w.squeeze(), np.zeros(N_fft - Nr))) + w_fft_dB = 10*np.log10(np.abs(np.fft.fftshift(np.fft.fft(w_padded)))**2) + w_fft_dB -= np.max(w_fft_dB) + theta_bins = np.arcsin(np.linspace(-1, 1, N_fft)) + + fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}) + ax.plot(theta_bins, w_fft_dB) + for null_rad in nulls_rad: + ax.plot([null_rad], [0], 'or') + ax.plot([theta_soi], [0], 'og') + ax.set_theta_zero_location('N') + ax.set_theta_direction(-1) + ax.set_thetagrids(np.arange(-90, 105, 15)) + ax.set_rlabel_position(55) + ax.set_thetamin(-90) + ax.set_thetamax(90) + ax.set_ylim([-40, 1]) + plt.show() + +.. image:: ../_images/null_steering.svg + :align: center + :target: ../_images/null_steering.svg + :alt: Beispiel für Nullsteuerung beim Beamforming + +******************* +MUSIC +******************* + +Wir wechseln nun zu einer anderen Art von Beamformer. Alle bisherigen fielen in die Kategorie „Delay-and-Sum", aber jetzt tauchen wir in „Unterraum"-Methoden ein. Diese beinhalten die Aufteilung in Signal- und Rauschunterraum, was bedeutet, dass wir die Anzahl der empfangenen Signale schätzen müssen. MUltiple SIgnal Classification (MUSIC) ist eine sehr beliebte Unterraummethode, die die Berechnung der Eigenvektoren der Kovarianzmatrix beinhaltet. Wir teilen die Eigenvektoren in zwei Gruppen auf: Signal-Unterraum und Rausch-Unterraum, projizieren dann Steuervektoren in den Rausch-Unterraum und suchen nach Nullstellen. + +Die Kern-MUSIC-Gleichung lautet: + +.. math:: + \hat{\theta} = \mathrm{argmax}\left(\frac{1}{s^H V_n V^H_n s}\right) + +wobei :math:`V_n` die Liste der Rausch-Unterraum-Eigenvektoren ist. Sie wird durch Berechnung der Eigenvektoren von :math:`R` gefunden (in Python: :code:`w, v = np.linalg.eig(R)`), dann werden die Vektoren basierend auf der geschätzten Anzahl der Signale aufgeteilt. :math:`V_n` hängt nicht vom Steuervektor :math:`s` ab, daher kann es vorberechnet werden. Der vollständige MUSIC-Code: + +.. code-block:: python + + num_expected_signals = 3 # Anzahl erwarteter Signale (kann verändert werden!) + + # Teil, der sich mit theta_i nicht ändert + R = np.cov(X) # Kovarianzmatrix (Nr x Nr) + w, v = np.linalg.eig(R) # Eigenzerlegung + eig_val_order = np.argsort(np.abs(w)) # Reihenfolge der Eigenwertbeträge + v = v[:, eig_val_order] # Eigenvektoren nach Magnitude sortieren + # Neue Eigenvektor-Matrix für den Rauschunterraum + V = np.zeros((Nr, Nr - num_expected_signals), dtype=np.complex64) + for i in range(Nr - num_expected_signals): + V[:, i] = v[:, i] + + theta_scan = np.linspace(-1*np.pi, np.pi, 1000) # -180 bis +180 Grad + results = [] + for theta_i in theta_scan: + s = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(theta_i)) # Steuervektor + s = s.reshape(-1,1) + metric = 1 / (s.conj().T @ V @ V.conj().T @ s) # MUSIC-Gleichung + metric = np.abs(metric.squeeze()) + metric = 10*np.log10(metric) + results.append(metric) + + results /= np.max(results) # normalisieren + +.. image:: ../_images/doa_music.svg + :align: center + :target: ../_images/doa_music.svg + :alt: Beispiel DOA mit MUSIC-Algorithmus + +Um die Anzahl der Signale zu schätzen, sortieren wir die Eigenwertbeträge und plotten sie: + +.. code-block:: python + + plot(10*np.log10(np.abs(w)),'.-') + +.. image:: ../_images/doa_eigenvalues.svg + :align: center + :target: ../_images/doa_eigenvalues.svg + +Die mit dem Rausch-Unterraum assoziierten Eigenwerte sind die kleinsten und tendieren zu einem ähnlichen Wert. Hier sehen wir klar drei Signale. Die folgende Animation zeigt, wie gut MUSIC zwei eng benachbarte Signale trennt: + +.. image:: ../_images/doa_music_animation.gif + :scale: 100 % + :align: center + +*** +LMS +*** + +Der Least Mean Squares (LMS)-Beamformer ist ein rechenarmer Beamformer, der von Bernard Widrow eingeführt wurde. Er unterscheidet sich von allen bisherigen Beamformern in zwei Punkten: 1) Er erfordert die Kenntnis des SOI (oder zumindest eines Teils davon, z. B. einer Synchronisationssequenz, Piloten usw.) und 2) er ist iterativ, d. h. die Gewichte werden über eine Reihe von Iterationen verfeinert. Der LMS-Algorithmus minimiert den mittleren quadratischen Fehler zwischen dem gewünschten Signal (SOI) und dem Ausgang des Beamformers: + +.. math:: + + w_{n+1} = w_n + \mu \underbrace{\left(y_n - w_{n}^H x_n\right)^*}_{Fehler} x_n + +wobei :math:`w_n` der Gewichtsvektor bei Iteration/Sample :math:`n`, :math:`\mu` die Schrittgröße, :math:`x_n` das empfangene Sample bei :math:`n`, :math:`y_n` der erwartete Wert (d. h. das bekannte SOI) und :math:`*` eine komplexe Konjugation ist. Der Term :math:`w_{n}^H x_n` ist einfach das Ergebnis der Anwendung der aktuellen Gewichte auf das Eingangssignal. Die Schrittgröße :math:`\mu` steuert, wie schnell die Gewichte gegen ihre optimalen Werte konvergieren. + +Im folgenden Python-Beispiel simulieren wir ein 8-Element-Array mit einem SOI aus einem wiederholten Gold-Code (als BPSK gesendet) und zwei Ton-Störsendern bei 60 und -50 Grad: + +.. image:: ../_images/doa_lms_animation.gif + :scale: 100 % + :align: center + +.. code-block:: python + + # Szenario + sample_rate = 1e6 + d = 0.5 # halber Wellenlängenabstand + N = 100000 # Anzahl der Samples + Nr = 8 # Elemente + theta_soi = 20 / 180 * np.pi + theta2 = 60 / 180 * np.pi + theta3 = -50 / 180 * np.pi + t = np.arange(N)/sample_rate # Zeitvektor + s1 = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(theta_soi)).reshape(-1,1) # 8x1 + s2 = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(theta2)).reshape(-1,1) + s3 = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(theta3)).reshape(-1,1) + + # SOI ist ein Gold-Code, wiederholt, Länge 127 + gold_code = np.array([-1, 1, 1, -1, 1, 1, 1, 1, -1, -1, -1, 1, 1, -1, -1, -1, -1, -1, 1, 1, 1, -1, -1, 1, 1, 1, -1, 1, 1, 1, 1, 1, 1, -1, -1, -1, 1, 1, 1, -1, -1, 1, 1, -1, -1, 1, -1, 1, -1, -1, 1, -1, -1, -1, -1, -1, -1, 1, 1, -1, 1, -1, 1, -1, 1, 1, -1, -1, -1, -1, 1, 1, 1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, -1, -1, 1, 1, 1, -1, 1, -1, -1, -1, 1, 1, 1, 1, -1, 1, 1, 1, -1, 1, -1, -1, -1, -1, 1, -1, 1, 1, -1, -1, -1, -1, 1, -1, 1, 1, -1, -1, -1, -1, -1, -1, 1, 1]) + soi_samples_per_symbol = 8 + soi = np.repeat(gold_code, soi_samples_per_symbol) + num_sequence_repeats = int(N / soi.shape[0]) + 1 # Anzahl der Wiederholungen + soi = np.tile(soi, num_sequence_repeats)[:N] # Sequenz wiederholen und kürzen + soi = soi.reshape(1, -1) # 1xN + + # Störsender (Ton-Jammer) aus verschiedenen Richtungen + tone2 = np.exp(2j*np.pi*0.02e6*t).reshape(1,-1) + tone3 = np.exp(2j*np.pi*0.03e6*t).reshape(1,-1) + + # Empfangenes Signal simulieren + r = s1 @ soi + s2 @ tone2 + s3 @ tone3 + n = np.random.randn(Nr, N) + 1j*np.random.randn(Nr, N) + r = r + 0.5*n # 8xN + + # LMS – Richtung des SOI unbekannt, aber SOI-Signal selbst bekannt + mu = 0.5e-5 # LMS-Schrittgröße + w_lms = np.zeros((Nr, 1), dtype=np.complex128) # mit Nullen beginnen + + # Empfangene Samples durchlaufen + error_log = [] + for i in range(N): + r_sample = r[:, i].reshape(-1, 1) # 8x1 + soi_sample = soi[0, i] # Skalar + y = w_lms.conj().T @ r_sample # Gewichte anwenden + y = y.squeeze() # als Skalar + error = soi_sample - y + error_log.append(np.abs(error)**2) + w_lms += mu * np.conj(error) * r_sample # Gewichte sind noch 8x1 + + w_lms /= np.linalg.norm(w_lms) # Gewichte normalisieren + + plt.plot(error_log) + plt.xlabel('Iteration') + plt.ylabel('Mittlerer quadratischer Fehler') + plt.show() + + # Strahlmuster wie zuvor plotten + +******************* +Trainingsdaten +******************* + +Im Kontext der Array-Verarbeitung gibt es das Konzept des „Trainings", bei dem die Kovarianzmatrix :code:`R` berechnet wird, bevor das potenzielle SOI vorhanden ist. Dies wird besonders in der Radartechnik verwendet, wo die meiste Zeit kein SOI vorhanden ist und der gesamte Erkennungsprozess darin besteht, eine Reihe von Winkeln zu testen, ob ein SOI vorhanden ist. Wenn wir :code:`R` vor dem SOI berechnen, können wir Gewichte mit Methoden wie MVDR berechnen, die nur die Störer und Rauschbedingungen in der Kovarianzmatrix enthalten. So besteht keine Gefahr, dass MVDR eine Nullstelle in oder nahe der Richtung des SOI platziert. + +Zur Demonstration des Wertes von Trainingsdaten verwenden wir Aufzeichnungen eines echten 16-Element-Arrays (QUAD-MxFE-Plattform von Analog Devices). Zunächst führen wir MVDR wie üblich durch, wobei das gesamte empfangene Signal zur Berechnung von :code:`R` verwendet wird. Dann verwenden wir eine separate Aufzeichnung (ohne SOI) für :code:`R`. + +Die Aufzeichnungen wurden bei einer HF-Frequenz von 3,3 GHz mit einem Array mit 0,045 m Abstand (d = 0,495) und einer Abtastrate von 30 MHz aufgenommen. Wir bezeichnen die drei Signale als A, B und C. Signal C ist das SOI, A und B sind Störsender. Daher benötigen wir eine Aufzeichnung nur mit A und B für die Trainingsdaten. + +Aufzeichnungsdateien: + +https://github.com/777arc/777arc.github.io/raw/master/3p3G_A_B.npy + +https://github.com/777arc/777arc.github.io/raw/master/3p3G_A_B_C.npy + +Normale MVDR mit der A_B_C-Aufzeichnung: + +.. code-block:: python + + import matplotlib.pyplot as plt + import numpy as np + + # Array-Parameter + center_freq = 3.3e9 + sample_rate = 30e6 + d = 0.045 * center_freq / 3e8 + print("d:", d) + + # Enthält alle drei Signale; C ist unser SOI + filename = '3p3G_A_B_C.npy' + X = np.load(filename) + Nr = X.shape[0] + +DOA mit MVDR zur Identifizierung der Ankunftswinkel: + +.. code-block:: python + + # DOA durchführen, um Ankunftswinkel von C zu finden + theta_scan = np.linspace(-1*np.pi/2, np.pi/2, 10000) # zwischen -90 und +90 Grad + results = [] + R = X @ X.conj().T # Kovarianzmatrix berechnen + Rinv = np.linalg.pinv(R) + for theta_i in theta_scan: + a = np.exp(2j * np.pi * d * np.arange(X.shape[0]) * np.sin(theta_i)) # Steuervektor + a = a.reshape(-1,1) + power = 1/(a.conj().T @ Rinv @ a).squeeze() # MVDR-Leistungsgleichung + power_dB = 10*np.log10(np.abs(power)) + results.append(power_dB) + results -= np.max(results) # auf 0 dB am Peak normalisieren + +.. image:: ../_images/DOA_without_training.svg + :align: center + :target: ../_images/DOA_without_training.svg + :alt: DOA ohne Trainingsdaten + +Ankunftswinkel von C extrahieren: + +.. code-block:: python + + # Winkel von C extrahieren, nach Nullen bei den Störsenderwinkeln + results_temp = np.array(results) + results_temp[int(len(results)*0.4):] = -9999*np.ones(int(len(results)*0.6)) + max_angle = theta_scan[np.argmax(results_temp)] # Radiant + print("max_angle:", max_angle) + +MVDR-Gewichte berechnen: + +.. code-block:: python + + # MVDR-Gewichte berechnen + s = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(max_angle)) # Steuervektor + s = s.reshape(-1,1) + w = (Rinv @ s)/(s.conj().T @ Rinv @ s) # MVDR/Capon-Gleichung + +.. raw:: html + +
+ Plot-Code aufklappen (nichts Neues) + +.. code-block:: python + + # Strahlmuster berechnen + w = w.squeeze() + N_fft = 2048 + w_padded = np.concatenate((w, np.zeros(N_fft - Nr))) + w_fft_dB = 10*np.log10(np.abs(np.fft.fftshift(np.fft.fft(w_padded)))**2) + w_fft_dB -= np.max(w_fft_dB) + theta_bins = np.arcsin(np.linspace(-1, 1, N_fft)) + + # Strahlmuster und DOA-Ergebnisse plotten + plt.plot(theta_bins * 180 / np.pi, w_fft_dB) + plt.plot(theta_scan * 180 / np.pi, results, 'r') + plt.vlines(ymax=np.max(results), ymin=np.min(results) , x=max_angle*180/np.pi, color='g', linestyle='--') + plt.xlabel("Winkel [Grad]") + plt.ylabel("Magnitude [dB]") + plt.title("Strahlmuster und DOA-Ergebnisse, ohne Training") + plt.grid() + plt.show() + +.. raw:: html + +
+ +.. image:: ../_images/DOA_without_training_pattern.svg + :align: center + :target: ../_images/DOA_without_training_pattern.svg + :alt: DOA ohne Trainingsdaten: DOA und MVDR-Strahlmuster + +Wir erzeugen erfolgreich Nullstellen bei A und B. Das Lobe bei C ist jedoch nicht so stark, da der Hauptstrahl gegen die Nullstellen kämpft. Um einen starken Hauptstrahl bei unserem :code:`max_angle` zu erhalten, verwenden wir **Trainingsdaten**. + +Laden der A-B-Aufzeichnung als Trainingsdaten: + +.. code-block:: python + + # Trainingsdaten laden (nur A und B), dann Rinv berechnen + filename = '3p3G_A_B.npy' + X_A_B = np.load(filename) + R_training = X_A_B @ X_A_B.conj().T # Kovarianzmatrix berechnen + Rinv_training = np.linalg.pinv(R_training) + +MVDR-Gewichte mit Trainings-Rinv berechnen: + +.. code-block:: python + + # MVDR-Gewichte mit Trainings-Rinv berechnen + s = np.exp(2j * np.pi * d * np.arange(Nr) * np.sin(max_angle)) # Steuervektor + s = s.reshape(-1,1) + w = (Rinv_training @ s)/(s.conj().T @ Rinv_training @ s) # MVDR/Capon-Gleichung + +.. image:: ../_images/DOA_with_training.svg + :align: center + :target: ../_images/DOA_with_training.svg + :alt: DOA mit Trainingsdaten: DOA und MVDR-Strahlmuster + +Jetzt gibt es Nullstellen bei A und B, aber dieses Mal einen massiven Hauptstrahl in Richtung unseres Interessenwinkels C. Das ist die Stärke von Trainingsdaten. + +******************************* +Breitbandstörer simulieren +******************************* + +Die Methode, die wir bisher verwendet haben, um Signale zu simulieren, die aus einem bestimmten Ankunftswinkel auf unser Array treffen, verwendet eine Schmalbandannahme – d. h. das Signal wird als eine einzelne Frequenz angenommen. Dies funktioniert nicht gut für Breitbandsignale (z. B. mit einer Bandbreite größer als etwa 5% der Mittenfrequenz). Wir beschreiben kurz einen Trick zur Simulation von Breitband-**Rauschen** aus einer bestimmten Richtung (z. B. Barrage-Jamming). + +Die Methode baut eine Kovarianzmatrix :code:`R`, indem die Beiträge jeder Breitbandrauschquelle summiert werden. Die Quadratwurzelmatrix :code:`A` wird berechnet und die Samples :code:`X` werden durch „Einfärben" von normalem komplexem Gaußschen Rauschen mit :code:`A` erzeugt. Ein wichtiger Parameter ist :code:`fractional_bw` (Bandbreite des Rauschsignals geteilt durch Mittenfrequenz): + +.. code-block:: python + + N = 10 # Anzahl der Elemente im ULA + num_samples = 10000 + d = 0.5 + + num_jammers = 3 + jammer_pow_dB = np.array([30, 30, 30]) # Jammer-Leistungen in dB + jammer_aoa_deg = np.array([-70, -20, 40]) # Jammer-Winkel in Grad + jammer_aoa = np.sin(np.deg2rad(jammer_aoa_deg)) * np.pi + element_gain_dB = np.zeros(N) # Gewinne der Array-Elemente in dB + element_gain_linear = 10.0 ** (element_gain_dB / 10) # in lineare Werte umwandeln + fractional_bw = 0.1 # wenn 0, entspricht die Methode dem traditionellen Ansatz + + # NxN Jammer-Kovarianzmatrix R aufbauen + R = np.zeros((N, N), dtype=complex) + for m in range(N): + for n in range(N): + for j in range(num_jammers): + total_element_gain = np.sqrt(element_gain_linear[m] * element_gain_linear[n]) + sinc_term = np.sinc(0.5 * fractional_bw * (m - n) * jammer_aoa[j] / np.pi) + exp_term = np.exp(1j * (m - n) * jammer_aoa[j]) + R[m, n] += 10.0 ** (jammer_pow_dB[j] / 10) * total_element_gain * sinc_term * exp_term + R = np.eye(N, dtype=complex) + R + + # Empfangene Samples erzeugen + A = fractional_matrix_power(R, 0.5) # Matrix-Quadratwurzel berechnen + A = A / np.sqrt(2) + X = np.zeros((N, num_samples), dtype=complex) + for k in range(num_samples): + noise_vec = np.random.randn(N) + 1j * np.random.randn(N) # komplexes Rauschen + X[:, k] = A.conj().T @ noise_vec + +Mit :code:`fractional_bw=0` (Schmalband-Annahme): + +.. image:: ../_images/doa_covariance_method_1.svg + :align: center + :target: ../_images/doa_covariance_method_1.svg + :alt: DOA-Kovarianzmethod mit Bruchbandbreite 0 + +Mit :code:`fractional_bw=0.1` (Breitbandrauschen), wodurch MVDR viel breitere Nullstellen erzeugt: + +.. image:: ../_images/doa_covariance_method_2.svg + :align: center + :target: ../_images/doa_covariance_method_2.svg + :alt: DOA-Kovarianzmethod mit Bruchbandbreite 0.1 + +******************* +Kreisförmige Arrays +******************* + +Wir sprechen kurz über das Uniforme Kreisförmige Array (UCA), das für DOA beliebt ist, da es die 180-Grad-Mehrdeutigkeit von ULAs umgeht. Das KrakenSDR ist z. B. ein 5-Element-Array, und es ist üblich, diese fünf Elemente in einem Kreis mit gleichem Abstand anzuordnen. Theoretisch reichen nur drei Elemente für ein UCA. + +Der gesamte Code, den wir bisher untersucht haben, gilt für UCAs; wir müssen nur die Steuervektor-Gleichung durch eine UCA-spezifische ersetzen: + +.. code-block:: python + + radius = 0.05 # normiert durch Wellenlänge! + d = np.sqrt(2 * radius**2 * (1 - np.cos(2*np.pi/Nr))) + sf = 1.0 / (np.sqrt(2.0) * np.sqrt(1.0 - np.cos(2*np.pi/Nr))) # Skalierungsfaktor + x = d * sf * np.cos(2 * np.pi / Nr * np.arange(Nr)) + y = -1 * d * sf * np.sin(2 * np.pi / Nr * np.arange(Nr)) + s = np.exp(1j * 2 * np.pi * (x * np.cos(theta) + y * np.sin(theta))) + s = s.reshape(-1, 1) # Nrx1 + +Außerdem solltest du von 0 bis 360 Grad scannen, anstatt nur von -90 bis +90 Grad wie bei einem ULA. + +Für 2D-Arrays (z. B. rechteckig) siehe :ref:`2d-beamforming-chapter`. + +************************* +Schlussfolgerung und Referenzen +************************* + +Den gesamten Python-Code, einschließlich des Codes zur Erzeugung der Abbildungen/Animationen, findest du `auf der GitHub-Seite des Lehrbuchs `_. + +* DOA-Implementierung in GNU Radio – https://github.com/EttusResearch/gr-doa +* DOA-Implementierung für KrakenSDR – https://github.com/krakenrf/krakensdr_doa/blob/main/_signal_processing/krakenSDR_signal_processor.py + +[1] Mailloux, Robert J. Phased Array Antenna Handbook. Second edition, Artech House, 2005 + +[2] Van Trees, Harry L. Optimum Array Processing: Part IV of Detection, Estimation, and Modulation Theory. Wiley, 2002. + +.. |br| raw:: html + +
diff --git a/content-de/filters.rst b/content-de/filters.rst new file mode 100644 index 00000000..77a51c11 --- /dev/null +++ b/content-de/filters.rst @@ -0,0 +1,752 @@ +.. _filters-chapter: + +############# +Filter +############# + +In diesem Kapitel lernst du digitale Filter mit Python kennen. Wir behandeln Filtertypen (FIR/IIR sowie Tiefpass/Hochpass/Bandpass/Bandsperr-Filter), wie Filter digital dargestellt werden und wie sie entworfen werden. Wir schließen mit einer Einführung in das Pulsformen, das wir im Kapitel :ref:`pulse-shaping-chapter` weiter vertiefen. + +************************* +Grundlagen der Filter +************************* + +Filter werden in vielen Bereichen eingesetzt. Beispielsweise macht die Bildverarbeitung intensiven Gebrauch von 2D-Filtern, bei denen Ein- und Ausgaben Bilder sind. Vielleicht verwendest du jeden Morgen einen Filter, um deinen Kaffee zu brühen, der Feststoffe von Flüssigkeit trennt. In der DSP werden Filter hauptsächlich verwendet für: + +1. Trennung von Signalen, die kombiniert wurden (z.B. das gewünschte Signal herausfiltern) +2. Entfernung von überschüssigem Rauschen nach dem Empfang eines Signals +3. Wiederherstellung von Signalen, die in irgendeiner Weise verzerrt wurden (z.B. ist ein Audio-Equalizer ein Filter) + +Es gibt sicherlich weitere Anwendungen für Filter, aber dieses Kapitel soll das Konzept einführen, anstatt alle Möglichkeiten des Filterns zu erklären. + +Du denkst vielleicht, dass wir uns nur um digitale Filter kümmern; dieses Lehrbuch befasst sich schließlich mit DSP. Es ist jedoch wichtig zu wissen, dass viele Filter analog sein werden, wie die in unseren SDRs, die vor dem Analog-Digital-Wandler (ADC) auf der Empfangsseite platziert sind. Das folgende Bild stellt einen schematischen Analogfilterschaltkreis einem Ablaufdiagramm eines digitalen Filteralgorithmus gegenüber. + +.. image:: ../_images_de/analog_digital_filter.png + :scale: 70 % + :align: center + :alt: Analoge vs. digitale Filter + +In der DSP, wo Ein- und Ausgaben Signale sind, hat ein Filter ein Eingangssignal und ein Ausgangssignal: + +.. tikz:: [font=\sffamily\Large, scale=2] + \definecolor{babyblueeyes}{rgb}{0.36, 0.61, 0.83} + \node [draw, + color=white, + fill=babyblueeyes, + minimum width=4cm, + minimum height=2.4cm + ] (filter) {Filter}; + \draw[<-, very thick] (filter.west) -- ++(-2,0) node[left,align=center]{Eingang\\(Zeitbereich)} ; + \draw[->, very thick] (filter.east) -- ++(2,0) node[right,align=center]{Ausgang\\(Zeitbereich)}; + :libs: positioning + :xscale: 80 + +Du kannst nicht zwei verschiedene Signale in einen einzigen Filter einspeisen, ohne sie vorher zusammenzuaddieren oder eine andere Operation durchzuführen. Ebenso ist die Ausgabe immer ein Signal, d.h. ein 1D-Array von Zahlen. + +Es gibt vier grundlegende Filtertypen: Tiefpass, Hochpass, Bandpass und Bandsperr. Jeder Typ verändert Signale, um sich auf verschiedene Frequenzbereiche zu konzentrieren. Die folgenden Diagramme zeigen, wie Frequenzen in Signalen für jeden Typ gefiltert werden, zunächst nur mit positiven Frequenzen (leichter zu verstehen), dann auch mit negativen. + +.. image:: ../_images_de/filter_types.png + :scale: 70 % + :align: center + :alt: Filtertypen, einschließlich Tiefpass-, Hochpass-, Bandpass- und Bandsperrfilterung im Frequenzbereich + + +.. START OF FILTER TYPES TIKZ +.. raw:: html + +
+ +.. This draw the lowpass filter +.. tikz:: [font=\sffamily\large] + \draw[->, thick] (-5,0) -- (5,0) node[below]{Frequenz}; + \draw[->, thick] (0,-0.5) node[below]{0 Hz} -- (0,5) node[left=1cm]{\textbf{Tiefpass}}; + \draw[red, thick, smooth] plot[tension=0.5] coordinates{(-5,0) (-2.5,0.5) (-1.5,3) (1.5,3) (2.5,0.5) (5,0)}; + :xscale: 100 + +.. raw:: html + + + +.. this draws the highpass filter +.. tikz:: [font=\sffamily\large] + \draw[->, thick] (-5,0) -- (5,0) node[below]{Frequenz}; + \draw[->, thick] (0,-0.5) node[below]{0 Hz} -- (0,5) node[left=1cm]{\textbf{Hochpass}}; + \draw[red, thick, smooth] plot[tension=0.5] coordinates{(-5,3) (-2.5,2.5) (-1.5,0.3) (1.5,0.3) (2.5,2.5) (5,3)}; + :xscale: 100 + +.. raw:: html + +
+ +.. this draws the bandpass filter +.. tikz:: [font=\sffamily\large] + \draw[->, thick] (-5,0) -- (5,0) node[below]{Frequenz}; + \draw[->, thick] (0,-0.5) node[below]{0 Hz} -- (0,5) node[left=1cm]{\textbf{Bandpass}}; + \draw[red, thick, smooth] plot[tension=0.5] coordinates{(-5,0) (-4.5,0.3) (-3.5,3) (-2.5,3) (-1.5,0.3) (1.5, 0.3) (2.5,3) (3.5, 3) (4.5,0.3) (5,0)}; + :xscale: 100 + +.. raw:: html + + + +.. and finally the bandstop filter +.. tikz:: [font=\sffamily\large] + \draw[->, thick] (-5,0) -- (5,0) node[below]{Frequenz}; + \draw[->, thick] (0,-0.5) node[below]{0 Hz} -- (0,5) node[left=1cm]{\textbf{Bandsperre}}; + \draw[red, thick, smooth] plot[tension=0.5] coordinates{(-5,3) (-4.5,2.7) (-3.5,0.3) (-2.5,0.3) (-1.5,2.7) (1.5, 2.7) (2.5,0.3) (3.5, 0.3) (4.5,2.7) (5,3)}; + :xscale: 100 + +.. raw:: html + +
+ +.. .......................... end of filter plots in tikz + + +Jeder Filter lässt bestimmte Frequenzen in einem Signal durch und blockiert andere. Der Frequenzbereich, den ein Filter durchlässt, wird als „Durchlassbereich" bezeichnet, und der blockierte Bereich heißt „Sperrbereich". Im Fall des Tiefpassfilters lässt er niedrige Frequenzen durch und sperrt hohe Frequenzen, sodass 0 Hz immer im Durchlassbereich liegt. Bei einem Hochpass- und Bandpassfilter liegt 0 Hz immer im Sperrbereich. + +Verwechsle diese Filtertypen nicht mit der algorithmischen Implementierung des Filters (z.B. IIR vs. FIR). Der mit Abstand häufigste Typ ist der Tiefpassfilter (TF), da wir Signale oft im Basisband darstellen. Ein Tiefpassfilter ermöglicht es uns, alles „rund um" unser Signal herauszufiltern und überschüssiges Rauschen sowie andere Signale zu entfernen. + +************************* +Filterdarstellung +************************* + +Für die meisten Filter, die wir sehen werden (bekannt als FIR- oder Finite Impulse Response-Filter), können wir den Filter selbst mit einem einzigen Array von Fließkommazahlen darstellen. Für im Frequenzbereich symmetrische Filter sind diese Zahlen reell (im Gegensatz zu komplex), und es gibt in der Regel eine ungerade Anzahl davon. Wir nennen dieses Array von Fließkommazahlen „Filterkoeffizienten" (engl. filter taps). Wir verwenden oft :math:`h` als Symbol für Filterkoeffizienten. Hier ist ein Beispiel für eine Menge von Filterkoeffizienten, die einen Filter definieren: + +.. code-block:: python + + h = [ 9.92977939e-04 1.08410297e-03 8.51595307e-04 1.64604862e-04 + -1.01714338e-03 -2.46268845e-03 -3.58236429e-03 -3.55412543e-03 + -1.68583512e-03 2.10562324e-03 6.93100252e-03 1.09302641e-02 + 1.17766532e-02 7.60955496e-03 -1.90555639e-03 -1.48306750e-02 + -2.69313236e-02 -3.25659606e-02 -2.63400086e-02 -5.04184562e-03 + 3.08099470e-02 7.64264738e-02 1.23536693e-01 1.62377258e-01 + 1.84320776e-01 1.84320776e-01 1.62377258e-01 1.23536693e-01 + 7.64264738e-02 3.08099470e-02 -5.04184562e-03 -2.63400086e-02 + -3.25659606e-02 -2.69313236e-02 -1.48306750e-02 -1.90555639e-03 + 7.60955496e-03 1.17766532e-02 1.09302641e-02 6.93100252e-03 + 2.10562324e-03 -1.68583512e-03 -3.55412543e-03 -3.58236429e-03 + -2.46268845e-03 -1.01714338e-03 1.64604862e-04 8.51595307e-04 + 1.08410297e-03 9.92977939e-04] + +Anwendungsbeispiel +######################## + +Um zu lernen, wie Filter verwendet werden, schauen wir uns ein Beispiel an, bei dem wir unser SDR auf die Frequenz eines vorhandenen Signals einstellen und es von anderen Signalen isolieren möchten. Denke daran, dass wir unserem SDR mitteilen, auf welche Frequenz es sich einstellen soll, aber die Samples, die das SDR aufnimmt, befinden sich im Basisband, was bedeutet, dass das Signal als um 0 Hz zentriert angezeigt wird. Wir müssen verfolgen, auf welche Frequenz wir das SDR eingestellt haben. Folgendes könnten wir empfangen: + +.. image:: ../_images_de/filter_use_case.png + :scale: 70 % + :align: center + :alt: GNU Radio Frequenzbereichsdiagramm des Nutz-Signals, eines Störsignals und des Rauschbodens + +Da unser Signal bereits bei DC (0 Hz) zentriert ist, wissen wir, dass wir einen Tiefpassfilter wollen. Wir müssen eine „Grenzfrequenz" (auch Eckfrequenz genannt) wählen, die bestimmt, wann der Durchlassbereich in den Sperrbereich übergeht. Die Grenzfrequenz wird immer in Hz angegeben. In diesem Beispiel scheinen 3 kHz ein guter Wert zu sein: + +.. image:: ../_images_de/filter_use_case2.png + :scale: 70 % + :align: center + +Allerdings liegt die negative Frequenzgrenze bei den meisten Tiefpassfiltern ebenfalls bei -3 kHz, d.h. sie ist symmetrisch um DC (später wirst du sehen, warum). Unsere Grenzfrequenzen sehen etwa so aus (der Durchlassbereich ist der Bereich dazwischen): + +.. image:: ../_images_de/filter_use_case3.png + :scale: 70 % + :align: center + +Nach dem Erstellen und Anwenden des Filters mit einer Grenzfrequenz von 3 kHz erhalten wir: + +.. image:: ../_images_de/filter_use_case4.png + :scale: 70 % + :align: center + :alt: GNU Radio Frequenzbereichsdiagramm des Nutz-Signals und eines Störsignals mit herausgefilterter Interferenz + +Dieses gefilterte Signal wird verwirrend aussehen, bis du dich erinnerst, dass unser Rauschboden *bei* der grünen Linie bei etwa -65 dB lag. Obwohl wir das störende Signal bei 10 kHz noch sehen können, haben wir die Leistung dieses Signals *erheblich* reduziert. Sie liegt jetzt unter dem ursprünglichen Rauschboden! Wir haben auch den größten Teil des Rauschens entfernt, das im Sperrbereich vorhanden war. + +Neben der Grenzfrequenz ist der andere Hauptparameter unseres Tiefpassfilters die sogenannte „Übergangsbreite". Die Übergangsbreite, auch in Hz gemessen, gibt dem Filter vor, wie schnell er zwischen Durchlass- und Sperrbereich wechseln muss, da ein sofortiger Übergang unmöglich ist. + +Lass uns die Übergangsbreite visualisieren. Im Diagramm unten stellt die :green:`grüne` Linie die ideale Reaktion für den Übergang zwischen Durchlass- und Sperrbereich dar, die im Wesentlichen eine Übergangsbreite von null hat. Die :red:`rote` Linie demonstriert das Ergebnis eines realistischen Filters, das etwas Welligkeit und eine bestimmte Übergangsbreite aufweist. + +.. image:: ../_images_de/realistic_filter.png + :scale: 100 % + :align: center + :alt: Frequenzgang eines Tiefpassfilters mit Welligkeit und Übergangsbreite + +Du fragst dich vielleicht, warum wir die Übergangsbreite nicht einfach so klein wie möglich machen. Der Grund ist hauptsächlich, dass eine kleinere Übergangsbreite zu mehr Koeffizienten führt, und mehr Koeffizienten bedeuten mehr Berechnungen — wir werden bald sehen, warum. Ein 50-Koeffizienten-Filter kann den ganzen Tag laufen und dabei 1 % der CPU eines Raspberry Pi verwenden. Ein 50.000-Koeffizienten-Filter hingegen würde deine CPU zum Explodieren bringen! Normalerweise verwenden wir ein Filter-Design-Tool, schauen dann, wie viele Koeffizienten es ausgibt, und wenn es viel zu viele sind (z.B. mehr als 100), erhöhen wir die Übergangsbreite. Das hängt natürlich alles von der Anwendung und der Hardware ab, auf der der Filter läuft. + +Im obigen Filterbeispiel haben wir eine Grenzfrequenz von 3 kHz und eine Übergangsbreite von 1 kHz verwendet (es ist schwer, die Übergangsbreite nur durch Betrachten dieser Screenshots zu erkennen). Der resultierende Filter hatte 77 Koeffizienten. + +Zurück zur Filterdarstellung. Obwohl wir möglicherweise die Liste der Koeffizienten für einen Filter zeigen, stellen wir Filter normalerweise visuell im Frequenzbereich dar. Wir nennen das den „Frequenzgang" des Filters, und er zeigt uns das Verhalten des Filters in der Frequenz. Hier ist der Frequenzgang des Filters, den wir gerade verwendet haben: + +.. image:: ../_images_de/filter_use_case5.png + :scale: 100 % + :align: center + +Beachte, dass das, was ich hier zeige, *kein* Signal ist — es ist lediglich die Frequenzbereichsdarstellung des Filters. Das kann anfangs etwas schwer zu begreifen sein, aber wenn wir uns Beispiele und Code ansehen, wird es klar. + +Ein gegebener Filter hat auch eine Zeitbereichsdarstellung; sie wird als „Impulsantwort" des Filters bezeichnet, weil es das ist, was du im Zeitbereich siehst, wenn du einen Impuls durch den Filter schickst. (Suche nach „Dirac-Delta-Funktion" für weitere Informationen darüber, was ein Impuls ist.) Bei einem FIR-Filter sind die Impulsantwort einfach die Koeffizienten selbst. Für den 77-Koeffizienten-Filter, den wir zuvor verwendet haben, sind die Koeffizienten: + +.. code-block:: python + + h = [-0.00025604525581002235, 0.00013669139298144728, 0.0005385575350373983, + 0.0008378280326724052, 0.000906112720258534, 0.0006353431381285191, + -9.884083502996931e-19, -0.0008822851814329624, -0.0017323142383247614, + -0.0021665366366505623, -0.0018335371278226376, -0.0005912294145673513, + 0.001349081052467227, 0.0033936649560928345, 0.004703888203948736, + 0.004488115198910236, 0.0023609865456819534, -0.0013707970501855016, + -0.00564080523326993, -0.008859002031385899, -0.009428252466022968, + -0.006394983734935522, 4.76480351940553e-18, 0.008114570751786232, + 0.015200719237327576, 0.018197273835539818, 0.01482443418353796, + 0.004636279307305813, -0.010356673039495945, -0.025791890919208527, + -0.03587324544787407, -0.034922562539577484, -0.019146423786878586, + 0.011919975280761719, 0.05478153005242348, 0.10243935883045197, + 0.1458890736103058, 0.1762896478176117, 0.18720689415931702, + 0.1762896478176117, 0.1458890736103058, 0.10243935883045197, + 0.05478153005242348, 0.011919975280761719, -0.019146423786878586, + -0.034922562539577484, -0.03587324544787407, -0.025791890919208527, + -0.010356673039495945, 0.004636279307305813, 0.01482443418353796, + 0.018197273835539818, 0.015200719237327576, 0.008114570751786232, + 4.76480351940553e-18, -0.006394983734935522, -0.009428252466022968, + -0.008859002031385899, -0.00564080523326993, -0.0013707970501855016, + 0.0023609865456819534, 0.004488115198910236, 0.004703888203948736, + 0.0033936649560928345, 0.001349081052467227, -0.0005912294145673513, + -0.0018335371278226376, -0.0021665366366505623, -0.0017323142383247614, + -0.0008822851814329624, -9.884083502996931e-19, 0.0006353431381285191, + 0.000906112720258534, 0.0008378280326724052, 0.0005385575350373983, + 0.00013669139298144728, -0.00025604525581002235] + +Und obwohl wir noch nicht mit dem Filterdesign begonnen haben, ist hier der Python-Code, der diesen Filter erzeugt hat: + +.. code-block:: python + + import numpy as np + from scipy import signal + import matplotlib.pyplot as plt + + num_taps = 51 # Es hilft, eine ungerade Anzahl von Koeffizienten zu verwenden + cut_off = 3000 # Hz + sample_rate = 32000 # Hz + + # Tiefpassfilter erstellen + h = signal.firwin(num_taps, cut_off, fs=sample_rate) + + # Impulsantwort darstellen + plt.plot(h, '.-') + plt.show() + +Das einfache Darstellen dieses Arrays von Fließkommazahlen gibt uns die Impulsantwort des Filters: + +.. image:: ../_images_de/impulse_response.png + :scale: 100 % + :align: center + :alt: Beispiel der Impulsantwort eines Filters, die Koeffizienten im Zeitbereich darstellend + +Und hier ist der Code, der verwendet wurde, um den Frequenzgang zu erzeugen, der zuvor gezeigt wurde. Er ist etwas komplizierter, da wir das x-Achsen-Array der Frequenzen erstellen müssen. + +.. code-block:: python + + # Frequenzgang darstellen + H = np.abs(np.fft.fft(h, 1024)) # 1024-Punkt-FFT nehmen und Betrag berechnen + H = np.fft.fftshift(H) # 0 Hz in die Mitte legen + w = np.linspace(-sample_rate/2, sample_rate/2, len(H)) # x-Achse + plt.plot(w, H, '.-') + plt.show() + +Reelle vs. komplexe Filter +############################ + +Der Filter, den ich dir gezeigt habe, hatte reelle Koeffizienten, aber Koeffizienten können auch komplex sein. Ob die Koeffizienten reell oder komplex sind, muss nicht mit dem Signal übereinstimmen, das du durchschickst, d.h. du kannst ein komplexes Signal durch einen Filter mit reellen Koeffizienten schicken und umgekehrt. Wenn die Koeffizienten reell sind, ist der Frequenzgang des Filters symmetrisch um DC (0 Hz). Typischerweise verwenden wir komplexe Koeffizienten, wenn wir Asymmetrie benötigen, was nicht sehr häufig vorkommt. + +.. draw real vs complex filter +.. tikz:: [font=\sffamily\Large,scale=2] + \definecolor{babyblueeyes}{rgb}{0.36, 0.61, 0.83} + \draw[->, thick] (-5,0) node[below]{$-\frac{f_s}{2}$} -- (5,0) node[below]{$\frac{f_s}{2}$}; + \draw[->, thick] (0,-0.5) node[below]{0 Hz} -- (0,1); + \draw[babyblueeyes, smooth, line width=3pt] plot[tension=0.1] coordinates{(-5,0) (-1,0) (-0.5,2) (0.5,2) (1,0) (5,0)}; + \draw[->,thick] (6,0) node[below]{$-\frac{f_s}{2}$} -- (16,0) node[below]{$\frac{f_s}{2}$}; + \draw[->,thick] (11,-0.5) node[below]{0 Hz} -- (11,1); + \draw[babyblueeyes, smooth, line width=3pt] plot[tension=0] coordinates{(6,0) (11,0) (11,2) (11.5,2) (12,0) (16,0)}; + \draw[font=\huge\bfseries] (0,2.5) node[above,align=center]{Tiefpassfilter\\mit reellen Koeffizienten}; + \draw[font=\huge\bfseries] (11,2.5) node[above,align=center]{Tiefpassfilter\\mit komplexen Koeffizienten}; + +Als Beispiel für komplexe Koeffizienten kehren wir zum Filteranwendungsfall zurück, außer dass wir diesmal das andere störende Signal empfangen möchten (ohne das Radio neu einstellen zu müssen). Das bedeutet, wir wollen einen Bandpassfilter, aber keinen symmetrischen. Wir möchten nur Frequenzen zwischen etwa 7 kHz und 13 kHz durchlassen (wir wollen nicht auch -13 kHz bis -7 kHz durchlassen): + +.. image:: ../_images_de/filter_use_case6.png + :scale: 70 % + :align: center + +Eine Möglichkeit, diese Art von Filter zu entwerfen, besteht darin, einen Tiefpassfilter mit einer Grenzfrequenz von 3 kHz zu erstellen und ihn dann im Frequenzbereich zu verschieben. Erinnere dich, dass wir x(t) (Zeitbereich) durch Multiplikation mit :math:`e^{j2\pi f_0t}` im Frequenzbereich verschieben können. In diesem Fall sollte :math:`f_0` 10 kHz sein, was unseren Filter um 10 kHz nach oben verschiebt. Beachte, dass in unserem Python-Code von oben :math:`h` die Filterkoeffizienten des Tiefpassfilters waren. Um unseren Bandpassfilter zu erstellen, müssen wir diese Koeffizienten mit :math:`e^{j2\pi f_0t}` multiplizieren, wobei es erforderlich ist, einen Zeitvektor basierend auf unserer Abtastperiode (Kehrwert der Abtastrate) zu erstellen: + +.. code-block:: python + + # (h wurde mit dem ersten Code-Snippet gefunden) + + # Den Filter im Frequenzbereich verschieben durch Multiplikation mit exp(j*2*pi*f0*t) + f0 = 10e3 # Verschiebungsbetrag + Ts = 1.0/sample_rate # Abtastperiode + t = np.arange(0.0, Ts*len(h), Ts) # Zeitvektor. Argumente sind (Start, Stop, Schritt) + exponential = np.exp(2j*np.pi*f0*t) # das ist im Wesentlichen eine komplexe Sinuswelle + + h_band_pass = h * exponential # Verschiebung durchführen + + # Impulsantwort darstellen + plt.figure('impulse') + plt.plot(np.real(h_band_pass), '.-') + plt.plot(np.imag(h_band_pass), '.-') + plt.legend(['real', 'imag'], loc=1) + + # Frequenzgang darstellen + H = np.abs(np.fft.fft(h_band_pass, 1024)) # 1024-Punkt-FFT und Betrag berechnen + H = np.fft.fftshift(H) # 0 Hz in die Mitte legen + w = np.linspace(-sample_rate/2, sample_rate/2, len(H)) # x-Achse + plt.figure('freq') + plt.plot(w, H, '.-') + plt.xlabel('Frequenz [Hz]') + plt.show() + +Die Diagramme der Impulsantwort und des Frequenzgangs sind unten gezeigt: + +.. image:: ../_images_de/shifted_filter.png + :scale: 60 % + :align: center + +Da unser Filter nicht symmetrisch um 0 Hz ist, muss er komplexe Koeffizienten verwenden. Daher benötigen wir zwei Linien, um diese komplexen Koeffizienten darzustellen. Was wir im linken Diagramm oben sehen, ist immer noch die Impulsantwort. Unser Frequenzgangdiagramm ist das, was wirklich bestätigt, dass wir den gewünschten Filter erstellt haben, der alles außer dem Signal bei 10 kHz herausfiltert. Denke noch einmal daran, dass das obige Diagramm *kein* tatsächliches Signal ist: Es ist lediglich eine Darstellung des Filters. Es kann sehr verwirrend sein zu verstehen, denn wenn du den Filter auf das Signal anwendest und die Ausgabe im Frequenzbereich darstellst, wird es in vielen Fällen ungefähr wie der Frequenzgang des Filters selbst aussehen. + +Wenn dieser Abschnitt zur Verwirrung beigetragen hat, keine Sorge — 99 % der Zeit wirst du sowieso mit einfachen Tiefpassfiltern mit reellen Koeffizienten arbeiten. + +.. _convolution-section: + +*********** +Faltung +*********** + +Wir machen einen kurzen Abstecher, um den Faltungsoperator einzuführen. Überspringe diesen Abschnitt, wenn du bereits damit vertraut bist. + +Zwei Signale zusammenzuaddieren ist eine Möglichkeit, zwei Signale zu einem zu kombinieren. Im Kapitel :ref:`freq-domain-chapter` haben wir untersucht, wie die Linearitätseigenschaft beim Addieren zweier Signale gilt. Faltung ist eine weitere Möglichkeit, zwei Signale zu einem zu kombinieren, aber sie unterscheidet sich sehr stark von der einfachen Addition. Die Faltung zweier Signale ist wie das Schieben eines über das andere und Integrieren. Sie ist der Kreuzkorrelation *sehr* ähnlich, falls du mit dieser Operation vertraut bist. Tatsächlich ist sie in vielen Fällen äquivalent zu einer Kreuzkorrelation. Wir verwenden typischerweise das Symbol :code:`*`, um eine Faltung zu bezeichnen, insbesondere in mathematischen Gleichungen. + +Ich glaube, die Faltungsoperation lässt sich am besten durch Beispiele erlernen. In diesem ersten Beispiel falten wir zwei Rechteckimpulse miteinander: + +.. image:: ../_images_de/rect_rect_conv.gif + :scale: 90 % + :align: center + +Wir haben zwei Eingangssignale (ein rotes, ein blaues), und dann wird die Ausgabe der Faltung in schwarz angezeigt. Du kannst sehen, dass die Ausgabe die Integration der beiden Signale ist, während eines über das andere gleitet. Da es sich um eine gleitende Integration handelt, ist das Ergebnis ein Dreieck mit einem Maximum an dem Punkt, wo beide Rechteckimpulse perfekt übereinander lagen. + +Schauen wir uns einige weitere Faltungen an: + +.. image:: ../_images_de/rect_fat_rect_conv.gif + :scale: 90 % + :align: center + +| + +.. image:: ../_images_de/rect_exp_conv.gif + :scale: 90 % + :align: center + +| + +.. image:: ../_images_de/gaussian_gaussian_conv.gif + :scale: 90 % + :align: center + +Beachte, dass eine Gauß-Kurve, die mit einer Gauß-Kurve gefaltet wird, eine weitere Gauß-Kurve ergibt, aber mit einem breiteren Impuls und niedrigerer Amplitude. + +Aufgrund dieser „gleitenden" Natur ist die Länge der Ausgabe tatsächlich länger als die Eingabe. Wenn ein Signal :code:`M` Samples und das andere Signal :code:`N` Samples hat, kann die Faltung der beiden :code:`N+M-1` Samples erzeugen. Funktionen wie :code:`numpy.convolve()` haben jedoch eine Möglichkeit anzugeben, ob du die gesamte Ausgabe (:code:`max(M, N)` Samples) oder nur die Samples möchtest, wo sich die Signale vollständig überlappten (:code:`max(M, N) - min(M, N) + 1`, falls du neugierig bist). Es ist nicht nötig, sich in dieses Detail zu vertiefen. Wisse nur, dass die Länge der Ausgabe einer Faltung nicht einfach die Länge der Eingaben ist. + +Warum ist Faltung in der DSP wichtig? Nun, zunächst einmal können wir ein Signal einfach filtern, indem wir die Impulsantwort dieses Filters nehmen und sie mit dem Signal falten. FIR-Filterung ist einfach eine Faltungsoperation. + +.. image:: ../_images_de/filter_convolve.png + :scale: 70 % + :align: center + +Es kann verwirrend sein, weil wir früher erwähnt haben, dass die Faltung zwei *Signale* als Eingabe nimmt und eines ausgibt. Wir können die Impulsantwort wie ein Signal behandeln, und Faltung ist schließlich ein mathematischer Operator, der auf zwei 1D-Arrays operiert. Wenn eines dieser 1D-Arrays die Impulsantwort des Filters ist, kann das andere 1D-Array ein Stück des Eingangssignals sein, und die Ausgabe wird eine gefilterte Version der Eingabe sein. + +Schauen wir uns ein weiteres Beispiel an, damit es klarer wird. Im folgenden Beispiel stellt das Dreieck die Impulsantwort unseres Filters dar, und das :green:`grüne` Signal ist unser zu filterndes Signal. + +.. image:: ../_images_de/convolution.gif + :scale: 70 % + :align: center + +Die :red:`rote` Ausgabe ist das gefilterte Signal. + +Frage: Welcher Filtertyp war das Dreieck? + +.. raw:: html + +
+ Antwort + +Es hat die hochfrequenten Komponenten des grünen Signals geglättet (d.h. die scharfen Übergänge des Rechtecks), also wirkt es als Tiefpassfilter. + +.. raw:: html + +
+ + +Nun, da wir beginnen, die Faltung zu verstehen, präsentiere ich die mathematische Gleichung dafür. Das Sternchen (*) wird typischerweise als Symbol für Faltung verwendet: + +.. math:: + + (f * g)(t) = \int f(\tau) g(t - \tau) d\tau + +In dem obigen Ausdruck ist :math:`g(t)` das Signal oder die Eingabe, die gespiegelt wird und über :math:`f(t)` gleitet, aber :math:`g(t)` und :math:`f(t)` können ausgetauscht werden und der Ausdruck bleibt gleich. In der Regel wird das kürzere Array als :math:`g(t)` verwendet. Faltung ist gleich einer Kreuzkorrelation, definiert als :math:`\int f(\tau) g(t+\tau)`, wenn :math:`g(t)` symmetrisch ist, d.h. sich nicht ändert, wenn es um den Ursprung gespiegelt wird. + + +************************* +Filterimplementierung +************************* + +Wir werden nicht zu tief in die Implementierung von Filtern eintauchen. Stattdessen konzentriere ich mich auf das Filterdesign (einsatzbereite Implementierungen findest du sowieso in jeder Programmiersprache). Für jetzt gibt es folgendes zu merken: Um ein Signal mit einem FIR-Filter zu filtern, fältest du einfach die Impulsantwort (das Array von Koeffizienten) mit dem Eingangssignal. In der diskreten Welt verwenden wir eine diskrete Faltung (Beispiel unten). Die als b bezeichneten Dreiecke sind die Koeffizienten. In dem Ablaufdiagramm bedeuten die mit :math:`z^{-1}` beschrifteten Quadrate über den Dreiecken eine Verzögerung um einen Zeitschritt. + +.. image:: ../_images_de/discrete_convolution.png + :scale: 80 % + :align: center + :alt: Implementierung eines FIR-Filters mit Verzögerungen, Koeffizienten und Summationen + +Du kannst jetzt vielleicht verstehen, warum wir sie Filter-„Koeffizienten" (engl. taps) nennen, basierend auf der Art, wie der Filter selbst implementiert ist. + +FIR vs. IIR +############## + +Es gibt zwei Hauptklassen von Digitalfiltern: FIR und IIR + +1. Finite Impulse Response (FIR) — endliche Impulsantwort +2. Infinite Impulse Response (IIR) — unendliche Impulsantwort + +Wir werden nicht zu tief in die Theorie gehen, aber für jetzt erinnere: FIR-Filter sind einfacher zu entwerfen und können alles tun, was du möchtest, wenn du genug Koeffizienten verwendest. IIR-Filter sind komplizierter und können instabil werden, sind aber effizienter (verwenden weniger CPU und Speicher für den gegebenen Filter). Wenn jemand dir einfach eine Liste von Koeffizienten gibt, wird angenommen, dass es sich um Koeffizienten für einen FIR-Filter handelt. Wenn er anfängt, „Pole" zu erwähnen, spricht er von IIR-Filtern. Wir bleiben in diesem Lehrbuch bei FIR-Filtern. + +Unten ist ein Beispiel-Frequenzgang, der einen FIR- und IIR-Filter vergleicht, die fast genau dasselbe Filtern durchführen; sie haben eine ähnliche Übergangsbreite, die wie wir gelernt haben bestimmt, wie viele Koeffizienten benötigt werden. Der FIR-Filter hat 50 Koeffizienten und der IIR-Filter hat 12 Pole, was in Bezug auf die erforderlichen Berechnungen etwa 12 Koeffizienten entspricht. + +.. image:: ../_images_de/FIR_IIR.png + :scale: 70 % + :align: center + :alt: Vergleich von FIR- und IIR-Filtern durch Beobachtung des Frequenzgangs + +Die Lektion ist, dass der FIR-Filter für die ungefähr gleiche Filteroperation erheblich mehr Rechenressourcen benötigt als der IIR. + +Hier sind einige Beispiele aus der realen Welt von FIR- und IIR-Filtern, die du vielleicht schon verwendet hast. + +Wenn du einen „gleitenden Durchschnitt" über eine Liste von Zahlen berechnest, ist das einfach ein FIR-Filter mit Koeffizienten von 1: +- h = [1 1 1 1 1 1 1 1 1 1] für einen gleitenden Durchschnittsfilter mit einer Fenstergröße von 10. Es ist auch ein Tiefpassfilter — warum ist das so? Was ist der Unterschied zwischen der Verwendung von 1en und der Verwendung von Koeffizienten, die auf null abklingen? + +.. raw:: html + +
+ Antwort + +Ein gleitender Durchschnittsfilter ist ein Tiefpassfilter, weil er „hochfrequente" Änderungen glättet, was normalerweise der Grund ist, warum man einen verwenden würde. Der Grund für die Verwendung von Koeffizienten, die auf beiden Enden auf null abklingen, besteht darin, einen plötzlichen Sprung in der Ausgabe zu vermeiden, z.B. wenn das zu filternde Signal eine Weile null war und dann plötzlich hochsprang. + +.. raw:: html + +
+ +Jetzt ein IIR-Beispiel. Hast du schon einmal so etwas gemacht: + + x = x*0.99 + new_value*0.01 + +wobei 0,99 und 0,01 die Aktualisierungsgeschwindigkeit des Wertes (oder die Abklingrate, das Gleiche) darstellen? Es ist eine bequeme Möglichkeit, eine Variable langsam zu aktualisieren, ohne sich die letzten mehreren Werte merken zu müssen. Das ist eigentlich eine Form eines IIR-Tiefpassfilters. Hoffentlich kannst du sehen, warum IIR-Filter weniger stabil sind als FIR. Werte verschwinden nie vollständig! + +************************* +FIR-Filterdesign +************************* + +In der Praxis werden die meisten Menschen ein Filter-Design-Tool oder eine Funktion im Code (z.B. Python/SciPy) verwenden, die den Filter entwirft. Wir beginnen damit zu zeigen, was in Python möglich ist, und gehen dann zu Tools von Drittanbietern über. Unser Fokus liegt auf FIR-Filtern, da sie bei weitem am häufigsten in der DSP verwendet werden. + +Mit Python +################# + +Als Teil des Filterdesigns, bei dem es darum geht, die Filterkoeffizienten für unsere gewünschte Antwort zu generieren, müssen wir den Filtertyp (Tiefpass, Hochpass, Bandpass oder Bandsperre), die Grenzfrequenz(en), die Anzahl der Koeffizienten und optional die Übergangsbreite festlegen. + +Es gibt zwei Hauptfunktionen in SciPy, die wir zum Entwerfen von FIR-Filtern verwenden, beide verwenden die sogenannte Fenstermethode. Zunächst gibt es :code:`scipy.signal.firwin()`, das am einfachsten zu verwenden ist; es liefert die Koeffizienten für einen linearphasigen FIR-Filter. Der Funktion muss die Anzahl der Koeffizienten und die Grenzfrequenz (für Tief-/Hochpass) und zwei Grenzfrequenzen für Bandpass/Bandsperre angegeben werden. Optional kann die Übergangsbreite angegeben werden. Wenn du die Abtastrate über :code:`fs` angibst, sind die Einheiten deiner Grenzfrequenz und Übergangsbreite in Hz, andernfalls sind sie in normalisierten Hz (0 bis 1 Hz). Der Parameter :code:`pass_zero` ist standardmäßig :code:`True`, aber wenn du einen Hochpass- oder Bandpassfilter möchtest, musst du ihn auf :code:`False` setzen; er gibt an, ob 0 Hz im Durchlassbereich enthalten sein soll. Es wird empfohlen, eine ungerade Anzahl von Koeffizienten zu verwenden, und 101 Koeffizienten sind ein guter Ausgangspunkt. Lass uns z.B. einen Bandpassfilter von 100 kHz bis 200 kHz mit einer Abtastrate von 1 MHz generieren: + +.. code-block:: python + + from scipy.signal import firwin + sample_rate = 1e6 + h = firwin(101, [100e3, 200e3], pass_zero=False, fs=sample_rate) + print(h) + +Die zweite Funktion ist :code:`scipy.signal.firwin2()`, die flexibler ist und zum Entwerfen von Filtern mit benutzerdefinierten Frequenzgängen verwendet werden kann, da du ihr eine Liste von Frequenzen und den gewünschten Gewinn bei jeder Frequenz angibst. Sie erfordert ebenfalls die Anzahl der Koeffizienten und unterstützt denselben :code:`fs`-Parameter wie oben erwähnt. Lass uns z.B. einen Filter mit einem Tiefpassbereich bis 100 kHz und einem separaten Bandpassbereich von 200 kHz bis 300 kHz generieren, aber mit dem halben Gewinn des Tiefpassbereichs, und wir verwenden eine Übergangsbreite von 10 kHz: + +.. code-block:: python + + from scipy.signal import firwin2 + sample_rate = 1e6 + freqs = [0, 100e3, 110e3, 190e3, 200e3, 300e3, 310e3, 500e3] + gains = [1, 1, 0, 0, 0.5, 0.5, 0, 0] + h2 = firwin2(101, freqs, gains, fs=sample_rate) + print(h2) + +Um den FIR-Filter tatsächlich auf ein Signal anzuwenden, gibt es mehrere Möglichkeiten, die alle eine Faltungsoperation zwischen den zu filternden Samples und den oben generierten Filterkoeffizienten beinhalten: + +- :code:`np.convolve` +- :code:`scipy.signal.convolve` +- :code:`scipy.signal.fftconvolve` +- :code:`scipy.signal.lfilter` + +Die oben genannten Faltungsfunktionen haben alle einen :code:`mode`-Parameter, der die Optionen :code:`'full'`, :code:`'valid'` oder :code:`'same'` akzeptiert. Der Unterschied liegt in der Größe der Ausgabe, da bei einer Faltung am Anfang und Ende Transienten entstehen, wie wir früher in diesem Kapitel gesehen haben. Die Option :code:`'valid'` enthält keine Transienten, aber die Ausgabe wird etwas kleiner sein als das in die Funktion eingespeiste Signal. Die Option :code:`'same'` gibt eine Ausgabe der gleichen Größe wie das Eingangssignal aus, was nützlich ist, wenn man die Zeit oder andere zeitbereichsbezogene Signalmerkmale verfolgt. Schließlich enthält die Option :code:`'full'` alle Transienten und gibt das gesamte Faltungsergebnis aus. + +Wir werden nun alle vier Funktionen auf die oben erstellten firwin2-Koeffizienten anwenden, und zwar auf ein Testsignal aus weißem Gaußschen Rauschen. Beachte, dass :code:`lfilter` ein zusätzliches Argument hat (das 2. Argument), das für einen FIR-Filter immer 1 ist. + +.. code-block:: python + + import numpy as np + from scipy.signal import firwin2, convolve, fftconvolve, lfilter + + # Testsignal erstellen, wir verwenden Gaußsches Rauschen + sample_rate = 1e6 # Hz + N = 1000 # zu simulierende Samples + x = np.random.randn(N) + 1j * np.random.randn(N) + + # FIR-Filter erstellen, gleicher wie das 2. Beispiel oben + freqs = [0, 100e3, 110e3, 190e3, 200e3, 300e3, 310e3, 500e3] + gains = [1, 1, 0, 0, 0.5, 0.5, 0, 0] + h2 = firwin2(101, freqs, gains, fs=sample_rate) + + # Filter mit vier verschiedenen Methoden anwenden + x_numpy = np.convolve(h2, x) + x_scipy = convolve(h2, x) # SciPy-Faltung + x_fft_convolve = fftconvolve(h2, x) + x_lfilter = lfilter(h2, 1, x) # 2. Arg ist für FIR-Filter immer 1 + + # Beweisen, dass alle dieselbe Ausgabe liefern + print(x_numpy[0:2]) + print(x_scipy[0:2]) + print(x_fft_convolve[0:2]) + print(x_lfilter[0:2]) + +Der obige Code zeigt die grundlegende Verwendung dieser vier Methoden, aber du fragst dich vielleicht, welche die beste ist. Die Diagramme unten zeigen alle vier Methoden mit einem Bereich von Koeffizientengrößen, auf einem Eingangssignal von 1.000 bzw. 100.000 Samples. Es wurde auf einem Intel Core i9-10900K ausgeführt. + +.. image:: ../_images_de/convolve_comparison_1000.svg + :align: center + :target: ../_images_de/convolve_comparison_1000.svg + +.. image:: ../_images_de/convolve_comparison_100000.svg + :align: center + :target: ../_images_de/convolve_comparison_100000.svg + +Wie du sehen kannst, wechselt :code:`scipy.signal.convolve` ab einer bestimmten Eingabegröße automatisch zu einer FFT-basierten Methode. So oder so ist :code:`fftconvolve` der klare Gewinner für diese Koeffizienten- und Eingangsgrößen, die in HF-Anwendungen recht typisch sind. Viel Code in PySDR verwendet tatsächlich :code:`np.convolve`, einfach weil es einen Import weniger ist und der Leistungsunterschied bei niedrigen Datenraten oder Nicht-Echtzeit-Anwendungen vernachlässigbar ist. + +Schließlich zeigen wir die Ausgabe im Frequenzbereich, damit wir endlich überprüfen können, ob die firwin2-Methode uns einen Filter gegeben hat, der unseren Entwurfsparametern entspricht. Ausgehend vom obigen Code, der uns :code:`h2` geliefert hat: + +.. code-block:: python + + # Signal aus Gaußschem Rauschen simulieren + N = 100000 # Signallänge + x = np.random.randn(N) + 1j * np.random.randn(N) # komplexes Signal + + # PSD des Eingangssignals speichern + PSD_input = 10*np.log10(np.fft.fftshift(np.abs(np.fft.fft(x))**2)/len(x)) + + # Filter anwenden + x = fftconvolve(x, h2, 'same') + + # PSD des Ausgangssignals betrachten + PSD_output = 10*np.log10(np.fft.fftshift(np.abs(np.fft.fft(x))**2)/len(x)) + f = np.linspace(-sample_rate/2/1e6, sample_rate/2/1e6, len(PSD_output)) + plt.plot(f, PSD_input, alpha=0.8) + plt.plot(f, PSD_output, alpha=0.8) + plt.xlabel('Frequenz [MHz]') + plt.ylabel('PSD [dB]') + plt.axis([sample_rate/-2/1e6, sample_rate/2/1e6, -40, 20]) + plt.legend(['Eingang', 'Ausgang'], loc=1) + plt.grid() + plt.savefig('../_images/fftconvolve.svg', bbox_inches='tight') + plt.show() + +Wir können sehen, dass der Bandpassbereich 3 dB niedriger ist als der Tiefpassbereich: + +.. image:: ../_images_de/fftconvolve.svg + :align: center + :target: ../_images_de/fftconvolve.svg + +Am Rande gibt es noch eine weitere wenig bekannte Option zum Anwenden des Filters auf ein Signal, namens :code:`scipy.signal.filtfilt`, die „Nullphasenfilterung" durchführt. Sie hilft dabei, Merkmale in einem gefilterten Zeitwellenform genau dort zu erhalten, wo sie im ungefilterten Signal auftreten. Sie tut dies, indem sie die Filterkoeffizienten zweimal anwendet, zuerst in Vorwärtsrichtung und dann in Rückwärtsrichtung. Der Frequenzgang ist also eine quadrierte Version dessen, was man normalerweise erhalten würde. Weitere Informationen findest du unter https://www.mathworks.com/help/signal/ref/filtfilt.html oder https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.filtfilt.html. + +Zustandsbehaftetes Filtern +########################### + +Wenn du eine Echtzeit-Anwendung erstellst und die Filterfunktion auf aufeinanderfolgende Blöcke von Samples aufrufen musst, profitierst du davon, dass dein Filter zustandsbehaftet ist. Das bedeutet, du gibst jedem Aufruf Anfangsbedingungen an, die aus der Ausgabe des vorherigen Filteraufrufs stammen. Dies beseitigt Transienten, die entstehen, wenn ein Signal startet und stoppt (schließlich sind die Samples, die du in nachfolgenden Blöcken einspeist, zusammenhängend, vorausgesetzt, deine Anwendung kann mithalten). Der Zustand muss zwischen den Aufrufen gespeichert werden, und er muss auch ganz zu Beginn deines Codes für den ersten Filteraufruf initialisiert werden. Glücklicherweise enthält SciPy :code:`lfilter_zi`, das Anfangsbedingungen für lfilter konstruiert. Unten ist ein Beispiel für die Verarbeitung von Blöcken zusammenhängender Samples mit zustandsbehaftetem Filtern: + +.. code-block:: python + + b = taps + a = 1 # für FIR, aber nicht 1 für IIR + zi = lfilter_zi(b, a) # Anfangsbedingungen berechnen + while True: + samples = sdr.read_samples(num_samples) # Durch die Empfangsfunktion deines SDR ersetzen + samples_filtered, zi = lfilter(b, a, samples, zi=zi) # Filter anwenden + +Tools von Drittanbietern +######################### + +Du kannst auch Tools außerhalb von Python verwenden, um einen benutzerdefinierten FIR-Filter zu entwerfen. Für Studenten empfehle ich diese benutzerfreundliche Web-App von Peter Isza, die Impuls- und Frequenzgang anzeigt: http://t-filter.engineerjs.com. Mit den Standardwerten (zumindest zum Zeitpunkt des Schreibens) ist sie so eingerichtet, einen Tiefpassfilter mit einem Durchlassband von 0 bis 400 Hz und einem Sperrband ab 500 Hz zu entwerfen. Die Abtastrate beträgt 2 kHz, sodass die maximale Frequenz, die wir „sehen" können, 1 kHz ist. + +.. image:: ../_images_de/filter_designer1.png + :scale: 70 % + :align: center + +Klicke auf die Schaltfläche „Design Filter", um die Koeffizienten zu erstellen und den Frequenzgang darzustellen. + +.. image:: ../_images_de/filter_designer2.png + :scale: 70 % + :align: center + +Klicke auf den Text „Impulse Response" über dem Graphen, um die Impulsantwort zu sehen, die eine Darstellung der Koeffizienten ist, da dies ein FIR-Filter ist. + +.. image:: ../_images_de/filter_designer3.png + :scale: 70 % + :align: center + +Diese App enthält sogar den C++-Quellcode zur Implementierung und Verwendung dieses Filters. Die Web-App enthält keine Möglichkeit, IIR-Filter zu entwerfen, die im Allgemeinen viel schwieriger zu entwerfen sind. + + +**************************** +Beliebiger Frequenzgang +**************************** + +Jetzt werden wir eine Möglichkeit betrachten, selbst einen FIR-Filter in Python zu entwerfen, beginnend mit dem gewünschten Frequenzbereichsgang und rückwärts zur Impulsantwort arbeitend. Letztendlich wird unser Filter so dargestellt (durch seine Koeffizienten). + +Du beginnst damit, einen Vektor deines gewünschten Frequenzgangs zu erstellen. Lass uns einen willkürlich geformten Tiefpassfilter entwerfen, der unten gezeigt wird: + +.. image:: ../_images_de/filter_design1.png + :scale: 70 % + :align: center + +Der Code zur Erstellung dieses Filters ist recht einfach: + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + H = np.hstack((np.zeros(20), np.arange(10)/10, np.zeros(20))) + w = np.linspace(-0.5, 0.5, 50) + plt.plot(w, H, '.-') + plt.show() + + +:code:`hstack()` ist eine Möglichkeit, Arrays in numpy zu verketten. Wir wissen, dass es zu einem Filter mit komplexen Koeffizienten führen wird. Warum? + +.. raw:: html + +
+ Antwort + +Es ist nicht symmetrisch um 0 Hz. + +.. raw:: html + +
+ +Unser Endziel ist es, die Koeffizienten dieses Filters zu finden, damit wir ihn tatsächlich verwenden können. Wie erhalten wir die Koeffizienten aus dem Frequenzgang? Nun, wie konvertieren wir vom Frequenzbereich zurück in den Zeitbereich? Inverse FFT (IFFT)! Erinnere dich, dass die IFFT-Funktion fast genau dieselbe wie die FFT-Funktion ist. Wir müssen auch unseren gewünschten Frequenzgang vor der IFFT mit IFFTshift verschieben, und dann nach der IFFT noch einmal einen IFFTshift anwenden (nein, sie heben sich nicht gegenseitig auf, du kannst es versuchen). Dieser Prozess mag verwirrend erscheinen. Erinnere dich einfach, dass du immer nach einer FFT einen FFTshift und nach einer IFFT einen IFFTshift anwenden solltest. + +.. code-block:: python + + h = np.fft.ifftshift(np.fft.ifft(np.fft.ifftshift(H))) + plt.plot(np.real(h)) + plt.plot(np.imag(h)) + plt.legend(['real','imag'], loc=1) + plt.show() + +.. image:: ../_images_de/filter_design2.png + :scale: 90 % + :align: center + +Wir verwenden diese oben gezeigten Koeffizienten als unseren Filter. Wir wissen, dass die Impulsantwort die Koeffizienten darstellt, also ist das, was wir oben sehen, *unsere* Impulsantwort. Lass uns die FFT unserer Koeffizienten nehmen, um zu sehen, wie der Frequenzbereich tatsächlich aussieht. Wir führen eine 1.024-Punkt-FFT durch, um eine hohe Auflösung zu erhalten: + +.. code-block:: python + + H_fft = np.fft.fftshift(np.abs(np.fft.fft(h, 1024))) + plt.plot(H_fft) + plt.show() + +.. image:: ../_images_de/filter_design3.png + :scale: 70 % + :align: center + +Sieh, wie der Frequenzgang nicht sehr gerade ist... er entspricht nicht sehr gut unserem Original, wenn du dich an die Form erinnerst, für die wir ursprünglich einen Filter erstellen wollten. Ein Hauptgrund ist, dass unsere Impulsantwort noch nicht zu Ende abgeklungen ist, d.h. linke und rechte Seiten erreichen nicht null. Wir haben zwei Möglichkeiten, damit sie auf null abklingt: + +**Option 1:** Wir „fenstern" unsere aktuelle Impulsantwort, sodass sie auf beiden Seiten auf 0 abklingt. Es beinhaltet die Multiplikation unserer Impulsantwort mit einer „Fensterfunktion", die bei null beginnt und endet. + +.. code-block:: python + + # Nach dem Erstellen von h mit dem vorherigen Code, Fenster erstellen und anwenden + window = np.hamming(len(h)) + h = h * window + +.. image:: ../_images_de/filter_design4.png + :scale: 70 % + :align: center + + +**Option 2:** Wir regenerieren unsere Impulsantwort mit mehr Punkten, sodass sie Zeit hat abzuklingen. Wir müssen unsere ursprünglichen Frequenzbereichsarray (Interpolation) auflösen. + +.. code-block:: python + + H = np.hstack((np.zeros(200), np.arange(100)/100, np.zeros(200))) + w = np.linspace(-0.5, 0.5, 500) + plt.plot(w, H, '.-') + plt.show() + # (der Rest des Codes ist derselbe) + +.. image:: ../_images_de/filter_design5.png + :scale: 60 % + :align: center + +.. image:: ../_images_de/filter_design6.png + :scale: 70 % + :align: center + + +.. image:: ../_images_de/filter_design7.png + :scale: 50 % + :align: center + +Beide Optionen haben funktioniert. Welche würdest du wählen? Die zweite Methode führte zu mehr Koeffizienten, aber die erste Methode führte zu einem Frequenzgang, der nicht sehr scharf war und dessen Abfallflanke nicht sehr steil war. Es gibt zahlreiche Möglichkeiten, einen Filter zu entwerfen, jede mit ihren eigenen Kompromissen. Viele betrachten Filterdesign als Kunst. + +************************* +Einführung in das Pulsformen +************************* + +Wir werden kurz ein sehr interessantes Thema in der DSP einführen: das Pulsformen. Wir werden das Thema in einem eigenen Kapitel später ausführlicher behandeln, siehe :ref:`pulse-shaping-chapter`. Es lohnt sich, es neben dem Filtern zu erwähnen, da Pulsformen letztendlich eine Art Filter ist, der für einen bestimmten Zweck mit besonderen Eigenschaften verwendet wird. + +Wie wir gelernt haben, verwenden digitale Signale Symbole, um ein oder mehrere Bits an Information darzustellen. Wir verwenden ein digitales Modulationsschema wie ASK, PSK, QAM, FSK usw., um einen Träger zu modulieren, damit Informationen drahtlos gesendet werden können. Als wir QPSK im Kapitel :ref:`modulation-chapter` simulierten, simulierten wir nur ein Sample pro Symbol, d.h. jede komplexe Zahl, die wir erstellt haben, war einer der Punkte auf dem Konstellationsdiagramm — es war ein Symbol. In der Praxis generieren wir normalerweise mehrere Samples pro Symbol, und der Grund hat mit Filterung zu tun. + +Wir verwenden Filter, um die „Form" unserer Symbole zu gestalten, weil die Form im Zeitbereich die Form im Frequenzbereich ändert. Der Frequenzbereich informiert uns darüber, wie viel Spektrum/Bandbreite unser Signal verwendet, und wir wollen es normalerweise minimieren. Es ist wichtig zu verstehen, dass sich die spektralen Eigenschaften (Frequenzbereich) der Basisbandsymbole nicht ändern, wenn wir einen Träger modulieren; es verschiebt das Basisband nur im Frequenzbereich nach oben, während die Form gleich bleibt, was bedeutet, dass die verwendete Bandbreite gleich bleibt. Wenn wir 1 Sample pro Symbol verwenden, ist es wie das Senden von Rechteckimpulsen. Tatsächlich ist BPSK mit 1 Sample pro Symbol *nur* eine Rechteckwelle aus zufälligen 1en und -1en: + +.. image:: ../_images_de/bpsk.svg + :align: center + :target: ../_images_de/bpsk.svg + +Und wie wir gelernt haben, sind Rechteckimpulse nicht effizient, weil sie eine übermäßige Menge an Spektrum verwenden: + +.. image:: ../_images_de/square-wave.svg + :align: center + +Also „pulsformen" wir diese blockartigen Symbole, damit sie im Frequenzbereich weniger Bandbreite belegen. Wir „pulsformen" durch die Verwendung eines Tiefpassfilters, weil er die höherfrequenten Komponenten unserer Symbole verwirft. Unten ist ein Beispiel für Symbole im Zeit- (oben) und Frequenzbereich (unten), vor und nach der Anwendung eines Pulsformungsfilters: + +.. image:: ../_images_de/pulse_shaping.png + :scale: 70 % + :align: center + +| + +.. image:: ../_images_de/pulse_shaping_freq.png + :scale: 90 % + :align: center + :alt: Demonstration der Pulsformung eines HF-Signals zur Reduzierung der belegten Bandbreite + +Beachte, wie viel schneller das Signal im Frequenzbereich abfällt. Die Nebenzipfel sind nach der Pulsformung ~30 dB niedriger; das ist 1.000-fach weniger! Und noch wichtiger: Die Hauptkeule ist schmaler, sodass für dieselbe Anzahl von Bits pro Sekunde weniger Spektrum genutzt wird. + +Für jetzt solltest du wissen, dass gängige Pulsformungsfilter folgende sind: + +1. Raised-Cosine-Filter +2. Wurzel-Raised-Cosine-Filter +3. Sinc-Filter +4. Gaußscher Filter + +Diese Filter haben im Allgemeinen einen Parameter, den du anpassen kannst, um die verwendete Bandbreite zu verringern. Unten wird der Zeit- und Frequenzbereich eines Raised-Cosine-Filters mit verschiedenen Werten von :math:`\beta` gezeigt, dem Parameter, der definiert, wie steil der Abfall ist. + +.. image:: ../_images_de/pulse_shaping_rolloff.png + :scale: 40 % + :align: center + +Du kannst sehen, dass ein niedrigerer Wert von :math:`\beta` das verwendete Spektrum reduziert (für dieselbe Datenmenge). Wenn der Wert jedoch zu niedrig ist, dauert es länger, bis die Zeitbereichssymbole auf null abklingen. Tatsächlich klingen die Symbole bei :math:`\beta=0` nie vollständig auf null ab, was bedeutet, dass wir solche Symbole in der Praxis nicht übertragen können. Ein :math:`\beta`-Wert von etwa 0,35 ist üblich. + +Du wirst im Kapitel :ref:`pulse-shaping-chapter` noch viel mehr über Pulsformen lernen, einschließlich einiger besonderer Eigenschaften, die Pulsformungsfilter erfüllen müssen. diff --git a/content-de/frequency_domain.rst b/content-de/frequency_domain.rst new file mode 100644 index 00000000..be25fde8 --- /dev/null +++ b/content-de/frequency_domain.rst @@ -0,0 +1,588 @@ +.. _freq-domain-chapter: + +##################### +Frequenzbereich +##################### + +Dieses Kapitel führt den Frequenzbereich ein und behandelt Fourier-Reihen, Fourier-Transformation, Fourier-Eigenschaften, FFT, Fensterfunktionen und Spektrogramme anhand von Python-Beispielen. + +Einer der coolsten Nebeneffekte beim Erlernen von DSP und drahtloser Kommunikation ist, dass du auch lernst, im Frequenzbereich zu denken. Die meiste Erfahrung der Menschen beim *Arbeiten* im Frequenzbereich beschränkt sich auf das Einstellen der Bass-/Mitten-/Höhen-Regler im Audiosystem eines Autos. Die meiste Erfahrung beim *Betrachten* von etwas im Frequenzbereich beschränkt sich auf das Sehen eines Audio-Equalizers, wie in diesem Clip: + +.. image:: ../_images_de/audio_equalizer.webp + :align: center + +Am Ende dieses Kapitels wirst du verstehen, was der Frequenzbereich wirklich bedeutet, wie man zwischen Zeit und Frequenz konvertiert (und was dabei passiert), und einige interessante Prinzipien, die wir in unserem gesamten Studium von DSP und SDR verwenden werden. Am Ende dieses Lehrbuchs wirst du garantiert ein Meister im Arbeiten im Frequenzbereich sein! + +Zunächst: Warum schauen wir uns Signale gerne im Frequenzbereich an? Hier sind zwei Beispielsignale, sowohl im Zeit- als auch im Frequenzbereich dargestellt. + +.. image:: ../_images_de/time_and_freq_domain_example_signals.png + :scale: 40 % + :align: center + :alt: Two signals in the time domain may look like noise, but in the frequency domain we see additional features + +Wie du siehst, sehen sie im Zeitbereich beide irgendwie wie Rauschen aus, aber im Frequenzbereich können wir verschiedene Merkmale erkennen. Alles befindet sich in seiner natürlichen Form im Zeitbereich; wenn wir Signale abtasten, werden wir sie im Zeitbereich abtasten, weil man ein Signal nicht *direkt* im Frequenzbereich abtasten kann. Aber das Interessante passiert normalerweise im Frequenzbereich. + +*************** +Fourier-Reihe +*************** + +Die Grundlagen des Frequenzbereichs beginnen mit dem Verständnis, dass jedes Signal als Summe von Sinuswellen dargestellt werden kann. Wenn wir ein Signal in seine zusammensetzenden Sinuswellen zerlegen, nennen wir das eine Fourier-Reihe. Hier ist ein Beispiel für ein Signal, das nur aus zwei Sinuswellen besteht: + +.. image:: ../_images_de/summing_sinusoids.svg + :align: center + :target: ../_images_de/summing_sinusoids.svg + :alt: Simple example of how a signal can be made up of multiple sinusoids, demonstrating the Fourier Series + +Hier ist ein weiteres Beispiel; die rote Kurve unten approximiert eine Sägezahnwelle durch Summierung von bis zu 10 Sinuswellen. Wir können sehen, dass es keine perfekte Rekonstruktion ist – es würde eine unendliche Anzahl von Sinuswellen benötigen, um diese Sägezahnwelle aufgrund der scharfen Übergänge zu reproduzieren: + +.. image:: ../_images_de/fourier_series_triangle.gif + :scale: 70 % + :align: center + :alt: Animation of the Fourier series decomposition of a triangle wave (a.k.a. sawtooth) + +Einige Signale benötigen mehr Sinuswellen als andere, und einige benötigen eine unendliche Anzahl, obwohl sie immer mit einer begrenzten Anzahl approximiert werden können. Hier ist ein weiteres Beispiel für ein Signal, das in eine Reihe von Sinuswellen zerlegt wird: + +.. image:: ../_images_de/fourier_series_arbitrary_function.gif + :scale: 70 % + :align: center + :alt: Animation of the Fourier series decomposition of an arbitrary function made up of square pulses + +Um zu verstehen, wie wir ein Signal in Sinuswellen oder Sinusoide zerlegen können, müssen wir zunächst die drei Attribute einer Sinuswelle wiederholen: + +#. Amplitude +#. Frequenz +#. Phase + +**Amplitude** gibt die „Stärke" der Welle an, während **Frequenz** die Anzahl der Wellen pro Sekunde ist. **Phase** wird verwendet, um darzustellen, wie die Sinuswelle in der Zeit verschoben ist, von 0 bis 360 Grad (oder 0 bis :math:`2\pi`), aber sie muss relativ zu etwas sein, um eine Bedeutung zu haben, z. B. zwei Signale mit der gleichen Frequenz, die 30 Grad außer Phase zueinander sind. + +.. image:: ../_images_de/amplitude_phase_period.svg + :align: center + :target: ../_images_de/amplitude_phase_period.svg + :alt: Reference diagram of amplitude, phase, and frequency of a sine wave (a.k.a. sinusoid) + +An diesem Punkt hast du vielleicht erkannt, dass ein „Signal" im Wesentlichen nur eine Funktion ist, die normalerweise „über die Zeit" dargestellt wird (d. h. die x-Achse). Ein weiteres Attribut, das leicht zu merken ist, ist die **Periode**, die die Umkehrung der **Frequenz** ist. Die **Periode** eines Sinusoids ist die Zeitspanne in Sekunden, die die Welle benötigt, um einen Zyklus abzuschließen. Daher ist die Einheit der Frequenz 1/Sekunden oder Hz. + +Wenn wir ein Signal in eine Summe von Sinuswellen zerlegen, hat jede eine bestimmte **Amplitude**, **Phase** und **Frequenz**. Die **Amplitude** jeder Sinuswelle sagt uns, wie stark die **Frequenz** im ursprünglichen Signal vorhanden war. Mach dir im Moment nicht zu viele Gedanken über die **Phase**, außer dass du erkennst, dass der einzige Unterschied zwischen sin() und cos() eine Phasenverschiebung (Zeitverschiebung) ist. + +Es ist wichtiger, das zugrundeliegende Konzept zu verstehen als die eigentlichen Gleichungen zur Lösung einer Fourier-Reihe, aber für diejenigen, die an den Gleichungen interessiert sind, verweise ich auf Wolframs prägnante Erklärung: https://mathworld.wolfram.com/FourierSeries.html. + +******************** +Zeit-Frequenz-Paare +******************** + +Wir haben festgestellt, dass Signale als Sinuswellen dargestellt werden können, die mehrere Attribute haben. Lass uns nun lernen, Signale im Frequenzbereich darzustellen. Während der Zeitbereich zeigt, wie ein Signal sich über die Zeit verändert, zeigt der Frequenzbereich, wie viel eines Signals in welchen Frequenzen steckt. Statt dass die x-Achse die Zeit ist, ist sie nun die Frequenz. Wir können ein gegebenes Signal sowohl im Zeit- *als auch* im Frequenzbereich darstellen. Schauen wir uns einige einfache Beispiele an. + +Hier sieht eine Sinuswelle mit Frequenz f im Zeit- und Frequenzbereich aus: + +.. image:: ../_images_de/sine-wave.png + :scale: 70 % + :align: center + :alt: The time-frequency Fourier pair of a sine wave, which is an impulse in the frequency domain + +Der Zeitbereich sollte sehr vertraut aussehen. Es ist eine oszillierende Funktion. Mach dir keine Sorgen darüber, an welchem Punkt im Zyklus es beginnt oder wie lange es dauert. Die wichtigste Erkenntnis ist, dass das Signal eine **einzelne Frequenz** hat, weshalb wir im Frequenzbereich eine einzelne Spitze sehen. Bei welcher Frequenz diese Sinuswelle auch immer schwingt, dort werden wir die Spitze im Frequenzbereich sehen. Der mathematische Name für eine solche Spitze wird „Impuls" genannt. + +Was passiert nun, wenn wir einen Impuls im Zeitbereich haben? Stell dir eine Tonaufnahme vor, bei der jemand in die Hände klatscht oder einen Nagel mit einem Hammer schlägt. Dieses Zeit-Frequenz-Paar ist etwas weniger intuitiv. + +.. image:: ../_images_de/impulse.png + :scale: 70 % + :align: center + :alt: The time-frequency Fourier pair of an impulse in the time domain, which is a horizontal line (all frequencies) in the frequency domain + +Wie wir sehen können, ist eine Spitze/ein Impuls im Zeitbereich im Frequenzbereich flach und enthält theoretisch jede Frequenz. Es gibt keinen theoretisch perfekten Impuls, da er im Zeitbereich unendlich kurz sein müsste. Wie die Sinuswelle spielt es keine Rolle, wo im Zeitbereich der Impuls auftritt. Die wichtigste Erkenntnis hier ist, dass schnelle Änderungen im Zeitbereich zu vielen auftretenden Frequenzen führen. + +Als nächstes schauen wir uns die Zeit- und Frequenzbereichsdarstellungen einer Rechteckwelle an: + +.. image:: ../_images_de/square-wave.svg + :align: center + :target: ../_images_de/square-wave.svg + :alt: The time-frequency Fourier pair of a square wave, which is a sinc (sin(x)/x function) in the frequency domain + +Auch dies ist weniger intuitiv, aber wir können sehen, dass der Frequenzbereich eine starke Spitze hat, die zufällig bei der Frequenz der Rechteckwelle liegt, aber es gibt mehr Spitzen, wenn wir in höhere Frequenzen gehen. Dies liegt an der schnellen Änderung im Zeitbereich, genau wie im vorherigen Beispiel. Aber es ist nicht flach in der Frequenz. Es hat Spitzen in Abständen, und der Pegel nimmt langsam ab (obwohl er für immer weitergeht). Eine Rechteckwelle im Zeitbereich hat ein sin(x)/x-Muster im Frequenzbereich (a.k.a. die Sinc-Funktion). + +Was passiert, wenn wir ein konstantes Signal im Zeitbereich haben? Ein konstantes Signal hat keine „Frequenz". Schauen wir mal: + +.. image:: ../_images_de/dc-signal.png + :scale: 80 % + :align: center + :alt: The time-frequency Fourier pair of a DC signal, which is an impulse at 0 Hz in the frequency domain + +Da es im konstantes Signal im Zeitbereich keine Frequenz gibt, haben wir im Frequenzbereich eine Spitze bei 0 Hz. Das macht Sinn, wenn man darüber nachdenkt. Der Frequenzbereich wird nicht „leer" sein, weil das nur passiert, wenn kein Signal vorhanden ist (d.h. Zeitbereich von 0en). Wir nennen 0 Hz im Frequenzbereich „DC", weil es durch ein DC-Signal in der Zeit verursacht wird (ein konstantes Signal, das sich nicht ändert). Beachte, dass wenn wir die Amplitude unseres DC-Signals im Zeitbereich erhöhen, die Spitze bei 0 Hz im Frequenzbereich ebenfalls zunimmt. + +Später werden wir lernen, was genau die y-Achse im Frequenzbereichsdiagramm bedeutet, aber im Moment kannst du es dir als eine Art Amplitude vorstellen, die dir sagt, wie viel von dieser Frequenz im Zeitbereichssignal vorhanden war. + +***************** +Fourier-Transformation +***************** + +Mathematisch wird die „Transformation", die wir verwenden, um vom Zeitbereich in den Frequenzbereich und zurück zu gehen, als Fourier-Transformation bezeichnet. Sie ist wie folgt definiert: + +.. math:: + X(f) = \int x(t) e^{-j2\pi ft} dt + +Für ein Signal :math:`x(t)` können wir die Frequenzbereichsversion :math:`X(f)` mit dieser Formel erhalten. Wir werden die Zeitbereichsversion einer Funktion mit :math:`x(t)` oder :math:`y(t)` darstellen und die entsprechende Frequenzbereichsversion mit :math:`X(f)` und :math:`Y(f)`. Beachte das :math:`t` für Zeit und :math:`f` für Frequenz. Das :math:`j` ist einfach die imaginäre Einheit; du hast sie vielleicht als :math:`i` im Mathematikunterricht gesehen. In der Ingenieurwissenschaft und Informatik verwenden wir „j", weil „i" oft auf Strom verweist, und in der Programmierung wird es oft als Iterator verwendet. + +Die Rückkehr vom Frequenzbereich in den Zeitbereich ist fast dasselbe, abgesehen von einem negativen Vorzeichen: + +.. math:: + x(t) = \int X(f) e^{j2\pi ft} df + +Beachte, dass viele Lehrbücher und andere Ressourcen :math:`w` anstelle von :math:`2\pi f` verwenden, wobei :math:`w` die Winkelfrequenz in Radiant pro Sekunde ist, während :math:`f` in Hz ist. Alles, was du wissen musst, ist: + +.. math:: + \omega = 2 \pi f + +Auch wenn es einen :math:`2 \pi`-Term zu vielen Gleichungen hinzufügt, ist es einfacher, bei Frequenz in Hz zu bleiben, da wir in Hz bei den meisten SDR- und HF-Signalverarbeitungsanwendungen arbeiten. + +Die obige Gleichung für die Fourier-Transformation ist die kontinuierliche Form, die du nur in mathematischen Problemen sehen wirst. Die diskrete Form ist viel näher an dem, was im Code implementiert wird: + +.. math:: + X_k = \sum_{n=0}^{N-1} x_n e^{-\frac{j2\pi}{N}kn} + +Beachte, dass der Hauptunterschied darin besteht, dass wir das Integral durch eine Summation ersetzt haben. Der Index :math:`k` geht von 0 bis N-1. + +Es ist OK, wenn keine dieser Gleichungen viel für dich bedeutet. Wir müssen sie eigentlich nicht direkt verwenden, um coole Sachen mit DSP und SDRs zu machen! + +************************* +Zeit-Frequenz-Eigenschaften +************************* + +Früher haben wir uns Beispiele angeschaut, wie Signale im Zeit- und Frequenzbereich aussehen. Jetzt werden wir fünf wichtige „Fourier-Eigenschaften" behandeln. Dies sind Eigenschaften, die uns sagen: Wenn wir ____ mit unserem Zeitbereichssignal machen, dann passiert ____ mit unserem Frequenzbereichssignal. Sie geben uns einen wichtigen Einblick in die Art der digitalen Signalverarbeitung (DSP), die wir in der Praxis an Zeitbereichssignalen durchführen werden. + +1. Linearitätseigenschaft: + +.. math:: + a x(t) + b y(t) \leftrightarrow a X(f) + b Y(f) + +Diese Eigenschaft ist wahrscheinlich die einfachste zu verstehen. Wenn wir zwei Signale in der Zeit addieren, wird die Frequenzbereichsversion ebenfalls die beiden addierten Frequenzbereichssignale sein. Es sagt uns auch, dass wenn wir eines davon mit einem Skalierungsfaktor multiplizieren, der Frequenzbereich ebenfalls um denselben Betrag skaliert wird. Der Nutzen dieser Eigenschaft wird deutlicher, wenn wir mehrere Signale zusammenaddieren. + +2. Frequenzverschiebungseigenschaft: + +.. math:: + e^{2 \pi j f_0 t}x(t) \leftrightarrow X(f-f_0) + +Der Term links von x(t) ist das, was wir einen „komplexen Sinus" oder „komplexen Exponential" nennen. Im Moment müssen wir nur wissen, dass es im Wesentlichen nur eine Sinuswelle bei Frequenz :math:`f_0` ist. Diese Eigenschaft sagt uns, dass wenn wir ein Signal :math:`x(t)` nehmen und es mit einer Sinuswelle multiplizieren, wir im Frequenzbereich :math:`X(f)` erhalten, das jedoch um eine bestimmte Frequenz :math:`f_0` verschoben ist. Diese Frequenzverschiebung lässt sich leichter visualisieren: + +.. image:: ../_images_de/freq-shift.svg + :align: center + :target: ../_images_de/freq-shift.svg + :alt: Depiction of a frequency shift of a signal in the frequency domain + +Frequenzverschiebung ist ein wesentlicher Bestandteil von DSP, da wir Signale aus vielen Gründen in der Frequenz nach oben und unten verschieben wollen. Diese Eigenschaft sagt uns, wie das geht (mit einer Sinuswelle multiplizieren). Hier ist eine weitere Möglichkeit, diese Eigenschaft zu visualisieren: + +.. image:: ../_images_de/freq-shift-diagram.svg + :align: center + :target: ../_images_de/freq-shift-diagram.svg + :alt: Visualization of a frequency shift by multiplying by a sine wave or sinusoid + +3. Zeitskalierungseigenschaft: + +.. math:: + x(at) \leftrightarrow X\left(\frac{f}{a}\right) + +Auf der linken Seite der Gleichung sehen wir, dass wir unser Signal x(t) im Zeitbereich skalieren. Hier ist ein Beispiel für ein Signal, das in der Zeit skaliert wird, und dann was mit den Frequenzbereichsversionen passiert. + +.. image:: ../_images_de/time-scaling.svg + :align: center + :target: ../_images_de/time-scaling.svg + :alt: Depiction of the time scaling Fourier transform property in both time and frequency domain + +Skalierung in der Zeit schrumpft oder dehnt das Signal im Wesentlichen auf der x-Achse. Was uns diese Eigenschaft sagt, ist, dass die Skalierung im Zeitbereich eine umgekehrte Skalierung im Frequenzbereich verursacht. Wenn wir zum Beispiel Bits schneller übertragen, müssen wir mehr Bandbreite verwenden. Die Eigenschaft hilft zu erklären, warum Signale mit höherer Datenrate mehr Bandbreite/Spektrum belegen. Wenn Zeit-Frequenz-Skalierung proportional statt umgekehrt proportional wäre, könnten Mobilfunkanbieter so viele Bits pro Sekunde übertragen, wie sie wollten, ohne Milliarden für das Spektrum zu bezahlen! Leider ist das nicht der Fall. + +Diejenigen, die mit dieser Eigenschaft bereits vertraut sind, werden möglicherweise einen fehlenden Skalierungsfaktor bemerken; er wird der Einfachheit halber weggelassen. Für praktische Zwecke macht es keinen Unterschied. + +4. Faltungseigenschaft im Zeitbereich: + +.. math:: + \int x(\tau) y(t-\tau) d\tau \leftrightarrow X(f)Y(f) + +Es wird die Faltungseigenschaft genannt, weil wir im Zeitbereich x(t) und y(t) falten. Du weißt vielleicht noch nichts über die Faltungsoperation, also stell sie dir im Moment wie eine Kreuzkorrelation vor, obwohl wir in :ref:`diesem Abschnitt ` tiefer in Faltungen eintauchen werden. Wenn wir Zeitbereichssignale falten, ist das äquivalent zur Multiplikation der Frequenzbereichsversionen dieser beiden Signale. Es ist sehr verschieden von der Addition zweier Signale. Wenn du zwei Signale addierst, passiert, wie wir gesehen haben, nichts Besonderes; du addierst einfach die Frequenzbereichsversion. Aber wenn du zwei Signale faltest, ist es wie das Erstellen eines neuen dritten Signals aus ihnen. Faltung ist die einzeln wichtigste Technik in DSP, obwohl wir zuerst verstehen müssen, wie Filter funktionieren, um sie vollständig zu begreifen. + +Bevor wir weitermachen, um kurz zu erklären, warum diese Eigenschaft so wichtig ist, betrachte diese Situation: Du hast ein Signal, das du empfangen möchtest, und es gibt ein störendes Signal daneben. + +.. image:: ../_images_de/two-signals.svg + :align: center + :target: ../_images_de/two-signals.svg + +Das Konzept der Maskierung wird in der Programmierung häufig verwendet, also lass es uns hier verwenden. Was wäre, wenn wir die folgende Maske erstellen und sie mit dem Signal oben multiplizieren könnten, um das unerwünschte herauszufiltern? + +.. image:: ../_images_de/masking.svg + :align: center + :target: ../_images_de/masking.svg + +Wir führen DSP-Operationen normalerweise im Zeitbereich durch, also nutzen wir die Faltungseigenschaft, um zu sehen, wie wir diese Maskierung im Zeitbereich durchführen können. Sagen wir, x(t) ist unser empfangenes Signal. Sei Y(f) die Maske, die wir im Frequenzbereich anwenden wollen. Das bedeutet, y(t) ist die Zeitbereichsdarstellung unserer Maske, und wenn wir sie mit x(t) falten, können wir das unerwünschte Signal „herausfiltern". + +.. tikz:: [font=\Large\bfseries\sffamily] + \definecolor{babyblueeyes}{rgb}{0.36, 0.61, 0.83} + \draw (0,0) node[align=center,babyblueeyes] {E.g., our received signal}; + \draw (0,-4) node[below, align=center,babyblueeyes] {E.g., the mask}; + \draw (0,-2) node[align=center,scale=2]{$\int x(\tau)y(t-\tau)d\tau \leftrightarrow X(f)Y(f)$}; + \draw[->,babyblueeyes,thick] (-4,0) -- (-5.5,-1.2); + \draw[->,babyblueeyes,thick] (2.5,-0.5) -- (3,-1.3); + \draw[->,babyblueeyes,thick] (-2.5,-4) -- (-3.8,-2.8); + \draw[->,babyblueeyes,thick] (3,-4) -- (5.2,-2.8); + :xscale: 70 + +Wenn wir über Filterung sprechen, wird die Faltungseigenschaft mehr Sinn ergeben. + +5. Faltungseigenschaft im Frequenzbereich: + +Zuletzt möchte ich darauf hinweisen, dass die Faltungseigenschaft auch umgekehrt funktioniert, obwohl wir sie nicht so häufig wie die Zeitbereichsfaltung verwenden werden: + +.. math:: + x(t)y(t) \leftrightarrow \int X(g) Y(f-g) dg + +Es gibt andere Eigenschaften, aber die obigen fünf sind meiner Meinung nach die wichtigsten zu verstehen. Auch wenn wir den Beweis für jede Eigenschaft nicht durchgegangen sind, benutzen wir die mathematischen Eigenschaften, um Einblick zu gewinnen, was mit realen Signalen passiert, wenn wir Analyse und Verarbeitung durchführen. Lass dich nicht von den Gleichungen ablenken. Stelle sicher, dass du die Beschreibung jeder Eigenschaft verstehst. + + +****************************** +Schnelle Fourier-Transformation (FFT) +****************************** + +Zurück zur Fourier-Transformation. Ich habe dir die Gleichung für die diskrete Fourier-Transformation gezeigt, aber was du beim Codieren 99,9 % der Zeit verwenden wirst, ist die FFT-Funktion fft(). Die Fast Fourier Transform (FFT) ist einfach ein Algorithmus zur Berechnung der diskreten Fourier-Transformation. Sie wurde vor Jahrzehnten entwickelt, und obwohl es Variationen bei der Implementierung gibt, ist sie immer noch der führende Algorithmus zur Berechnung einer diskreten Fourier-Transformation. Zum Glück, wenn man bedenkt, dass sie „Fast" (Schnell) im Namen verwendet haben. + +Die FFT ist eine Funktion mit einem Eingang und einem Ausgang. Sie konvertiert ein Signal von Zeit in Frequenz: + +.. image:: ../_images_de/fft-block-diagram.svg + :align: center + :target: ../_images_de/fft-block-diagram.svg + :alt: FFT is a function with one input (time domain) and one output (frequency domain) + +Wir werden in diesem Lehrbuch nur 1D-FFTs behandeln (2D wird für die Bildverarbeitung und andere Anwendungen verwendet). Für unsere Zwecke stell dir die FFT-Funktion als eine Funktion mit einem Eingang vor: ein Vektor von Samples, und einem Ausgang: die Frequenzbereichsversion dieses Vektors von Samples. Die Größe des Ausgangs ist immer gleich der Größe des Eingangs. Wenn ich 1.024 Samples in die FFT eingebe, erhalte ich 1.024 zurück. Der verwirrende Teil ist, dass der Ausgang immer im Frequenzbereich liegt, und daher ändert sich die „Spanne" der x-Achse, wenn wir sie darstellen würden, nicht basierend auf der Anzahl der Samples im Zeitbereichseingang. Visualisieren wir das, indem wir die Eingangs- und Ausgangsarrays zusammen mit den Einheiten ihrer Indizes betrachten: + +.. image:: ../_images_de/fft-io.svg + :align: center + :target: ../_images_de/fft-io.svg + :alt: Reference diagram for the input (seconds) and output (bandwidth) format of the FFT function showing frequency bins and delta-t and delta-f + +Da der Ausgang im Frequenzbereich liegt, basiert die Spanne der x-Achse auf der Abtastrate, die wir im nächsten Kapitel behandeln werden. Wenn wir mehr Samples für den Eingangsvektor verwenden, erhalten wir eine bessere Auflösung im Frequenzbereich (zusätzlich zur Verarbeitung von mehr Samples auf einmal). Wir „sehen" durch einen größeren Eingang nicht wirklich mehr Frequenzen. Die einzige Möglichkeit wäre, die Abtastrate zu erhöhen (die Abtastperiode :math:`\Delta t` zu verringern). + +Wie stellen wir diesen Ausgang tatsächlich dar? Als Beispiel, sagen wir, unsere Abtastrate war 1 Million Samples pro Sekunde (1 MHz). Wie wir im nächsten Kapitel lernen werden, bedeutet das, dass wir unabhängig davon, wie viele Samples wir in die FFT eingeben, nur Signale bis zu 0,5 MHz sehen können. Der Ausgang der FFT wird wie folgt dargestellt: + +.. image:: ../_images_de/negative-frequencies.svg + :align: center + :target: ../_images_de/negative-frequencies.svg + :alt: Introducing negative frequencies + +Es ist immer der Fall; der Ausgang der FFT zeigt immer :math:`\text{-} f_s/2` bis :math:`f_s/2`, wobei :math:`f_s` die Abtastrate ist. D.h., der Ausgang wird immer einen negativen und einen positiven Teil haben. Wenn der Eingang komplex ist, werden die negativen und positiven Teile unterschiedlich sein, aber wenn er real ist, werden sie identisch sein. + +Bezüglich des Frequenzintervalls entspricht jeder Bin :math:`f_s/N` Hz, d.h. das Eingeben von mehr Samples in jede FFT führt zu einer feineren Auflösung im Ausgang. Ein sehr kleines Detail, das ignoriert werden kann, wenn du neu bist: mathematisch entspricht der letzte Index nicht *genau* :math:`f_s/2`, sondern :math:`f_s/2 - f_s/N`, was für ein großes :math:`N` ungefähr :math:`f_s/2` sein wird. + +******************** +Negative Frequenzen +******************** + +Was zum Teufel ist eine negative Frequenz? Im Moment musst du nur wissen, dass sie mit der Verwendung komplexer Zahlen (imaginäre Zahlen) zusammenhängen – es gibt eigentlich keine „negative Frequenz" beim Senden/Empfangen von HF-Signalen, es ist nur eine Darstellung, die wir verwenden. Hier ist eine intuitive Möglichkeit, darüber nachzudenken. Stell dir vor, wir sagen unserem SDR, es soll auf 100 MHz (das UKW-Radio-Band) abstimmen und mit einer Rate von 10 MHz abtasten. Mit anderen Worten, wir werden das Spektrum von 95 MHz bis 105 MHz betrachten. Vielleicht gibt es drei Signale: + +.. image:: ../_images_de/negative-frequencies2.svg + :align: center + :target: ../_images_de/negative-frequencies2.svg + +Wenn das SDR uns nun die Samples gibt, wird es so aussehen: + +.. image:: ../_images_de/negative-frequencies3.svg + :align: center + :target: ../_images_de/negative-frequencies3.svg + :alt: Negative frequencies are simply the frequencies below the center (a.k.a. carrier) frequency that the radio tuned to + +Denk daran, dass wir das SDR auf 100 MHz abgestimmt haben. Das Signal, das bei etwa 97,5 MHz war, erscheint also bei -2,5 MHz, wenn wir es digital darstellen, was technisch gesehen eine negative Frequenz ist. In Wirklichkeit ist es nur eine Frequenz, die niedriger als die Mittenfrequenz ist. Das wird mehr Sinn ergeben, wenn wir mehr über Sampling lernen und Erfahrung mit unseren SDRs sammeln. + +Aus mathematischer Sicht können negative Frequenzen durch Betrachtung der komplexen Exponentialfunktion :math:`e^{2j \pi f t}` gesehen werden. Wenn wir eine negative Frequenz haben, können wir sehen, dass es ein komplexer Sinus ist, der sich in die entgegengesetzte Richtung dreht. + +.. math:: + e^{2j \pi f t} = \cos(2 \pi f t) + j \sin(2 \pi f t) \quad \mathrm{\textcolor{blue}{blau}} + +.. math:: + e^{2j \pi (-f) t} = \cos(2 \pi f t) - j \sin(2 \pi f t) \quad \mathrm{\textcolor{red}{rot}} + +.. image:: ../_images_de/negative_freq_animation.gif + :align: center + :scale: 75 % + :target: ../_images_de/negative_freq_animation.gif + :alt: Animation of a positive and negative frequency sinusoid on the complex plane + +Der Grund, warum wir den komplexen Exponential oben verwendet haben, ist, dass ein einfaches :math:`cos()` oder :math:`sin()` sowohl positive als auch negative Frequenzen enthält, wie durch die Euler-Formel angewendet auf einen Sinus bei Frequenz :math:`f` über die Zeit :math:`t` zu sehen ist: + +.. math:: + \cos(2 \pi f t) = \underbrace{\frac{1}{2} e^{2j \pi f t}}_\text{positiv} + \underbrace{\frac{1}{2} e^{-2j \pi f t}}_\text{negativ} + +.. math:: + \sin(2 \pi f t) = \underbrace{\frac{1}{2j} e^{2j \pi f t}}_\text{positiv} - \underbrace{\frac{1}{2j} e^{-2j \pi f t}}_\text{negativ} + +In der HF-Signalverarbeitung neigen wir daher dazu, im Allgemeinen komplexe Exponentialfunktionen anstelle von Cosinus und Sinus zu verwenden. + +**************************** +Reihenfolge in der Zeit spielt keine Rolle +**************************** + +Denk daran, dass eine FFT auf vielen Samples gleichzeitig durchgeführt wird, d.h., du kannst den Frequenzbereich eines einzelnen Zeitpunkts (eines Samples) nicht beobachten; es braucht eine Zeitspanne, um damit zu arbeiten (viele Samples). Die FFT-Funktion „mischt" im Wesentlichen das Eingangssignal, um den Ausgang zu bilden, der eine andere Skala und andere Einheiten hat. Wir sind schließlich nicht mehr im Zeitbereich. Eine gute Möglichkeit, diesen Unterschied zwischen den Bereichen zu internalisieren, ist die Erkenntnis, dass das Ändern der Reihenfolge der Ereignisse im Zeitbereich die Frequenzkomponenten im Signal nicht verändert. D.h., das Durchführen **einer einzigen** FFT der folgenden zwei Signale ergibt beide dieselben zwei Spitzen, weil das Signal einfach zwei Sinuswellen bei verschiedenen Frequenzen sind. Das Ändern der Reihenfolge, in der die Sinuswellen auftreten, ändert nicht die Tatsache, dass es zwei Sinuswellen bei verschiedenen Frequenzen sind. Das setzt voraus, dass beide Sinuswellen innerhalb derselben Zeitspanne auftreten, die in die FFT eingegeben wird; wenn du die FFT-Größe verkleinerst und mehrere FFTs durchführst (wie wir es im Spektrogramm-Abschnitt tun werden), kannst du die beiden Sinuswellen unterscheiden. + +.. image:: ../_images_de/fft_signal_order.png + :scale: 50 % + :align: center + :alt: When performing an FFT on a set of samples, the order in time that different frequencies occurred within those samples doesn't change the resulting FFT output + +Technisch gesehen ändert sich die Phase der FFT-Werte aufgrund der Zeitverschiebung der Sinusoide. Für die ersten Kapitel dieses Lehrbuchs werden wir uns jedoch hauptsächlich mit der Magnitude der FFT befassen. + +******************* +FFT in Python +******************* + +Nachdem wir gelernt haben, was eine FFT ist und wie der Ausgang dargestellt wird, lass uns tatsächlich etwas Python-Code ansehen und NumPys FFT-Funktion np.fft.fft() verwenden. Es wird empfohlen, eine vollständige Python-Konsole/IDE auf deinem Computer zu verwenden, aber im Notfall kannst du die verlinkte Online-Web-Python-Konsole am unteren Rand der Navigationsleiste links verwenden. + +Zuerst müssen wir ein Signal im Zeitbereich erstellen. Mach gerne mit deiner eigenen Python-Konsole mit. Um die Dinge einfach zu halten, werden wir eine einfache Sinuswelle bei 0,15 Hz erstellen. Wir werden auch eine Abtastrate von 1 Hz verwenden, was bedeutet, dass wir in der Zeit bei 0, 1, 2, 3 Sekunden usw. abtasten. + +.. code-block:: python + + import numpy as np + t = np.arange(100) + s = np.sin(0.15*2*np.pi*t) + +Wenn wir :code:`s` darstellen, sieht es so aus: + +.. image:: ../_images_de/fft-python1.svg + :target: ../_images_de/fft-python1.svg + :align: center + +Als nächstes verwenden wir NumPys FFT-Funktion: + +.. code-block:: python + + S = np.fft.fft(s) + +Wenn wir uns :code:`S` ansehen, sehen wir, dass es ein Array komplexer Zahlen ist: + +.. code-block:: python + + S = array([-0.01865008 +0.00000000e+00j, -0.01171553 -2.79073782e-01j,0.02526446 -8.82681208e-01j, 3.50536075 -4.71354150e+01j, -0.15045671 +1.31884375e+00j, -0.10769903 +7.10452463e-01j, -0.09435855 +5.01303240e-01j, -0.08808671 +3.92187956e-01j, -0.08454414 +3.23828386e-01j, -0.08231753 +2.76337148e-01j, -0.08081535 +2.41078885e-01j, -0.07974909 +2.13663710e-01j,... + +Hinweis: Egal was du tust, wenn du auf komplexe Zahlen stößt, versuche die Magnitude und die Phase zu berechnen und sieh, ob sie mehr Sinn ergeben. Lass uns genau das tun und die Magnitude und Phase darstellen. In den meisten Sprachen ist abs() eine Funktion für die Magnitude einer komplexen Zahl. Die Funktion für die Phase variiert je nach Programmiersprache, aber in Python können wir NumPys :code:`np.angle()` verwenden, das die Phase in Radiant zurückgibt. + +.. code-block:: python + + import matplotlib.pyplot as plt + S_mag = np.abs(S) + S_phase = np.angle(S) + plt.plot(t,S_mag,'.-') + plt.plot(t,S_phase,'.-') + +.. image:: ../_images_de/fft-python2.svg + :target: ../_images_de/fft-python2.svg + :align: center + +Im Moment geben wir keine x-Achse für die Diagramme an, es ist nur der Index des Arrays (von 0 aufwärts zählend). Aus mathematischen Gründen hat der Ausgang der FFT das folgende Format: + +.. image:: ../_images_de/fft-python3.svg + :align: center + :target: ../_images_de/fft-python3.svg + :alt: Arrangement of the output of an FFT before doing an FFT shift + +Wir wollen jedoch 0 Hz (DC) in der Mitte und negative Frequenzen links (so möchten wir Dinge gerne visualisieren). Jedes Mal, wenn wir eine FFT durchführen, müssen wir daher eine „FFT-Verschiebung" durchführen, was nur eine einfache Array-Neuanordnungsoperation ist, ähnlich wie eine zirkuläre Verschiebung, aber mehr ein „Leg das hierher und das dorthin". Das Diagramm unten definiert vollständig, was die FFT-Verschiebungsoperation tut: + +.. image:: ../_images_de/fft-python4.svg + :align: center + :target: ../_images_de/fft-python4.svg + :alt: Reference diagram of the FFT shift function, showing positive and negative frequencies and DC + +Zu unserer Bequemlichkeit hat NumPy eine FFT-Verschiebungsfunktion, :code:`np.fft.fftshift()`. Ersetze die np.fft.fft()-Zeile durch: + +.. code-block:: python + + S = np.fft.fftshift(np.fft.fft(s)) + +Wir müssen auch die x-Achsenwerte/Beschriftungen herausfinden. Denk daran, dass wir eine Abtastrate von 1 Hz verwendet haben, um die Dinge einfach zu halten. Das bedeutet, dass der linke Rand des Frequenzbereichsdiagramms -0,5 Hz und der rechte Rand 0,5 Hz ist. Wenn das keinen Sinn ergibt, wird es nach dem Kapitel über :ref:`sampling-chapter` klar sein. Bleiben wir bei der Annahme, dass unsere Abtastrate 1 Hz war, und stellen den Ausgang der FFT-Magnitude und -Phase mit einer richtigen x-Achsenbeschriftung dar. Hier ist die endgültige Version dieses Python-Beispiels und die Ausgabe: + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + + Fs = 1 # Hz + N = 100 # Anzahl der zu simulierenden Punkte und unsere FFT-Größe + + t = np.arange(N) # weil unsere Abtastrate 1 Hz ist + s = np.sin(0.15*2*np.pi*t) + S = np.fft.fftshift(np.fft.fft(s)) + S_mag = np.abs(S) + S_phase = np.angle(S) + f = np.arange(Fs/-2, Fs/2, Fs/N) + plt.figure(0) + plt.plot(f, S_mag,'.-') + plt.figure(1) + plt.plot(f, S_phase,'.-') + plt.show() + +.. image:: ../_images_de/fft-python5.svg + :target: ../_images_de/fft-python5.svg + :align: center + +Wir sehen unsere Spitze bei 0,15 Hz, was die Frequenz ist, die wir beim Erstellen der Sinuswelle verwendet haben. Das bedeutet, dass unsere FFT funktioniert hat! Wenn wir den Code, der diese Sinuswelle erzeugt hat, nicht kennen würden, aber nur die Liste der Samples hätten, könnten wir die FFT verwenden, um die Frequenz zu bestimmen. Der Grund, warum wir auch bei -0,15 Hz eine Spitze sehen, hängt damit zusammen, dass es ein reales Signal war, kein komplexes, und wir werden das später vertiefen. + +****************************** +Fensterfunktionen +****************************** + +Wenn wir eine FFT verwenden, um die Frequenzkomponenten unseres Signals zu messen, nimmt die FFT an, dass ihr ein Teil eines *periodischen* Signals gegeben wird. Sie verhält sich so, als ob der von uns bereitgestellte Signalabschnitt sich auf unbestimmte Zeit fortsetzt. Es ist so, als ob das letzte Sample des Abschnitts mit dem ersten Sample verbunden wäre. Dies ergibt sich aus der Theorie hinter der Fourier-Transformation. Es bedeutet, dass wir abrupte Übergänge zwischen dem ersten und letzten Sample vermeiden wollen, da abrupte Übergänge im Zeitbereich wie viele Frequenzen aussehen, und in Wirklichkeit verbindet unser letztes Sample nicht wirklich zurück mit unserem ersten Sample. Einfach ausgedrückt: Wenn wir eine FFT von 100 Samples durchführen und dabei :code:`np.fft.fft(x)` verwenden, wollen wir, dass :code:`x[0]` und :code:`x[99]` gleich oder nahe beieinander liegen. + +Die Art, wie wir diese zyklische Eigenschaft ausgleichen, ist durch „Fensterfunktionen". Direkt vor der FFT multiplizieren wir den Signalabschnitt mit einer Fensterfunktion, die einfach eine beliebige Funktion ist, die an beiden Enden auf null abfällt. Das stellt sicher, dass der Signalabschnitt bei null beginnt und endet und sich verbindet. Gebräuchliche Fensterfunktionen sind Hamming, Hanning, Blackman und Kaiser. Wenn du keine Fensterfunktion anwendest, wird das als Verwendung eines „rechteckigen" Fensters bezeichnet, weil es wie die Multiplikation mit einem Array von Einsen ist. Hier sehen mehrere Fensterfunktionen aus: + +.. image:: ../_images_de/windows.svg + :align: center + :target: ../_images_de/windows.svg + :alt: Windowing function in time and frequency domain of rectangular, hamming, hanning, bartlet, blackman, and kaiser windows + +Ein einfacher Ansatz für Anfänger ist, bei einem Hamming-Fenster zu bleiben, das in Python mit :code:`np.hamming(N)` erstellt werden kann, wobei N die Anzahl der Elemente im Array ist, was deine FFT-Größe ist. In der obigen Übung würden wir das Fenster direkt vor der FFT anwenden. Nach der 2. Codezeile würden wir einfügen: + +.. code-block:: python + + s = s * np.hamming(100) + +Wenn du Angst hast, das falsche Fenster zu wählen, mach dir keine Sorgen. Der Unterschied zwischen Hamming, Hanning, Blackman und Kaiser ist im Vergleich zur Nichtverwendung eines Fensters sehr minimal, da sie alle auf beiden Seiten auf null abfallen und das zugrundeliegende Problem lösen. + + +******************* +FFT-Größenauswahl +******************* + +Das Letzte, was zu beachten ist, ist die FFT-Größe. Die beste FFT-Größe ist immer eine Zweierpotenz, wegen der Art und Weise, wie die FFT implementiert wird. Du kannst eine Größe verwenden, die keine Zweierpotenz ist, aber es wird langsamer sein. Gebräuchliche Größen liegen zwischen 128 und 4.096, obwohl du durchaus größer gehen kannst. In der Praxis müssen wir möglicherweise Signale verarbeiten, die Millionen oder Milliarden von Samples lang sind, daher müssen wir das Signal aufteilen und viele FFTs durchführen. Das bedeutet, dass wir viele Ausgaben erhalten werden. Wir können sie entweder mitteln oder sie über die Zeit darstellen (besonders wenn sich unser Signal über die Zeit ändert). Du musst nicht *jedes* Sample eines Signals durch eine FFT führen, um eine gute Frequenzbereichsdarstellung dieses Signals zu erhalten. Du könntest beispielsweise nur 1.024 von jeweils 100.000 Samples in einem Signal mit FFT verarbeiten und es wird wahrscheinlich trotzdem gut aussehen, solange das Signal immer eingeschaltet ist. + +.. _spectrogram-section: + +********************* +Spektrogramm/Wasserfall +********************* + +Ein Spektrogramm ist die Darstellung, die Frequenz über Zeit zeigt. Es ist einfach eine Reihe von FFTs, die übereinander gestapelt sind (vertikal, wenn du die Frequenz auf der horizontalen Achse haben möchtest). Wir können es auch in Echtzeit anzeigen, oft als Wasserfall bezeichnet. Ein Spektrumanalysator ist das Gerät, das dieses Spektrogramm/diesen Wasserfall anzeigt. Das Diagramm unten zeigt, wie ein Array von IQ-Samples aufgeteilt werden kann, um ein Spektrogramm zu bilden: + +.. image:: ../_images_de/spectrogram_diagram.svg + :align: center + :target: ../_images_de/spectrogram_diagram.svg + :alt: Spectrogram (a.k.a. waterfall) diagram showing how FFT slices are arrange/stacked to form a time-frequency plot + +Da ein Spektrogramm das Darstellen von 2D-Daten beinhaltet, ist es effektiv ein 3D-Diagramm, daher müssen wir eine Farbkarte verwenden, um die FFT-Magnituden darzustellen, die die „Werte" sind, die wir darstellen möchten. Hier ist ein Beispiel eines Spektrogramms mit Frequenz auf der horizontalen/x-Achse und Zeit auf der vertikalen/y-Achse. Blau stellt die niedrigste Energie dar und Rot ist die höchste. Wir können sehen, dass es bei DC (0 Hz) in der Mitte eine starke Spitze gibt, mit einem variierenden Signal darum herum. Blau stellt unseren Rauschpegel dar. + +.. image:: ../_images_de/waterfall.png + :scale: 120 % + :align: center + +Denk daran, es sind nur Reihen von FFTs, die aufeinander gestapelt sind, jede Reihe ist 1 FFT (technisch gesehen die Magnitude von 1 FFT). Stelle sicher, dass du dein Eingangssignal in Scheiben deiner FFT-Größe aufteilst (z. B. 1.024 Samples pro Scheibe). Bevor wir in den Code zum Erstellen eines Spektrogramms einsteigen, hier ist ein Beispielsignal, das wir verwenden werden; es ist einfach ein Ton im weißem Rauschen: + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + + sample_rate = 1e6 + + # Ton plus Rauschen generieren + t = np.arange(1024*1000)/sample_rate # Zeitvektor + f = 50e3 # Frequenz des Tons + x = np.sin(2*np.pi*f*t) + 0.2*np.random.randn(len(t)) + +Hier ist, wie es im Zeitbereich aussieht (erste 200 Samples): + +.. image:: ../_images_de/spectrogram_time.svg + :align: center + :target: ../_images_de/spectrogram_time.svg + +In Python können wir ein Spektrogramm wie folgt generieren: + +.. code-block:: python + + # Simuliere das obige Signal oder verwende dein eigenes Signal + + fft_size = 1024 + num_rows = len(x) // fft_size # // ist eine ganzzahlige Division, die abrundet + spectrogram = np.zeros((num_rows, fft_size)) + for i in range(num_rows): + spectrogram[i,:] = 10*np.log10(np.abs(np.fft.fftshift(np.fft.fft(x[i*fft_size:(i+1)*fft_size])))**2) + + # Zeit beginnt oben und geht nach unten, z.B. wird Sample x[0] Teil der obersten angezeigten Zeile sein + plt.imshow(spectrogram, aspect='auto', extent = [sample_rate/-2/1e6, sample_rate/2/1e6, len(x)/sample_rate, 0]) + plt.xlabel("Frequenz [MHz]") + plt.ylabel("Zeit [s]") + plt.show() + +Dies sollte das Folgende erzeugen, was kein sehr interessantes Spektrogramm ist, da es kein zeitvariables Verhalten gibt. Es gibt zwei Töne, weil wir ein reales Signal simuliert haben, und reale Signale haben immer eine negative PSD, die mit der positiven Seite übereinstimmt. Beachte, dass bei dieser Implementierung die oberste Zeile dem Beginn des Signals entspricht. Für interessantere Beispiele von Spektrogrammen schau dir https://www.IQEngine.org an! + +.. image:: ../_images_de/spectrogram.svg + :align: center + :target: ../_images_de/spectrogram.svg + +********************* +FFT-Implementierung +********************* + +Auch wenn NumPy die FFT bereits für uns implementiert hat, ist es schön zu wissen, wie sie im Wesentlichen funktioniert. Der beliebteste FFT-Algorithmus ist der Cooley-Tukey-FFT-Algorithmus, der um 1805 von Carl Friedrich Gauss erfunden und später 1965 von James Cooley und John Tukey wiederentdeckt und popularisiert wurde. + +Die Grundversion dieses Algorithmus funktioniert bei FFT-Größen als Zweierpotenz und ist für komplexe Eingaben gedacht, kann aber auch mit realen Eingaben arbeiten. Der Baustein dieses Algorithmus ist als Schmetterling bekannt, was im Wesentlichen eine N = 2 große FFT ist, bestehend aus zwei Multiplikationen und zwei Summationen: + +.. image:: ../_images_de/butterfly.svg + :align: center + :target: ../_images_de/butterfly.svg + :alt: Cooley-Tukey FFT algorithm butterfly radix-2 + +oder + +.. math:: + y_0 = x_0 + x_1 w^k_N + + y_1 = x_0 - x_1 w^k_N + +wobei :math:`w^k_N = e^{j2\pi k/N}` als Twiddle-Faktoren bekannt sind (:math:`N` ist die Größe der Teil-FFT und :math:`k` ist der Index). Beachte, dass der Eingang und Ausgang komplex sein soll, z. B. könnte :math:`x_0` 0,6123 - 0,5213j sein, und die Summen/Multiplikationen sind komplex. + +Der Algorithmus ist rekursiv und teilt sich in zwei Hälften, bis alles, was übrig bleibt, eine Reihe von Schmetterlingen ist; dies wird unten anhand einer FFT der Größe 8 dargestellt: + +.. image:: ../_images_de/butterfly2.svg + :align: center + :target: ../_images_de/butterfly2.svg + :alt: Cooley-Tukey FFT algorithm size 8 + +Jede Spalte in diesem Muster ist eine Reihe von Operationen, die parallel durchgeführt werden können, und :math:`log_2(N)` Schritte werden durchgeführt, weshalb die rechnerische Komplexität der FFT O(:math:`N\log N`) beträgt, während eine DFT O(:math:`N^2`) ist. + +Für diejenigen, die lieber in Code als in Gleichungen denken, zeigt folgendes eine einfache Python-Implementierung der FFT, zusammen mit einem Beispielsignal bestehend aus einem Ton plus Rauschen, um die FFT auszuprobieren. + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + + def fft(x): + N = len(x) + if N == 1: + return x + twiddle_factors = np.exp(-2j * np.pi * np.arange(N//2) / N) + x_even = fft(x[::2]) # Rekursion! + x_odd = fft(x[1::2]) + return np.concatenate([x_even + twiddle_factors * x_odd, + x_even - twiddle_factors * x_odd]) + + # Ton + Rauschen simulieren + sample_rate = 1e6 + f_offset = 0.2e6 # 200 kHz Versatz vom Träger + N = 1024 + t = np.arange(N)/sample_rate + s = np.exp(2j*np.pi*f_offset*t) + n = (np.random.randn(N) + 1j*np.random.randn(N))/np.sqrt(2) # Einheitliches komplexes Rauschen + r = s + n # 0 dB SNR + + # FFT durchführen, FFT-Verschiebung, in dB umwandeln + X = fft(r) + X_shifted = np.roll(X, N//2) # äquivalent zu np.fft.fftshift + X_mag = 10*np.log10(np.abs(X_shifted)**2) + + # Ergebnisse darstellen + f = np.linspace(sample_rate/-2, sample_rate/2, N)/1e6 # Darstellung in MHz + plt.plot(f, X_mag) + plt.plot(f[np.argmax(X_mag)], np.max(X_mag), 'rx') # Maximum anzeigen + plt.grid() + plt.xlabel('Frequenz [MHz]') + plt.ylabel('Magnitude [dB]') + plt.show() + + +.. image:: ../_images_de/fft_in_python.svg + :align: center + :target: ../_images_de/fft_in_python.svg + :alt: python implementation of fft example + +Für diejenigen, die an JavaScript- und/oder WebAssembly-basierten Implementierungen interessiert sind, schau dir die `WebFFT `_-Bibliothek für die Durchführung von FFTs in Web- oder NodeJS-Anwendungen an; sie enthält mehrere Implementierungen und es gibt ein `Benchmark-Tool `_, das zur Leistungsvergleich der einzelnen Implementierungen verwendet wird. diff --git a/content-de/hackrf.rst b/content-de/hackrf.rst new file mode 100644 index 00000000..efb06ca6 --- /dev/null +++ b/content-de/hackrf.rst @@ -0,0 +1,273 @@ +.. _hackrf-chapter: + +#################### +HackRF One in Python +#################### + +Das `HackRF One `_ von Great Scott Gadgets ist ein USB 2.0 SDR, das von 1 MHz bis 6 GHz senden oder empfangen kann und eine Abtastrate von 2 bis 20 MHz hat. Es wurde 2014 veröffentlicht und hat im Laufe der Jahre einige kleinere Verbesserungen erfahren. Es ist eines der wenigen kostengünstigen senderfähigen SDRs, das bis auf 1 MHz hinuntergeht, was es ideal für HF-Anwendungen (z.B. Amateurfunk) zusätzlich zu Spaß bei höheren Frequenzen macht. Die maximale Sendeleistung von 15 dBm ist ebenfalls höher als bei den meisten anderen SDRs; vollständige Sendeleistungsspezifikationen findest du `auf dieser Seite `_. Es verwendet Halbduplexbetrieb, d.h. es befindet sich zu einem bestimmten Zeitpunkt entweder im Sende- oder Empfangsmodus, und es verwendet einen 8-Bit-ADC/DAC. + +.. image:: ../_images/hackrf1.jpeg + :scale: 60 % + :align: center + :alt: HackRF One + +******************************** +HackRF-Architektur +******************************** + +Das HackRF basiert auf dem Analog Devices MAX2839-Chip, einem 2,3-GHz-bis-2,7-GHz-Transceiver, der ursprünglich für WiMAX entwickelt wurde, in Kombination mit einem MAX5864-HF-Frontend-Chip (im Wesentlichen nur der ADC und DAC) und einem RFFC5072-Breitband-Synthesizer/VCO (zur Auf- und Abwärtskonvertierung des Signals in der Frequenz). Dies steht im Gegensatz zu den meisten anderen kostengünstigen SDRs, die einen einzelnen Chip namens RFIC verwenden. Abgesehen von der Einstellung der im RFFC5072 erzeugten Frequenz werden alle anderen Parameter, die wir anpassen werden, wie Dämpfung und analoge Filterung, im MAX2839 vorgenommen. Anstatt ein FPGA oder System-on-Chip (SoC) wie viele SDRs zu verwenden, verwendet das HackRF ein Complex Programmable Logic Device (CPLD), das als einfache Verbindungslogik fungiert, und einen Mikrocontroller, den ARM-basierten LPC4320, der alle eingebetteten DSP-Aufgaben und die USB-Schnittstelle mit dem Host übernimmt (sowohl die Übertragung von IQ-Samples in beide Richtungen als auch die Steuerung der SDR-Einstellungen). Das folgende schöne Blockdiagramm von Great Scott Gadgets zeigt die Architektur der neuesten Revision des HackRF One: + +.. image:: ../_images/hackrf_block_diagram.webp + :align: center + :alt: HackRF One Block Diagram + :target: ../_images/hackrf_block_diagram.webp + +Das HackRF One ist sehr erweiterbar und hackbar. Im Inneren des Gehäuses befinden sich vier Header (P9, P20, P22 und P28); Details dazu findest du `hier `_. Beachte, dass sich 8 GPIO-Pins und 4 ADC-Eingänge auf dem P20-Header befinden, während SPI, I2C und UART auf dem P22-Header sind. Der P28-Header kann verwendet werden, um Sende-/Empfangsoperationen mit einem anderen Gerät (z.B. TR-Schalter, externer Verstärker oder ein anderes HackRF) über den Trigger-Eingang und -Ausgang zu triggern/synchronisieren, mit einer Verzögerung von weniger als einer Abtastperiode. + +.. image:: ../_images/hackrf2.jpeg + :scale: 50 % + :align: center + :alt: HackRF One PCB + +Der Takt, der sowohl für den LO als auch für den ADC/DAC verwendet wird, wird entweder vom internen 25-MHz-Oszillator oder von einer externen 10-MHz-Referenz abgeleitet, die über SMA eingespeist wird. Unabhängig davon, welcher Takt verwendet wird, erzeugt das HackRF ein 10-MHz-Taktsignal auf CLKOUT; ein Standard-3,3-V-10-MHz-Rechtecksignal für eine hochohmige Last. Der CLKIN-Anschluss ist dafür ausgelegt, ein ähnliches 10-MHz-3,3-V-Rechtecksignal aufzunehmen, und das HackRF One verwendet den Eingangsclk anstelle des internen Quarzes, wenn ein Taktsignal erkannt wird (die Umschaltung zu oder von CLKIN erfolgt nur, wenn ein Sende- oder Empfangsvorgang beginnt). + +******************************** +Software- und Hardware-Setup +******************************** + +Der Software-Installationsprozess umfasst zwei Schritte: Zuerst installieren wir die HackRF-Hauptbibliothek von Great Scott Gadgets und dann installieren wir die Python-API. + +HackRF-Bibliothek installieren +############################### + +Folgendes wurde auf Ubuntu 22.04 getestet (unter Verwendung von Commit-Hash 17f3943 im März '25): + +.. code-block:: bash + + git clone https://github.com/greatscottgadgets/hackrf.git + cd hackrf + git checkout 17f3943 + cd host + mkdir build + cd build + cmake .. + make + sudo make install + sudo ldconfig + sudo cp /usr/local/bin/hackrf* /usr/bin/. + +Nach der Installation von :code:`hackrf` kannst du folgende Hilfsprogramme ausführen: + +* :code:`hackrf_info` - Geräteinformationen vom HackRF lesen, wie Seriennummer und Firmware-Version. +* :code:`hackrf_transfer` - Signale mit dem HackRF senden und empfangen. Eingabe-/Ausgabedateien sind vorzeichenbehaftete 8-Bit-Quadratur-Samples. +* :code:`hackrf_sweep` - Ein Kommandozeilen-Spektrumanalysator. +* :code:`hackrf_clock` - Takteingangs- und -ausgangskonfiguration lesen und schreiben. +* :code:`hackrf_operacake` - Opera Cake Antennen-Switch, der an HackRF angeschlossen ist, konfigurieren. +* :code:`hackrf_spiflash` - Ein Werkzeug zum Schreiben neuer Firmware auf das HackRF. Siehe: Firmware aktualisieren. +* :code:`hackrf_debug` - Register und andere Low-Level-Konfiguration für das Debugging lesen und schreiben. + +Wenn du Ubuntu über WSL verwendest, musst du auf der Windows-Seite das HackRF USB-Gerät an WSL weiterleiten, zuerst durch die Installation des neuesten `usbipd-Utility-msi `_ (diese Anleitung setzt voraus, dass du usbipd-win 4.0.0 oder höher hast), dann PowerShell im Administratormodus öffnen und folgendes ausführen: + +.. code-block:: bash + + usbipd list + + usbipd bind --busid 1-10 + usbipd attach --wsl --busid 1-10 + +Auf der WSL-Seite solltest du :code:`lsusb` ausführen und ein neues Element namens :code:`Great Scott Gadgets HackRF One` sehen können. Beachte, dass du das Flag :code:`--auto-attach` zum :code:`usbipd attach`-Befehl hinzufügen kannst, wenn es automatisch neu verbinden soll. Zuletzt musst du die udev-Regeln mit folgendem Befehl hinzufügen: + +.. code-block:: bash + + echo 'ATTR{idVendor}=="1d50", ATTR{idProduct}=="6089", SYMLINK+="hackrf-one-%k", MODE="660", TAG+="uaccess"' | sudo tee /etc/udev/rules.d/53-hackrf.rules + sudo udevadm trigger + +Dann das HackRF One ausstecken und wieder einstecken (und den :code:`usbipd attach`-Teil wiederholen). Ich hatte Berechtigungsprobleme mit dem folgenden Schritt, bis ich auf der Windows-Seite `WSL USB Manager `_ für die Weiterleitung zu WSL verwendete, der offenbar auch die udev-Regeln behandelt. + +Ob du auf nativem Linux oder WSL bist, an diesem Punkt solltest du :code:`hackrf_info` ausführen und etwas wie folgendes sehen können: + +.. code-block:: bash + + hackrf_info version: git-17f39433 + libhackrf version: git-17f39433 (0.9) + Found HackRF + Index: 0 + Serial number: 00000000000000007687865765a765 + Board ID Number: 2 (HackRF One) + Firmware Version: 2024.02.1 (API:1.08) + Part ID Number: 0xa000cb3c 0x004f4762 + Hardware Revision: r10 + Hardware appears to have been manufactured by Great Scott Gadgets. + Hardware supported by installed firmware: HackRF One + +Lass uns auch eine IQ-Aufzeichnung des UKW-Bands machen, 10 MHz breit zentriert bei 100 MHz, und wir nehmen 1 Million Samples: + +.. code-block:: bash + + hackrf_transfer -r out.iq -f 100000000 -s 10000000 -n 1000000 -a 0 -l 30 -g 50 + +Dieses Hilfsprogramm erzeugt eine binäre IQ-Datei mit int8-Samples (2 Bytes pro IQ-Sample), die in unserem Fall 2 MB groß sein sollte. Falls du neugierig bist, kann die Signalaufzeichnung in Python mit folgendem Code gelesen werden: + +.. code-block:: python + + import numpy as np + samples = np.fromfile('out.iq', dtype=np.int8) + samples = samples[::2] + 1j * samples[1::2] + print(len(samples)) + print(samples[0:10]) + print(np.max(samples)) + +Wenn dein Maximum 127 ist (was bedeutet, dass du den ADC gesättigt hast), dann senke die beiden Verstärkungswerte am Ende des Befehls. + +Python-API installieren +######################## + +Zuletzt müssen wir die HackRF One `Python-Bindings `_ installieren, die von `GvozdevLeonid `_ gepflegt werden. Dies wurde auf Ubuntu 22.04 am 11.04.2024 mit dem neuesten Main-Branch getestet. + +.. code-block:: bash + + sudo apt install libusb-1.0-0-dev + pip install python_hackrf==1.2.7 + +Wir können die obige Installation testen, indem wir folgenden Code ausführen. Wenn es keine Fehler gibt (es wird auch keine Ausgabe geben), sollte alles einsatzbereit sein! + +.. code-block:: python + + from python_hackrf import pyhackrf # type: ignore + pyhackrf.pyhackrf_init() + sdr = pyhackrf.pyhackrf_open() + sdr.pyhackrf_set_sample_rate(10e6) + sdr.pyhackrf_set_antenna_enable(False) + sdr.pyhackrf_set_freq(100e6) + sdr.pyhackrf_set_amp_enable(False) + sdr.pyhackrf_set_lna_gain(30) # LNA-Verstärkung - 0 bis 40 dB in 8-dB-Schritten + sdr.pyhackrf_set_vga_gain(50) # VGA-Verstärkung - 0 bis 62 dB in 2-dB-Schritten + sdr.pyhackrf_close() + +Für einen tatsächlichen Test des Empfangens von Samples siehe den Beispielcode unten. + +******************************** +Sende- und Empfangsverstärkung +******************************** + +Empfangsseite +############# + +Das HackRF One hat auf der Empfangsseite drei verschiedene Verstärkungsstufen: + +* HF (:code:`amp`, entweder 0 oder 11 dB) +* ZF (:code:`lna`, 0 bis 40 dB in 8-dB-Schritten) +* Basisband (:code:`vga`, 0 bis 62 dB in 2-dB-Schritten) + +Für den Empfang der meisten Signale wird empfohlen, den HF-Verstärker ausgeschaltet (0 dB) zu lassen, es sei denn, du hast es mit einem extrem schwachen Signal zu tun und es gibt definitiv keine starken Signale in der Nähe. Die ZF-(LNA-)Verstärkung ist die wichtigste Verstärkungsstufe, die angepasst werden sollte, um deinen SNR zu maximieren und gleichzeitig eine Sättigung des ADC zu vermeiden – das ist der erste Regler, den du anpassen solltest. Die Basisbandverstärkung kann auf einem relativ hohen Wert belassen werden, z.B. lassen wir sie einfach bei 50 dB. + +Sendeseite +########## + +Auf der Sendeseite gibt es zwei Verstärkungsstufen: + +* HF [entweder 0 oder 11 dB] +* ZF [0 bis 47 dB in 1-dB-Schritten] + +Du wirst wahrscheinlich den HF-Verstärker aktivieren wollen, und dann kannst du die ZF-Verstärkung nach deinen Bedürfnissen anpassen. + +************************************************** +IQ-Samples mit dem HackRF in Python empfangen +************************************************** + +Derzeit enthält das :code:`python_hackrf`-Python-Paket keine Komfortfunktionen zum Empfangen von Samples; es ist lediglich eine Reihe von Python-Bindings, die auf die C++-API des HackRF abgebildet werden. Das bedeutet, dass wir für den Empfang von IQ eine beträchtliche Menge Code verwenden müssen. Das Python-Paket ist so eingerichtet, dass es eine Callback-Funktion zum Empfang weiterer Samples verwendet – das ist eine Funktion, die wir einrichten müssen, die aber automatisch aufgerufen wird, wenn mehr Samples vom HackRF bereit sind. Diese Callback-Funktion muss immer drei spezifische Argumente haben und :code:`0` zurückgeben, wenn wir einen weiteren Satz von Samples wollen. Im folgenden Code konvertieren wir innerhalb jedes Aufrufs unserer Callback-Funktion die Samples in NumPys komplexen Typ, skalieren sie von -1 bis +1 und speichern sie dann in einem größeren :code:`samples`-Array. + +Wenn in deinem Zeitdiagramm die Samples die ADC-Grenzen von -1 und +1 erreichen, reduziere :code:`lna_gain` um 3 dB, bis es klar nicht mehr die Grenzen erreicht. + +.. code-block:: python + + from python_hackrf import pyhackrf # type: ignore + import matplotlib.pyplot as plt + import numpy as np + import time + + # Diese Einstellungen sollten mit dem hackrf_transfer-Beispiel im Lehrbuch übereinstimmen, und der resultierende Wasserfall sollte ungefähr gleich aussehen + recording_time = 1 # Sekunden + center_freq = 100e6 # Hz + sample_rate = 10e6 + baseband_filter = 7.5e6 + lna_gain = 30 # 0 bis 40 dB in 8-dB-Schritten + vga_gain = 50 # 0 bis 62 dB in 2-dB-Schritten + + pyhackrf.pyhackrf_init() + sdr = pyhackrf.pyhackrf_open() + + allowed_baseband_filter = pyhackrf.pyhackrf_compute_baseband_filter_bw_round_down_lt(baseband_filter) # unterstützte Bandbreite relativ zur gewünschten berechnen + + sdr.pyhackrf_set_sample_rate(sample_rate) + sdr.pyhackrf_set_baseband_filter_bandwidth(allowed_baseband_filter) + sdr.pyhackrf_set_antenna_enable(False) # Scheint die Stromversorgung des Antennenanschlusses zu aktivieren/deaktivieren. Standardmäßig False. Die Firmware deaktiviert dies automatisch nach Rückkehr in den IDLE-Modus + + sdr.pyhackrf_set_freq(center_freq) + sdr.pyhackrf_set_amp_enable(False) # Standardmäßig False + sdr.pyhackrf_set_lna_gain(lna_gain) # LNA-Verstärkung - 0 bis 40 dB in 8-dB-Schritten + sdr.pyhackrf_set_vga_gain(vga_gain) # VGA-Verstärkung - 0 bis 62 dB in 2-dB-Schritten + + print(f'center_freq: {center_freq} sample_rate: {sample_rate} baseband_filter: {allowed_baseband_filter}') + + num_samples = int(recording_time * sample_rate) + samples = np.zeros(num_samples, dtype=np.complex64) + last_idx = 0 + + def rx_callback(device, buffer, buffer_length, valid_length): # Diese Callback-Funktion muss immer diese vier Argumente haben + global samples, last_idx + + accepted = valid_length // 2 + accepted_samples = buffer[:valid_length].astype(np.int8) # -128 bis 127 + accepted_samples = accepted_samples[0::2] + 1j * accepted_samples[1::2] # In komplexen Typ konvertieren (IQ entflechten) + accepted_samples /= 128 # -1 bis +1 + samples[last_idx: last_idx + accepted] = accepted_samples + + last_idx += accepted + + return 0 + + sdr.set_rx_callback(rx_callback) + sdr.pyhackrf_start_rx() + print('is_streaming', sdr.pyhackrf_is_streaming()) + + time.sleep(recording_time) + + sdr.pyhackrf_stop_rx() + sdr.pyhackrf_close() + pyhackrf.pyhackrf_exit() + + samples = samples[100000:] # Die ersten 100k Samples verwerfen, nur zur Sicherheit wegen Transienten + + fft_size = 2048 + num_rows = len(samples) // fft_size + spectrogram = np.zeros((num_rows, fft_size)) + for i in range(num_rows): + spectrogram[i, :] = 10 * np.log10(np.abs(np.fft.fftshift(np.fft.fft(samples[i * fft_size:(i+1) * fft_size]))) ** 2) + extent = [(center_freq + sample_rate / -2) / 1e6, (center_freq + sample_rate / 2) / 1e6, len(samples) / sample_rate, 0] + + plt.figure(0) + plt.imshow(spectrogram, aspect='auto', extent=extent) # type: ignore + plt.xlabel("Frequenz [MHz]") + plt.ylabel("Zeit [s]") + + plt.figure(1) + plt.plot(np.real(samples[0:10000])) + plt.plot(np.imag(samples[0:10000])) + plt.xlabel("Samples") + plt.ylabel("Amplitude") + plt.legend(["Real", "Imaginär"]) + + plt.show() + +Wenn du eine Antenne verwendest, die das UKW-Band empfangen kann, solltest du etwas wie folgendes erhalten, mit mehreren UKW-Sendern, die im Wasserfalldiagramm sichtbar sind: + +.. image:: ../_images/hackrf_time_screenshot.png + :align: center + :scale: 50 % + :alt: Time plot of the samples grabbed from HackRF + +.. image:: ../_images/hackrf_freq_screenshot.png + :align: center + :scale: 50 % + :alt: Spectrogram (frequency over time) plot of the samples grabbed from HackRF diff --git a/content-de/intro.rst b/content-de/intro.rst new file mode 100644 index 00000000..09bb59e3 --- /dev/null +++ b/content-de/intro.rst @@ -0,0 +1,72 @@ +.. _intro-chapter: + +############# +Einführung +############# + +*************************** +Zweck und Zielgruppe +*************************** + +Zunächst einige wichtige Begriffe: + +**Software-Defined Radio (SDR):** + Als *Konzept* bezieht es sich auf die Verwendung von Software zur Durchführung von Signalverarbeitungsaufgaben, die traditionell von Hardware durchgeführt wurden, die speziell für Radio-/HF-Anwendungen angedacht wurden. Diese Software kann auf einem Mikroprozessor (CPU), FPGA oder sogar GPU ausgeführt werden und kann für Echtzeitanwendungen oder die Offline-Verarbeitung aufgezeichneter Signale verwendet werden. Synonyme sind „Software-Radio" und „digitale HF-Signalverarbeitung". + + Als *Gerät* (z. B. „ein SDR") bezieht es sich typischerweise auf ein Gerät, an das du eine Antenne anschließen und HF-Signale empfangen kannst, wobei die digitalisierten HF-Samples zur Verarbeitung oder Aufzeichnung an einen Computer gesendet werden (z. B. der Transfer über USB, Ethernet, PCI). Viele SDRs haben auch Sendefähigkeiten, die es dem Computer ermöglichen, Samples an das SDR zu senden, das dann das Signal auf einer bestimmten HF-Frequenz zu übertragen. Einige eingebettete SDRs enthalten einen integrierten Computer. + +**Digitale Signalverarbeitung (DSP):** + Die digitale Verarbeitung von Signalen; in unserem Fall HF-Signale. + +Dieses Lehrbuch dient als praktische Einführung in die Bereiche DSP, SDR und drahtlose Kommunikation. Es richtet sich an jemanden, der: + +#. Daran interessiert ist, SDRs für coole Dinge zu *verwenden* +#. Gut mit Python umgehen kann +#. Relativ neu in den Bereichen DSP, drahtlose Kommunikation und SDR ist +#. Visuell lernt und Animationen gegenüber Gleichungen bevorzugt +#. Gleichungen am besten versteht, *nachdem* die Konzepte gelernt wurden +#. Auf der Suche nach prägnanten Erklärungen ist, nicht nach einem 1.000-seitigen Lehrbuch + +Ein Beispiel wäre ein Informatikstudent, der nach dem Abschluss an einem Job im Bereich drahtlose Kommunikation interessiert ist, obwohl es von jedem genutzt werden kann, der SDR lernen möchte und Programmiererfahrung hat. Als solches deckt es die notwendige Theorie zum Verständnis von DSP-Techniken ab, ohne die intensive Mathematik, die normalerweise in DSP-Kursen enthalten ist. Anstatt uns in Gleichungen zu vergraben, werden viele Bilder und Animationen verwendet, um die Konzepte zu vermitteln, wie die Animation der komplexen Ebene der Fourier-Reihe unten. Ich glaube, dass Gleichungen am besten *nach* dem Erlernen der Konzepte durch Visualisierungen und praktische Übungen verstanden werden. Die intensive Nutzung von Animationen ist der Grund, warum PySDR nie eine gedruckte Version haben wird, die bei Amazon verkauft wird. + +.. image:: ../_images_de/fft_logo_wide.gif + :scale: 70 % + :align: center + :alt: Das PySDR-Logo, erstellt mit einer Fourier-Transformation + +Dieses Lehrbuch soll Konzepte schnell und reibungslos einführen und den Lesern ermöglichen, DSP durchzuführen und SDRs intelligent zu nutzen. Es ist nicht als Referenzlehrbuch für alle DSP/SDR-Themen gedacht; es gibt bereits viele großartige Lehrbücher, wie das `SDR-Lehrbuch von Analog Devices +`_ und `dspguide.com `_. Du kannst immer Google verwenden, um trigonometrische Identitäten oder die Shannon-Grenze nachzuschlagen. Betrachte dieses Lehrbuch als Einstieg in die Welt von DSP und SDR: Es ist leichter und weniger zeitaufwendig und kostenintensiv im Vergleich zu traditionelleren Kursen und Lehrbüchern. + +Um grundlegende DSP-Theorie abzudecken, wird ein gesamtes Semester „Signale und Systeme", ein typischer Kurs in der Elektrotechnik, in wenige Kapitel komprimiert. Sobald die DSP-Grundlagen abgedeckt sind, steigen wir in SDRs ein, obwohl DSP- und drahtlose Kommunikationskonzepte im gesamten Lehrbuch weiterhin auftauchen. + +Codebeispiele werden in Python bereitgestellt. Sie verwenden NumPy, Pythons Standardbibliothek für Arrays und höhere Mathematik. Die Beispiele basieren auch auf Matplotlib, einer Python-Plotbibliothek, die eine einfache Möglichkeit bietet, Signale, Arrays und komplexe Zahlen zu visualisieren. Beachte, dass Python zwar im Allgemeinen „langsamer" als C++ ist, die meisten mathematischen Funktionen in Python/NumPy jedoch in C/C++ implementiert und stark optimiert sind. Ebenso ist die von uns verwendete SDR-API lediglich eine Reihe von Python-Bindungen für C/C++-Funktionen/-Klassen. Diejenigen, die wenig Python-Erfahrung haben, aber eine solide Grundlage in MATLAB, Ruby oder Perl haben, werden wahrscheinlich nach dem Kennenlernen der Python-Syntax gut zurechtkommen. + + +*************** +Mitwirken +*************** + +Wenn du von PySDR profitiert hast, teile es bitte mit Kollegen, Studenten und anderen lebenslangen Lernenden, die an dem Material interessiert sein könnten. Du kannst auch über das `PySDR Patreon `_ spenden, um dich zu bedanken und deinen Namen links auf jeder Seite unter der Kapitelliste zu erhalten. Es gibt auch die Möglichkeit, `eine einmalige Spende zu machen `_. + +Wenn du einen Teil dieses Lehrbuchs durcharbeitest und mir eine E-Mail an marc@pysdr.org mit Fragen/Kommentaren/Vorschlägen schickst, dann herzlichen Glückwunsch, du hast zu diesem Lehrbuch beigetragen! Du kannst das Quellmaterial auch direkt auf der `GitHub-Seite des Lehrbuchs `_ bearbeiten (deine Änderung startet einen neuen Pull-Request). Zögere nicht, ein Issue oder sogar einen Pull Request (PR) mit Korrekturen oder Verbesserungen einzureichen. Diejenigen, die wertvolles Feedback/Korrekturen einreichen, werden dauerhaft zum Abschnitt „Danksagungen" unten hinzugefügt. Nicht gut mit Git, aber hast Änderungsvorschläge? Schreib mir gerne eine E-Mail an marc@pysdr.org. + +***************** +Danksagungen +***************** + +Vielen Dank an alle, die einen Teil dieses Lehrbuchs gelesen und Feedback gegeben haben, und insbesondere an: + +- `Barry Duggan `_ +- Matthew Hannon +- James Hayek +- Deidre Stuffer +- Tarik Benaddi für die `Übersetzung von PySDR ins Französische `_ +- `Daniel Versluis `_ für die `Übersetzung von PySDR ins Niederländische `_ +- `mrbloom `_ für die `Übersetzung von PySDR ins Ukrainische `_ +- `Yimin Zhao `_ für die `Übersetzung von PySDR ins vereinfachte Chinesisch `_ +- `Eduardo Chancay `_ für die `Übersetzung von PySDR ins Spanische `_ +- `Dipl. Ing. (FH) Viet Dang `_ für die `Übersetzung von PySDR ins Deutsche `_ +- John Marcovici +- `Vishwaksen Reddy Dhareddy `_ für den Beitrag zum Abschnitt „Echtzeit-Paketerkennung" im Erkennungskapitel + +Sowie allen `PySDR Patreon `_-Unterstützern! diff --git a/content-de/iq_files.rst b/content-de/iq_files.rst new file mode 100644 index 00000000..bff10e49 --- /dev/null +++ b/content-de/iq_files.rst @@ -0,0 +1,502 @@ +.. _iq-files-chapter: + +################## +IQ-Dateien und SigMF +################## + +In all unseren bisherigen Python-Beispielen haben wir Signale als eindimensionale NumPy-Arrays vom Typ "complex float" gespeichert. In diesem Kapitel lernen wir, wie Signale in einer Datei gespeichert und anschließend wieder in Python eingelesen werden können, und wir stellen den SigMF-Standard vor. Das Speichern von Signaldaten in einer Datei ist äußerst nützlich: Vielleicht möchtest du ein Signal aufzeichnen, um es später manuell offline zu analysieren, es mit einem Kollegen zu teilen oder einen ganzen Datensatz aufzubauen. + +************************* +Binärdateien +************************* + +Zur Erinnerung: Ein digitales Signal im Basisband ist eine Folge komplexer Zahlen. + +Beispiel: [0.123 + j0.512, 0.0312 + j0.4123, 0.1423 + j0.06512, ...] + +Diese Zahlen entsprechen [I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, ...] + +Wenn wir komplexe Zahlen in einer Datei speichern möchten, speichern wir sie im Format IQIQIQIQIQIQIQIQ. Das heißt, wir speichern eine Reihe von Ganzzahlen oder Gleitkommazahlen hintereinander, und beim Einlesen müssen wir sie wieder in [I+jQ, I+jQ, ...] aufteilen. + +Obwohl es möglich ist, die komplexen Zahlen in einer Textdatei oder CSV-Datei zu speichern, bevorzugen wir das Speichern in einer sogenannten „Binärdatei", um Speicherplatz zu sparen. Bei hohen Abtastraten können deine Signalaufzeichnungen leicht mehrere GB groß sein, und wir wollen so speichereffizient wie möglich sein. Wenn du jemals eine Datei in einem Texteditor geöffnet hast und sie unverständlich aussah (wie der Screenshot unten), war es wahrscheinlich eine Binärdatei. Binärdateien enthalten eine Folge von Bytes, und du musst das Format selbst im Blick behalten. Binärdateien sind die effizienteste Methode zur Datenspeicherung, vorausgesetzt, alle möglichen Komprimierungen wurden durchgeführt. Da unsere Signale meist wie eine zufällige Folge von Ganzzahlen oder Gleitkommazahlen aussehen, versuchen wir in der Regel nicht, die Daten zu komprimieren. Binärdateien werden auch für viele andere Dinge verwendet, z.B. für kompilierte Programme (auch „Binaries" genannt). Wenn sie zum Speichern von Signalen verwendet werden, nennen wir sie binäre „IQ-Dateien" mit der Dateiendung .iq. + +.. image:: ../_images/binary_file.png + :scale: 70 % + :align: center + +In Python ist der Standard-Komplextyp np.complex128, der zwei 64-Bit-Gleitkommazahlen pro Sample verwendet. In der DSP/SDR-Welt verwenden wir jedoch tendenziell 16-Bit-Ganzzahlen oder 32-Bit-Gleitkommazahlen, da die ADCs unserer SDRs nicht **so** viel Präzision haben, dass 64-Bit-Gleitkommazahlen gerechtfertigt wären. Tatsächlich haben die meisten SDRs 12-Bit-ADCs, sodass wir den Speicherbedarf minimieren können, indem wir als 16-Bit-Ganzzahlen speichern (np.int16 in Python). Jedes IQ-Sample benötigt dann 4 Bytes, und unsere HF-Aufzeichnung erzeugt eine Datei, die 4-mal die Abtastrate in Bytes groß ist – bekannt als „Seans 4x-Regel". In den Python-Beispielen unten verwenden wir **np.complex64**, das zwei 32-Bit-Gleitkommazahlen verwendet, da Python keinen nativen komplexen Ganzzahltyp hat (das hindert uns nicht daran, IQ als Ganzzahlen in eine Datei zu speichern, wie du gleich sehen wirst). Wenn du ein Signal in Python verarbeitest, spielt das keine große Rolle. Aber wenn du das eindimensionale Array in einer Datei speichern möchtest, solltest du sicherstellen, dass es zuerst ein Array vom Typ np.complex64 ist (oder np.int16 mit verschachtelten IQ-Werten). + +************************* +Python-Beispiele +************************* + +In Python und speziell in NumPy verwenden wir die Funktion :code:`tofile()`, um ein NumPy-Array in eine Datei zu speichern. Hier ist ein kurzes Beispiel, das ein einfaches QPSK-Signal mit Rauschen erstellt und es in eine Datei im selben Verzeichnis speichert, aus dem wir unser Skript ausführen: + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + + num_symbols = 10000 + + # x_symbols enthält komplexe Zahlen, die die QPSK-Symbole darstellen. Jedes Symbol hat den Betrag 1 und einen Phasenwinkel entsprechend einem der vier QPSK-Konstellationspunkte (45, 135, 225 oder 315 Grad) + x_int = np.random.randint(0, 4, num_symbols) # 0 bis 3 + x_degrees = x_int*360/4.0 + 45 # 45, 135, 225, 315 Grad + x_radians = x_degrees*np.pi/180.0 # sin() und cos() nehmen Bogenmaß + x_symbols = np.cos(x_radians) + 1j*np.sin(x_radians) # erzeugt unsere komplexen QPSK-Symbole + n = (np.random.randn(num_symbols) + 1j*np.random.randn(num_symbols))/np.sqrt(2) # AWGN mit Einheitsleistung + r = x_symbols + n * np.sqrt(0.01) # Rauschleistung von 0.01 + print(r) + plt.plot(np.real(r), np.imag(r), '.') + plt.grid(True) + plt.show() + + # Jetzt in eine IQ-Datei speichern + print(type(r[0])) # Datentyp prüfen. Ups, es ist 128 statt 64! + r = r.astype(np.complex64) # In 64 konvertieren + print(type(r[0])) # Verifizieren, dass es 64 ist + r.tofile('qpsk_in_noise.iq') # In Datei speichern + + +Prüfe nun die Details der erzeugten Datei und schau, wie viele Bytes sie hat. Es sollten num_symbols * 8 sein, da wir np.complex64 verwendet haben, was 8 Bytes pro Sample entspricht (4 Bytes pro Gleitkommazahl, 2 Gleitkommazahlen pro Sample). + +Mit einem neuen Python-Skript können wir diese Datei mit :code:`np.fromfile()` einlesen, wie folgt: + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + + samples = np.fromfile('qpsk_in_noise.iq', np.complex64) # Datei einlesen. Wir müssen das Format angeben + print(samples) + + # Konstellation plotten, um sicherzustellen, dass sie korrekt aussieht + plt.plot(np.real(samples), np.imag(samples), '.') + plt.grid(True) + plt.show() + +Ein häufiger Fehler ist, :code:`np.fromfile()` das Dateiformat nicht mitzuteilen. Binärdateien enthalten keine Informationen über ihr Format. Standardmäßig geht :code:`np.fromfile()` davon aus, dass es ein Array von float64-Werten liest. + +Die meisten anderen Programmiersprachen haben Methoden zum Einlesen von Binärdateien, z.B. in MATLAB kann man fread() verwenden. Zum visuellen Analysieren einer RF-Datei siehe den Abschnitt weiter unten. + +Wenn du es jemals mit int16-Werten (auch bekannt als Short-Ints) oder einem anderen Datentyp zu tun hast, für den NumPy kein komplexes Äquivalent hat, musst du die Samples als reelle Werte einlesen, auch wenn sie tatsächlich komplex sind. Der Trick besteht darin, sie als reell einzulesen und sie dann selbst wieder in das IQIQIQ...-Format zu verschachteln. Zwei verschiedene Möglichkeiten dafür sind unten gezeigt: + +.. code-block:: python + + samples = np.fromfile('iq_samples_as_int16.iq', np.int16).astype(np.float32).view(np.complex64) + +oder + +.. code-block:: python + + samples = np.fromfile('iq_samples_as_int16.iq', np.int16) + samples /= 32768 # in -1 bis +1 umwandeln (optional) + samples = samples[::2] + 1j*samples[1::2] # in IQIQIQ... konvertieren + +***************************** +Umstieg von MATLAB +***************************** + +Wenn du von MATLAB zu Python wechselst, fragst du dich vielleicht, wie du deine MATLAB-Variablen und .mat-Dateien als binäre IQ-Dateien speichern kannst. Zunächst müssen wir einen Formattyp wählen. Wenn unsere Samples beispielsweise Ganzzahlen zwischen -127 und +127 sind, können wir 8-Bit-Ints verwenden. In diesem Fall können wir folgenden MATLAB-Code verwenden, um die Samples in eine binäre IQ-Datei zu speichern: + +.. code-block:: MATLAB + + % Angenommen, unsere IQ-Samples befinden sich in der Variable samples + disp(samples(1:20)) + filename = 'samples.iq' + fwrite(fopen(filename,'w'), reshape([real(samples);imag(samples)],[],1), 'int8') + +Alle erlaubten Formattypen für fwrite() findest du in der `MATLAB-Dokumentation `_. Am besten bleibst du bei :code:`'int8'`, :code:`'int16'` oder :code:`'float32'`. + +Auf der Python-Seite kannst du diese Datei wie folgt einlesen: + +.. code-block:: python + + samples = np.fromfile('samples.iq', np.int8) + samples = samples[::2] + 1j*samples[1::2] + print(samples[0:20]) # sicherstellen, dass die ersten 20 Samples mit MATLAB übereinstimmen + +Für mit :code:`'float32'` aus MATLAB gespeicherte Daten kannst du auf der Python-Seite :code:`np.complex64` verwenden, was verschachtelte float32-Werte sind. Den Teil :code:`samples[::2] + 1j*samples[1::2]` kannst du dann weglassen, da NumPy die verschachtelten Gleitkommazahlen automatisch als komplexe Zahlen interpretiert. + +***************************** +Visuelle Analyse einer RF-Datei +***************************** + +Obwohl wir im Kapitel :ref:`freq-domain-chapter` gelernt haben, unsere eigenen Spektrogrammplots zu erstellen, kommt nichts an bereits fertige Software heran. Wenn es darum geht, RF-Aufzeichnungen ohne Installation zu analysieren, ist die empfohlene Website `IQEngine `_ – ein komplettes Toolkit zum Analysieren, Verarbeiten und Teilen von RF-Aufzeichnungen. + +Für diejenigen, die eine Desktop-Anwendung bevorzugen, gibt es auch `inspectrum `_. Inspectrum ist ein recht einfaches, aber leistungsstarkes grafisches Werkzeug zum visuellen Durchsuchen einer RF-Datei mit feiner Kontrolle über den Farbkartenbereich und die FFT-Größe (Zoomstufe). Du kannst Alt gedrückt halten und das Scrollrad verwenden, um durch die Zeit zu scrollen. Es hat optionale Cursors zum Messen der Zeit zwischen zwei Energieausbrüchen und die Möglichkeit, einen Ausschnitt der RF-Datei in eine neue Datei zu exportieren. Für die Installation auf Debian-basierten Plattformen wie Ubuntu verwende folgende Befehle: + +.. code-block:: bash + + sudo apt-get install qt5-default libfftw3-dev cmake pkg-config libliquid-dev + git clone https://github.com/miek/inspectrum.git + cd inspectrum + mkdir build + cd build + cmake .. + make + sudo make install + inspectrum + +.. image:: ../_images/inspectrum.jpg + :scale: 30 % + :align: center + +************************* +Maximalwerte und Sättigung +************************* + +Beim Empfangen von Samples von einem SDR ist es wichtig, den maximalen Samplewert zu kennen. Viele SDRs geben die Samples als Gleitkommazahlen mit einem Maximalwert von 1,0 und einem Minimalwert von -1,0 aus. Andere SDRs liefern Samples als Ganzzahlen, meist 16-Bit, wobei die Maximal- und Minimalwerte +32767 bzw. -32768 sind (sofern nicht anders angegeben). Du kannst durch 32.768 teilen, um sie in Gleitkommazahlen von -1,0 bis 1,0 umzuwandeln. Der Grund, den Maximalwert deines SDR zu kennen, liegt in der Sättigung: Wenn ein sehr starkes Signal empfangen wird (oder wenn der Gain zu hoch eingestellt ist), „sättigt" der Empfänger und schneidet die hohen Werte auf den maximalen Samplewert ab. Die ADCs unserer SDRs haben eine begrenzte Anzahl von Bits. Beim Entwickeln einer SDR-Anwendung ist es ratsam, immer auf Sättigung zu prüfen und sie bei Auftreten irgendwie anzuzeigen. + +Ein gesättigtes Signal sieht im Zeitbereich abgehackt aus, wie folgt: + +.. image:: ../_images/saturated_time.png + :scale: 30 % + :align: center + :alt: Beispiel eines gesättigten Empfängers, bei dem das Signal abgeschnitten ist + +Aufgrund der plötzlichen Änderungen im Zeitbereich durch das Abschneiden kann der Frequenzbereich verschmiert aussehen. Mit anderen Worten: Der Frequenzbereich enthält falsche Merkmale, die durch die Sättigung entstanden sind und nicht wirklich Teil des Signals sind. Dies kann zu Verwirrung bei der Signalanalyse führen. + +***************************** +SigMF und IQ-Dateien annotieren +***************************** + +Da die IQ-Datei selbst keine zugehörigen Metadaten enthält, ist es üblich, eine zweite Datei mit Informationen über das Signal zu haben, mit demselben Dateinamen, aber einer .txt- oder anderen Dateiendung. Diese sollte mindestens die verwendete Abtastrate und die Frequenz, auf die das SDR abgestimmt war, enthalten. Nach der Analyse des Signals könnte die Metadatendatei Informationen über Samplebereiche interessanter Merkmale enthalten, wie z.B. Energieausbrüche. Der Sample-Index ist einfach eine Ganzzahl, die bei 0 beginnt und bei jedem komplexen Sample um 1 erhöht wird. Wenn du wüsstest, dass Energie von Sample 492342 bis 528492 vorhanden ist, könntest du die Datei einlesen und diesen Teil des Arrays herausziehen: :code:`samples[492342:528493]`. + +Glücklicherweise gibt es jetzt einen offenen Standard, der ein Metadatenformat zur Beschreibung von Signalaufzeichnungen festlegt, bekannt als `SigMF `_. Durch die Verwendung eines offenen Standards wie SigMF können mehrere Parteien RF-Aufzeichnungen einfacher teilen und verschiedene Werkzeuge auf denselben Datensätzen verwenden, wie z.B. `IQEngine `_. Es verhindert auch den „Bitrot" von RF-Datensätzen, bei dem Details der Aufnahme im Laufe der Zeit verloren gehen, weil die Aufzeichnungsdetails nicht zusammen mit der Aufzeichnung selbst gespeichert werden. + +Die einfachste (und minimalste) Methode, den SigMF-Standard zur Beschreibung einer erstellten binären IQ-Datei zu verwenden, besteht darin, die .iq-Datei in .sigmf-data umzubenennen und eine neue Datei mit demselben Namen, aber der Endung .sigmf-meta zu erstellen. Dabei muss sichergestellt werden, dass das Datentypfeld in der Meta-Datei dem Binärformat deiner Datendatei entspricht. Diese Meta-Datei ist eine Klartextdatei mit JSON-Inhalt, die du einfach mit einem Texteditor öffnen und manuell ausfüllen kannst (später besprechen wir das programmatische Vorgehen). Hier ist eine Beispiel-.sigmf-meta-Datei, die du als Vorlage verwenden kannst: + +.. code-block:: + + { + "global": { + "core:datatype": "cf32_le", + "core:sample_rate": 1000000, + "core:hw": "PlutoSDR with 915 MHz whip antenna", + "core:author": "Art Vandelay", + "core:version": "1.0.0" + }, + "captures": [ + { + "core:sample_start": 0, + "core:frequency": 915000000 + } + ], + "annotations": [] + } + +Beachte: :code:`core:cf32_le` gibt an, dass deine .sigmf-data vom Typ IQIQIQIQ... mit 32-Bit-Gleitkommazahlen ist, d.h. np.complex64, wie wir es zuvor verwendet haben. In der Spezifikation findest du weitere verfügbare Datentypen, z.B. wenn du reelle statt komplexe Daten hast oder 16-Bit-Ganzzahlen statt Gleitkommazahlen verwendest, um Speicherplatz zu sparen. + +Neben dem Datentyp sind die wichtigsten auszufüllenden Zeilen :code:`core:sample_rate` und :code:`core:frequency`. Es ist gute Praxis, auch Informationen über die verwendete Hardware (:code:`core:hw`) einzugeben, wie z.B. SDR-Typ und Antenne, sowie eine Beschreibung des Bekannten über das/die Signal(e) in der Aufzeichnung in :code:`core:description`. :code:`core:version` ist einfach die Version des SigMF-Standards, der zum Zeitpunkt der Erstellung der Metadatendatei verwendet wurde. + +Wenn du deine RF-Aufzeichnung innerhalb von Python aufzeichnest, z.B. mit der Python-API für dein SDR, kannst du das manuelle Erstellen dieser Metadatendateien vermeiden, indem du das SigMF Python-Paket verwendest. Dieses kann auf einem Ubuntu/Debian-basierten Betriebssystem wie folgt installiert werden: + +.. code-block:: bash + + pip install sigmf + +Der Python-Code zum Schreiben der .sigmf-meta-Datei für das Beispiel am Anfang dieses Kapitels, in dem wir :code:`qpsk_in_noise.iq` gespeichert haben, ist unten dargestellt: + +.. code-block:: python + + import datetime as dt + + import numpy as np + import sigmf + from sigmf import SigMFFile + + # + + # r.tofile('qpsk_in_noise.iq') + r.tofile('qpsk_in_noise.sigmf-data') # obige Zeile durch diese ersetzen + + # Metadaten erstellen + meta = SigMFFile( + data_file='qpsk_in_noise.sigmf-data', # Endung ist optional + global_info = { + SigMFFile.DATATYPE_KEY: 'cf32_le', + SigMFFile.SAMPLE_RATE_KEY: 8000000, + SigMFFile.AUTHOR_KEY: 'Dein Name und/oder E-Mail', + SigMFFile.DESCRIPTION_KEY: 'Simulation von QPSK mit Rauschen', + SigMFFile.VERSION_KEY: sigmf.__version__, + } + ) + + # Capture-Eintrag bei Zeitindex 0 erstellen + meta.add_capture(0, metadata={ + SigMFFile.FREQUENCY_KEY: 915000000, + SigMFFile.DATETIME_KEY: dt.datetime.now(dt.timezone.utc).isoformat(), + }) + + # Auf Fehler prüfen und auf Disk schreiben + meta.validate() + meta.tofile('qpsk_in_noise.sigmf-meta') # Endung ist optional + +Ersetze einfach :code:`8000000` und :code:`915000000` durch die Variablen, die du für Abtastrate und Mittenfrequenz verwendet hast. + +Um eine SigMF-Aufzeichnung in Python einzulesen, verwende folgenden Code. In diesem Beispiel sollten die beiden SigMF-Dateien :code:`qpsk_in_noise.sigmf-meta` und :code:`qpsk_in_noise.sigmf-data` heißen. + +.. code-block:: python + + from sigmf import SigMFFile, sigmffile + + # Datensatz laden + filename = 'qpsk_in_noise' + signal = sigmffile.fromfile(filename) + samples = signal.read_samples().view(np.complex64).flatten() + print(samples[0:10]) # erste 10 Samples ansehen + + # Einige Metadaten und alle Annotierungen abrufen + sample_rate = signal.get_global_field(SigMFFile.SAMPLE_RATE_KEY) + sample_count = signal.sample_count + signal_duration = sample_count / sample_rate + +Weitere Details findest du in der `SigMF Python-Dokumentation `_. + +Ein kleiner Bonus für alle, die bis hier gelesen haben: Das SigMF-Logo ist selbst als SigMF-Aufzeichnung gespeichert, und wenn das Signal als Konstellation (IQ-Plot) über die Zeit geplottet wird, ergibt es folgende Animation: + +.. image:: ../_images/sigmf_logo.gif + :scale: 100 % + :align: center + :alt: Die SigMF-Logo-Animation + +Der Python-Code zum Einlesen der Logo-Datei (abrufbar `hier `_) und zur Erstellung des animierten GIFs ist unten dargestellt: + +.. code-block:: python + + from pathlib import Path + from tempfile import TemporaryDirectory + + import numpy as np + import matplotlib.pyplot as plt + import imageio.v3 as iio + from sigmf import SigMFFile, sigmffile + + # Datensatz laden + filename = 'sigmf_logo' # angenommen, es liegt im selben Verzeichnis wie dieses Skript + signal = sigmffile.fromfile(filename) + samples = signal.read_samples().view(np.complex64).flatten() + + # Nullen am Ende hinzufügen, damit der Übergang bei der Wiederholung der Animation erkennbar ist + samples = np.concatenate((samples, np.zeros(50000))) + + sample_count = len(samples) + samples_per_frame = 5000 + num_frames = int(sample_count/samples_per_frame) + + with TemporaryDirectory() as temp_dir: + filenames = [] + output_dir = Path(temp_dir) + for i in range(num_frames): + print(f"Frame {i} von {num_frames}") + # Frame plotten + fig, ax = plt.subplots(figsize=(5, 5)) + samples_frame = samples[i*samples_per_frame:(i+1)*samples_per_frame] + ax.plot(np.real(samples_frame), np.imag(samples_frame), color="cyan", marker=".", linestyle="None", markersize=1) + ax.axis([-0.35,0.35,-0.35,0.35]) # Achsen konstant halten + ax.set_facecolor('black') # Hintergrundfarbe + + # Plot in Datei speichern + filename = output_dir.joinpath(f"sigmf_logo_{i}.png") + fig.savefig(filename, bbox_inches='tight') + plt.close() + filenames.append(filename) + + # Animiertes GIF erstellen + images = [iio.imread(f) for f in filenames] + iio.imwrite('sigmf_logo.gif', images, fps=20) + +************************************** +SigMF Collection für Array-Aufzeichnungen +************************************** + +Wenn du ein Phased Array, MIMO-Digitalarray, TDOA-Sensoren oder eine andere Situation hast, in der du mehrere Kanäle synchronisierter RF-Daten aufzeichnest, fragst du dich wahrscheinlich, wie du das rohe IQ mehrerer Streams mit SigMF in Dateien speicherst. Das SigMF **Collection**-System wurde genau für diese Anwendungen entwickelt. Eine Collection ist einfach eine Gruppe von SigMF-Recordings (jedes bestehend aus einer Meta- und einer Datendatei), die über eine übergeordnete :code:`.sigmf-collection`-JSON-Datei zusammengefasst werden. Diese JSON-Datei ist recht einfach aufgebaut: Sie benötigt die SigMF-Version, eine optionale Beschreibung und dann eine Liste von „Streams", die eigentlich nur der Basisname jeder SigMF-Recording in der Collection ist. Hier ist ein Beispiel einer :code:`.sigmf-collection`-Datei: + +.. code-block:: json + + { + "collection": { + "core:version": "1.2.0", + "core:description": "eine 4-Elemente-Phased-Array-Aufzeichnung", + "core:streams": [ + { + "name": "channel-0" + }, + { + "name": "channel-1" + }, + { + "name": "channel-2" + }, + { + "name": "channel-3" + } + ] + } + } + +Die Namen der Recordings müssen nicht :code:`channel-0`, :code:`channel-1`, ... sein – sie können beliebig sein, solange sie eindeutig sind und jeder einem Daten- und einer Meta-Datei entspricht. Im obigen Beispiel muss diese .sigmf-collection-Datei, die wir z.B. :code:`4_element_recording.sigmf-collection` nennen könnten, im selben Verzeichnis wie die Meta- und Datendateien liegen: + +* :code:`4_element_recording.sigmf-collection` +* :code:`channel-0.sigmf-meta` +* :code:`channel-0.sigmf-data` +* :code:`channel-1.sigmf-meta` +* :code:`channel-1.sigmf-data` +* :code:`channel-2.sigmf-meta` +* :code:`channel-2.sigmf-data` +* :code:`channel-3.sigmf-meta` +* :code:`channel-3.sigmf-data` + +Du denkst vielleicht, dass das zu sehr vielen Dateien führt – ein 16-Elemente-Array würde z.B. 33 Dateien ergeben! Aus diesem Grund führte SigMF das **Archive**-System ein, was schlicht SigMFs Begriff für das Tarball-Archivieren einer Dateisammlung ist. Eine SigMF-Archive-Datei verwendet die Endung :code:`.sigmf`, nicht :code:`.tar`! Viele Leute denken, dass .tar-Dateien komprimiert sind, aber das stimmt nicht. Sie sind lediglich eine Methode zum Zusammenfassen von Dateien (es ist im Wesentlichen eine Dateiverkettung ohne Komprimierung). Vielleicht hast du schon eine :code:`.tar.gz`-Datei gesehen – das ist ein Tarball, der mit gzip komprimiert wurde. Für unsere SigMF-Archive werden wir keine Komprimierung verwenden, da die Datendateien bereits binär sind und sich nicht stark komprimieren lassen, vor allem wenn automatische Verstärkungsregelung verwendet wurde. Wenn du ein SigMF-Archiv in Python erstellen möchtest, kannst du alle Dateien in einem Verzeichnis wie folgt in einen Tarball packen: + +.. code-block:: python + + import tarfile + import os + + target_dir = '/mnt/c/Users/marclichtman/Downloads/exampletar/' # SigMF-Dateien befinden sich hier + with tarfile.open(os.path.join(target_dir, '4_element_recording.sigmf'), 'x') as tar: # x bedeutet erstellen, aber fehlschlagen, falls bereits vorhanden + for file in os.listdir(target_dir): + tar.add(os.path.join(target_dir, file), arcname=file) # arcname verhindert, dass der vollständige Pfad im Tar enthalten ist + +Das war's! Versuche (vorübergehend), .sigmf in .tar umzubenennen, und sieh dir die Dateien in deinem Datei-Browser an. Um eine der Dateien direkt (ohne manuelles Entpacken des Tarballs) in Python zu öffnen, verwende: + +.. code-block:: python + + import tarfile + import json + + collection_file = '/mnt/c/Users/marclichtman/Downloads/exampletar/4_element_recording.sigmf' + tar_obj = tarfile.open(collection_file) + print(tar_obj.getnames()) # Liste aller Dateinamen im Tar als Strings + channel_0_meta = tar_obj.extractfile('channel-0.sigmf-meta').read() # eine der Meta-Dateien einlesen, als Beispiel + channel_0_dict = json.loads(channel_0_meta) # in Python-Dictionary konvertieren + print(channel_0_dict) + +Zum Einlesen von IQ-Samples aus dem Tar verwenden wir statt :code:`np.fromfile()` die Funktion :code:`np.frombuffer()`: + +.. code-block:: python + + import tarfile + import numpy as np + + collection_file = '/mnt/c/Users/marclichtman/Downloads/exampletar/4_element_recording.sigmf' + tar_obj = tarfile.open(collection_file) + channel_0_data_f = tar_obj.extractfile('channel-0.sigmf-data').read() # Typ: bytes + samples = np.frombuffer(channel_0_data_f, dtype=np.int16) + samples = samples[::2] + 1j*samples[1::2] # in IQIQIQ... konvertieren + samples /= 32768 # in -1 bis +1 konvertieren + print(samples[0:10]) + +Wenn du zu einem anderen Teil der Datei springen möchtest, verwende :code:`tar_obj.extractfile('channel-0.sigmf-data').seek(offset)`. Um eine bestimmte Anzahl von Bytes zu lesen, verwende :code:`.read(num_bytes)`. Stelle sicher, dass die Anzahl der Bytes ein Vielfaches deines Datentyps ist! + +Zusammenfassend sollten beim Erstellen eines neuen SigMF Collection Archives folgende Schritte durchgeführt werden: + +1. Erstelle die .sigmf-meta- und .sigmf-data-Datei für jeden Kanal +2. Erstelle die .sigmf-collection-Datei +3. Packe alle Dateien in einen .sigmf-Tarball +4. (Optional) Teile die .sigmf-Datei mit anderen! + +Zum Einlesen der Aufzeichnung musst du den Tarball nicht entpacken – du kannst die Dateien direkt lesen. + +********************** +Midas Blue File Format +********************** + +Blue Files, auch bekannt als BLUEFILES oder Midas Files, sind ein Dateiformat, das eine Vielzahl von Datenstrukturen darstellen kann, einschließlich ein- und zweidimensionaler Daten, und wird in bestimmten Organisationen zur Aufzeichnung von rohen HF-Signalen in Dateien verwendet. Im Kontext von RF/SDR können Blue Files als IQ-Dateiformat betrachtet werden. Blue Files werden im X-Midas Signalverarbeitungsframework sowie in dessen Ablegern Midas 2k (C++), NeXtMidas (Java) und XMPy (Python) verwendet. Wer REDHAWK kennt: Ein Teil von NeXtMidas ist darin eingebettet. Einige Anwendungen erzeugen Blue Files mit der Dateiendung :code:`.blue`, andere verwenden :code:`.cdif` – beides ist dasselbe zugrunde liegende Format. + +Blue Files sind Binärdateien mit drei Komponenten in folgender Reihenfolge: + +1. 512-Byte-Header mit Datei-Metadaten +2. Daten, in unserem Fall binäres IQ (Ints oder Floats im Format IQIQIQ...) +3. Optionaler „Extended Header" (auch bekannt als tailing bytes) mit Hilfs-Metadaten in Form beliebiger Schlüssel/Wert-Paare + +Die im Header enthaltenen Felder sind auf `dieser Seite `_ beschrieben. Wichtige für uns sind: + +- Byte 52: Datenfomatcode, zwei Zeichen. Das erste Zeichen gibt an, ob es sich um reelle (S) oder komplexe (C) Daten handelt. Das zweite Zeichen bezeichnet den Datentyp: :code:`B` = 8-Bit-Ganzzahl mit Vorzeichen, :code:`I` = 16-Bit-Ganzzahl mit Vorzeichen, :code:`L` = 32-Bit-Ganzzahl mit Vorzeichen, :code:`F` = 32-Bit-Gleitkommazahl, :code:`D` = 64-Bit-Gleitkommazahl. +- Byte 8: Datendarstellung, vier Zeichen: :code:`IEEE` bedeutet Big-Endian, :code:`EEEI` bedeutet Little-Endian (am häufigsten) +- Byte 24: Extended-Header-Start, ein int32, in 512-Byte-Blöcken +- Byte 28: Extended-Header-Größe, ein int32, in Bytes +- Byte 264: Zeitintervall zwischen Samples, d.h. 1/Abtastrate, als float64 in Sekunden + +So entspricht beispielsweise :code:`CI` dem SigMF-Typ :code:`ci16_le`, und :code:`CF` entspricht SigMF's :code:`cf32_le`. Auch wenn der Extended Header (d.h. die tailing bytes) seine Länge und Startposition angegeben hat, ist der einfache Ansatz, einfach die letzten paar tausend IQ-Samples der Datei zu ignorieren – damit vermeidest du den Extended Header fast mit Sicherheit und liest keine ungültigen IQ-Werte ein. + +Der Python-Code zum Einlesen der oben besprochenen Felder sowie der IQ-Samples ist wie folgt: + +.. code-block:: python + + import numpy as np + import os + import matplotlib.pyplot as plt + + filename = 'deinedatei.blue' # oder cdif + + filesize = os.path.getsize(filename) + print('Dateigröße', filesize, 'Bytes') + with open(filename, 'rb') as f: + header = f.read(512) + + # Header dekodieren + dtype = header[52:54].decode('utf-8') # z.B. 'CI' + endianness = header[8:12].decode('utf-8') # sollte 'EEEI' sein! ab hier nehmen wir das an + extended_header_start = int.from_bytes(header[24:28], byteorder='little') * 512 # in Bytes + extended_header_size = int.from_bytes(header[28:32], byteorder='little') + if extended_header_size != filesize - extended_header_start: + print('Warnung: Extended-Header-Größe scheint falsch') + time_interval = np.frombuffer(header[264:272], dtype=np.float64)[0] + sample_rate = 1/time_interval + print('Abtastrate', sample_rate/1e6, 'MHz') + + # IQ-Samples einlesen + if dtype == 'CI': + samples = np.fromfile(filename, dtype=np.int16, offset=512, count=(filesize-extended_header_size)) + samples = samples[::2] + 1j*samples[1::2] # in IQIQIQ... konvertieren + + # Jeden 1000sten Sample plotten, um auf Fehler zu prüfen + print(len(samples)) + plt.plot(samples.real[::1000]) + plt.show() + +Der „Extended Header" (d.h. die tailing bytes) mit seinen beliebigen Schlüssel/Wert-Paaren wird in einem Format beschrieben, das in Abschnitt 3.3 der `Blue File Format Spezifikation `_ festgelegt ist. Er enthält oft Informationen wie die HF-Frequenz, den Gain und den verwendeten Empfänger/SDR. Der Python-Code zum Dekodieren dieser Schlüssel/Wert-Paare ist unten dargestellt, angepasst von `diesem Code `_: + +.. code-block:: python + + ... + + # Extended Header am Ende der Datei einlesen + with open(filename, 'rb') as f: + f.seek(filesize-extended_header_size) + ext_header = f.read(extended_header_size) + print("Länge des Extended Headers", len(ext_header), '\n') + + def parse_extended_header(idx): + next_offset = np.frombuffer(ext_header[idx:idx+4], dtype=np.int32)[0] + non_data_length = np.frombuffer(ext_header[idx+4:idx+6], dtype=np.int16)[0] + name_length = ext_header[idx+6] + dataStart = idx + 8 + dataLength = dataStart + next_offset - non_data_length + midas_to_np = {'O' : np.uint8, 'B' : np.int8, 'I' : np.int16, 'L' : np.int32, 'X' : np.int64, 'F' : np.float32, 'D' : np.float64} + format_code = chr(ext_header[idx+7]) + if format_code == 'A': + val = ext_header[dataStart:dataLength].decode('latin_1') + else: + val = np.frombuffer(ext_header[dataStart:dataLength], dtype=midas_to_np[format_code])[0] + key = ext_header[dataLength:dataLength+name_length].decode('latin_1') + print(key, ' ', val) + return idx + next_offset + + next_idx = 0 + while next_idx < extended_header_size: + next_idx = parse_extended_header(next_idx) + +Als Randnotiz: Blue Files und andere binäre IQ-Formate mit Metadaten und Daten in derselben Datei sind der Grund, warum SigMF eine Variante namens Non-Conforming Datasets (NCDs) enthält. Diese erlauben es, binäre IQ-Dateien mit zusätzlichen Bytes am Anfang und/oder Ende (für Metadaten) in ein SigMF-ähnliches Format zu bringen. Weitere Informationen findest du in den SigMF-Metadatenfeldern: dataset, header_bytes, trailing_bytes. Rein aus der Perspektive des Datenlesens können wir eine Blue File wie eine normale binäre IQ-Datei behandeln, solange wir die ersten 512 Bytes und alle Extended-Header-Bytes am Ende ignorieren. + +Externe Ressourcen zu Blue Files: + +#. https://web.archive.org/web/20150413061156/http://nextmidas.techma.com/nm/nxm/sys/docs/MidasBlueFileFormat.pdf +#. https://sigplot.lgsinnovations.com/html/doc/bluefile.html +#. https://lgsinnovations.github.io/sigfile/bluefile.js.html +#. http://nextmidas.com.s3-website-us-gov-west-1.amazonaws.com/ +#. https://web.archive.org/web/20181020012349/http://nextmidas.techma.com/nm/htdocs/usersguide/BlueFiles.html +#. https://github.com/Geontech/XMidasBlueReader diff --git a/content-de/link_budgets.rst b/content-de/link_budgets.rst new file mode 100644 index 00000000..c9a499c7 --- /dev/null +++ b/content-de/link_budgets.rst @@ -0,0 +1,262 @@ +.. _link-budgets-chapter: + +################## +Linkbudgets +################## + +Dieses Kapitel behandelt Linkbudgets. Ein großer Teil davon ist das Verständnis von Sende-/Empfangsleistung, Pfadverlust, Antennengewinn, Rauschen und SNR. Wir schließen mit der Erstellung eines Beispiel-Linkbudgets für ADS-B ab, das sind Signale, die von Verkehrsflugzeugen ausgestrahlt werden, um ihre Position und andere Informationen zu übermitteln. + +************************* +Einführung +************************* + +Ein Linkbudget ist eine Aufstellung aller Gewinne und Verluste vom Sender zum Empfänger in einem Kommunikationssystem. Linkbudgets beschreiben eine Richtung der Funkverbindung. Die meisten Kommunikationssysteme sind bidirektional, daher muss es ein separates Uplink- und Downlink-Budget geben. Das „Ergebnis" des Linkbudgets sagt dir ungefähr, welches Signal-Rausch-Verhältnis (abgekürzt SNR, wie in diesem Lehrbuch verwendet, oder S/N) du an deinem Empfänger erwarten kannst. Eine weitere Analyse wäre notwendig, um zu prüfen, ob dieses SNR für deine Anwendung ausreichend hoch ist. + +Du studierst Linkbudgets nicht, um tatsächlich ein Linkbudget für eine bestimmte Situation erstellen zu können, sondern um eine systemebenenbasierte Sichtweise der drahtlosen Kommunikation zu erlernen und zu entwickeln. + +Wir behandeln zunächst das Budget für die empfangene Signalleistung, dann das Rauschleistungsbudget, und kombinieren schließlich beide, um das SNR (Signalleistung geteilt durch Rauschleistung) zu ermitteln. + +************************* +Signalleistungsbudget +************************* + +Unten ist das grundlegendste Diagramm einer generischen Funkverbindung. In diesem Kapitel konzentrieren wir uns auf eine Richtung, d.h. von einem Sender (Tx) zum Empfänger (Rx). Für ein gegebenes System kennen wir die *Sendeleistung*; sie ist normalerweise eine Einstellung im Sender. Wie ermitteln wir die *empfangene* Leistung am Empfänger? + +.. image:: ../_images_de/tx_rx_system.svg + :align: center + :target: ../_images_de/tx_rx_system.svg + +Wir benötigen vier Systemparameter, um die empfangene Leistung zu bestimmen, die unten mit ihren üblichen Abkürzungen aufgeführt sind. Dieses Kapitel geht auf jeden von ihnen ein. + +- **Pt** - Sendeleistung +- **Gt** - Gewinn der Sendeantenne +- **Gr** - Gewinn der Empfangsantenne +- **Lp** - Abstand zwischen Tx und Rx (d.h. wie viel Drahtlos-Pfadverlust) + +.. image:: ../_images_de/tx_rx_system_params.svg + :align: center + :target: ../_images_de/tx_rx_system_params.svg + :alt: Darstellung der Parameter eines Linkbudgets + +Sendeleistung +##################### + +Sendeleistung ist recht einfach; sie ist ein Wert in Watt, dBW oder dBm (erinnere dich, dBm ist eine Kurzform für dBmW). Jeder Sender hat einen oder mehrere Verstärker, und die Sendeleistung ist größtenteils eine Funktion dieser Verstärker. Eine Analogie für Sendeleistung wäre die Wattangabe (Leistung) einer Glühbirne: Je höher die Wattzahl, desto mehr Licht strahlt die Birne ab. Hier sind Beispiele für ungefähre Sendeleistungen verschiedener Technologien: + +================== ===== ======= +\ Leistung +------------------ -------------- +Bluetooth 10 mW -20 dBW +WLAN 100mW -10 dBW +LTE-Basisstation 1W 0 dBW +UKW-Sender 10kW 40 dBW +================== ===== ======= + +Antennengewinne +##################### + +Sende- und Empfangsantennengewinne sind entscheidend für die Berechnung von Linkbudgets. Was ist der Antennengewinn, fragst du dich? Er gibt die Richtwirkung der Antenne an. Du wirst ihn vielleicht als Antennenleistungsgewinn bezeichnet sehen, aber lass dich davon nicht irreführen — die einzige Möglichkeit für eine Antenne, einen höheren Gewinn zu haben, besteht darin, Energie in einem konzentrierteren Bereich auszustrahlen. + +Gewinne werden in dB (einheitenlos) angegeben; erinnere dich im Kapitel :ref:`noise-chapter` warum dB für unser Szenario einheitenlos ist. Typischerweise sind Antennen entweder omnidirektional, was bedeutet, dass ihre Leistung in alle Richtungen abstrahlt, oder direktional, was bedeutet, dass ihre Leistung in eine bestimmte Richtung abstrahlt. Wenn sie omnidirektional sind, liegt ihr Gewinn zwischen 0 dB und 3 dB. Eine direktionale Antenne hat einen höheren Gewinn, normalerweise 5 dB oder mehr, und kann bis zu etwa 60 dB gehen. + +.. image:: ../_images_de/antenna_gain_patterns.png + :scale: 80 % + :align: center + +Wenn eine direktionale Antenne verwendet wird, muss sie entweder in die richtige Richtung installiert werden oder an einem mechanischen Schwenkkopf befestigt sein. Sie könnte auch ein Phased Array sein, das elektronisch gesteuert werden kann (d.h. durch Software). + +.. image:: ../_images_de/antenna_steering.png + :scale: 80 % + :align: center + +Omnidirektionale Antennen werden verwendet, wenn eine Ausrichtung in die richtige Richtung nicht möglich ist, wie bei deinem Mobiltelefon und Laptop. In 5G können Telefone in den höheren Frequenzbändern wie 28 GHz (Verizon) und 39 GHz (AT&T) mit einem Array von Antennen und elektronischem Strahlschwenken arbeiten. + +In einem Linkbudget müssen wir davon ausgehen, dass jede direktionale Antenne, ob Sende- oder Empfangsantenne, in die richtige Richtung zeigt. Wenn sie nicht richtig ausgerichtet ist, wird unser Linkbudget ungenau sein und es könnte zu Kommunikationsausfällen kommen (z.B. wenn die Satellitenschüssel auf deinem Dach von einem Basketball getroffen wird und sich verschiebt). Im Allgemeinen gehen unsere Linkbudgets von idealen Bedingungen aus, während ein sonstiger Verlust hinzugefügt wird, um reale Faktoren zu berücksichtigen. + +Pfadverlust +##################### + +Wenn sich ein Signal durch die Luft (oder das Vakuum) bewegt, nimmt seine Stärke ab. Stell dir vor, du hältst ein kleines Solarpanel vor eine Glühbirne. Je weiter das Solarpanel entfernt ist, desto weniger Energie nimmt es von der Glühbirne auf. **Fluss** ist ein Begriff in der Physik und Mathematik, definiert als „wie viel Zeug durch dein Ding geht". Bei uns ist es die Menge des elektromagnetischen Feldes, das in unsere Empfangsantenne eintritt. Wir möchten wissen, wie viel Leistung für eine gegebene Entfernung verloren geht. + +.. image:: ../_images_de/flux.png + :scale: 80 % + :align: center + +Der freie-Raum-Pfadverlust (FSPL, Free Space Path Loss) gibt uns den Pfadverlust ohne Hindernisse für eine gegebene Entfernung an. In seiner allgemeinen Form: :math:`\mathrm{FSPL} = ( 4\pi d / \lambda )^2`. Für weitere Informationen siehe die Friis-Übertragungsformel. (Fun Fact: Signale begegnen beim Durchqueren des freien Raums einem Impedanzwiderstand von 377 Ohm.) Für die Erstellung von Linkbudgets können wir dieselbe Gleichung verwenden, aber in dB umgewandelt: + +.. math:: + \mathrm{FSPL}_{dB} = 20 \log_{10} d + 20 \log_{10} f - 147{,}55 \left[ dB \right] + +In Linkbudgets erscheint er in dB, einheitenlos, da es ein Verlust ist. :math:`d` ist in Metern und ist der Abstand zwischen Sender und Empfänger. :math:`f` ist in Hz und ist die Trägerfrequenz. Es gibt nur ein Problem mit dieser einfachen Gleichung: Wir werden nicht immer freien Raum zwischen Sender und Empfänger haben. Frequenzen reflektieren stark in Innenräumen (die meisten Frequenzen können durch Wände gehen, nur nicht durch Metall oder dickes Mauerwerk). Für diese Situationen gibt es verschiedene Nicht-Freiraummodelle. Ein gängiges für Städte und Vorstadtbereiche (z.B. Mobilfunk) ist das Okumura-Hata-Modell: + +.. math:: + L_{path} = 69{,}55 + 26{,}16 \log_{10} f - 13{,}82 \log_{10} h_B - C_H + \left[ 44{,}9 - 6{,}55 \log_{10} h_B \right] \log_{10} d + +wobei :math:`L_{path}` der Pfadverlust in dB ist, :math:`h_B` die Höhe der Sendeantenne über dem Boden in Metern ist, :math:`f` die Trägerfrequenz in MHz ist, :math:`d` der Abstand zwischen Tx und Rx in km ist und :math:`C_H` der sogenannte „Antennenhöhen-Korrekturfaktor" ist, der basierend auf der Stadtgröße und dem Trägerfrequenzbereich definiert wird: + +:math:`C_H` für kleine/mittlere Städte: + +.. math:: + C_H = 0{,}8 + (1{,}1 \log_{10} f - 0{,}7 ) h_M - 1{,}56 \log_{10} f + +:math:`C_H` für große Städte, wenn :math:`f` unter 200 MHz liegt: + +.. math:: + C_H = 8{,}29 ( log_{10}(1{,}54 h_M))^2 - 1{,}1 + +:math:`C_H` für große Städte, wenn :math:`f` über 200 MHz, aber unter 1,5 GHz liegt: + +.. math:: + C_H = 3{,}2 ( log_{10}(11{,}75 h_M))^2 - 4{,}97 + +wobei :math:`h_M` die Höhe der Empfangsantenne über dem Boden in Metern ist. + +Mache dir keine Sorgen, wenn das obige Okumura-Hata-Modell verwirrend erschien; es wird hier hauptsächlich gezeigt, um zu demonstrieren, wie viel komplizierter Nicht-Freiraummodelle im Vergleich zu unserer einfachen FSPL-Gleichung sind. Das Endergebnis all dieser Modelle ist eine einzelne Zahl, die wir für den Pfadverlustanteil unseres Linkbudgets verwenden können. Im Rest dieses Kapitels bleiben wir bei FSPL. + +Sonstige Verluste +##################### + +In unserem Linkbudget möchten wir auch sonstige Verluste berücksichtigen. Wir fassen diese zu einem Begriff zusammen, normalerweise irgendwo zwischen 1 und 3 dB. Beispiele für sonstige Verluste: + +- Kabelverlust +- Atmosphärischer Verlust +- Ungenauigkeiten bei der Antennenausrichtung +- Niederschlag + +Das Diagramm unten zeigt den atmosphärischen Verlust in dB/km über der Frequenz (wir werden normalerweise < 40 GHz sein). Wenn du dir etwas Zeit nimmst, die y-Achse zu verstehen, wirst du sehen, dass Kurzstrecken-Kommunikation unter 40 GHz **und** unter 1 km einen atmosphärischen Verlust von 1 dB oder weniger hat, und wir ihn daher im Allgemeinen ignorieren. Atmosphärischer Verlust spielt eine wichtige Rolle bei der Satellitenkommunikation, wo das Signal viele Kilometer durch die Atmosphäre zurücklegen muss. + +.. image:: ../_images_de/atmospheric_attenuation.svg + :align: center + :target: ../_images_de/atmospheric_attenuation.svg + :alt: Diagramm der atmosphärischen Dämpfung in dB/km über der Frequenz mit Spitzen von H2O (Wasser) und O2 (Sauerstoff) + +Signalleistungsgleichung +########################### + +Jetzt ist es an der Zeit, all diese Gewinne und Verluste zusammenzuführen, um unsere Signalleistung am Empfänger :math:`P_r` zu berechnen: + +.. math:: + P_r = P_t + G_t + G_r - L_p - L_{misc} \quad \mathrm{dBW} + +Insgesamt ist es eine einfache Gleichung. Wir addieren die Gewinne und Verluste. Manche betrachten es nicht einmal als Gleichung. Normalerweise zeigen wir die Gewinne, Verluste und Summen in einer Tabelle ähnlich einer Buchhaltung: + +.. list-table:: + :widths: 15 10 + :header-rows: 0 + + * - Pt = 1,0 W + - 0 dBW + * - Gt = 100 + - 20,0 dB + * - Gr = 1 + - 0 dB + * - Lp + - -162,0 dB + * - Lmisc + - -1,0 dB + * - **Pr** + - **-143,0 dBW** + +EIRP +##### + +Als kurzer Einschub: Du wirst möglicherweise die Kenngröße EIRP (Equivalent Isotropically Radiated Power, äquivalente isotropisch abgestrahlte Leistung) sehen, die als :math:`P_t + G_t - L_{Kabel}` definiert ist und die Einheit dBW hat. Durch Addition der Sendeleistung mit dem Sendeantennengain und Subtraktion der sendeseitigen Kabelverluste erhalten wir eine nützliche Kenngröße, die die „hypothetische" Leistung darstellt, die von einer isotropischen (perfekt omnidirektionalen) Antenne abgestrahlt werden müsste, um dieselbe Signalstärke **in Richtung des Hauptstrahls der Antenne** zu liefern. Dieser letzte Teil wird betont, weil jede Antenne mit einem hohen Gewinn (:math:`G_t`) diesen hohen Gewinn nur liefert, wenn sie richtig ausgerichtet ist. Wenn du also gut ausgerichtet bist, gibt dir EIRP alles, was du über die Sendeseite des Linkbudgets wissen musst, weshalb es eine Kenngröße ist, die häufig in Datenblättern von direktionalen Sendern wie Satelliten-Bodenstationen zu finden ist (normalerweise in Form von „max EIRP"). + +************************* +Rauschleistungsbudget +************************* + +Jetzt, da wir die empfangene Signalleistung kennen, wechseln wir das Thema zum empfangenen Rauschen, da wir beides benötigen, um das SNR zu berechnen. Wir können die empfangene Rauschleistung mit einem ähnlichen Leistungsbudget ermitteln. + +Jetzt ist ein guter Zeitpunkt, darüber zu sprechen, wo Rauschen in unsere Kommunikationsverbindung eintritt. Antwort: **Am Empfänger!** Das Signal wird erst dann mit Rauschen korrumpiert, wenn wir es empfangen wollen. Es ist *äußerst* wichtig, diese Tatsache zu verstehen! Viele Studierende internalisieren das nicht ganz und machen daher einen törichten Fehler. Es schwirrt kein Rauschen in der Luft um uns herum. Das Rauschen kommt daher, dass unser Empfänger einen Verstärker und andere Elektronik hat, die nicht perfekt sind und nicht bei 0 Grad Kelvin (K) betrieben werden. + +Eine verbreitete und einfache Formulierung für das Rauschbudget verwendet den „kTB"-Ansatz: + +.. math:: + P_{Rauschen} = kTB + +- :math:`k` – Boltzmann-Konstante = 1,38 x 10-23 J/K = **-228,6 dBW/K/Hz**. Für alle Neugierigen ist die Boltzmann-Konstante eine physikalische Konstante, die die mittlere kinetische Energie der Teilchen in einem Gas mit der Temperatur des Gases verknüpft. +- :math:`T` – Systemrauschtemperatur in K (Kryokühler?), größtenteils basierend auf unserem Verstärker. Das ist der Term, der am schwierigsten zu bestimmen ist und normalerweise sehr ungefähr ist. Du wirst möglicherweise mehr für einen Verstärker mit einer niedrigeren Rauschtemperatur zahlen. +- :math:`B` – Signalbandbreite in Hz, vorausgesetzt, du filterst das Rauschen um dein Signal herum heraus. Ein LTE-Downlink-Signal mit einer Breite von 10 MHz hat :math:`B` auf 10 MHz gesetzt, was 70 dBHz entspricht. + +Das Multiplizieren (oder Addieren in dB) von kTB ergibt unsere Rauschleistung, d.h. den unteren Term unserer SNR-Gleichung. + +************************* +SNR +************************* + +Jetzt, da wir beide Zahlen haben, können wir das Verhältnis nehmen, um das SNR zu finden (weitere Informationen über SNR findest du im Kapitel :ref:`noise-chapter`): + +.. math:: + \mathrm{SNR} = \frac{P_{Signal}}{P_{Rauschen}} + +.. math:: + \mathrm{SNR_{dB}} = P_{Signal\_dB} - P_{Rauschen\_dB} + +Wir streben normalerweise ein SNR > 10 dB an, obwohl es wirklich von der Anwendung abhängt. In der Praxis kann das SNR durch Betrachten der FFT des empfangenen Signals oder durch Berechnen der Leistung mit und ohne das vorhandene Signal überprüft werden (erinnere dich: Varianz = Leistung). Je höher das SNR, desto mehr Bits pro Symbol kannst du ohne zu viele Fehler verwalten. + +*************************** +Beispiel-Linkbudget: ADS-B +*************************** + +Automatic Dependent Surveillance-Broadcast (ADS-B) ist eine Technologie, die von Flugzeugen verwendet wird, um Signale auszusenden, die ihre Position und anderen Status mit Flugsicherungs-Bodenstationen und anderen Flugzeugen teilen. ADS-B ist automatisch, da es keinen Piloten oder externe Eingaben erfordert; es hängt von Daten aus dem Navigationssystem des Flugzeugs und anderen Computern ab. Die Nachrichten sind nicht verschlüsselt (super!). ADS-B-Ausrüstung ist derzeit in Teilen des australischen Luftraums obligatorisch, während die Vereinigten Staaten je nach Größe die Ausrüstung einiger Flugzeuge vorschreiben. + +.. image:: ../_images_de/adsb.jpg + :scale: 120 % + :align: center + +Die physikalische (PHY) Schicht von ADS-B hat folgende Eigenschaften: + +- Übertragung auf 1.090 MHz +- Signalbandbreite ca. 2 MHz +- PPM-Modulation +- Datenrate von 1 Mbit/s, mit Nachrichten zwischen 56 und 112 Mikrosekunden +- Nachrichten tragen jeweils 15 Bytes an Daten, sodass normalerweise mehrere Nachrichten für alle Flugzeuginformationen benötigt werden +- Mehrfachzugriff wird dadurch erreicht, dass Nachrichten mit einer zufällig zwischen 0,4 und 0,6 Sekunden variierenden Periode ausgesendet werden. Diese Randomisierung soll verhindern, dass alle Übertragungen von Flugzeugen aufeinander treffen (einige können sich trotzdem überschneiden, aber das ist in Ordnung) +- ADS-B-Antennen sind vertikal polarisiert +- Die Sendeleistung variiert, sollte aber in der Größenordnung von 100 W (20 dBW) liegen +- Der Sendeantennengain ist omnidirektional, zeigt aber nur nach unten, also sagen wir 3 dB +- ADS-B-Empfänger haben ebenfalls eine omnidirektionale Antenne, also sagen wir 0 dB + +Der Pfadverlust hängt davon ab, wie weit das Flugzeug von unserem Empfänger entfernt ist. Als Beispiel: Es sind etwa 30 km von der University of Maryland (wo der Kurs, aus dem der Inhalt dieses Lehrbuchs stammt, gelehrt wurde) zum BWI-Flughafen. Lass uns den FSPL für diese Entfernung und eine Frequenz von 1.090 MHz berechnen: + +.. math:: + \mathrm{FSPL}_{dB} = 20 \log_{10} d + 20 \log_{10} f - 147{,}55 \left[ \mathrm{dB} \right] + + \mathrm{FSPL}_{dB} = 20 \log_{10} 30e3 + 20 \log_{10} 1090e6 - 147{,}55 \left[ \mathrm{dB} \right] + + \mathrm{FSPL}_{dB} = 122{,}7 \left[ \mathrm{dB} \right] + +Eine andere Möglichkeit ist, :math:`d` als Variable im Linkbudget zu belassen und herauszufinden, wie weit entfernt wir Signale basierend auf einem erforderlichen SNR hören können. + +Da wir definitiv keinen freien Raum haben werden, fügen wir noch 3 dB sonstiger Verluste hinzu. Wir setzen den sonstigen Verlust auf insgesamt 6 dB, um zu berücksichtigen, dass unsere Antenne nicht gut angepasst ist und Kabel-/Steckverbinderverluste vorhanden sind. Angesichts all dieser Kriterien sieht unser Signal-Linkbudget wie folgt aus: + +.. list-table:: + :widths: 15 10 + :header-rows: 0 + + * - Pt + - 20 dBW + * - Gt + - 3 dB + * - Gr + - 0 dB + * - Lp + - -122,7 dB + * - Lmisc + - -6 dB + * - **Pr** + - **-105,7 dBW** + +Für unser Rauschbudget: + +- B = 2 MHz = 2e6 = 63 dBHz +- T müssen wir schätzen, sagen wir 300 K, was 24,8 dBK entspricht. Es variiert je nach Qualität des Empfängers +- k ist immer -228,6 dBW/K/Hz + +.. math:: + P_{Rauschen} = k + T + B = -140{,}8 \quad \mathrm{dBW} + +Daher ist unser SNR -105,7 - (-140,8) = **35,1 dB**. Es ist nicht überraschend, dass es eine große Zahl ist, wenn man bedenkt, dass wir behaupten, nur 30 km vom Flugzeug unter freiem Raum entfernt zu sein. Wenn ADS-B-Signale keine 30 km reichen könnten, wäre ADS-B kein sehr effektives System — niemand würde den anderen hören, bis er sehr nah heran ist. In diesem Beispiel können wir die Signale leicht dekodieren; Pulspositionsmodulation (PPM) ist recht robust und erfordert kein so hohes SNR. Was schwierig ist, ist wenn du versuchst, ADS-B in einem Klassenraum zu empfangen, mit einer Antenne, die sehr schlecht angepasst ist, und einem starken UKW-Radiosender in der Nähe, der Interferenz verursacht. Diese Faktoren könnten leicht zu 20–30 dB an Verlusten führen. + +Dieses Beispiel war wirklich nur eine Überschlagsrechnung, aber es hat die Grundlagen der Erstellung eines Linkbudgets und das Verständnis der wichtigen Parameter einer Kommunikationsverbindung demonstriert. diff --git a/content-de/multipath_fading.rst b/content-de/multipath_fading.rst new file mode 100644 index 00000000..ce00be03 --- /dev/null +++ b/content-de/multipath_fading.rst @@ -0,0 +1,152 @@ +.. _multipath-chapter: + +####################### +Mehrwegeausbreitung +####################### + +In diesem Kapitel stellen wir die Mehrwegeausbreitung vor – ein Ausbreitungsphänomen, bei dem Signale auf zwei oder mehr Wegen den Empfänger erreichen. Dieses Phänomen tritt in realen drahtlosen Systemen auf. Bislang haben wir nur den "AWGN-Kanal" besprochen, d.h. ein Modell für einen drahtlosen Kanal, bei dem dem Signal einfach Rauschen hinzugefügt wird – was nur für Signale über Kabel und einige Satellitenkommunikationssysteme gilt. + +************************* +Mehrwegeausbreitung +************************* + +Alle realistischen drahtlosen Kanäle enthalten viele "Reflektoren", da RF-Signale reflektiert werden. Jedes Objekt zwischen oder in der Nähe des Senders (Tx) oder Empfängers (Rx) kann zusätzliche Ausbreitungswege verursachen. Jeder Weg erfährt eine unterschiedliche Phasenverschiebung (Verzögerung) und Dämpfung (Amplitudenskalierung). Am Empfänger addieren sich alle Wege. Sie können sich konstruktiv, destruktiv oder gemischt addieren. Wir bezeichnen dieses Konzept mehrerer Signalwege als "Mehrwegeausbreitung". Es gibt den Sichtverbindungsweg (LOS, Line-of-Sight) und dann alle anderen Wege. Im folgenden Beispiel zeigen wir den LOS-Weg und einen einzelnen Nicht-LOS-Weg: + +.. image:: ../_images/multipath.svg + :align: center + :target: ../_images/multipath.svg + :alt: Einfache Darstellung der Mehrwegeausbreitung mit dem LOS-Weg und einem einzelnen Mehrweg + +Destruktive Interferenz kann auftreten, wenn die Überlagerung der Wege ungünstig ausfällt. Betrachte das obige Beispiel mit nur zwei Wegen. Abhängig von der Frequenz und dem genauen Abstand der Wege können die beiden Wege mit ungefähr gleicher Amplitude und 180 Grad Phasenverschiebung empfangen werden, wodurch sie sich gegenseitig auslöschen (wie unten dargestellt). Du hast vielleicht im Physikunterricht von konstruktiver und destruktiver Interferenz gehört. In drahtlosen Systemen bezeichnen wir diese destruktive Überlagerung als "tiefes Fading" (Deep Fade), weil unser Signal kurzzeitig verschwindet. + +.. image:: ../_images/destructive_interference.svg + :align: center + :target: ../_images/destructive_interference.svg + +Wege können sich auch konstruktiv addieren und ein starkes Signal erzeugen. Jeder Weg hat eine unterschiedliche Phasenverschiebung und Amplitude, die wir in einem Zeitbereichsdiagramm, dem sogenannten "Leistungsverzögerungsprofil" (Power Delay Profile), visualisieren können: + +.. image:: ../_images/multipath2.svg + :align: center + :target: ../_images/multipath2.svg + :alt: Mehrwegeausbreitung mit dem Leistungsverzögerungsprofil über die Zeit + +Der erste Weg, der der y-Achse am nächsten liegt, ist immer der LOS-Weg (sofern vorhanden), da kein anderer Weg den Empfänger schneller erreichen kann als der LOS-Weg. Typischerweise nimmt die Amplitude mit zunehmender Verzögerung ab, da ein Weg, der später am Empfänger ankommt, eine größere Distanz zurückgelegt hat. + +************************* +Fading +************************* + +Was in der Regel passiert, ist eine Mischung aus konstruktiver und destruktiver Interferenz, die sich mit der Zeit ändert, wenn sich Rx, Tx oder die Umgebung bewegen/verändern. Wir verwenden den Begriff "Fading", wenn wir uns auf die Auswirkungen eines Mehrwegekanals beziehen, der sich **über die Zeit** verändert. Deshalb bezeichnen wir es oft als "Mehrwege-Fading"; es ist wirklich die Kombination aus konstruktiver/destruktiver Interferenz und einer sich verändernden Umgebung. Das Ergebnis ist ein SNR, das über die Zeit variiert; Änderungen liegen je nach Bewegungsgeschwindigkeit von Tx/Rx typischerweise im Millisekunden- bis Mikrosekundenbereich. Unten ist ein SNR-Plot über die Zeit in Millisekunden, der Mehrwege-Fading demonstriert. + +.. image:: ../_images/multipath_fading.png + :scale: 100 % + :align: center + :alt: Mehrwege-Fading verursacht periodische tiefe Fades oder Nullstellen, wo der SNR extrem niedrig wird + +Es gibt zwei Arten von Fading aus **Zeit**-Domain-Perspektive: + +- **Langsames Fading:** Der Kanal ändert sich innerhalb eines Pakets nicht. D.h. ein tiefes Null bei langsamem Fading löscht das gesamte Paket aus. +- **Schnelles Fading:** Der Kanal ändert sich sehr schnell im Vergleich zur Länge eines Pakets. Vorwärtsfehlerkorrektur (Forward Error Correction), kombiniert mit Interleaving, kann schnelles Fading bekämpfen. + +Es gibt auch zwei Arten von Fading aus **Frequenz**-Domain-Perspektive: + +**Frequenzselektives Fading**: Die konstruktive/destruktive Interferenz ändert sich innerhalb des Frequenzbereichs des Signals. Bei einem Breitbandsignal überdecken wir einen großen Frequenzbereich. Denke daran, dass die Wellenlänge bestimmt, ob es konstruktiv oder destruktiv ist. Wenn unser Signal einen weiten Frequenzbereich umfasst, umfasst es auch einen weiten Wellenlängenbereich (da Wellenlänge die inverse Frequenz ist). Folglich können wir unterschiedliche Kanalqualitäten in verschiedenen Teilen unseres Signals (im Frequenzbereich) erhalten. Daher der Name frequenzselektives Fading. + +**Flaches Fading**: Tritt auf, wenn die Signalbandbreite schmal genug ist, sodass alle Frequenzen ungefähr denselben Kanal erfahren. Wenn ein tiefes Fading auftritt, verschwindet das gesamte Signal (für die Dauer des tiefen Fadings). + +In der folgenden Abbildung zeigt die :red:`rote` Form unser Signal im Frequenzbereich, und die schwarze geschwungene Linie zeigt den aktuellen Kanalzustand über die Frequenz. Da das schmalere Signal die gleichen Kanalbedingungen im gesamten Signal erfährt, erlebt es flaches Fading. Das breitere Signal erfährt deutlich frequenzselektives Fading. + +.. image:: ../_images/flat_vs_freq_selective.png + :scale: 70 % + :align: center + :alt: Flaches Fading vs. frequenzselektives Fading + +Hier ist ein Beispiel eines 16 MHz breiten Signals, das kontinuierlich sendet. Es gibt mehrere Momente in der Mitte, wo kurzzeitig ein Teil des Signals fehlt. Dieses Beispiel zeigt frequenzselektives Fading, das Lücken im Signal erzeugt, die einige Frequenzen auslöschen, andere jedoch nicht. + +.. image:: ../_images/fading_example.jpg + :scale: 60 % + :align: center + :alt: Beispiel für frequenzselektives Fading in einem Spektrogramm (auch Wasserfallplot), das Verschmierung und ein Loch im Spektrogramm bei einem tiefen Null zeigt + +************************** +Rayleigh-Fading simulieren +************************** + +Rayleigh-Fading wird verwendet, um Fading über die Zeit zu modellieren, wenn es keinen signifikanten LOS-Weg gibt. Wenn ein dominanter LOS-Weg vorhanden ist, ist das Rician-Fading-Modell besser geeignet, aber wir konzentrieren uns auf Rayleigh. Beachte, dass die Rayleigh- und Rician-Modelle weder den primären Pfadverlust zwischen Sender und Empfänger (wie den als Teil eines Linkbudgets berechneten Pfadverlust) noch Abschattung durch große Objekte beinhalten. Ihre Rolle ist es, das Mehrwege-Fading zu modellieren, das über die Zeit als Ergebnis von Bewegung und Streuobjekten in der Umgebung auftritt. + +Aus dem Rayleigh-Fading-Modell ergeben sich viele Theorien, wie z.B. Ausdrücke für die Pegelüberschreitungsrate und die durchschnittliche Fadedauer. Das Rayleigh-Fading-Modell sagt uns jedoch nicht direkt, wie wir einen Kanal mit dem Modell tatsächlich simulieren sollen. Um Rayleigh-Fading in der Simulation zu erzeugen, müssen wir eine von vielen veröffentlichten Methoden verwenden, und im folgenden Python-Beispiel verwenden wir Clarkes "Summe von Sinusoidalsignalen"-Methode. + +Um einen Rayleigh-Fading-Kanal in Python zu generieren, müssen wir zunächst die maximale Doppler-Verschiebung in Hz angeben, die davon abhängt, wie schnell sich Sender und/oder Empfänger bewegen, bezeichnet mit :math:`\Delta v`. Wenn die Geschwindigkeit klein im Vergleich zur Lichtgeschwindigkeit ist – was in der drahtlosen Kommunikation immer der Fall ist – kann die Doppler-Verschiebung berechnet werden als: + +.. math:: + + f_D = \frac{\Delta v f_c}{c} + +wobei :math:`c` die Lichtgeschwindigkeit ist, ungefähr 3e8 m/s, und :math:`f_c` die Trägerfrequenz der Übertragung ist. + +Wir wählen auch die Anzahl der zu simulierenden Sinusoide, und es gibt keine richtige Antwort, da sie auf der Anzahl der Streuer in der Umgebung basiert, die wir nie wirklich kennen. Als Teil der Berechnungen nehmen wir an, dass die Phase des empfangenen Signals von jedem Weg gleichmäßig zufällig zwischen 0 und :math:`2\pi` ist. Der folgende Code simuliert einen Rayleigh-Fading-Kanal mit Clarkes Methode: + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + + # Simulationsparameter, diese können frei angepasst werden + v_mph = 60 # Geschwindigkeit von TX oder RX, in Meilen pro Stunde + center_freq = 200e6 # RF-Trägerfrequenz in Hz + Fs = 1e5 # Abtastrate der Simulation + N = 100 # Anzahl der zu summierenden Sinusoide + + v = v_mph * 0.44704 # Umrechnung in m/s + fd = v*center_freq/3e8 # maximale Doppler-Verschiebung + print("Maximale Doppler-Verschiebung:", fd) + t = np.arange(0, 1, 1/Fs) # Zeitvektor. (Start, Stop, Schritt) + x = np.zeros(len(t)) + y = np.zeros(len(t)) + for i in range(N): + alpha = (np.random.rand() - 0.5) * 2 * np.pi + phi = (np.random.rand() - 0.5) * 2 * np.pi + x = x + np.random.randn() * np.cos(2 * np.pi * fd * t * np.cos(alpha) + phi) + y = y + np.random.randn() * np.sin(2 * np.pi * fd * t * np.cos(alpha) + phi) + + # z ist der komplexe Koeffizient, der den Kanal darstellt – als Phasenverschiebung und Amplitudenskalierung zu verstehen + z = (1/np.sqrt(N)) * (x + 1j*y) # das ist, was du bei der Kanalsimulation tatsächlich verwenden würdest + z_mag = np.abs(z) # Betrag für die Darstellung nehmen + z_mag_dB = 10*np.log10(z_mag) # in dB umrechnen + + # Fading über die Zeit darstellen + plt.plot(t, z_mag_dB) + plt.plot([0, 1], [0, 0], ':r') # 0 dB + plt.legend(['Rayleigh-Fading', 'Kein Fading']) + plt.axis([0, 1, -15, 5]) + plt.show() + +Wenn du dieses Kanalmodell als Teil einer größeren Simulation verwenden möchtest, multiplizierst du das empfangene Signal einfach mit der komplexen Zahl :code:`z`, die flaches Fading darstellt. Der Wert :code:`z` würde dann bei jedem Zeitschritt aktualisiert. Das bedeutet, dass alle Frequenzkomponenten des Signals zu einem gegebenen Zeitpunkt denselben Kanal erfahren, sodass du **kein** frequenzselektives Fading simulierst – dafür ist eine Mehrtap-Kanalimpulsantwort erforderlich, auf die wir in diesem Kapitel nicht eingehen. Wenn wir den Betrag von :code:`z` betrachten, können wir das Rayleigh-Fading über die Zeit sehen: + +.. image:: ../_images/rayleigh.svg + :align: center + :target: ../_images/rayleigh.svg + :alt: Simulation von Rayleigh-Fading + +Beachte die tiefen Fades, die kurz auftreten, sowie den kleinen Zeitanteil, in dem der Kanal tatsächlich besser abschneidet als ohne Fading. + + +**************************** +Mehrwege-Fading bekämpfen +**************************** + +In der modernen Kommunikation haben wir Methoden entwickelt, um Mehrwege-Fading zu bekämpfen. + +CDMA +##### + +3G-Mobilfunk verwendet eine Technologie namens Code Division Multiple Access (CDMA). Mit CDMA nimmst du ein Schmalbandssignal und spreizst es vor der Übertragung über eine breite Bandbreite (mit einer Spreizspektrumtechnik namens DSSS). Bei frequenzselektivem Fading ist es unwahrscheinlich, dass alle Frequenzen gleichzeitig ein tiefes Null haben. Am Empfänger wird die Spreizung rückgängig gemacht, und dieser De-Spreizungsprozess bekämpft ein tiefes Null erheblich. + +.. image:: ../_images/cdma.png + :scale: 100 % + :align: center + +OFDM +##### + +4G-Mobilfunk, WiFi und viele andere Technologien verwenden ein Schema namens Orthogonal Frequency-Division Multiplexing (OFDM). OFDM verwendet sogenannte Subträger, bei denen wir das Signal im Frequenzbereich in viele schmale, eng beieinander liegende Signale aufteilen. Um Mehrwege-Fading zu bekämpfen, können wir die Zuweisung von Daten an Subträger vermeiden, die sich in einem tiefen Fading befinden, obwohl dies erfordert, dass die Empfangsseite schnell genug Kanalinformationen zurück an den Sender schickt. Wir können auch höherwertige Modulationsverfahren Subträgern mit guter Kanalqualität zuweisen, um unsere Datenrate zu maximieren. diff --git a/content-de/noise.rst b/content-de/noise.rst new file mode 100644 index 00000000..b901047a --- /dev/null +++ b/content-de/noise.rst @@ -0,0 +1,631 @@ +.. _noise-chapter: + +################################## +Rauschen und Zufallsvariablen +################################## + +In diesem Kapitel besprechen wir Rauschen, einschließlich dessen Modellierung und Behandlung in einem drahtlosen Kommunikationssystem. Konzepte umfassen AWGN, komplexes Rauschen und SNR/SINR. Wir führen auch Dezibel (dB) ein, da es in der drahtlosen Kommunikation und bei SDRs weit verbreitet ist. Abschließend gehen wir tiefer in die grundlegenden Konzepte der Zufallsvariablen und Zufallsprozesse ein, die für das Verständnis von Rauschen, Kanaleffekten und vielen Signalverarbeitungstechniken in der drahtlosen Kommunikation unerlässlich sind. Wir behandeln Wahrscheinlichkeitsverteilungen, Erwartungswert, Varianz und wie sich Zufallsprozesse im Laufe der Zeit entwickeln. Diese Konzepte bilden das mathematische Fundament für die Analyse von Rauschen und vielen anderen Themen in SDR und DSP. + +************************ +Gaußsches Rauschen +************************ + +Die meisten Menschen kennen das Konzept von Rauschen: unerwünschte Schwankungen, die unser gewünschtes Signal verdecken können. Rauschen sieht etwa so aus: + +.. image:: ../_images_de/noise.png + :scale: 70 % + :align: center + :target: ../_images_de/noise.png + +Beachte, dass der Durchschnittswert im Zeitbereichsgraph null ist. Wenn der Durchschnittswert nicht null wäre, könnten wir den Durchschnittswert subtrahieren, ihn als Bias bezeichnen, und wir hätten einen Durchschnitt von null übrig. Beachte auch, dass die einzelnen Punkte im Graphen *nicht* „gleichmäßig zufällig" sind, d.h. größere Werte sind seltener; die meisten Punkte liegen näher an null. + +Wir nennen diese Art von Rauschen „Gaußsches Rauschen". Es ist ein gutes Modell für die Art von Rauschen, die aus vielen natürlichen Quellen stammt, wie z.B. thermische Schwingungen von Atomen im Silizium der HF-Komponenten unseres Empfängers. Der **zentrale Grenzwertsatz** sagt uns, dass die Summe vieler Zufallsprozesse dazu neigt, eine Gaußsche Verteilung zu haben, selbst wenn die einzelnen Prozesse andere Verteilungen haben. Mit anderen Worten: Wenn viele zufällige Dinge passieren und sich akkumulieren, erscheint das Ergebnis annähernd Gaußsch, selbst wenn die einzelnen Dinge nicht Gaußsch verteilt sind. + + +.. image:: ../_images_de/central_limit_theorem.svg + :align: center + :target: ../_images_de/central_limit_theorem.svg + :alt: Visualisierung des zentralen Grenzwertsatzes als Summe vieler Zufallsprozesse, die zu einer Normalverteilung (auch Gaußverteilung genannt) führen + +Die Gaußsche Verteilung wird auch als „Normalverteilung" bezeichnet (erinnere dich an die Glockenkurve). + +Die Gaußsche Verteilung hat zwei Parameter: Mittelwert und Varianz. Wir haben bereits besprochen, wie der Mittelwert als null betrachtet werden kann, da man den Mittelwert (oder Bias) immer entfernen kann, wenn er nicht null ist. Die Varianz ändert, wie „stark" das Rauschen ist. Eine höhere Varianz führt zu größeren Zahlen. Aus diesem Grund definiert die Varianz die Rauschleistung. + +Varianz ist gleich Standardabweichung hoch zwei (:math:`\sigma^2`). + +************************ +Dezibel (dB) +************************ + +Wir machen jetzt einen kurzen Ausflug, um dB formal einzuführen. Du hast vielleicht schon von dB gehört, und wenn du bereits damit vertraut bist, kannst du diesen Abschnitt überspringen. + +Das Arbeiten mit dB ist äußerst nützlich, wenn wir gleichzeitig mit kleinen und großen Zahlen umgehen müssen oder einfach mit einer Reihe von sehr großen Zahlen. Überlege, wie umständlich es wäre, mit Zahlen der Größenordnung in Beispiel 1 und Beispiel 2 zu arbeiten. + +Beispiel 1: Signal 1 wird mit 2 Watt empfangen und der Rauschboden liegt bei 0,0000002 Watt. + +Beispiel 2: Eine Küchenmaschine ist 100.000 Mal lauter als ein ruhiges Landgebiet, und eine Kettensäge ist 10.000 Mal lauter als eine Küchenmaschine (in Bezug auf die Leistung der Schallwellen). + +Ohne dB, d.h. wenn wir in normalen „linearen" Einheiten arbeiten, müssen wir viele Nullen verwenden, um die Werte in den Beispielen 1 und 2 darzustellen. Ehrlich gesagt würden wir den Rauschboden nicht einmal sehen, wenn wir Signal 1 über die Zeit aufzeichnen würden. Wenn die Skala der y-Achse z.B. von 0 bis 3 Watt reicht, wäre das Rauschen zu klein, um im Diagramm sichtbar zu sein. Um diese Skalen gleichzeitig darzustellen, arbeiten wir mit einer logarithmischen Skala. + +Um die Skalierungsprobleme, die wir in der Signalverarbeitung begegnen, weiter zu veranschaulichen, betrachte die folgenden Wasserfalldarstellungen von drei gleichen Signalen. Die linke Seite zeigt das Originalsignal auf linearer Skala, und die rechte Seite zeigt die in logarithmische Skala (dB) umgewandelten Signale. Beide Darstellungen verwenden exakt dieselbe Farbkarte, wobei Blau der niedrigste Wert und Gelb der höchste ist. Das Signal auf der linken Seite auf der linearen Skala ist kaum zu erkennen. + +.. image:: ../_images_de/linear_vs_log.png + :scale: 70 % + :align: center + :alt: Darstellung der Bedeutung von dB (Dezibel) mit einem Spektrogramm in linearer vs. logarithmischer Skala + :target: ../_images_de/linear_vs_log.png + +Für einen gegebenen Wert x können wir x in dB mit der folgenden Formel darstellen: + +.. math:: + x_{dB} = 10 \log_{10} x + +In Python: + +.. code-block:: python + + x_db = 10.0 * np.log10(x) + +Du hast vielleicht gesehen, dass :code:`10 *` in anderen Bereichen ein :code:`20 *` ist. Immer wenn du es mit einer Leistung zu tun hast, verwendest du 10, und du verwendest 20, wenn du mit einem Nicht-Leistungswert wie Spannung oder Strom arbeitest. In der DSP arbeiten wir in der Regel mit Leistung. + +Wir rechnen von dB zurück in linear (normale Zahlen) mit: + +.. math:: + x = 10^{x_{dB}/10} + +In Python: + +.. code-block:: python + + x = 10.0 ** (x_db / 10.0) + +Verliere dich nicht in der Formel, denn es gibt hier ein wichtiges Konzept zu verstehen. In der DSP arbeiten wir gleichzeitig mit sehr großen und sehr kleinen Zahlen (z.B. die Stärke eines Signals im Vergleich zur Stärke des Rauschens). Die logarithmische Skala von dB ermöglicht uns eine größere Dynamik, wenn wir Zahlen ausdrücken oder darstellen. Sie bietet auch einige Vorteile, wie z.B. die Möglichkeit zu addieren, wo wir normalerweise multiplizieren würden (wie wir im Kapitel :ref:`link-budgets-chapter` sehen werden). + +Einige häufige Fehler, auf die man bei Neulingen in Bezug auf dB stößt: + +1. Verwenden des natürlichen Logarithmus anstelle des Logarithmus zur Basis 10, da die log()-Funktion der meisten Programmiersprachen eigentlich der natürliche Logarithmus ist. +2. Vergessen, dB anzugeben, wenn eine Zahl ausgedrückt oder eine Achse beschriftet wird. Wenn wir in dB sind, müssen wir es irgendwo kennzeichnen. +3. Wenn du in dB bist, addierst/subtrahierst du Werte anstatt zu multiplizieren/dividieren, z.B.: + +.. image:: ../_images_de/db.png + :scale: 80 % + :align: center + :target: ../_images_de/db.png + +Es ist auch wichtig zu verstehen, dass dB technisch gesehen keine „Einheit" ist. Ein Wert in dB allein ist einheitenlos, wie wenn etwas 2x größer ist — es gibt keine Einheiten, bis du mir die Einheiten nennst. dB ist eine relative Angabe. In der Audiotechnik meinen sie, wenn sie dB sagen, eigentlich dBA, was eine Einheit für den Schallpegel ist (das A sind die Einheiten). In der Funktechnik verwenden wir typischerweise Watt für einen tatsächlichen Leistungspegel. Daher kannst du dBW als Einheit sehen, die relativ zu 1 W ist. Du kannst auch dBmW sehen (oft kurz als dBm geschrieben), was relativ zu 1 mW ist. Zum Beispiel kann jemand sagen „unser Sender ist auf 3 dBW eingestellt" (also 2 Watt). Manchmal verwenden wir dB allein, was bedeutet, es ist relativ und hat keine Einheiten. Man kann sagen: „unser Signal wurde 20 dB über dem Rauschboden empfangen". Hier ist ein kleiner Tipp: 0 dBm = -30 dBW. + +Hier sind einige häufige Umrechnungen, die ich dir zu merken empfehle: + +====== ===== +Linear dB +====== ===== +1x 0 dB +2x 3 dB +10x 10 dB +0,5x -3 dB +0,1x -10 dB +100x 20 dB +1000x 30 dB +10000x 40 dB +====== ===== + +Hier sind schließlich einige Beispielleistungspegel in dBm, um diese Zahlen in eine Perspektive zu setzen: + +=========== === +80 dBm Sendeleistung eines ländlichen UKW-Radiosenders +62 dBm Maximale Leistung eines Amateurfunksenders +60 dBm Leistung einer typischen Haushaltsmikrowelle +37 dBm Maximale Leistung eines typischen tragbaren CB- oder Amateurfunkgeräts +27 dBm Typische Sendeleistung eines Mobiltelefons +15 dBm Typische WLAN-Sendeleistung +10 dBm Maximale Sendeleistung von Bluetooth (Version 4) +-10 dBm Maximale empfangene Leistung für WLAN +-70 dBm Beispiel für empfangene Leistung eines Amateurfunksignals +-100 dBm Minimale empfangene Leistung für WLAN +-127 dBm Typische empfangene Leistung von GPS-Satelliten +=========== === + + +************************* +Rauschen im Frequenzbereich +************************* + +Im Kapitel :ref:`freq-domain-chapter` haben wir uns mit „Fourier-Paaren" befasst, d.h. wie ein bestimmtes Zeitbereichssignal im Frequenzbereich aussieht. Wie sieht nun Gaußsches Rauschen im Frequenzbereich aus? Die folgenden Graphen zeigen simuliertes Rauschen im Zeitbereich (oben) und eine Darstellung der Leistungsspektraldichte (PSD) dieses Rauschens (unten). Diese Diagramme wurden aus GNU Radio entnommen. + +.. image:: ../_images_de/noise_freq.png + :scale: 110 % + :align: center + :alt: AWGN im Zeitbereich ist auch Gaußsches Rauschen im Frequenzbereich, obwohl es wie eine flache Linie aussieht, wenn du den Betrag nimmst und Mittelung durchführst + :target: ../_images_de/noise_freq.png + +Wir können sehen, dass es über alle Frequenzen hinweg ungefähr gleich aussieht und relativ flach ist. Es stellt sich heraus, dass Gaußsches Rauschen im Zeitbereich auch im Frequenzbereich Gaußsches Rauschen ist. Warum sehen die beiden obigen Diagramme dann nicht gleich aus? Das liegt daran, dass das Frequenzbereichsdiagramm den Betrag der FFT zeigt, sodass es nur positive Zahlen gibt. Wichtig ist, dass eine logarithmische Skala verwendet wird, also der Betrag in dB angezeigt wird. Andernfalls würden diese Graphen gleich aussehen. Wir können das selbst beweisen, indem wir etwas Rauschen (im Zeitbereich) in Python erzeugen und dann die FFT berechnen. + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + + N = 1024 # Anzahl der zu simulierenden Samples, beliebige Zahl wählbar + x = np.random.randn(N) + plt.plot(x, '.-') + plt.show() + + X = np.fft.fftshift(np.fft.fft(x)) + X = X[N//2:] # nur positive Frequenzen betrachten; // ist ganzzahlige Division + plt.plot(np.real(X), '.-') + plt.show() + +Beachte, dass die Funktion :code:`randn()` standardmäßig Mittelwert = 0 und Varianz = 1 verwendet. Beide Diagramme werden etwa so aussehen: + +.. image:: ../_images_de/noise_python.png + :scale: 100 % + :align: center + :alt: Beispiel für in Python simuliertes weißes Rauschen + :target: ../_images_de/noise_python.png + +Du kannst dann die flache PSD, die wir in GNU Radio hatten, erzeugen, indem du den Logarithmus nimmst und viele davon mittelst. Das Signal, das wir erzeugen und dessen FFT wir berechnet haben, war ein reelles Signal (im Gegensatz zu einem komplexen), und die FFT eines reellen Signals hat übereinstimmende negative und positive Teile; deshalb haben wir nur den positiven Teil der FFT-Ausgabe gespeichert (die zweite Hälfte). Aber warum haben wir nur „reelles" Rauschen erzeugt, und wie spielen komplexe Signale dabei eine Rolle? + +************************* +Komplexes Rauschen +************************* + +„Komplexes Gaußsches Rauschen" ist das, was wir erleben werden, wenn wir ein Signal im Basisband haben; die Rauschleistung ist gleichmäßig auf den Real- und Imaginärteil aufgeteilt. Und am wichtigsten ist, dass Real- und Imaginärteil unabhängig voneinander sind — wenn du die Werte des einen kennst, sagt das dir nichts über die Werte des anderen. + +Wir können komplexes Gaußsches Rauschen in Python erzeugen mit: + +.. code-block:: python + + n = np.random.randn() + 1j * np.random.randn() + +Aber Vorsicht! Die obige Gleichung erzeugt nicht die gleiche „Menge" an Rauschen wie :code:`np.random.randn()` in Bezug auf die Leistung (bekannt als Rauschleistung). Wir können die durchschnittliche Leistung eines Signals (oder Rauschens) mit null Mittelwert berechnen mit: + +.. code-block:: python + + power = np.var(x) + +wobei np.var() die Funktion für die Varianz ist. Hier ist die Leistung unseres Signals n gleich 2. Um komplexes Rauschen mit „Einheitsleistung", d.h. einer Leistung von 1 (was die Dinge vereinfacht), zu erzeugen, müssen wir folgendes verwenden: + +.. code-block:: python + + n = (np.random.randn(N) + 1j*np.random.randn(N))/np.sqrt(2) # AWGN mit Einheitsleistung + +Um komplexes Rauschen im Zeitbereich darzustellen, benötigen wir wie bei jedem komplexen Signal zwei Linien: + +.. code-block:: python + + n = (np.random.randn(N) + 1j*np.random.randn(N))/np.sqrt(2) + plt.plot(np.real(n),'.-') + plt.plot(np.imag(n),'.-') + plt.legend(['real','imag']) + plt.show() + +.. image:: ../_images_de/noise3.png + :scale: 80 % + :align: center + :alt: In Python simuliertes komplexes Rauschen + :target: ../_images_de/noise3.png + +Du kannst sehen, dass Real- und Imaginärteil vollständig unabhängig sind. + +Wie sieht komplexes Gaußsches Rauschen auf einem IQ-Diagramm aus? Erinnere dich, dass das IQ-Diagramm den Realteil (horizontale Achse) und den Imaginärteil (vertikale Achse) zeigt, die beide unabhängige Gaußsche Zufallsvariablen sind. + +.. code-block:: python + + plt.plot(np.real(n),np.imag(n),'.') + plt.grid(True, which='both') + plt.axis([-2, 2, -2, 2]) + plt.show() + +.. image:: ../_images_de/noise_iq.png + :scale: 60 % + :align: center + :alt: Komplexes Rauschen auf einem IQ- oder Konstellationsdiagramm, in Python simuliert + :target: ../_images_de/noise_iq.png + +Es sieht so aus, wie wir es erwarten würden: ein zufälliger Blob, der um 0 + 0j oder den Ursprung zentriert ist. Lass uns zum Spaß versuchen, einem QPSK-Signal Rauschen hinzuzufügen, um zu sehen, wie das IQ-Diagramm aussieht: + +.. image:: ../_images_de/noisey_qpsk.png + :scale: 60 % + :align: center + :alt: Verrauschtes QPSK in Python simuliert + :target: ../_images_de/noisey_qpsk.png + +Was passiert, wenn das Rauschen stärker wird? + +.. image:: ../_images_de/noisey_qpsk2.png + :scale: 50 % + :align: center + :alt: Verrauschtes QPSK mit stärkerem Rauschen in Python simuliert + :target: ../_images_de/noisey_qpsk2.png + +Wir beginnen zu verstehen, warum das drahtlose Übertragen von Daten nicht so einfach ist. Wir möchten so viele Bits pro Symbol wie möglich senden, aber wenn das Rauschen zu hoch ist, erhalten wir fehlerhafte Bits auf der Empfangsseite. + +************************* +AWGN +************************* + +Additives Weißes Gaußsches Rauschen (AWGN, engl. Additive White Gaussian Noise) ist eine Abkürzung, die du in der DSP- und SDR-Welt häufig hören wirst. Das GN (Gaußsches Rauschen) haben wir bereits besprochen. „Additiv" bedeutet einfach, dass das Rauschen zu unserem empfangenen Signal addiert wird. „Weiß" bedeutet im Frequenzbereich, dass das Spektrum über das gesamte Beobachtungsband flach ist. In der Praxis wird es fast immer weiß oder annähernd weiß sein. In diesem Lehrbuch verwenden wir AWGN als einzige Form von Rauschen bei Kommunikationsverbindungen, Linkbudgets usw. Nicht-AWGN-Rauschen ist eher ein Nischenthema. + +************************* +SNR und SINR +************************* + +Das Signal-Rausch-Verhältnis (SNR, engl. Signal-to-Noise Ratio) ist die Messgröße, mit der wir die Stärkeunterschiede zwischen Signal und Rauschen messen. Es ist ein Verhältnis und daher einheitenlos. SNR wird in der Praxis fast immer in dB angegeben. Oft programmieren wir in Simulationen so, dass unsere Signale eine Einheitsleistung haben (Leistung = 1). Auf diese Weise können wir ein SNR von 10 dB erzeugen, indem wir Rauschen mit -10 dB Leistung erzeugen, indem wir die Varianz bei der Erzeugung des Rauschens anpassen. + +.. math:: + \mathrm{SNR} = \frac{P_{Signal}}{P_{Rauschen}} + +.. math:: + \mathrm{SNR_{dB}} = P_{Signal\_dB} - P_{Rauschen\_dB} + +Wenn jemand „SNR = 0 dB" sagt, bedeutet das, dass Signal- und Rauschleistung gleich sind. Ein positives SNR bedeutet, dass unser Signal höhere Leistung als das Rauschen hat, während ein negatives SNR bedeutet, dass das Rauschen höhere Leistung hat. Das Erkennen von Signalen bei negativem SNR ist in der Regel ziemlich schwierig. + +Wie bereits erwähnt, ist die Leistung in einem Signal gleich der Varianz des Signals. Daher können wir SNR als das Verhältnis von Signal- zu Rauschvarianz darstellen: + +.. math:: + \mathrm{SNR} = \frac{P_{Signal}}{P_{Rauschen}} = \frac{\sigma^2_{Signal}}{\sigma^2_{Rauschen}} + +Das Signal-zu-Interferenz-plus-Rausch-Verhältnis (SINR, engl. Signal-to-Interference-plus-Noise Ratio) ist im Wesentlichen dasselbe wie SNR, außer dass Interferenz zusammen mit dem Rauschen im Nenner enthalten ist. + +.. math:: + \mathrm{SINR} = \frac{P_{Signal}}{P_{Interferenz} + P_{Rauschen}} + +Was als Interferenz gilt, hängt von der Anwendung/Situation ab, aber in der Regel handelt es sich um ein anderes Signal, das das Nutz-Signal (SOI, Signal of Interest) stört, und entweder im Frequenzbereich mit dem SOI überlappt und/oder aus irgendeinem Grund nicht herausgefiltert werden kann. + +********************************* +Tieferer Einblick in Zufallsvariablen +********************************* + +Bisher haben wir vermieden, zu mathematisch zu werden, aber jetzt machen wir einen Schritt zurück und stellen das Konzept der Zufallsvariablen vor und wie sie im Kontext der drahtlosen Kommunikation und SDR verwendet werden. Eine **Zufallsvariable** ist ein mathematisches Konzept, das Ergebnisse eines Zufallsexperiments auf numerische Werte abbildet. Zufallsvariablen repräsentieren Größen, deren Werte bis zur Beobachtung oder Messung ungewiss sind, wie unsere Rausch-Samples. Denke an das Würfeln mit einem sechsseitigen Würfel. Bevor du würfelst, weißt du nicht, welche Zahl erscheint. Wir können eine Zufallsvariable :math:`X` definieren, die das Ergebnis des Würfelns darstellt. Der Wert von :math:`X` ist eine der Zahlen {1, 2, 3, 4, 5, 6}, aber wir wissen nicht welche, bis wir tatsächlich würfeln. + +Im Kontext der drahtlosen Kommunikation und SDR sind Zufallsvariablen überall: + +* Das thermische Rauschen in einem Empfänger wird zu jedem Zeitpunkt als Zufallsvariable modelliert +* Die Amplitude eines empfangenen Signals, das von Mehrwegeausbreitung beeinflusst wird, ist zufällig +* Der durch einen sich ändernden Kanal eingeführte Phasenversatz kann als Zufallsvariable zwischen :math:`0` und :math:`2\pi` modelliert werden +* Selbst die Datenbits, die wir senden, können als Zufallsvariablen behandelt werden + +**Einzelne Stichprobe vs. viele Stichproben** + +Dies ist eine entscheidende Unterscheidung, die oft zu Verwirrung führt: + +* Eine **einzelne Realisierung** oder **einzelne Stichprobe** einer Zufallsvariable ist nur eine Zahl — ein Ergebnis des Zufallsexperiments +* Um eine Zufallsvariable zu charakterisieren (ihren Durchschnitt, ihre Streuung usw. zu finden), benötigen wir **viele Realisierungen** — viele Ergebnisse + +Wenn du z.B. ``np.random.randn()`` in Python ohne Argumente aufrufst, gibt es eine einzelne Zufallszahl aus einer Gaußschen Verteilung zurück. Diese einzelne Zahl sagt dir fast nichts über die Verteilung selbst. Aber wenn du ``np.random.randn(10000)`` aufrufst und 10.000 Samples erzeugst, kannst du jetzt Eigenschaften der Verteilung wie ihren Mittelwert und ihre Varianz schätzen. + +.. code-block:: python + + import numpy as np + + # Einzelne Stichprobe - nur eine Zahl + x_single = np.random.randn() + print(x_single) # könnte 0.534, -1.23 oder ein anderer Wert sein + + # Viele Stichproben - jetzt können wir die Verteilung charakterisieren + x_many = np.random.randn(10000) + print(np.mean(x_many)) # wird nahe bei 0 liegen + print(np.var(x_many)) # wird nahe bei 1 liegen + +Gemeinsame Verteilungen +######################## + +Bisher haben wir uns auf einzelne Zufallsvariablen konzentriert. Wenn wir gleichzeitig mit zwei oder mehr Zufallsvariablen arbeiten, verwenden wir eine **gemeinsame Verteilung** (engl. Joint Distribution). + +Für kontinuierliche Variablen :math:`X` und :math:`Y` wird dies durch die **gemeinsame Wahrscheinlichkeitsdichtefunktion (PDF)** beschrieben: + +.. math:: + f_{X,Y}(x,y) + +Die gemeinsame PDF sagt uns, wie wahrscheinlich es ist, dass :math:`X` den Wert :math:`x` *und* :math:`Y` gleichzeitig den Wert :math:`y` annimmt. + +Aus der gemeinsamen PDF können wir berechnen: + +* Marginale PDFs (z.B. :math:`f_X(x)` oder :math:`f_Y(y)`) +* Erwartungswerte wie :math:`E[XY]` +* Kovarianz und Korrelation +* Wahrscheinlichkeiten, die beide Variablen betreffen + +Die marginale PDF von :math:`X` erhält man z.B. durch Integration über :math:`Y`: + +.. math:: + f_X(x) = \int_{-\infty}^{\infty} f_{X,Y}(x,y)\,dy + +Gemeinsame Verteilungen bilden das mathematische Fundament für das Verständnis von Abhängigkeit, Korrelation und Unabhängigkeit zwischen Zufallsvariablen. + + +Wahrscheinlichkeitsverteilungen +################################ + +Eine **Wahrscheinlichkeitsverteilung** beschreibt, wie wahrscheinlich verschiedene Werte einer Zufallsvariablen sind. Für eine stetige Zufallsvariable verwenden wir eine **Wahrscheinlichkeitsdichtefunktion (PDF)**, die mit :math:`f_X(x)` bezeichnet wird. Die PDF sagt uns die relative Wahrscheinlichkeit, dass die Zufallsvariable verschiedene Werte annimmt. + +Die wichtigste Verteilung in SDR und Kommunikation ist die **Gaußsche (Normal-)Verteilung**. Eine Gaußsche Zufallsvariable :math:`X` mit Mittelwert :math:`\mu` und Varianz :math:`\sigma^2` hat die PDF: + +.. math:: + f_X(x) = \frac{1}{\sqrt{2\pi\sigma^2}} e^{-\frac{(x-\mu)^2}{2\sigma^2}} + +Das ist die berühmte „Glockenkurve", die du wahrscheinlich schon gesehen hast. Die Verteilung wird vollständig durch zwei Parameter charakterisiert: + +* **Mittelwert** :math:`\mu`: der Mittelpunkt der Verteilung +* **Varianz** :math:`\sigma^2`: wie weit die Verteilung gestreut ist (die Standardabweichung :math:`\sigma` ist die Quadratwurzel der Varianz) + +In Python erzeugt ``np.random.randn()`` Stichproben aus einer **Standard-Gaußverteilung** mit :math:`\mu = 0` und :math:`\sigma^2 = 1`. Wir können das visualisieren: + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + + # 10.000 Stichproben aus der Standard-Gaußverteilung erzeugen + x = np.random.randn(10000) + + # Histogramm zur Visualisierung der Verteilung erstellen + plt.hist(x, bins=50, density=True, alpha=0.7, edgecolor='black') + plt.xlabel('Wert') + plt.ylabel('Wahrscheinlichkeitsdichte') + plt.title('Gaußverteilung (μ=0, σ²=1)') + plt.grid(True) + plt.show() + +.. image:: ../_images_de/gaussian_histogram.png + :scale: 80% + :align: center + :alt: Histogramm von Gaußsch verteilten Stichproben + :target: ../_images_de/gaussian_histogram.png + +Erwartungswert (a.k.a. Mittelwert) +#################################### + +Der **Erwartungswert** einer Zufallsvariablen, bezeichnet als :math:`E[X]` oder :math:`\mu`, stellt ihren durchschnittlichen Wert über viele Realisierungen dar. Für eine stetige Zufallsvariable mit PDF :math:`f_X(x)` ist der Erwartungswert: + +.. math:: + E[X] = \int_{-\infty}^{\infty} x \cdot f_X(x) \, dx + +In der Praxis schätzen wir den Erwartungswert, wenn wir :math:`N` Stichproben :math:`x_1, x_2, \ldots, x_N` aus der Verteilung haben, mit dem **Stichprobenmittelwert**: + +.. math:: + \hat{\mu} = \frac{1}{N} \sum_{n=1}^{N} x_n + +Der Erwartungswert ist ein **linearer Operator**, was bedeutet: + +* :math:`E[aX + b] = aE[X] + b` für Konstanten :math:`a` und :math:`b` +* :math:`E[X + Y] = E[X] + E[Y]` für beliebige zwei Zufallsvariablen + +Diese Linearität ist in der Signalverarbeitung äußerst nützlich! + +Varianz und Standardabweichung +################################ + +Die **Varianz** einer Zufallsvariablen, bezeichnet als :math:`\text{Var}(X)` oder :math:`\sigma^2`, misst, wie weit ihre Werte um den Mittelwert gestreut sind. Sie ist definiert als der Erwartungswert der quadrierten Abweichung vom Mittelwert: + +.. math:: + \text{Var}(X) = E[(X - \mu)^2] = E[X^2] - (E[X])^2 + +Wenn wir :math:`N` Stichproben haben, schätzen wir die Varianz mit: + +.. math:: + \hat{\sigma}^2 = \frac{1}{N} \sum_{n=1}^{N} (x_n - \hat{\mu})^2 + +Die **Standardabweichung** :math:`\sigma` ist einfach die Quadratwurzel der Varianz: :math:`\sigma = \sqrt{\sigma^2}`. + +Beachte das Symbol :math:`\enspace \hat{} \enspace`, bekannt als „Hut" (engl. hat), in der obigen Gleichung bei :math:`\sigma` und beim Stichprobenmittelwert. Der Hut symbolisiert, dass wir den Mittelwert/die Varianz schätzen. Es ist nicht immer genau gleich dem wahren Mittelwert/der wahren Varianz, aber es nähert sich dem wahren Wert an, je mehr Stichproben wir hinzufügen. + +**Wichtige Eigenschaft:** Wenn :math:`X` eine Zufallsvariable mit Varianz :math:`\sigma^2` ist, dann gilt: + +* Skalierung: :math:`\text{Var}(aX) = a^2 \text{Var}(X)` +* Verschiebung: :math:`\text{Var}(X + b) = \text{Var}(X)` (das Hinzufügen einer Konstante ändert die Streuung nicht) + +Und entsprechend für die Standardabweichung :math:`\sigma`: + +* Skalierung: :math:`\sigma(aX) = a\sigma(X)` +* Verschiebung: :math:`\sigma(X+b) = \sigma(X)` + +.. image:: ../_images_de/gaussian_transformed.png + :scale: 80% + :align: center + :alt: Skalierung und Verschiebung der Gaußverteilung (beachte die Skalen auf den x- und y-Achsen) + :target: ../_images_de/gaussian_transformed.png + +Skalierung und Verschiebung der Gaußverteilung. (Beachte die Skalen auf den x- und y-Achsen) + +**Varianz und Leistung** + +In der Signalverarbeitung ist für ein Signal **mit Mittelwert null** (Mittelwert ≈ 0) die Varianz gleich der **mittleren Leistung**. Deshalb verwenden wir diese Begriffe oft synonym: + +.. math:: + P = \text{Var}(X) = E[X^2] \quad \text{(wenn } E[X] = 0\text{)} + +Diese Beziehung ist fundamental für die Analyse von Rauschleistung, Signal-Rausch-Verhältnis (SNR) und Linkbudgets. + +.. code-block:: python + + noise_power = 2.0 + n = np.random.randn(N) * np.sqrt(noise_power) + print(np.var(n)) # wird ungefähr 2,0 sein + +Kovarianz +########## + +Die **Kovarianz** zwischen zwei Zufallsvariablen :math:`X` und :math:`Y` ist definiert als: + +.. math:: + \text{Cov}(X,Y) = E[(X - E[X])(Y - E[Y])] + +Eine äquivalente und oft bequemere Form ist: + +.. math:: + \text{Cov}(X,Y) = E[XY] - E[X]E[Y] + +Kovarianz misst, wie zwei Variablen gemeinsam variieren: + +* Positive Kovarianz: sie neigen dazu, gemeinsam zu steigen und zu fallen +* Negative Kovarianz: eine neigt dazu zu steigen, wenn die andere fällt +* Null-Kovarianz: sie sind unkorreliert + +Wenn beide Variablen den Mittelwert null haben, vereinfacht sich das zu: + +.. math:: + \text{Cov}(X,Y) = E[XY] + +Kovarianz hat Einheiten (sie ist nicht normiert), weshalb wir in der Praxis oft den **Korrelationskoeffizienten** (oder einfach Korrelation) verwenden: + +.. math:: + \rho_{XY} = \frac{\text{Cov}(X,Y)}{\sigma_X \sigma_Y} + +Dies ergibt einen dimensionslosen Wert zwischen −1 und +1. + +Varianz einer Summe von Variablen +################################## + +In der Signalverarbeitung haben wir es oft mit Summen von Zufallsvariablen zu tun, wie z.B. Signal plus Rauschen: + +.. math:: + Z = X + Y + +Die Varianz dieser Summe hängt davon ab, ob :math:`X` und :math:`Y` unabhängig sind (oder allgemeiner, korreliert sind). + +Im allgemeinen Fall: + +.. math:: + \text{Var}(X + Y) = \text{Var}(X) + \text{Var}(Y) + 2\,\text{Cov}(X,Y) + +wobei :math:`\text{Cov}(X,Y)` die **Kovarianz** zwischen :math:`X` und :math:`Y` ist. + +**Unabhängiger Fall** + +Wenn :math:`X` und :math:`Y` unabhängig (oder einfach unkorreliert) sind, vereinfacht sich der Ausdruck zu: + +.. math:: + \text{Var}(X + Y) = \text{Var}(X) + \text{Var}(Y) + +Dieses Ergebnis ist in der Kommunikationstechnik äußerst wichtig. Wenn z.B. ein empfangenes Signal lautet: + +.. math:: + R = S + N + +wobei :math:`S` das Signal und :math:`N` unabhängiges Rauschen ist, dann ist die Gesamtleistung einfach die Summe von Signalleistung und Rauschleistung. + +Deshalb sind SNR-Berechnungen so unkompliziert. + +************************ +Komplexe Zufallsvariablen +************************ + +In SDR arbeiten wir intensiv mit **komplexwertigen Signalen**, was bedeutet, dass wir auch mit komplexen Zufallsvariablen arbeiten. Eine komplexe Zufallsvariable hat die Form: + +.. math:: + Z = X + jY + +wobei :math:`X` und :math:`Y` beide reellwertige Zufallsvariablen sind, die die Inphase- (I) und Quadraturkomponenten (Q) darstellen. + +**Komplexes Gaußsches Rauschen** + +Die häufigste komplexe Zufallsvariable in der drahtlosen Kommunikation ist das **komplexe Gaußsche Rauschen**, bei dem sowohl :math:`X` als auch :math:`Y` unabhängige Gaußsche Zufallsvariablen mit derselben Varianz sind. + +Wenn z.B. :math:`X \sim \mathcal{N}(\alpha_1, \sigma_1^2)` und :math:`Y \sim \mathcal{N}(\alpha_2, \sigma_2^2)` unabhängig sind, dann hat die komplexe Zufallsvariable :math:`Z = X + jY`: + +* Mittelwert: :math:`E[Z] = E[X] + jE[Y] = \alpha_1 + j\alpha_2` +* Varianz (Leistung): :math:`\text{Var}(Z) = \text{Var}(X) + \text{Var}(Y) = \sigma_1^2 + \sigma_2^2` + +.. image:: ../_images_de/gaussian_IQ.png + :scale: 80% + :align: center + :alt: Komplexes Gaußsches Rauschen, visualisiert als zwei unabhängige Gaußsche Zufallsvariablen auf den I- und Q-Achsen + :target: ../_images_de/gaussian_IQ.png + +Deshalb verwenden wir folgendes, wenn wir komplexes Gaußsches Rauschen mit Einheitsleistung (Varianz = 1) erzeugen wollen: + +.. code-block:: python + + N = 10000 + n = (np.random.randn(N) + 1j*np.random.randn(N)) / np.sqrt(2) + print(np.var(n)) # ~ 1 + +Die Division durch :math:`\sqrt{2}` stellt sicher, dass die Gesamtleistung (Summe der I- und Q-Varianzen) gleich 1 ist. + +.. code-block:: python + + # Ohne Normalisierung: + n_raw = np.random.randn(N) + 1j*np.random.randn(N) + print(np.var(np.real(n_raw))) # ~ 1 + print(np.var(np.imag(n_raw))) # ~ 1 + print(np.var(n_raw)) # ~ 2 (Gesamtleistung) + + # Mit Normalisierung: + n_norm = n_raw / np.sqrt(2) + print(np.var(n_norm)) # ~ 1 (Einheitsleistung) + +**************** +Zufallsprozesse +**************** + +Bisher haben wir Zufallsvariablen besprochen — zufällige Werte an einem einzelnen Punkt. Ein **Zufallsprozess** (auch als **stochastischer Prozess** bezeichnet) ist eine Sammlung von Zufallsvariablen, die durch die Zeit indiziert sind: + +.. math:: + X(t) \quad \text{oder} \quad X[n] \text{ für diskrete Zeit} + +Zu jedem Zeitpunkt :math:`t` ist :math:`X(t)` eine Zufallsvariable. Stelle dir einen Zufallsprozess als ein Signal vor, das sich zufällig im Laufe der Zeit entwickelt. + +Beispiele in der drahtlosen Kommunikation: + +* Rauschen am Empfänger: :math:`N(t)` oder :math:`N[n]` +* Ein Signal, das zeitlich variablem Fading ausgesetzt ist: :math:`H(t)S(t)` +* Stichproben von einem SDR: jeder Batch ist eine Realisierung eines Zufallsprozesses + +**Stationäre Prozesse** + +Ein Zufallsprozess ist **stationär**, wenn sich seine statistischen Eigenschaften nicht über die Zeit ändern. Insbesondere hat ein **Weitgehend stationärer (WSS, Wide-Sense Stationary)** Prozess: + +* Konstanten Mittelwert: :math:`E[X(t)] = \mu` für alle :math:`t` +* Autokorrelation, die nur von der Zeitdifferenz abhängt: :math:`E[X(t)X(t+\tau)]` hängt nur von :math:`\tau` ab, nicht von :math:`t` + +Viele Rauschquellen in drahtlosen Systemen sind annähernd stationär, was die Analyse erheblich vereinfacht. + +**Weißes Rauschen** + +**Weißes Rauschen** ist ein Zufallsprozess, bei dem Samples zu verschiedenen Zeitpunkten unkorreliert sind und die Leistungsspektraldichte über alle Frequenzen konstant ist. Additives Weißes Gaußsches Rauschen (AWGN) ist sowohl: + +* **Weiß**: zeitlich unkorreliert, flaches Leistungsspektrum +* **Gaußsch**: jedes Sample ist Gaußsch verteilt + +Wenn wir Rauschen in Python mit ``np.random.randn(N)`` erzeugen, ist jedes der :math:`N` Samples eine unabhängige Gaußsche Zufallsvariable, was einen Weißrauschprozess erzeugt. + + +Unabhängigkeit und Korrelation +################################ + +Zwei Zufallsvariablen :math:`X` und :math:`Y` sind **unabhängig**, wenn das Wissen über den Wert einer nichts über die andere aussagt. Mathematisch faktorisiert ihre gemeinsame PDF: + +.. math:: + f_{X,Y}(x,y) = f_X(x) \cdot f_Y(y) + +Unabhängigkeit ist eine starke Bedingung. Eine schwächere Bedingung ist **unkorreliert**, was bedeutet: + +.. math:: + E[XY] = E[X]E[Y] + +Für Gaußsche Zufallsvariablen impliziert Unkorreliertheit Unabhängigkeit (das ist eine spezielle Eigenschaft der Gaußverteilung). + +Bei komplexem Gaußschen Rauschen sind I- und Q-Komponenten unabhängig: + +.. code-block:: python + + N = 10000 + I = np.random.randn(N) + Q = np.random.randn(N) + + # Unabhängigkeit über Korrelation überprüfen + correlation = np.corrcoef(I, Q)[0, 1] + print(f"Korrelation zwischen I und Q: {correlation:.4f}") # ~ 0 + +*************************** +Weiterführende Literatur +*************************** + +1. Papoulis, A., & Pillai, S. U. (2002). *Probability, Random Variables, and Stochastic Processes*. McGraw-Hill. +2. Kay, S. M. (2006). *Intuitive Probability and Random Processes using MATLAB®*. Springer. +3. https://en.wikipedia.org/wiki/Random_variable +4. https://en.wikipedia.org/wiki/Normal_distribution +5. https://en.wikipedia.org/wiki/Stochastic_process +6. https://en.wikipedia.org/wiki/Additive_white_Gaussian_noise +7. https://en.wikipedia.org/wiki/Signal-to-noise_ratio diff --git a/content-de/phaser.rst b/content-de/phaser.rst new file mode 100644 index 00000000..058c5aea --- /dev/null +++ b/content-de/phaser.rst @@ -0,0 +1,532 @@ +.. _phaser-chapter: + +#################### +Praktisch mit dem Phaser +#################### + +In diesem Kapitel verwenden wir das `Analog Devices Phaser `_ (auch CN0566 oder ADALM-PHASER), ein kostengünstiges 8-Kanal-Phased-Array-SDR, das einen PlutoSDR, Raspberry Pi und ADAR1000-Beamformer kombiniert und für einen Betrieb um 10,25 GHz konzipiert ist. Wir behandeln die Einrichtungs- und Kalibrierungsschritte und gehen dann durch einige Beamforming-Beispiele in Python. Für diejenigen, die keinen Phaser haben, sind Screenshots und Animationen enthalten. + +.. image:: ../_images/phaser_on_tripod.png + :scale: 60 % + :align: center + :alt: Der Phaser (CN0566) von Analog Devices + +************************ +Hardware-Übersicht +************************ + +.. image:: ../_images/phaser_front_and_back.png + :scale: 40 % + :align: center + :alt: Vorder- und Rückseite des Phaser-Geräts + +Der Phaser ist eine einzelne Platine, die das Phased Array und viele andere Komponenten enthält, mit einem Raspberry Pi auf einer Seite und einem Pluto auf der anderen Seite. Das übergeordnete Blockdiagramm ist unten dargestellt. Einige wichtige Punkte: + +1. Obwohl es wie ein 32-Element-2D-Array aussieht, ist es eigentlich ein 8-Element-1D-Array +2. Beide Empfangskanäle des Pluto werden verwendet (der zweite Kanal nutzt einen u.FL-Stecker auf der Platine selbst) +3. Der LO auf der Platine wird verwendet, um das empfangene Signal von etwa 10,25 GHz auf etwa 2 GHz abwärts zu mischen, damit der Pluto es empfangen kann +4. Jeder ADAR1000 hat vier Phasenschieber mit einstellbarer Verstärkung, und alle vier Kanäle werden summiert, bevor sie an den Pluto gesendet werden +5. Der Phaser enthält im Wesentlichen zwei „Subarrays", wobei jedes Subarray vier Kanäle enthält +6. Nicht gezeigt sind GPIO- und Seriellsignale vom Raspberry Pi zur Steuerung verschiedener Komponenten auf dem Phaser + +.. image:: ../_images/phaser_components.png + :scale: 40 % + :align: center + :alt: Die Komponenten des Phaser (CN0566) einschließlich ADF4159, LTC5548, ADAR1000 + +Ignorieren wir für jetzt die Sendeseite des Phasers; in diesem Kapitel verwenden wir nur das HB100-Gerät als Testsender. Der ADF4159 ist ein Frequenzsynthesizer, der einen Ton bis zu 13 GHz erzeugt – was wir den Lokaloszillator (LO) nennen. Dieser LO wird in einen Mischer, den LTC5548, eingespeist, der für Auf- oder Abwärtskonversion verwendet werden kann. Für die Abwärtskonversion nimmt er den LO sowie ein Signal von 2 bis 14 GHz und multipliziert die beiden, was eine Frequenzverschiebung durchführt. Das resultierende abwärtsgemischte Signal kann sich irgendwo von DC bis 6 GHz befinden, obwohl wir etwa 2 GHz anstreben. Der ADAR1000 ist ein 4-Kanal-Analog-Beamformer; der Phaser nutzt zwei davon. Auf dem Phaser gibt jeder ADAR1000 ein Signal aus, das abwärtsgemischt und dann vom Pluto empfangen wird. Mit dem Raspberry Pi können wir die Phase und Verstärkung aller acht Kanäle in Echtzeit steuern. + +Für Interessierte ist unten ein etwas detaillierteres Blockdiagramm angegeben. + +.. image:: ../_images/phaser_detailed_block_diagram.png + :scale: 80 % + :align: center + :alt: Detailliertes Blockdiagramm des Phaser (CN0566) + + +************************ +SD-Karten-Vorbereitung +************************ + +Wir gehen davon aus, dass du den Raspberry Pi auf dem Phaser direkt (mit Monitor/Tastatur/Maus) verwendest. Dies vereinfacht die Einrichtung, da Analog Devices ein vorgefertigtes SD-Karten-Image mit allen notwendigen Treibern und Software veröffentlicht. Du kannst das SD-Karten-Image herunterladen und Anweisungen zum Abbilden der SD-Karte `hier `_ finden. Das Image basiert auf Raspberry Pi OS und enthält alle erforderliche Software bereits installiert. + +************************ +Hardware-Vorbereitung +************************ + +1. Verbinde Plutos mittleren Micro-USB-Port mit dem Raspberry Pi +2. Optional: Schraube das Stativ vorsichtig in die Stativhalterung +3. Wir gehen davon aus, dass du ein HDMI-Display, eine USB-Tastatur und eine USB-Maus am Raspberry Pi verwendest +4. Versorge Pi und Phaser-Platine über den Type-C-Port des Phaser (CN0566), d. h. schließe KEIN Netzteil am USB-C des Raspberry Pi an + +************************ +Software-Installation +************************ + +Nachdem du mit dem vorgefertigten Image im Raspberry Pi gebootet hast (Standard-Benutzer/Passwort: analog/analog), wird empfohlen, die folgenden Schritte auszuführen: + +.. code-block:: bash + + wget https://github.com/mthoren-adi/rpi_setup_stuff/raw/main/phaser/phaser_sdcard_setup.sh + sudo chmod +x phaser_sdcard_setup.sh + ./phaser_sdcard_setup.sh + sudo reboot + + sudo raspi-config + +Weitere Hilfe bei der Einrichtung des Phasers findest du auf der `Phaser-Wiki-Quickstart-Seite `_. + +************************ +HB100-Einrichtung +************************ + +.. image:: ../_images/phaser_hb100.png + :scale: 50 % + :align: center + :alt: HB100, das mit dem Phaser geliefert wird + +Das HB100, das mit dem Phaser geliefert wird, ist ein kostengünstiges Doppler-Radarmodul, das wir als Testsender verwenden; es sendet einen kontinuierlichen Ton um 10 GHz. Es wird mit 2 AA-Batterien oder einer 3-V-Tischversorgung betrieben und hat eine rote LED, wenn es eingeschaltet ist. + +Da das HB100 kostengünstig ist und günstige HF-Komponenten verwendet, variiert seine Sendefrequenz von Einheit zu Einheit um Hunderte von MHz – ein Bereich, der größer ist als die höchste Bandbreite, die wir mit dem Pluto empfangen können (56 MHz). Um sicherzustellen, dass wir unseren Pluto so abstimmen, dass das HB100-Signal immer empfangen wird, müssen wir die Sendefrequenz des HB100 bestimmen. Dies geschieht mit einer Beispiel-App von Analog Devices, die einen Frequenzsweep durchführt und FFTs berechnet, während sie nach einem Spike sucht. Stelle sicher, dass dein HB100 eingeschaltet und in der Nähe des Phasers ist, und führe dann das Dienstprogramm aus: + +.. code-block:: bash + + cd ~/pyadi-iio/examples/phaser + python phaser_find_hb100.py + +Es sollte eine Datei namens :code:`hb100_freq_val.pkl` im selben Verzeichnis erstellen. Diese Datei enthält die HB100-Sendefrequenz in Hz (gepickelt, daher nicht im Klartext sichtbar), die wir im nächsten Schritt verwenden. + +************************ +Kalibrierung +************************ + +Schließlich müssen wir das Phased Array kalibrieren. Dazu muss das HB100 in Hauptstrahlrichtung (0 Grad) des Arrays gehalten werden. Die Seite des HB100 mit dem Barcode ist die Seite, die das Signal sendet; diese Seite sollte einige Fuß vom Phaser entfernt, direkt davor und mittig ausgerichtet, auf den Phaser gezeigt werden. Dann führe das Kalibrierungsdienstprogramm aus: + +.. code-block:: bash + + python phaser_examples.py cal + +Dies erstellt zwei weitere Pickle-Dateien: phase_cal_val.pkl und gain_cal_val.pkl im selben Verzeichnis. Jede enthält ein Array mit 8 Zahlen, die den Phasen- und Verstärkungs-Korrekturen entsprechen, die zur Kalibrierung jedes Kanals benötigt werden. Diese Werte sind für jeden Phaser einzigartig, da sie während der Herstellung variieren können. + +************************ +Vorgefertigte Beispiel-App +************************ + +Nachdem wir unseren Phaser kalibriert und die HB100-Frequenz gefunden haben, können wir die Beispiel-App ausführen, die Analog Devices bereitstellt: + +.. code-block:: bash + + python phaser_gui.py + +Wenn du das Kontrollkästchen „Auto Refresh Data" unten links aktivierst, sollte es zu laufen beginnen. Du solltest etwas Ähnliches wie das Folgende sehen, wenn du das HB100 in der Hauptstrahlrichtung des Phasers hältst. + +.. image:: ../_images/phaser_gui.png + :scale: 50 % + :align: center + :alt: Phaser-Beispiel-GUI-Tool von Analog Devices + +************************ +Phaser in Python +************************ + +Jetzt tauchen wir in den praktischen Python-Teil ein. Für diejenigen, die keinen Phaser haben, werden Screenshots und Animationen bereitgestellt. + +Phaser und Pluto initialisieren +############################## + +Der folgende Python-Code richtet unseren Phaser und Pluto ein. Zu diesem Zeitpunkt solltest du bereits die Kalibrierungsschritte ausgeführt haben, die drei Pickle-Dateien erzeugen. Stelle sicher, dass du das Python-Skript aus demselben Verzeichnis heraus ausführst, in dem sich diese Pickle-Dateien befinden. + +.. code-block:: python + + import time + import sys + import matplotlib.pyplot as plt + import numpy as np + import pickle + from adi import ad9361 + from adi.cn0566 import CN0566 + + phase_cal = pickle.load(open("phase_cal_val.pkl", "rb")) + gain_cal = pickle.load(open("gain_cal_val.pkl", "rb")) + signal_freq = pickle.load(open("hb100_freq_val.pkl", "rb")) + d = 0.014 # Abstand zwischen benachbarten Antennenelementen + + phaser = CN0566(uri="ip:localhost") + sdr = ad9361(uri="ip:192.168.2.1") + phaser.sdr = sdr + print("PlutoSDR und CN0566 verbunden!") + + time.sleep(0.5) # von Analog Devices empfohlen + + phaser.configure(device_mode="rx") + + # Alle Antennenelemente auf halbe Skalierung setzen + gain = 64 # 64 ist etwa halbe Skalierung + for i in range(8): + phaser.set_chan_gain(i, gain, apply_cal=False) + + # Strahl in Hauptstrahlrichtung (null Grad) ausrichten + phaser.set_beam_phase_diff(0.0) + + # Verschiedene SDR-Einstellungen + sdr._ctrl.debug_attrs["adi,frequency-division-duplex-mode-enable"].value = "1" + sdr._ctrl.debug_attrs["adi,ensm-enable-txnrx-control-enable"].value = "0" # Pin-Steuerung deaktivieren + sdr._ctrl.debug_attrs["initialize"].value = "1" + sdr.rx_enabled_channels = [0, 1] # Rx1 und Rx2 aktivieren + sdr._rxadc.set_kernel_buffers_count(1) # Keine veralteten Puffer + sdr.tx_hardwaregain_chan0 = int(-80) # Sicherstellen, dass die Tx-Kanäle gedämpft sind + sdr.tx_hardwaregain_chan1 = int(-80) + + # Grundlegende PlutoSDR-Einstellungen + sample_rate = 30e6 + sdr.sample_rate = int(sample_rate) + sdr.rx_buffer_size = int(1024) # Samples pro Puffer + sdr.rx_rf_bandwidth = int(10e6) # Analogfilter-Bandbreite + + # Manuelle Verstärkung (keine automatische Verstärkungsregelung) + sdr.gain_control_mode_chan0 = "manual" + sdr.gain_control_mode_chan1 = "manual" + sdr.rx_hardwaregain_chan0 = 10 # dB, 0 ist die niedrigste Verstärkung + sdr.rx_hardwaregain_chan1 = 10 # dB + + sdr.rx_lo = int(2.2e9) # Der Pluto stimmt auf diese Frequenz ab + + # PLL des Phasers (ADF4159 auf der Platine) einstellen + offset = 1000000 # kleiner Versatz, damit wir nicht bei 0 Hz mit DC-Spike sind + phaser.lo = int(signal_freq + sdr.rx_lo - offset) + + +Samples vom Pluto empfangen +################################ + +Zu diesem Zeitpunkt sind Phaser und Pluto konfiguriert. Wir können nun Daten vom Pluto empfangen. Holen wir uns einen einzelnen Batch von 1024 Samples und nehmen dann die FFT jedes der beiden Kanäle. + +.. code-block:: python + + # Samples empfangen (wie viele wir für rx_buffer_size gesetzt haben) + data = sdr.rx() + + # FFT berechnen + PSD0 = 10*np.log10(np.abs(np.fft.fftshift(np.fft.fft(data[0])))**2) + PSD1 = 10*np.log10(np.abs(np.fft.fftshift(np.fft.fft(data[1])))**2) + f = np.linspace(-sample_rate/2, sample_rate/2, len(data[0])) + + # Zeitbereichsplot zum Überprüfen des HB100-Empfangs und der Sättigung + plt.subplot(2, 1, 1) + plt.plot(data[0].real) # nur Realteil plotten + plt.plot(data[1].real) + plt.xlabel("Datenpunkt") + plt.ylabel("ADC-Ausgang") + + # PSDs zeigen, wo das HB100-Signal ist + plt.subplot(2, 1, 2) + plt.plot(f/1e6, PSD0) + plt.plot(f/1e6, PSD1) + plt.xlabel("Frequenz [MHz]") + plt.ylabel("Signalstärke [dB]") + plt.tight_layout() + plt.show() + +Was du zu diesem Zeitpunkt siehst, hängt davon ab, ob dein HB100 eingeschaltet ist und wohin es zeigt. Wenn du es einige Fuß vom Phaser entfernt und direkt darauf gerichtet hältst, solltest du etwas wie dieses sehen: + +.. image:: ../_images/phaser_rx_psd.png + :scale: 100 % + :align: center + :alt: Phaser-Anfangsbeispiel + +Beachte den starken Spike nahe 0 Hz; der zweite kürzere Spike ist lediglich ein Artefakt, das ignoriert werden kann, da es etwa 40 dB niedriger ist. + +Beamforming durchführen +############################## + +Nun schwenken wir die Phase! Im folgenden Code schwenken wir die Phase von -180 bis +180 Grad in 2-Grad-Schritten. Beachte, dass dies nicht der Winkel ist, auf den der Beamformer zeigt; es ist die Phasendifferenz zwischen benachbarten Kanälen. Wir müssen den Ankunftswinkel (AoA) berechnen, der jedem Phasenschritt entspricht: + +.. math:: + + \phi = \frac{2 \pi d}{\lambda} \sin(\theta_{AOA}) + +wobei :math:`\theta_{AOA}` der Ankunftswinkel bezüglich der Hauptstrahlrichtung, :math:`d` der Antennenabstand in Metern und :math:`\lambda` die Wellenlänge des Signals ist. Umstellen nach :math:`\theta_{AOA}`: + +.. math:: + + \theta_{AOA} = \sin^{-1}\left(\frac{c \phi}{2 \pi f d}\right) + +.. code-block:: python + + powers = [] # Haupt-DOA-Ergebnis + angle_of_arrivals = [] + for phase in np.arange(-180, 180, 2): # Winkel schwenken + print(phase) + # Phasendifferenz zwischen benachbarten Kanälen setzen + for i in range(8): + channel_phase = (phase * i + phase_cal[i]) % 360.0 + phaser.elements.get(i + 1).rx_phase = channel_phase + phaser.latch_rx_settings() # Einstellungen anwenden + + steer_angle = np.degrees(np.arcsin(max(min(1, (3e8 * np.radians(phase)) / (2 * np.pi * signal_freq * phaser.element_spacing)), -1))) + angle_of_arrivals.append(steer_angle) + data = phaser.sdr.rx() # Batch von Samples empfangen + data_sum = data[0] + data[1] # zwei Subarrays summieren + power_dB = 10*np.log10(np.sum(np.abs(data_sum)**2)) + powers.append(power_dB) + + powers -= np.max(powers) # normalisieren, sodass Maximum bei 0 dB ist + + plt.plot(angle_of_arrivals, powers, '.-') + plt.xlabel("Ankunftswinkel") + plt.ylabel("Magnitude [dB]") + plt.show() + +Für jeden :code:`phase`-Wert setzen wir die Phasenschieber, fügen die Phasenkalibrierungswerte hinzu und berechnen dann die Signalleistung. Das Ergebnis sollte ungefähr so aussehen: + +.. image:: ../_images/phaser_sweep.png + :scale: 100 % + :align: center + :alt: Phaser-Einzelsweep + +In diesem Beispiel wurde das HB100 leicht seitlich der Hauptstrahlrichtung gehalten. + +Für einen Polarplot: + +.. code-block:: python + + # Polarplot + fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}) + ax.plot(np.deg2rad(angle_of_arrivals), powers) # x-Achse in Radiant + ax.set_rticks([-40, -30, -20, -10, 0]) + ax.set_thetamin(np.min(angle_of_arrivals)) + ax.set_thetamax(np.max(angle_of_arrivals)) + ax.set_theta_direction(-1) # im Uhrzeigersinn zunehmen + ax.set_theta_zero_location('N') # 0 Grad nach oben + ax.grid(True) + plt.show() + +.. image:: ../_images/phaser_sweep_polar.png + :scale: 100 % + :align: center + :alt: Phaser-Einzelsweep als Polarplot + +Durch das Maximum können wir die Ankunftsrichtung des Signals schätzen! + +Echtzeit und räumliche Fensterung +##################################### + +Bisher haben wir die Verstärkungsanpassungen jedes Kanals auf gleiche Werte gelassen, sodass alle acht Kanäle gleichmäßig summiert werden. Genau wie wir ein Fenster vor der FFT anwenden, können wir ein Fenster im räumlichen Bereich anwenden, indem wir Gewichte auf diese acht Kanäle anwenden. Wir verwenden dieselben Fensterfunktionen wie Hanning, Hamming usw. Passen wir auch den Code an, damit er in Echtzeit läuft: + +.. code-block:: python + + plt.ion() # für Echtzeitansicht benötigt + print("Gestartet, mit Strg+C stoppen") + try: + while True: + powers = [] # Haupt-DOA-Ergebnis + angle_of_arrivals = [] + for phase in np.arange(-180, 180, 6): # Winkel schwenken + # Phasendifferenz zwischen benachbarten Kanälen setzen + for i in range(8): + channel_phase = (phase * i + phase_cal[i]) % 360.0 + phaser.elements.get(i + 1).rx_phase = channel_phase + + # Verstärkungen setzen, incl. gain_cal (kann für Fensterung verwendet werden) + gain_list = [127] * 8 # rechteckiges Fenster [127, 127, 127, 127, 127, 127, 127, 127] + #gain_list = np.rint(np.hamming(8) * 127) # [ 10, 32, 82, 121, 121, 82, 32, 10] + #gain_list = np.rint(np.hanning(10)[1:-1] * 127) # [ 15, 52, 95, 123, 123, 95, 52, 15] + #gain_list = np.rint(np.blackman(10)[1:-1] * 127) # [ 6, 33, 80, 121, 121, 80, 33, 6] + #gain_list = np.rint(np.bartlett(10)[1:-1] * 127) # [ 28, 56, 85, 113, 113, 85, 56, 28] + for i in range(8): + channel_gain = int(gain_list[i] * gain_cal[i]) + phaser.elements.get(i + 1).rx_gain = channel_gain + + phaser.latch_rx_settings() # Einstellungen anwenden + + steer_angle = np.degrees(np.arcsin(max(min(1, (3e8 * np.radians(phase)) / (2 * np.pi * signal_freq * phaser.element_spacing)), -1))) + angle_of_arrivals.append(steer_angle) + data = phaser.sdr.rx() # Samples empfangen + data_sum = data[0] + data[1] # Subarrays summieren + power_dB = 10*np.log10(np.sum(np.abs(data_sum)**2)) + powers.append(power_dB) + + powers -= np.max(powers) # normalisieren + + # Echtzeitansicht + plt.plot(angle_of_arrivals, powers, '.-') + plt.xlabel("Ankunftswinkel") + plt.ylabel("Magnitude [dB]") + plt.draw() + plt.pause(0.001) + plt.clf() + + except KeyboardInterrupt: + sys.exit() # Python beenden + +Du solltest eine Echtzeitversion der vorherigen Übung sehen. Versuche, die :code:`gain_list` zu wechseln, um verschiedene Fenster auszuprobieren. Hier ist ein Beispiel mit dem rechteckigen Fenster (d. h. keine Fensterfunktion): + +.. image:: ../_images/phaser_animation_rect.gif + :scale: 100 % + :align: center + :alt: Beamforming-Animation mit dem Phaser und einem rechteckigen Fenster + +und hier ein Beispiel mit dem Hamming-Fenster: + +.. image:: ../_images/phaser_animation_hamming.gif + :scale: 100 % + :align: center + :alt: Beamforming-Animation mit dem Phaser und einem Hamming-Fenster + +Beachte das Fehlen von Nebenkeulen beim Hamming-Fenster. Jedes Fenster außer dem rechteckigen reduziert die Nebenkeulen erheblich, aber dafür wird die Hauptkeule etwas breiter. + +************************ +Monopuls-Tracking +************************ + +Bisher haben wir einzelne Sweeps durchgeführt, um den Ankunftswinkel eines Testsenders (des HB100) zu finden. Angenommen, wir möchten kontinuierlich ein Kommunikations- oder Radarsignal empfangen, das sich bewegt und dessen Ankunftswinkel sich im Laufe der Zeit ändert. Diesen Prozess nennen wir Tracking, und er setzt voraus, dass wir bereits eine grobe Schätzung des Ankunftswinkels haben. Wir verwenden Monopuls-Tracking, um die Gewichte adaptiv zu aktualisieren, damit die Hauptkeule mit der Zeit auf das Signal gerichtet bleibt. + +Das 1943 von Robert Page am Naval Research Laboratory (NRL) erfundene Grundkonzept des Monopuls-Trackings besteht darin, zwei Strahlen zu verwenden, die beide leicht vom aktuellen Ankunftswinkel (oder zumindest unserer Schätzung davon) versetzt sind, aber auf verschiedenen Seiten, wie im Diagramm unten gezeigt. + +.. image:: ../_images/monopulse.svg + :align: center + :target: ../_images/monopulse.svg + :alt: Monopuls-Strahldiagramm mit zwei Strahlen und dem Summenstrahl + +Wir nehmen dann sowohl die Summe als auch die Differenz (auch Delta genannt) dieser beiden Strahlen digital, was bedeutet, dass wir zwei digitale Kanäle des Phasers verwenden müssen. Der Summenstrahl entspricht einem Strahl, der auf den aktuellen Ankunftswinkel zentriert ist und für Demodulation/Dekodierung verwendet werden kann. Der Delta-Strahl hat eine Nullstelle beim Ankunftswinkel. Wir können das Verhältnis zwischen Summen- und Delta-Strahl (als Fehler bezeichnet) für unser Tracking verwenden. Im Code unten ist :code:`data[0]` der erste Kanal des Pluto (erste vier Phaser-Elemente) und :code:`data[1]` der zweite Kanal (zweite vier Elemente): + +.. code-block:: python + + data = phaser.sdr.rx() + sum_beam = data[0] + data[1] + delta_beam = data[0] - data[1] + error = np.mean(np.real(delta_beam / sum_beam)) + +Das Vorzeichen des Fehlers sagt uns, aus welcher Richtung das Signal tatsächlich kommt, und die Magnitude sagt uns, wie weit wir vom Signal entfernt sind. Durch Wiederholen dieses Prozesses in Echtzeit können wir das Signal verfolgen. + +Zuerst kopieren wir den Code, den wir früher verwendet haben, um einen 180-Grad-Sweep durchzuführen, und extrahieren die Phase, bei der die empfangene Leistung maximal war: + +.. code-block:: python + + # Phase einmal sweepen für erste AoA-Schätzung (Code von oben) + # ... + current_phase = phase_angles[np.argmax(powers)] + print("max_phase:", current_phase) + +Als nächstes erstellen wir zwei Strahlen, wir beginnen damit, 5 Grad niedriger und 5 Grad höher als die aktuelle Schätzung zu versuchen (in Phaseneinheiten). Die folgenden Code-Zeilen steuern die ersten 4 Elemente für den unteren Strahl und die letzten 4 Elemente für den oberen Strahl: + +.. code-block:: python + + # Zwei Strahlen auf beiden Seiten unserer aktuellen Schätzung erstellen + phase_offset = np.radians(5) # KANN ANGEPASST WERDEN + phase_lower = current_phase - phase_offset + phase_upper = current_phase + phase_offset + # erste 4 Elemente für unteren Strahl + for i in range(0, 4): + channel_phase = (phase_lower * i + phase_cal[i]) % 360.0 + phaser.elements.get(i + 1).rx_phase = channel_phase + # letzte 4 Elemente für oberen Strahl + for i in range(4, 8): + channel_phase = (phase_upper * i + phase_cal[i]) % 360.0 + phaser.elements.get(i + 1).rx_phase = channel_phase + phaser.latch_rx_settings() # Einstellungen anwenden + +Bevor wir das eigentliche Tracking durchführen, testen wir das oben Genannte, indem wir die Strahlgewichte konstant lassen und das HB100 links und rechts bewegen: + +.. code-block:: python + + print("HB100 ETWAS NACH LINKS UND RECHTS BEWEGEN") + error_log = [] + for i in range(1000): + data = phaser.sdr.rx() # Batch von Samples empfangen + sum_beam = data[0] + data[1] + delta_beam = data[0] - data[1] + error = np.mean(np.real(delta_beam / sum_beam)) + error_log.append(error) + print(error) + time.sleep(0.01) + + plt.plot(error_log) + plt.plot([0,len(error_log)], [0,0], 'r--') + plt.xlabel("Zeit") + plt.ylabel("Fehler") + plt.show() + +.. image:: ../_images/monopulse_waving.svg + :align: center + :target: ../_images/monopulse_waving.svg + :alt: Fehlerfunktion für Monopuls-Tracking ohne Gewichtsaktualisierung + +Das Take-away ist: Je weiter sich das HB100 vom Startwinkel entfernt, desto höher der Fehler, und das Vorzeichen des Fehlers sagt uns, auf welcher Seite das HB100 relativ zum Startwinkel ist. + +Nun nutzen wir den Fehlerwert, um die Gewichte zu aktualisieren. Wir ersetzen die vorherige Schleife durch eine neue, die den gesamten Prozess umfasst: + +.. code-block:: python + + # Phase einmal sweepen für erste AoA-Schätzung + # ... + current_phase = phase_angles[np.argmax(powers)] + print("max_phase:", current_phase) + + # current_phase basierend auf Fehler aktualisieren + print("HB100 ETWAS NACH LINKS UND RECHTS BEWEGEN") + phase_log = [] + error_log = [] + for ii in range(500): + # Zwei Strahlen auf beiden Seiten unserer aktuellen Schätzung erstellen + phase_offset = np.radians(5) + phase_lower = current_phase - phase_offset + phase_upper = current_phase + phase_offset + # erste 4 Elemente für unteren Strahl + for i in range(0, 4): + channel_phase = (phase_lower * i + phase_cal[i]) % 360.0 + phaser.elements.get(i + 1).rx_phase = channel_phase + # letzte 4 Elemente für oberen Strahl + for i in range(4, 8): + channel_phase = (phase_upper * i + phase_cal[i]) % 360.0 + phaser.elements.get(i + 1).rx_phase = channel_phase + phaser.latch_rx_settings() # Einstellungen anwenden + + data = phaser.sdr.rx() # Batch von Samples empfangen + sum_beam = data[0] + data[1] + delta_beam = data[0] - data[1] + error = np.mean(np.real(delta_beam / sum_beam)) + error_log.append(error) + print(error) + + # Geschätzte Ankunftsrichtung basierend auf Fehler aktualisieren + current_phase += -10 * error # manuell angepasst für angemessene Tracking-Geschwindigkeit + steer_angle = np.degrees(np.arcsin(max(min(1, (3e8 * np.radians(current_phase)) / (2 * np.pi * signal_freq * phaser.element_spacing)), -1))) + phase_log.append(steer_angle) # Steuerwinkel statt Phase plotten sieht besser aus + + time.sleep(0.01) + + fig, [ax0, ax1] = plt.subplots(2, 1, figsize=(8, 10)) + + ax0.plot(phase_log) + ax0.plot([0,len(phase_log)], [0,0], 'r--') + ax0.set_xlabel("Zeit") + ax0.set_ylabel("Phasenschätzung [Grad]") + + ax1.plot(error_log) + ax1.plot([0,len(error_log)], [0,0], 'r--') + ax1.set_xlabel("Zeit") + ax1.set_ylabel("Fehler") + + plt.show() + +.. image:: ../_images/monopulse_tracking.svg + :align: center + :target: ../_images/monopulse_tracking.svg + :alt: Monopuls-Tracking-Demo mit Phaser und HB100 + +Man sieht, dass der Fehler im Wesentlichen die Ableitung der Phasenschätzung ist; da wir erfolgreich tracken, entspricht die Phasenschätzung mehr oder weniger dem tatsächlichen Ankunftswinkel. Das Ziel ist, dass die Änderung des Ankunftswinkels nie so schnell ist, dass das Signal über die Hauptkeulen der zwei Strahlen hinausgeht. + +Praktische Anwendungsfälle des Monopuls-Trackings sind fast immer 2D (mit einem 2D/planaren Array statt einem linearen Array wie dem Phaser). Im 2D-Fall werden vier Strahlen erstellt, und es gibt einen Summenstrahl und vier Delta-Strahlen für das Lenken in beiden Dimensionen. + +************************ +Radar mit dem Phaser +************************ + +Demnächst verfügbar! + +************************ +Schlussfolgerung +************************ + +Der gesamte Code zur Erzeugung der Abbildungen in diesem Kapitel ist auf der GitHub-Seite des Lehrbuchs verfügbar. diff --git a/content-de/pluto.rst b/content-de/pluto.rst new file mode 100644 index 00000000..668d51d6 --- /dev/null +++ b/content-de/pluto.rst @@ -0,0 +1,702 @@ +.. _pluto-chapter: + +#################################### +PlutoSDR in Python +#################################### + +.. image:: ../_images_de/pluto.png + :scale: 50 % + :align: center + :alt: Das PlutoSDR von Analog Devices + +In diesem Kapitel lernst du, wie du die Python-API für das `PlutoSDR `_ verwendest, ein kostengünstiges SDR von Analog Devices. Wir behandeln die Installationsschritte für das PlutoSDR, um Treiber und Software zum Laufen zu bringen, und besprechen dann das Senden und Empfangen mit dem PlutoSDR in Python. Zuletzt zeigen wir, wie du `Maia SDR `_ und `IQEngine `_ verwendest, um das PlutoSDR in einen leistungsstarken Spektrumanalysator zu verwandeln! + +************************ +Überblick über das PlutoSDR +************************ + +Das PlutoSDR (auch bekannt als ADALM-PLUTO) ist ein kostengünstiges SDR (etwas über 200 US-Dollar), das in der Lage ist, Signale von 70 MHz bis 6 GHz zu senden und zu empfangen. Es ist ein großartiges SDR für alle, die über das 20-Dollar-RTL-SDR hinausgewachsen sind. Das Pluto verwendet eine USB-2.0-Schnittstelle, was die Abtastrate auf etwa 5 MHz begrenzt, wenn du 100 % der Samples über die Zeit empfangen möchtest. Dennoch kann es bis zu 61 MHz abtasten, und du kannst zusammenhängende Bursts von bis zu etwa 10 Millionen Samples auf einmal aufnehmen, sodass das Pluto eine enorme Menge an Spektrum gleichzeitig erfassen kann. Es ist technisch gesehen ein 2x2-Gerät, aber der zweite Sende- und Empfangskanal ist nur über U.FL-Steckverbinder im Gehäuse zugänglich, und sie teilen sich dieselben Oszillatoren, sodass du nicht gleichzeitig auf zwei verschiedenen Frequenzen empfangen kannst. Unten ist das Blockdiagramm des Pluto sowie der AD936x gezeigt, der integrierte HF-Schaltkreis (RFIC) im Inneren des Pluto. + +.. image:: ../_images_de/adi-adalm-pluto-diagram-large.jpg + :scale: 60 % + :align: center + :alt: Blockdiagramm des PlutoSDR + +.. image:: ../_images_de/ad9361.svg + :align: center + :target: ../_images_de/ad9361.svg + :alt: Blockdiagramm des AD9361/AD9363 RFIC im PlutoSDR + +************************ +Software/Treiber Installation +************************ + +VM einrichten +############# + +Der in diesem Lehrbuch bereitgestellte Python-Code sollte unter Windows, Mac und Linux funktionieren, aber die folgenden Installationsanweisungen gelten speziell für Ubuntu 22. Wenn du Schwierigkeiten hast, die Software auf deinem Betriebssystem zu installieren, und den `von Analog Devices bereitgestellten Anweisungen `_ folgst, empfehle ich, eine Ubuntu-22-VM zu installieren und die folgenden Anweisungen auszuprobieren. Alternativ, wenn du Windows 11 verwendest, läuft das Windows-Subsystem für Linux (WSL) mit Ubuntu 22 recht gut und unterstützt Grafik von Haus aus. + +1. Installiere und öffne `VirtualBox `_. +2. Erstelle eine neue VM. Für die Speichergröße empfehle ich, 50 % des RAM deines Computers zu verwenden. +3. Erstelle die virtuelle Festplatte, wähle VDI und weise dynamisch Größe zu. 15 GB sollten ausreichen. Wenn du auf der sicheren Seite sein möchtest, kannst du mehr verwenden. +4. Lade Ubuntu 22 Desktop .iso herunter – https://ubuntu.com/download/desktop +5. Starte die VM. Du wirst nach Installationsmedien gefragt. Wähle die Ubuntu-22-Desktop-.iso-Datei. Wähle „Ubuntu installieren", verwende Standardoptionen, und ein Popup warnt dich vor den Änderungen, die du vornehmen wirst. Klicke auf „Fortfahren". Wähle Name/Passwort und warte dann, bis die VM die Initialisierung abgeschlossen hat. Nach dem Abschluss wird die VM neu gestartet, aber du solltest die VM nach dem Neustart ausschalten. +6. Gehe in die VM-Einstellungen (das Zahnrad-Symbol). +7. Wähle unter System > Prozessor mindestens 3 CPUs. Wenn du eine echte Grafikkarte hast, wähle unter Anzeige > Videospeicher einen viel höheren Wert. +8. Starte deine VM. +9. Ich empfehle, VM-Gastzusätze zu installieren. Gehe innerhalb der VM zu Geräte > Gasterweiterungen-CD einlegen > klicke auf „Ausführen", wenn ein Fenster erscheint. Folge den Anweisungen. Starte die VM neu. Die gemeinsame Zwischenablage kann über Geräte > Gemeinsame Zwischenablage > Bidirektional aktiviert werden. + +PlutoSDR verbinden +################## + +1. Wenn du OSX verwendest, aktiviere innerhalb von OSX (nicht in der VM) in den Systemeinstellungen „Kernel-Erweiterungen". Installiere dann HoRNDIS (möglicherweise musst du danach neu starten). +2. Wenn du Windows verwendest, installiere diesen Treiber: https://github.com/analogdevicesinc/plutosdr-m2k-drivers-win/releases/download/v0.7/PlutoSDR-M2k-USB-Drivers.exe +3. Wenn du Linux verwendest, musst du nichts Besonderes tun. +4. Schließe das Pluto über USB an den Host-Rechner an. Achte darauf, den mittleren USB-Anschluss am Pluto zu verwenden, da der andere nur zur Stromversorgung dient. Das Anschließen des Pluto sollte einen virtuellen Netzwerkadapter erstellen, d.h. das Pluto erscheint wie ein USB-Ethernet-Adapter. +5. Öffne auf dem Host-Rechner (nicht in der VM) ein Terminal oder dein bevorzugtes Ping-Tool und pinge 192.168.2.1. Wenn das nicht funktioniert, halte an und behebe das Netzwerkinterface-Problem. +6. Öffne innerhalb der VM ein neues Terminal. +7. Pinge 192.168.2.1. Wenn das nicht funktioniert, halte hier an und behebe das Problem. Während des Pingens trenne dein Pluto und stelle sicher, dass das Pingen stoppt. Wenn es weiter pingt, befindet sich etwas anderes mit dieser IP-Adresse im Netzwerk, und du musst die IP des Pluto (oder des anderen Geräts) ändern, bevor du fortfährst. +8. Notiere dir die IP-Adresse des Pluto, da du sie benötigst, wenn wir mit dem Pluto in Python arbeiten. + +PlutoSDR-Treiber installieren +############################## + +Die folgenden Terminal-Befehle sollten die neueste Version von folgendem erstellen und installieren: + +1. **libiio**, Analog Devices' „plattformübergreifende" Bibliothek zur Anbindung von Hardware +2. **libad9361-iio**, der AD9361 ist der spezifische HF-Chip im PlutoSDR +3. **pyadi-iio**, die Python-API des Pluto, *das ist unser eigentliches Ziel*, aber es hängt von den beiden vorherigen Bibliotheken ab + + +.. code-block:: bash + + sudo apt-get update + sudo apt-get install build-essential git libxml2-dev bison flex libcdk5-dev cmake python3-pip libusb-1.0-0-dev libavahi-client-dev libavahi-common-dev libaio-dev + cd ~ + git clone --branch v0.23 https://github.com/analogdevicesinc/libiio.git + cd libiio + mkdir build + cd build + cmake -DPYTHON_BINDINGS=ON .. + make -j$(nproc) + sudo make install + sudo ldconfig + + cd ~ + git clone https://github.com/analogdevicesinc/libad9361-iio.git + cd libad9361-iio + mkdir build + cd build + cmake .. + make -j$(nproc) + sudo make install + + cd ~ + git clone --branch v0.0.14 https://github.com/analogdevicesinc/pyadi-iio.git + cd pyadi-iio + pip3 install --upgrade pip + pip3 install -r requirements.txt + sudo python3 setup.py install + +PlutoSDR-Treiber testen +######################## + +Öffne ein neues Terminal (in deiner VM) und gib folgende Befehle ein: + +.. code-block:: bash + + python3 + import adi + sdr = adi.Pluto('ip:192.168.2.1') # oder die IP deines Pluto + sdr.sample_rate = int(2.5e6) + sdr.rx() + +Wenn du bis hierher ohne Fehler gekommen bist, kannst du mit den nächsten Schritten fortfahren. + +IP-Adresse des Pluto ändern +############################ + +Wenn die Standard-IP 192.168.2.1 aus irgendeinem Grund nicht funktioniert, weil du bereits ein 192.168.2.0-Subnetz hast oder weil du mehrere Plutos gleichzeitig verbinden möchtest, kannst du die IP mit diesen Schritten ändern: + +1. Bearbeite die Datei config.txt auf dem PlutoSDR-Massenspeichergerät (d.h. dem USB-Laufwerk, das nach dem Anschließen des Pluto erscheint). Gib die neue gewünschte IP ein. +2. Wirf das Massenspeichergerät aus (ziehe das Pluto nicht ab!). In Ubuntu 22 gibt es ein Auswurf-Symbol neben dem PlutoSDR-Gerät im Datei-Explorer. +3. Warte einige Sekunden und trenne dann das Pluto und schließe es wieder an. Überprüfe die config.txt, um festzustellen, ob deine Änderungen gespeichert wurden. + +Beachte, dass dieser Vorgang auch zum Aufspielen eines anderen Firmware-Images auf das Pluto verwendet wird. Weitere Details findest du unter https://wiki.analog.com/university/tools/pluto/users/firmware. + +PlutoSDR „hacken" zur Erweiterung des HF-Bereichs +#################################################### + +Das PlutoSDR wird mit einem eingeschränkten Mittelfrequenzbereich und einer begrenzten Abtastrate geliefert, aber der zugrunde liegende Chip ist für viel höhere Frequenzen ausgelegt. Folge diesen Schritten, um den vollen Frequenzbereich des Chips freizuschalten. Bitte beachte, dass dieser Vorgang von Analog Devices bereitgestellt wird und daher so risikoarm wie möglich ist. Die Frequenzbeschränkung des PlutoSDR hängt damit zusammen, dass Analog Devices die AD936x-Chips nach strengen Leistungsanforderungen bei höheren Frequenzen verlagern (engl.„binning" bzw. sortiert). Als SDR-Enthusiasten und Experimentatoren sind wir nicht allzu besorgt über diese Leistungsanforderungen. + +Zeit zum Hacken! Öffne ein Terminal (entweder Host oder VM, es spielt keine Rolle): + +.. code-block:: bash + + ssh root@192.168.2.1 + +Das Standardpasswort lautet :code:`analog` + +Du solltest den PlutoSDR-Willkommensbildschirm sehen. Du hast jetzt eine SSH-Verbindung zum ARM-Prozessor des Pluto hergestellt! +Wenn du ein Pluto mit Firmware-Version 0.31 oder niedriger hast, gib die folgenden Befehle ein: + +.. code-block:: bash + + fw_setenv attr_name compatible + fw_setenv attr_val ad9364 + reboot + +Und für 0.32 und höher verwende: + +.. code-block:: bash + + fw_setenv compatible ad9364 + reboot + +Du solltest jetzt in der Lage sein, bis auf 6 GHz zu tunen und bis auf 70 MHz herunterzugehen, ganz zu schweigen von einer Abtastrate von bis zu 56 MHz! + +************************ +Empfangen +************************ + +Das Abtasten mit der Python-API des PlutoSDR ist unkompliziert. Bei jeder SDR-Anwendung müssen wir die Mittelfrequenz, die Abtastrate und die Verstärkung (oder ob automatische Verstärkungsregelung verwendet werden soll) angeben. Es kann weitere Details geben, aber diese drei Parameter sind notwendig, damit das SDR genügend Informationen hat, um Samples zu empfangen. Einige SDRs haben einen Befehl, um das Abtasten zu starten, während andere wie das Pluto mit dem Abtasten beginnen, sobald es initialisiert wird. Sobald der interne Puffer des SDR voll ist, werden die ältesten Samples verworfen. Alle SDR-APIs haben eine Art „Samples empfangen"-Funktion, und beim Pluto ist das rx(), die einen Batch von Samples zurückgibt. Die spezifische Anzahl von Samples pro Batch wird durch die zuvor eingestellte Puffergröße definiert. + +Der folgende Code setzt voraus, dass du die Python-API des Pluto installiert hast. Dieser Code initialisiert das Pluto, stellt die Abtastrate auf 1 MHz ein, setzt die Mittelfrequenz auf 100 MHz und stellt die Verstärkung auf 70 dB ein, wobei die automatische Verstärkungsregelung ausgeschaltet ist. Beachte, dass die Reihenfolge, in der du Mittelfrequenz, Verstärkung und Abtastrate festlegst, normalerweise keine Rolle spielt. Im folgenden Code-Snippet teilen wir dem Pluto mit, dass wir 10.000 Samples pro Aufruf von rx() erhalten möchten. Wir geben die ersten 10 Samples aus. + +.. code-block:: python + + import numpy as np + import adi + + sample_rate = 1e6 # Hz + center_freq = 100e6 # Hz + num_samps = 10000 # Anzahl der Samples pro rx()-Aufruf + + sdr = adi.Pluto('ip:192.168.2.1') + sdr.gain_control_mode_chan0 = 'manual' + sdr.rx_hardwaregain_chan0 = 70.0 # dB + sdr.rx_lo = int(center_freq) + sdr.sample_rate = int(sample_rate) + sdr.rx_rf_bandwidth = int(sample_rate) # Filterbreite, vorerst gleich der Abtastrate setzen + sdr.rx_buffer_size = num_samps + + samples = sdr.rx() # Samples vom Pluto empfangen + print(samples[0:10]) + + +Vorerst werden wir mit diesen Samples nichts Interessantes anstellen, aber der Rest dieses Lehrbuchs ist voll von Python-Code, der auf IQ-Samples wie die oben empfangenen arbeitet. + + +Empfangsverstärkung +################### + +Das Pluto kann entweder mit einer festen Empfangsverstärkung oder einer automatischen konfiguriert werden. Eine automatische Verstärkungsregelung (AGC) passt die Empfangsverstärkung automatisch an, um einen starken Signalpegel aufrechtzuerhalten (-12 dBFS für alle Interessierten). AGC darf nicht mit dem Analog-Digital-Wandler (ADC) verwechselt werden, der das Signal digitalisiert. Technisch gesehen ist der AGC ein geschlossener Rückkopplungskreis, der die Verstärkung des Verstärkers in Abhängigkeit vom empfangenen Signal regelt. Sein Ziel ist es, trotz eines schwankenden Eingangsleistungspegels einen konstanten Ausgangsleistungspegel aufrechtzuerhalten. In der Regel passt der AGC die Verstärkung an, um eine Sättigung des Empfängers zu vermeiden (d.h. das Erreichen der Obergrenze des ADC-Bereichs), während das Signal gleichzeitig so viele ADC-Bits wie möglich „ausfüllt". + +Der integrierte HF-Schaltkreis (RFIC) im PlutoSDR hat ein AGC-Modul mit einigen verschiedenen Einstellungen. (Ein RFIC ist ein Chip, der als Transceiver fungiert: Er sendet und empfängt Radiowellen.) Zunächst ist zu beachten, dass die Empfangsverstärkung am Pluto einen Bereich von 0 bis 74,5 dB hat. Im „manuellen" AGC-Modus ist der AGC ausgeschaltet und du musst dem Pluto mitteilen, welche Empfangsverstärkung es verwenden soll, z.B.: + +.. code-block:: python + + + sdr.gain_control_mode_chan0 = "manual" # AGC ausschalten + gain = 50.0 # zulässiger Bereich ist 0 bis 74,5 dB + sdr.rx_hardwaregain_chan0 = gain # Empfangsverstärkung einstellen + +Wenn du den AGC aktivieren möchtest, musst du einen von zwei Modi wählen: + +1. :code:`sdr.gain_control_mode_chan0 = "slow_attack"` +2. :code:`sdr.gain_control_mode_chan0 = "fast_attack"` + +Wenn der AGC aktiviert ist, gibst du keinen Wert für :code:`rx_hardwaregain_chan0` an. Er wird ignoriert, da das Pluto selbst die Verstärkung für das Signal anpasst. Das Pluto hat zwei Modi für den AGC: schneller Angriff und langsamer Angriff, wie im obigen Code-Snippet gezeigt. Der Unterschied zwischen den beiden ist intuitiv. Der Fast-Attack-Modus reagiert schneller auf Signale. Mit anderen Worten: Der Verstärkungswert ändert sich schneller, wenn sich der empfangene Signalpegel ändert. Die Anpassung an Signalleistungspegel kann wichtig sein, insbesondere für Systeme mit Zeitduplex (TDD), die dieselbe Frequenz zum Senden und Empfangen verwenden. Die Einstellung der Verstärkungsregelung auf den Fast-Attack-Modus begrenzt in diesem Szenario die Signaldämpfung. In beiden Modi maximiert der AGC die Verstärkungseinstellung, wenn kein Signal vorhanden ist und nur Rauschen vorliegt; wenn ein Signal erscheint, sättigt es kurzzeitig den Empfänger, bis der AGC reagieren und die Verstärkung herunterregeln kann. Du kannst den aktuellen Verstärkungspegel immer in Echtzeit mit folgendem Befehl überprüfen: + +.. code-block:: python + + sdr._get_iio_attr('voltage0','hardwaregain', False) + +Weitere Details über den AGC des Pluto, wie z.B. das Ändern der erweiterten AGC-Einstellungen, findest du im `Abschnitt „RX Gain Control" dieser Seite `_. + +************************ +Senden +************************ + +Bevor du ein Signal mit deinem Pluto über die Luft überträgst, stelle sicher, dass du ein SMA-Kabel zwischen dem TX-Anschluss des Pluto und dem Gerät verbindest, das als Empfänger dient. Es ist wichtig, immer zunächst über ein Kabel zu übertragen, besonders während du lernst, *wie* man sendet, um sicherzustellen, dass sich das SDR so verhält, wie du es beabsichtigst. Halte deine Sendeleistung stets sehr niedrig, damit du den Empfänger nicht übersteuert, da das Kabel das Signal nicht wie der Funkkanal dämpft. Wenn du ein Dämpfungsglied besitzt (z.B. 30 dB), wäre jetzt ein guter Zeitpunkt, es zu verwenden. Wenn du kein anderes SDR oder einen Spektrumanalysator als Empfänger hast, kannst du theoretisch den RX-Anschluss desselben Pluto verwenden, aber das kann kompliziert werden. Ich empfehle, ein 10-Dollar-RTL-SDR als empfangendes SDR zu verwenden. + +Das Senden ist dem Empfangen sehr ähnlich, nur dass wir dem SDR anstatt zu sagen, es soll eine bestimmte Anzahl von Samples empfangen, eine bestimmte Anzahl von Samples zum Senden geben. Anstatt :code:`rx_lo` werden wir :code:`tx_lo` einstellen, um anzugeben, auf welcher Trägerfrequenz gesendet werden soll. Die Abtastrate wird zwischen RX und TX geteilt, sodass wir sie wie gewohnt einstellen werden. Ein vollständiges Beispiel für das Senden ist unten gezeigt, wo wir einen Sinuston bei +100 kHz erzeugen und dann das komplexe Signal bei einer Trägerfrequenz von 915 MHz übertragen, sodass der Empfänger einen Träger bei 915,1 MHz sieht. Es gibt eigentlich keinen praktischen Grund dafür; wir hätten die center_freq einfach auf 915,1e6 setzen und ein Array von 1en übertragen können, aber wir wollten komplexe Samples zu Demonstrationszwecken erzeugen. + +.. code-block:: python + + import numpy as np + import adi + + sample_rate = 1e6 # Hz + center_freq = 915e6 # Hz + + sdr = adi.Pluto("ip:192.168.2.1") + sdr.sample_rate = int(sample_rate) + sdr.tx_rf_bandwidth = int(sample_rate) # Filtergrenzfrequenz, gleich der Abtastrate setzen + sdr.tx_lo = int(center_freq) + sdr.tx_hardwaregain_chan0 = -50 # Erhöhen für mehr Sendeleistung, gültiger Bereich ist -90 bis 0 dB + + N = 10000 # Anzahl der auf einmal zu sendenden Samples + t = np.arange(N)/sample_rate + samples = 0.5*np.exp(2.0j*np.pi*100e3*t) # Sinuston von 100 kHz, sollte beim Empfänger bei 915,1 MHz erscheinen + samples *= 2**14 # Das PlutoSDR erwartet Samples zwischen -2^14 und +2^14, nicht -1 und +1 wie einige SDRs + + # Unseren Batch von Samples 100 Mal senden, insgesamt 1 Sekunde, wenn USB mithalten kann + for i in range(100): + sdr.tx(samples) # den Batch von Samples einmal senden + +Hier einige Hinweise zu diesem Code. Zunächst möchtest du deine IQ-Samples so simulieren, dass sie zwischen -1 und 1 liegen, aber bevor du sie überträgst, musst du sie aufgrund der Implementierung der :code:`tx()`-Funktion von Analog Devices mit 2^14 skalieren. Wenn du dir nicht sicher bist, welche Minimal- und Maximalwerte deine Samples haben, gib sie einfach mit :code:`print(np.min(samples), np.max(samples))` aus oder schreibe eine if-Anweisung, um sicherzustellen, dass sie nie über 1 oder unter -1 gehen (vorausgesetzt, dieser Code kommt vor der 2^14-Skalierung). Was die Sendeleistung betrifft, ist der Bereich -90 bis 0 dB, wobei 0 dB die höchste Sendeleistung ist. Wir möchten immer mit einer niedrigen Sendeleistung beginnen und sie bei Bedarf erhöhen, daher ist die Verstärkung standardmäßig auf -50 dB eingestellt, was am unteren Ende liegt. Stelle sie nicht einfach auf 0 dB, nur weil dein Signal nicht erscheint; es könnte etwas anderes falsch sein, und du möchtest deinen Empfänger nicht beschädigen. + +Samples wiederholt senden +########################## + +Wenn du denselben Satz von Samples kontinuierlich wiederholen möchtest, anstatt eine for/while-Schleife in Python wie oben zu verwenden, kannst du dem Pluto dies mit nur einer Zeile mitteilen: + +.. code-block:: python + + sdr.tx_cyclic_buffer = True # Zyklische Puffer aktivieren + +Du würdest dann deine Samples wie gewohnt senden: :code:`sdr.tx(samples)` nur einmal, und das Pluto sendet das Signal unbegrenzt, bis der :code:`sdr`-Objektdestruktor aufgerufen wird. Um die kontinuierlich gesendeten Samples zu ändern, kannst du nicht einfach erneut :code:`sdr.tx(samples)` mit einem neuen Satz von Samples aufrufen; du musst zuerst :code:`sdr.tx_destroy_buffer()` aufrufen und dann :code:`sdr.tx(samples)`. + +Rechtlich über die Luft senden +################################ + +Unzählige Male wurden ich von Studenten gefragt, auf welchen Frequenzen sie mit einer Antenne senden dürfen (in den Vereinigten Staaten). Die kurze Antwort ist: auf keiner, soweit ich weiß. Wenn Menschen auf spezifische Vorschriften hinweisen, die von Sendeleistungsgrenzen sprechen, beziehen sie sich normalerweise auf die `FCC-Vorschriften „Titel 47, Teil 15" (47 CFR 15) `_. Aber das sind Vorschriften für Hersteller, die Geräte bauen und verkaufen, die im ISM-Band betrieben werden, und die Vorschriften erörtern, wie sie getestet werden sollen. Ein Part-15-Gerät ist eines, bei dem eine Einzelperson keine Lizenz benötigt, um das Gerät im verwendeten Spektrum zu betreiben, aber das Gerät selbst muss autorisiert/zertifiziert sein, bevor es vermarktet und verkauft wird. Die Part-15-Vorschriften legen maximale Sende- und Empfangsleistungspegel für die verschiedenen Spektralbereiche fest, aber keine davon gilt tatsächlich für eine Person, die ein Signal mit einem SDR oder einem selbst gebauten Radio überträgt. Die einzigen Vorschriften, die ich finden konnte und die sich auf Radios beziehen, die keine eigentlichen Produkte sind, galten speziell für den Betrieb einer Kleinsendestelle im AM/FM-Band. Es gibt auch einen Abschnitt über „selbst gebaute Geräte", aber er besagt ausdrücklich, dass er nicht für etwas gilt, das aus einem Bausatz zusammengebaut wurde, und es wäre eine Dehnung zu sagen, ein Sendesystem mit einem SDR sei ein selbst gebautes Gerät. Zusammenfassend sind die FCC-Vorschriften nicht so einfach wie „du kannst auf diesen Frequenzen nur unterhalb dieser Leistungspegel senden", sondern vielmehr eine riesige Menge von Regeln für Tests und Compliance. + +Eine andere Betrachtungsweise wäre zu sagen: „Nun, das sind keine Part-15-Geräte, aber lass uns die Part-15-Regeln so befolgen, als wären sie es." Für das 915-MHz-ISM-Band lauten die Regeln, dass „die Feldstärke jeglicher Emissionen innerhalb des angegebenen Frequenzbandes 500 Mikrovolt/Meter bei 30 Metern nicht überschreiten darf. Die Emissionsgrenze in diesem Absatz basiert auf Messinstrumenten, die einen Mittelwertdetektor verwenden." Wie du sehen kannst, ist es nicht so einfach wie eine maximale Sendeleistung in Watt. + +Wenn du deine Amateurfunklizenz hast, erlaubt die FCC die Nutzung bestimmter Bänder, die für den Amateurfunk reserviert sind. Es gibt immer noch Richtlinien zu befolgen und maximale Sendeleistungen, aber zumindest sind diese Zahlen in Watt effektiver Strahlungsleistung angegeben. `Diese Infografik `_ zeigt, welche Bänder je nach Lizenzklasse (Technician, General und Extra) zur Verfügung stehen. Ich empfehle jedem, der mit SDRs senden möchte, die Amateurfunklizenz zu erwerben; weitere Informationen findest du auf `ARRL's Getting Licensed-Seite `_. + +Wenn jemand mehr Details darüber hat, was erlaubt und was nicht erlaubt ist, schreib mir bitte eine E-Mail. + +************************************************ +Gleichzeitiges Senden und Empfangen +************************************************ + +Mit dem tx_cyclic_buffer-Trick kannst du ganz einfach gleichzeitig empfangen und senden, indem du den Sender startest und dann empfängst. +Das folgende Codebeispiel zeigt das gleichzeitige Senden eines QPSK-Signals im 915-MHz-Band, den Empfang und die Darstellung der Leistungsspektraldichte (PSD). + +.. code-block:: python + + import numpy as np + import adi + import matplotlib.pyplot as plt + + sample_rate = 1e6 # Hz + center_freq = 915e6 # Hz + num_samps = 100000 # Anzahl der Samples pro rx()-Aufruf + + sdr = adi.Pluto("ip:192.168.2.1") + sdr.sample_rate = int(sample_rate) + + # TX konfigurieren + sdr.tx_rf_bandwidth = int(sample_rate) # Filtergrenzfrequenz, gleich der Abtastrate setzen + sdr.tx_lo = int(center_freq) + sdr.tx_hardwaregain_chan0 = -50 # Erhöhen für mehr Sendeleistung, gültiger Bereich ist -90 bis 0 dB + + # RX konfigurieren + sdr.rx_lo = int(center_freq) + sdr.rx_rf_bandwidth = int(sample_rate) + sdr.rx_buffer_size = num_samps + sdr.gain_control_mode_chan0 = 'manual' + sdr.rx_hardwaregain_chan0 = 0.0 # dB, erhöhen für mehr Empfangsverstärkung, aber vorsichtig sein, ADC nicht zu sättigen + + # Sendesignal erstellen (QPSK, 16 Samples pro Symbol) + num_symbols = 1000 + x_int = np.random.randint(0, 4, num_symbols) # 0 bis 3 + x_degrees = x_int*360/4.0 + 45 # 45, 135, 225, 315 Grad + x_radians = x_degrees*np.pi/180.0 # sin() und cos() nehmen Radiant + x_symbols = np.cos(x_radians) + 1j*np.sin(x_radians) # erzeugt unsere QPSK-Komplexsymbole + samples = np.repeat(x_symbols, 16) # 16 Samples pro Symbol (Rechteckimpulse) + samples *= 2**14 # Das PlutoSDR erwartet Samples zwischen -2^14 und +2^14, nicht -1 und +1 wie einige SDRs + + # Sender starten + sdr.tx_cyclic_buffer = True # Zyklische Puffer aktivieren + sdr.tx(samples) # Senden beginnen + + # Puffer zur Sicherheit leeren + for i in range (0, 10): + raw_data = sdr.rx() + + # Samples empfangen + rx_samples = sdr.rx() + print(rx_samples) + + # Senden stoppen + sdr.tx_destroy_buffer() + + # Leistungsspektraldichte berechnen (Frequenzbereichsversion des Signals) + psd = np.abs(np.fft.fftshift(np.fft.fft(rx_samples)))**2 + psd_dB = 10*np.log10(psd) + f = np.linspace(sample_rate/-2, sample_rate/2, len(psd)) + + # Zeitbereich darstellen + plt.figure(0) + plt.plot(np.real(rx_samples[::100])) + plt.plot(np.imag(rx_samples[::100])) + plt.xlabel("Zeit") + + # Frequenzbereich darstellen + plt.figure(1) + plt.plot(f/1e6, psd_dB) + plt.xlabel("Frequenz [MHz]") + plt.ylabel("PSD") + plt.show() + + +Du solltest etwas wie das Folgende sehen, vorausgesetzt, du hast richtige Antennen oder ein Kabel angeschlossen: + +.. image:: ../_images_de/pluto_tx_rx.svg + :align: center + +Es ist eine gute Übung, :code:`sdr.tx_hardwaregain_chan0` und :code:`sdr.rx_hardwaregain_chan0` langsam anzupassen, um sicherzustellen, dass das empfangene Signal wie erwartet schwächer/stärker wird. + +********************************** +Maia SDR und IQEngine +********************************** + +Möchtest du dein Pluto als Echtzeit-Spektrumanalysator auf deinem PC oder Smartphone verwenden? Das Open-Source-Projekt `Maia SDR `_ bietet ein modifiziertes Firmware-Image für das Pluto, das eine FFT auf dem FPGA des Pluto ausführt und einen Webserver auf dem ARM-Prozessor des Pluto betreibt! Diese Weboberfläche wird verwendet, um die Frequenz und andere SDR-Parameter einzustellen und das Spektrogramm in einer Wasserfalldarstellung anzuzeigen. Du kannst Aufnahmen der rohen IQ-Samples bis zu 400 MB speichern und sie auf deinen Computer/dein Telefon herunterladen oder in IQEngine anzeigen. + +Installiere die neueste Maia-Pluto-Firmware, indem du das `neueste Release `_ herunterlädst, insbesondere die Datei namens :code:`plutosdr-fw-maia-sdr-vX.Y.Z.zip`. Entpacke die Datei und kopiere die :code:`pluto.frm`-Datei auf das Massenspeichergerät deines Pluto (es ähnelt einem USB-Flash-Laufwerk), dann wirf das Pluto aus (ziehe es nicht ab). Dies ist derselbe Vorgang wie das Aktualisieren der Firmware des Pluto; es blinkt mehrere Minuten lang und startet dann neu. Stelle abschließend eine SSH-Verbindung zum Pluto her, wie wir es im Abschnitt „Pluto hacken" getan haben, indem du :code:`ssh root@192.168.2.1` in einem Terminal eingibst, mit dem Standardpasswort :code:`analog`. Sobald du verbunden bist, musst du die folgenden drei Befehle nacheinander ausführen: + +.. code-block:: bash + + fw_setenv ramboot_verbose 'adi_hwref;echo Copying Linux from DFU to RAM... && run dfu_ram;if run adi_loadvals; then echo Loaded AD936x refclk frequency and model into devicetree; fi; envversion;setenv bootargs console=ttyPS0,115200 maxcpus=${maxcpus} rootfstype=ramfs root=/dev/ram0 rw earlyprintk clk_ignore_unused uio_pdrv_genirq.of_id=uio_pdrv_genirq uboot="${uboot-version}" && bootm ${fit_load_address}#${fit_config}' + + fw_setenv qspiboot_verbose 'adi_hwref;echo Copying Linux from QSPI flash to RAM... && run read_sf && if run adi_loadvals; then echo Loaded AD936x refclk frequency and model into devicetree; fi; envversion;setenv bootargs console=ttyPS0,115200 maxcpus=${maxcpus} rootfstype=ramfs root=/dev/ram0 rw earlyprintk clk_ignore_unused uio_pdrv_genirq.of_id=uio_pdrv_genirq uboot="${uboot-version}" && bootm ${fit_load_address}#${fit_config} || echo BOOT failed entering DFU mode ... && run dfu_sf' + + fw_setenv qspiboot 'set stdout nulldev;adi_hwref;test -n $PlutoRevA || gpio input 14 && set stdout serial@e0001000 && sf probe && sf protect lock 0 100000 && run dfu_sf; set stdout serial@e0001000;itest *f8000258 == 480003 && run clear_reset_cause && run dfu_sf; itest *f8000258 == 480007 && run clear_reset_cause && run ramboot_verbose; itest *f8000258 == 480006 && run clear_reset_cause && run qspiboot_verbose; itest *f8000258 == 480002 && run clear_reset_cause && exit; echo Booting silently && set stdout nulldev; run read_sf && run adi_loadvals; envversion;setenv bootargs console=ttyPS0,115200 maxcpus=${maxcpus} rootfstype=ramfs root=/dev/ram0 rw quiet loglevel=4 clk_ignore_unused uio_pdrv_genirq.of_id=uio_pdrv_genirq uboot="${uboot-version}" && bootm ${fit_load_address}#${fit_config} || set stdout serial@e0001000;echo BOOT failed entering DFU mode ... && sf protect lock 0 100000 && run dfu_sf' + +(Weitere Informationen dazu, warum dies notwendig ist, findest du auf der `Maia-Installationsseite `_) + +Starte dein Pluto noch einmal neu. Ab diesem Zeitpunkt sollte das Pluto Maia ausführen! Öffne http://192.168.2.1:8000 in einem Webbrowser und du solltest den Maia-Echtzeit-Spektrumanalysator und das SDR-Bedienfeld sehen, wie im Screenshot unten gezeigt: + +.. image:: ../_images_de/Maia.png + :scale: 40 % + :align: center + :alt: Screenshot von Maia SDR + +Um zu testen, wie schnell Maia laufen kann, versuche, die :code:`Spectrum Rate` auf 100 Hz oder mehr zu erhöhen. Neben der Steuerung der wichtigsten SDR-Parameter wie Frequenz, Abtastrate und Verstärkung kannst du auf die Schaltfläche :code:`Record` am unteren Rand klicken, und es beginnt, die rohen IQ-Samples im Speicher des Pluto aufzuzeichnen. Du kannst dann die Aufnahme in IQEngine öffnen, um sie über die Schaltfläche :code:`Recording` und den Link :code:`View in IQEngine` anzuzeigen, wie im Screenshot unten gezeigt, oder die Datei auf deinem Gerät speichern. + +.. image:: ../_images_de/IQEngine_from_Maia.png + :scale: 40 % + :align: center + :alt: Screenshot von IQEngine, geöffnet von Maia SDR + + +************************ +Referenz-API +************************ + +Die vollständige Liste der SDR-Eigenschaften und -Funktionen, die du aufrufen kannst, findest du im `pyadi-iio Pluto Python-Code (AD936X) `_. + +************************ +Python-Übungen +************************ + +Anstatt dir Code zum Ausführen bereitzustellen, habe ich mehrere Übungen erstellt, bei denen 95 % des Codes vorhanden ist und der restliche Code einfaches Python ist, das du selbst erstellen musst. Die Übungen sollen nicht schwierig sein. Es fehlt gerade genug Code, damit du nachdenken musst. + +Übung 1: USB-Durchsatz bestimmen +################################## + +Lass uns Samples vom PlutoSDR empfangen und dabei sehen, wie viele Samples pro Sekunde wir durch die USB-2.0-Verbindung übertragen können. + +**Deine Aufgabe ist es, ein Python-Skript zu erstellen, das die Rate bestimmt, mit der Samples in Python empfangen werden, d.h., zähle die empfangenen Samples und verfolge die Zeit, um die Rate zu ermitteln. Dann probiere verschiedene sample_rate- und buffer_size-Werte aus, um zu sehen, wie sie die maximal erreichbare Rate beeinflussen.** + +Beachte, dass es bedeutet, wenn du weniger Samples pro Sekunde empfängst als die angegebene sample_rate, dass du einen gewissen Anteil von Samples verlierst/verwirfst, was bei hohen sample_rate-Werten wahrscheinlich passieren wird. Das Pluto verwendet nur USB 2.0. + +Der folgende Code dient als Ausgangspunkt und enthält die Anweisungen, die du benötigst, um diese Aufgabe zu erfüllen. + +.. code-block:: python + + import numpy as np + import adi + import matplotlib.pyplot as plt + import time + + sample_rate = 10e6 # Hz + center_freq = 100e6 # Hz + + sdr = adi.Pluto("ip:192.168.2.1") + sdr.sample_rate = int(sample_rate) + sdr.rx_rf_bandwidth = int(sample_rate) # Filtergrenzfrequenz, gleich der Abtastrate setzen + sdr.rx_lo = int(center_freq) + sdr.rx_buffer_size = 1024 # Puffer, den das Pluto zum Puffern von Samples verwendet + samples = sdr.rx() # Samples vom Pluto empfangen + +Zum Messen, wie lange etwas dauert, kannst du folgenden Code verwenden: + +.. code-block:: python + + start_time = time.time() + # etwas tun + end_time = time.time() + print('Vergangene Sekunden:', end_time - start_time) + +Hier sind einige Hinweise zum Einstieg. + +Hinweis 1: Du musst die Zeile :code:`samples = sdr.rx()` in eine Schleife setzen, die viele Male läuft (z.B. 100 Mal). Du musst zählen, wie viele Samples du bei jedem Aufruf von :code:`sdr.rx()` erhältst, während du die verstrichene Zeit verfolgst. + +Hinweis 2: Nur weil du Samples pro Sekunde berechnest, bedeutet das nicht, dass du genau 1 Sekunde lang Samples empfangen musst. Du kannst die Anzahl der empfangenen Samples durch die verstrichene Zeit dividieren. + +Hinweis 3: Beginne mit :code:`sample_rate = 10e6` wie im Code gezeigt, da diese Rate viel höher ist als USB 2.0 unterstützen kann. Du kannst sehen, wie viele Daten durchkommen. Dann kannst du rx_buffer_size anpassen. Mache es viel größer und schau, was passiert. Sobald du ein funktionierendes Skript hast und mit :code:`rx_buffer_size` herumgespielt hast, versuche :code:`sample_rate` anzupassen. Finde heraus, wie weit du heruntergehen musst, bis du 100 % der Samples in Python empfangen kannst (d.h. mit 100 % Auslastung abtasten). + +Hinweis 4: Versuche in der Schleife, in der du :code:`sdr.rx()` aufrufst, so wenig wie möglich zu tun, damit keine zusätzliche Verzögerung in der Ausführungszeit entsteht. Führe keine rechenintensiven Operationen wie Ausgaben innerhalb der Schleife durch. + +Als Teil dieser Übung bekommst du eine Vorstellung vom maximalen Durchsatz von USB 2.0. Du kannst online nachschlagen, um deine Ergebnisse zu überprüfen. + +Als Bonus versuche, :code:`center_freq` und :code:`rx_rf_bandwidth` zu ändern, um zu sehen, ob es die Rate beeinflusst, mit der du Samples vom Pluto empfangen kannst. + + +Übung 2: Ein Spektrogramm/Wasserfall erstellen +############################################### + +Für diese Übung erstellst du ein Spektrogramm (auch bekannt als Wasserfall), wie wir am Ende des :ref:`freq-domain-chapter`-Kapitels gelernt haben. Ein Spektrogramm ist einfach eine Menge von FFTs, die übereinander angezeigt werden. Mit anderen Worten: Es ist ein Bild, bei dem eine Achse die Frequenz und die andere Achse die Zeit darstellt. + +Im :ref:`freq-domain-chapter`-Kapitel haben wir den Python-Code gelernt, um eine FFT durchzuführen. Für diese Übung kannst du Code-Schnipsel aus der vorherigen Übung sowie ein wenig grundlegenden Python-Code verwenden. + +Hinweise: + +1. Versuche, :code:`sdr.rx_buffer_size` auf die FFT-Größe zu setzen, sodass du immer 1 FFT für jeden Aufruf von :code:`sdr.rx()` durchführst. +2. Erstelle ein 2D-Array, um alle FFT-Ergebnisse zu speichern, wobei jede Zeile 1 FFT ist. Ein 2D-Array gefüllt mit Nullen kann erstellt werden mit: :code:`np.zeros((num_rows, fft_size))`. Greife auf Zeile :code:`i` des Arrays zu mit: :code:`waterfall_2darray[i,:]`. +3. :code:`plt.imshow()` ist eine bequeme Möglichkeit, ein 2D-Array anzuzeigen. Es skaliert die Farbe automatisch. + +Als erweiterte Aufgabe lass das Spektrogramm live aktualisieren. + +****** +Pluto+ +****** + +Das Pluto+ (auch bekannt als Pluto Plus) ist eine inoffizielle und verbesserte Version des originalen PlutoSDR, die hauptsächlich bei AliExpress erhältlich ist. Es enthält einen Gigabit-Ethernet-Anschluss, beide RX- und beide TX-Kanäle als SMA-Buchsen, einen MicroSD-Slot, einen 0,5-PPM-VCTCXO und einen externen Takteingang über einen U.FL-Anschluss auf der Platine. + +.. image:: ../_images_de/pluto_plus.png + :scale: 70 % + :align: center + :alt: Das Pluto Plus + +Der Ethernet-Anschluss ist ein enormes Upgrade, da er die erreichbare Abtastrate beim Empfangen oder Senden mit 100 % Auslastung erheblich erhöht. Das Pluto und das Pluto+ verwenden standardmäßig 16 Bit für I und Q, obwohl es nur einen 12-Bit-ADC hat, was 4 Bytes pro IQ-Sample entspricht. Gigabit-Ethernet mit 90 % Effizienz entspricht 900 Mb/s oder 112,5 MB/s, also bei 4 Bytes pro IQ-Sample entspricht das einer maximalen Abtastrate von etwa 28 MHz, wenn du alle Samples über einen längeren Zeitraum empfangen möchtest (z.B. mehr als eine Sekunde). Zum Vergleich: USB 3.0 kann etwa 56 MHz erreichen, und USB 2.0 liegt bei etwa 5 MHz. Es gibt auch eine Grenze dafür, was Python basierend auf der Leistung deines Computers verarbeiten kann, sowie die spezifische DSP-Anwendung, die du auf den Samples ausführen möchtest (oder die Schreibgeschwindigkeit auf die Festplatte, wenn du sie einfach in eine Datei aufzeichnest). Abtastraten von etwa 10 MHz sind für Python-basierte SDR-Anwendungen mit dem Pluto+ über Ethernet realistischer. + +.. image:: ../_images_de/pluto_plus_pcb.jpg + :scale: 30 % + :align: center + :alt: Foto der Pluto-Plus-Platine + +Um die IP-Adresse für den Ethernet-Anschluss einzustellen, schließe das Pluto+ über USB an und öffne das Massenspeichergerät, bearbeite config.txt, um :code:`[USB_ETHERNET]` auszufüllen. Trenne das Pluto+ kurz vom Strom. Du solltest jetzt in der Lage sein, über Ethernet per SSH unter der eingegebenen IP eine Verbindung zum Pluto+ herzustellen. Wenn es funktioniert hat, kannst du das Micro-USB-Kabel an den 5V-Anschluss umstecken, sodass es das Pluto+ nur noch mit Strom versorgt und alle Kommunikation über Ethernet erzwingt. Denke daran, dass du sowohl mit dem regulären PlutoSDR (als auch mit dem Pluto+) bis zu 61 MHz Bandbreite abtasten und zusammenhängende Chunks von bis zu ~10 Millionen Samples gleichzeitig empfangen kannst, solange du zwischen den Chunks wartest. Das ermöglicht leistungsstarke Spektrumsensierungsanwendungen. + +Der Python-Code für das Pluto+ ist derselbe wie für das PlutoSDR, außer dass du :code:`192.168.2.1` durch die eingestellte Ethernet-IP ersetzen musst. Versuche, Samples in einer Schleife zu empfangen und zu zählen, wie viele du empfängst, um zu sehen, wie hoch du die Abtastrate erhöhen kannst, während du in Python noch ungefähr die entsprechende Anzahl von Samples pro Sekunde empfängst. Als Hinweis: Das Erhöhen von rx_buffer_size auf einen sehr großen Wert hilft, den Durchsatz zu erhöhen. + +************ +AntSDR E200 +************ + +Das AntSDR E200, das wir als AntSDR bezeichnen werden, ist ein kostengünstiges SDR auf Basis des 936X, sehr ähnlich dem Pluto und Pluto+, hergestellt von einem Unternehmen namens MicroPhase aus Shanghai, China. Ähnlich wie das Pluto+ verwendet es eine 1-GB-Ethernet-Verbindung, obwohl das AntSDR keine USB-Datentverbindung bietet. Was das AntSDR einzigartig macht, ist seine Fähigkeit, genau wie ein Pluto mit der IIO-Bibliothek oder als USRP mit der UHD-Bibliothek zu fungieren. Standardmäßig wird es mit dem Pluto-Verhalten geliefert, aber der Wechsel in den USRP/UHD-Modus ist ein einfaches Firmware-Update. Beide Firmware-Sätze sind im Wesentlichen nur Kopien von Analog Devices/Ettus mit sehr geringfügigen Anpassungen zur Unterstützung der Hardware des AntSDR. Ein weiterer einzigartiger Aspekt ist, dass du die Platine entweder mit dem 9363- oder dem 9361-Chip kaufen kannst; obwohl es sich funktional um dasselbe Teil handelt, ist der 9361 im Werk für höhere HF-Leistung bei den oberen Frequenzen sortiert. Beachte, dass das Pluto und das Pluto+ nur mit dem 9363 geliefert werden. Die AntSDR-Spezifikationen behaupten, dass die 9363-basierte Version nur bis 3,8 GHz und eine 20-MHz-Abtastrate erreicht, aber das stimmt nicht; es kann die vollen 6 GHz und etwa 60 MHz Abtastrate erreichen (obwohl nicht 100 % der Samples über 1-GB-Ethernet übertragen werden). Wie die anderen Plutos ist das AntSDR ein 2x2-Gerät, wobei die zweiten Sende- und Empfangskanäle über U.FL-Steckverbinder auf der Platine zugänglich sind. Alle anderen HF-Leistungs- und technischen Spezifikationen sind dem Pluto/Pluto+ ähnlich oder identisch. Es ist bei `Crowd Supply `_ und AliExpress erhältlich. + +.. image:: ../_images_de/AntSDR.png + :scale: 80 % + :align: center + :alt: Das AntSDR E200 SDR mit optionalem Gehäuse + +Der kleine DIP-Schalter auf dem AntSDR wechselt zwischen dem Booten von der SD-Karte oder dem integrierten Quad-SPI-Flash-Speicher (QSPI). Zum Zeitpunkt dieses Schreibens wird das E200 mit der Pluto-Firmware im QPSI und der USRP/UHD-Firmware auf der SD-Karte geliefert, sodass der Schalter verwendet werden kann, um ohne weitere Schritte zwischen den Modi zu wechseln. + +Das Blockdiagramm des E200 ist unten gezeigt. + +.. image:: ../_images_de/AntSDR_E200_block_diagram.png + :scale: 80 % + :align: center + :alt: Das Blockdiagramm des AntSDR E200 + +Das Einrichten und Verwenden des AntSDR im Pluto-Modus ähnelt dem Pluto+. Beachte nur, dass die Standard-IP 192.168.1.10 ist und es keine USB-Datenverbindung hat, sodass kein Massenspeichergerät zum Aktualisieren der Firmware oder zum Ändern von Einstellungen vorhanden ist. Stattdessen kann eine SD-Karte zum Aktualisieren der Firmware und SSH zum Ändern von Einstellungen verwendet werden. Alternativ, wenn du SSH in das Gerät verwenden kannst, kannst du die IP-Adresse des Geräts mit dem Befehl ändern: :code:`fw_setenv ipaddr_eth 192.168.2.1`, wobei du die IP-Adresse durch die gewünschte ersetzt. Die Pluto/IIO-Firmware findest du hier https://github.com/MicroPhase/antsdr-fw-patch und die USRP/UHD-Firmware hier https://github.com/MicroPhase/antsdr_uhd. + +Wenn die SD-Karte nicht mit dem USRP/UHD-Treiber geliefert wurde oder du die neueste Version installieren möchtest, kannst du `diese Schritte `_ befolgen, um die USRP/UHD-Firmware auf dem AntSDR sowie die hostseitigen Treiber auf deinem Rechner zu installieren, die eine leicht angepasste Version des normalen UHD-hostseitigen Codes sind. Du kannst dann :code:`uhd_find_devices` und :code:`uhd_usrp_probe` wie gewohnt verwenden (siehe das USRP-Kapitel für weitere Informationen und Beispielcode, der mit dem AntSDR im USRP-Modus funktioniert). Die folgenden Befehle wurden zum Installieren des hostseitigen Codes auf Ubuntu 22 verwendet: + +.. code-block:: bash + + sudo apt-get update + sudo apt-get install autoconf automake build-essential ccache cmake cpufrequtils doxygen ethtool \ + g++ git inetutils-tools libboost-all-dev libncurses5 libncurses5-dev libusb-1.0-0 libusb-1.0-0-dev \ + libusb-dev python3-dev python3-mako python3-numpy python3-requests python3-scipy python3-setuptools \ + python3-ruamel.yaml + cd ~ + git clone git@github.com:MicroPhase/antsdr_uhd.git + cd host + mkdir build + cd build + cmake -DENABLE_X400=OFF -DENABLE_N320=OFF -DENABLE_X300=OFF -DENABLE_USRP2=OFF -DENABLE_USRP1=OFF -DENABLE_N300=OFF -DENABLE_E320=OFF -DENABLE_E300=OFF ../ + (HINWEIS - stelle an diesem Punkt sicher, dass du in den „aktivierten Komponenten" ANT und LibUHD - Python API siehst) + make -j8 + sudo make install + sudo ldconfig + export PYTHONPATH="${PYTHONPATH}:/usr/local/lib/python3/dist-packages" + sudo sysctl -w net.core.rmem_max=1000000 + sudo sysctl -w net.core.wmem_max=1000000 + +Auf der Geräteseite wurde die bereits auf der SD-Karte befindliche USRP-Firmware verwendet, indem der DIP-Schalter unter dem Ethernet-Anschluss auf „SD" umgeschaltet wurde. + +Das AntSDR kann mit den folgenden Befehlen identifiziert und überprüft werden: + +.. code-block:: bash + + uhd_find_devices --args addr=192.168.1.10 + uhd_usrp_probe --args addr=192.168.1.10 + +Unten ist ein Beispiel der Ausgabe bei korrekter Funktion: + +.. code-block:: bash + + $ uhd_find_devices --args addr=192.168.1.10 + [INFO] [UHD] linux; GNU C++ version 11.3.0; Boost_107400; UHD_4.1.0.0-0-d2f0b1b1 + -------------------------------------------------- + -- UHD Device 0 + -------------------------------------------------- + Device Address: + serial: 0223D80FF0D767EBC6D3AAAA6793E64D + addr: 192.168.1.10 + name: ANTSDR-E200 + product: E200 v1 + type: ant + + $ uhd_usrp_probe --args addr=192.168.1.10 + [INFO] [UHD] linux; GNU C++ version 11.3.0; Boost_107400; UHD_4.1.0.0-0-d2f0b1b1 + [INFO] [ANT] Detected Device: ANTSDR + [INFO] [ANT] Initialize CODEC control... + [INFO] [ANT] Initialize Radio control... + [INFO] [ANT] Performing register loopback test... + [INFO] [ANT] Register loopback test passed + [INFO] [ANT] Performing register loopback test... + [INFO] [ANT] Register loopback test passed + [INFO] [ANT] Setting master clock rate selection to 'automatic'. + [INFO] [ANT] Asking for clock rate 16.000000 MHz... + [INFO] [ANT] Actually got clock rate 16.000000 MHz. + _____________________________________________________ + / + | Device: B-Series Device + | _____________________________________________________ + | / + | | Mboard: B210 + | | magic: 45568 + | | eeprom_revision: v0.1 + | | eeprom_compat: 1 + | | product: MICROPHASE + | | name: ANT + | | serial: 0223D80FF0D767EBC6D3AAAA6793E64D + | | FPGA Version: 16.0 + | | + | | Time sources: none, internal, external + | | Clock sources: internal, external + | | Sensors: ref_locked + | | _____________________________________________________ + | | / + | | | RX DSP: 0 + | | | + | | | Freq range: -8.000 to 8.000 MHz + | | _____________________________________________________ + | | / + | | | RX DSP: 1 + | | | + | | | Freq range: -8.000 to 8.000 MHz + | | _____________________________________________________ + | | / + | | | RX Dboard: A + | | | _____________________________________________________ + | | | / + | | | | RX Frontend: A + | | | | Name: FE-RX1 + | | | | Antennas: TX/RX, RX2 + | | | | Sensors: temp, rssi, lo_locked + | | | | Freq range: 50.000 to 6000.000 MHz + | | | | Gain range PGA: 0.0 to 76.0 step 1.0 dB + | | | | Bandwidth range: 200000.0 to 56000000.0 step 0.0 Hz + | | | | Connection Type: IQ + | | | | Uses LO offset: No + | | | _____________________________________________________ + | | | / + | | | | RX Frontend: B + | | | | Name: FE-RX2 + | | | | Antennas: TX/RX, RX2 + | | | | Sensors: temp, rssi, lo_locked + | | | | Freq range: 50.000 to 6000.000 MHz + | | | | Gain range PGA: 0.0 to 76.0 step 1.0 dB + | | | | Bandwidth range: 200000.0 to 56000000.0 step 0.0 Hz + | | | | Connection Type: IQ + | | | | Uses LO offset: No + | | | _____________________________________________________ + | | | / + | | | | RX Codec: A + | | | | Name: B210 RX dual ADC + | | | | Gain Elements: None + | | _____________________________________________________ + | | / + | | | TX DSP: 0 + | | | + | | | Freq range: -8.000 to 8.000 MHz + | | _____________________________________________________ + | | / + | | | TX DSP: 1 + | | | + | | | Freq range: -8.000 to 8.000 MHz + | | _____________________________________________________ + | | / + | | | TX Dboard: A + | | | _____________________________________________________ + | | | / + | | | | TX Frontend: A + | | | | Name: FE-TX1 + | | | | Antennas: TX/RX + | | | | Sensors: temp, lo_locked + | | | | Freq range: 50.000 to 6000.000 MHz + | | | | Gain range PGA: 0.0 to 89.8 step 0.2 dB + | | | | Bandwidth range: 200000.0 to 56000000.0 step 0.0 Hz + | | | | Connection Type: IQ + | | | | Uses LO offset: No + | | | _____________________________________________________ + | | | / + | | | | TX Frontend: B + | | | | Name: FE-TX2 + | | | | Antennas: TX/RX + | | | | Sensors: temp, lo_locked + | | | | Freq range: 50.000 to 6000.000 MHz + | | | | Gain range PGA: 0.0 to 89.8 step 0.2 dB + | | | | Bandwidth range: 200000.0 to 56000000.0 step 0.0 Hz + | | | | Connection Type: IQ + | | | | Uses LO offset: No + | | | _____________________________________________________ + | | | / + | | | | TX Codec: A + | | | | Name: B210 TX dual DAC + | | | | Gain Elements: None + + +Schließlich kannst du die Python-API mit dem folgenden Python-Snippet testen, entweder in einem Python-Terminal oder einem Python-Skript: + +.. code-block:: python + + import uhd + usrp = uhd.usrp.MultiUSRP("addr=192.168.1.10") + samples = usrp.recv_num_samps(10000, 100e6, 1e6, [0], 50) + print(samples[0:10]) + +Dies sollte 10.000 Samples bei 100 MHz Mittelfrequenz, 1 MHz Abtastrate und 50 dB Verstärkung empfangen. Es werden die IQ-Werte der ersten 10 Samples ausgegeben, um zu überprüfen, ob alles funktioniert hat. Für die nächsten Schritte und weitere Beispiele findest du im :ref:`usrp-chapter`-Kapitel. + +Wenn :code:`import uhd` einen ModuleNotFoundError ausgibt, musst du möglicherweise folgende Zeile zu deiner .bashrc-Datei hinzufügen: + +.. code-block:: bash + + export PYTHONPATH="${PYTHONPATH}:/usr/local/lib/python3/dist-packages" + + + +************ +AntSDR E310 +************ + +Zusätzlich zum E200 stellt MicroPhase auch ein Modell namens AntSDR E310 her. Das AntSDR E310 ist dem E200 sehr ähnlich, außer dass es den zweiten Empfangs- und zweiten Sendekanal als SMA-Buchsen an der Vorderseite hat und derzeit nur den Pluto/IIO-Modus unterstützt (kein USRP-Modus). Es verwendet denselben FPGA wie das E200. Ein weiterer Unterschied ist ein extra USB-C-Anschluss, der als USB-OTG-Schnittstelle fungiert (z.B. zum Anschließen eines USB-Laufwerks). Das AntSDR E310 ist nur bei `AliExpress `_ erhältlich (nicht bei Crowd Supply wie das E200). Zum Zeitpunkt dieses Schreibens hat das E310 ungefähr denselben Preis wie das E200. Wenn du also nicht vorhast, den „USRP-Modus" zu verwenden, und es wünschenswert findest, die zusätzlichen Kanäle über SMA zugänglich zu haben, auch wenn es einen etwas größeren Formfaktor bedeutet, ist das E310 eine gute Wahl. + +.. image:: ../_images_de/AntSDR_E310.png + :scale: 80 % + :align: center + :alt: Das AntSDR E310 SDR mit optionalem Gehäuse + +.. image:: ../_images_de/AntSDR_Comparison.jpg + :scale: 70 % + :align: center + :alt: Das AntSDR E200 und E310 nebeneinander diff --git a/content-de/pulse_shaping.rst b/content-de/pulse_shaping.rst new file mode 100644 index 00000000..a804ad6d --- /dev/null +++ b/content-de/pulse_shaping.rst @@ -0,0 +1,261 @@ +.. _pulse-shaping-chapter: + +####################### +Impulsformung +####################### + +Dieses Kapitel behandelt Impulsformung, Intersymbolinterferenz, Matched Filter und Raised-Cosine-Filter. Am Ende verwenden wir Python, um BPSK-Symbolen Impulsformung hinzuzufügen. Du kannst diesen Abschnitt als Teil II des Filterkapitels betrachten, in dem wir tiefer in die Impulsformung eintauchen. + +********************************** +Intersymbolinterferenz (ISI) +********************************** + +Im Kapitel :ref:`filters-chapter` haben wir gelernt, dass blockförmige Symbole/Impulse übermäßig viel Spektrum verwenden, und wir können die verwendete Spektrummenge durch das "Formen" unserer Impulse erheblich reduzieren. Du kannst jedoch nicht einfach irgendeinen Tiefpassfilter verwenden, sonst kann Intersymbolinterferenz (ISI) entstehen, bei der Symbole ineinander übergehen und sich gegenseitig stören. + +Wenn wir digitale Symbole übertragen, senden wir sie nacheinander (im Gegensatz dazu, eine Zeit dazwischen zu warten). Wenn du einen Impulsformfilter anwendest, verlängert er den Impuls im Zeitbereich (um ihn im Frequenzbereich zu komprimieren), wodurch sich benachbarte Symbole überlappen. Die Überlappung ist in Ordnung, solange dein Impulsformfilter dieses eine Kriterium erfüllt: Alle Impulse müssen an jedem Vielfachen unserer Symbolperiode :math:`T` zu null summieren, außer bei einem der Impulse. Die Idee wird am besten durch folgende Visualisierung verständlich: + +.. image:: ../_images/pulse_train.svg + :align: center + :target: ../_images/pulse_train.svg + :alt: Ein Impulszug aus Sinc-Impulsen + +Wie du sehen kannst, gibt es in jedem Intervall von :math:`T` genau einen Peak eines Impulses, während die restlichen Impulse bei 0 liegen (sie schneiden die x-Achse). Wenn der Empfänger das Signal abtastet, tut er dies zum richtigen Zeitpunkt (am Peak der Impulse), was bedeutet, dass dies der einzige relevante Zeitpunkt ist. Normalerweise gibt es am Empfänger einen Symbolsynchronisierungsblock, der sicherstellt, dass die Symbole an den Peaks abgetastet werden. + +********************************** +Matched Filter +********************************** + +Ein Trick, den wir in der drahtlosen Kommunikation verwenden, nennt sich Matched Filtering. Um Matched Filtering zu verstehen, musst du zunächst diese zwei Punkte verstehen: + +1. Die oben besprochenen Impulse müssen nur *am Empfänger* vor der Abtastung perfekt ausgerichtet sein. Bis dahin ist es nicht wichtig, ob ISI vorhanden ist, d.h. die Signale können mit ISI durch die Luft fliegen und das ist in Ordnung. + +2. Wir möchten einen Tiefpassfilter in unserem Sender einsetzen, um die von unserem Signal verwendete Spektrummenge zu reduzieren. Aber der Empfänger benötigt auch einen Tiefpassfilter, um so viel Rauschen/Interferenz neben dem Signal wie möglich zu eliminieren. Daher haben wir am Sender (Tx) und am Empfänger (Rx) jeweils einen Tiefpassfilter, und die Abtastung erfolgt nach beiden Filtern (und den Auswirkungen des drahtlosen Kanals). + +Was wir in der modernen Kommunikation tun, ist den Impulsformfilter gleichmäßig zwischen Tx und Rx aufzuteilen. Sie müssen *nicht* identische Filter sein, aber theoretisch ist der optimale lineare Filter zur Maximierung des SNR bei AWGN, *denselben* Filter bei Tx und Rx zu verwenden. Diese Strategie nennt sich das "Matched Filter"-Konzept. + +Eine andere Art, über Matched Filter nachzudenken, ist, dass der Empfänger das empfangene Signal mit dem bekannten Vorlagensignal korreliert. Das Vorlagensignal sind im Wesentlichen die Impulse, die der Sender sendet, unabhängig von den auf sie angewendeten Phasen-/Amplitudenverschiebungen. Zur Erinnerung: Filtern erfolgt durch Faltung, die im Grunde Korrelation ist (tatsächlich sind sie mathematisch identisch, wenn die Vorlage symmetrisch ist). Dieser Prozess der Korrelation des empfangenen Signals mit der Vorlage gibt uns die beste Chance, das Gesendete wiederherzustellen, und deshalb ist es theoretisch optimal. Als Analogie denke an ein Bilderkennungssystem, das mithilfe einer Gesichtsvorlage und einer 2D-Korrelation nach Gesichtern sucht: + +.. image:: ../_images/face_template.png + :scale: 70 % + :align: center + +********************************** +Einen Filter halbieren +********************************** + +Wie teilen wir einen Filter tatsächlich in zwei Hälften auf? Faltung ist assoziativ, was bedeutet: + +.. math:: + (f * g) * h = f * (g * h) + +Stellen wir uns vor, :math:`f` ist unser Eingangssignal, und :math:`g` und :math:`h` sind Filter. :math:`f` mit :math:`g` und dann :math:`h` zu filtern ist dasselbe wie mit einem einzigen Filter :math:`g * h` zu filtern. + +Beachte auch, dass Faltung im Zeitbereich Multiplikation im Frequenzbereich entspricht: + +.. math:: + g(t) * h(t) \leftrightarrow G(f)H(f) + +Um einen Filter zu halbieren, kann man die Quadratwurzel der Frequenzantwort nehmen. + +.. math:: + X(f) = X_H(f) X_H(f) \quad \mathrm{wobei} \quad X_H(f) = \sqrt{X(f)} + +Unten ist ein vereinfachtes Diagramm einer Sende- und Empfangskette, bei der ein Raised-Cosine-Filter (RC) in zwei Root-Raised-Cosine-Filter (RRC) aufgeteilt wird; der auf der Sendeseite ist der Impulsformfilter, und der auf der Empfangsseite ist das Matched Filter. Zusammen bewirken sie, dass die Impulse am Demodulator so aussehen, als wären sie mit einem einzigen RRC-Filter impulsgeformt worden. + +.. image:: ../_images/splitting_rc_filter.svg + :align: center + :target: ../_images/splitting_rc_filter.svg + :alt: Diagramm einer Sende- und Empfangskette mit einem Raised-Cosine-Filter (RC), der in zwei Root-Raised-Cosine-Filter (RRC) aufgeteilt wird + +********************************** +Spezifische Impulsformfilter +********************************** + +Wir wissen, dass wir Folgendes möchten: + +1. Einen Filter entwerfen, der die Bandbreite unseres Signals reduziert (um weniger Spektrum zu verwenden), und alle Impulse außer einem sollten in jedem Symbolintervall zu null summieren. + +2. Den Filter halbieren, eine Hälfte in Tx und die andere in Rx platzieren. + +Schauen wir uns einige spezifische Filter an, die häufig zur Impulsformung verwendet werden. + +Raised-Cosine-Filter +######################### + +Der beliebteste Impulsformfilter scheint der "Raised-Cosine"-Filter zu sein. Es ist ein guter Tiefpassfilter zur Begrenzung der Bandbreite unseres Signals, und er hat die Eigenschaft, in Intervallen von :math:`T` zu null zu summieren: + +.. image:: ../_images/raised_cosine.svg + :align: center + :target: ../_images/raised_cosine.svg + :alt: Der Raised-Cosine-Filter im Zeitbereich mit verschiedenen Roll-Off-Werten + +Beachte, dass obiger Plot im Zeitbereich ist. Er zeigt die Impulsantwort des Filters. Der Parameter :math:`\beta` ist der einzige Parameter des Raised-Cosine-Filters und bestimmt, wie schnell der Filter im Zeitbereich abfällt, was umgekehrt proportional dazu ist, wie schnell er in der Frequenz abfällt: + +.. image:: ../_images/raised_cosine_freq.svg + :align: center + :target: ../_images/raised_cosine_freq.svg + :alt: Der Raised-Cosine-Filter im Frequenzbereich mit verschiedenen Roll-Off-Werten + +Der Grund, warum er Raised-Cosine-Filter heißt, ist, dass der Frequenzbereich bei :math:`\beta = 1` eine halbe Periode einer Kosinuswelle ist, die auf die x-Achse angehoben wird. + +Die Gleichung, die die Impulsantwort des Raised-Cosine-Filters definiert, lautet: + +.. math:: + h(t) = \mathrm{sinc}\left( \frac{t}{T} \right) \frac{\cos\left(\frac{\pi\beta t}{T}\right)}{1 - \left( \frac{2 \beta t}{T} \right)^2} + +Weitere Informationen zur :math:`\mathrm{sinc}()`-Funktion findest du `hier `_. Du findest möglicherweise anderswo Gleichungen, die einen zusätzlichen Skalierungsfaktor :math:`\frac{1}{T}` enthalten; dieser bewirkt, dass der Filter eine Einheitsverstärkung hat, sodass das Ausgangssignal dieselbe Leistung wie das Eingangssignal hat (eine gängige Praxis beim Filterentwurf im Allgemeinen). Wir wenden ihn jedoch auf einen Impulszug von Symbolen (z.B. 1er und -1er) an und möchten nicht, dass die Amplitude dieser Symbole nach der Impulsformung geändert wird, daher lassen wir den Skalierungsfaktor weg. Dies wird klarer, sobald wir uns in das Python-Beispiel vertiefen und die Ausgabe plotten. + +Denke daran: Wir teilen diesen Filter gleichmäßig zwischen Tx und Rx auf. Das führt uns zum Root-Raised-Cosine-Filter (RRC)! + +Root-Raised-Cosine-Filter +######################### + +Der Root-Raised-Cosine-Filter (RRC) ist das, was wir tatsächlich in unserem Tx und Rx implementieren. Zusammen bilden sie einen normalen Raised-Cosine-Filter, wie besprochen. Da das Halbieren eines Filters die Quadratwurzel im Frequenzbereich erfordert, wird die Impulsantwort etwas unübersichtlich: + +.. image:: ../_images/rrc_filter.png + :scale: 70 % + :align: center + +Glücklicherweise ist es ein häufig verwendeter Filter, und es gibt viele Implementierungen, darunter `in Python `_. + +Andere Impulsformfilter +########################### + +Andere Filter umfassen den Gaußschen Filter, dessen Impulsantwort einer Gaußschen Funktion ähnelt. Es gibt auch einen Sinc-Filter, der dem Raised-Cosine-Filter bei :math:`\beta = 0` entspricht. Der Sinc-Filter ist eher ein idealer Filter, was bedeutet, dass er die notwendigen Frequenzen ohne viel Übergangsbereich eliminiert. + +********************************** +Roll-Off-Faktor +********************************** + +Untersuchen wir den Parameter :math:`\beta`. Es ist eine Zahl zwischen 0 und 1 und wird als "Roll-Off"-Faktor oder manchmal als "Überschussbandbreite" bezeichnet. Er bestimmt, wie schnell der Filter im Zeitbereich auf null abfällt. Zur Erinnerung: Um als Filter verwendet zu werden, sollte die Impulsantwort auf beiden Seiten auf null abklingen: + +.. image:: ../_images/rrc_rolloff.svg + :align: center + :target: ../_images/rrc_rolloff.svg + :alt: Plot des Raised-Cosine Roll-Off-Parameters + +Je kleiner :math:`\beta` wird, desto mehr Filtertaps sind erforderlich. Wenn :math:`\beta = 0` ist, erreicht die Impulsantwort nie vollständig null, also versuchen wir, :math:`\beta` so klein wie möglich zu halten, ohne andere Probleme zu verursachen. Je kleiner der Roll-Off, desto kompakter können wir unser Signal für eine gegebene Symbolrate im Frequenzbereich erzeugen, was immer wichtig ist. + +Eine häufig verwendete Gleichung zur näherungsweisen Berechnung der Bandbreite in Hz für eine gegebene Symbolrate und einen Roll-Off-Faktor lautet: + +.. math:: + \mathrm{BW} = R_S(\beta + 1) + +:math:`R_S` ist die Symbolrate in Hz. In der drahtlosen Kommunikation bevorzugen wir üblicherweise einen Roll-Off zwischen 0,2 und 0,5. Als Faustregel gilt: Ein digitales Signal, das die Symbolrate :math:`R_S` verwendet, belegt etwas mehr als :math:`R_S` an Spektrum, einschließlich positiver und negativer Spektrumanteile. Sobald wir unser Signal hochkonvertieren und senden, sind beide Seiten relevant. Wenn wir QPSK mit 1 Million Symbolen pro Sekunde (MSps) senden, belegt es etwa 1,3 MHz. Die Datenrate beträgt 2 Mbps (QPSK verwendet 2 Bits pro Symbol), einschließlich Overhead wie Kanalcodierung und Frame-Header. + +********************************** +Python-Übung +********************************** + +Als Python-Übung filtern und formen wir einige Impulse. Wir verwenden BPSK-Symbole, damit es einfacher zu visualisieren ist – vor der Impulsformung sendet BPSK 1er oder -1er mit dem "Q"-Anteil gleich null. Mit Q gleich null können wir nur den I-Anteil plotten, was einfacher zu betrachten ist. + +In dieser Simulation verwenden wir 8 Abtastwerte pro Symbol, und anstelle eines rechteckwellenähnlichen Signals aus 1ern und -1ern verwenden wir einen Impulszug aus Dirac-Impulsen. Wenn du einen Impuls durch einen Filter schickst, ist die Ausgabe die Impulsantwort (daher der Name). Wenn du also eine Reihe von Impulsen möchtest, verwende Dirac-Impulse mit Nullen dazwischen, um rechteckige Impulse zu vermeiden. + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + from scipy import signal + + num_symbols = 10 + sps = 8 + + bits = np.random.randint(0, 2, num_symbols) # Zu übertragende Daten, 1er und 0er + + x = np.array([]) + for bit in bits: + pulse = np.zeros(sps) + pulse[0] = bit*2-1 # ersten Wert auf 1 oder -1 setzen + x = np.concatenate((x, pulse)) # die 8 Abtastwerte zum Signal hinzufügen + plt.figure(0) + plt.plot(x, '.-') + plt.grid(True) + plt.show() + +.. image:: ../_images/pulse_shaping_python1.png + :scale: 80 % + :align: center + :alt: Ein Impulszug aus Dirac-Impulsen im Zeitbereich, simuliert in Python + +Zu diesem Zeitpunkt sind unsere Symbole noch 1er und -1er. Lass dich nicht von der Verwendung der Dirac-Impulse verwirren. Tatsächlich könnte es einfacher sein, die Impulsantwort *nicht* zu visualisieren, sondern es als Array zu betrachten: + +.. code-block:: python + + bits: [0, 1, 1, 1, 1, 0, 0, 0, 1, 1] + BPSK-Symbole: [-1, 1, 1, 1, 1, -1, -1, -1, 1, 1] + Mit 8 Abtastwerten pro Symbol: [-1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, ...] + +Wir erstellen einen Raised-Cosine-Filter mit einem :math:`\beta` von 0,35, und wir machen ihn 101 Taps lang, damit das Signal genug Zeit hat, auf null abzuklingen. Obwohl die Raised-Cosine-Gleichung nach unserer Symbolperiode und einem Zeitvektor :math:`t` fragt, können wir eine **Abtast**periode von 1 Sekunde annehmen, um unsere Simulation zu "normalisieren". Das bedeutet, unsere Symbolperiode :math:`Ts` ist 8, weil wir 8 Abtastwerte pro Symbol haben. Unser Zeitvektor ist dann eine Liste von ganzen Zahlen. Mit der Art, wie die Raised-Cosine-Gleichung funktioniert, möchten wir, dass :math:`t=0` in der Mitte liegt. Wir erzeugen den 101-langen Zeitvektor, der bei -51 beginnt und bei +51 endet. + +.. code-block:: python + + # Raised-Cosine-Filter erstellen + num_taps = 101 + beta = 0.35 + Ts = sps # Abtastrate als 1 Hz angenommen, Abtastperiode ist 1, also Symbolperiode ist 8 + t = np.arange(num_taps) - (num_taps-1)//2 + h = np.sinc(t/Ts) * np.cos(np.pi*beta*t/Ts) / (1 - (2*beta*t/Ts)**2) + plt.figure(1) + plt.plot(t, h, '.') + plt.grid(True) + plt.show() + + +.. image:: ../_images/pulse_shaping_python2.png + :scale: 80 % + :align: center + +Beachte, dass die Ausgabe definitiv auf null abklingt. Die Tatsache, dass wir 8 Abtastwerte pro Symbol verwenden, bestimmt, wie schmal dieser Filter erscheint und wie schnell er auf null abfällt. Die obige Impulsantwort sieht wie ein typischer Tiefpassfilter aus, und es gibt keine Möglichkeit für uns zu erkennen, dass es sich um einen impulsformspezifischen Filter im Vergleich zu einem anderen Tiefpassfilter handelt. + +Schließlich können wir unser Signal :math:`x` filtern und das Ergebnis untersuchen. Konzentriere dich nicht zu sehr auf die Einführung einer for-Schleife im bereitgestellten Code. Wir werden nach dem Codeblock erklären, warum sie da ist. + +.. code-block:: python + + # Signal filtern, um die Impulsformung anzuwenden + x_shaped = np.convolve(x, h) + plt.figure(2) + plt.plot(x_shaped, '.-') + for i in range(num_symbols): + plt.plot([i*sps+num_taps//2,i*sps+num_taps//2], [0, x_shaped[i*sps+num_taps//2]]) + plt.grid(True) + plt.show() + +.. image:: ../_images/pulse_shaping_python3.svg + :align: center + :target: ../_images/pulse_shaping_python3.svg + +Das resultierende Signal ist aus vielen unserer Impulsantworten summiert, wobei ungefähr die Hälfte zunächst mit -1 multipliziert wurde. Es mag kompliziert aussehen, aber wir werden es gemeinsam durchgehen. + +Zunächst gibt es transiente Abtastwerte vor und nach den Daten, die durch den Filter und die Funktionsweise der Faltung entstehen. Diese zusätzlichen Abtastwerte werden in unsere Übertragung aufgenommen, enthalten aber keine "Peaks" von Impulsen. + +Zweitens wurden die vertikalen Linien in der for-Schleife zur Visualisierung erstellt. Sie sollen zeigen, wo Intervalle von :math:`Ts` auftreten. Diese Intervalle repräsentieren, wo dieses Signal vom Empfänger abgetastet wird. Beachte, dass die Kurve bei Intervallen von :math:`Ts` genau den Wert 1,0 oder -1,0 hat, was sie zu den idealen Zeitpunkten zum Abtasten macht. + +Wenn wir dieses Signal hochkonvertieren und senden würden, müsste der Empfänger bestimmen, wo die Grenzen von :math:`Ts` liegen – z.B. mit einem Symbolsynchronisierungsalgorithmus. Auf diese Weise weiß der Empfänger *genau*, wann er abtasten soll, um die richtigen Daten zu erhalten. Wenn der Empfänger etwas zu früh oder zu spät abtastet, sieht er Werte, die aufgrund von ISI leicht verzerrt sind, und wenn er weit daneben liegt, erhält er eine Menge merkwürdiger Zahlen. + +Hier ist ein Beispiel, das mit GNU Radio erstellt wurde und zeigt, wie der IQ-Plot (a.k.a. Konstellation) aussieht, wenn wir zum richtigen und falschen Zeitpunkt abtasten. Die Bitwerte der ursprünglichen Impulse sind annotiert. + +.. image:: ../_images/symbol_sync1.png + :scale: 50 % + :align: center + +Das folgende Diagramm zeigt die ideale Abtastposition in der Zeit sowie den IQ-Plot: + +.. image:: ../_images/symbol_sync2.png + :scale: 40 % + :align: center + :alt: GNU Radio-Simulation mit perfektem Timing beim Abtasten + +Vergleiche das mit dem schlechtesten Abtastzeitpunkt. Beachte die drei Cluster in der Konstellation. Wir tasten direkt zwischen jedem Symbol ab; unsere Abtastwerte werden völlig daneben liegen. + +.. image:: ../_images/symbol_sync3.png + :scale: 40 % + :align: center + :alt: GNU Radio-Simulation mit unvollkommenem Timing beim Abtasten + +Hier ist ein weiteres Beispiel für eine schlechte Abtastzeit, irgendwo zwischen unserem idealen und schlechtesten Fall. Beachte die vier Cluster. Bei hohem SNR könnten wir mit diesem Abtastzeitintervall durchkommen, obwohl es nicht empfehlenswert ist. + +.. image:: ../_images/symbol_sync4.png + :scale: 40 % + :align: center + +Denke daran, dass unsere Q-Werte nicht im Zeitbereichsplot gezeigt werden, weil sie ungefähr null sind, sodass sich die IQ-Plots nur horizontal ausbreiten. diff --git a/content-de/pyqt.rst b/content-de/pyqt.rst new file mode 100644 index 00000000..17944e8b --- /dev/null +++ b/content-de/pyqt.rst @@ -0,0 +1,881 @@ +.. _pyqt-chapter: + +########################## +Echtzeit-GUIs mit PyQt +########################## + +In diesem Kapitel lernst du, wie du mit Python mithilfe von PyQt, den Python-Bindings für Qt, Echtzeit-Benutzeroberflächen (GUIs) erstellst. Im Rahmen dieses Kapitels bauen wir einen Spektrumanalysator mit Zeit-, Frequenz- und Spektrogramm/Wasserfall-Darstellungen sowie Eingabe-Widgets zum Einstellen der verschiedenen SDR-Parameter. Das Beispiel unterstützt PlutoSDR, USRP oder einen reinen Simulationsmodus. + +**************** +Einführung +**************** + +Qt (ausgesprochen „Cute") ist ein Framework zum Erstellen von GUI-Anwendungen, die unter Linux, Windows, macOS und sogar Android laufen können. Es ist ein sehr leistungsfähiges Framework, das in vielen kommerziellen Anwendungen eingesetzt wird, und ist für maximale Performance in C++ geschrieben. PyQt sind die Python-Bindings für Qt und bieten eine Möglichkeit, GUI-Anwendungen in Python zu erstellen, während die Leistung des effizienten C++-basierten Frameworks genutzt wird. In diesem Kapitel lernst du, wie du mit PyQt einen Echtzeit-Spektrumanalysator erstellen kannst, der mit einem SDR (oder mit einem simulierten Signal) verwendet werden kann. Der Spektrumanalysator verfügt über Zeit-, Frequenz- und Spektrogramm/Wasserfall-Darstellungen sowie Eingabe-Widgets zum Einstellen der verschiedenen SDR-Parameter. Wir verwenden `PyQtGraph `_, eine separate Bibliothek, die auf PyQt aufbaut, für die Darstellungen. Auf der Eingabeseite verwenden wir Schieberegler, Dropdown-Menüs und Druckknöpfe. Das Beispiel unterstützt PlutoSDR, USRP oder reinen Simulationsmodus. Obwohl der Beispielcode PyQt6 verwendet, ist jede einzelne Zeile identisch mit PyQt5 (abgesehen vom :code:`import`), aus API-Perspektive hat sich zwischen den beiden Versionen sehr wenig geändert. Dieses Kapitel ist naturgemäß sehr Python-Code-lastig, da wir anhand von Beispielen erklären. Am Ende dieses Kapitels wirst du mit den Bausteinen vertraut sein, die du zur Erstellung deiner eigenen interaktiven SDR-Anwendung benötigst! + +**************** +Qt-Überblick +**************** + +Qt ist ein sehr großes Framework, und wir werden nur an der Oberfläche kratzen, was es leisten kann. Es gibt jedoch einige Schlüsselkonzepte, die wichtig sind, wenn man mit Qt/PyQt arbeitet: + +- **Widgets**: Widgets sind die Bausteine einer Qt-Anwendung und dienen zur Erstellung der GUI. Es gibt viele verschiedene Widget-Typen, darunter Schaltflächen, Schieberegler, Labels und Diagramme. Widgets können in Layouts angeordnet werden, die bestimmen, wie sie auf dem Bildschirm positioniert werden. + +- **Layouts**: Layouts dienen zur Anordnung von Widgets in einem Fenster. Es gibt verschiedene Layout-Typen, darunter horizontale, vertikale, Gitter- und Formularlayouts. Layouts werden verwendet, um komplexe GUIs zu erstellen, die auf Änderungen der Fenstergröße reagieren. + +- **Signals und Slots**: Signals und Slots sind eine Möglichkeit, zwischen verschiedenen Teilen einer Qt-Anwendung zu kommunizieren. Ein Signal wird von einem Objekt ausgegeben, wenn ein bestimmtes Ereignis eintritt, und mit einem Slot verbunden, der eine Callback-Funktion ist, die aufgerufen wird, wenn das Signal ausgesendet wird. Signals und Slots werden verwendet, um eine ereignisgesteuerte Struktur in einer Qt-Anwendung zu erzeugen und die GUI reaktionsfähig zu halten. + +- **Style Sheets**: Style Sheets werden verwendet, um das Erscheinungsbild von Widgets in einer Qt-Anwendung anzupassen. Style Sheets werden in einer CSS-ähnlichen Sprache geschrieben und können verwendet werden, um Farbe, Schriftart und Größe von Widgets zu ändern. + +- **Grafiken**: Qt verfügt über ein leistungsstarkes Grafik-Framework, das zur Erstellung benutzerdefinierter Grafiken in einer Qt-Anwendung verwendet werden kann. Das Grafik-Framework enthält Klassen zum Zeichnen von Linien, Rechtecken, Ellipsen und Text sowie Klassen zur Behandlung von Maus- und Tastaturereignissen. + +- **Multithreading**: Qt hat eingebaute Unterstützung für Multithreading und stellt Klassen für die Erstellung von Worker-Threads bereit, die im Hintergrund laufen. Multithreading wird verwendet, um langwierige Operationen in einer Qt-Anwendung auszuführen, ohne den Haupt-GUI-Thread zu blockieren. + +- **OpenGL**: Qt hat eingebaute Unterstützung für OpenGL und stellt Klassen für die Erstellung von 3D-Grafiken in einer Qt-Anwendung bereit. OpenGL wird für Anwendungen verwendet, die leistungsstarke 3D-Grafiken erfordern. In diesem Kapitel konzentrieren wir uns ausschließlich auf 2D-Anwendungen. + +************************* +Grundlegendes App-Layout +************************* + +Bevor wir uns mit den verschiedenen Qt-Widgets befassen, schauen wir uns das Layout einer typischen Qt-Anwendung an. Eine Qt-Anwendung besteht aus einem Hauptfenster, das ein zentrales Widget enthält, das wiederum den Hauptinhalt der Anwendung enthält. Mit PyQt können wir eine minimale Qt-Anwendung erstellen, die nur einen einzigen QPushButton enthält: + +.. code-block:: python + + from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton + + # QMainWindow ableiten, um das Hauptfenster der Anwendung anzupassen + class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + + # Beispiel-GUI-Komponente + example_button = QPushButton('Push Me') + def on_button_click(): + print("beep") + example_button.clicked.connect(on_button_click) + + self.setCentralWidget(example_button) + + app = QApplication([]) + window = MainWindow() + window.show() # Fenster sind standardmäßig ausgeblendet + app.exec() # Ereignisschleife starten + +Probiere den Code selbst aus – du wirst wahrscheinlich :code:`pip install PyQt6` ausführen müssen. Beachte, dass die allerletzte Zeile blockierend ist – alles, was du nach dieser Zeile hinzufügst, wird erst ausgeführt, wenn du das Fenster schließt. Der QPushButton, den wir erstellen, hat sein :code:`clicked`-Signal mit einer Callback-Funktion verbunden, die „beep" auf der Konsole ausgibt. + +******************************* +Anwendung mit Worker-Thread +******************************* + +Das minimale Beispiel oben hat ein Problem: Es lässt uns keinen Platz für SDR/DSP-orientierten Code. Das :code:`__init__` von :code:`MainWindow` ist der Ort, an dem die GUI konfiguriert und Callbacks definiert werden, aber du möchtest auf keinen Fall anderen Code (wie SDR- oder DSP-Code) dort hinzufügen. Der Grund ist, dass die GUI single-threaded ist, und wenn du den GUI-Thread mit langwierigem Code blockierst, friert die GUI ein oder ruckelt – und wir wollen eine möglichst flüssige GUI. Um das zu umgehen, können wir einen Worker-Thread verwenden, um den SDR/DSP-Code im Hintergrund auszuführen. + +Das folgende Beispiel erweitert das minimale Beispiel um einen Worker-Thread, der Code (in der :code:`run`-Funktion) ununterbrochen ausführt. Wir verwenden jedoch kein :code:`while True:`, weil wir aufgrund der Art und Weise, wie PyQt intern funktioniert, wollen, dass unsere :code:`run`-Funktion periodisch beendet wird und von vorn beginnt. Um dies zu erreichen, wird das :code:`end_of_run`-Signal des Worker-Threads (das wir im nächsten Abschnitt näher besprechen) mit einer Callback-Funktion verbunden, die die :code:`run`-Funktion des Worker-Threads erneut auslöst. Wir müssen den Worker-Thread auch im :code:`MainWindow`-Code initialisieren, was die Erstellung eines neuen :code:`QThread` und die Zuweisung unseres benutzerdefinierten Workers dazu umfasst. Dieser Code mag kompliziert erscheinen, aber es ist ein sehr häufiges Muster in PyQt-Anwendungen. Das Wichtigste dabei: GUI-orientierter Code gehört in :code:`MainWindow`, SDR/DSP-orientierter Code in die :code:`run`-Funktion des Worker-Threads. + +.. code-block:: python + + from PyQt6.QtCore import QThread, pyqtSignal, QObject, QTimer + from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton + import time + + # Nicht-GUI-Operationen (einschließlich SDR) müssen in einem separaten Thread laufen + class SDRWorker(QObject): + end_of_run = pyqtSignal() + + # Hauptschleife + def run(self): + print("Starting run()") + time.sleep(1) + self.end_of_run.emit() # MainWindow mitteilen, dass wir fertig sind + + # QMainWindow ableiten, um das Hauptfenster der Anwendung anzupassen + class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + + # Worker und Thread initialisieren + self.sdr_thread = QThread() + worker = SDRWorker() + worker.moveToThread(self.sdr_thread) + + # Beispiel-GUI-Komponente + example_button = QPushButton('Push Me') + def on_button_click(): + print("beep") + example_button.clicked.connect(on_button_click) + self.setCentralWidget(example_button) + + # Das lässt die run()-Funktion ununterbrochen wiederholen + def end_of_run_callback(): + QTimer.singleShot(0, worker.run) # Worker sofort erneut ausführen + worker.end_of_run.connect(end_of_run_callback) + + self.sdr_thread.started.connect(worker.run) # startet den ersten run()-Aufruf, wenn der Thread beginnt + self.sdr_thread.start() # Thread starten + + app = QApplication([]) + window = MainWindow() + window.show() # Fenster sind standardmäßig ausgeblendet + app.exec() # Ereignisschleife starten + +Führe den obigen Code aus – du solltest jede Sekunde ein „Starting run()" in der Konsole sehen, und der Druckknopf sollte weiterhin funktionieren (ohne jede Verzögerung). Im Worker-Thread machen wir jetzt nur einen Print und einen Sleep, aber bald werden wir die SDR-Verarbeitung und den DSP-Code dort hinzufügen. + +************************* +Signals und Slots +************************* + +Im obigen Beispiel haben wir das :code:`end_of_run`-Signal verwendet, um zwischen dem Worker-Thread und dem GUI-Thread zu kommunizieren. Dies ist ein häufiges Muster in PyQt-Anwendungen und wird als „Signals and Slots"-Mechanismus bezeichnet. Ein Signal wird von einem Objekt ausgesendet (in diesem Fall dem Worker-Thread) und mit einem Slot verbunden (in diesem Fall der Callback-Funktion :code:`end_of_run_callback` im GUI-Thread). Das Signal kann mit mehreren Slots verbunden werden, und der Slot kann mit mehreren Signals verbunden werden. Das Signal kann auch Argumente tragen, die an den Slot übergeben werden, wenn das Signal ausgesendet wird. Beachte, dass wir die Dinge auch umkehren können: Der GUI-Thread kann ein Signal an den Slot des Worker-Threads senden. Der Signal/Slot-Mechanismus ist eine leistungsstarke Möglichkeit, zwischen verschiedenen Teilen einer PyQt-Anwendung zu kommunizieren, eine ereignisgesteuerte Struktur zu erzeugen, und wird im folgenden Beispielcode ausgiebig genutzt. Denk einfach daran: Ein Slot ist lediglich eine Callback-Funktion, und ein Signal ist eine Möglichkeit, diese Callback-Funktion zu signalisieren. + +************************* +PyQtGraph +************************* + +PyQtGraph ist eine Bibliothek, die auf PyQt und NumPy aufbaut und schnelle und effiziente Darstellungsfähigkeiten bietet, da PyQt zu allgemein gehalten ist, um Darstellungsfunktionen mitzuliefern. Sie ist für den Einsatz in Echtzeitanwendungen konzipiert und auf Geschwindigkeit optimiert. Sie ähnelt in vieler Hinsicht Matplotlib, ist aber für Echtzeitanwendungen statt für Einzeldarstellungen gedacht. Mit dem einfachen Beispiel unten kannst du die Leistung von PyQtGraph mit Matplotlib vergleichen – ändere einfach :code:`if True:` zu :code:`False:`. Auf einem Intel Core i9-10900K @ 3,70 GHz aktualisierte der PyQtGraph-Code mit über 1000 FPS, während der Matplotlib-Code mit 40 FPS aktualisierte. Wenn du jedoch von Matplotlib profitierst (z. B. um Entwicklungszeit zu sparen oder weil du ein bestimmtes Feature möchtest, das PyQtGraph nicht unterstützt), kannst du Matplotlib-Diagramme in eine PyQt-Anwendung einbinden und den folgenden Code als Ausgangspunkt verwenden. + +.. raw:: html + +
+ Expand for comparison code + +.. code-block:: python + + import numpy as np + import time + import matplotlib + matplotlib.use('Qt5Agg') + from PyQt6 import QtCore, QtWidgets + from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas + from matplotlib.figure import Figure + import pyqtgraph as pg # tested with pyqtgraph==0.13.7 + + n_data = 1024 + + if True: + class MplCanvas(FigureCanvas): + def __init__(self): + fig = Figure(figsize=(13, 8), dpi=100) + self.axes = fig.add_subplot(111) + super(MplCanvas, self).__init__(fig) + + + class MainWindow(QtWidgets.QMainWindow): + def __init__(self): + super(MainWindow, self).__init__() + + self.canvas = MplCanvas() + self._plot_ref = self.canvas.axes.plot(np.arange(n_data), '.-r')[0] + self.canvas.axes.set_xlim(0, n_data) + self.canvas.axes.set_ylim(-5, 5) + self.canvas.axes.grid(True) + self.setCentralWidget(self.canvas) + + # Timer einrichten, der die Neuzeichnung durch Aufruf von update_plot auslöst + self.timer = QtCore.QTimer() + self.timer.setInterval(0) # Timer sofort starten + self.timer.timeout.connect(self.update_plot) # Timer startet sich automatisch neu + self.timer.start() + self.start_t = time.time() # für Benchmarking + + self.show() + + def update_plot(self): + self._plot_ref.set_ydata(np.random.randn(n_data)) + self.canvas.draw() # Canvas zum Aktualisieren und Neuzeichnen auslösen + print('FPS:', 1/(time.time()-self.start_t)) # ~42 FPS auf einem i9-10900K + self.start_t = time.time() + + else: + class MainWindow(QtWidgets.QMainWindow): + def __init__(self): + super(MainWindow, self).__init__() + + self.time_plot = pg.PlotWidget() + self.time_plot.setYRange(-5, 5) + self.time_plot_curve = self.time_plot.plot([]) + self.setCentralWidget(self.time_plot) + + # Timer einrichten, der die Neuzeichnung durch Aufruf von update_plot auslöst + self.timer = QtCore.QTimer() + self.timer.setInterval(0) # Timer sofort starten + self.timer.timeout.connect(self.update_plot) # Timer startet sich automatisch neu + self.timer.start() + self.start_t = time.time() # für Benchmarking + + self.show() + + def update_plot(self): + self.time_plot_curve.setData(np.random.randn(n_data)) + print('FPS:', 1/(time.time()-self.start_t)) # ~42 FPS auf einem i9-10900K + self.start_t = time.time() + + app = QtWidgets.QApplication([]) + w = MainWindow() + app.exec() + +.. raw:: html + +
+ +Was die Verwendung von PyQtGraph betrifft, importieren wir es mit :code:`import pyqtgraph as pg` und können dann ein Qt-Widget für ein 1D-Diagramm wie folgt erstellen (dieser Code gehört in das :code:`__init__` von :code:`MainWindow`): + +.. code-block:: python + + # Beispiel-PyQtGraph-Plot + time_plot = pg.PlotWidget(labels={'left': 'Amplitude', 'bottom': 'Time'}) + time_plot_curve = time_plot.plot(np.arange(1000), np.random.randn(1000)) # x und y + time_plot.setYRange(-5, 5) + + self.setCentralWidget(time_plot) + +.. image:: ../_images/pyqtgraph_example.png + :scale: 80 % + :align: center + :alt: PyQtGraph example + +Man sieht, wie unkompliziert es ist, ein Diagramm einzurichten, und das Ergebnis ist einfach ein weiteres Widget, das du deiner GUI hinzufügen kannst. Neben 1D-Diagrammen hat PyQtGraph auch ein Äquivalent zu Matplotlibs :code:`imshow()`, das 2D mithilfe einer Farbpalette darstellt – das werden wir für unser Echtzeit-Spektrogramm/Wasserfall verwenden. Ein schöner Aspekt von PyQtGraph ist, dass die erstellten Diagramme einfach Qt-Widgets sind, und wir können andere Qt-Elemente (z. B. ein Rechteck einer bestimmten Größe an einer bestimmten Koordinate) mit purem PyQt hinzufügen. Das liegt daran, dass PyQtGraph die :code:`QGraphicsScene`-Klasse von PyQt nutzt, die eine Oberfläche zur Verwaltung einer großen Anzahl von 2D-Grafikelementen bietet, und nichts hindert uns daran, Linien, Rechtecke, Text, Ellipsen, Polygone und Bitmaps mit reinem PyQt hinzuzufügen. + +******* +Layouts +******* + +In den obigen Beispielen haben wir :code:`self.setCentralWidget()` verwendet, um das Haupt-Widget des Fensters festzulegen. Das ist eine einfache Methode, erlaubt aber keine komplexeren Layouts. Für komplexere Layouts können wir Layouts verwenden, die eine Möglichkeit sind, Widgets in einem Fenster anzuordnen. Es gibt verschiedene Layout-Typen, darunter :code:`QHBoxLayout`, :code:`QVBoxLayout`, :code:`QGridLayout` und :code:`QFormLayout`. :code:`QHBoxLayout` und :code:`QVBoxLayout` ordnen Widgets horizontal bzw. vertikal an. :code:`QGridLayout` ordnet Widgets in einem Raster an, und :code:`QFormLayout` ordnet Widgets in einem zweispaltigen Layout an, mit Labels in der ersten und Eingabe-Widgets in der zweiten Spalte. + +Um ein neues Layout zu erstellen und Widgets hinzuzufügen, füge folgendes in das :code:`__init__` deines :code:`MainWindow` ein: + +.. code-block:: python + + layout = QHBoxLayout() + layout.addWidget(QPushButton("Left-Most")) + layout.addWidget(QPushButton("Center"), 1) + layout.addWidget(QPushButton("Right-Most"), 2) + self.setLayout(layout) + +In diesem Beispiel stapeln wir die Widgets horizontal; durch Ersetzen von :code:`QHBoxLayout` durch :code:`QVBoxLayout` können wir sie stattdessen vertikal stapeln. Die Funktion :code:`addWidget` wird verwendet, um Widgets zum Layout hinzuzufügen, und das optionale zweite Argument ist ein Dehnungsfaktor, der bestimmt, wie viel Platz das Widget im Verhältnis zu den anderen Widgets im Layout einnehmen soll. + +:code:`QGridLayout` hat zusätzliche Parameter, weil du Zeile und Spalte des Widgets angeben musst, und optional, wie viele Zeilen und Spalten das Widget überspannen soll (Standard ist jeweils 1). Hier ist ein Beispiel für ein :code:`QGridLayout`: + +.. code-block:: python + + layout = QGridLayout() + layout.addWidget(QPushButton("Button at (0, 0)"), 0, 0) + layout.addWidget(QPushButton("Button at (0, 1)"), 0, 1) + layout.addWidget(QPushButton("Button at (0, 2)"), 0, 2) + layout.addWidget(QPushButton("Button at (1, 0)"), 1, 0) + layout.addWidget(QPushButton("Button at (1, 1)"), 1, 1) + layout.addWidget(QPushButton("Button at (1, 2)"), 1, 2) + layout.addWidget(QPushButton("Button at (2, 0) spanning 2 columns"), 2, 0, 1, 2) + self.setLayout(layout) + +.. image:: ../_images/qt_layouts.svg + :align: center + :target: ../_images/qt_layouts.svg + :alt: Qt Layouts showing examples of QHBoxLayout, QVBoxLayout, and QGridLayout + +Für unseren Spektrumanalysator verwenden wir :code:`QGridLayout` für das Gesamtlayout, aber wir werden auch :code:`QHBoxLayout` hinzufügen, um Widgets innerhalb eines Gitterplatzes horizontal zu stapeln. Layouts können einfach verschachtelt werden, indem ein neues Layout erstellt und dem übergeordneten Layout hinzugefügt wird: + +.. code-block:: python + + layout = QGridLayout() + self.setLayout(layout) + inner_layout = QHBoxLayout() + layout.addLayout(inner_layout) + +******************* +:code:`QPushButton` +******************* + +Das erste eigentliche Widget, das wir besprechen, ist der :code:`QPushButton`, eine einfache Schaltfläche, die geklickt werden kann. Wir haben bereits gesehen, wie man einen :code:`QPushButton` erstellt und sein :code:`clicked`-Signal mit einer Callback-Funktion verbindet. Der :code:`QPushButton` hat einige weitere Signals, darunter :code:`pressed`, :code:`released` und :code:`toggled`. Das :code:`toggled`-Signal wird ausgesendet, wenn die Schaltfläche aktiviert oder deaktiviert wird, und ist nützlich zum Erstellen von Umschaltflächen. Der :code:`QPushButton` hat auch einige Eigenschaften, darunter :code:`text`, :code:`icon` und :code:`checkable`, sowie eine Methode namens :code:`click()`, die einen Klick auf die Schaltfläche simuliert. In unserem SDR-Spektrumanalysator verwenden wir Schaltflächen, um einen Auto-Range für Diagramme auszulösen, wobei die aktuellen Daten zur Berechnung der y-Grenzen verwendet werden. Da wir den :code:`QPushButton` bereits verwendet haben, gehen wir hier nicht weiter ins Detail, aber weitere Informationen findest du in der `QPushButton-Dokumentation `_. + +*************** +:code:`QSlider` +*************** + +Der :code:`QSlider` ist ein Widget, mit dem der Benutzer einen Wert aus einem Wertebereich auswählen kann. Der :code:`QSlider` hat einige Eigenschaften, darunter :code:`minimum`, :code:`maximum`, :code:`value` und :code:`orientation`. Er hat auch einige Signals, darunter :code:`valueChanged`, :code:`sliderPressed` und :code:`sliderReleased`, sowie eine Methode :code:`setValue()`, die den Wert des Schiebereglers setzt – diese werden wir häufig verwenden. Die Dokumentationsseite für `QSlider ist hier `_. + +In unserem Spektrumanalysator verwenden wir :code:`QSlider` zur Einstellung der Mittenfrequenz und der Verstärkung des SDR. Hier ist der Ausschnitt aus dem endgültigen Anwendungscode, der den Verstärkungs-Schieberegler erstellt: + +.. code-block:: python + + # Verstärkungs-Schieberegler mit Label + gain_slider = QSlider(Qt.Orientation.Horizontal) + gain_slider.setRange(0, 73) # Min und Max, inklusiv. Schrittweite ist immer 1 + gain_slider.setValue(50) # Anfangswert + gain_slider.setTickPosition(QSlider.TickPosition.TicksBelow) + gain_slider.setTickInterval(2) # nur für visuelle Darstellung + gain_slider.sliderMoved.connect(worker.update_gain) + gain_label = QLabel() + def update_gain_label(val): + gain_label.setText("Gain: " + str(val)) + gain_slider.sliderMoved.connect(update_gain_label) + update_gain_label(gain_slider.value()) # Label initialisieren + layout.addWidget(gain_slider, 5, 0) + layout.addWidget(gain_label, 5, 1) + +Sehr wichtig zu wissen: :code:`QSlider` verwendet Integer-Werte. Durch Setzen des Bereichs von 0 bis 73 erlauben wir dem Schieberegler, ganzzahlige Werte zwischen diesen Zahlen (einschließlich Start und Ende) auszuwählen. Das :code:`setTickInterval(2)` ist rein visuell. Aus diesem Grund verwenden wir kHz als Einheit für den Frequenz-Schieberegler, damit wir eine Auflösung bis auf 1 kHz erhalten. + +In der Mitte des obigen Codes erstellen wir ein :code:`QLabel`, das einfach ein Text-Label zur Anzeige ist. Damit es den aktuellen Wert des Schiebereglers anzeigt, müssen wir eine Callback-Funktion (also einen Slot) erstellen, die das Label aktualisiert. Wir verbinden diese Callback-Funktion mit dem :code:`sliderMoved`-Signal, das automatisch ausgesendet wird, wenn der Schieberegler bewegt wird. Wir rufen die Callback-Funktion auch einmal auf, um das Label mit dem aktuellen Wert des Schiebereglers zu initialisieren (in unserem Fall 50). Außerdem müssen wir das :code:`sliderMoved`-Signal mit einem Slot im Worker-Thread verbinden, der die Verstärkung des SDR aktualisiert (denk daran: SDR und DSP gehören nicht in den Haupt-GUI-Thread). Die Callback-Funktion, die diesen Slot definiert, wird später besprochen. + +***************** +:code:`QComboBox` +***************** + +Die :code:`QComboBox` ist ein Dropdown-Widget, mit dem der Benutzer ein Element aus einer Liste von Elementen auswählen kann. Die :code:`QComboBox` hat einige Eigenschaften, darunter :code:`currentText`, :code:`currentIndex` und :code:`count`. Sie hat auch einige Signals, darunter :code:`currentTextChanged`, :code:`currentIndexChanged` und :code:`activated`, sowie Methoden wie :code:`addItem()` zum Hinzufügen eines Elements zur Liste und :code:`insertItem()` zum Einfügen eines Elements an einem bestimmten Index – diese werden wir in unserem Spektrumanalysator-Beispiel aber nicht verwenden. Die Dokumentationsseite für `QComboBox ist hier `_. + +In unserem Spektrumanalysator verwenden wir :code:`QComboBox`, um die Abtastrate aus einer vordefinierten Liste auszuwählen. Am Anfang unseres Codes definieren wir die möglichen Abtastraten mit :code:`sample_rates = [56, 40, 20, 10, 5, 2, 1, 0.5]`. Im :code:`__init__` von :code:`MainWindow` erstellen wir die :code:`QComboBox` wie folgt: + +.. code-block:: python + + # Abtastraten-Dropdown mit QComboBox + sample_rate_combobox = QComboBox() + sample_rate_combobox.addItems([str(x) + ' MHz' for x in sample_rates]) + sample_rate_combobox.setCurrentIndex(0) # Index angeben, nicht String + sample_rate_combobox.currentIndexChanged.connect(worker.update_sample_rate) + sample_rate_label = QLabel() + def update_sample_rate_label(val): + sample_rate_label.setText("Sample Rate: " + str(sample_rates[val]) + " MHz") + sample_rate_combobox.currentIndexChanged.connect(update_sample_rate_label) + update_sample_rate_label(sample_rate_combobox.currentIndex()) # Label initialisieren + layout.addWidget(sample_rate_combobox, 6, 0) + layout.addWidget(sample_rate_label, 6, 1) + +Der einzige wirkliche Unterschied zum Schieberegler ist :code:`addItems()`, dem du die Liste der Strings als Optionen übergibst, und :code:`setCurrentIndex()`, das den Startwert festlegt. + +**************** +Lambda-Funktionen +**************** + +Erinnere dich an den obigen Code, in dem wir Folgendes geschrieben haben: + +.. code-block:: python + + def update_sample_rate_label(val): + sample_rate_label.setText("Sample Rate: " + str(sample_rates[val]) + " MHz") + sample_rate_combobox.currentIndexChanged.connect(update_sample_rate_label) + +Wir erstellen eine Funktion mit nur einer einzigen Codezeile im Inneren und übergeben dann diese Funktion (Funktionen sind auch Objekte!) an :code:`connect()`. Um dies zu vereinfachen, schreiben wir dieses Code-Muster mit einfachem Python um: + +.. code-block:: python + + def my_function(x): + print(x) + y.call_that_takes_in_function_obj(my_function) + +In dieser Situation haben wir eine Funktion mit nur einer Codezeile im Inneren, und wir referenzieren diese Funktion nur einmal – beim Setzen des :code:`connect`-Callbacks. In solchen Situationen können wir eine Lambda-Funktion verwenden, eine Möglichkeit, eine Funktion in einer einzigen Zeile zu definieren. Hier ist der obige Code mit einer Lambda-Funktion umgeschrieben: + +.. code-block:: python + + y.call_that_takes_in_function_obj(lambda x: print(x)) + +Wenn du noch nie eine Lambda-Funktion verwendet hast, mag das seltsam wirken, und du musst sie nicht unbedingt verwenden – aber sie spart zwei Zeilen Code und macht den Code kompakter. Der Name des temporären Arguments kommt nach „lambda", und alles nach dem Doppelpunkt ist der Code, der auf diese Variable angewendet wird. Es werden auch mehrere Argumente mit Kommas unterstützt, oder sogar keine Argumente mit :code:`lambda : `. Versuche als Übung, die Funktion :code:`update_sample_rate_label` oben mit einer Lambda-Funktion umzuschreiben. + +*********************** +PyQtGraph's PlotWidget +*********************** + +PyQtGraphs :code:`PlotWidget` ist ein PyQt-Widget zur Erstellung von 1D-Diagrammen, ähnlich wie Matplotlibs :code:`plt.plot(x,y)`. Wir werden es für die Zeit- und Frequenz-PSD-Diagramme verwenden, obwohl es auch gut für IQ-Diagramme geeignet ist (die unser Spektrumanalysator nicht enthält). Für Interessierte: PlotWidget ist eine Unterklasse von PyQts `QGraphicsView `_, einem Widget zur Darstellung des Inhalts einer `QGraphicsScene `_, die eine Oberfläche zur Verwaltung einer großen Anzahl von 2D-Grafikelementen in Qt ist. Das Wichtigste über PlotWidget zu wissen ist, dass es einfach ein Widget ist, das ein einzelnes `PlotItem `_ enthält. Aus Dokumentationsperspektive solltest du daher direkt die PlotItem-Dokumentation konsultieren: ``_. Ein PlotItem enthält eine ViewBox zur Darstellung der darzustellenden Daten sowie AxisItems und Labels für Achsen und Titel. + +Das einfachste Beispiel für die Verwendung eines PlotWidget lautet wie folgt (das im :code:`__init__` von :code:`MainWindow` hinzugefügt werden muss): + +.. code-block:: python + + import pyqtgraph as pg + plotWidget = pg.plot(title="My Title") + plotWidget.plot(x, y) + +wobei x und y typischerweise NumPy-Arrays sind, genau wie bei Matplotlibs :code:`plt.plot()`. Dies stellt jedoch ein statisches Diagramm dar, bei dem sich die Daten nie ändern. Für unseren Spektrumanalysator wollen wir die Daten im Worker-Thread aktualisieren. Daher müssen wir beim Initialisieren des Diagramms noch keine Daten übergeben – wir müssen es nur einrichten. So initialisieren wir das Zeitbereich-Diagramm in unserem Spektrumanalysator: + +.. code-block:: python + + # Zeitbereich-Diagramm + time_plot = pg.PlotWidget(labels={'left': 'Amplitude', 'bottom': 'Time [microseconds]'}) + time_plot.setMouseEnabled(x=False, y=True) + time_plot.setYRange(-1.1, 1.1) + time_plot_curve_i = time_plot.plot([]) + time_plot_curve_q = time_plot.plot([]) + layout.addWidget(time_plot, 1, 0) + +Wir erstellen zwei verschiedene Kurven, eine für I und eine für Q. Der restliche Code erklärt sich von selbst. Um das Diagramm aktualisieren zu können, müssen wir im :code:`__init__` von :code:`MainWindow` einen Slot (eine Callback-Funktion) erstellen: + +.. code-block:: python + + def time_plot_callback(samples): + time_plot_curve_i.setData(samples.real) + time_plot_curve_q.setData(samples.imag) + +Wir verbinden diesen Slot mit dem Signal des Worker-Threads, das ausgesendet wird, wenn neue Samples verfügbar sind, wie später gezeigt. + +Als letztes fügen wir im :code:`__init__` von :code:`MainWindow` zwei Schaltflächen rechts neben dem Diagramm hinzu, die einen Auto-Range auslösen. Eine verwendet das aktuelle Min/Max, eine andere setzt den Bereich auf -1,1 bis 1,1 (entspricht den ADC-Grenzen vieler SDRs plus 10% Spielraum). Wir erstellen ein inneres Layout vom Typ QVBoxLayout, um diese zwei Schaltflächen vertikal zu stapeln. Hier ist der Code zum Hinzufügen der Schaltflächen: + +.. code-block:: python + + # Auto-Range-Schaltflächen für Zeitdiagramm + time_plot_auto_range_layout = QVBoxLayout() + layout.addLayout(time_plot_auto_range_layout, 1, 1) + auto_range_button = QPushButton('Auto Range') + auto_range_button.clicked.connect(lambda : time_plot.autoRange()) # lambda bedeutet unbenannte Funktion + time_plot_auto_range_layout.addWidget(auto_range_button) + auto_range_button2 = QPushButton('-1 to +1\n(ADC limits)') + auto_range_button2.clicked.connect(lambda : time_plot.setYRange(-1.1, 1.1)) + time_plot_auto_range_layout.addWidget(auto_range_button2) + +Und so sieht es am Ende aus: + +.. image:: ../_images/pyqt_time_plot.png + :scale: 50 % + :align: center + :alt: PyQtGraph Time Plot + +Für das Frequenzbereich-Diagramm (PSD) verwenden wir ein ähnliches Muster. + +********************* +PyQtGraph's ImageItem +********************* + +Ein Spektrumanalysator ist ohne ein Wasserfall-Diagramm (auch Echtzeit-Spektrogramm genannt) nicht vollständig. Dafür verwenden wir PyQtGraphs ImageItem, das Bilder mit 1, 3 oder 4 „Kanälen" rendert. Ein Kanal bedeutet, dass du ihm ein 2D-Array aus Floats oder Ints übergibst, das dann eine Lookup-Tabelle (LUT) nutzt, um eine Farbpalette anzuwenden und letztlich das Bild zu erstellen. Alternativ kannst du RGB (3 Kanäle) oder RGBA (4 Kanäle) übergeben. Wir berechnen unser Spektrogramm als 2D-NumPy-Array aus Floats und übergeben es direkt an das ImageItem. Wir wählen eine Farbpalette und nutzen sogar die eingebaute Funktionalität zur Anzeige einer grafischen LUT, die die Werteverteilung unserer Daten und die Anwendung der Farbpalette darstellt. + +Die eigentliche Initialisierung des Wasserfall-Diagramms ist recht unkompliziert: Wir verwenden ein PlotWidget als Container (damit Achsen angezeigt werden) und fügen dann ein ImageItem hinzu: + +.. code-block:: python + + # Wasserfall-Diagramm + waterfall = pg.PlotWidget(labels={'left': 'Time [s]', 'bottom': 'Frequency [MHz]'}) + imageitem = pg.ImageItem(axisOrder='col-major') # dieses Argument dient nur der Performance + waterfall.addItem(imageitem) + waterfall.setMouseEnabled(x=False, y=False) + waterfall_layout.addWidget(waterfall) + +Der Slot/Callback zum Aktualisieren der Wasserfall-Daten, der in das :code:`__init__` von :code:`MainWindow` gehört, sieht wie folgt aus: + +.. code-block:: python + + def waterfall_plot_callback(spectrogram): + imageitem.setImage(spectrogram, autoLevels=False) + sigma = np.std(spectrogram) + mean = np.mean(spectrogram) + self.spectrogram_min = mean - 2*sigma # im Fensterzustand speichern + self.spectrogram_max = mean + 2*sigma + +Dabei ist spectrogram ein 2D-NumPy-Array aus Floats. Zusätzlich zum Setzen der Bilddaten berechnen wir ein Min und Max für die Farbpalette basierend auf Mittelwert und Varianz der Daten, das wir später verwenden. Der letzte Teil des GUI-Codes für das Spektrogramm ist die Erstellung der Farbskala, die auch die verwendete Farbpalette festlegt: + +.. code-block:: python + + # Farbskala für Wasserfall + colorbar = pg.HistogramLUTWidget() + colorbar.setImageItem(imageitem) # verbindet die Skala mit dem Wasserfall-ImageItem + colorbar.item.gradient.loadPreset('viridis') # Farbpalette setzen, auch für das ImageItem + imageitem.setLevels((-30, 20)) # muss nach Erstellung der Farbskala kommen + waterfall_layout.addWidget(colorbar) + +Die zweite Zeile ist wichtig – sie verbindet diese Farbskala mit dem ImageItem. Hier wählen wir auch die Farbpalette und setzen die Anfangspegel (-30 dB bis +20 dB in unserem Fall). Im Worker-Thread-Code siehst du, wie das 2D-Spektrogramm-Array berechnet und gespeichert wird. Unten ist ein Screenshot dieses GUI-Teils, der die eingebaute Funktionalität der Farbskala und LUT-Anzeige zeigt. Beachte, dass die seitliche Glockenkurve die Verteilung der Spektrogramm-Werte zeigt – sehr nützlich. + +.. image:: ../_images/pyqt_spectrogram.png + :scale: 50 % + :align: center + :alt: PyQtGraph Spectrogram and colorbar + +*********************** +Worker-Thread +*********************** + +Erinnere dich, dass wir am Anfang dieses Kapitels gelernt haben, wie man einen separaten Thread erstellt, wobei wir eine Klasse namens SDRWorker mit einer :code:`run()`-Funktion verwendet haben. Hier werden wir den gesamten SDR- und DSP-Code unterbringen, mit Ausnahme der SDR-Initialisierung, die wir vorerst global vornehmen. Der Worker-Thread ist auch dafür verantwortlich, die drei Diagramme zu aktualisieren, indem er Signals aussendet, wenn neue Samples verfügbar sind, um die Callback-Funktionen auszulösen, die wir bereits in :code:`MainWindow` erstellt haben und die letztendlich die Diagramme aktualisieren. Die SDRWorker-Klasse lässt sich in drei Bereiche aufteilen: + +#. :code:`init()` – zum Initialisieren von Zustand, z. B. des Spektrogramm-2D-Arrays +#. PyQt Signals – hier müssen wir unsere benutzerdefinierten Signals definieren, die ausgesendet werden sollen +#. PyQt Slots – die Callback-Funktionen, die durch GUI-Ereignisse wie einen Schieberegler-Bewegung ausgelöst werden +#. :code:`run()` – die Hauptschleife, die ununterbrochen läuft + +*********************** +PyQt Signals +*********************** + +Im GUI-Code mussten wir keine Signals definieren, weil sie in den verwendeten Widgets eingebaut waren, wie z. B. :code:`valueChanged` bei :code:`QSlider`. Unsere SDRWorker-Klasse ist benutzerdefiniert, und alle Signals, die wir senden möchten, müssen vor dem Start von :code:`run()` definiert werden. Hier ist der Code für die SDRWorker-Klasse, der vier Signals und ihre entsprechenden Datentypen definiert: + +.. code-block:: python + + # PyQt Signals + time_plot_update = pyqtSignal(np.ndarray) + freq_plot_update = pyqtSignal(np.ndarray) + waterfall_plot_update = pyqtSignal(np.ndarray) + end_of_run = pyqtSignal() # tritt viele Male pro Sekunde auf + +Die ersten drei Signals senden ein einzelnes Objekt – ein NumPy-Array. Das letzte Signal sendet kein Objekt mit. Du kannst auch mehrere Objekte gleichzeitig senden, indem du einfach Kommas zwischen Datentypen verwendest, aber wir benötigen das für unsere Anwendung nicht. Überall innerhalb von :code:`run()` können wir mit nur einer Codezeile ein Signal an den GUI-Thread senden, zum Beispiel: + +.. code-block:: python + + self.time_plot_update.emit(samples) + +Es gibt noch einen letzten Schritt, um alle Signal/Slot-Verbindungen herzustellen: Im GUI-Code (am Ende des :code:`__init__` von :code:`MainWindow`) müssen wir die Signals des Worker-Threads mit den Slots der GUI verbinden, zum Beispiel: + +.. code-block:: python + + worker.time_plot_update.connect(time_plot_callback) # Signal mit Callback verbinden + +Beachte, dass :code:`worker` die Instanz der SDRWorker-Klasse ist, die wir im GUI-Code erstellt haben. Wir verbinden also das Signal des Worker-Threads namens :code:`time_plot_update` mit dem Slot der GUI namens :code:`time_plot_callback`, den wir zuvor definiert haben. Jetzt ist ein guter Zeitpunkt, die bisher gezeigten Code-Ausschnitte noch einmal zu überprüfen und zu sehen, wie sie alle zusammenpassen, um sicherzustellen, dass du verstehst, wie GUI und Worker-Thread miteinander kommunizieren – das ist ein zentraler Bestandteil der PyQt-Programmierung. + +*********************** +Worker-Thread Slots +*********************** + +Die Slots des Worker-Threads sind Callback-Funktionen, die durch GUI-Ereignisse ausgelöst werden, z. B. wenn der Verstärkungs-Schieberegler bewegt wird. Sie sind recht unkompliziert – dieser Slot zum Beispiel aktualisiert die Verstärkung des SDR auf den neuen vom Schieberegler gewählten Wert: + +.. code-block:: python + + def update_gain(self, val): + print("Updated gain to:", val, 'dB') + sdr.set_rx_gain(val) + +*********************** +Worker-Thread Run() +*********************** + +Die :code:`run()`-Funktion ist der Ort, an dem der ganze DSP-Spaß passiert! In unserer Anwendung beginnen wir jede :code:`run()`-Funktion damit, eine Reihe von Samples vom SDR zu empfangen (oder Samples zu simulieren, wenn du kein SDR hast). + +.. code-block:: python + + # Hauptschleife + def run(self): + if sdr_type == "pluto": + samples = sdr.rx()/2**11 # Samples empfangen + elif sdr_type == "usrp": + streamer.recv(recv_buffer, metadata) + samples = recv_buffer[0] # wird np.complex64 sein + elif sdr_type == "sim": + tone = np.exp(2j*np.pi*self.sample_rate*0.1*np.arange(fft_size)/self.sample_rate) + noise = np.random.randn(fft_size) + 1j*np.random.randn(fft_size) + samples = self.gain*tone*0.02 + 0.1*noise + # Auf -1 bis +1 begrenzen, um ADC-Bitgrenzen zu simulieren + np.clip(samples.real, -1, 1, out=samples.real) + np.clip(samples.imag, -1, 1, out=samples.imag) + + ... + +Für das simulierte Beispiel erzeugen wir einen Ton mit etwas weißem Rauschen und begrenzen die Samples dann auf -1 bis +1. + +Nun zum DSP! Wir wissen, dass wir die FFT für das Frequenzbereich-Diagramm und das Spektrogramm benötigen. Es stellt sich heraus, dass wir einfach die PSD für diesen Satz von Samples als eine Zeile des Spektrogramms verwenden können. Wir müssen also nur unser Spektrogramm/Wasserfall um eine Zeile nach oben verschieben und die neue Zeile unten (oder oben – das spielt keine Rolle) hinzufügen. Für jede Diagramm-Aktualisierung senden wir das Signal mit den aktualisierten Daten aus. Wir signalisieren auch das Ende der :code:`run()`-Funktion, damit der GUI-Thread sofort einen weiteren :code:`run()`-Aufruf startet. Insgesamt ist es eigentlich nicht viel Code: + +.. code-block:: python + + ... + + self.time_plot_update.emit(samples[0:time_plot_samples]) + + PSD = 10.0*np.log10(np.abs(np.fft.fftshift(np.fft.fft(samples)))**2/fft_size) + self.PSD_avg = self.PSD_avg * 0.99 + PSD * 0.01 + self.freq_plot_update.emit(self.PSD_avg) + + self.spectrogram[:] = np.roll(self.spectrogram, 1, axis=1) # Wasserfall um 1 Zeile verschieben + self.spectrogram[:,0] = PSD # letzte Zeile mit neuen FFT-Ergebnissen füllen + self.waterfall_plot_update.emit(self.spectrogram) + + self.end_of_run.emit() # Signal senden, um die Schleife fortzusetzen + # Ende von run() + +Beachte, dass wir nicht den gesamten Satz von Samples an das Zeitdiagramm senden, weil es zu viele Punkte zum Anzeigen wären – stattdessen senden wir nur die ersten 500 Samples (am Anfang des Skripts konfigurierbar, hier nicht gezeigt). Für das PSD-Diagramm verwenden wir einen gleitenden Durchschnitt der PSD, indem wir die vorherige PSD speichern und 1% der neuen PSD hinzufügen. Das ist eine einfache Methode, um das PSD-Diagramm zu glätten. Beachte, dass die Reihenfolge der :code:`emit()`-Aufrufe für die Signals keine Rolle spielt; sie hätten genauso gut alle am Ende von :code:`run()` stehen können. + +*********************** +Vollständiger Beispiel-Code +*********************** + +Bisher haben wir uns Ausschnitte der Spektrumanalysator-App angesehen, aber jetzt schauen wir uns endlich den vollständigen Code an und probieren ihn aus. Er unterstützt derzeit PlutoSDR, USRP oder Simulationsmodus. Wenn du kein Pluto oder USRP hast, lass den Code einfach so wie er ist – er sollte den Simulationsmodus verwenden. Andernfalls ändere :code:`sdr_type`. Im Simulationsmodus wirst du bemerken, dass das Signal im Zeitbereich abgeschnitten wird, wenn du die Verstärkung ganz aufgedreht hast, was zu Störsignalen im Frequenzbereich führt. + +Verwende diesen Code gerne als Ausgangspunkt für deine eigene Echtzeit-SDR-Anwendung! Unten ist auch eine Animation der App in Aktion, wobei ein Pluto verwendet wird, um das 750-MHz-Mobilfunkband und dann 2,4-GHz-WLAN zu betrachten. Eine Version in höherer Qualität ist auf YouTube `hier `_ verfügbar. + +.. image:: ../_images/pyqt_animation.gif + :scale: 100 % + :align: center + :alt: Animated gif showing the PyQt spectrum analyzer app in action + +Bekannte Fehler (um sie zu beheben, `bearbeite dies `_): + +#. Die x-Achse des Wasserfalls aktualisiert sich nicht beim Ändern der Mittenfrequenz (das PSD-Diagramm schon) + +Vollständiger Code: + +.. code-block:: python + + from PyQt6.QtCore import QSize, Qt, QThread, pyqtSignal, QObject, QTimer + from PyQt6.QtWidgets import QApplication, QMainWindow, QGridLayout, QWidget, QSlider, QLabel, QHBoxLayout, QVBoxLayout, QPushButton, QComboBox # tested with PyQt6==6.7.0 + import pyqtgraph as pg # tested with pyqtgraph==0.13.7 + import numpy as np + import time + import signal # ermöglicht, dass Strg+C die App wirklich schließt + + # Standardwerte + fft_size = 4096 # bestimmt Puffergröße + num_rows = 200 + center_freq = 750e6 + sample_rates = [56, 40, 20, 10, 5, 2, 1, 0.5] # MHz + sample_rate = sample_rates[0] * 1e6 + time_plot_samples = 500 + gain = 50 # 0 bis 73 dB. int + + sdr_type = "sim" # oder "usrp" oder "pluto" + + # SDR initialisieren + if sdr_type == "pluto": + import adi + sdr = adi.Pluto("ip:192.168.1.10") + sdr.rx_lo = int(center_freq) + sdr.sample_rate = int(sample_rate) + sdr.rx_rf_bandwidth = int(sample_rate*0.8) # Bandbreite des Anti-Aliasing-Filters + sdr.rx_buffer_size = int(fft_size) + sdr.gain_control_mode_chan0 = 'manual' + sdr.rx_hardwaregain_chan0 = gain # dB + elif sdr_type == "usrp": + import uhd + #usrp = uhd.usrp.MultiUSRP(args="addr=192.168.1.10") + usrp = uhd.usrp.MultiUSRP(args="addr=192.168.1.201") + usrp.set_rx_rate(sample_rate, 0) + usrp.set_rx_freq(uhd.libpyuhd.types.tune_request(center_freq), 0) + usrp.set_rx_gain(gain, 0) + + # Stream und Empfangspuffer einrichten + st_args = uhd.usrp.StreamArgs("fc32", "sc16") + st_args.channels = [0] + metadata = uhd.types.RXMetadata() + streamer = usrp.get_rx_stream(st_args) + recv_buffer = np.zeros((1, fft_size), dtype=np.complex64) + + # Stream starten + stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.start_cont) + stream_cmd.stream_now = True + streamer.issue_stream_cmd(stream_cmd) + + def flush_buffer(): + for _ in range(10): + streamer.recv(recv_buffer, metadata) + + class SDRWorker(QObject): + def __init__(self): + super().__init__() + self.gain = gain + self.sample_rate = sample_rate + self.freq = 0 # in kHz, um mit QSlider-Integers und max. 2 Milliarden umzugehen + self.spectrogram = -50*np.ones((fft_size, num_rows)) + self.PSD_avg = -50*np.ones(fft_size) + + # PyQt Signals + time_plot_update = pyqtSignal(np.ndarray) + freq_plot_update = pyqtSignal(np.ndarray) + waterfall_plot_update = pyqtSignal(np.ndarray) + end_of_run = pyqtSignal() # tritt viele Male pro Sekunde auf + + # PyQt Slots + def update_freq(self, val): # TODO: SDR KÖNNTE AUCH IM GUI-THREAD GEÄNDERT WERDEN + print("Updated freq to:", val, 'kHz') + if sdr_type == "pluto": + sdr.rx_lo = int(val*1e3) + elif sdr_type == "usrp": + usrp.set_rx_freq(uhd.libpyuhd.types.tune_request(val*1e3), 0) + flush_buffer() + + def update_gain(self, val): + print("Updated gain to:", val, 'dB') + self.gain = val + if sdr_type == "pluto": + sdr.rx_hardwaregain_chan0 = val + elif sdr_type == "usrp": + usrp.set_rx_gain(val, 0) + flush_buffer() + + def update_sample_rate(self, val): + print("Updated sample rate to:", sample_rates[val], 'MHz') + if sdr_type == "pluto": + sdr.sample_rate = int(sample_rates[val] * 1e6) + sdr.rx_rf_bandwidth = int(sample_rates[val] * 1e6 * 0.8) + elif sdr_type == "usrp": + usrp.set_rx_rate(sample_rates[val] * 1e6, 0) + flush_buffer() + + # Hauptschleife + def run(self): + start_t = time.time() + + if sdr_type == "pluto": + samples = sdr.rx()/2**11 # Samples empfangen + elif sdr_type == "usrp": + streamer.recv(recv_buffer, metadata) + samples = recv_buffer[0] # wird np.complex64 sein + elif sdr_type == "sim": + tone = np.exp(2j*np.pi*self.sample_rate*0.1*np.arange(fft_size)/self.sample_rate) + noise = np.random.randn(fft_size) + 1j*np.random.randn(fft_size) + samples = self.gain*tone*0.02 + 0.1*noise + # Auf -1 bis +1 begrenzen, um ADC-Bitgrenzen zu simulieren + np.clip(samples.real, -1, 1, out=samples.real) + np.clip(samples.imag, -1, 1, out=samples.imag) + + self.time_plot_update.emit(samples[0:time_plot_samples]) + + PSD = 10.0*np.log10(np.abs(np.fft.fftshift(np.fft.fft(samples)))**2/fft_size) + self.PSD_avg = self.PSD_avg * 0.99 + PSD * 0.01 + self.freq_plot_update.emit(self.PSD_avg) + + self.spectrogram[:] = np.roll(self.spectrogram, 1, axis=1) # Wasserfall um 1 Zeile verschieben + self.spectrogram[:,0] = PSD # letzte Zeile mit neuen FFT-Ergebnissen füllen + self.waterfall_plot_update.emit(self.spectrogram) + + print("Frames per second:", 1/(time.time() - start_t)) + self.end_of_run.emit() # Signal senden, um die Schleife fortzusetzen + + + # QMainWindow ableiten, um das Hauptfenster der Anwendung anzupassen + class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + + self.setWindowTitle("The PySDR Spectrum Analyzer") + self.setFixedSize(QSize(1500, 1000)) # Fenstergröße, sollte auf 1920x1080 passen + + self.spectrogram_min = 0 + self.spectrogram_max = 0 + + layout = QGridLayout() # Gesamtlayout + + # Worker und Thread initialisieren + self.sdr_thread = QThread() + self.sdr_thread.setObjectName('SDR_Thread') # in htop sichtbar; F2 -> Display options -> Show custom thread names + worker = SDRWorker() + worker.moveToThread(self.sdr_thread) + + # Zeitdiagramm + time_plot = pg.PlotWidget(labels={'left': 'Amplitude', 'bottom': 'Time [microseconds]'}) + time_plot.setMouseEnabled(x=False, y=True) + time_plot.setYRange(-1.1, 1.1) + time_plot_curve_i = time_plot.plot([]) + time_plot_curve_q = time_plot.plot([]) + layout.addWidget(time_plot, 1, 0) + + # Auto-Range-Schaltflächen für Zeitdiagramm + time_plot_auto_range_layout = QVBoxLayout() + layout.addLayout(time_plot_auto_range_layout, 1, 1) + auto_range_button = QPushButton('Auto Range') + auto_range_button.clicked.connect(lambda : time_plot.autoRange()) # lambda bedeutet unbenannte Funktion + time_plot_auto_range_layout.addWidget(auto_range_button) + auto_range_button2 = QPushButton('-1 to +1\n(ADC limits)') + auto_range_button2.clicked.connect(lambda : time_plot.setYRange(-1.1, 1.1)) + time_plot_auto_range_layout.addWidget(auto_range_button2) + + # Frequenzdiagramm + freq_plot = pg.PlotWidget(labels={'left': 'PSD', 'bottom': 'Frequency [MHz]'}) + freq_plot.setMouseEnabled(x=False, y=True) + freq_plot_curve = freq_plot.plot([]) + freq_plot.setXRange(center_freq/1e6 - sample_rate/2e6, center_freq/1e6 + sample_rate/2e6) + freq_plot.setYRange(-30, 20) + layout.addWidget(freq_plot, 2, 0) + + # Auto-Range-Schaltfläche für Frequenzdiagramm + auto_range_button = QPushButton('Auto Range') + auto_range_button.clicked.connect(lambda : freq_plot.autoRange()) # lambda bedeutet unbenannte Funktion + layout.addWidget(auto_range_button, 2, 1) + + # Layout-Container für Wasserfall-Elemente + waterfall_layout = QHBoxLayout() + layout.addLayout(waterfall_layout, 3, 0) + + # Wasserfall-Diagramm + waterfall = pg.PlotWidget(labels={'left': 'Time [s]', 'bottom': 'Frequency [MHz]'}) + imageitem = pg.ImageItem(axisOrder='col-major') # dieses Argument dient nur der Performance + waterfall.addItem(imageitem) + waterfall.setMouseEnabled(x=False, y=False) + waterfall_layout.addWidget(waterfall) + + # Farbskala für Wasserfall + colorbar = pg.HistogramLUTWidget() + colorbar.setImageItem(imageitem) # verbindet die Skala mit dem Wasserfall-ImageItem + colorbar.item.gradient.loadPreset('viridis') # Farbpalette setzen, auch für das ImageItem + imageitem.setLevels((-30, 20)) # muss nach Erstellung der Farbskala kommen + waterfall_layout.addWidget(colorbar) + + # Auto-Range-Schaltfläche für Wasserfall + auto_range_button = QPushButton('Auto Range\n(-2σ to +2σ)') + def update_colormap(): + imageitem.setLevels((self.spectrogram_min, self.spectrogram_max)) + colorbar.setLevels(self.spectrogram_min, self.spectrogram_max) + auto_range_button.clicked.connect(update_colormap) + layout.addWidget(auto_range_button, 3, 1) + + # Frequenz-Schieberegler mit Label, alle Einheiten in kHz + freq_slider = QSlider(Qt.Orientation.Horizontal) + freq_slider.setRange(0, int(6e6)) + freq_slider.setValue(int(center_freq/1e3)) + freq_slider.setTickPosition(QSlider.TickPosition.TicksBelow) + freq_slider.setTickInterval(int(1e6)) + freq_slider.sliderMoved.connect(worker.update_freq) # es gibt auch valueChanged + freq_label = QLabel() + def update_freq_label(val): + freq_label.setText("Frequency [MHz]: " + str(val/1e3)) + freq_plot.autoRange() + freq_slider.sliderMoved.connect(update_freq_label) + update_freq_label(freq_slider.value()) # Label initialisieren + layout.addWidget(freq_slider, 4, 0) + layout.addWidget(freq_label, 4, 1) + + # Verstärkungs-Schieberegler mit Label + gain_slider = QSlider(Qt.Orientation.Horizontal) + gain_slider.setRange(0, 73) + gain_slider.setValue(gain) + gain_slider.setTickPosition(QSlider.TickPosition.TicksBelow) + gain_slider.setTickInterval(2) + gain_slider.sliderMoved.connect(worker.update_gain) + gain_label = QLabel() + def update_gain_label(val): + gain_label.setText("Gain: " + str(val)) + gain_slider.sliderMoved.connect(update_gain_label) + update_gain_label(gain_slider.value()) # Label initialisieren + layout.addWidget(gain_slider, 5, 0) + layout.addWidget(gain_label, 5, 1) + + # Abtastraten-Dropdown mit QComboBox + sample_rate_combobox = QComboBox() + sample_rate_combobox.addItems([str(x) + ' MHz' for x in sample_rates]) + sample_rate_combobox.setCurrentIndex(0) # muss mit dem Standard oben übereinstimmen + sample_rate_combobox.currentIndexChanged.connect(worker.update_sample_rate) + sample_rate_label = QLabel() + def update_sample_rate_label(val): + sample_rate_label.setText("Sample Rate: " + str(sample_rates[val]) + " MHz") + sample_rate_combobox.currentIndexChanged.connect(update_sample_rate_label) + update_sample_rate_label(sample_rate_combobox.currentIndex()) # Label initialisieren + layout.addWidget(sample_rate_combobox, 6, 0) + layout.addWidget(sample_rate_label, 6, 1) + + central_widget = QWidget() + central_widget.setLayout(layout) + self.setCentralWidget(central_widget) + + # Signals und Slots + def time_plot_callback(samples): + time_plot_curve_i.setData(samples.real) + time_plot_curve_q.setData(samples.imag) + + def freq_plot_callback(PSD_avg): + # TODO: Prüfen, ob man nur die visuellen Ticks ändern kann statt der x-Werte + f = np.linspace(freq_slider.value()*1e3 - worker.sample_rate/2.0, freq_slider.value()*1e3 + worker.sample_rate/2.0, fft_size) / 1e6 + freq_plot_curve.setData(f, PSD_avg) + freq_plot.setXRange(freq_slider.value()*1e3/1e6 - worker.sample_rate/2e6, freq_slider.value()*1e3/1e6 + worker.sample_rate/2e6) + + def waterfall_plot_callback(spectrogram): + imageitem.setImage(spectrogram, autoLevels=False) + sigma = np.std(spectrogram) + mean = np.mean(spectrogram) + self.spectrogram_min = mean - 2*sigma # im Fensterzustand speichern + self.spectrogram_max = mean + 2*sigma + + def end_of_run_callback(): + QTimer.singleShot(0, worker.run) # Worker sofort erneut ausführen + + worker.time_plot_update.connect(time_plot_callback) # Signal mit Callback verbinden + worker.freq_plot_update.connect(freq_plot_callback) + worker.waterfall_plot_update.connect(waterfall_plot_callback) + worker.end_of_run.connect(end_of_run_callback) + + self.sdr_thread.started.connect(worker.run) # startet Worker, wenn Thread beginnt + self.sdr_thread.start() + + + app = QApplication([]) + window = MainWindow() + window.show() # Fenster sind standardmäßig ausgeblendet + signal.signal(signal.SIGINT, signal.SIG_DFL) # ermöglicht, dass Strg+C die App wirklich schließt + app.exec() # Ereignisschleife starten + + if sdr_type == "usrp": + stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.stop_cont) + streamer.issue_stream_cmd(stream_cmd) diff --git a/content-de/rds.rst b/content-de/rds.rst new file mode 100644 index 00000000..1b476dfd --- /dev/null +++ b/content-de/rds.rst @@ -0,0 +1,944 @@ +.. _rds-chapter: + +################## +End-to-End-Beispiel +################## + +In diesem Kapitel bringen wir viele der zuvor gelernten Konzepte zusammen und gehen ein vollständiges Beispiel für den Empfang und die Dekodierung eines realen digitalen Signals durch. Wir werden uns das Radio Data System (RDS) ansehen, ein Kommunikationsprotokoll zum Einbetten kleiner Informationsmengen in UKW-Rundfunksendungen, wie Sender- und Songnamen. Wir werden FM demodulieren, frequenzverschieben, filtern, dezimieren, resamplen, synchronisieren, dekodieren und die Bytes parsen. Eine Beispiel-IQ-Datei wird zu Testzwecken bereitgestellt, falls du kein SDR zur Hand hast. + +******************************** +Einführung in UKW-Radio und RDS +******************************** + +Um RDS zu verstehen, müssen wir zunächst UKW-Rundfunksendungen und die Struktur ihrer Signale wiederholen. Du bist wahrscheinlich mit dem Audioanteil von UKW-Signalen vertraut, die einfach Audiosignale sind, die frequenzmoduliert und auf Mittenfrequenzen entsprechend dem Sendernamen übertragen werden, z.B. ist "WPGC 95.5 FM" genau auf 95,5 MHz zentriert. Zusätzlich zum Audioanteil enthält jede UKW-Sendung einige andere Komponenten, die zusammen mit dem Audio frequenzmoduliert werden. Anstatt einfach nach der Signalstruktur zu googeln, werfen wir einen Blick auf die Leistungsspektraldichte (PSD) eines Beispiel-UKW-Signals, *nach* der FM-Demodulation. Wir sehen nur den positiven Anteil, da die Ausgabe der FM-Demodulation ein reales Signal ist, obwohl der Eingang komplex war (den Code zur Durchführung dieser Demodulation sehen wir in Kürze). + +.. image:: ../_images/fm_psd.svg + :align: center + :target: ../_images/fm_psd.svg + :alt: Leistungsspektraldichte (PSD) eines UKW-Radiosignals nach der FM-Demodulation, zeigt RDS + +Wenn wir das Signal im Frequenzbereich betrachten, bemerken wir folgende einzelne Signale: + +#. Ein leistungsstarkes Signal zwischen 0 - 17 kHz +#. Ein Ton bei 19 kHz +#. Zentriert bei 38 kHz und ungefähr 30 kHz breit sehen wir ein interessant aussehendes symmetrisches Signal +#. Doppelkeulenförmiges Signal zentriert bei 57 kHz +#. Einzelkeulenförmiges Signal zentriert bei 67 kHz + +Das ist im Wesentlichen alles, was wir nur durch Betrachtung der PSD bestimmen können, und denke daran, dass dies *nach* der FM-Demodulation ist. Die PSD vor der FM-Demodulation sieht wie folgt aus, was uns nicht viel verrät. + +.. image:: ../_images/fm_before_demod.svg + :align: center + :target: ../_images/fm_before_demod.svg + :alt: Leistungsspektraldichte (PSD) eines UKW-Radiosignals vor jeder Demodulation + +Es ist jedoch wichtig zu verstehen, dass beim FM-Modulieren eines Signals eine höhere Frequenz im Datensignal zu einer höheren Frequenz im resultierenden FM-Signal führt. Das bei 67 kHz zentrierte Signal erhöht also die gesamte vom übertragenen FM-Signal belegte Bandbreite, da die maximale Frequenzkomponente jetzt bei etwa 75 kHz liegt, wie in der ersten PSD oben gezeigt. `Carsons Bandbreitenregel `_ angewendet auf FM sagt uns, dass UKW-Sender etwa 250 kHz Spektrum belegen, weshalb wir normalerweise mit 250 kHz abtasten (zur Erinnerung: bei der Quadratur/IQ-Abtastung entspricht die empfangene Bandbreite der Abtastrate). + +Als kurze Nebenbemerkung: Manche Leser kennen vielleicht den Anblick des UKW-Bandes mit einem SDR oder Spektrumanalysator und sehen das folgende Spektrogramm, wobei sie glauben, dass die blockartige Signale neben einigen UKW-Sendern RDS sind. + +.. image:: ../_images/fm_band_psd.png + :scale: 80 % + :align: center + :alt: Spektrogramm des UKW-Bands + +Es stellt sich heraus, dass diese blockartigen Signale tatsächlich HD Radio sind, eine digitale Version desselben UKW-Radiosignals (gleicher Audioinhalt). Diese digitale Version führt zu einem Audio-Signal höherer Qualität am Empfänger, da analoges UKW nach der Demodulation immer etwas Rauschen enthält (als analoges Verfahren), während das digitale Signal ohne Rauschen demoduliert/dekodiert werden kann, vorausgesetzt es gibt null Bitfehler. + +Zurück zu den fünf Signalen, die wir in unserer PSD entdeckt haben; das folgende Diagramm zeigt, wofür jedes Signal verwendet wird. + +.. image:: ../_images/fm_psd_labeled.svg + :align: center + :target: ../_images/fm_psd_labeled.svg + :alt: Komponenten innerhalb eines UKW-Radiosignals, einschließlich Mono- und Stereoaudio, RDS und DirectBand-Signale + +Die einzelnen Signale in beliebiger Reihenfolge: + +Die Mono- und Stereoaudiosignale tragen einfach das Audiosignal, wobei Addition und Subtraktion den linken und rechten Kanal ergibt. + +Der 19-kHz-Pilotton wird zur Demodulation des Stereoaudios verwendet. Wenn man ihn verdoppelt, dient er als Frequenz- und Phasenreferenz, da das Stereoaudiosignal bei 38 kHz zentriert ist. Das Verdoppeln des Tons kann einfach durch Quadrieren der Abtastwerte erfolgen – erinnere dich an die Frequenzverschiebungs-Fourier-Eigenschaft aus dem Kapitel :ref:`freq-domain-chapter`. + +DirectBand war ein nordamerikanisches drahtloses Datennetzwerk im Besitz von Microsoft, das auch als "MSN Direct" auf dem Verbrauchermarkt bekannt war. DirectBand übermittelte Informationen an Geräte wie tragbare GPS-Empfänger, Armbanduhren und Heim-Wetterstationen. Es ermöglichte sogar Benutzern, kurze Nachrichten über Windows Live Messenger zu empfangen. Eine der erfolgreichsten Anwendungen von DirectBand waren Echtzeit-Staudaten auf Garmin-GPS-Empfängern, die von Millionen von Menschen genutzt wurden, bevor Smartphones allgegenwärtig wurden. Der DirectBand-Dienst wurde im Januar 2012 eingestellt, was die Frage aufwirft, warum wir ihn in unserem UKW-Signal sehen, das nach 2012 aufgezeichnet wurde. Meine einzige Vermutung ist, dass die meisten UKW-Sender lange vor 2012 entwickelt und gebaut wurden und ohne aktiven DirectBand-Feed immer noch etwas senden, vielleicht Pilotsymbole. + +Schließlich kommen wir zu RDS, dem Fokus des restlichen Kapitels. Wie wir in unserer ersten PSD sehen können, hat RDS etwa 4 kHz Bandbreite (bevor es FM-moduliert wird) und liegt zwischen dem Stereoaudio und dem DirectBand-Signal. Es ist ein Niedrigdatenraten-Digitalkommunikationsprotokoll, das UKW-Sendern ermöglicht, Sender-Identifikation, Programminformationen, Zeit und andere verschiedene Informationen zusammen mit dem Audio zu übermitteln. Der RDS-Standard ist als IEC-Standard 62106 veröffentlicht und `kann hier gefunden werden `_. + +******************************** +Das RDS-Signal +******************************** + +In diesem Kapitel werden wir Python zum Empfangen von RDS verwenden, aber um den Empfang am besten zu verstehen, müssen wir zunächst lernen, wie das Signal geformt und übertragen wird. + +Sendeseite +############# + +Die vom UKW-Sender zu übertragenden RDS-Informationen (z.B. Titelname usw.) werden in Sätze von 8 Bytes codiert. Jeder Satz von 8 Bytes, der 64 Bits entspricht, wird mit 40 "Prüfbits" kombiniert, um eine einzelne "Gruppe" zu bilden. Diese 104 Bits werden zusammen übertragen, obwohl es keine Zeitlücke zwischen Gruppen gibt, sodass der Empfänger aus seiner Perspektive diese Bits ununterbrochen empfängt und die Grenze zwischen den 104-Bit-Gruppen bestimmen muss. Wir werden mehr Details zur Codierung und Nachrichtenstruktur sehen, sobald wir uns mit der Empfangsseite beschäftigen. + +Zur drahtlosen Übertragung dieser Bits verwendet RDS BPSK, das wie wir im Kapitel :ref:`modulation-chapter` gelernt haben, ein einfaches digitales Modulationsverfahren ist, das 1er und 0er der Phase eines Trägers zuordnet. Wie viele BPSK-basierte Protokolle verwendet RDS differentielle Codierung, was einfach bedeutet, dass die 1er und 0er der Daten in Änderungen von 1ern und 0ern codiert werden, was es ermöglicht, nicht mehr auf eine 180-Grad-Phasendrehung zu achten (mehr dazu später). Die BPSK-Symbole werden mit 1187,5 Symbolen pro Sekunde übertragen, und da BPSK ein Bit pro Symbol trägt, bedeutet das, dass RDS eine rohe Datenrate von etwa 1,2 kbps hat (einschließlich Overhead). RDS enthält keine Kanalcodierung (a.k.a. Vorwärtsfehlerkorrektur), obwohl die Datenpakete eine zyklische Redundanzprüfung (CRC) enthalten, um zu wissen, wann ein Fehler aufgetreten ist. + +Das endgültige BPSK-Signal wird dann auf 57 kHz hochverschoben und zu allen anderen Komponenten des FM-Signals hinzugefügt, bevor es auf der Stationsfrequenz FM-moduliert und über die Luft übertragen wird. UKW-Radiosignale werden mit extrem hoher Leistung im Vergleich zu den meisten anderen drahtlosen Kommunikationen übertragen, bis zu 80 kW! Deshalb haben viele SDR-Benutzer einen FM-Ablehnfilter (d.h. einen Bandsperrfilter) in Reihe mit ihrer Antenne, damit FM keine Interferenz zu dem hinzufügt, was sie empfangen möchten. + +Obwohl dies nur ein kurzer Überblick über die Sendeseite war, werden wir mehr Details besprechen, wenn wir den RDS-Empfang behandeln. + +Empfangsseite +############ + +Um RDS zu demodulieren und zu dekodieren, führen wir die folgenden Schritte durch, von denen viele den senderseitigen Schritten in umgekehrter Reihenfolge entsprechen (du musst diese Liste nicht auswendig lernen, wir gehen jeden Schritt unten einzeln durch): + +#. Ein UKW-Radiosignal zentriert auf die Stationsfrequenz empfangen (oder eine IQ-Aufzeichnung einlesen), üblicherweise bei einer Abtastrate von 250 kHz +#. Das UKW mithilfe der sogenannten "Quadratur-Demodulation" demodulieren +#. Um 57 kHz frequenzverschieben, sodass das RDS-Signal bei 0 Hz zentriert ist +#. Tiefpassfilter, um alles außer RDS herauszufiltern (wirkt auch als Matched Filter) +#. Um 10 dezimieren, damit wir bei einer niedrigeren Abtastrate arbeiten können, da wir die höheren Frequenzen ohnehin herausgefiltert haben +#. Auf 19 kHz resamplen, was uns eine ganzzahlige Anzahl von Abtastwerten pro Symbol gibt +#. Zeitsynchronisation auf Symbolebene, in diesem Beispiel mit Mueller und Müller +#. Feine Frequenzsynchronisation mit einem Costas-Regelkreis +#. Das BPSK in 1er und 0er demodulieren +#. Differenzielle Dekodierung, um die angewendete differentielle Codierung rückgängig zu machen +#. Dekodierung der 1er und 0er in Gruppen von Bytes +#. Parsen der Gruppen von Bytes in unsere endgültige Ausgabe + +Auch wenn das viele Schritte zu sein scheinen, ist RDS tatsächlich eines der einfachsten drahtlosen digitalen Kommunikationsprotokolle. Ein modernes drahtloses Protokoll wie WiFi oder 5G erfordert ein ganzes Lehrbuch, nur um die übergeordneten PHY/MAC-Schichtinformationen abzudecken. + +Wir werden nun in den Python-Code eintauchen, der zum Empfangen von RDS verwendet wird. Dieser Code wurde mit einer `UKW-Radioaufzeichnung, die du hier findest `_, getestet. Du solltest auch dein eigenes Signal einspeisen können, solange es mit ausreichend hohem SNR empfangen wird – einfach auf die Mittenfrequenz des Senders abstimmen und bei 250 kHz abtasten. Um die empfangene Signalleistung zu maximieren (z.B. wenn du drinnen bist), hilft es, eine Halbwellen-Dipolantenne der richtigen Länge (~1,5 Meter) zu verwenden, nicht die 2,4-GHz-Antennen, die mit dem Pluto geliefert werden. Davon abgesehen ist FM ein sehr lautes Signal, und wenn du in der Nähe eines Fensters oder draußen bist, werden die 2,4-GHz-Antennen wahrscheinlich ausreichen, um die stärkeren Radiosender zu empfangen. + +In diesem Abschnitt präsentieren wir kleine Teile des Codes einzeln mit Erläuterungen, aber derselbe Code wird am Ende dieses Kapitels in einem großen Block bereitgestellt. Jeder Abschnitt präsentiert einen Codeblock und erklärt dann, was er tut. + +******************************** +Ein Signal erfassen +******************************** + +.. code-block:: python + + import numpy as np + from scipy.signal import resample_poly, firwin, bilinear, lfilter + import matplotlib.pyplot as plt + + # Signal einlesen + x = np.fromfile('/home/marc/Downloads/fm_rds_250k_1Msamples.iq', dtype=np.complex64) + sample_rate = 250e3 + center_freq = 99.5e6 + +Wir lesen unsere Testaufzeichnung ein, die mit 250 kHz abgetastet und auf einem UKW-Sender mit hohem SNR empfangen wurde. Stelle sicher, dass du den Dateipfad entsprechend deinem System und dem Speicherort der Aufzeichnung aktualisierst. Wenn du ein SDR bereits in Python eingerichtet und zum Laufen gebracht hast, kannst du gerne ein Live-Signal empfangen, obwohl es hilfreich ist, zuerst den gesamten Code mit einer `bekannt funktionierenden IQ-Aufzeichnung `_ getestet zu haben. Wir verwenden :code:`x` zur Speicherung des aktuell manipulierten Signals. + +******************************** +FM-Demodulation +******************************** + +.. code-block:: python + + # Quadratur-Demodulation + x = 0.5 * np.angle(x[0:-1] * np.conj(x[1:])) # siehe https://wiki.gnuradio.org/index.php/Quadrature_Demod + +Wie am Anfang dieses Kapitels besprochen, werden mehrere einzelne Signale in der Frequenz kombiniert und FM-moduliert, um das tatsächlich über die Luft übertragene Signal zu erstellen. Der erste Schritt ist also, diese FM-Modulation rückgängig zu machen. Eine andere Art, darüber nachzudenken: Die Information ist in der Frequenzvariation des empfangenen Signals gespeichert, und wir möchten es demodulieren, sodass die Information jetzt in der Amplitude und nicht in der Frequenz ist. Beachte, dass die Ausgabe dieser Demodulation ein reales Signal ist, obwohl wir ein komplexes Signal eingegeben haben. + +Was diese einzelne Python-Zeile tut, ist zunächst das Produkt unseres Signals mit einer verzögerten und konjugierten Version unseres Signals zu berechnen. Dann findet sie die Phase jedes Abtastwerts in diesem Ergebnis, was der Moment ist, an dem es von komplex zu real wechselt. Um uns zu beweisen, dass dies uns die in den Frequenzvariation enthaltene Information liefert, betrachte einen Ton bei Frequenz :math:`f` mit einer beliebigen Phase :math:`\phi`, den wir als :math:`e^{j2 \pi (f t + \phi)}` darstellen können. Bei der diskreten Zeit, die ein ganzzahliges :math:`n` anstelle von :math:`t` verwendet, wird dies zu :math:`e^{j2 \pi (f n + \phi)}`. Die konjugierte und verzögerte Version ist :math:`e^{-j2 \pi (f (n-1) + \phi)}`. Das Multiplizieren dieser beiden ergibt :math:`e^{j2 \pi f}`, was großartig ist, weil :math:`\phi` verschwunden ist, und wenn wir die Phase dieses Ausdrucks berechnen, bleibt nur :math:`f` übrig. + +Ein praktischer Nebeneffekt der FM-Modulation ist, dass Amplitudenvariationen des empfangenen Signals die Lautstärke des Audios tatsächlich nicht verändern, im Gegensatz zum AM-Radio. + +******************************** +Frequenzverschiebung +******************************** + +.. code-block:: python + + # Frequenzverschiebung + N = len(x) + f_o = -57e3 # Betrag der Verschiebung + t = np.arange(N)/sample_rate # Zeitvektor + x = x * np.exp(2j*np.pi*f_o*t) # Abwärtsverschiebung + +Als nächstes verschieben wir die Frequenz um 57 kHz nach unten, mit dem :math:`e^{j2 \pi f_ot}`-Trick aus dem Kapitel :ref:`sync-chapter`, wobei :code:`f_o` die Frequenzverschiebung in Hz und :code:`t` einfach ein Zeitvektor ist. Da es ein reales Signal ist, das eingegeben wird, ist es eigentlich egal, ob du -57 oder +57 kHz verwendest, da die negativen Frequenzen den positiven entsprechen, sodass wir in jedem Fall unser RDS auf 0 Hz verschoben bekommen. + +******************************** +Filter zum Isolieren von RDS +******************************** + +.. code-block:: python + + # Tiefpassfilter + taps = firwin(numtaps=101, cutoff=7.5e3, fs=sample_rate) + x = np.convolve(x, taps, 'valid') + +Nun müssen wir alles außer RDS herausfiltern. Da wir RDS bei 0 Hz zentriert haben, ist ein Tiefpassfilter genau das, was wir möchten. Wir verwenden :code:`firwin()`, um einen FIR-Filter zu entwerfen (d.h. die Taps zu finden), der nur wissen muss, wie viele Taps wir möchten und die Grenzfrequenz. Die Abtastrate muss ebenfalls angegeben werden, sonst ergibt die Grenzfrequenz für firwin keinen Sinn. Das Ergebnis ist ein symmetrischer Tiefpassfilter, sodass wir wissen, dass die Taps reelle Zahlen sein werden, und wir können den Filter auf unser Signal mittels Faltung anwenden. Wir wählen :code:`'valid'`, um die Randeffekte der Faltung loszuwerden, obwohl es in diesem Fall keine Rolle spielt, da wir ein so langes Signal einspeisen, dass ein paar merkwürdige Abtastwerte an beiden Rändern nichts durcheinanderbringen werden. + +Randnotiz: Irgendwann werde ich den obigen Filter aktualisieren, um ein richtiges Matched Filter (Root-Raised-Cosine, glaube ich, was RDS verwendet) zu verwenden. Ich erhielt jedoch dieselben Fehlerraten mit dem firwin()-Ansatz wie mit dem korrekten Matched Filter in GNU Radio, also ist es offensichtlich keine strenge Anforderung. + +******************************** +Um 10 dezimieren +******************************** + +.. code-block:: python + + # Um 10 dezimieren, jetzt wo wir gefiltert haben und kein Aliasing auftreten wird + x = x[::10] + sample_rate = 25e3 + +Immer wenn du auf einen kleinen Bruchteil deiner Bandbreite herausfiltert (z.B. haben wir mit 125 kHz *echter* Bandbreite begonnen und nur 7,5 kHz davon behalten), macht es Sinn zu dezimieren. Erinnere dich an den Anfang des Kapitels :ref:`sampling-chapter`, wo wir über die Nyquist-Rate und die Fähigkeit lernten, bandbegrenzte Informationen vollständig zu speichern, solange wir mit der doppelten Höchstfrequenz abtasten. Jetzt, da wir unseren Tiefpassfilter verwendet haben, liegt unsere Höchstfrequenz bei etwa 7,5 kHz, sodass wir nur eine Abtastrate von 15 kHz benötigen. Sicherheitshalber fügen wir etwas Spielraum hinzu und verwenden eine neue Abtastrate von 25 kHz (das funktioniert später mathematisch gut). + +Wir führen die Dezimierung durch, indem wir einfach 9 von je 10 Abtastwerten verwerfen, da wir vorher bei 250 kHz waren und jetzt 25 kHz haben möchten. Das mag zunächst verwirrend erscheinen, da das Verwerfen von 90% der Abtastwerte sich anfühlt, als würde man Informationen wegwerfen. Wenn du jedoch das Kapitel :ref:`sampling-chapter` durchgehst, wirst du sehen, warum wir nichts verlieren, weil wir ordnungsgemäß gefiltert haben (was als Anti-Aliasing-Filter fungierte) und unsere Maximalfrequenz und damit die Signalbandbreite reduziert haben. + +Aus Code-Perspektive ist dies wahrscheinlich der einfachste Schritt von allen, aber vergiss nicht, deine Variable :code:`sample_rate` auf die neue Abtastrate zu aktualisieren. + +******************************** +Auf 19 kHz resamplen +******************************** + +.. code-block:: python + + # Auf 19 kHz resamplen + x = resample_poly(x, 19, 25) # hoch, runter + sample_rate = 19e3 + +Im Kapitel :ref:`pulse-shaping-chapter` haben wir das Konzept der "Abtastwerte pro Symbol" gefestigt und die Bequemlichkeit einer ganzzahligen Anzahl von Abtastwerten pro Symbol gelernt (ein Bruchwert ist gültig, aber nicht praktisch). Wie zuvor erwähnt, verwendet RDS BPSK und überträgt 1187,5 Symbole pro Sekunde. Wenn wir unser Signal wie bisher bei 25 kHz belassen, haben wir 21,052631579 Abtastwerte pro Symbol (denke kurz über die Mathematik nach, wenn das nicht stimmt). Was wir wirklich wollen, ist eine Abtastrate, die ein ganzzahliges Vielfaches von 1187,5 Hz ist, aber wir können nicht zu niedrig gehen, sonst können wir nicht die volle Bandbreite des Signals "speichern". Im vorherigen Unterabschnitt haben wir über die Notwendigkeit einer Abtastrate von mindestens 15 kHz gesprochen, und wir haben 25 kHz gewählt, um etwas Spielraum zu haben. + +Die beste Abtastrate zum Resamplen hängt davon ab, wie viele Abtastwerte pro Symbol wir möchten. Angenommen, wir zielen auf 10 Abtastwerte pro Symbol ab. Die RDS-Symbolrate von 1187,5 multipliziert mit 10 würde uns eine Abtastrate von 11,875 kHz geben, was leider nicht hoch genug für Nyquist ist. Was ist mit 13 Abtastwerten pro Symbol? 1187,5 multipliziert mit 13 ergibt 15437,5 Hz, was über 15 kHz liegt, aber eine ziemlich ungerade Zahl ist. Wie wäre es mit der nächsten Zweierpotenz, also 16 Abtastwerte pro Symbol? 1187,5 multipliziert mit 16 ist genau 19 kHz! Die gerade Zahl ist weniger Zufall als eine bewusste Protokolldesign-Entscheidung. + +Um von 25 kHz auf 19 kHz zu resamplen, verwenden wir :code:`resample_poly()`, das um einen ganzzahligen Wert aufwärts sampelt, filtert und dann um einen ganzzahligen Wert abwärts sampelt. Das ist praktisch, weil wir anstelle von 25000 und 19000 einfach 25 und 19 eingeben können. Wenn wir 13 Abtastwerte pro Symbol mit einer Abtastrate von 15437,5 Hz verwendet hätten, könnten wir :code:`resample_poly()` nicht verwenden und der Resampling-Prozess wäre viel komplizierter. + +Denke immer daran, deine Variable :code:`sample_rate` zu aktualisieren, wenn du eine Operation durchführst, die sie ändert. + +*********************************** +Zeitsynchronisation (Symbolebene) +*********************************** + +.. code-block:: python + + # Symbolsynchronisation, wie im Sync-Kapitel gemacht + samples = x # um mit dem Sync-Kapitel übereinzustimmen + samples_interpolated = resample_poly(samples, 32, 1) # wir verwenden 32 als Interpolationsfaktor, beliebig gewählt + sps = 16 + mu = 0.01 # Anfangsschätzung der Phase des Abtastwerts + out = np.zeros(len(samples) + 10, dtype=np.complex64) + out_rail = np.zeros(len(samples) + 10, dtype=np.complex64) # speichert Werte; jede Iteration braucht die vorherigen 2 Werte plus aktuellen + i_in = 0 # Eingangs-Abtastwert-Index + i_out = 2 # Ausgangsindex (erste zwei Ausgaben sind 0) + while i_out < len(samples) and i_in+32 < len(samples): + out[i_out] = samples_interpolated[i_in*32 + int(mu*32)] # den vermeintlich "besten" Abtastwert nehmen + out_rail[i_out] = int(np.real(out[i_out]) > 0) + 1j*int(np.imag(out[i_out]) > 0) + x = (out_rail[i_out] - out_rail[i_out-2]) * np.conj(out[i_out-1]) + y = (out[i_out] - out[i_out-2]) * np.conj(out_rail[i_out-1]) + mm_val = np.real(y - x) + mu += sps + 0.01*mm_val + i_in += int(np.floor(mu)) # auf nächste ganze Zahl abrunden, da als Index verwendet + mu = mu - np.floor(mu) # ganzzahligen Teil von mu entfernen + i_out += 1 # Ausgangsindex erhöhen + x = out[2:i_out] # erste zwei entfernen und alles nach i_out (das nie befüllt wurde) + +Wir sind endlich bereit für unsere Symbol/Zeitsynchronisation. Hier verwenden wir denselben Mueller-und-Müller-Taktwiederherstellungscode aus dem Kapitel :ref:`sync-chapter`. Verweise darauf, wenn du mehr darüber erfahren möchtest, wie es funktioniert. Wir setzen die Abtastwerte pro Symbol (:code:`sps`) auf 16, wie zuvor besprochen. Ein mu-Verstärkungswert von 0,01 wurde durch Experimentieren als gut funktionierend gefunden. Die Ausgabe sollte nun ein Abtastwert pro Symbol sein, d.h. unsere Ausgabe sind unsere "weichen Symbole", möglicherweise mit eingeschlossenem Frequenzoffset. Das folgende Konstellationsplot-Animation wird verwendet, um zu überprüfen, dass wir BPSK-Symbole erhalten (mit einem Frequenzoffset, der eine Rotation verursacht): + +.. image:: ../_images/constellation-animated.gif + :scale: 80 % + :align: center + :alt: Animation von BPSK, das sich dreht, weil noch keine feine Frequenzsynchronisation durchgeführt wurde + +Wenn du dein eigenes FM-Signal verwendest und an diesem Punkt keine zwei deutlichen Cluster von komplexen Abtastwerten erhältst, bedeutet das entweder, dass die Symbolsynchronisation oben fehlgeschlagen ist, oder dass etwas mit einem der vorherigen Schritte nicht stimmt. Du musst die Konstellation nicht animieren, aber wenn du sie plottest, vermeide es, alle Abtastwerte zu plotten, da es nur wie ein Kreis aussieht. Wenn du nur 100 oder 200 Abtastwerte auf einmal plottest, bekommst du ein besseres Gefühl dafür, ob sie sich in zwei Clustern befinden oder nicht, auch wenn sie sich drehen. + +******************************** +Feine Frequenzsynchronisation +******************************** + +.. code-block:: python + + # Feine Frequenzsynchronisation + samples = x # um mit dem Sync-Kapitel übereinzustimmen + N = len(samples) + phase = 0 + freq = 0 + # Diese zwei Parameter sind anzupassen, um den Feedback-Regelkreis schneller oder langsamer zu machen + alpha = 8.0 + beta = 0.02 + out = np.zeros(N, dtype=np.complex64) + freq_log = [] + for i in range(N): + out[i] = samples[i] * np.exp(-1j*phase) # Eingabe um das Inverse des geschätzten Phasenoffsets anpassen + error = np.real(out[i]) * np.imag(out[i]) # Fehlerformel für Costas-Regelkreis 2. Ordnung (z.B. für BPSK) + + # Regelkreis fortschreiben (Phase und Frequenzoffset neu berechnen) + freq += (beta * error) + freq_log.append(freq * sample_rate / (2*np.pi)) # von Winkelgeschwindigkeit in Hz umrechnen zum Protokollieren + phase += freq + (alpha * error) + + # Optional: Phase so anpassen, dass sie immer zwischen 0 und 2pi liegt + while phase >= 2*np.pi: + phase -= 2*np.pi + while phase < 0: + phase += 2*np.pi + x = out + +Wir kopieren auch den Python-Code für die feine Frequenzsynchronisation aus dem Kapitel :ref:`sync-chapter`, der einen Costas-Regelkreis verwendet, um den verbleibenden Frequenzoffset zu entfernen und unser BPSK auf die reale (I)-Achse auszurichten, indem Q so nahe wie möglich an null gebracht wird. Alles, was in Q verbleibt, ist wahrscheinlich auf das Rauschen im Signal zurückzuführen, vorausgesetzt, der Costas-Regelkreis wurde ordnungsgemäß eingestellt. Aus Spaß sehen wir dieselbe Animation wie oben, aber nach der Frequenzsynchronisation (kein Drehen mehr!): + +.. image:: ../_images/constellation-animated-postcostas.gif + :scale: 80 % + :align: center + :alt: Animation des Frequenzsynchronisationsprozesses mit einem Costas-Regelkreis + +Zusätzlich können wir den geschätzten Frequenzfehler über die Zeit betrachten, um den Costas-Regelkreis bei der Arbeit zu sehen – beachte, wie wir ihn im obigen Code protokolliert haben. Es scheint, dass etwa 13 Hz Frequenzoffset vorhanden waren, entweder aufgrund des Oszillators/LO des Senders oder des LO des Empfängers (höchstwahrscheinlich des Empfängers). Wenn du dein eigenes FM-Signal verwendest, musst du möglicherweise :code:`alpha` und :code:`beta` optimieren, bis die Kurve ähnlich aussieht; sie sollte die Synchronisation recht schnell erreichen (z.B. ein paar hundert Symbole) und sie mit minimaler Schwingung aufrechterhalten. Das Muster, das du unten nach Erreichen des Gleichgewichtszustands siehst, ist Frequenz-Jitter, keine Schwingung. + +.. image:: ../_images/freq_error.png + :scale: 40 % + :align: center + :alt: Der Frequenzsynchronisationsprozess mit einem Costas-Regelkreis zeigt den geschätzten Frequenzoffset über die Zeit + +******************************** +BPSK demodulieren +******************************** + +.. code-block:: python + + # BPSK demodulieren + bits = (np.real(x) > 0).astype(int) # 1er und 0er + +Das Demodulieren des BPSK ist an diesem Punkt sehr einfach: Jeder Abtastwert repräsentiert ein weiches Symbol, also müssen wir nur prüfen, ob jeder Abtastwert über oder unter 0 liegt. Das :code:`.astype(int)` sorgt dafür, dass wir mit einem Array von Integer-Werten statt einem Array von Booleans arbeiten. Du magst dich fragen, ob über/unter null 1 oder 0 repräsentiert. Wie du im nächsten Schritt sehen wirst, ist das egal! + +******************************** +Differentielle Dekodierung +******************************** + +.. code-block:: python + + # Differentielle Dekodierung, damit es keine Rolle spielt, ob unser BPSK um 180 Grad gedreht war + bits = (bits[1:] - bits[0:-1]) % 2 + bits = bits.astype(np.uint8) # für den Decoder + +Das BPSK-Signal verwendete bei seiner Erstellung differentielle Codierung, was bedeutet, dass jede 1 und 0 der ursprünglichen Daten so transformiert wurde, dass ein Wechsel von 1 zu 0 oder 0 zu 1 auf 1 gemappt wurde und kein Wechsel auf 0. Der nette Vorteil der differenziellen Codierung ist, dass man sich keine Sorgen über 180-Grad-Rotationen beim BPSK-Empfang machen muss, da es keine Rolle mehr spielt, ob wir 1 als größer oder kleiner als null betrachten. Was zählt, ist die Änderung zwischen 1 und 0. Dieses Konzept ist vielleicht leichter zu verstehen, indem man Beispieldaten betrachtet; unten werden die ersten 10 Symbole vor und nach der differenziellen Dekodierung gezeigt: + +.. code-block:: python + + [1 1 1 1 0 1 0 0 1 1] # vor der differenziellen Dekodierung + [- 0 0 0 1 1 1 0 1 0] # nach der differenziellen Dekodierung + +******************************** +RDS-Dekodierung +******************************** + +Wir haben endlich unsere Informationsbits und sind bereit, zu dekodieren, was sie bedeuten! Der unten bereitgestellte massive Codeblock ist das, was wir verwenden, um die 1er und 0er in Gruppen von Bytes zu dekodieren. Dieser Teil würde viel mehr Sinn ergeben, wenn wir zuerst den Senderteil von RDS erstellt hätten, aber wisse vorerst, dass in RDS Bytes in Gruppen von 12 Bytes gruppiert sind, wobei die ersten 8 die Daten darstellen und die letzten 4 als Synchronisationswort (sogenannte "Offset-Wörter") fungieren. Die letzten 4 Bytes werden vom nächsten Schritt (dem Parser) nicht benötigt, daher fügen wir sie nicht in die Ausgabe ein. Dieser Codeblock nimmt die oben erstellten 1er und 0er (in Form eines 1D-Arrays von uint8) auf und gibt eine Liste von Listen von Bytes aus (eine Liste von 8 Bytes, wobei diese 8 Bytes in einer Liste sind). Das macht es für den nächsten Schritt praktisch, der durch die Liste von 8 Bytes iteriert, eine Gruppe von 8 nach der anderen. + +Der größte Teil des eigentlichen Dekodierungscodes dreht sich um Synchronisation (auf Byte-Ebene, nicht Symbol) und Fehlerprüfung. Er arbeitet in Blöcken von 104 Bits; jeder Block wird entweder korrekt oder fehlerhaft empfangen (mit CRC zur Prüfung), und alle 50 Blöcke wird geprüft, ob mehr als 35 davon mit Fehler empfangen wurden. In diesem Fall wird alles zurückgesetzt und versucht, erneut zu synchronisieren. Die CRC wird mit einer 10-Bit-Prüfung und Polynom :math:`x^{10}+x^8+x^7+x^5+x^4+x^3+1` durchgeführt; dies geschieht, wenn :code:`reg` mit 0x5B9 XOR-verknüpft wird, was dem binären Äquivalent dieses Polynoms entspricht. In Python sind die bitweisen Operatoren für [und, oder, nicht, xor] :code:`& | ~ ^`, genau wie in C++. Ein Linksbitshift ist :code:`x << y` (entspricht der Multiplikation von x mit 2**y), und ein Rechtsbitshift ist :code:`x >> y` (entspricht der Division von x durch 2**y), ebenfalls wie in C++. + +Beachte: Du **musst** diesen Code nicht durchgehen, besonders wenn du dich auf die Physical Layer (PHY) Seite von DSP und SDR konzentrierst, da dies *keine* Signalverarbeitung darstellt. Dieser Code ist lediglich eine Implementierung eines RDS-Decoders, und im Wesentlichen kann nichts davon für andere Protokolle wiederverwendet werden, da er so spezifisch für die Funktionsweise von RDS ist. + +.. code-block:: python + + # Konstanten + syndrome = [383, 14, 303, 663, 748] + offset_pos = [0, 1, 2, 3, 2] + offset_word = [252, 408, 360, 436, 848] + + # siehe Anhang B, Seite 64 des Standards + def calc_syndrome(x, mlen): + reg = 0 + plen = 10 + for ii in range(mlen, 0, -1): + reg = (reg << 1) | ((x >> (ii-1)) & 0x01) + if (reg & (1 << plen)): + reg = reg ^ 0x5B9 + for ii in range(plen, 0, -1): + reg = reg << 1 + if (reg & (1 << plen)): + reg = reg ^ 0x5B9 + return reg & ((1 << plen) - 1) # untere plen Bits von reg auswählen + + # Alle Arbeitsvariablen initialisieren, die wir während der Schleife benötigen + synced = False + presync = False + + wrong_blocks_counter = 0 + blocks_counter = 0 + group_good_blocks_counter = 0 + + reg = np.uint32(0) # war unsigned long in C++ (64 Bit), aber numpy unterstützt keine bitweisen Ops von uint64 + lastseen_offset_counter = 0 + lastseen_offset = 0 + + # der Synchronisationsprozess ist in Anhang C, Seite 66 des Standards beschrieben + bytes_out = [] + for i in range(len(bits)): + # in C++ wird reg nicht initialisiert, also ist es anfangs zufällig; bei uns sind es 0er + # bits sind entweder 0 oder 1 + reg = np.bitwise_or(np.left_shift(reg, 1), bits[i]) # reg enthält die letzten 26 RDS-Bits + if not synced: + reg_syndrome = calc_syndrome(reg, 26) + for j in range(5): + if reg_syndrome == syndrome[j]: + if not presync: + lastseen_offset = j + lastseen_offset_counter = i + presync = True + else: + if offset_pos[lastseen_offset] >= offset_pos[j]: + block_distance = offset_pos[j] + 4 - offset_pos[lastseen_offset] + else: + block_distance = offset_pos[j] - offset_pos[lastseen_offset] + if (block_distance*26) != (i - lastseen_offset_counter): + presync = False + else: + print('Sync State Detected') + wrong_blocks_counter = 0 + blocks_counter = 0 + block_bit_counter = 0 + block_number = (j + 1) % 4 + group_assembly_started = False + synced = True + break # Syndrom gefunden, keine weiteren Zyklen + + else: # SYNCHRONISIERT + # warten bis 26 Bits in den Puffer kommen + if block_bit_counter < 25: + block_bit_counter += 1 + else: + good_block = False + dataword = (reg >> 10) & 0xffff + block_calculated_crc = calc_syndrome(dataword, 16) + checkword = reg & 0x3ff + if block_number == 2: # Sonderfall von C oder C' Offset-Wort verwalten + block_received_crc = checkword ^ offset_word[block_number] + if (block_received_crc == block_calculated_crc): + good_block = True + else: + block_received_crc = checkword ^ offset_word[4] + if (block_received_crc == block_calculated_crc): + good_block = True + else: + wrong_blocks_counter += 1 + good_block = False + else: + block_received_crc = checkword ^ offset_word[block_number] # bitweises XOR + if block_received_crc == block_calculated_crc: + good_block = True + else: + wrong_blocks_counter += 1 + good_block = False + + # CRC-Prüfung abgeschlossen + if block_number == 0 and good_block: + group_assembly_started = True + group_good_blocks_counter = 1 + group = bytearray(8) # 8 Bytes mit 0er befüllt + if group_assembly_started: + if not good_block: + group_assembly_started = False + else: + # rohe Datenbytes, wie von RDS empfangen + group[block_number*2] = (dataword >> 8) & 255 + group[block_number*2+1] = dataword & 255 + group_good_blocks_counter += 1 + if group_good_blocks_counter == 5: + bytes_out.append(group) # Liste von Länge-8-Listen von Bytes + block_bit_counter = 0 + block_number = (block_number + 1) % 4 + blocks_counter += 1 + if blocks_counter == 50: + if wrong_blocks_counter > 35: # So viele falsche Blöcke bedeuten Sync-Verlust + print("Lost Sync (Got ", wrong_blocks_counter, " bad blocks on ", blocks_counter, " total)") + synced = False + presync = False + else: + print("Still Sync-ed (Got ", wrong_blocks_counter, " bad blocks on ", blocks_counter, " total)") + blocks_counter = 0 + wrong_blocks_counter = 0 + +Unten ist eine Beispielausgabe dieses Dekodierungsschritts. Beachte, wie in diesem Beispiel die Synchronisation recht schnell erfolgte, aber dann aus irgendeinem Grund ein paar Mal verloren geht, obwohl alle Daten noch geparst werden können, wie wir sehen werden. Wenn du die herunterladbare 1M-Abtastwerte-Datei verwendest, siehst du nur die ersten paar Zeilen unten. Der tatsächliche Inhalt dieser Bytes sieht je nach Darstellung wie Zufallszahlen/-zeichen aus, aber im nächsten Schritt werden wir sie in menschenlesbare Informationen parsen! + +.. code-block:: console + + Sync State Detected + Still Sync-ed (Got 0 bad blocks on 50 total) + Still Sync-ed (Got 0 bad blocks on 50 total) + Still Sync-ed (Got 0 bad blocks on 50 total) + Still Sync-ed (Got 0 bad blocks on 50 total) + Still Sync-ed (Got 1 bad blocks on 50 total) + Still Sync-ed (Got 5 bad blocks on 50 total) + Still Sync-ed (Got 26 bad blocks on 50 total) + Lost Sync (Got 50 bad blocks on 50 total) + Sync State Detected + Still Sync-ed (Got 3 bad blocks on 50 total) + ... + +******************************** +RDS-Parsen +******************************** + +Da wir jetzt Bytes in Gruppen von 8 haben, können wir die endgültigen Daten extrahieren, d.h. die endgültige Ausgabe, die für Menschen verständlich ist. Dies ist als Parsen der Bytes bekannt, und wie der Decoder im vorherigen Abschnitt ist es einfach eine Implementierung des RDS-Protokolls und ist wirklich nicht so wichtig zu verstehen. Glücklicherweise ist es nicht viel Code, wenn man die zwei am Anfang definierten Tabellen nicht einrechnet, die einfach die Lookup-Tabellen für den Typ des UKW-Kanals und das Versorgungsgebiet sind. + +Für diejenigen, die lernen möchten, wie dieser Code funktioniert, gebe ich einige zusätzliche Informationen. Das Protokoll verwendet das Konzept eines A/B-Flags, was bedeutet, dass einige Nachrichten als A und andere als B markiert sind, und das Parsen ändert sich je nachdem, ob es A oder B ist (ob es A oder B ist, wird im dritten Bit des zweiten Bytes gespeichert). Es verwendet auch verschiedene "Gruppen"-Typen, die Nachrichtentypen ähneln. In diesem Code parsen wir nur Nachrichtentyp 2, das ist der Nachrichtentyp, der den Radiotext enthält – das ist der interessante Teil: der Text, der auf dem Display im Auto scrollt. Wir werden immer noch in der Lage sein, den Kanaltyp und die Region zu parsen, da sie in jeder Nachricht gespeichert sind. Beachte, dass :code:`radiotext` eine Zeichenkette ist, die auf lauter Leerzeichen initialisiert wird, langsam befüllt wird während Bytes geparst werden, und dann zu lauter Leerzeichen zurückgesetzt wird, wenn eine bestimmte Gruppe von Bytes empfangen wird. + +.. code-block:: python + + # Anhang F des RBDS-Standards Tabelle F.1 (Nordamerika) und Tabelle F.2 (Europa) + # Europa Nordamerika + pty_table = [["Undefined", "Undefined"], + ["News", "News"], + ["Current Affairs", "Information"], + ["Information", "Sports"], + ["Sport", "Talk"], + ["Education", "Rock"], + ["Drama", "Classic Rock"], + ["Culture", "Adult Hits"], + ["Science", "Soft Rock"], + ["Varied", "Top 40"], + ["Pop Music", "Country"], + ["Rock Music", "Oldies"], + ["Easy Listening", "Soft"], + ["Light Classical", "Nostalgia"], + ["Serious Classical", "Jazz"], + ["Other Music", "Classical"], + ["Weather", "Rhythm & Blues"], + ["Finance", "Soft Rhythm & Blues"], + ["Children's Programmes", "Language"], + ["Social Affairs", "Religious Music"], + ["Religion", "Religious Talk"], + ["Phone-In", "Personality"], + ["Travel", "Public"], + ["Leisure", "College"], + ["Jazz Music", "Spanish Talk"], + ["Country Music", "Spanish Music"], + ["National Music", "Hip Hop"], + ["Oldies Music", "Unassigned"], + ["Folk Music", "Unassigned"], + ["Documentary", "Weather"], + ["Alarm Test", "Emergency Test"], + ["Alarm", "Emergency"]] + pty_locale = 1 # auf 0 für Europa setzen, um die erste Spalte zu verwenden + + # Seite 72, Anhang D, Tabelle D.2 im Standard + coverage_area_codes = ["Local", + "International", + "National", + "Supra-regional", + "Regional 1", + "Regional 2", + "Regional 3", + "Regional 4", + "Regional 5", + "Regional 6", + "Regional 7", + "Regional 8", + "Regional 9", + "Regional 10", + "Regional 11", + "Regional 12"] + + radiotext_AB_flag = 0 + radiotext = [' ']*65 + first_time = True + for group in bytes_out: + group_0 = group[1] | (group[0] << 8) + group_1 = group[3] | (group[2] << 8) + group_2 = group[5] | (group[4] << 8) + group_3 = group[7] | (group[6] << 8) + + group_type = (group_1 >> 12) & 0xf # Bedeutung: ["BASIC", "PIN/SL", "RT", "AID", "CT", "TDC", "IH", "RP", "TMC", "EWS", "___", "___", "___", "___", "EON", "___"] + AB = (group_1 >> 11 ) & 0x1 # b wenn 1, a wenn 0 + + program_identification = group_0 # "PI" + + program_type = (group_1 >> 5) & 0x1f # "PTY" + pty = pty_table[program_type][pty_locale] + + pi_area_coverage = (program_identification >> 8) & 0xf + coverage_area = coverage_area_codes[pi_area_coverage] + + pi_program_reference_number = program_identification & 0xff # einfach ein Int + + if first_time: + print("PTY:", pty) + print("program:", pi_program_reference_number) + print("coverage_area:", coverage_area) + first_time = False + + if group_type == 2: + # wenn das A/B-Flag umgeschaltet wird, aktuellen Radiotext leeren + if radiotext_AB_flag != ((group_1 >> 4) & 0x01): + radiotext = [' ']*65 + radiotext_AB_flag = (group_1 >> 4) & 0x01 + text_segment_address_code = group_1 & 0x0f + if AB: + radiotext[text_segment_address_code * 2 ] = chr((group_3 >> 8) & 0xff) + radiotext[text_segment_address_code * 2 + 1] = chr(group_3 & 0xff) + else: + radiotext[text_segment_address_code *4 ] = chr((group_2 >> 8) & 0xff) + radiotext[text_segment_address_code * 4 + 1] = chr(group_2 & 0xff) + radiotext[text_segment_address_code * 4 + 2] = chr((group_3 >> 8) & 0xff) + radiotext[text_segment_address_code * 4 + 3] = chr(group_3 & 0xff) + print(''.join(radiotext)) + else: + pass + +Unten ist die Ausgabe des Parse-Schritts für ein Beispiel-UKW-Sender. Beachte, wie er die Radiotext-Zeichenkette über mehrere Nachrichten aufbauen muss und dann periodisch die Zeichenkette löscht und von vorne beginnt. Wenn du die 1M-Abtastwerte-Datei verwendest, siehst du nur die ersten paar Zeilen unten. + +.. code-block:: console + + PTY: Top 40 + program: 29 + coverage_area: Regional 4 + ing. + ing. Upb + ing. Upbeat. + ing. Upbeat. Rea + + WAY- + WAY-FM U + WAY-FM Uplif + WAY-FM Uplifting + WAY-FM Uplifting. Up + WAY-FM Uplifting. Upbeat + WAY-FM Uplifting. Upbeat. Re + + WayF + WayFM Up + WayFM Uplift + WayFM Uplifting. + WayFM Uplifting. Upb + WayFM Uplifting. Upbeat. + WayFM Uplifting. Upbeat. Rea + + + +******************************** +Abschluss und endgültiger Code +******************************** + +Geschafft! Unten ist der gesamte obige Code zusammengefügt. Er sollte mit der `Test-UKW-Radioaufzeichnung, die du hier findest `_, funktionieren. Du solltest auch dein eigenes Signal einspeisen können, solange es mit ausreichend hohem SNR empfangen wird. Wenn du Anpassungen vornehmen musstest, um es mit deiner eigenen Aufzeichnung oder einem Live-SDR zum Laufen zu bringen, teile uns mit, was du getan hast; du kannst es als GitHub-PR auf `der GitHub-Seite des Lehrbuchs `_ einreichen. Eine Version dieses Codes mit Dutzenden von Debug-Plots/Prints `findest du hier `_. + +.. raw:: html + +
+ Endgültiger Code + +.. code-block:: python + + import numpy as np + from scipy.signal import resample_poly, firwin, bilinear, lfilter + import matplotlib.pyplot as plt + + # Signal einlesen + x = np.fromfile('/your/path/fm_rds_250k_1Msamples.iq', dtype=np.complex64) + sample_rate = 250e3 + center_freq = 99.5e6 + + # Quadratur-Demodulation + x = 0.5 * np.angle(x[0:-1] * np.conj(x[1:])) + + # Frequenzverschiebung + N = len(x) + f_o = -57e3 + t = np.arange(N)/sample_rate + x = x * np.exp(2j*np.pi*f_o*t) + + # Tiefpassfilter + taps = firwin(numtaps=101, cutoff=7.5e3, fs=sample_rate) + x = np.convolve(x, taps, 'valid') + + # Um 10 dezimieren + x = x[::10] + sample_rate = 25e3 + + # Auf 19 kHz resamplen + x = resample_poly(x, 19, 25) + sample_rate = 19e3 + + # Symbolsynchronisation + samples = x + samples_interpolated = resample_poly(samples, 32, 1) + sps = 16 + mu = 0.01 + out = np.zeros(len(samples) + 10, dtype=np.complex64) + out_rail = np.zeros(len(samples) + 10, dtype=np.complex64) + i_in = 0 + i_out = 2 + while i_out < len(samples) and i_in+32 < len(samples): + out[i_out] = samples_interpolated[i_in*32 + int(mu*32)] + out_rail[i_out] = int(np.real(out[i_out]) > 0) + 1j*int(np.imag(out[i_out]) > 0) + x = (out_rail[i_out] - out_rail[i_out-2]) * np.conj(out[i_out-1]) + y = (out[i_out] - out[i_out-2]) * np.conj(out_rail[i_out-1]) + mm_val = np.real(y - x) + mu += sps + 0.01*mm_val + i_in += int(np.floor(mu)) + mu = mu - np.floor(mu) + i_out += 1 + x = out[2:i_out] + + sample_rate /= 16 + + # Feine Frequenzsynchronisation + samples = x + N = len(samples) + phase = 0 + freq = 0 + alpha = 8.0 + beta = 0.02 + out = np.zeros(N, dtype=np.complex64) + freq_log = [] + for i in range(N): + out[i] = samples[i] * np.exp(-1j*phase) + error = np.real(out[i]) * np.imag(out[i]) + freq += (beta * error) + freq_log.append(freq * sample_rate / (2*np.pi)) + phase += freq + (alpha * error) + while phase >= 2*np.pi: + phase -= 2*np.pi + while phase < 0: + phase += 2*np.pi + x = out + + # BPSK demodulieren + bits = (np.real(x) > 0).astype(int) + + # Differentielle Dekodierung + bits = (bits[1:] - bits[0:-1]) % 2 + bits = bits.astype(np.uint8) + + ########### + # DECODER # + ########### + + syndrome = [383, 14, 303, 663, 748] + offset_pos = [0, 1, 2, 3, 2] + offset_word = [252, 408, 360, 436, 848] + + def calc_syndrome(x, mlen): + reg = 0 + plen = 10 + for ii in range(mlen, 0, -1): + reg = (reg << 1) | ((x >> (ii-1)) & 0x01) + if (reg & (1 << plen)): + reg = reg ^ 0x5B9 + for ii in range(plen, 0, -1): + reg = reg << 1 + if (reg & (1 << plen)): + reg = reg ^ 0x5B9 + return reg & ((1 << plen) - 1) + + synced = False + presync = False + wrong_blocks_counter = 0 + blocks_counter = 0 + group_good_blocks_counter = 0 + reg = np.uint32(0) + lastseen_offset_counter = 0 + lastseen_offset = 0 + bytes_out = [] + for i in range(len(bits)): + reg = np.bitwise_or(np.left_shift(reg, 1), bits[i]) + if not synced: + reg_syndrome = calc_syndrome(reg, 26) + for j in range(5): + if reg_syndrome == syndrome[j]: + if not presync: + lastseen_offset = j + lastseen_offset_counter = i + presync = True + else: + if offset_pos[lastseen_offset] >= offset_pos[j]: + block_distance = offset_pos[j] + 4 - offset_pos[lastseen_offset] + else: + block_distance = offset_pos[j] - offset_pos[lastseen_offset] + if (block_distance*26) != (i - lastseen_offset_counter): + presync = False + else: + print('Sync State Detected') + wrong_blocks_counter = 0 + blocks_counter = 0 + block_bit_counter = 0 + block_number = (j + 1) % 4 + group_assembly_started = False + synced = True + break + else: + if block_bit_counter < 25: + block_bit_counter += 1 + else: + good_block = False + dataword = (reg >> 10) & 0xffff + block_calculated_crc = calc_syndrome(dataword, 16) + checkword = reg & 0x3ff + if block_number == 2: + block_received_crc = checkword ^ offset_word[block_number] + if (block_received_crc == block_calculated_crc): + good_block = True + else: + block_received_crc = checkword ^ offset_word[4] + if (block_received_crc == block_calculated_crc): + good_block = True + else: + wrong_blocks_counter += 1 + good_block = False + else: + block_received_crc = checkword ^ offset_word[block_number] + if block_received_crc == block_calculated_crc: + good_block = True + else: + wrong_blocks_counter += 1 + good_block = False + if block_number == 0 and good_block: + group_assembly_started = True + group_good_blocks_counter = 1 + group = bytearray(8) + if group_assembly_started: + if not good_block: + group_assembly_started = False + else: + group[block_number*2] = (dataword >> 8) & 255 + group[block_number*2+1] = dataword & 255 + group_good_blocks_counter += 1 + if group_good_blocks_counter == 5: + bytes_out.append(group) + block_bit_counter = 0 + block_number = (block_number + 1) % 4 + blocks_counter += 1 + if blocks_counter == 50: + if wrong_blocks_counter > 35: + print("Lost Sync (Got ", wrong_blocks_counter, " bad blocks on ", blocks_counter, " total)") + synced = False + presync = False + else: + print("Still Sync-ed (Got ", wrong_blocks_counter, " bad blocks on ", blocks_counter, " total)") + blocks_counter = 0 + wrong_blocks_counter = 0 + + ########### + # PARSER # + ########### + + pty_table = [["Undefined", "Undefined"], + ["News", "News"], + ["Current Affairs", "Information"], + ["Information", "Sports"], + ["Sport", "Talk"], + ["Education", "Rock"], + ["Drama", "Classic Rock"], + ["Culture", "Adult Hits"], + ["Science", "Soft Rock"], + ["Varied", "Top 40"], + ["Pop Music", "Country"], + ["Rock Music", "Oldies"], + ["Easy Listening", "Soft"], + ["Light Classical", "Nostalgia"], + ["Serious Classical", "Jazz"], + ["Other Music", "Classical"], + ["Weather", "Rhythm & Blues"], + ["Finance", "Soft Rhythm & Blues"], + ["Children's Programmes", "Language"], + ["Social Affairs", "Religious Music"], + ["Religion", "Religious Talk"], + ["Phone-In", "Personality"], + ["Travel", "Public"], + ["Leisure", "College"], + ["Jazz Music", "Spanish Talk"], + ["Country Music", "Spanish Music"], + ["National Music", "Hip Hop"], + ["Oldies Music", "Unassigned"], + ["Folk Music", "Unassigned"], + ["Documentary", "Weather"], + ["Alarm Test", "Emergency Test"], + ["Alarm", "Emergency"]] + pty_locale = 1 + + coverage_area_codes = ["Local", + "International", + "National", + "Supra-regional", + "Regional 1", + "Regional 2", + "Regional 3", + "Regional 4", + "Regional 5", + "Regional 6", + "Regional 7", + "Regional 8", + "Regional 9", + "Regional 10", + "Regional 11", + "Regional 12"] + + radiotext_AB_flag = 0 + radiotext = [' ']*65 + first_time = True + for group in bytes_out: + group_0 = group[1] | (group[0] << 8) + group_1 = group[3] | (group[2] << 8) + group_2 = group[5] | (group[4] << 8) + group_3 = group[7] | (group[6] << 8) + group_type = (group_1 >> 12) & 0xf + AB = (group_1 >> 11 ) & 0x1 + program_identification = group_0 + program_type = (group_1 >> 5) & 0x1f + pty = pty_table[program_type][pty_locale] + pi_area_coverage = (program_identification >> 8) & 0xf + coverage_area = coverage_area_codes[pi_area_coverage] + pi_program_reference_number = program_identification & 0xff + if first_time: + print("PTY:", pty) + print("program:", pi_program_reference_number) + print("coverage_area:", coverage_area) + first_time = False + if group_type == 2: + if radiotext_AB_flag != ((group_1 >> 4) & 0x01): + radiotext = [' ']*65 + radiotext_AB_flag = (group_1 >> 4) & 0x01 + text_segment_address_code = group_1 & 0x0f + if AB: + radiotext[text_segment_address_code * 2 ] = chr((group_3 >> 8) & 0xff) + radiotext[text_segment_address_code * 2 + 1] = chr(group_3 & 0xff) + else: + radiotext[text_segment_address_code *4 ] = chr((group_2 >> 8) & 0xff) + radiotext[text_segment_address_code * 4 + 1] = chr(group_2 & 0xff) + radiotext[text_segment_address_code * 4 + 2] = chr((group_3 >> 8) & 0xff) + radiotext[text_segment_address_code * 4 + 3] = chr(group_3 & 0xff) + print(''.join(radiotext)) + else: + pass + +.. raw:: html + +
+ +Die Beispiel-UKW-Aufzeichnung, die mit diesem Code funktioniert, `findest du hier `_. + +Für diejenigen, die das eigentliche Audiosignal demodulieren möchten, füge einfach die folgenden Zeilen direkt nach dem Abschnitt "Ein Signal erfassen" ein (besonderer Dank an `Joel Cordeiro `_ für den Code): + +.. code-block:: python + + # Folgenden Code direkt nach dem Abschnitt "Ein Signal erfassen" einfügen + + from scipy.io import wavfile + + # Demodulation + x = np.diff(np.unwrap(np.angle(x))) + + # De-Emphasis-Filter, H(s) = 1/(RC*s + 1), implementiert als IIR via bilineare Transformation + bz, az = bilinear(1, [75e-6, 1], fs=sample_rate) + x = lfilter(bz, az, x) + + # um 6 dezimieren, um Mono-Audio zu erhalten + x = x[::6] + sample_rate_audio = sample_rate/6 + + # Lautstärke normalisieren, damit sie zwischen -1 und +1 liegt + x /= np.max(np.abs(x)) + + # manche Maschinen wollen int16 + x *= 32767 + x = x.astype(np.int16) + + # Als WAV-Datei speichern (z.B. in Audacity öffnen) + wavfile.write('fm.wav', int(sample_rate_audio), x) + +Der komplizierteste Teil ist der De-Emphasis-Filter, `über den du hier mehr erfahren kannst `_, obwohl er eigentlich ein optionaler Schritt ist, wenn du mit Audio mit unausgewogener Bass-/Höhenbalance einverstanden bist. Für Neugierige sieht die Frequenzantwort des `IIR `_-De-Emphasis-Filters so aus. Er filtert keine Frequenzen vollständig heraus, sondern ist eher ein "Formgebungs"-Filter. + +.. image:: ../_images/fm_demph_filter_freq_response.svg + :align: center + :target: ../_images/fm_demph_filter_freq_response.svg + +******************************** +Danksagungen +******************************** + +Die meisten der oben beschriebenen Schritte zum Empfangen von RDS wurden aus der GNU Radio-Implementierung von RDS adaptiert, die im GNU Radio Out-of-Tree-Modul namens `gr-rds `_ lebt, ursprünglich von Dimitrios Symeonidis erstellt und von Bastian Bloessl gepflegt. Ich möchte die Arbeit dieser Autoren würdigen. Um dieses Kapitel zu erstellen, begann ich damit, gr-rds in GNU Radio mit einer funktionierenden UKW-Aufzeichnung zu verwenden, und konvertierte jeden der Blöcke (einschließlich vieler eingebauter Blöcke) langsam zu Python. Es brauchte viel Zeit; es gibt einige Nuancen zu den eingebauten Blöcken, die leicht zu übersehen sind, und der Wechsel von stream-artigem Signalprozessieren (d.h. die Verwendung einer Work-Funktion, die einige tausend Abtastwerte auf zustandsbehaftete Weise verarbeitet) zu einem Python-Block ist nicht immer unkompliziert. GNU Radio ist ein erstaunliches Werkzeug für diese Art von Prototyping, und ich hätte nie all diesen funktionierenden Python-Code ohne es erstellen können. + +******************************** +Weiterführende Lektüre +******************************** + +#. https://en.wikipedia.org/wiki/Radio_Data_System +#. `https://www.sigidwiki.com/wiki/Radio_Data_System_(RDS) `_ +#. https://github.com/bastibl/gr-rds +#. https://www.gnuradio.org/ diff --git a/content-de/rtlsdr.rst b/content-de/rtlsdr.rst new file mode 100644 index 00000000..c8c3cd72 --- /dev/null +++ b/content-de/rtlsdr.rst @@ -0,0 +1,220 @@ +.. _rtlsdr-chapter: + +################## +RTL-SDR in Python +################## + +Das RTL-SDR ist mit einem Preis von etwa 30 USD das bei weitem günstigste SDR und ein hervorragender Einstieg in die Welt der Software-Defined Radios. +Obwohl es nur zum Empfangen geeignet ist und nur bis zu etwa 1,75 GHz abstimmen kann, gibt es zahlreiche Anwendungsmöglichkeiten. +In diesem Kapitel lernen wir, wie man die RTL-SDR-Software einrichtet und wie man dessen Python-API verwendet. + +.. image:: ../_images/rtlsdrs.svg + :align: center + :target: ../_images/rtlsdrs.svg + :alt: Beispiele für RTL-SDRs + +******************************** +Hintergrund zum RTL-SDR +******************************** + +Das RTL-SDR entstand um das Jahr 2010, als Enthusiasten herausfanden, dass sie günstige DVB-T-Dongles hacken konnten, die den Realtek RTL2832U-Chip enthielten. DVB-T ist ein digitaler Fernsehstandard, der hauptsächlich in Europa verwendet wird. +Das Besondere am RTL2832U war, dass die rohen IQ-Samples direkt abgerufen werden konnten, was es ermöglichte, den Chip als universelles Empfangs-SDR zu nutzen. + +Der RTL2832U-Chip enthält den Analog-Digital-Wandler (ADC) und den USB-Controller, muss jedoch mit einem HF-Tuner kombiniert werden. Verbreitete Tuner-Chips sind der Rafael Micro R820T, R828D und Elonics E4000. Der abstimmbare Frequenzbereich hängt vom Tuner-Chip ab und liegt typischerweise bei etwa 50 – 1700 MHz. Die maximale Abtastrate hingegen wird vom RTL2832U und dem USB-Bus des Computers bestimmt und beträgt ohne zu viele fehlende Samples üblicherweise etwa 2,4 MHz. Diese Tuner sind extrem günstig und weisen eine sehr schlechte HF-Empfindlichkeit auf; daher ist es oft notwendig, einen rauscharmen Verstärker (LNA) und einen Bandpassfilter hinzuzufügen, um schwache Signale zu empfangen. + +Der RTL2832U verwendet immer 8-Bit-Samples, sodass der Host-Rechner zwei Bytes pro IQ-Sample empfängt. Premium-RTL-SDRs sind in der Regel mit einem temperaturgesteuerten Oszillator (TCXO) anstelle des günstigeren Quarzoszillators ausgestattet, was eine bessere Frequenzstabilität bietet. Ein weiteres optionales Merkmal ist ein Bias-Tee (auch Bias-T genannt), eine integrierte Schaltung, die am SMA-Anschluss etwa 4,5 V Gleichspannung bereitstellt, um bequem einen externen LNA oder andere HF-Komponenten zu versorgen. Diese zusätzliche Gleichspannung liegt auf der HF-Seite des SDRs und stört den normalen Empfangsbetrieb nicht. + +Für diejenigen, die sich für Richtungsbestimmung (Direction of Arrival, DOA) oder andere Beamforming-Anwendungen interessieren, ist das `KrakenSDR `_ ein phasenkohärentes SDR, das aus fünf RTL-SDRs besteht, die sich einen Oszillator und einen Sample-Takt teilen. + +******************************** +Software-Einrichtung +******************************** + +Ubuntu (oder Ubuntu unter WSL) +############################### + +Auf Ubuntu 20, 22 und anderen Debian-basierten Systemen kann die RTL-SDR-Software mit folgendem Befehl installiert werden: + +.. code-block:: bash + + sudo apt install rtl-sdr + +Dadurch werden die librtlsdr-Bibliothek sowie Kommandozeilenwerkzeuge wie :code:`rtl_sdr`, :code:`rtl_tcp`, :code:`rtl_fm` und :code:`rtl_test` installiert. + +Anschließend wird der Python-Wrapper für librtlsdr installiert: + +.. code-block:: bash + + sudo pip install pyrtlsdr + +Falls Ubuntu über WSL verwendet wird, muss auf der Windows-Seite das neueste `Zadig `_ heruntergeladen und ausgeführt werden, um den „WinUSB"-Treiber für das RTL-SDR zu installieren (es kann zwei Bulk-In-Schnittstellen geben – in diesem Fall ist „WinUSB" auf beiden zu installieren). Danach das RTL-SDR einmal aus- und wieder einstecken. + +Als nächstes muss das RTL-SDR-USB-Gerät an WSL weitergeleitet werden. Dazu wird zunächst das neueste `usbipd utility msi `_ installiert (diese Anleitung geht von usbipd-win 4.0.0 oder höher aus). Dann wird PowerShell im Administratormodus geöffnet und folgendes ausgeführt: + +.. code-block:: bash + + # (RTL-SDR ausstecken) + usbipd list + # (RTL-SDR einstecken) + usbipd list + # (das neue Gerät finden und den Index im folgenden Befehl einsetzen) + usbipd bind --busid 1-5 + usbipd attach --wsl --busid 1-5 + +Auf der WSL-Seite sollte :code:`lsusb` ausgeführt werden können, und ein neues Gerät namens RTL2838 DVB-T oder ähnliches sollte erscheinen. + +Bei Berechtigungsproblemen (z. B. wenn der unten beschriebene Test nur mit :code:`sudo` funktioniert) müssen udev-Regeln eingerichtet werden. Zunächst :code:`lsusb` ausführen, um die ID des RTL-SDR zu finden, dann die Datei :code:`/etc/udev/rules.d/10-rtl-sdr.rules` mit folgendem Inhalt erstellen (idVendor und idProduct entsprechend anpassen, falls abweichend): + +.. code-block:: + + SUBSYSTEM=="usb", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="2838", MODE="0666" + +Um udev neu zu laden, folgenden Befehl ausführen: + +.. code-block:: bash + + sudo udevadm control --reload-rules + sudo udevadm trigger + +Falls unter WSL die Meldung :code:`Failed to send reload request: No such file or directory` erscheint, bedeutet dies, dass der udev-Dienst nicht läuft. In diesem Fall :code:`sudo nano /etc/wsl.conf` öffnen und folgende Zeilen hinzufügen: + +.. code-block:: bash + + [boot] + command="service udev start" + +Anschließend WSL mit folgendem Befehl in PowerShell als Administrator neu starten: :code:`wsl.exe --shutdown`. + +Möglicherweise muss das RTL-SDR auch aus- und wieder eingesteckt werden (unter WSL muss :code:`usbipd attach` erneut ausgeführt werden). + +Windows +################### + +Für Windows-Nutzer siehe https://www.rtl-sdr.com/rtl-sdr-quick-start-guide/. + +******************************** +RTL-SDR testen +******************************** + +Wenn die Software-Einrichtung erfolgreich war, sollte folgender Test ausführbar sein, der das RTL-SDR auf das UKW-Radioband abstimmt und 1 Million Samples in eine Datei namens :code:`recording.iq` unter :code:`/tmp` aufzeichnet: + +.. code-block:: bash + + rtl_sdr /tmp/recording.iq -s 2e6 -f 100e6 -n 1e6 + +Falls die Meldung :code:`No supported devices found` erscheint (auch beim Hinzufügen von :code:`sudo`) dann kann Linux das RTL-SDR überhaupt nicht erkennen. Falls es mit :code:`sudo` funktioniert, liegt ein Problem mit den udev-Regeln vor. In diesem Fall den Computer nach der oben beschriebenen udev-Einrichtung neu starten. Alternativ kann :code:`sudo` für alles verwendet werden, einschließlich der Ausführung von Python. + +Die Erkennung des RTL-SDR durch Python kann mit folgendem Skript getestet werden: + +.. code-block:: python + + from rtlsdr import RtlSdr + + sdr = RtlSdr() + sdr.sample_rate = 2.048e6 # Hz + sdr.center_freq = 100e6 # Hz + sdr.freq_correction = 60 # PPM + sdr.gain = 'auto' + + print(len(sdr.read_samples(1024))) + sdr.close() + +Die erwartete Ausgabe lautet: + +.. code-block:: bash + + Found Rafael Micro R820T tuner + [R82XX] PLL not locked! + 1024 + +******************************** +RTL-SDR Python-Code +******************************** + +Der obige Code kann als grundlegendes Verwendungsbeispiel des RTL-SDR in Python betrachtet werden. Die folgenden Abschnitte gehen näher auf die verschiedenen Einstellungen und Nutzungstipps ein. + +RTL-SDR-Abstürze vermeiden +############################### + +Am Ende unseres Skripts oder wann immer wir mit dem Abrufen von Samples vom RTL-SDR fertig sind, rufen wir :code:`sdr.close()` auf. Dies verhindert, dass das RTL-SDR in einen fehlerhaften Zustand gerät, in dem es aus- und wieder eingesteckt werden müsste. Auch mit close() kann dies noch passieren – man erkennt es daran, dass das RTL-SDR beim Aufruf von read_samples() einfriert. In diesem Fall muss das RTL-SDR aus- und wieder eingesteckt werden, möglicherweise ist auch ein Neustart des Computers notwendig. Unter WSL muss das RTL-SDR mit usbipd erneut verbunden werden. + +Verstärkungseinstellung +######################### + +Durch das Setzen von :code:`sdr.gain = 'auto'` wird die automatische Verstärkungsregelung (AGC) aktiviert, die dazu führt, dass das RTL-SDR die Empfangsverstärkung basierend auf den empfangenen Signalen anpasst und versucht, den 8-Bit-ADC ohne Übersteuerung auszulasten. In vielen Situationen, wie etwa beim Erstellen eines Spektrumanalysators, ist es sinnvoll, die Verstärkung auf einem konstanten Wert zu halten, sodass eine manuelle Verstärkung eingestellt werden muss. Das RTL-SDR hat keine stufenlos einstellbare Verstärkung; die Liste der gültigen Verstärkungswerte kann mit :code:`print(sdr.valid_gains_db)` angezeigt werden. Wird ein nicht auf dieser Liste befindlicher Wert gesetzt, wählt das Gerät automatisch den nächstliegenden zulässigen Wert. Die aktuelle Verstärkungseinstellung kann jederzeit mit :code:`print(sdr.gain)` abgerufen werden. Im folgenden Beispiel wird die Verstärkung auf 49,6 dB gesetzt, 4096 Samples empfangen und diese dann im Zeitbereich dargestellt: + +.. code-block:: python + + from rtlsdr import RtlSdr + import numpy as np + import matplotlib.pyplot as plt + + sdr = RtlSdr() + sdr.sample_rate = 2.048e6 # Hz + sdr.center_freq = 100e6 # Hz + sdr.freq_correction = 60 # PPM + print(sdr.valid_gains_db) + sdr.gain = 49.6 + print(sdr.gain) + + x = sdr.read_samples(4096) + sdr.close() + + plt.plot(x.real) + plt.plot(x.imag) + plt.legend(["I", "Q"]) + plt.savefig("../_images/rtlsdr-gain.svg", bbox_inches='tight') + plt.show() + +.. image:: ../_images/rtlsdr-gain.svg + :align: center + :target: ../_images/rtlsdr-gain.svg + :alt: RTL-SDR Beispiel mit manueller Verstärkung + +Einige Dinge sind hier zu beachten. Die ersten ca. 2000 Samples scheinen kaum Signalleistung zu enthalten, da sie Transienten darstellen. Es wird empfohlen, die ersten 2000 Samples in jedem Skript zu verwerfen, z. B. mit :code:`sdr.read_samples(2048)`, ohne die Ausgabe weiterzuverwenden. Außerdem ist zu beachten, dass pyrtlsdr die Samples als Float-Werte zwischen -1 und +1 zurückgibt. Obwohl ein 8-Bit-ADC verwendet wird, der ganzzahlige Werte liefert, teilt pyrtlsdr diese aus Bequemlichkeitsgründen durch 127,0. + +Erlaubte Abtastraten +##################### + +Die meisten RTL-SDRs erfordern, dass die Abtastrate entweder zwischen 230–300 kHz oder zwischen 900 kHz und 3,2 MHz liegt. Beachte, dass bei höheren Raten, insbesondere über 2,4 MHz, möglicherweise nicht alle Samples vollständig über die USB-Verbindung übertragen werden. Wird eine nicht unterstützte Abtastrate angegeben, gibt das Gerät den Fehler :code:`rtlsdr.rtlsdr.LibUSBError: Error code -22: Could not set sample rate to 899000 Hz` zurück. Bei einer zulässigen Abtastrate wird die genaue Abtastrate in der Konsolenausgabe angezeigt; dieser genaue Wert kann auch durch Aufruf von :code:`sdr.sample_rate` abgerufen werden. In manchen Anwendungen kann die Verwendung des exakten Wertes in Berechnungen von Vorteil sein. + +Als Übung setzen wir die Abtastrate auf 2,4 MHz und erstellen ein Spektrogramm des UKW-Radiobands: + +.. code-block:: python + + # ... + sdr.sample_rate = 2.4e6 # Hz + # ... + + fft_size = 512 + num_rows = 500 + x = sdr.read_samples(2048) # anfängliche leere Samples verwerfen + x = sdr.read_samples(fft_size*num_rows) # alle Samples für das Spektrogramm abrufen + spectrogram = np.zeros((num_rows, fft_size)) + for i in range(num_rows): + spectrogram[i,:] = 10*np.log10(np.abs(np.fft.fftshift(np.fft.fft(x[i*fft_size:(i+1)*fft_size])))**2) + extent = [(sdr.center_freq + sdr.sample_rate/-2)/1e6, + (sdr.center_freq + sdr.sample_rate/2)/1e6, + len(x)/sdr.sample_rate, 0] + plt.imshow(spectrogram, aspect='auto', extent=extent) + plt.xlabel("Frequenz [MHz]") + plt.ylabel("Zeit [s]") + plt.show() + +.. image:: ../_images/rtlsdr-waterfall.svg + :align: center + :target: ../_images/rtlsdr-waterfall.svg + :alt: RTL-SDR Wasserfall (auch Spektrogramm) Beispiel + +PPM-Einstellung +################ + +Für diejenigen, die neugierig auf die PPM-Einstellung sind: Jedes RTL-SDR weist aufgrund der günstigen Tuner-Chips und mangelnder Kalibrierung einen kleinen Frequenzversatz/Fehler auf. Der Frequenzversatz sollte über das Spektrum hinweg relativ linear sein (kein konstanter Frequenzversatz), sodass wir ihn durch Eingabe eines PPM-Werts in Teilen pro Million korrigieren können. Wird beispielsweise auf 100 MHz abgestimmt und der PPM-Wert auf 25 gesetzt, verschiebt sich das empfangene Signal um 100e6/1e6*25 = 2500 Hz nach oben. Schmalere Signale sind stärker von Frequenzfehlern betroffen. Viele moderne Signale enthalten jedoch einen Frequenzsynchronisierungsschritt, der eventuelle Frequenzversätze beim Sender, Empfänger oder durch den Doppler-Effekt korrigiert. + +******************************** +Weiterführende Links +******************************** + +#. `RTL-SDR.com's About Page `_ +#. https://hackaday.com/2019/07/31/rtl-sdr-seven-years-later/ +#. https://osmocom.org/projects/rtl-sdr/wiki/Rtl-sdr diff --git a/content-de/sampling.rst b/content-de/sampling.rst new file mode 100644 index 00000000..05807fb8 --- /dev/null +++ b/content-de/sampling.rst @@ -0,0 +1,457 @@ +.. _sampling-chapter: + +################## +IQ-Abtastung +################## + +In diesem Kapitel stellen wir ein Konzept namens IQ-Abtastung vor, auch bekannt als komplexe Abtastung oder Quadratur-Abtastung. Wir behandeln auch Nyquist-Abtastung, komplexe Zahlen, HF-Träger, Abwärtsmischung und spektrale Leistungsdichte. IQ-Abtastung ist die Form der Abtastung, die ein SDR durchführt, sowie viele digitale Empfänger (und Sender). Es ist eine etwas komplexere Version der regulären digitalen Abtastung (Wortspiel beabsichtigt), also werden wir es langsam angehen, und mit etwas Übung wird das Konzept sicher klick machen! + +************************* +Grundlagen der Abtastung +************************* + +Bevor wir in die IQ-Abtastung einsteigen, lass uns besprechen, was Abtastung eigentlich bedeutet. Du bist vielleicht auf Abtastung gestoßen, ohne es zu merken, indem du Audio mit einem Mikrofon aufgezeichnet hast. Das Mikrofon ist ein Wandler, der Schallwellen in ein elektrisches Signal (einen Spannungspegel) umwandelt. Dieses elektrische Signal wird von einem Analog-Digital-Wandler (ADC) transformiert, der eine digitale Darstellung der Schallwelle erzeugt. Vereinfacht gesagt nimmt das Mikrofon Schallwellen auf, die in Elektrizität umgewandelt werden, und diese Elektrizität wird wiederum in Zahlen umgewandelt. Der ADC fungiert als Brücke zwischen den analogen und digitalen Domänen. SDRs sind überraschend ähnlich. Anstatt eines Mikrofons verwenden sie jedoch eine Antenne, obwohl sie auch ADCs verwenden. In beiden Fällen wird der Spannungspegel mit einem ADC abgetastet. Für SDRs gilt: Funkwellen rein, Zahlen raus. + +Egal ob wir es mit Audio- oder Hochfrequenzfrequenzen zu tun haben, müssen wir abtasten, wenn wir ein Signal digital erfassen, verarbeiten oder speichern wollen. Abtastung mag unkompliziert erscheinen, aber es steckt viel dahinter. Eine technischere Möglichkeit, die Abtastung eines Signals zu betrachten, ist das Erfassen von Werten zu bestimmten Zeitpunkten und das digitale Speichern. Sagen wir, wir haben eine zufällige Funktion :math:`S(t)`, die alles darstellen kann, und es ist eine kontinuierliche Funktion, die wir abtasten wollen: + +.. image:: ../_images_de/sampling.svg + :align: center + :target: ../_images_de/sampling.svg + :alt: Concept of sampling a signal, showing sample period T, the samples are the blue dots + +Dann zeichnen wir den Wert von :math:`S(t)` in regelmäßigen Abständen von :math:`T` Sekunden auf, bekannt als die **Abtastperiode**. Die Häufigkeit, mit der wir abtasten, d.h. die Anzahl der pro Sekunde genommenen Samples, ist einfach :math:`\frac{1}{T}`. Wir nennen dies die **Abtastrate**, und sie ist die Umkehrung der Abtastperiode. Wenn wir beispielsweise eine Abtastrate von 10 Hz haben, beträgt die Abtastperiode 0,1 Sekunden; zwischen jedem Sample liegen 0,1 Sekunden. In der Praxis werden unsere Abtastraten in der Größenordnung von Hunderten von kHz bis zu Zehnten von MHz oder sogar höher liegen. Wenn wir Signale abtasten, müssen wir auf die Abtastrate achten, es ist ein sehr wichtiger Parameter. + +Für diejenigen, die die Mathematik bevorzugen: Sei :math:`S_n` das Sample :math:`n`, normalerweise eine ganze Zahl beginnend bei 0. Mit dieser Konvention kann der Abtastprozess mathematisch als :math:`S_n = S(nT)` für ganzzahlige Werte von :math:`n` dargestellt werden. D.h., wir werten das analoge Signal :math:`S(t)` bei diesen Intervallen von :math:`nT` aus. + +************************* +Nyquist-Abtastung +************************* + +Für ein gegebenes Signal ist die große Frage oft, wie schnell wir abtasten müssen. Lass uns ein Signal untersuchen, das nur eine Sinuswelle der Frequenz f ist, unten in Grün gezeigt. Sagen wir, wir tasten mit einer Rate Fs ab (Samples in Blau gezeigt). Wenn wir dieses Signal mit einer Rate abtasten, die gleich f ist (d.h. Fs = f), erhalten wir etwas, das so aussieht: + +.. image:: ../_images_de/sampling_Fs_0.3.svg + :align: center + +Die rote gestrichelte Linie im obigen Bild rekonstruiert eine andere (falsche) Funktion, die zu denselben aufgezeichneten Samples hätte führen können. Dies zeigt, dass unsere Abtastrate zu niedrig war, da dieselben Samples von zwei verschiedenen Funktionen stammen könnten, was zu Mehrdeutigkeit führt. Wenn wir das ursprüngliche Signal genau rekonstruieren wollen, dürfen wir diese Mehrdeutigkeit nicht haben. + +Lass uns etwas schneller abtasten, bei Fs = 1,2f: + +.. image:: ../_images_de/sampling_Fs_0.36.svg + :align: center + +Auch hier gibt es ein anderes Signal, das zu diesen Samples passen könnte. Diese Mehrdeutigkeit bedeutet, dass wenn jemand uns diese Liste von Samples geben würde, wir nicht unterscheiden könnten, welches Signal das ursprüngliche war, basierend auf unserer Abtastung. + +Wie wäre es mit Abtastung bei Fs = 1,5f: + +.. image:: ../_images_de/sampling_Fs_0.45.svg + :align: center + :alt: Example of sampling ambiguity when a signal is not sampled fast enough (below the Nyquist rate) + +Immer noch nicht schnell genug! Gemäß einem DSP-Theoriestück, das wir nicht vertiefen werden, musst du mit **der doppelten** Frequenz des Signals abtasten, um die Mehrdeutigkeit zu beseitigen, die wir erleben: + +.. image:: ../_images_de/sampling_Fs_0.6.svg + :align: center + +Diesmal gibt es kein falsches Signal, weil wir schnell genug abgetastet haben, dass kein Signal existiert, das zu diesen Samples passt außer dem, das du siehst (es sei denn, du gehst *höher* in der Frequenz, aber das werden wir später besprechen). + +Im obigen Beispiel war unser Signal nur eine einfache Sinuswelle, die meisten tatsächlichen Signale werden viele Frequenzkomponenten haben. Um ein gegebenes Signal genau abzutasten, muss die Abtastrate „mindestens doppelt so hoch wie die Frequenz der maximalen Frequenzkomponente" sein. Hier ist eine Visualisierung anhand eines Beispiel-Frequenzbereichsdiagramms; beachte, dass es immer einen Rauschpegel gibt, sodass die höchste Frequenz normalerweise eine Annäherung ist: + +.. image:: ../_images_de/max_freq.svg + :align: center + :target: ../_images_de/max_freq.svg + :alt: Nyquist sampling means that your sample rate is higher than the signal's maximum bandwidth + +Wir müssen die höchste Frequenzkomponente identifizieren, sie verdoppeln und sicherstellen, dass wir mit dieser Rate oder schneller abtasten. Die Mindestrate, mit der wir abtasten können, wird als Nyquist-Rate bezeichnet. Mit anderen Worten, die Nyquist-Rate ist die Mindestrate, mit der ein (bandbegrenztes) Signal abgetastet werden muss, um alle seine Informationen zu erhalten. Es ist ein äußerst wichtiges Stück Theorie in DSP und SDR, das als Brücke zwischen kontinuierlichen und diskreten Signalen dient. + +.. image:: ../_images_de/nyquist_rate.png + :scale: 70% + :align: center + +Wenn wir nicht schnell genug abtasten, erhalten wir etwas, das Aliasing genannt wird, worüber wir später lernen werden, aber wir versuchen es um jeden Preis zu vermeiden. Was unsere SDRs tun (und die meisten Empfänger im Allgemeinen) ist, alles über Fs/2 direkt vor der Abtastung herauszufiltern. Wenn wir versuchen, ein Signal mit einer zu niedrigen Abtastrate zu empfangen, wird dieser Filter einen Teil des Signals abschneiden. Unsere SDRs gehen große Mühe darauf, uns Samples frei von Aliasing und anderen Unvollkommenheiten zu liefern. Da der Anti-Aliasing-Filter des SDRs nicht sofort vom Durchlassbereich zum Sperrbereich übergeht (er braucht ein kleines Übergangsband), lautet die Faustregel, dass nur die mittleren 4/5 deiner Abtastrate nutzbare Bandbreite sind, bekannt als „Seans 4/5-Regel". + +************************* +Quadratur-Abtastung +************************* + +Der Begriff „Quadratur" hat viele Bedeutungen, aber im Kontext von DSP und SDR bezieht er sich auf zwei Wellen, die 90 Grad außer Phase sind. Warum 90 Grad außer Phase? Betrachte, wie zwei Wellen, die 180 Grad außer Phase sind, im Wesentlichen dieselbe Welle sind, wobei eine mit -1 multipliziert wird. Durch 90 Grad außer Phase werden sie orthogonal, und es gibt viele coole Dinge, die du mit orthogonalen Funktionen machen kannst. Der Einfachheit halber verwenden wir Sinus und Kosinus als unsere zwei Sinuswellen, die 90 Grad außer Phase sind. + +Als nächstes weisen wir Variablen zu, um die **Amplitude** von Sinus und Kosinus darzustellen. Wir verwenden :math:`I` für den cos() und :math:`Q` für den sin(): + +.. math:: + I \cos(2\pi ft) + + Q \sin(2\pi ft) + + +Wir können dies visuell sehen, indem wir I und Q gleich 1 setzen: + +.. image:: ../_images_de/IQ_wave.png + :scale: 70% + :align: center + :alt: I and Q visualized as amplitudes of sinusoids that get summed together + +Wir nennen den cos() die „In-Phase"-Komponente, daher der Name I, und der sin() ist die um 90 Grad phasenverschobene oder „Quadratur"-Komponente, daher Q. Auch wenn du es versehentlich vertauschst und Q dem cos() und I dem sin() zuweist, macht das in den meisten Situationen keinen Unterschied. + +IQ-Abtastung ist einfacher aus der Sicht des Senders zu verstehen. Betrachte die Aufgabe, ein HF-Signal bei einer bestimmten Frequenz :math:`f` (in Hz) durch die Luft zu senden. Wir möchten diese Sinuswelle der Frequenz :math:`f` nehmen und ihre Amplitude :math:`A` und Phase :math:`\phi` steuern: + +.. math:: + + A \cos(2 \pi f t - \phi) + +Das negative Vorzeichen ist reine Konvention und ist nicht wichtig für das Verständnis des Konzepts. Zu jedem gegebenen Zeitpunkt gibt es möglicherweise eine andere Amplitude und Phase, die wir übertragen möchten, sodass wir sie als Funktion der Zeit darstellen können, um formeller zu sein: + +.. math:: + + A(t) \cos(2 \pi f t - \phi(t)) + +Es stellt sich heraus, dass es in HF-Schaltkreisen einfach ist, die Amplitude einer Sinuswelle zu steuern, aber schwer, die Phase zu steuern. Was wir tun können, ist die trigonometrische Identität zu nutzen: :math:`a \cos(x) + b \sin(x) = A \cos(x - \phi)`, die uns sagt, dass eine Summe eines cos() und sin() der gleichen Frequenz, jeweils mit Phase 0, äquivalent zu einem einzelnen cos() mit Amplitude :math:`A` und Phase :math:`\phi` ist. Mit I und Q anstelle von :math:`a` und :math:`b` und dem Hinzufügen von :math:`2 \pi f t` erhalten wir: + +.. math:: + + A \cos(2 \pi f t - \phi) + + = I \cos(2 \pi f t) + Q \sin(2 \pi f t) + +wobei + +.. math:: + + A = \sqrt{I^2 + Q^2} + + \phi = \tan^{-1}\left(\frac{Q}{I}\right) + +Mit diesem I- und Q-Ansatz können wir jede gewünschte Magnitude und Phase übertragen, mithilfe einer Schaltung, die ungefähr so aussieht: + +.. image:: ../_images_de/IQ_diagram.png + :scale: 80% + :align: center + :alt: Diagram showing how I and Q are modulated onto a carrier + + +Sagen wir, wir haben ein IQ-Sample, die einzelne komplexe Zahl :math:`I + jQ`. Wir können dieses IQ-Sample auf eine Sinuswelle **modulieren**; die Amplitude und Phase werden durch das IQ-Sample bestimmt: + +.. math:: + + x(t) = I \cos(2\pi ft) + Q \sin(2\pi ft) + + \qquad \qquad \qquad \qquad = \left(\sqrt{I^2+Q^2}\right) \cos\left(2\pi ft - \tan^{-1}\left(\frac{Q}{I}\right)\right) + +Obwohl wir die Mathematik gesehen haben, lass uns mit dem Addieren zweier Sinusoide spielen, die 90 Grad außer Phase sind. Im folgenden Video gibt es einen Schieberegler zum Anpassen von I und einen weiteren für Q, die Amplitude des Kosinus und Sinus. Was dargestellt wird, sind der Kosinus (rot), der Sinus (blau) und die Summe der beiden (grün). + +.. image:: ../_images_de/IQ3.gif + :scale: 100% + :align: center + :target: ../_images_de/IQ3.gif + :alt: GNU Radio animation showing I and Q as amplitudes of sinusoids that get summed together + +Der Code, der für diese auf pyqtgraph basierende Python-App verwendet wurde, kann `hier `_ gefunden werden. + +Die wichtigsten Erkenntnisse sind, dass wenn wir cos() und sin() addieren, wir eine weitere reine Sinuswelle derselben Frequenz erhalten, aber mit einer anderen Phase und Amplitude. Auch ändert sich die Phase, wenn wir langsam einen der beiden Teile entfernen oder hinzufügen (die Amplitude ändert sich auch). Das ist alles ein Ergebnis der trigonometrischen Identität: :math:`a \cos(x) + b \sin(x) = A \cos(x-\phi)`. Der „Nutzen" dieses Verhaltens ist, dass wir die Phase und Amplitude einer resultierenden Sinuswelle steuern können, indem wir die Amplituden I und Q anpassen (wir müssen nicht die Phase des Kosinus oder Sinus anpassen). Beispielsweise könnten wir I und Q so anpassen, dass die Amplitude konstant bleibt und die Phase das ist, was wir wollen. Als Sender ist diese Fähigkeit äußerst nützlich, weil wir wissen, dass wir ein sinusförmiges Signal senden müssen, damit es als elektromagnetische Welle durch die Luft fliegen kann. Und es ist viel einfacher, zwei Amplituden anzupassen und eine Additionsoperation durchzuführen, als eine Amplitude und eine Phase anzupassen. Es ermöglicht uns auch, Basisbandsignale bequemer darzustellen, unabhängig vom Träger. + +************************* +Komplexe Zahlen +************************* + +Letztendlich ist die IQ-Konvention eine alternative Möglichkeit, Magnitude und Phase darzustellen, was uns zu komplexen Zahlen und der Fähigkeit führt, sie auf einer komplexen Ebene darzustellen. Du hast vielleicht komplexe Zahlen in anderen Kursen gesehen. Nehmen wir die komplexe Zahl 0,7-0,4j als Beispiel: + +.. image:: ../_images_de/complex_plane_1.png + :scale: 70% + :align: center + +Eine komplexe Zahl ist wirklich nur zwei Zahlen zusammen, ein realer und ein imaginärer Anteil. Eine komplexe Zahl hat auch eine Magnitude und Phase, was mehr Sinn ergibt, wenn du sie als Vektor statt als Punkt betrachtest. Magnitude ist die Länge der Linie zwischen dem Ursprung und dem Punkt (d.h. Länge des Vektors), während die Phase der Winkel zwischen dem Vektor und 0 Grad ist, den wir als die positive reale Achse definieren: + +.. image:: ../_images_de/complex_plane_2.png + :scale: 70% + :align: center + :alt: A vector on the complex plane + +Diese Darstellung eines Sinusoids ist als „Phasor-Diagramm" bekannt. Es geht einfach darum, komplexe Zahlen darzustellen und sie als Vektoren zu behandeln. Was sind nun Magnitude und Phase unserer Beispiel-Komplexzahl 0,7-0,4j? Für eine gegebene komplexe Zahl, wobei :math:`a` der Realteil und :math:`b` der Imaginärteil ist: + +.. math:: + \mathrm{Magnitude} = \sqrt{a^2 + b^2} = 0{,}806 + + \mathrm{Phase} = \tan^{-1} \left( \frac{b}{a} \right) = -29{,}7^{\circ} = -0{,}519 \quad \mathrm{Radiant} + +In Python kannst du np.abs(x) und np.angle(x) für die Magnitude und Phase verwenden. Der Eingang kann eine komplexe Zahl oder ein Array komplexer Zahlen sein, und der Ausgang ist eine **reelle** Zahl(en) (vom Datentyp float). + +Du hast vielleicht inzwischen herausgefunden, wie dieses Vektor- oder Phasor-Diagramm mit der IQ-Konvention zusammenhängt: I ist real und Q ist imaginär. Von diesem Punkt an, wenn wir die komplexe Ebene zeichnen, werden wir sie mit I und Q anstelle von real und imaginär beschriften. Sie sind immer noch komplexe Zahlen! + +.. image:: ../_images_de/complex_plane_3.png + :scale: 70% + :align: center + +Sagen wir nun, wir möchten unseren Beispielpunkt 0,7-0,4j übertragen. Wir werden folgendes übertragen: + +.. math:: + + x(t) = I \cos(2\pi ft) + Q \sin(2\pi ft) + + \quad \quad \quad = 0{,}7 \cos(2\pi ft) - 0{,}4 \sin(2\pi ft) + +Wir können die trigonometrische Identität :math:`a \cos(x) + b \sin(x) = A \cos(x-\phi)` verwenden, wobei :math:`A` unsere Magnitude ist, die mit :math:`\sqrt{I^2 + Q^2}` berechnet wird, und :math:`\phi` unsere Phase ist, gleich :math:`\tan^{-1} \left( Q/I \right)`. Die obige Gleichung wird nun: + +.. math:: + x(t) = 0{,}806 \cos(2\pi ft + 0{,}519) + +Auch wenn wir mit einer komplexen Zahl begonnen haben, ist das, was wir übertragen, ein reales Signal mit einer bestimmten Magnitude und Phase; man kann mit elektromagnetischen Wellen nicht wirklich etwas Imaginäres übertragen. Wir verwenden nur imaginäre/komplexe Zahlen, um darzustellen, *was* wir übertragen. Wir werden über das :math:`f` in Kürze sprechen. + +************************* +Komplexe Zahlen in FFTs +************************* + +Die obigen komplexen Zahlen wurden als Zeitbereich-Samples angenommen, aber du wirst auch auf komplexe Zahlen stoßen, wenn du eine FFT durchführst. Als wir im letzten Kapitel Fourier-Reihen und FFTs behandelt haben, waren wir noch nicht in komplexe Zahlen eingetaucht. Wenn du die FFT einer Reihe von Samples nimmst, findet sie die Frequenzbereichsdarstellung. Wir haben darüber gesprochen, wie die FFT herausfindet, welche Frequenzen in dieser Menge von Samples vorhanden sind (die Magnitude der FFT gibt die Stärke jeder Frequenz an). Aber was die FFT auch tut, ist herauszufinden, welche Verzögerung (Zeitverschiebung) auf jede dieser Frequenzen angewendet werden muss, damit die Menge von Sinusoiden addiert werden kann, um das Zeitbereichssignal zu rekonstruieren. Diese Verzögerung ist einfach die Phase der FFT. Der Ausgang einer FFT ist ein Array komplexer Zahlen, und jede komplexe Zahl gibt dir die Magnitude und Phase, und der Index dieser Zahl gibt dir die Frequenz. Wenn du Sinusoide bei diesen Frequenzen/Magnituden/Phasen erzeugst und sie zusammen addierst, erhältst du dein ursprüngliches Zeitbereichssignal (oder etwas sehr ähnliches, und dort kommt das Nyquist-Abtasttheorem ins Spiel). + +************************* +Empfängerseite +************************* + +Lass uns nun die Perspektive eines Radioempfängers einnehmen, der versucht, ein Signal zu empfangen (z.B. ein UKW-Radiosignal). Mit IQ-Abtastung sieht das Diagramm nun so aus: + +.. image:: ../_images_de/IQ_diagram_rx.png + :scale: 70% + :align: center + :alt: Receiving IQ samples by directly multiplying the input signal by a sine wave and a 90 degree shifted version of that sine wave + +Was hereinkommt, ist ein reales Signal, das von unserer Antenne empfangen wird, und diese werden in IQ-Werte umgewandelt. Was wir tun, ist die I- und Q-Zweige einzeln mit zwei ADCs abzutasten, und dann kombinieren wir die Paare und speichern sie als komplexe Zahlen. Mit anderen Worten, zu jedem Zeitschritt tastest du einen I-Wert und einen Q-Wert ab und kombinierst sie in der Form :math:`I + jQ` (d.h. eine komplexe Zahl pro IQ-Sample). Es wird immer eine „Abtastrate" geben, die Rate, mit der die Abtastung durchgeführt wird. Jemand könnte sagen: „Ich habe ein SDR, das mit einer Abtastrate von 2 MHz läuft." Was er meint, ist, dass das SDR zwei Millionen IQ-Samples pro Sekunde empfängt. + +Wenn jemand dir ein paar IQ-Samples gibt, sehen sie wie ein 1D-Array/Vektor komplexer Zahlen aus. Dieser Punkt, komplex oder nicht, ist das, worauf dieses gesamte Kapitel hingearbeitet hat, und wir haben es endlich geschafft. + +In diesem Lehrbuch wirst du **sehr** vertraut damit werden, wie IQ-Samples funktionieren, wie man sie mit einem SDR empfängt und sendet, wie man sie in Python verarbeitet und wie man sie zur späteren Analyse in einer Datei speichert. + +Ein letzter wichtiger Hinweis: Die obige Abbildung zeigt, was **innerhalb** des SDR passiert. Wir müssen tatsächlich keine Sinuswelle erzeugen, um 90 Grad verschieben, multiplizieren oder addieren – das SDR macht das für uns. Wir sagen dem SDR, mit welcher Frequenz wir abtasten oder bei welcher Frequenz wir unsere Samples übertragen möchten. Auf der Empfängerseite wird uns das SDR die IQ-Samples liefern. Für die Übertragungsseite müssen wir dem SDR die IQ-Samples bereitstellen. Was den Datentyp betrifft, werden sie entweder komplexe Ints oder Floats sein. + +.. _downconversion-section: + +************************** +Träger und Abwärtsmischung +************************** + +Bis jetzt haben wir nicht über Frequenz gesprochen, aber wir haben gesehen, dass es ein :math:`f` in den Gleichungen mit dem cos() und sin() gab. Diese Frequenz ist die Mittenfrequenz des Signals, das wir tatsächlich durch die Luft senden (die Frequenz der elektromagnetischen Welle). Wir bezeichnen es als den „Träger", weil er unser Signal auf einer bestimmten HF-Frequenz trägt. Wenn wir auf eine Frequenz mit unserem SDR abstimmen und Samples empfangen, werden unsere Informationen in I und Q gespeichert; dieser Träger erscheint nicht in I und Q. + +Zur Referenz verwenden Radiosignale wie UKW-Radio, WiFi, Bluetooth, LTE, GPS usw. normalerweise eine Frequenz (d.h. einen Träger) zwischen 100 MHz und 6 GHz. Diese Frequenzen reisen sehr gut durch die Luft, aber sie benötigen keine sehr langen Antennen oder viel Strom zum Senden oder Empfangen. Deine Mikrowelle kocht Essen mit elektromagnetischen Wellen bei 2,4 GHz. Wenn es ein Leck in der Tür gibt, wird deine Mikrowelle WiFi-Signale stören und möglicherweise auch deine Haut verbrennen. Eine andere Form elektromagnetischer Wellen ist Licht. Sichtbares Licht hat eine Frequenz von etwa 500 THz. Es ist so hoch, dass wir keine traditionellen Antennen verwenden, um Licht zu übertragen. Wir verwenden Methoden wie LEDs, die Halbleiterbauelemente sind. Sie erzeugen Licht, wenn Elektronen zwischen den Atomorbitalen des Halbleitermaterials springen, und die Farbe hängt davon ab, wie weit sie springen. Technisch gesehen ist Hochfrequenz (HF) als der Bereich von etwa 20 kHz bis 300 GHz definiert. Dies sind die Frequenzen, bei denen Energie aus einem oszillierenden elektrischen Strom von einem Leiter (einer Antenne) abstrahlen und durch den Raum reisen kann. Der Bereich von 100 MHz bis 6 GHz sind die nützlicheren Frequenzen, zumindest für die meisten modernen Anwendungen. Frequenzen über 6 GHz werden seit Jahrzehnten für Radar und Satellitenkommunikation verwendet und werden jetzt in 5G „mmWave" (24-29 GHz) eingesetzt, um die unteren Bänder zu ergänzen und die Geschwindigkeiten zu erhöhen. + +Wenn wir unsere IQ-Werte schnell ändern und unseren Träger übertragen, nennt man das das Modulieren des Trägers (mit Daten oder was auch immer wir wollen). Wenn wir I und Q ändern, ändern wir die Phase und Amplitude des Trägers. Eine andere Option ist, die Frequenz des Trägers zu ändern, d.h. ihn leicht nach oben oder unten zu verschieben, was das UKW-Radio macht. Es ist leicht, zwischen dem Signal, das wir übertragen möchten (das typischerweise viele Frequenzkomponenten enthält), und der Frequenz, auf der wir es übertragen (unsere Trägerfrequenz), zu verwechseln. Dies wird hoffentlich klarer, wenn wir Basisband- und Bandpasssignale behandeln. + +Zurück zur Abtastung für einen Moment. Anstatt Samples zu empfangen, indem wir das von der Antenne empfangene Signal mit cos() und sin() multiplizieren und dann I und Q aufzeichnen, was wäre, wenn wir das Signal von der Antenne direkt in einen einzelnen ADC einspeisen? Angenommen, die Trägerfrequenz ist 2,4 GHz, wie bei WiFi oder Bluetooth. Das bedeutet, dass wir mit 4,8 GHz abtasten müssten, wie wir gelernt haben. Das ist extrem schnell! Ein ADC, der so schnell abtastet, kostet Tausende von Euro. Stattdessen „mischen wir das Signal herunter", sodass das Signal, das wir abtasten möchten, um DC oder 0 Hz zentriert ist. Diese Abwärtsmischung erfolgt vor dem Abtasten. Wir gehen von: + +.. math:: + + I \underbrace{\cos(2\pi ft)}_{Träger} \ + \ \ Q \underbrace{\sin(2\pi ft)}_{Träger} + +zu nur I und Q. + +Lass uns die Abwärtsmischung im Frequenzbereich visualisieren: + +.. image:: ../_images_de/downconversion.png + :scale: 60% + :align: center + :alt: The downconversion process where a signal is frequency shifted from RF to 0 Hz or baseband + +Wenn wir um 0 Hz zentriert sind, ist die maximale Frequenz nicht mehr 2,4 GHz, sondern basiert auf den Eigenschaften des Signals, da wir den Träger entfernt haben. Die meisten Signale sind etwa 100 kHz bis 40 MHz breit in der Bandbreite, sodass wir durch Abwärtsmischung mit einer *viel* niedrigeren Rate abtasten können. Sowohl die B2X0 USRPs als auch der PlutoSDR enthalten einen HF-integrierten Schaltkreis (RFIC), der bis zu 56 MHz abtasten kann, was für die meisten Signale, denen wir begegnen werden, hoch genug ist. + +Um es noch einmal zu betonen: Der Abwärtsmischungsprozess wird von unserem SDR durchgeführt; als Benutzer des SDRs müssen wir nichts anderes tun, als ihm zu sagen, auf welche Frequenz es abstimmen soll. Abwärts- (und Aufwärts-)mischung wird von einer Komponente namens Mischer durchgeführt, der normalerweise in Diagrammen als Multiplikationssymbol innerhalb eines Kreises dargestellt wird. Der Mischer nimmt ein Signal auf, gibt das herunter-/hochgemischte Signal aus und hat einen dritten Anschluss, der verwendet wird, um einen Oszillator einzuspeisen. Die Frequenz des Oszillators bestimmt die auf das Signal angewendete Frequenzverschiebung, und der Mischer ist im Wesentlichen nur eine Multiplikationsfunktion (denk daran, dass die Multiplikation mit einem Sinus eine Frequenzverschiebung verursacht). + +Zuletzt bist du vielleicht neugierig, wie schnell Signale durch die Luft reisen. Erinnere dich aus dem Physikunterricht, dass Radiowellen nur elektromagnetische Wellen bei niedrigen Frequenzen sind (zwischen ungefähr 3 kHz und 80 GHz). Sichtbares Licht sind auch elektromagnetische Wellen bei viel höheren Frequenzen (400 THz bis 700 THz). Alle elektromagnetischen Wellen reisen mit Lichtgeschwindigkeit, die ungefähr 3e8 m/s beträgt, zumindest wenn sie durch Luft oder ein Vakuum reisen. Da sie sich immer mit der gleichen Geschwindigkeit bewegen, hängt die Entfernung, die die Welle in einer vollständigen Oszillation zurücklegt (ein vollständiger Zyklus der Sinuswelle), von ihrer Frequenz ab. Wir nennen diese Entfernung die Wellenlänge, bezeichnet als :math:`\lambda`. Du hast diese Beziehung wahrscheinlich schon gesehen: + +.. math:: + f = \frac{c}{\lambda} + +wobei :math:`c` die Lichtgeschwindigkeit ist, typischerweise auf 3e8 gesetzt, wenn :math:`f` in Hz und :math:`\lambda` in Metern ist. In der drahtlosen Kommunikation wird diese Beziehung wichtig, wenn wir zu Antennen kommen, weil du für den Empfang eines Signals bei einer bestimmten Trägerfrequenz :math:`f` eine Antenne benötigst, die zu ihrer Wellenlänge :math:`\lambda` passt; normalerweise hat die Antenne eine Länge von :math:`\lambda/2` oder :math:`\lambda/4`. Unabhängig von der Frequenz/Wellenlänge werden Informationen in diesem Signal jedoch immer mit Lichtgeschwindigkeit vom Sender zum Empfänger reisen. Bei der Berechnung dieser Verzögerung durch die Luft lautet eine Faustregel, dass Licht ungefähr einen Fuß in einer Nanosekunde zurücklegt. Eine weitere Faustregel: Ein Signal, das zu einem Satelliten in geostationärer Umlaufbahn und zurück reist, benötigt ungefähr 0,25 Sekunden für den gesamten Trip. + +************************** +Empfängerarchitekturen +************************** + +Die Abbildung im Abschnitt „Empfängerseite" zeigt, wie das Eingangssignal heruntergemischt und in I und Q aufgeteilt wird. Diese Anordnung wird als „Direktmischung" oder „Zero-IF" bezeichnet, weil die HF-Frequenzen direkt ins Basisband konvertiert werden. Eine weitere Option ist, gar nicht herunterzumischen und so schnell abzutasten, um alles von 0 Hz bis 1/2 der Abtastrate zu erfassen. Diese Strategie wird „Direktabtastung" oder „Direkt-HF" genannt und erfordert einen äußerst teuren ADC-Chip. Eine dritte Architektur, die beliebt ist, weil es so war, wie alte Radios arbeiteten, ist als „Superhet" bekannt. Sie beinhaltet Abwärtsmischung, aber nicht bis auf 0 Hz. Sie platziert das Zielsignal bei einer Zwischenfrequenz, bekannt als „ZF". Ein rauscharmer Verstärker (LNA) ist einfach ein Verstärker, der für sehr schwache Signale am Eingang ausgelegt ist. Hier sind die Blockdiagramme dieser drei Architekturen; beachte, dass auch Variationen und Hybriden dieser Architekturen existieren: + +.. image:: ../_images_de/receiver_arch_diagram.svg + :align: center + :target: ../_images_de/receiver_arch_diagram.svg + :alt: Three common receiver architectures: direct sampling, direct conversion, and superheterodyne + +*********************************** +Basisband- und Bandpasssignale +*********************************** + +Wir bezeichnen ein Signal, das um 0 Hz zentriert ist, als „Basisband". Umgekehrt bezieht sich „Bandpass" darauf, wenn ein Signal bei einer HF-Frequenz weit von 0 Hz entfernt existiert, das für die drahtlose Übertragung nach oben verschoben wurde. Es gibt keine Vorstellung von einer „Basisbandübertragung", weil man nichts Imaginäres übertragen kann. Ein Signal im Basisband kann perfekt bei 0 Hz zentriert sein, wie der rechte Teil der Abbildung in Abschnitt :ref:`downconversion-section`. Es könnte *nahe* an 0 Hz sein, wie die zwei unten gezeigten Signale. Diese zwei Signale gelten immer noch als Basisband. Ebenfalls gezeigt ist ein Beispiel-Bandpasssignal, das bei einer sehr hohen Frequenz :math:`f_c` zentriert ist. + +.. image:: ../_images_de/baseband_bandpass.png + :scale: 50% + :align: center + :alt: Baseband vs bandpass + +Du hörst vielleicht auch den Begriff Zwischenfrequenz oder ZF, was ein intermediärer Konvertierungsschritt innerhalb eines Radios zwischen Basisband und Bandpass/HF ist. + +Wir neigen dazu, Signale im Basisband zu erstellen, aufzuzeichnen oder zu analysieren, weil wir mit einer niedrigeren Abtastrate arbeiten können (aus im vorherigen Unterabschnitt besprochenen Gründen). Es ist wichtig zu beachten, dass Basisband-HF-Signale **komplexe** Signale sind, während Signale im Bandpass (z.B. Signale, die wir tatsächlich über HF senden) **reelle** Signale sind. Jedes Signal, das durch eine Antenne geleitet wird, muss real sein, weil man ein komplexes/imaginäres Signal nicht direkt übertragen kann. Du weißt, dass ein Signal definitiv ein komplexes Signal ist, wenn die negativen und positiven Frequenzanteile des Signals nicht exakt gleich sind. Komplexe Zahlen ermöglichen es uns, negative Frequenzen darzustellen. In Wirklichkeit gibt es keine negativen Frequenzen; es ist nur der Teil des Signals unterhalb der Trägerfrequenz. + +Wenn wir keine imaginäre Komponente in unserem Signal haben, haben wir keine Q-Werte (oder du kannst dir vorstellen, dass alle Q-Werte gleich null sind). Das bedeutet, dass wir nur Kosinussignale ohne Phasenverschiebung haben. Eine Summe von Kosinussignalen ohne Phasenverschiebung wird symmetrisch um die y-Achse sein, wenn wir den Frequenzbereich darstellen, weil die positiven und negativen Komponenten dieselben sind. + +Im früheren Abschnitt, wo wir mit dem komplexen Punkt 0,7 - 0,4j gespielt haben, war das im Wesentlichen ein Sample in einem Basisbandsignal. Die meiste Zeit, wenn du komplexe Samples (IQ-Samples) siehst, bist du im Basisband. Signale werden selten digital bei HF dargestellt oder gespeichert, wegen der Datenmenge, die das erfordern würde, und der Tatsache, dass wir uns normalerweise nur für einen kleinen Teil des HF-Spektrums interessieren. + +*************************** +DC-Spitze und Offset-Abstimmung +*************************** + +Sobald du anfängst, mit SDRs zu arbeiten, wirst du oft eine große Spitze in der Mitte der FFT finden. +Sie wird als „DC-Versatz" oder „DC-Spitze" oder manchmal „LO-Leckage" bezeichnet, wobei LO für lokalen Oszillator steht. + +Hier ist ein Beispiel einer DC-Spitze: + +.. image:: ../_images_de/dc_spike.png + :scale: 50% + :align: center + :alt: DC spike shown in a power spectral density (PSD) + +Da das SDR auf eine Mittenfrequenz abstimmt, entspricht der 0-Hz-Anteil der FFT der Mittenfrequenz. +Das heißt, eine DC-Spitze bedeutet nicht unbedingt, dass bei der Mittenfrequenz Energie vorhanden ist. +Wenn es nur eine DC-Spitze gibt und der Rest der FFT wie Rauschen aussieht, ist höchstwahrscheinlich kein Signal an der angezeigten Stelle vorhanden. + +Ein DC-Versatz ist ein häufiges Artefakt in Direktmischempfängern, das ist die Architektur, die für SDRs wie den PlutoSDR, RTL-SDR, LimeSDR und viele Ettus USRPs verwendet wird. In Direktmischempfängern mischt ein Oszillator, der LO, das Signal von seiner tatsächlichen Frequenz ins Basisband. Infolgedessen erscheint die Leckage dieses LO in der Mitte der beobachteten Bandbreite. LO-Leckage ist zusätzliche Energie, die durch die Kombination von Frequenzen entsteht. Das Entfernen dieses zusätzlichen Rauschens ist schwierig, weil es nahe am gewünschten Ausgangssignal liegt. Viele HF-integrierte Schaltkreise (RFICs) haben einen eingebauten automatischen DC-Versatz-Entfernungsmechanismus, aber er erfordert typischerweise ein vorhandenes Signal, um zu funktionieren. Deshalb wird die DC-Spitze sehr deutlich sein, wenn keine Signale vorhanden sind. + +Eine schnelle Möglichkeit, mit dem DC-Versatz umzugehen, ist das Signal zu überabtasten und es zu dezentrieren. Diese Technik nennt sich *Offset-Abstimmung*. +Als Beispiel sagen wir, wir wollen 5 MHz Spektrum bei 100 MHz betrachten. +Was wir stattdessen tun können, ist mit 20 MHz bei einer Mittenfrequenz von 95 MHz abzutasten. + +.. image:: ../_images_de/offtuning.png + :scale: 40 % + :align: center + :alt: The offset tuning process to avoid the DC spike + +Das blaue Kästchen oben zeigt, was vom SDR tatsächlich abgetastet wird, und das grüne Kästchen zeigt den Teil des Spektrums, den wir wollen. Unser LO wird auf 95 MHz eingestellt, weil das die Frequenz ist, auf die wir das SDR abstimmen. Da 95 MHz außerhalb des grünen Kästchens liegt, erhalten wir keine DC-Spitze. + +Es gibt ein Problem: Wenn wir wollen, dass unser Signal bei 100 MHz zentriert ist und nur 5 MHz enthält, müssen wir selbst eine Frequenzverschiebung, Filterung und Dezimierung des Signals durchführen (etwas, das wir später lernen werden). Glücklicherweise ist dieser Prozess der Offset-Abstimmung, auch als Anwenden eines LO-Versatzes bezeichnet, oft in die SDRs eingebaut, wo sie automatisch eine Offset-Abstimmung durchführen und dann die Frequenz auf deine gewünschte Mittenfrequenz verschieben. Wir profitieren davon, wenn das SDR es intern tun kann: Wir müssen keine höhere Abtastrate über unsere USB- oder Ethernet-Verbindung senden, was der Flaschenhals ist, wie hoch eine Abtastrate wir verwenden können. + +Dieser Unterabschnitt zu DC-Versätzen ist ein gutes Beispiel dafür, wie sich dieses Lehrbuch von anderen unterscheidet. Ein durchschnittliches DSP-Lehrbuch wird über Abtastung sprechen, aber es neigt dazu, Implementierungshürden wie DC-Versätze trotz ihrer Verbreitung in der Praxis nicht einzubeziehen. + + +**************************** +Abtastung mit unserem SDR +**************************** + +Für SDR-spezifische Informationen zur Durchführung von Abtastungen siehe eines der folgenden Kapitel: + +* :ref:`pluto-chapter` Kapitel +* :ref:`usrp-chapter` Kapitel + +************************* +Durchschnittliche Leistung berechnen +************************* + +In HF-DSP möchten wir oft die Leistung eines Signals berechnen, z.B. um das Vorhandensein des Signals zu erkennen, bevor wir versuchen, weitere DSP-Verarbeitung durchzuführen. Für ein diskretes komplexes Signal, d.h. eines, das wir abgetastet haben, können wir die durchschnittliche Leistung berechnen, indem wir die Magnitude jedes Samples nehmen, es quadrieren und dann den Mittelwert berechnen: + +.. math:: + P = \frac{1}{N} \sum_{n=1}^{N} |x[n]|^2 + +Denk daran, dass der Absolutbetrag einer komplexen Zahl nur die Magnitude ist, d.h. :math:`\sqrt{I^2+Q^2}` + +In Python sieht die Berechnung der durchschnittlichen Leistung so aus: + +.. code-block:: python + + avg_pwr = np.mean(np.abs(x)**2) + +Hier ist ein sehr nützlicher Trick zur Berechnung der durchschnittlichen Leistung eines abgetasteten Signals. +Wenn dein Signal ungefähr null Mittelwert hat – was normalerweise bei SDR der Fall ist (wir werden später sehen warum) – kann die Signalleistung gefunden werden, indem die Varianz der Samples berechnet wird. Unter diesen Umständen kannst du die Leistung in Python so berechnen: + +.. code-block:: python + + avg_pwr = np.var(x) # (Signal sollte ungefähr null Mittelwert haben) + +Der Grund, warum die Varianz der Samples die durchschnittliche Leistung berechnet, ist ganz einfach: Die Gleichung für die Varianz ist :math:`\frac{1}{N}\sum^N_{n=1} |x[n]-\mu|^2`, wobei :math:`\mu` der Mittelwert des Signals ist. Diese Gleichung sieht vertraut aus! Wenn :math:`\mu` null ist, wird die Gleichung zur Bestimmung der Varianz der Samples äquivalent zur Gleichung für die Leistung. Du kannst auch den Mittelwert aus den Samples in deinem Beobachtungsfenster subtrahieren und dann die Varianz berechnen. Wisse einfach, dass wenn der Mittelwert nicht null ist, Varianz und Leistung nicht gleich sind. + +********************************** +Spektrale Leistungsdichte berechnen +********************************** + +Im letzten Kapitel haben wir gelernt, dass wir ein Signal mit einer FFT in den Frequenzbereich konvertieren können, und das Ergebnis wird als spektrale Leistungsdichte (PSD) bezeichnet. +Die PSD ist ein äußerst nützliches Werkzeug zur Visualisierung von Signalen im Frequenzbereich, und viele DSP-Algorithmen werden im Frequenzbereich durchgeführt. +Um aber tatsächlich die PSD einer Gruppe von Samples zu finden und sie darzustellen, tun wir mehr als nur eine FFT zu nehmen. +Wir müssen die folgenden sechs Operationen durchführen, um die PSD zu berechnen: + +1. Nehmen die FFT unserer Samples. Wenn wir x Samples haben, ist die FFT-Größe standardmäßig die Länge von x. Lass uns die ersten 1024 Samples als Beispiel nehmen, um eine 1024-große FFT zu erstellen. Der Ausgang wird 1024 komplexe Floats sein. +2. Nehmen die Magnitude des FFT-Ausgangs, was uns 1024 reelle Floats liefert. +3. Die resultierende Magnitude quadrieren, um die Leistung zu erhalten. +4. Normalisieren: durch die FFT-Größe (:math:`N`) und Abtastrate (:math:`Fs`) dividieren. +5. In dB umrechnen mit :math:`10 \log_{10}()`; wir betrachten PSDs immer in logarithmischer Skala. +6. Eine FFT-Verschiebung durchführen, die im vorherigen Kapitel behandelt wurde, um „0 Hz" in der Mitte und negative Frequenzen links davon zu platzieren. + +Diese sechs Schritte in Python sind: + +.. code-block:: python + + Fs = 1e6 # sagen wir, wir haben mit 1 MHz abgetastet + # nehme an, x enthält dein Array von IQ-Samples + N = 1024 + x = x[0:N] # wir nehmen nur die FFT der ersten 1024 Samples, siehe Text unten + PSD = np.abs(np.fft.fft(x))**2 / (N*Fs) + PSD_log = 10.0*np.log10(PSD) + PSD_shifted = np.fft.fftshift(PSD_log) + +Optional können wir eine Fensterfunktion anwenden, wie wir im Kapitel :ref:`freq-domain-chapter` gelernt haben. Die Fensterfunktion würde direkt vor der Codezeile mit fft() angewendet werden. + +.. code-block:: python + + # füge die folgende Zeile nach x = x[0:1024] hinzu + x = x * np.hamming(len(x)) # Hamming-Fenster anwenden + +Um diese PSD darzustellen, müssen wir die Werte der x-Achse kennen. +Wie wir im letzten Kapitel gelernt haben, wenn wir ein Signal abtasten, „sehen" wir nur das Spektrum zwischen -Fs/2 und Fs/2, wobei Fs unsere Abtastrate ist. +Die Auflösung, die wir im Frequenzbereich erzielen, hängt von der Größe unserer FFT ab, die standardmäßig gleich der Anzahl der Samples ist, an denen wir die FFT-Operation durchführen. +In diesem Fall ist unsere x-Achse 1024 gleichmäßig verteilte Punkte zwischen -0,5 MHz und 0,5 MHz. +Wenn wir unser SDR auf 2,4 GHz abgestimmt hätten, wäre unser Beobachtungsfenster zwischen 2,3995 GHz und 2,4005 GHz. +In Python sieht das Verschieben des Beobachtungsfensters so aus: + +.. code-block:: python + + center_freq = 2.4e9 # Frequenz, auf die wir unser SDR abgestimmt haben + f = np.arange(Fs/-2.0, Fs/2.0, Fs/N) # Start, Stopp, Schritt. Zentriert um 0 Hz + f += center_freq # jetzt Mittenfrequenz hinzufügen + plt.plot(f, PSD_shifted) + plt.show() + +Wir sollten eine schöne PSD erhalten! + +Wenn du die PSD von Millionen von Samples finden möchtest, mach keine Millionen-Punkte-FFT, weil es wahrscheinlich ewig dauern wird. Es gibt dir einen Ausgang von einer Million „Frequenz-Bins", schließlich zu viel für eine Darstellung. +Stattdessen empfehle ich, mehrere kleinere PSDs durchzuführen und sie zu mitteln oder sie als Spektrogramm-Diagramm darzustellen. +Alternativ, wenn du weißt, dass sich dein Signal nicht schnell ändert, reicht es aus, ein paar Tausend Samples zu verwenden und die PSD davon zu finden; in diesem Zeitrahmen von ein paar Tausend Samples wirst du wahrscheinlich genug vom Signal erfassen, um eine gute Darstellung zu erhalten. + +Hier ist ein vollständiges Codebeispiel, das das Generieren eines Signals (komplexer Exponential bei 50 Hz) und Rauschen enthält. Beachte, dass N, die Anzahl der zu simulierenden Samples, zur FFT-Länge wird, weil wir die FFT des gesamten simulierten Signals nehmen. + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + + Fs = 300 # Abtastrate + Ts = 1/Fs # Abtastperiode + N = 2048 # Anzahl der zu simulierenden Samples + + t = Ts*np.arange(N) + x = np.exp(1j*2*np.pi*50*t) # simuliert Sinus bei 50 Hz + + n = (np.random.randn(N) + 1j*np.random.randn(N))/np.sqrt(2) # komplexes Rauschen mit Einheitsleistung + noise_power = 2 + r = x + n * np.sqrt(noise_power) + + PSD = np.abs(np.fft.fft(r))**2 / (N*Fs) + PSD_log = 10.0*np.log10(PSD) + PSD_shifted = np.fft.fftshift(PSD_log) + + f = np.arange(Fs/-2.0, Fs/2.0, Fs/N) # Start, Stopp, Schritt + + plt.plot(f, PSD_shifted) + plt.xlabel("Frequenz [Hz]") + plt.ylabel("Magnitude [dB]") + plt.grid(True) + plt.show() + +Ausgabe: + +.. image:: ../_images_de/fft_example1.svg + :align: center + +****************** +Weiterführende Literatur (auf Englisch) +****************** + +#. https://web.archive.org/web/20220613052830/http://rfic.eecs.berkeley.edu/~niknejad/ee242/pdf/eecs242_lect3_rxarch.pdf diff --git a/content-de/sync.rst b/content-de/sync.rst new file mode 100644 index 00000000..cb343be3 --- /dev/null +++ b/content-de/sync.rst @@ -0,0 +1,618 @@ +.. _sync-chapter: + +################ +Synchronisation +################ + +Dieses Kapitel behandelt die Synchronisation drahtloser Signale in Zeit und Frequenz, um Trägerfrequenzoffsets zu korrigieren und eine Timing-Ausrichtung auf Symbol- und Rahmenebene durchzuführen. Wir werden die Mueller-und-Müller-Taktwiederherstellungstechnik und den Costas-Regelkreis in Python nutzen. + +*************************** +Einführung +*************************** + +Wir haben besprochen, wie man digital über die Luft sendet, indem man ein digitales Modulationsverfahren wie QPSK verwendet und Impulsformung anwendet, um die Signalbandbreite zu begrenzen. Kanalcodierung kann verwendet werden, um mit verrauschten Kanälen umzugehen, z.B. wenn das SNR am Empfänger niedrig ist. Es hilft immer, so viel wie möglich herauszufiltern, bevor man das Signal digital verarbeitet. In diesem Kapitel untersuchen wir, wie Synchronisation auf der Empfangsseite durchgeführt wird. Synchronisation ist eine Reihe von Verarbeitungsschritten, die *vor* der Demodulation und Kanaldecodierung stattfinden. Die gesamte Tx-Kanal-Rx-Kette ist unten dargestellt, wobei die in diesem Kapitel behandelten Blöcke gelb hervorgehoben sind. (Dieses Diagramm ist nicht vollständig – die meisten Systeme enthalten auch Entzerrung und Multiplexing.) + +.. image:: ../_images/sync-diagram.svg + :align: center + :target: ../_images/sync-diagram.svg + :alt: Die Sende-Empfangs-Kette, mit den in diesem Kapitel besprochenen Blöcken gelb hervorgehoben, inklusive Zeit- und Frequenzsynchronisation + +*************************** +Drahtlosen Kanal simulieren +*************************** + +Bevor wir lernen, wie man Zeit- und Frequenzsynchronisation implementiert, müssen wir unsere simulierten Signale realistischer machen. Ohne das Hinzufügen einer zufälligen Zeitverzögerung ist die Zeitdomain-Synchronisation trivial. Tatsächlich muss man nur die Abtastverzögerung der verwendeten Filter berücksichtigen. Wir möchten auch einen Frequenzoffset simulieren, denn wie wir besprechen werden, sind Oszillatoren nicht perfekt; es wird immer einen gewissen Offset zwischen den Mittenfrequenzen von Sender und Empfänger geben. + +Untersuchen wir Python-Code zur Simulation einer nicht-ganzzahligen Verzögerung und eines Frequenzoffsets. Der Python-Code in diesem Kapitel baut auf dem Code auf, den wir während der Python-Übung zur Impulsformung geschrieben haben (klicke unten, falls du ihn benötigst); du kannst ihn als Ausgangspunkt des Codes in diesem Kapitel betrachten, und aller neue Code kommt danach. + +.. raw:: html + +
+ Python-Code aus der Impulsformung + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + from scipy import signal + import math + + # Dieser Teil stammt aus der Impulsformungs-Übung + num_symbols = 100 + sps = 8 + bits = np.random.randint(0, 2, num_symbols) # Zu übertragende Daten, 1er und 0er + pulse_train = np.array([]) + for bit in bits: + pulse = np.zeros(sps) + pulse[0] = bit*2-1 # ersten Wert auf 1 oder -1 setzen + pulse_train = np.concatenate((pulse_train, pulse)) # die 8 Abtastwerte zum Signal hinzufügen + + # Raised-Cosine-Filter erstellen + num_taps = 101 + beta = 0.35 + Ts = sps # Abtastrate als 1 Hz angenommen, Abtastperiode ist 1, Symbolperiode ist 8 + t = np.arange(-51, 52) # letzte Zahl nicht enthalten + h = np.sinc(t/Ts) * np.cos(np.pi*beta*t/Ts) / (1 - (2*beta*t/Ts)**2) + + # Signal filtern, um Impulsformung anzuwenden + samples = np.convolve(pulse_train, h, "same") + +.. raw:: html + +
+ +Wir lassen den Code für Plots weg, da du mittlerweile gelernt hast, wie man beliebige Signale plottet. Plots wie in diesem Lehrbuch hübsch zu gestalten erfordert viel Zusatzcode, der zum Verständnis nicht notwendig ist. + + +Verzögerung hinzufügen +############## + +Wir können eine Verzögerung einfach durch Verschieben von Abtastwerten simulieren, aber das simuliert nur eine Verzögerung, die ein ganzzahliges Vielfaches unserer Abtastperiode ist. In der realen Welt wird die Verzögerung ein Bruchteil einer Abtastperiode sein. Wir können die Verzögerung um einen Bruchteil einer Abtastperiode simulieren, indem wir einen "fraktionalen Verzögerungsfilter" erstellen, der alle Frequenzen durchlässt, aber die Abtastwerte um einen Betrag verzögert, der nicht auf das Abtastintervall beschränkt ist. Du kannst es dir als Allpass-Filter vorstellen, das auf alle Frequenzen dieselbe Phasenverschiebung anwendet. (Zur Erinnerung: Zeitverzögerung und Phasenverschiebung sind äquivalent.) Der Python-Code zum Erstellen dieses Filters wird unten gezeigt: + +.. code-block:: python + + # Fraktionalen Verzögerungsfilter erstellen und anwenden + delay = 0.4 # fraktionale Verzögerung, in Abtastwerten + N = 21 # Anzahl der Taps, ungerade halten + n = np.arange(-(N-1)//2, N//2+1) # -10,-9,...,0,...,9,10 + h = np.sinc(n - delay) # Filtertaps berechnen + h *= np.hamming(N) # Filter fenstern, damit er auf beiden Seiten auf 0 abklingt + h /= np.sum(h) # normalisieren für Einheitsverstärkung, Amplitude/Leistung nicht verändern + samples = np.convolve(samples, h) # Filter anwenden + +Wie du sehen kannst, berechnen wir die Filtertaps mithilfe einer sinc()-Funktion. Ein Sinc im Zeitbereich ist ein Rechteck im Frequenzbereich, und unser Rechteck für diesen Filter umfasst den gesamten Frequenzbereich unseres Signals. Dieser Filter verändert das Signal nicht, er verzögert es lediglich in der Zeit. In unserem Beispiel verzögern wir um 0,4 Abtastwerte. Beachte, dass das Anwenden *jedes* Filters ein Signal um die Hälfte der Filtertaps minus eins verzögert, durch den Vorgang der Faltung des Signals durch den Filter. + +Wenn wir den "Vorher"- und "Nachher"-Zustand der Filterung eines Signals darstellen, können wir die fraktionale Verzögerung beobachten. In unserem Plot zoomen wir auf nur ein paar Symbole. Sonst ist die fraktionale Verzögerung nicht erkennbar. + +.. image:: ../_images/fractional-delay-filter.svg + :align: center + :target: ../_images/fractional-delay-filter.svg + + + +Frequenzoffset hinzufügen +########################## + +Um unser simuliertes Signal realistischer zu machen, wenden wir einen Frequenzoffset an. Sagen wir, unsere Abtastrate in dieser Simulation ist 1 MHz (es spielt keine Rolle, wie groß sie tatsächlich ist, aber du wirst sehen, warum es einfacher ist, eine Zahl zu wählen). Wenn wir einen Frequenzoffset von 13 kHz (eine beliebige Zahl) simulieren möchten, können wir dies mit folgendem Code tun: + +.. code-block:: python + + # Frequenzoffset anwenden + fs = 1e6 # Abtastrate als 1 MHz angenommen + fo = 13000 # Frequenzoffset simulieren + Ts = 1/fs # Abtastperiode berechnen + t = np.arange(0, Ts*len(samples), Ts) # Zeitvektor erstellen + samples = samples * np.exp(1j*2*np.pi*fo*t) # Frequenzverschiebung durchführen + +Unten wird das Signal vor und nach dem Frequenzoffset demonstriert. + +.. image:: ../_images/sync-freq-offset.svg + :align: center + :target: ../_images/sync-freq-offset.svg + :alt: Python-Simulation eines Signals vor und nach dem Anwenden eines Frequenzoffsets + +Wir haben den Q-Anteil nicht geplottet, da wir BPSK übertragen haben, wobei Q immer null ist. Da wir jetzt eine Frequenzverschiebung hinzufügen, um drahtlose Kanäle zu simulieren, verteilt sich die Energie auf I und Q. Ab jetzt sollten wir sowohl I als auch Q plotten. Probiere gerne einen anderen Frequenzoffset in deinem Code aus. Wenn du den Offset auf etwa 1 kHz absenkst, kannst du die Sinuswelle in der Hüllkurve des Signals sehen, da sie langsam genug schwingt, um mehrere Symbole zu umspannen. + +Was die Wahl einer beliebigen Abtastrate betrifft: Wenn du den Code genauer betrachtest, wirst du bemerken, dass es auf das Verhältnis von :code:`fo` zu :code:`fs` ankommt. + +Du kannst dir vorstellen, dass die beiden oben gezeigten Codeblöcke den drahtlosen Kanal simulieren. Der Code sollte nach dem sendeseitigen Code (was wir im Kapitel zur Impulsformung gemacht haben) und vor dem empfangsseitigen Code kommen, den wir im Rest dieses Kapitels erkunden werden. + +*************************** +Zeitsynchronisation +*************************** + +Wenn wir ein Signal drahtlos übertragen, kommt es beim Empfänger mit einer zufälligen Phasenverschiebung an, die durch die zurückgelegte Zeit verursacht wird. Wir können nicht einfach mit unserer Symbolrate Symbole abtasten, da wir das Signal wahrscheinlich nicht am richtigen Punkt des Impulses abtasten, wie am Ende des Kapitels :ref:`pulse-shaping-chapter` besprochen. Schau dir die drei Abbildungen am Ende dieses Kapitels an, falls du es nicht mehr im Kopf hast. + +Die meisten Timing-Synchronisationstechniken haben die Form eines Phasenregelkreises (PLL); wir werden PLLs hier nicht im Detail studieren, aber es ist wichtig, den Begriff zu kennen, und du kannst bei Interesse selbst darüber lesen. PLLs sind geschlossene Regelkreise, die Feedback verwenden, um kontinuierlich etwas anzupassen; in unserem Fall ermöglicht eine Zeitverschiebung die Abtastung am Peak der digitalen Symbole. + +Du kannst dir die Timing-Wiederherstellung als einen Block im Empfänger vorstellen, der einen Strom von Abtastwerten akzeptiert und einen anderen Strom ausgibt (ähnlich wie ein Filter). Wir programmieren diesen Timing-Wiederherstellungsblock mit Informationen über unser Signal, wobei die Anzahl der Abtastwerte pro Symbol am wichtigsten ist (oder unsere beste Schätzung davon, wenn wir nicht 100% sicher sind, was übertragen wurde). Dieser Block wirkt als "Dezimator", d.h. unsere Abtastausgabe ist ein Bruchteil der Anzahl der eingehenden Abtastwerte. Wir möchten einen Abtastwert pro digitalem Symbol, also ist die Dezimationsrate einfach die Anzahl der Abtastwerte pro Symbol. Wenn der Sender mit 1M Symbolen pro Sekunde sendet und wir mit 16 Msps abtasten, erhalten wir 16 Abtastwerte pro Symbol. Das ist die Abtastrate, die in den Timing-Sync-Block eingeht. Die Abtastrate, die aus dem Block herauskommt, beträgt 1 Msps, da wir einen Abtastwert pro digitalem Symbol möchten. + +Die meisten Timing-Wiederherstellungsmethoden nutzen die Tatsache, dass unsere digitalen Symbole ansteigen und dann abfallen, und der Scheitelpunkt ist der Punkt, an dem wir das Symbol abtasten möchten. Anders ausgedrückt: Wir tasten den maximalen Punkt nach dem Betrag ab: + +.. image:: ../_images/symbol_sync2.png + :scale: 40 % + :align: center + +Es gibt viele Timing-Wiederherstellungsmethoden, die meisten ähneln einem PLL. Im Allgemeinen ist der Unterschied zwischen ihnen die Gleichung, die zur "Korrektur" des Timing-Offsets verwendet wird, den wir als :math:`\mu` oder :code:`mu` im Code bezeichnen. Der Wert von :code:`mu` wird bei jeder Schleifeniteration aktualisiert. Er ist in Abtastwert-Einheiten angegeben, und du kannst ihn als den Betrag betrachten, um den wir verschieben müssen, um zum "perfekten" Zeitpunkt abtasten zu können. Wenn also :code:`mu = 3.61` ist, bedeutet das, dass wir den Eingang um 3,61 Abtastwerte verschieben müssen, um am richtigen Punkt zu tasten. Da wir 8 Abtastwerte pro Symbol haben, wird :code:`mu`, wenn es über 8 geht, einfach wieder auf null zurückgesetzt. + +Der folgende Python-Code implementiert die Mueller-und-Müller-Taktwiederherstellungstechnik. + +.. code-block:: python + + mu = 0 # Anfangsschätzung der Phase des Abtastwerts + out = np.zeros(len(samples) + 10, dtype=np.complex64) + out_rail = np.zeros(len(samples) + 10, dtype=np.complex64) # speichert Werte; jede Iteration benötigt die vorherigen 2 Werte plus den aktuellen + i_in = 0 # Eingangs-Abtastwert-Index + i_out = 2 # Ausgangsindex (erste zwei Ausgaben sind 0) + while i_out < len(samples) and i_in+16 < len(samples): + out[i_out] = samples[i_in] # den vermeintlich "besten" Abtastwert nehmen + out_rail[i_out] = int(np.real(out[i_out]) > 0) + 1j*int(np.imag(out[i_out]) > 0) + x = (out_rail[i_out] - out_rail[i_out-2]) * np.conj(out[i_out-1]) + y = (out[i_out] - out[i_out-2]) * np.conj(out_rail[i_out-1]) + mm_val = np.real(y - x) + mu += sps + 0.3*mm_val + i_in += int(np.floor(mu)) # auf nächste ganze Zahl abrunden, da als Index verwendet + mu = mu - np.floor(mu) # ganzzahligen Teil von mu entfernen + i_out += 1 # Ausgangsindex erhöhen + out = out[2:i_out] # erste zwei entfernen und alles nach i_out (das nie befüllt wurde) + samples = out # diese Zeile nur einschließen, wenn du diesen Code-Ausschnitt mit dem Costas-Regelkreis verbinden möchtest + +Der Timing-Wiederherstellungsblock erhält "empfangene" Abtastwerte und produziert einen Ausgangs-Abtastwert nach dem anderen (beachte, dass :code:`i_out` bei jeder Schleifeniteration um 1 erhöht wird). Der Wiederherstellungsblock nimmt die "empfangenen" Abtastwerte nicht einfach nacheinander, weil die Schleife :code:`i_in` anpasst. Sie überspringt einige Abtastwerte in dem Versuch, den "korrekten" Abtastwert zu ziehen, der der am Peak des Impulses wäre. Während die Schleife Abtastwerte verarbeitet, synchronisiert sie sich langsam auf das Symbol, oder versucht es zumindest, indem sie :code:`mu` anpasst. Aufgrund der Struktur des Codes wird der ganzzahlige Teil von :code:`mu` zu :code:`i_in` addiert und dann von :code:`mu` entfernt (beachte, dass :code:`mm_val` in jeder Schleife negativ oder positiv sein kann). Sobald sie vollständig synchronisiert ist, sollte die Schleife nur den mittleren Abtastwert aus jedem Symbol/Impuls ziehen. Du kannst die Konstante 0,3 anpassen, was beeinflusst, wie schnell der Feedback-Regelkreis reagiert; ein höherer Wert lässt ihn schneller reagieren, erhöht aber das Stabilitätsrisiko. + +Der nächste Plot zeigt eine Beispielausgabe, bei der wir die fraktionale Zeitverzögerung sowie den Frequenzoffset *deaktiviert* haben. Wir zeigen nur I, da Q mit deaktiviertem Frequenzoffset lauter Nullen ist. Die drei Plots sind übereinander gestapelt, um zu zeigen, wie die Bits vertikal ausgerichtet sind. + +**Oberer Plot** + Ursprüngliche BPSK-Symbole, d.h. 1er und -1er. Zur Erinnerung: Es gibt Nullen dazwischen, weil wir 8 Abtastwerte pro Symbol möchten. +**Mittlerer Plot** + Abtastwerte nach der Impulsformung, aber vor dem Synchronisierer. +**Unterer Plot** + Ausgabe des Symbolsynchronisierers, der nur 1 Abtastwert pro Symbol liefert. Diese Abtastwerte können direkt in einen Demodulator eingespeist werden, der für BPSK prüft, ob der Wert größer oder kleiner als 0 ist. + +.. image:: ../_images/time-sync-output.svg + :align: center + :target: ../_images/time-sync-output.svg + +Konzentrieren wir uns auf den unteren Plot, der die Ausgabe des Synchronisierers ist. Es dauerte fast 30 Symbole, bis die Synchronisation auf die richtige Verzögerung eingerastet ist. Da es unvermeidlich Zeit braucht, bis Synchronisierer einrasten, verwenden viele Kommunikationsprotokolle eine Präambel, die eine Synchronisierungssequenz enthält: Sie dient als Ankündigung, dass ein neues Paket angekommen ist, und gibt dem Empfänger Zeit, sich darauf zu synchronisieren. Aber nach diesen ~30 Abtastwerten funktioniert der Synchronisierer perfekt. Wir haben perfekte 1er und -1er, die mit den Eingangsdaten übereinstimmen. Es hilft, dass diesem Beispiel kein Rauschen hinzugefügt wurde. Füge gerne Rauschen oder Zeitverschiebungen hinzu und beobachte, wie sich der Synchronisierer verhält. Wenn wir QPSK verwendet hätten, würden wir mit komplexen Zahlen arbeiten, aber der Ansatz wäre derselbe. + +**************************************** +Zeitsynchronisation mit Interpolation +**************************************** + +Symbolsynchronisierer tendieren dazu, die Eingangs-Abtastwerte um einen bestimmten Faktor zu interpolieren, z.B. 16, sodass sie um einen *Bruchteil* eines Abtastwerts verschieben können. Die zufällige Verzögerung durch den drahtlosen Kanal wird wahrscheinlich kein genaues Vielfaches eines Abtastwerts sein, sodass der Peak des Symbols möglicherweise nicht auf einem Abtastwert liegt. Dies gilt insbesondere, wenn es nur 2 oder 4 Abtastwerte pro Symbol geben könnte. Durch die Interpolation der Abtastwerte können wir "zwischen" tatsächlichen Abtastwerten abtasten, um den genauen Peak jedes Symbols zu treffen. Die Ausgabe des Synchronisierers ist immer noch nur 1 Abtastwert pro Symbol. Die Eingangs-Abtastwerte selbst werden interpoliert. + +Unser oben implementierter Zeitssynchronisations-Python-Code enthielt keine Interpolation. Um unseren Code zu erweitern, aktiviere die fraktionale Zeitverzögerung, die wir am Anfang dieses Kapitels implementiert haben, damit unser empfangenes Signal eine realistischere Verzögerung aufweist. Lasse den Frequenzoffset zunächst deaktiviert. Wenn du die Simulation erneut ausführst, wirst du feststellen, dass der Synchronisierer nicht vollständig auf das Signal synchronisiert. Das liegt daran, dass wir nicht interpolieren und der Code keine Möglichkeit hat, "zwischen Abtastwerten zu abtasten", um die fraktionale Verzögerung auszugleichen. Fügen wir die Interpolation hinzu. + +Eine schnelle Möglichkeit, ein Signal in Python zu interpolieren, ist die Verwendung von SciPy's :code:`signal.resample` oder :code:`signal.resample_poly`. Diese Funktionen tun dasselbe, arbeiten aber intern unterschiedlich. Wir verwenden die letztere Funktion, da sie tendenziell schneller ist. Interpolieren wir um den Faktor 16 (beliebig gewählt, andere Werte können ausprobiert werden), d.h. wir fügen 15 zusätzliche Abtastwerte zwischen jeden Abtastwert ein. Es kann in einer Codezeile erledigt werden, und es sollte *vor* der Zeitsynchronisation passieren (vor dem großen Code-Ausschnitt oben). Plotten wir auch den Vorher- und Nachher-Zustand, um den Unterschied zu sehen: + +.. code-block:: python + + samples_interpolated = signal.resample_poly(samples, 16, 1) + + # Alt vs. neu plotten + plt.figure('before interp') + plt.plot(samples,'.-') + plt.figure('after interp') + plt.plot(samples_interpolated,'.-') + plt.show() + +Wenn wir *sehr* weit hineinzoomen, sehen wir, dass es dasselbe Signal ist, nur mit 16x mehr Punkten: + +.. image:: ../_images/time-sync-interpolated-samples.svg + :align: center + :target: ../_images/time-sync-interpolated-samples.svg + :alt: Beispiel für die Interpolation eines Signals in Python + +Hoffentlich wird deutlich, warum wir innerhalb des Timing-Sync-Blocks interpolieren müssen. Diese zusätzlichen Abtastwerte ermöglichen die Berücksichtigung einer Bruchteilverzögerung. Zusätzlich zur Berechnung von :code:`samples_interpolated` müssen wir auch eine Codezeile in unserem Zeitssynchronisierer ändern. Wir ändern die erste Zeile innerhalb der while-Schleife zu: + +.. code-block:: python + + out[i_out] = samples_interpolated[i_in*16 + int(mu*16)] + +Wir haben hier ein paar Dinge geändert. Erstens können wir :code:`i_in` nicht mehr direkt als Eingangs-Abtastwert-Index verwenden. Wir müssen ihn mit 16 multiplizieren, weil wir unsere Eingangs-Abtastwerte um 16 interpoliert haben. Zur Erinnerung: Der Feedback-Regelkreis passt die Variable :code:`mu` an. Sie repräsentiert die Verzögerung, die dazu führt, dass wir im richtigen Moment abtasten. Erinnere dich auch daran, dass wir nach der Berechnung des neuen Werts von :code:`mu` den ganzzahligen Teil zu :code:`i_in` addiert haben. Jetzt verwenden wir den Restteil, der ein Float von 0 bis 1 ist und den Bruchteil eines Abtastwerts repräsentiert, um den wir verzögern müssen. Zuvor konnten wir nicht um einen Bruchteil eines Abtastwerts verzögern, jetzt schon – zumindest in Schritten von 1/16 eines Abtastwerts. Was wir tun, ist :code:`mu` mit 16 zu multiplizieren, um herauszufinden, um wie viele Abtastwerte unseres interpolierten Signals wir verzögern müssen. Und dann runden wir diese Zahl, da der Wert in den Klammern letztendlich ein Index ist und eine ganze Zahl sein muss. + +Die tatsächliche Plot-Ausgabe dieses neuen Codes sollte in etwa gleich aussehen wie zuvor. Wir haben unsere Simulation nur realistischer gemacht, indem wir eine Bruchteilverzögerung hinzugefügt haben, und dann haben wir den Interpolator zum Synchronisierer hinzugefügt, um diese Bruchteilverzögerung auszugleichen. + +Spielé gerne mit verschiedenen Interpolationsfaktoren herum, d.h. ändere alle 16er auf einen anderen Wert. Du kannst auch den Frequenzoffset aktivieren oder dem Signal weißes Gaußsches Rauschen vor dem Empfang hinzufügen, um zu sehen, wie sich das auf die Synchronisationsleistung auswirkt (Hinweis: Du musst möglicherweise den Multiplikator 0,3 anpassen). + +Wenn wir nur den Frequenzoffset mit einer Frequenz von 1 kHz aktivieren, ergibt sich folgende Zeitsync-Performance. Wir müssen jetzt sowohl I als auch Q zeigen, da wir einen Frequenzoffset hinzugefügt haben: + +.. image:: ../_images/time-sync-output2.svg + :align: center + :target: ../_images/time-sync-output2.svg + :alt: Ein Python-simuliertes Signal mit einem leichten Frequenzoffset + +Es mag schwer zu erkennen sein, aber die Zeitsynchronisation funktioniert noch einwandfrei. Es dauert etwa 20 bis 30 Symbole, bis sie eingerastet ist. Es gibt jedoch ein Sinusmuster, da wir noch einen Frequenzoffset haben, und wir werden im nächsten Abschnitt lernen, wie wir damit umgehen. + +Unten ist der IQ-Plot (a.k.a. Konstellationsplot) des Signals vor und nach der Synchronisation. Du kannst Abtastwerte auf einem IQ-Plot mit einem Streudiagramm darstellen: :code:`plt.plot(np.real(samples), np.imag(samples), '.')`. In der Animation unten haben wir die ersten 30 Symbole ausgelassen. Sie traten auf, bevor die Zeitsynchronisation abgeschlossen war. Die verbleibenden Symbole liegen alle grob auf dem Einheitskreis, aufgrund des Frequenzoffsets. + +.. image:: ../_images/time-sync-constellation.svg + :align: center + :target: ../_images/time-sync-constellation.svg + :alt: Ein IQ-Plot eines Signals vor und nach der Zeitsynchronisation + +Um noch mehr Einblick zu gewinnen, können wir die Konstellation über die Zeit betrachten, um zu erkennen, was tatsächlich mit den Symbolen passiert. Ganz am Anfang, für kurze Zeit, sind die Symbole nicht 0 oder auf dem Einheitskreis. Das ist der Zeitraum, in dem die Zeitsynchronisation die richtige Verzögerung findet. Es geht sehr schnell, schau genau hin! Das Drehen ist nur der Frequenzoffset. Frequenz ist eine konstante Phasenänderung, sodass ein Frequenzoffset eine Rotation des BPSK verursacht (was im statischen/persistenten Plot oben einen Kreis erzeugt). + +.. image:: ../_images/time-sync-constellation-animated.gif + :align: center + :target: ../_images/time-sync-constellation-animated.gif + :alt: Animation eines IQ-Plots von BPSK mit einem Frequenzoffset, der rotierende Cluster zeigt + +Hoffentlich hast du durch das Sehen eines Beispiels der tatsächlich stattfindenden Zeitsynchronisation ein Gefühl dafür, was sie tut und eine allgemeine Vorstellung, wie sie funktioniert. In der Praxis würde die von uns erstellte while-Schleife nur auf einer kleinen Anzahl von Abtastwerten gleichzeitig arbeiten (z.B. 1000). Du musst den Wert von :code:`mu` zwischen den Aufrufen der Sync-Funktion speichern, sowie die letzten paar Werte von :code:`out` und :code:`out_rail`. + +Als nächstes untersuchen wir die Frequenzsynchronisation, die wir in grobe und feine Frequenzsynchronisation aufteilen. Die grobe kommt üblicherweise vor der Zeitsynchronisation, die feine danach. + + + +********************************** +Grobe Frequenzsynchronisation +********************************** + +Auch wenn wir dem Sender und Empfänger sagen, auf derselben Mittenfrequenz zu arbeiten, wird es aufgrund von Hardware-Unvollkommenheiten (z.B. des Oszillators) oder eines Doppler-Shifts durch Bewegung einen leichten Frequenzoffset geben. Dieser Frequenzoffset wird im Verhältnis zur Trägerfrequenz winzig sein, aber selbst ein kleiner Offset kann ein digitales Signal durcheinanderbringen. Der Offset wird sich wahrscheinlich über die Zeit ändern, was einen ständig laufenden Feedback-Regelkreis erfordert, um den Offset zu korrigieren. Als Beispiel hat der Oszillator im Pluto eine maximale Offset-Spezifikation von 25 PPM. Das sind 25 Teile pro Million relativ zur Mittenfrequenz. Wenn du auf 2,4 GHz abgestimmt bist, wäre das ein maximaler Offset von +/- 60 kHz. Die Abtastwerte, die unser SDR liefert, liegen im Basisband, was dazu führt, dass sich jeder Frequenzoffset in diesem Basisbandsignal manifestiert. Ein BPSK-Signal mit einem kleinen Trägeroffset sieht ungefähr wie der unten stehende Zeitplot aus, was für die Demodulation von Bits offensichtlich nicht ideal ist. Wir müssen alle Frequenzoffsets vor der Demodulation entfernen. + +.. image:: ../_images/carrier-offset.png + :scale: 60 % + :align: center + +Die Frequenzsynchronisation ist üblicherweise in grobe und feine Synchronisation unterteilt, wobei die grobe große Offsets in der Größenordnung von kHz oder mehr korrigiert, während die feine das verbleibende korrigiert. Die grobe findet vor der Zeitsynchronisation statt, die feine danach. + +Mathematisch gilt: Wenn wir ein Basisbandsignal :math:`s(t)` haben und es einen Frequenz-(a.k.a. Träger-)Offset von :math:`f_o` Hz erfährt, können wir das Empfangene darstellen als: + +.. math:: + + r(t) = s(t) e^{j2\pi f_o t} + n(t) + +wobei :math:`n(t)` das Rauschen ist. + +Der erste Trick, den wir kennenlernen, um eine grobe Frequenzoffset-Schätzung durchzuführen (wenn wir die Offsetfrequenz schätzen können, können wir sie rückgängig machen), ist, unser Signal zu quadrieren. Ignorieren wir zunächst das Rauschen, um die Mathematik einfacher zu halten: + +.. math:: + + r^2(t) = s^2(t) e^{j4\pi f_o t} + +Sehen wir uns an, was passiert, wenn wir unser Signal :math:`s(t)` quadrieren, indem wir betrachten, was QPSK täte. Das Quadrieren komplexer Zahlen führt zu interessantem Verhalten, besonders wenn wir über Konstellationen wie BPSK und QPSK sprechen. Die folgende Animation zeigt, was passiert, wenn du QPSK quadrierst und dann noch einmal quadrierst. Ich habe speziell QPSK anstelle von BPSK verwendet, weil du sehen kannst, dass du beim einmaligen Quadrieren von QPSK im Wesentlichen BPSK erhältst. Und nach einem weiteren Quadrieren wird es ein einzelner Cluster. (Danke an http://ventrella.com/ComplexSquaring/ für diese nette Web-App.) + +.. image:: ../_images/squaring-qpsk.gif + :scale: 80 % + :align: center + +Sehen wir uns an, was passiert, wenn unser QPSK-Signal eine kleine Phasenrotation und Amplitudenskalierung erfährt, was realistischer ist: + +.. image:: ../_images/squaring-qpsk2.gif + :scale: 80 % + :align: center + +Es wird immer noch ein Cluster, nur mit einer Phasenverschiebung. Die wichtigste Erkenntnis hier ist, dass wenn du QPSK zweimal (und BPSK einmal) quadrierst, alle vier Cluster von Punkten zu einem Cluster zusammengeführt werden. Warum ist das nützlich? Nun, durch das Zusammenführen der Cluster entfernen wir im Wesentlichen die Modulation! Wenn alle Punkte jetzt im selben Cluster sind, ist das wie eine Reihe von Konstanten. Es ist, als ob keine Modulation mehr vorhanden wäre, und das Einzige, was übrig bleibt, ist die Sinuswelle, die durch den Frequenzoffset verursacht wird (wir haben auch Rauschen, aber lass uns das vorerst weiterhin ignorieren). Es stellt sich heraus, dass du das Signal N-mal quadrieren musst, wobei N die Ordnung des verwendeten Modulationsverfahrens ist. Das bedeutet, dass dieser Trick nur funktioniert, wenn du das Modulationsverfahren im Voraus kennst. Die Gleichung lautet eigentlich: + +.. math:: + + r^N(t) = s^N(t) e^{j2N\pi f_o t} + +Für unseren BPSK-Fall mit Modulationsordnung 2 verwenden wir folgende Gleichung für unsere grobe Frequenzsynchronisation: + +.. math:: + + r^2(t) = s^2(t) e^{j4\pi f_o t} + +Wir haben entdeckt, was mit dem :math:`s(t)`-Teil der Gleichung passiert, aber was ist mit dem Sinusoid-Teil (a.k.a. komplexe Exponentialfunktion)? Wie wir sehen können, fügt er den :math:`N`-Term hinzu, was ihn einem Sinusoid bei einer Frequenz von :math:`Nf_o` statt nur :math:`f_o` entspricht. Eine einfache Methode, um :math:`f_o` herauszufinden, ist, die FFT des Signals nach N-maligem Quadrieren zu nehmen und zu sehen, wo die Spitze auftritt. Simulieren wir es in Python. Wir kehren zur Generierung unseres BPSK-Signals zurück, und anstatt eine Bruchteilverzögerung darauf anzuwenden, wenden wir einen Frequenzoffset an, indem wir das Signal mit :math:`e^{j2\pi f_o t}` multiplizieren, genau wie wir es im Kapitel :ref:`filters-chapter` getan haben, um einen Tiefpassfilter in einen Hochpassfilter umzuwandeln. + +Verwende den Code vom Anfang dieses Kapitels und wende einen +13-kHz-Frequenzoffset auf dein digitales Signal an. Dies kann direkt vor oder nach dem Hinzufügen der Bruchteilverzögerung passieren; es spielt keine Rolle, welche Reihenfolge. Es muss jedoch *nach* der Impulsformung, aber vor empfangsseitigen Funktionen wie Zeitsync erfolgen. + +Da wir nun ein Signal mit einem 13-kHz-Frequenzoffset haben, plotten wir die FFT vor und nach dem Quadrieren, um zu sehen, was passiert. Du solltest mittlerweile wissen, wie man eine FFT macht, einschließlich der abs()- und fftshift()-Operation. Für diese Übung spielt es keine Rolle, ob du den Logarithmus nimmst oder ob du nach dem abs() quadrierst. + +Zunächst das Signal vor dem Quadrieren (einfache FFT): + +.. code-block:: python + + psd = np.fft.fftshift(np.abs(np.fft.fft(samples))) + f = np.linspace(-fs/2.0, fs/2.0, len(psd)) + plt.plot(f, psd) + plt.show() + +.. image:: ../_images/coarse-freq-sync-before.svg + :align: center + :target: ../_images/coarse-freq-sync-before.svg + +Wir sehen keine Spitze, die mit dem Trägeroffset zusammenhängt. Sie wird von unserem Signal verdeckt. + +Jetzt mit hinzugefügtem Quadrieren (nur Potenz 2, da es BPSK ist): + +.. code-block:: python + + # Vor der FFT-Zeile hinzufügen + samples = samples**2 + +Wir müssen sehr weit hineinzoomen, um zu sehen, bei welcher Frequenz die Spitze liegt: + +.. image:: ../_images/coarse-freq-sync.svg + :align: center + :target: ../_images/coarse-freq-sync.svg + +Du kannst versuchen, die Anzahl der simulierten Symbole zu erhöhen (z.B. 1000 Symbole), damit wir genug Abtastwerte haben. Je mehr Abtastwerte in unsere FFT eingehen, desto genauer wird unsere Schätzung des Frequenzoffsets sein. Zur Erinnerung: Der obige Code sollte *vor* dem Timing-Synchronisierer kommen. + +Die Offset-Frequenzspitze erscheint bei :math:`Nf_o`. Wir müssen diesen Bin (26,6 kHz) durch 2 teilen, um unsere endgültige Antwort zu finden, die sehr nahe an den 13 kHz Frequenzoffset kommt, den wir am Anfang des Kapitels angewendet haben! Wenn du mit dieser Zahl gespielt hast und es nicht mehr 13 kHz sind, ist das in Ordnung. Stelle nur sicher, dass du weißt, was du eingestellt hast. + +Da unsere Abtastrate 1 MHz beträgt, betragen die maximalen Frequenzen, die wir sehen können, -500 kHz bis 500 kHz. Wenn wir unser Signal auf die Potenz N bringen, können wir Frequenzoffsets nur bis zu :math:`500e3/N` "sehen", oder im Fall von BPSK +/- 250 kHz. Wenn wir ein QPSK-Signal empfingen, wären es nur +/- 125 kHz, und Trägeroffsets über oder unter diesem Bereich wären mit dieser Technik außerhalb unseres Bereichs. Um ein Gefühl für den Doppler-Shift zu bekommen: Wenn du im 2,4-GHz-Band sendest und entweder Sender oder Empfänger mit 60 mph fährt (die Relativgeschwindigkeit ist entscheidend), würde es eine Frequenzverschiebung von 214 Hz verursachen. Der Offset durch einen minderwertigen Oszillator wird in dieser Situation wahrscheinlich der Hauptverursacher sein. + +Die eigentliche Korrektur dieses Frequenzoffsets erfolgt genau so, wie wir den Offset ursprünglich simuliert haben: Multiplikation mit einer komplexen Exponentialfunktion, diesmal jedoch mit negativem Vorzeichen, da wir den Offset entfernen möchten. + +.. code-block:: python + + max_freq = f[np.argmax(psd)] + Ts = 1/fs # Abtastperiode berechnen + t = np.arange(0, Ts*len(samples), Ts) # Zeitvektor erstellen + samples = samples * np.exp(-1j*2*np.pi*max_freq*t/2.0) + +Es liegt an dir, ob du ihn korrigieren oder den anfänglichen Frequenzoffset auf eine kleinere Zahl (wie 500 Hz) ändern möchtest, um die feine Frequenzsynchronisation zu testen, die wir jetzt lernen werden. + +********************************** +Feine Frequenzsynchronisation +********************************** + +Als nächstes wechseln wir zur feinen Frequenzsynchronisation. Der vorherige Trick ist eher für die grobe Synchronisation, und es ist kein geschlossener Regelkreis (Feedback-Typ). Für die feine Frequenzsynchronisation möchten wir einen Feedback-Regelkreis, durch den wir Abtastwerte streamen, was wiederum eine Form von PLL sein wird. Unser Ziel ist es, den Frequenzoffset auf null zu bringen und dort zu halten, auch wenn sich der Offset über die Zeit ändert. Wir müssen den Offset kontinuierlich verfolgen. Feine Frequenzsynchronisationstechniken funktionieren am besten mit einem Signal, das bereits zeitlich auf Symbolebene synchronisiert wurde. Der Code, den wir in diesem Abschnitt besprechen, kommt daher *nach* der Timing-Synchronisation. + +Wir verwenden eine Technik namens Costas-Regelkreis (Costas Loop). Es ist eine Form von PLL, die speziell für die Trägerfrequenzoffset-Korrektur für digitale Signale wie BPSK und QPSK entwickelt wurde. Sie wurde von John P. Costas bei General Electric in den 1950er Jahren erfunden und hatte einen großen Einfluss auf die moderne digitale Kommunikation. Der Costas-Regelkreis beseitigt den Frequenzoffset und korrigiert auch jeden Phasenoffset. Die Energie wird auf die I-Achse ausgerichtet. Frequenz ist lediglich eine Phasenänderung, sodass sie als eins verfolgt werden können. Der Costas-Regelkreis wird mit folgendem Diagramm zusammengefasst (beachte, dass die 1/2 in den Gleichungen weggelassen wurden, da sie funktional keine Rolle spielen). + +.. image:: ../_images/costas-loop.svg + :align: center + :target: ../_images/costas-loop.svg + :alt: Costas-Regelkreis-Diagramm mit mathematischen Ausdrücken, eine Form von PLL für die RF-Signalverarbeitung + +Der spannungsgesteuerte Oszillator (VCO) ist einfach ein Sin/Cos-Wellengenerator, der eine Frequenz basierend auf der Eingabe verwendet. In unserem Fall, da wir einen drahtlosen Kanal simulieren, ist es keine Spannung, sondern ein durch eine Variable dargestellter Pegel. Er bestimmt die Frequenz und Phase der generierten Sinus- und Kosinuswellen. Was er tut, ist das empfangene Signal mit einem intern generierten Sinusoid zu multiplizieren, in dem Versuch, den Frequenz- und Phasenoffset rückgängig zu machen. Dieses Verhalten ähnelt dem, wie ein SDR heruntermischt und die I- und Q-Zweige erstellt. + + +Unten ist der Python-Code für unseren Costas-Regelkreis: + +.. code-block:: python + + N = len(samples) + phase = 0 + freq = 0 + # Diese zwei Parameter sind anzupassen, um den Feedback-Regelkreis schneller oder langsamer zu machen (was die Stabilität beeinflusst) + alpha = 0.132 + beta = 0.00932 + out = np.zeros(N, dtype=np.complex64) + freq_log = [] + for i in range(N): + out[i] = samples[i] * np.exp(-1j*phase) # Eingabe-Abtastwert um das Inverse des geschätzten Phasenoffsets anpassen + error = np.real(out[i]) * np.imag(out[i]) # Fehlerformel für Costas-Regelkreis 2. Ordnung (z.B. für BPSK) + + # Regelkreis fortschreiben (Phase und Frequenzoffset neu berechnen) + freq += (beta * error) + freq_log.append(freq * fs / (2*np.pi)) # von Winkelgeschwindigkeit in Hz umrechnen zum Protokollieren + phase += freq + (alpha * error) + + # Optional: Phase so anpassen, dass sie immer zwischen 0 und 2pi liegt; Phase läuft alle 2pi durch + while phase >= 2*np.pi: + phase -= 2*np.pi + while phase < 0: + phase += 2*np.pi + + # Frequenz über Zeit plotten, um zu sehen wie lange es dauert, den richtigen Offset zu finden + plt.plot(freq_log,'.-') + plt.show() + +Hier ist viel los, also gehen wir es durch. Einige Zeilen sind einfach, andere sehr komplex. :code:`samples` ist unser Eingang und :code:`out` sind die Ausgangs-Abtastwerte. :code:`phase` und :code:`frequency` sind wie das :code:`mu` aus dem Zeitsync-Code. Sie enthalten die aktuellen Offset-Schätzungen, und bei jeder Schleifeniteration erstellen wir die Ausgangs-Abtastwerte, indem wir die Eingangs-Abtastwerte mit :code:`np.exp(-1j*phase)` multiplizieren. Die Variable :code:`error` enthält die "Fehler"-Metrik, und für einen Costas-Regelkreis 2. Ordnung ist es eine sehr einfache Gleichung. Wir multiplizieren den reellen Teil des Abtastwerts (I) mit dem imaginären Teil (Q), und da Q für BPSK gleich null sein sollte, wird die Fehlerfunktion minimiert, wenn kein Phasen- oder Frequenzoffset Energie von I nach Q verschiebt. Für einen Costas-Regelkreis 4. Ordnung ist es etwas komplexer, aber nicht viel länger, da sowohl I als auch Q Energie haben, selbst wenn kein Phasen- oder Frequenzoffset für QPSK vorhanden ist. Wenn du neugierig bist, wie es aussieht, klicke unten, aber wir verwenden es in unserem Code vorerst nicht. Der Grund, warum es für QPSK funktioniert, ist, dass wenn du den Absolutwert von I und Q nimmst, du +1+1j erhältst, und wenn kein Phasen- oder Frequenzoffset vorhanden ist, sollte die Differenz zwischen den Absolutwerten von I und Q nahe null sein. + +.. raw:: html + +
+ Costas-Regelkreis 4. Ordnung Fehlergleichung (für Neugierige) + +.. code-block:: python + + # Für QPSK + def phase_detector_4(sample): + if sample.real > 0: + a = 1.0 + else: + a = -1.0 + if sample.imag > 0: + b = 1.0 + else: + b = -1.0 + return a * sample.imag - b * sample.real + + + + +.. raw:: html + +
+ +Die Variablen :code:`alpha` und :code:`beta` bestimmen, wie schnell Phase bzw. Frequenz aktualisiert werden. Es gibt eine Theorie dahinter, warum ich diese zwei Werte gewählt habe; wir werden sie hier jedoch nicht besprechen. Wenn du neugierig bist, kannst du versuchen, :code:`alpha` und/oder :code:`beta` zu optimieren, um zu sehen, was passiert. + +Wir protokollieren den Wert von :code:`freq` bei jeder Iteration, um ihn am Ende zu plotten und zu sehen, wie der Costas-Regelkreis auf den richtigen Frequenzoffset konvergiert. Wir müssen :code:`freq` mit der Abtastrate multiplizieren und von der Winkelfrequenz in Hz umrechnen, indem wir durch :math:`2\pi` dividieren. Beachte: Wenn du vor dem Costas-Regelkreis eine Zeitsynchronisation durchgeführt hast, musst du auch durch deinen :code:`sps`-Wert (z.B. 8) dividieren, da die Abtastwerte aus der Zeitsynchronisation mit einer Rate ausgegeben werden, die gleich deiner ursprünglichen Abtastrate geteilt durch :code:`sps` ist. + +Abschließend addieren oder subtrahieren wir nach der Neuberechnung der Phase genug :math:`2 \pi`, um die Phase zwischen 0 und :math:`2 \pi` zu halten, was die Phase umläuft. + +Unser Signal vor und nach dem Costas-Regelkreis sieht so aus: + +.. image:: ../_images/costas-loop-output.svg + :align: center + :target: ../_images/costas-loop-output.svg + :alt: Python-Simulation eines Signals vor und nach dem Costas-Regelkreis + +Und die Frequenzoffset-Schätzung über die Zeit, die auf den richtigen Offset konvergiert (in diesem Beispielsignal wurde ein -300-Hz-Offset verwendet): + +.. image:: ../_images/costas-loop-freq-tracking.svg + :align: center + :target: ../_images/costas-loop-freq-tracking.svg + +Es dauert fast 70 Abtastwerte, bis der Algorithmus vollständig auf den Frequenzoffset eingerastet ist. Du kannst sehen, dass in meinem simulierten Beispiel nach der groben Frequenzsynchronisation noch etwa -300 Hz übrig waren. Deines kann variieren. Wie ich bereits erwähnt habe, kannst du die grobe Frequenzsynchronisation deaktivieren und den anfänglichen Frequenzoffset auf einen beliebigen Wert setzen und sehen, ob der Costas-Regelkreis es herausfindet. + +Der Costas-Regelkreis hat neben der Beseitigung des Frequenzoffsets unser BPSK-Signal auf den I-Anteil ausgerichtet, sodass Q wieder null ist. Es ist ein nützlicher Nebeneffekt des Costas-Regelkreises, und er lässt den Costas-Regelkreis im Wesentlichen als unseren Demodulator fungieren. Jetzt müssen wir nur noch I nehmen und prüfen, ob es größer oder kleiner als null ist. Wir werden nicht wissen, wie negativ und positiv zu 0 und 1 werden, da möglicherweise eine Invertierung vorhanden ist oder nicht; der Costas-Regelkreis (oder unsere Zeitsynchronisation) hat keine Möglichkeit, das zu wissen. Hier kommt die differentielle Codierung ins Spiel. Sie beseitigt die Mehrdeutigkeit, da 1er und 0er darauf basieren, ob sich das Symbol geändert hat, nicht ob es +1 oder -1 war. Wenn wir differentielle Codierung hinzufügten, würden wir immer noch BPSK verwenden. Wir würden einen Differenzcodierungsblock direkt vor der Modulation auf der Tx-Seite und direkt nach der Demodulation auf der Rx-Seite hinzufügen. + +Unten ist eine Animation der gleichzeitig laufenden Zeit- und Frequenzsynchronisation. Die Zeitsynchronisation erfolgt fast sofort, aber die Frequenzsynchronisation dauert fast die gesamte Animation, bis sie sich vollständig eingestellt hat. Das lag daran, dass :code:`alpha` und :code:`beta` mit 0,005 bzw. 0,001 zu niedrig eingestellt waren. Den Code zur Erzeugung dieser Animation findest du `hier `_. + +.. image:: ../_images/costas_animation.gif + :align: center + :target: ../_images/costas_animation.gif + :alt: Costas-Regelkreis-Animation + +Der folgende (ausgeklappte) Codeblock enthält das vollständige Python-Beispiel des bisherigen Kapitels. Dieses wurde mit Python 3.12.3 und NumPy 1.26.4 getestet. Es enthält auch eine Bitfehlerprüfung am Ende. AWGN wurde weggelassen, um zu sehen, wie eng das BPSK allein durch Synchronisation werden kann. Du kannst AWGN hinzufügen, z.B. direkt nach dem Hinzufügen der Bruchteilverzögerung. Beachte, dass der Plot von IQ über die Zeit vor der Frequenzsynchronisation liegt, sodass du sehen kannst, wie die BPSK-Energie langsam zwischen I und Q wechselt. + +.. raw:: html + +
+ Vollständiges Python-Beispiel + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + from scipy import signal + + # BPSK-Signal erstellen + num_symbols = 100 + sps = 8 + bits = np.random.randint(0, 2, num_symbols) # Zu übertragende Daten, 1er und 0er + pulse_train = np.array([]) + for bit in bits: + pulse = np.zeros(sps) + pulse[0] = bit*2-1 # ersten Wert auf 1 oder -1 setzen + pulse_train = np.concatenate((pulse_train, pulse)) # die 8 Abtastwerte zum Signal hinzufügen + + # Impulsformung auf BPSK anwenden + num_taps = 101 + beta = 0.35 + Ts = sps # Abtastrate als 1 Hz angenommen, Symbolperiode ist 8 + t = np.arange(-51, 52) # letzte Zahl nicht enthalten + h = np.sinc(t/Ts) * np.cos(np.pi*beta*t/Ts) / (1 - (2*beta*t/Ts)**2) + samples = np.convolve(pulse_train, h, 'same') + + # Fraktionalen Verzögerungsfilter erstellen und anwenden, um zufälligen Timing-Offset zu emulieren + delay = 0.456 # fraktionale Verzögerung, in Abtastwerten + N = 21 # Anzahl der Taps, ungerade halten + n = np.arange(-(N-1)//2, N//2+1) # -10,-9,...,0,...,9,10 + h = np.sinc(n - delay) # Filtertaps berechnen + h *= np.hamming(N) # Filter fenstern + h /= np.sum(h) # für Einheitsverstärkung normalisieren + samples = np.convolve(samples, h) # Filter anwenden + + # Erheblichen Frequenzoffset anwenden + fs = 1e6 # Abtastrate als 1 MHz angenommen + fo = 13000 # Frequenzoffset simulieren – GROBER OFFSET! + Ts = 1/fs # Abtastperiode berechnen + t = np.arange(0, Ts*len(samples), Ts) # Zeitvektor erstellen + samples = samples * np.exp(1j*2*np.pi*fo*t) # Frequenzverschiebung durchführen + + # Groben Frequenzoffset schätzen und korrigieren + samples_sq = samples**2 + psd = np.fft.fftshift(np.abs(np.fft.fft(samples_sq, 2048))) + f = np.linspace(-fs/2.0, fs/2.0, len(psd)) + max_freq = f[np.argmax(psd)] / 2.0 + print(f"Geschätzter Frequenzoffset: {max_freq:.2f} Hz") + Ts = 1/fs # Abtastperiode berechnen + t = np.arange(0, Ts*len(samples), Ts) # Zeitvektor erstellen + samples = samples * np.exp(-1j*2*np.pi*max_freq*t) + + # An diesem Punkt sollte weniger als 1 kHz Frequenzoffset im Signal vorhanden sein + + # Symbol-/Timing-Synchronisation + mu = 0 # Anfangsschätzung der Phase des Abtastwerts + out = np.zeros(len(samples) // sps + 2, dtype=np.complex64) + out_rail = np.zeros(len(samples) // sps + 2, dtype=np.complex64) + i_in = 0 # Eingangs-Abtastwert-Index + i_out = 2 # Ausgangsindex + interpolation_factor = 16 + samples_interpolated = signal.resample_poly(samples, interpolation_factor, 1) + while i_out < len(samples) and i_in+16 < len(samples): + out[i_out] = samples_interpolated[i_in*interpolation_factor + int(mu*interpolation_factor)] + out_rail[i_out] = int(np.real(out[i_out]) > 0) + 1j*int(np.imag(out[i_out]) > 0) + x = (out_rail[i_out] - out_rail[i_out-2]) * np.conj(out[i_out-1]) + y = (out[i_out] - out[i_out-2]) * np.conj(out_rail[i_out-1]) + mm_val = np.real(y - x) + mu += sps + 0.3*mm_val + i_in += int(np.floor(mu)) + mu = mu - np.floor(mu) + i_out += 1 + out = out[3:i_out] + samples = out + + plt.figure(2) + plt.plot(np.real(samples)) + plt.plot(np.imag(samples)) + plt.xlabel('Abtastwert-Index') + plt.ylabel('Abtastwert') + plt.legend(['I', 'Q']) + plt.grid() + + N = len(samples) + phase = 0 + freq = 0 + alpha = 0.132 + beta = 0.00932 + out = np.zeros(N, dtype=np.complex64) + freq_log = [] + for i in range(N): + out[i] = samples[i] * np.exp(-1j*phase) + error = np.real(out[i]) * np.imag(out[i]) + + freq += (beta * error) + freq_log.append(freq * fs / (2*np.pi)) + phase += freq + (alpha * error) + + while phase >= 2*np.pi: + phase -= 2*np.pi + while phase < 0: + phase += 2*np.pi + + # Bitfehlerrate berechnen + rx_bits = (np.real(out) > 0).astype(int) + num_bit_errors = np.sum(rx_bits != bits[:len(rx_bits)]) + print(f"Anzahl der Bitfehler: {num_bit_errors} von {len(rx_bits)} Bits, BER: {num_bit_errors/len(rx_bits):.4f}") + + # Frequenz über Zeit plotten + plt.figure(0) + plt.plot(freq_log,'.-') + plt.xlabel('Abtastwert-Index') + plt.ylabel('Frequenzoffset-Schätzung (Hz)') + + # Nach ~80 Abtastwerten synchronisiert – Konstellation der restlichen 20 plotten + plt.figure(1) + plt.plot(np.real(out[80:]), np.imag(out[80:]), '.') + plt.xlabel('I') + plt.ylabel('Q') + plt.xlim(-1.5, 1.5) + plt.ylim(-1.5, 1.5) + plt.grid() + plt.show() + +.. raw:: html + +
+ +*************************** +Rahmensynchronisation +*************************** + +Wir haben besprochen, wie man Zeit-, Frequenz- und Phasenoffsets in unserem empfangenen Signal korrigiert. Aber die meisten modernen Kommunikationsprotokolle senden nicht einfach mit 100% Tastverhältnis Bits in einem Strom. Stattdessen verwenden sie Pakete/Rahmen. Am Empfänger müssen wir in der Lage sein zu erkennen, wann ein neuer Rahmen beginnt. Üblicherweise enthält der Rahmen-Header (auf der MAC-Schicht) die Anzahl der Bytes im Rahmen. Wir können diese Information nutzen, um die Länge des Rahmens z.B. in Abtastwerten oder Symbolen zu kennen. Dennoch ist die Erkennung des Rahmenbeginns eine völlig separate Aufgabe. Unten ist ein Beispiel einer WiFi-Rahmenstruktur. Beachte, dass das Allererste, was übertragen wird, ein PHY-Schicht-Header ist, und die erste Hälfte dieses Headers ist eine "Präambel". Diese Präambel enthält eine Synchronisierungssequenz, die der Empfänger verwendet, um den Rahmenbeginn zu erkennen, und sie ist eine Sequenz, die dem Empfänger im Voraus bekannt ist. + +.. image:: ../_images/wifi-frame.png + :scale: 60 % + :align: center + +Eine gängige und unkomplizierte Methode zur Erkennung dieser Sequenzen am Empfänger ist die Kreuzkorrelation der empfangenen Abtastwerte mit der bekannten Sequenz. Wenn die Sequenz vorkommt, ähnelt diese Kreuzkorrelation einer Autokorrelation (mit hinzugefügtem Rauschen). Typischerweise werden für Präambeln Sequenzen gewählt, die gute Autokorrelationseigenschaften haben, z.B. erzeugt die Autokorrelation der Sequenz eine einzelne starke Spitze bei 0 und keine anderen Spitzen. Ein Beispiel sind Barker-Codes; in 802.11/WiFi wird eine Barker-Sequenz der Länge 11 für die 1- und 2-Mbit/sec-Raten verwendet: + +.. code-block:: + + +1 +1 +1 −1 −1 −1 +1 −1 −1 +1 −1 + +Du kannst sie als 11 BPSK-Symbole betrachten. Wir können die Autokorrelation dieser Sequenz sehr einfach in Python betrachten: + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + x = [1,1,1,-1,-1,-1,1,-1,-1,1,-1] + plt.plot(np.correlate(x,x,'same'),'.-') + plt.grid() + plt.show() + +.. image:: ../_images/barker-code.svg + :align: center + :target: ../_images/barker-code.svg + +Du kannst sehen, dass es in der Mitte 11 (Länge der Sequenz) ist und für alle anderen Verzögerungen -1 oder 0. Es eignet sich gut zum Auffinden des Rahmenbeginns, da es im Wesentlichen die Energie von 11 Symbolen integriert, um eine 1-Bit-Spitze in der Ausgabe der Kreuzkorrelation zu erzeugen. Tatsächlich ist der schwierigste Teil der Rahmenbeginnserkennung die Bestimmung eines guten Schwellenwerts. Du möchtest nicht, dass Rahmen, die eigentlich nicht Teil deines Protokolls sind, ausgelöst werden. Das bedeutet, dass du zusätzlich zur Kreuzkorrelation auch eine Art Leistungsnormalisierung durchführen musst, was wir hier nicht berücksichtigen. Bei der Wahl eines Schwellenwerts musst du einen Kompromiss zwischen Erkennungswahrscheinlichkeit und Fehlalarmwahrscheinlichkeit machen. Denke daran, dass der Rahmen-Header selbst Informationen enthält, sodass einige Fehlalarme in Ordnung sind; du wirst schnell feststellen, dass es kein tatsächlicher Rahmen ist, wenn du versuchst, den Header zu decodieren und die CRC zwangsläufig fehlschlägt (weil es kein tatsächlicher Rahmen war). Während einige Fehlalarme in Ordnung sind, ist das Verpassen einer Rahmenerkennung schlecht. + +Eine weitere Sequenz mit hervorragenden Autokorrelationseigenschaften sind Zadoff-Chu-Sequenzen, die in LTE verwendet werden. Sie haben den Vorteil, in Mengen zu existieren; du kannst mehrere verschiedene Sequenzen haben, die alle gute Autokorrelationseigenschaften haben, aber sich gegenseitig nicht auslösen (d.h. auch gute Kreuzkorrelationseigenschaften, wenn du verschiedene Sequenzen in der Menge kreuzkorrelierst). Dank dieser Eigenschaft werden verschiedenen Zelltürmen unterschiedliche Sequenzen zugewiesen, sodass ein Mobiltelefon nicht nur den Rahmenbeginn finden kann, sondern auch weiß, von welchem Turm es empfängt. diff --git a/content-de/usrp.rst b/content-de/usrp.rst new file mode 100644 index 00000000..9abea439 --- /dev/null +++ b/content-de/usrp.rst @@ -0,0 +1,351 @@ +.. _usrp-chapter: + +#################################### +USRP in Python +#################################### + +.. image:: ../_images_de/usrp.png + :scale: 50 % + :align: center + :alt: Die USRP-Gerätefamilie von Ettus Research + +In diesem Kapitel lernst du, wie du die UHD-Python-API verwendest, um ein `USRP `_ zu steuern und Signale zu empfangen/senden. Das USRP ist eine Reihe von SDRs von Ettus Research (jetzt Teil von NI). Wir besprechen das Senden und Empfangen am USRP in Python und gehen auf USRP-spezifische Themen ein, darunter Stream-Argumente, Subdevices, Kanäle sowie 10-MHz- und PPS-Synchronisation. + +************************ +Software/Treiber Installation +************************ + +Der in diesem Lehrbuch bereitgestellte Python-Code sollte unter Windows, Mac und Linux funktionieren, aber wir werden nur Treiber-/API-Installationsanweisungen für Ubuntu 22 bereitstellen (obwohl die folgenden Anweisungen auf den meisten Debian-basierten Distributionen funktionieren sollten). Wir beginnen mit der Erstellung einer Ubuntu-22-VirtualBox-VM; überspringe den VM-Teil, wenn dein Betriebssystem bereits einsatzbereit ist. Alternativ, wenn du Windows 11 verwendest, läuft das Windows-Subsystem für Linux (WSL) mit Ubuntu 22 recht gut und unterstützt Grafik von Haus aus. + +Ubuntu-22-VM einrichten +######################## + +(Optional) + +1. Ubuntu 22.04 Desktop .iso herunterladen – https://ubuntu.com/download/desktop +2. `VirtualBox `_ installieren und öffnen. +3. Eine neue VM erstellen. Für die Speichergröße empfehle ich, 50 % des RAM deines Computers zu verwenden. +4. Die virtuelle Festplatte erstellen, VDI wählen und dynamisch Größe zuweisen. 15 GB sollten ausreichen. Wenn du auf der sicheren Seite sein möchtest, kannst du mehr verwenden. +5. Die VM starten. Du wirst nach Installationsmedien gefragt. Wähle die Ubuntu-22-Desktop-.iso-Datei. Wähle „Ubuntu installieren", verwende Standardoptionen, und ein Popup warnt dich vor den Änderungen, die du vornehmen wirst. Klicke auf „Fortfahren". Wähle Name/Passwort und warte dann, bis die VM die Initialisierung abgeschlossen hat. Nach dem Abschluss wird die VM neu gestartet, aber du solltest die VM nach dem Neustart ausschalten. +6. In die VM-Einstellungen gehen (das Zahnrad-Symbol). +7. Unter System > Prozessor mindestens 3 CPUs auswählen. Wenn du eine echte Grafikkarte hast, wähle unter Anzeige > Videospeicher einen viel höheren Wert. +8. Die VM starten. +9. Für USRP-Geräte mit USB-Anschluss musst du VM-Gastzusätze installieren. Gehe innerhalb der VM zu Geräte > Gasterweiterungen-CD einlegen > klicke auf „Ausführen", wenn ein Fenster erscheint. Folge den Anweisungen. Starte die VM neu und versuche dann, das USRP an die VM weiterzuleiten, vorausgesetzt, es erscheint in der Liste unter Geräte > USB. Die gemeinsame Zwischenablage kann über Geräte > Gemeinsame Zwischenablage > Bidirektional aktiviert werden. + +UHD und Python-API installieren +################################# + +Die folgenden Terminal-Befehle sollten die neueste Version von UHD einschließlich der Python-API erstellen und installieren: + +.. code-block:: bash + + sudo apt update + sudo apt install git cmake libboost-all-dev libusb-1.0-0-dev build-essential + sudo pip install pybind11[global] + pip install numpy==1.26.4 docutils mako requests ruamel.yaml setuptools + cd ~ + git clone https://github.com/EttusResearch/uhd.git + cd uhd + git checkout v4.8.0.0 + cd host + mkdir build + cd build + cmake -DENABLE_TESTS=OFF -DENABLE_C_API=OFF -DENABLE_PYTHON_API=ON -DENABLE_MANUAL=OFF .. + make -j8 + sudo make install + sudo ldconfig + +Weitere Hilfe findest du auf Ettus' offizieller Seite `Building and Installing UHD from source `_. Beachte, dass es auch Methoden zum Installieren der Treiber gibt, die kein Erstellen aus dem Quellcode erfordern. + +UHD-Treiber und Python-API testen +################################### + +Öffne ein neues Terminal und gib folgende Befehle ein: + +.. code-block:: bash + + python3 + import uhd + usrp = uhd.usrp.MultiUSRP() + samples = usrp.recv_num_samps(10000, 100e6, 1e6, [0], 50) + print(samples[0:10]) + +Wenn keine Fehler auftreten, bist du startklar! + + +USRP-Geschwindigkeit in Python benchmarken +############################################ + +(Optional) + +Wenn du die Standardinstallation aus dem Quellcode verwendet hast, sollte der folgende Befehl die Empfangsrate deines USRP mit der Python-API benchmarken. Wenn die Verwendung von 56e6 viele verworfene Samples oder Überläufe verursacht, versuche die Zahl zu verringern. Verworfene Samples werden nicht unbedingt alles ruinieren, aber es ist eine gute Möglichkeit, die Ineffizienzen zu testen, die z.B. bei der Verwendung einer VM oder eines älteren Computers auftreten können. Bei Verwendung eines B 2X0 sollte ein relativ moderner Computer mit einem USB-3.0-Anschluss in der Lage sein, 56 MHz ohne verworfene Samples zu verarbeiten, insbesondere wenn num_recv_frames so hoch eingestellt ist. + +.. code-block:: bash + + python /usr/lib/uhd/examples/python/benchmark_rate.py --rx_rate 56e6 --args "num_recv_frames=1000" + + +************************ +Empfangen +************************ + +Das Empfangen von Samples von einem USRP ist mit der integrierten Komfortfunktion „recv_num_samps()" extrem einfach. Der folgende Python-Code stimmt das USRP auf 100 MHz ein, verwendet eine Abtastrate von 1 MHz und nimmt 10.000 Samples vom USRP auf, mit einer Empfangsverstärkung von 50 dB: + +.. code-block:: python + + import uhd + usrp = uhd.usrp.MultiUSRP() + samples = usrp.recv_num_samps(10000, 100e6, 1e6, [0], 50) # Einheiten: N, Hz, Hz, Liste der Kanal-IDs, dB + print(samples[0:10]) + +Die [0] weist das USRP an, seinen ersten Eingangsport zu verwenden und nur einen Kanal an Samples zu empfangen (um z.B. mit einem B210 gleichzeitig auf zwei Kanälen zu empfangen, könntest du [0, 1] verwenden). + +Hier ist ein Tipp, wenn du versuchst, mit einer hohen Rate zu empfangen, aber Überläufe bekommst (O's erscheinen in der Konsole). Anstatt :code:`usrp = uhd.usrp.MultiUSRP()` verwende: + +.. code-block:: python + + usrp = uhd.usrp.MultiUSRP("num_recv_frames=1000") + +Dies macht den Empfangspuffer viel größer (der Standardwert ist 32) und hilft, Überläufe zu reduzieren. Die tatsächliche Größe des Puffers in Bytes hängt vom USRP und der Art der Verbindung ab, aber :code:`num_recv_frames` auf einen Wert deutlich höher als 32 zu setzen, hilft in der Regel. + +Für ernsthafte Anwendungen empfehle ich, die Komfortfunktion recv_num_samps() nicht zu verwenden, da sie einige interessante Vorgänge im Hintergrund verbirgt, und es gibt eine Einrichtung, die bei jedem Aufruf stattfindet und die wir möglicherweise nur einmal zu Beginn durchführen möchten, z.B. wenn wir Samples unbegrenzt empfangen möchten. Der folgende Code hat die gleiche Funktionalität wie recv_num_samps() — tatsächlich ist es fast genau das, was aufgerufen wird, wenn du die Komfortfunktion verwendest — aber jetzt haben wir die Möglichkeit, das Verhalten zu ändern: + +.. code-block:: python + + import uhd + import numpy as np + + usrp = uhd.usrp.MultiUSRP() + + num_samps = 10000 # Anzahl der empfangenen Samples + center_freq = 100e6 # Hz + sample_rate = 1e6 # Hz + gain = 50 # dB + + usrp.set_rx_rate(sample_rate, 0) + usrp.set_rx_freq(uhd.libpyuhd.types.tune_request(center_freq), 0) + usrp.set_rx_gain(gain, 0) + + # Stream und Empfangspuffer einrichten + st_args = uhd.usrp.StreamArgs("fc32", "sc16") + st_args.channels = [0] + metadata = uhd.types.RXMetadata() + streamer = usrp.get_rx_stream(st_args) + recv_buffer = np.zeros((1, 1000), dtype=np.complex64) + + # Stream starten + stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.start_cont) + stream_cmd.stream_now = True + streamer.issue_stream_cmd(stream_cmd) + + # Samples empfangen + samples = np.zeros(num_samps, dtype=np.complex64) + for i in range(num_samps//1000): + streamer.recv(recv_buffer, metadata) + samples[i*1000:(i+1)*1000] = recv_buffer[0] + + # Stream stoppen + stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.stop_cont) + streamer.issue_stream_cmd(stream_cmd) + + print(len(samples)) + print(samples[0:10]) + +Mit num_samps auf 10.000 und recv_buffer auf 1000 gesetzt, läuft die for-Schleife 10 Mal, d.h. es gibt 10 Aufrufe von streamer.recv. Beachte, dass wir recv_buffer fest auf 1000 kodiert haben, aber du kannst den maximalen erlaubten Wert mit :code:`streamer.get_max_num_samps()` ermitteln, der oft bei etwa 3000 liegt. Beachte außerdem, dass recv_buffer 2D sein muss, da dieselbe API beim gleichzeitigen Empfangen auf mehreren Kanälen verwendet wird, aber in unserem Fall haben wir nur einen Kanal empfangen, sodass recv_buffer[0] uns das gewünschte 1D-Array von Samples lieferte. Du musst jetzt nicht zu viel darüber verstehen, wie der Stream gestartet/gestoppt wird, aber wisse, dass es neben dem „kontinuierlichen" Modus auch andere Optionen gibt, wie z.B. eine bestimmte Anzahl von Samples zu empfangen und den Stream automatisch stoppen zu lassen. Obwohl wir Metadaten in diesem Beispielcode nicht verarbeiten, enthält er alle auftretenden Fehler, die du durch Überprüfen von metadata.error_code bei jeder Iteration der Schleife kontrollieren kannst, falls gewünscht (Fehler erscheinen auch in der Konsole selbst als Ergebnis von UHD, also musst du sie nicht unbedingt in deinem Python-Code prüfen). + +Empfangsverstärkung +#################### + +Die folgende Liste zeigt den Verstärkungsbereich der verschiedenen USRPs — sie gehen alle von 0 dB bis zur unten angegebenen Zahl. Beachte, dass dies nicht dBm ist; es ist im Wesentlichen dBm kombiniert mit einem unbekannten Offset, da es sich nicht um kalibrierte Geräte handelt. + +* B200/B210/B200-mini: 76 dB +* X310/N210 mit WBX/SBX/UBX: 31,5 dB +* X310 mit TwinRX: 93 dB +* E310/E312: 76 dB +* N320/N321: 60 dB + +Du kannst auch den Befehl :code:`uhd_usrp_probe` in einem Terminal verwenden, und im Abschnitt „RX Frontend" wird der Verstärkungsbereich erwähnt. + +Beim Angeben der Verstärkung kannst du die normale set_rx_gain()-Funktion verwenden, die den Verstärkungswert in dB nimmt, aber du kannst auch set_normalized_rx_gain() verwenden, die einen Wert von 0 bis 1 nimmt und ihn automatisch in den Bereich des verwendeten USRP umrechnet. Dies ist praktisch, wenn du eine Anwendung erstellst, die verschiedene USRP-Modelle unterstützt. Der Nachteil der normalisierten Verstärkung ist, dass du keine Einheiten in dB mehr hast, sodass du z.B. den Betrag berechnen musst, wenn du deine Verstärkung um 10 dB erhöhen möchtest. + +Automatische Verstärkungsregelung +################################### + +Einige USRPs, einschließlich der B200- und E310-Serie, unterstützen die automatische Verstärkungsregelung (AGC), die die Empfangsverstärkung als Reaktion auf den empfangenen Signalpegel automatisch anpasst, um die ADC-Bits bestmöglich zu „füllen". AGC kann aktiviert werden mit: + +.. code-block:: python + + usrp.set_rx_agc(True, 0) # 0 für Kanal 0, d.h. den ersten Kanal des USRP + +Wenn du ein USRP hast, das keinen AGC implementiert, wird beim Ausführen der obigen Zeile eine Ausnahme ausgelöst. Wenn AGC aktiviert ist, hat das Einstellen der Verstärkung keinen Effekt. + +Stream-Argumente +**************** + +Im vollständigen Beispiel oben siehst du die Zeile :code:`st_args = uhd.usrp.StreamArgs("fc32", "sc16")`. Das erste Argument ist das CPU-Datenformat, also der Datentyp der Samples, sobald sie auf deinem Host-Computer sind. UHD unterstützt die folgenden CPU-Datentypen bei der Verwendung der Python-API: + +.. list-table:: + :widths: 15 20 30 + :header-rows: 1 + + * - Stream-Arg + - NumPy-Datentyp + - Beschreibung + * - fc64 + - np.complex128 + - Komplexwertige Daten mit doppelter Genauigkeit + * - fc32 + - np.complex64 + - Komplexwertige Daten mit einfacher Genauigkeit + +Möglicherweise siehst du andere Optionen in der Dokumentation für die UHD-C++-API, aber diese wurden in der Python-API nie implementiert, zumindest zum Zeitpunkt dieses Schreibens. + +Das zweite Argument ist das „Over-the-Wire"-Datenformat, d.h. der Datentyp der Samples, wie sie über USB/Ethernet/SFP zum Host gesendet werden. Für die Python-API sind die Optionen: „sc16", „sc12" und „sc8", wobei die 12-Bit-Option nur von bestimmten USRPs unterstützt wird. Diese Wahl ist wichtig, da die Verbindung zwischen dem USRP und dem Host-Computer oft der Engpass ist — durch den Wechsel von 16 Bit auf 8 Bit kannst du möglicherweise eine höhere Rate erreichen. Denke auch daran, dass viele USRPs ADCs mit 12 oder 14 Bit haben; die Verwendung von „sc16" bedeutet nicht, dass der ADC 16 Bit hat. + +Für den Kanalabschnitt der :code:`st_args` siehe den Abschnitt „Subdevice und Kanäle" unten. + +************************ +Senden +************************ + +Ähnlich wie die Komfortfunktion recv_num_samps() bietet UHD die Funktion send_waveform() zum Senden eines Batches von Samples. Ein Beispiel ist unten gezeigt. Wenn du eine Dauer (in Sekunden) angibst, die länger als das bereitgestellte Signal ist, wird es einfach wiederholt. Es hilft, die Werte der Samples zwischen -1,0 und 1,0 zu halten. + +.. code-block:: python + + import uhd + import numpy as np + usrp = uhd.usrp.MultiUSRP() + samples = 0.1*np.random.randn(10000) + 0.1j*np.random.randn(10000) # zufälliges Signal erzeugen + duration = 10 # Sekunden + center_freq = 915e6 + sample_rate = 1e6 + gain = 20 # [dB] niedrig starten und bei Bedarf erhöhen + usrp.send_waveform(samples, duration, center_freq, sample_rate, [0], gain) + +Für Details dazu, wie diese Komfortfunktion im Hintergrund funktioniert, sieh dir den Quellcode `hier `_ an. + + +Sendeleistungsverstärkung +########################## + +Ähnlich wie auf der Empfangsseite variiert der Sendeleistungsbereich je nach USRP-Modell und geht von 0 dB bis zur unten angegebenen Zahl: + +* B200/B210/B200-mini: 90 dB +* N210 mit WBX: 25 dB +* N210 mit SBX oder UBX: 31,5 dB +* E310/E312: 90 dB +* N320/N321: 60 dB + +Es gibt auch eine set_normalized_tx_gain()-Funktion, wenn du die Sendeleistungsverstärkung im Bereich 0 bis 1 angeben möchtest. + +************************************************ +Gleichzeitiges Senden und Empfangen +************************************************ + +Wenn du mit demselben USRP gleichzeitig senden und empfangen möchtest, ist der Schlüssel, dies mit mehreren Threads innerhalb desselben Prozesses zu tun; das USRP kann nicht mehrere Prozesse überspannen. Im C++-Beispiel `txrx_loopback_to_file `_ wird z.B. ein separater Thread erstellt, um den Sender auszuführen, und der Empfang erfolgt im Haupt-Thread. Du kannst auch einfach zwei Threads starten, einen zum Senden und einen zum Empfangen, wie es im Python-Beispiel `benchmark_rate `_ gemacht wird. Ein vollständiges Beispiel wird hier nicht gezeigt, da es ein ziemlich langes Beispiel wäre und Ettus' benchmark_rate.py immer als Ausgangspunkt dienen kann. + + +********************************* +Subdevice, Kanäle und Antennen +********************************* + +Eine häufige Fehlerquelle bei der Verwendung von USRPs ist die Auswahl des richtigen Subdevice und der Kanal-ID. Du hast vielleicht bemerkt, dass wir in jedem obigen Beispiel Kanal 0 verwendet haben und nichts im Zusammenhang mit Subdev angegeben haben. Wenn du ein B210 verwendest und einfach RF:B statt RF:A verwenden möchtest, musst du nur Kanal 1 statt 0 wählen. Aber bei USRPs wie dem X310, das zwei Tochterplatinen-Steckplätze hat, musst du UHD mitteilen, ob du Steckplatz A oder B und welchen Kanal auf dieser Tochterplatine verwenden möchtest, zum Beispiel: + +.. code-block:: python + + usrp.set_rx_subdev_spec("B:0") + +Wenn du den TX/RX-Anschluss statt RX2 (dem Standard) verwenden möchtest, ist es so einfach wie: + +.. code-block:: python + + usrp.set_rx_antenna('TX/RX', 0) # Kanal 0 auf 'TX/RX' setzen + +Dies steuert im Wesentlichen nur einen HF-Schalter auf dem USRP, um vom anderen SMA-Steckverbinder weiterzuleiten. + +Um gleichzeitig auf zwei Kanälen zu empfangen oder zu senden, gib anstelle von :code:`st_args.channels = [0]` eine Liste an, z.B. :code:`[0,1]`. Der Empfangs-Samples-Puffer muss in diesem Fall die Größe (2, N) statt (1, N) haben. Denke daran, dass bei den meisten USRPs beide Kanäle einen LO teilen, sodass du nicht gleichzeitig auf verschiedene Frequenzen abstimmen kannst. + +************************** +Synchronisation auf 10 MHz und PPS +************************** + +Einer der großen Vorteile der Verwendung eines USRP gegenüber anderen SDRs ist die Fähigkeit, sich mit einer externen Quelle oder einem integrierten `GPSDO `_ zu synchronisieren, was Anwendungen mit mehreren Empfängern wie TDOA ermöglicht. Wenn du eine externe 10-MHz- und PPS-Quelle an dein USRP angeschlossen hast, musst du nach der Initialisierung deines USRP diese zwei Zeilen aufrufen: + +.. code-block:: python + + usrp.set_clock_source("external") + usrp.set_time_source("external") + +Wenn du ein integriertes GPSDO verwendest, nutze stattdessen: + +.. code-block:: python + + usrp.set_clock_source("gpsdo") + usrp.set_time_source("gpsdo") + +Auf der Frequenzsynchronisationsseite gibt es nicht viel mehr zu tun; der im Mischer des USRP verwendete LO wird jetzt an die externe Quelle oder das `GPSDO `_ gebunden. Aber auf der Zeitseite möchtest du möglicherweise das USRP anweisen, genau beim PPS mit der Abtastung zu beginnen. Dies kann mit folgendem Code gemacht werden: + +.. code-block:: python + + # Kopiere das Empfangsbeispiel oben, alles bis zu # Stream starten + + # Auf 1 PPS warten, dann die Zeit beim nächsten PPS auf 0.0 setzen + time_at_last_pps = usrp.get_time_last_pps().get_real_secs() + while time_at_last_pps == usrp.get_time_last_pps().get_real_secs(): + time.sleep(0.1) # weiter warten, bis es passiert – wenn diese while-Schleife nie endet, ist das PPS-Signal nicht vorhanden + usrp.set_time_next_pps(uhd.libpyuhd.types.time_spec(0.0)) + + # Empfang von num_samps Samples genau 3 Sekunden nach dem letzten PPS planen + stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.num_done) + stream_cmd.num_samps = num_samps + stream_cmd.stream_now = False + stream_cmd.time_spec = uhd.libpyuhd.types.time_spec(3.0) # Startzeit festlegen (versuche, diesen Wert anzupassen) + streamer.issue_stream_cmd(stream_cmd) + + # Samples empfangen. recv() gibt zuerst Nullen zurück, dann unsere Samples, dann weitere Nullen + waiting_to_start = True # verfolgen, wo wir uns im Zyklus befinden + nsamps = 0 + i = 0 + samples = np.zeros(num_samps, dtype=np.complex64) + while nsamps != 0 or waiting_to_start: + nsamps = streamer.recv(recv_buffer, metadata) + if nsamps and waiting_to_start: + waiting_to_start = False + elif nsamps: + samples[i:i+nsamps] = recv_buffer[0][0:nsamps] + i += nsamps + +Wenn es scheint, als würde es nicht funktionieren, aber keine Fehler auftreten, versuche den Wert 3.0 auf etwas zwischen 1.0 und 5.0 zu ändern. Du kannst auch die Metadaten nach dem Aufruf von recv() überprüfen, indem du :code:`if metadata.error_code != uhd.types.RXMetadataErrorCode.none:` prüfst. + +Zum Debuggen kannst du überprüfen, ob das 10-MHz-Signal beim USRP ankommt, indem du den Rückgabewert von :code:`usrp.get_mboard_sensor("ref_locked", 0)` überprüfst. Wenn das PPS-Signal nicht ankommt, wirst du es merken, weil die erste while-Schleife im obigen Code nie endet. + +********************************************** +Phasenkohärente Synchronisation mehrerer B210s für MIMO +********************************************** + +Um Operationen wie Ankunftsrichtungsbestimmung (DOA) und digitales Beamforming mit phasengesteuerter Antennengruppe durchzuführen, benötigst du in der Regel alle Empfangskanäle phasenkohärent, d.h. die relativen Phasen zwischen den Empfangskanälen bleiben konstant und können herausgerechnet werden. Die USRPs B200 und B210 basieren auf dem AD9361-RFIC, der den LO intern generiert; es gibt keine Möglichkeit, ihm einen externen LO zuzuführen. Selbst wenn du dem USRP ein 10-MHz-Referenzsignal und PPS zuführst, ermöglicht das nur die Synchronisation mehrerer USRPs in Frequenz und Abtasttakt, nicht in Phase, da jedes Mal, wenn das Gerät eingeschaltet oder auf eine andere Frequenz abgestimmt wird, ein neuer zufälliger Phasenversatz durch die Teiler in den VCO/PLL-Ketten entsteht. Weitere Informationen findest du auf `dieser Seite `_. Eine Methode zur Phasensynchronisation besteht darin, Hardware hinzuzufügen, bei der ein Kalibriersignal (entweder vom USRP erzeugt, eine Breitbandrauschquelle oder ein Ton) aufgeteilt und in alle Empfangsanschlüsse eingespeist wird, und bei jedem Ein- oder Umschalten der USRPs eine schnelle Kalibrierung durchgeführt wird. Beachte, dass auch das Ändern der Verstärkung zu Phasenverschiebungen führt, aber solange die B210s auf derselben Verstärkung gehalten werden, sollte sich die Phasendifferenz nicht wesentlich ändern. Das `Techtile-Projekt `_ enthält weitere Informationen zu diesem Thema, einschließlich benutzerdefinierter Images, die es mehreren B210s ermöglichen können, gemeinsam umzustimmen und die Synchronisation aufrechtzuerhalten, obwohl wahrscheinlich jedes Mal, wenn die Radios eingeschaltet werden, eine Kalibrierung mit externer Hardware erforderlich ist. + +**** +GPIO +**** + +Die meisten USRPs haben einen GPIO-Header. Beim B200/B210 ist es der J504-Header, beim X310 befindet er sich an der Vorderseite. + +Zunächst einige der von Ettus verwendeten Begriffe. **CTRL** legt fest, ob der Pin durch ATR (automatisch) oder nur durch manuelle Steuerung gesteuert wird (1 für ATR, 0 für manuell). **DDR** (Data Direction Register) definiert, ob ein GPIO ein Ausgang (0) oder ein Eingang (1) ist. **OUT** wird verwendet, um den Wert eines Pins manuell zu setzen (nur im manuellen CTRL-Modus zu verwenden). + +Beispiel für die Verwendung von GPIO als Ausgang für den „AUX I/O"-Anschluss an der Vorderseite des X310; weitere Informationen findest du in `dieser Dokumentation `_. + +.. code-block:: python + + import uhd + import time + usrp = uhd.usrp.MultiUSRP() + usrp.set_gpio_attr('FP0A', 'CTRL', 0x000, 0xFFF) + usrp.set_gpio_attr('FP0A', 'DDR', 0xFFF, 0xFFF) + for i in range(10): + print("Aus") + usrp.set_gpio_attr('FP0A', 'OUT', 0x000, 0xFFF) + time.sleep(1) + print("An") + usrp.set_gpio_attr('FP0A', 'OUT', 0xFFF, 0xFFF) + time.sleep(1) diff --git a/content-es/intro.rst b/content-es/intro.rst index d0ecf9db..7c67449e 100644 --- a/content-es/intro.rst +++ b/content-es/intro.rst @@ -62,6 +62,7 @@ Gracias a cualquiera que haya leído alguna parte de este libro de texto y haya - `Daniel Versluis `_ por `traducir PySDR al Aleman `_ - `mrbloom `_ por `tranducir PySDR al ucraniano `_ - `Yimin Zhao `_ por `traducir PySDR al chino simplificado `_ -- `Eduardo Chancay `_ por `traducir PySDR al español `_ +- `Eduardo Chancay `_ por `traducir PySDR al español `_ +- `Dipl. Ing. (FH) Viet Dang `_ por `traducir PySDR al alemán `_ Así como todos nuestros `PySDR Patreon `_ ! \ No newline at end of file diff --git a/content-fr/intro.rst b/content-fr/intro.rst index 920a9110..52b7c25c 100644 --- a/content-fr/intro.rst +++ b/content-fr/intro.rst @@ -59,6 +59,7 @@ Nous remercions tous ceux qui ont lu une partie de ce manuel et nous ont fait pa - Matthew Hannon - James Hayek - Deidre Stuffer -- `Tarik Benaddi `_ pour la `traduction de PySDR en français `_ +- `Tarik Benaddi `_ pour la `traduction de PySDR en français `_ +- `Dipl. Ing. (FH) Viet Dang `_ pour la `traduction de PySDR en allemand `_ diff --git a/content-nl/intro.rst b/content-nl/intro.rst index d72ed591..04c015db 100644 --- a/content-nl/intro.rst +++ b/content-nl/intro.rst @@ -76,7 +76,8 @@ Bedankt aan iedereen die dit boek heeft gelezen en van feedback heeft voorzien, - Daniel Versluis voor het `vertalen van PySDR naar het Nederlands `_ - `mrbloom `_ voor het `vertalen van PySDR naar het Ukraiens `_ - `Yimin Zhao `_ voor het `vertalen van PySDR naar het Chinees `_ -- `Eduardo Chancay `_ voor het `vertalen van PySDR naar het Spaans `_ +- `Eduardo Chancay `_ voor het `vertalen van PySDR naar het Spaans `_ +- `Dipl. Ing. (FH) Viet Dang `_ voor het `vertalen van PySDR naar het Duits `_ ********************** Nederlandse vertaling diff --git a/content-ukraine/intro.rst b/content-ukraine/intro.rst index b5a2a593..cb39be73 100644 --- a/content-ukraine/intro.rst +++ b/content-ukraine/intro.rst @@ -87,7 +87,8 @@ textbook `_ за `переклад PySDR нідерландською `_ - `mrbloom `_ за `переклад PySDR українською `_ - `Yimin Zhao `_ за `переклад PySDR спрощеною китайською `_ -- `Eduardo Chancay `_ за `переклад PySDR іспанською `_ +- `Eduardo Chancay `_ за `переклад PySDR іспанською `_ +- `Dipl. Ing. (FH) Viet Dang `_ за `переклад PySDR німецькою `_ - John Marcovici А також усім прихильникам `PySDR Patreon `_! diff --git a/content-zh/intro.rst b/content-zh/intro.rst index 7216ea64..52599a5b 100644 --- a/content-zh/intro.rst +++ b/content-zh/intro.rst @@ -76,7 +76,8 @@ - `Daniel Versluis `_ 将 PySDR 翻译为 `荷兰语 `_ - `mrbloom `_ 将 PySDR 翻译为 `乌克兰语 `_ - `Yimin Zhao `_ 将 PySDR 翻译为 `简体中文 `_ -- `Eduardo Chancay `_ 将 PySDR 翻译为 `西班牙语 `_ +- `Eduardo Chancay `_ 将 PySDR 翻译为 `西班牙语 `_ +- `Dipl. Ing. (FH) Viet Dang `_ 将 PySDR 翻译为 `德语 `_ - John Marcovici - `Vishwaksen Reddy Dhareddy `_ 贡献了检测章节中的实时数据包检测部分 diff --git a/content/intro.rst b/content/intro.rst index 45d65b15..32b4aade 100644 --- a/content/intro.rst +++ b/content/intro.rst @@ -64,7 +64,8 @@ Thank you to anyone who has read any portion of this textbook and provided feedb - `Daniel Versluis `_ for `translating PySDR to Dutch `_ - `mrbloom `_ for `translating PySDR to Ukrainian `_ - `Yimin Zhao `_ for `translating PySDR to Simplified Chinese `_ -- `Eduardo Chancay `_ for `translating PySDR to Spanish `_ +- `Eduardo Chancay `_ for `translating PySDR to Spanish `_ +- `Dipl. Ing. (FH) Viet Dang `_ for `translating PySDR to German `_ - John Marcovici - `Vishwaksen Reddy Dhareddy `_ for contributing the Detection Chapter section on real-time packet detection diff --git a/figure-generating-scripts/generate_de_pngs.py b/figure-generating-scripts/generate_de_pngs.py new file mode 100644 index 00000000..3f92fac1 --- /dev/null +++ b/figure-generating-scripts/generate_de_pngs.py @@ -0,0 +1,204 @@ +""" +Recreate sine-wave.png and impulse1.png with German labels. +Outputs go directly to _images_de/. +""" +import numpy as np +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt +import matplotlib.patches as mpatches +from matplotlib.patches import FancyArrowPatch +import os + +OUT_DIR = os.path.join(os.path.dirname(__file__), '../_images_de') + +# ── shared style helpers ────────────────────────────────────────────────────── + +def arrow_axis(ax, xlabel, arrow_len_x=1.08, arrow_len_y=1.08, fontsize=13): + """Draw clean arrow-tip axes, hide the default spines/ticks.""" + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + ax.spines['left'].set_visible(False) + ax.spines['bottom'].set_visible(False) + ax.set_xticks([]) + ax.set_yticks([]) + + # x-axis arrow + ax.annotate('', xy=(arrow_len_x, 0), xycoords=('axes fraction', 'data'), + xytext=(0, 0), textcoords=('axes fraction', 'data'), + arrowprops=dict(arrowstyle='->', color='black', lw=1.2)) + # y-axis arrow + ax.annotate('', xy=(0, arrow_len_y), xycoords=('data', 'axes fraction'), + xytext=(0, 0), textcoords=('data', 'axes fraction'), + arrowprops=dict(arrowstyle='->', color='black', lw=1.2)) + + ax.text(arrow_len_x + 0.01, 0, xlabel, + transform=ax.get_yaxis_transform(), + fontsize=fontsize, va='center', ha='left', + fontstyle='italic') + + +# ── sine-wave.png (German: Zeit / Frequenz) ─────────────────────────────────── + +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(7, 2.2)) +fig.subplots_adjust(wspace=0.35, left=0.04, right=0.96, top=0.88, bottom=0.12) + +# Left: cosine wave — ~1.5 cycles, dashed tail, light grid +t = np.linspace(0, 1.3, 500) +y = np.cos(2 * np.pi * 1.15 * t) +split = 380 +ax1.plot(t[:split], y[:split], color='#1a6fdb', lw=2.0, solid_capstyle='round') +ax1.plot(t[split-1:], y[split-1:], color='#1a6fdb', lw=2.0, linestyle='--') +ax1.set_xlim(-0.05, 1.55) +ax1.set_ylim(-1.5, 1.7) +ax1.grid(True, color='#cccccc', lw=0.6, zorder=0) +arrow_axis(ax1, 'Zeit', arrow_len_x=1.06, arrow_len_y=1.12) +ax1.text(0.5, -0.22, r'$\cos(2\pi f t)$', transform=ax1.transAxes, + fontsize=12, ha='center', va='top', fontstyle='italic') + +# Right: frequency impulse at f — solid line (no arrowhead on spike) +ax2.plot([0.38, 0.38], [0.0, 0.75], color='#1a6fdb', lw=2.5, + solid_capstyle='round', transform=ax2.get_xaxis_transform()) +ax2.set_xlim(-0.05, 1.0) +ax2.set_ylim(-0.1, 1.1) +ax2.text(0.38, -0.12, r'$f$', transform=ax2.get_xaxis_transform(), + fontsize=12, ha='center', va='top', fontstyle='italic') +arrow_axis(ax2, 'Frequenz', arrow_len_x=1.06, arrow_len_y=1.12) + +fig.savefig(os.path.join(OUT_DIR, 'sine-wave.png'), dpi=150, bbox_inches='tight', + facecolor='white') +plt.close(fig) +print('Saved sine-wave.png') + + +# ── impulse1.png (German: Zeit / Frequenz) ──────────────────────────────────── + +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(7, 2.2)) +fig.subplots_adjust(wspace=0.35, left=0.04, right=0.96, top=0.88, bottom=0.12) + +# Left: single impulse in time domain +ax1.annotate('', xy=(0.35, 0.82), xycoords=('data', 'axes fraction'), + xytext=(0.35, 0.0), textcoords=('data', 'axes fraction'), + arrowprops=dict(arrowstyle='->', color='#1a6fdb', lw=3.5, + mutation_scale=12)) +ax1.set_xlim(-0.05, 1.0) +ax1.set_ylim(-0.1, 1.1) +arrow_axis(ax1, 'Zeit', arrow_len_x=1.06, arrow_len_y=1.12) + +# Right: flat magnitude (solid + dotted tail) +x_flat = np.linspace(0.0, 0.72, 200) +ax2.plot(x_flat, np.ones_like(x_flat) * 0.45, color='#1a6fdb', lw=2.8, + solid_capstyle='round') +x_dot = np.linspace(0.72, 0.98, 100) +ax2.plot(x_dot, np.ones_like(x_dot) * 0.45, color='#1a6fdb', lw=2.8, + linestyle=':', solid_capstyle='round') +ax2.set_xlim(-0.05, 1.0) +ax2.set_ylim(-0.1, 1.1) +arrow_axis(ax2, 'Frequenz', arrow_len_x=1.06, arrow_len_y=1.12) + +fig.savefig(os.path.join(OUT_DIR, 'impulse1.png'), dpi=150, bbox_inches='tight', + facecolor='white') +plt.close(fig) +print('Saved impulse1.png') + + +# ── dc-signal.png / dc-signal1.png (German: Zeit / Frequenz) ───────────────── + +def make_dc_signal(): + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(7, 2.2)) + fig.subplots_adjust(wspace=0.35, left=0.04, right=0.96, top=0.88, bottom=0.12) + + # Left: flat DC line in time domain + ax1.plot([0.05, 0.72], [0.45, 0.45], color='blue', lw=3.5, + solid_capstyle='round', transform=ax1.get_xaxis_transform()) + ax1.set_xlim(-0.05, 1.0) + ax1.set_ylim(-0.1, 1.1) + arrow_axis(ax1, 'Zeit', arrow_len_x=1.06, arrow_len_y=1.12) + + # Right: impulse at 0 (DC component) + ax2.plot([0.0, 0.0], [0.0, 0.78], color='blue', lw=3.5, + solid_capstyle='round', transform=ax2.get_xaxis_transform()) + ax2.set_xlim(-0.15, 1.0) + ax2.set_ylim(-0.1, 1.1) + ax2.text(0.0, -0.12, '0', transform=ax2.get_xaxis_transform(), + fontsize=12, ha='center', va='top') + arrow_axis(ax2, 'Frequenz', arrow_len_x=1.06, arrow_len_y=1.12) + + return fig + +fig = make_dc_signal() +fig.savefig(os.path.join(OUT_DIR, 'dc-signal.png'), dpi=150, bbox_inches='tight', + facecolor='white') +plt.close(fig) +print('Saved dc-signal.png') + +fig = make_dc_signal() +fig.savefig(os.path.join(OUT_DIR, 'dc-signal1.png'), dpi=150, bbox_inches='tight', + facecolor='white') +plt.close(fig) +print('Saved dc-signal1.png') + + +# ── symbols1.png (German: DATEN / Zeit / Ein Symbol / 7 Symbole gesamt) ────── + +bits = [1, 0, 1, 0, 0, 1, 0] +n = len(bits) + +# Build sharp square-wave path with a short lead-in at LOW level +x_wave = [-0.25, 0] +y_wave = [0, 0] +for i, b in enumerate(bits): + if i == 0: + x_wave += [0, 0] + y_wave += [0, b] + else: + x_wave += [i, i] + y_wave += [bits[i-1], b] + x_wave.append(i + 1) + y_wave.append(b) + +fig, ax = plt.subplots(figsize=(10, 2.5)) +for spine in ax.spines.values(): + spine.set_visible(False) +ax.set_xticks([]) +ax.set_yticks([]) + +ax.plot(x_wave, y_wave, color='#1f2fa8', lw=2.5, solid_capstyle='butt') + +# Bit labels centred in each symbol slot +for i, b in enumerate(bits): + ax.text(i + 0.5, 1.18, str(b), ha='center', va='bottom', + fontsize=15, color='#1f2fa8') + +# DATEN label on the far left, vertically centred on the wave +ax.text(-0.55, 0.5, 'DATEN', ha='right', va='center', + fontsize=16, fontweight='bold', color='black') + +# "(7 Symbole gesamt)" in red italic to the right of the wave +ax.text(n + 0.15, 0.5, '(7 Symbole gesamt)', ha='left', va='center', + fontsize=14, color='red', fontstyle='italic') + +# "Zeit →" small label + arrow at lower right of waveform +ax.annotate('', xy=(n - 0.05, -0.22), xytext=(n - 1.55, -0.22), + arrowprops=dict(arrowstyle='->', color='black', lw=1.3)) +ax.text(n - 1.62, -0.22, 'Zeit', ha='right', va='center', + fontsize=12, color='black') + +# Red measurement bracket under the 3rd symbol (index 2, the second "1") +bx_l, bx_r = 2.0, 3.0 +bx_mid = (bx_l + bx_r) / 2 +by = -0.42 +tick = 0.09 +ax.plot([bx_l, bx_r], [by, by], color='red', lw=2.0, solid_capstyle='butt') +ax.plot([bx_l, bx_l], [by - tick, by + tick], color='red', lw=2.0) +ax.plot([bx_r, bx_r], [by - tick, by + tick], color='red', lw=2.0) +ax.text(bx_mid, by - 0.14, 'Ein Symbol', ha='center', va='top', + fontsize=14, color='red', fontweight='bold') + +ax.set_xlim(-1.0, n + 3.2) +ax.set_ylim(-1.1, 1.7) + +fig.savefig(os.path.join(OUT_DIR, 'symbols1.png'), dpi=150, bbox_inches='tight', + facecolor='white') +plt.close(fig) +print('Saved symbols1.png') diff --git a/index-de.rst b/index-de.rst new file mode 100644 index 00000000..a0d636b1 --- /dev/null +++ b/index-de.rst @@ -0,0 +1,41 @@ +.. raw:: html + :file: _templates/homepage_de.html + +.. raw:: html + +
+

Expand for full table of contents

+ +.. toctree:: + :maxdepth: 3 + :numbered: 1 + + content-de/intro + content-de/frequency_domain + content-de/sampling + content-de/digital_modulation + content-de/pluto + content-de/usrp + content-de/bladerf + content-de/rtlsdr + content-de/hackrf + content-de/noise + content-de/filters + content-de/link_budgets + content-de/channel_coding + content-de/iq_files + content-de/multipath_fading + content-de/pulse_shaping + content-de/sync + content-de/rds + content-de/doa + content-de/2d_beamforming + content-de/phaser + content-de/cyclostationary + content-de/pyqt + content-de/detection + content-de/about_author + +.. raw:: html + +
diff --git a/requirements.txt b/requirements.txt index 36ebd0be..c93c87b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +setuptools sphinx==4.4.0 sphinxcontrib-tikz==0.4.20 sphinxcontrib-spelling==8.0.0