Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 8 Min.

PWM und Servo-Steuerung mit .NET

Pulsweitenmodulation als technische Grundlage der digitalen Steuerung von RC-Modellen.
© Sofija De Mitri, Patrizio De Mitri, Event Wave

Im RC-Modellbau werden die meisten Komponenten mit PWM-Signalen (Pulsweitenmodulation) gesteuert. Das gilt insbesondere für die Komponenten, die wir zum Fahren brauchen – namentlich: Servos und ESCs (Electronic Speed Controller) also der Fahrtregler. Wollen wir die Steuerung des DDC-Trucks, den wir im Artikel „SignalRC – in Echtzeit ans Steuer“ [1] ausführlich vorgestellt haben, über unseren Raspberry Pi durchführen, müssen wir diese Signale senden können.

Die Lichter – als PWM-Grundlage

Fangen wir mit dem einfachen Teil an: Die Beleuchtung an RC-Modellen besteht meistens aus LEDs. Diese haben den Vorteil, dass sie sich ganz hervorragend mit einem PWM-Signal dimmen lassen. Zunächst entscheidet man sich für eine Frequenz, in der die PWM arbeitet. Das wird im Modellbau häufig 50 Hz sein, weil viele Komponenten damit arbeiten. Außerdem ist die Frequenz hoch genug, dass unsere Augen nicht mehr wahrnehmen können, dass es flackert. (Achtung: Kameras können das Flackern oft noch sehen.) 

Nach der Frequenz kommt der zweite wichtige Wert dazu: der Duty Cycle. Dieser bestimmt, wie lange das Signal in einem Durchlauf der Frequenz auf HIGH gestellt ist. Ein Duty Cycle von 0 lässt also das Licht ausgeschaltet. Ein Duty Cycle von 1 entspricht voller Helligkeit, und entsprechend alles dazwischen bedeutet gedimmt. Hierbei muss man daran denken, dass die wahrgenommene Helligkeit nicht linear ist. Das ist aber ein ganz eigenes Thema.

Die Steuerung

Um es ein wenig einfacher zu machen, kann man davon ausgehen, dass die Steuerung von Electronic Speed Controllern und Servos grundsätzlich gleich funktioniert. Einige ESCs haben noch Features, die eine Nullstellung benötigen, bevor sie von vorwärts nach rückwärts umstellen. was lästig sein kann, aber hier auch erst mal nicht im Scope ist.

Servos werden über die Pulsweiten (typischerweise in Schritten von ~1–2 ms bei 50 Hz) auf ihre Position gesetzt.

Umsetzung direkt auf dem Raspberry Pi (gpio-Befehle)

Der Raspberry Pi verfügt von Haus aus über vier PWM-Ausgangspins. Diese werden allerdings nur von zwei PWM-Generatoren versorgt. Das hat auf der einen Seite den Charme, dass meist einer der Pins frei ist, aber auch den Nachteil, dass man maximal zwei Aktuatoren direkt vom Pi aus steuern kann. Für Gas und Lenkung reicht es aber erst einmal. Damit das funktioniert, muss der Pin im ersten Schritt initialisiert werden.

 

gpio -g mode <pin> pwm
 gpio pwm-ms
 gpio pwmc 192 
 gpio pwmr 2000

 

Die Pins, an denen das funktioniert, sind: 12, 13, 18, 19. Sie können aber je nach verwendetem Pi-Modell auch abweichen. Im Zweifelsfall ist die Pinout-Belegung des Modells zu prüfen. Das hilft ohnehin ungemein, damit man sieht, wo die Kabel angeschlossen werden müssen.

Der pwm-ms-Befehl stellt den PWM-Generator auf den mark-space-Modus. Hier wird nur einmal pro Cycle von HIGH nach LOW geschaltet. Dadurch steuert man direkt die Breiten des Pulses.

Der pwmc-Befehl stellt die Abtastung ein. Wenn der Zeitgeber eine Frequenz von 19.200.000 Hz hat, was beim Pi zutrifft, lassen wir nur zu, dass sich unser Ausgabewert alle 192 Impulse ändern lässt.

Der pwmr-Befehl gibt nun noch an, wie viele Schritte einen Cycle ausmachen sollen. Das brauchen wir auch gleich zum Setzen, da wird es klarer.

Jetzt ergibt sich: 19,2 MHz geteilt durch 192 Ticks pro Step = 100 kHz geteilt durch 2000 Steps pro Cycle = 50 Hz. Genau das, worauf wir hinauswollen.

Um unsere LEDs voll zu aktivieren, geben wir gpio -g pwm <pin> 2000 in die bash. Da tauchen die 2000 wieder auf. Hiermit sagen wir: „Schalte 2000 von 2000 Cycles auf HIGH.“ Entsprechend schaltet gpio -g pwm <pin> 0 null von 2000 Cycles auf HIGH, also ganz aus.

LEDs? Check!

Was ist mit den Servos? 

Servo-Cycle berechnen

Die Servos sind schon ein wenig anspruchsvoller. Sie erfordern eine Pulsweite irgendwo zwischen 0,5 ms und 2,5 ms bei 50 Hz.

Wie praktisch, dass unser Cycle auf 2000 gesetzt ist und 20 ms entspricht. Demnach können wir annehmen, dass jeder Step ~0,01ms entspricht.

Wir bekommen von unserem Sender immer einen Wert zwischen -1 und 1 geschickt. Diesen müssen wir jetzt auf den Servo-Wert umrechnen. 

Nun ist der Schritt nicht mehr weit, das Ganze in C# zu erledigen:

 

// wert nach 0-1 skalieren
 var pos = (input + 1) / 2;
 // ausreißer abschneiden
 pos = Math.Clamp(0, 1);
 // in PWM umwandeln
 var pwmValue = 50 + position * 200;
 // mit einer Helper funktion in der Bash ausführen
 await Bash.ExecuteAsync($"gpio -g pwm {PinNumber} {pwmValue}");

 

Zum Testen in Bash reicht auch: gpio -g pwm <pin> 125.

Wenn der Bash-Befehl ausgeführt wurde, sollte der Servo sofort zucken. Schon ist dein Fahrzeug fahrbereit.

Wenn die PWMs nicht reichen

Die eingebauten PWMs im Pi sind schön und gut. Für mein Modell allerdings noch nicht mal die halbe Zahl. Was also tun, wenn die Ports nicht reichen? Ein Extender muss her!

Für kleines Geld bekommt ihr beim Elektronikhändler eures Vertrauens ein PCA9685-Modul. Das ist eine kleine Platine, die der Pi via I²C ansteuern kann. Praktischerweise sind die Ausgänge auch wie Servokabel angeordnet, man kann also direkt Servos daran anschließen. (Bei LEDs vorsichtig sein, denn gerade superhelle oder parallel geschaltete LEDs können das Modul überlasten, und es verfügt über keine Schutzmaßnahmen. Auch Stecker zu verpolen kann das Modul zerstören.) Dieses Modul verfügt über 16 PWM-Generatoren, und das Beste ist: Ihr könnt bis zu 62 davon an eurem Pi anklemmen. Also keine Ausreden mehr hören, dass ihr zu wenig Channels habt. Nebenbei: SignalR überträgt bis zu short.MaxValue pro Fahrzeug. Ich will also nichts hören …

Einrichten

Das Gute vorweg: Der I²C Bus lässt sich recht komfortabel über dotnet lesen und schreiben, nämlich mit dem Namespace System.Device.I2c.

Zunächst muss sichergestellt sein, dass der I²C Bus in der raspi-config aktiviert wurde. Der Prozess ist recht intuitiv.

 

sudo raspi-config

 

Dann heißt es nach Interfaces und I²C zu suchen und diese zu aktivieren.

Das PCA9685-Modul

Vorweg einige Informationen zu dem Modul und zu I²C. Der Bus liest und setzt in dem Modul lediglich einzelne Speicherregister. Diese werden sofort nach dem Setzen von den PWM-Generatoren umgesetzt. Jetzt kommt der Clou: Die Berechnung ist natürlich komplett anders und der Zugriff auf die PWMs eben auch. 

Die Register befinden sich in folgendem Schema: 0x06 + 4 * channel also in Worten: Die PWM-Register beginnen an Byte-Position 6 und sind jeweils 4 Bytes lang.

Dabei gibt es die in Tabelle 1 gezeigten Registerfunktionen für jeden PWM-Ausgang – es sind im Grundsatz zwei short-Werte (12 Bit).

 

RegisterFunktion
0x00Start Tick unseres Impulses (unwichtig)
0x01Start Tick unseres Impulses (unwichtig)
0x02Anteil des HIGH Wertes
0x03Anteil des HIGH Wertes

 

Die Register am Anfang zeigt Tabelle 2.

 

RegisterFunktion
0x00Mode - Sleep, Restart
0x01Treiberverhalten
0xFEPrescale - Fequenzeinsteller

 

Erst mal initialisieren

Wir wollen sichergehen, dass das Modul auf 50 Hz gestellt ist, damit die Servos überhaupt etwas mit dem Signal anfangen können. Dazu etwas Hilfe zur Vereinfachung:

 

private void WriteRegisterUnchecked(byte register, byte value)
 {
     Span<byte> buffer = stackalloc byte[2];
     buffer[0] = register;
     buffer[1] = value;
 
     lock (_deviceLock)
     {
         Device.Write(buffer);
     }
 }
 
 internal void WriteChannelRegisters(int regBase, byte onLow, byte onHigh, byte offLow, byte offHigh)
 {
     EnsureInitialized();
 
     Span<byte> buffer = stackalloc byte[5];
     buffer[0] = (byte)regBase;
     buffer[1] = onLow;
     buffer[2] = onHigh;
     buffer[3] = offLow;
     buffer[4] = offHigh;
 
     lock (_deviceLock)
     {
         Device.Write(buffer);
     }
} 

 

Jetzt bringen wir das Modul in den Sleep-Modus, damit wir die Frequenz einstellen können. Danach rütteln wir es wieder wach, was dafür sorgt, dass der PWM-Tick losgeht: 

 

WriteRegisterUnchecked(0x00, 0x10); // Sleep
WriteRegisterUnchecked(0xFE, 0x79); // Prescale for ~50Hz
WriteRegisterUnchecked(0x00, 0x20); // Wake up, auto increment

 

Dann Kontrolle!

Schon können wir auch alle PWMs ansteuern:

 

private void SendPwmUpdate(int pwmValue)
{
     pwmValue = Math.Clamp(pwmValue, 0, PWM_RESOLUTION - 1);
     // wir starten immmer an Position 0
     byte onLow = 0x00;
     byte onHigh = 0x00;
     // bleiben so lange HIGH (MaxVal: 4096)
     byte offLow = (byte)(pwmValue & 0xFF);
     byte offHigh = (byte)((pwmValue >> 8) & 0x0F); // Unused Bits löschen
 
     // weil das Nachverfolgen einem echt Kopfschmerzen machen kann noch mal debug loggen
     _extension.Logger?.LogDebug(
         "Setting channel {Channel} to PWM {PWM} (Input={Input}) on Register 0x{Reg:X2}",
         _channel,
         pwmValue,
         pwmValue,
         _regBase
     );
     _extension.WriteChannelRegisters(_regBase, onLow, onHigh, offLow, offHigh);
}

 

_extension ist an der Stelle die Instanz des jeweiligen Boards, und _regBase ist das im Konstructor berechnete Anfangsregister.

Sollte der I²C-Bus deaktiviert sein oder der User keine Berechtigungen haben, in ihn hineinzuschreiben, werden hier natürlich entsprechende Exceptions geworfen. Wenn hingegen die Hardware defekt ist, erfolgt dies nicht.

Wenn es mal nicht geht

Da ich selbst schon an der Frage verzweifelt bin, warum der Code nicht funktioniert, hier noch ein paar Tipps, mit denen ihr herausfindet, ob es nicht doch an der Hardware liegt.

Erst mal Tools holen:

 

sudo apt update
sudo apt install -y i2c-tools

 

Dann checken, ob das Board im Bus angemeldet ist.

 

sudo i2cdetect -y 1

 

Hier sollte ein Raster erscheinen, in dem das Board auftaucht (oder mehrere, falls ihr habt). Im Normalfall, wenn ihr nichts an den Adressenpins gelötet, habt taucht es unter der Adresse 0x40 auf. Wenn ja, schon mal gut. Wenn nicht, ist entweder das Board defekt oder eure Verkabelung nicht korrekt.

Als Nächstes versuchen wir, das Board in den Sleep-Modus zu schicken, und versuchen den Wert zurückzulesen. So könnt ihr sicherstellen, dass das Modul Register schreiben lässt.

 

sudo i2cset -y 1 0x40 0x00 0x10 b
sudo i2cget -y 1 0x40 0x00 b

 

Danach der Gegentest mit dem Wake-Modus:

 

sudo i2cset -y 1 0x40 0x00 0x20 b
sudo i2cget -y 1 0x40 0x00 b

 

Wenn einer der Tests fehlschlägt, würde ich im ersten Schritt das PCA-Modul austauschen. Wenn es dann immer noch nicht funktioniert, lohnt sich ein Versuch mit einem anderen Raspberry Pi. Der I²C-Bus am Pi ist auch manchmal nicht in Ordnung.

Ende

An dieser Stelle viel Erfolg mit der Kontrolle über alles, was PWM braucht. Bis ganz bald!

 

[1] Georg Poweleit, SignalRC – in Echtzeit ans Steuer, dotnetpro 10-11/2025, Seite 46 ff.

 

Neueste Beiträge

Window Functions - Acht Kostbarkeiten in T-SQL, Teil 5
Durchblick mit Weitblick: Fensterfunktionen sind nicht nur ein Feature – sie können ein Paradigmenwechsel sein.
6 Minuten
SignalRC braucht LTE - Der DDC-Truck, Teil 2
Das LTE-Netz als Transportkanal digitaler Steuerungsdaten bei RC-Modellen.
6 Minuten
28. Jan 2026
BRIN-Indizes in PostgreSQL - Indizes & Co. in PostgreSQL, Teil 4
PostgreSQL mit BRIN vertritt die Idee, dass ein Index unvollkommen sein kann, solange er kostengünstig und in großem Maßstab effektiv ist. So entsteht eine pragmatische Optimierung, die Präzision gegen Einfachheit eintauscht – und dabei gewinnt.
6 Minuten

Das könnte Dich auch interessieren

Elektronische Schaltkreise im Browser simulieren - Simulation
Statt mit Steckfeld oder Lötkolben kann man auf dieser Website Schaltungen per Drag and Drop zusammenstellen und deren Verhalten testen.
2 Minuten
26. Jul 2018
SignalRC – in Echtzeit ans Steuer - Der DDC-Truck: Auf in die Welt mit SignalR, Raspberry Pi und sechs Rädern
Ein vernetztes Fahrzeug, gesteuert per Weboberfläche und LTE. SignalR sorgt dabei für millisekundenschnelle Kommunikation – direkt, stabil und skalierbar.
16 Minuten
21. Jan 2026
UIs für Linux - Bedienoberflächen entwickeln mithilfe von C#, .NET und Avalonia
Es gibt viele UI-Frameworks für .NET, doch nur sehr wenige davon unterstützen Linux. Avalonia schafft als etabliertes Open-Source-Projekt Abhilfe.
16 Minuten
16. Jun 2025
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige