Problemlösungen

Arduino Trimmrad zur Einstellung der Flugzeug-Trimmung (Teil 3)

Grundlagen


Arduino Mikrocontroller mit Ein- und Ausgabekomponenten
In diesem Artikel geht es um den Nachbau eines Trimmrades für Flugsimulatoren auf Basis eines Arduino-Controllers. Die Grundlagen können in vorhergehenden Artikeln nachgelesen werden.

Arduino Trimmrad zur Einstellung der Flugzeug-Trimmung (Teil 1)

Arduino Trimmrad zur Einstellung der Flugzeug-Trimmung (Teil 2)

Quelltext überarbeitet und mehr Funktionalität


Ein Flugzeug kann um drei Achsen gedreht werden: Querachse (Nicken), Längsachs (Rollen) und Hochachse (Gieren). Verschiedene Einflüsse können nun bewirken, dass das Flugzeug ungewollt um eine der Achsen rotiert wird. Die Trimmkräfte sollen dabei die Kräfte ausgleichen, die die Lage des Flugzeuges verändern würden.
Die Trimmung der Querachse bewirkt, dass das Flugzeug nicht unerwünscht nach oben oder unten fliegt, wenn die Beladung beispielsweise ungleichmäßig ist. Ein einseitiger Triebwerksausfalls bei mehrmotorigen Maschinen wirkt ein Drehmoment auf die Maschine, das um so größer ist, je weiter außen das gegenüberliegende noch funktionierende Triebwerk liegt. Um dieses Drehmoment auszugleichen, muss der Pilot gegensteuern. Alternativ kann der Pilot über die Trimmung ein entgegenwirkendes Drehmoment erzeugen und das Flugzeug so in den Geradeausflug bringen ohne ständig eingreifen zu müssen.

Bei einem Hobby-Flugsimulator ist in erster Linie die Trimmung um die Querachse von Bedeutung. In diesem Projekt soll zusätzlich noch eine Trimmung um die Hochachse möglich sein.
Bislang habe ich nur die Trimmung um eine Achse berücksichtigt. Es muss am Arduino nun also ein zusätzlicher Drehregler angeschlossen werden, um die Trimmung um eine weitere Achse zu ermöglichen. Da ich auch die Einstellung mit einer farbigen Leuchtdiode kenntlich machen möchte, habe ich auch ein zweites LED-Modul verbaut. Bei der Trimmung um die Längsachse nach unten (NOSE DOWN) soll die zugehörige LED rot leuchten. Bei der Drehung der Nase nach oben (NOSE UP) soll die LED grün leuchten. Bei der Drehung nach Links (NOSE LEFT) mit dem zweiten Drehregler, soll die entsprechende LED rot leuchten (Backbord). Bei der Drehung nach rechts (NOSE RIGHT) soll die LED grün leuchten (Steuerbord).

X und Y statt Z


Das HID-Project, welches als Basis für die Kommunikation des Arduino über USB mit dem PC dient, definiert verschiedene Buttons, Achsen und Hat-Switches. Bislang hatte ich die Z-Achse verwendet, welche mit einem Wertebereich von -128 bis 127 ausreichend war. Diese Z-Achse wird beispielsweise für Schubregler verwendet, während die X- und Y-Achse für die Joystick-Kontrolle genutzt wird. Die Achsen X und Y ermöglichen eine deutlich höhere Präzision mit Werten von -32768 bis -32767. Dieser Wertebereich ist viel größer als für den aktuellen Anwendungszweck notwendig. Man müsste den Drehregler also um über 30.000 Stufen drehen, um den gesamten Wertebereich auszuschöpfen, wenn der Wert mit jedem Einrasten des Drehreglers um eine Stufe erhöht wird. Dies ist natürlich nicht praktikabel, weshalb der Wert mit jeder Stufe einfach um einen größeren Betrag, beispielsweise 500, verändert wird.
Genau genommen, gibt es sogar zwei Z-Achsen mit einem kleinen Wertebereich, die Achse “Rz” und die Achse “Z”. Gegebenenfalls werde ich diese zukünftig verwenden.

Die verfügbaren Optionen können in der Datei Gamepad.cpp der HID-Controller-API nachgelesen werden. Nachfolgend ein Auszug aus der Datei.

/* 4 16bit Axis */
0x05, 0x01,			/* USAGE_PAGE (Generic Desktop) */
0xa1, 0x00,			/* COLLECTION (Physical) */
0x09, 0x30,			/* USAGE (X) */
0x09, 0x31,			/* USAGE (Y) */
0x09, 0x33,			/* USAGE (Rx) */
0x09, 0x34,			/* USAGE (Ry) */
0x16, 0x00, 0x80,	/* LOGICAL_MINIMUM (-32768) */
0x26, 0xFF, 0x7F,	/* LOGICAL_MAXIMUM (32767) */
0x75, 0x10,			/* REPORT_SIZE (16) */
0x95, 0x04,			/* REPORT_COUNT (4) */
0x81, 0x02,			/* INPUT (Data,Var,Abs) */
/* 2 8bit Axis */
0x09, 0x32,			/* USAGE (Z) */
0x09, 0x35,			/* USAGE (Rz) */
0x15, 0x80,			/* LOGICAL_MINIMUM (-128) */
0x25, 0x7F,			/* LOGICAL_MAXIMUM (127) */
0x75, 0x08,			/* REPORT_SIZE (8) */
0x95, 0x02,			/* REPORT_COUNT (2) */
0x81, 0x02,			/* INPUT (Data,Var,Abs) */
0xc0,


Der verwendete Drehwinkelgeber KY-040 hat zusätzlich einen Button integriert, der beim Drücken des Drehreglersausgelöst wird. In diesem Projekt nutze ich die Funktion, um die eingestellten Werte auf 0 zurückzusetzen.

Der Input ist folgendermaßen definiert:

pinMode(rotary_1_switch, INPUT_PULLUP);

INPUT_PULLUP bedeutet, dass der Pegel am Input-Pin standardmäßig auf HIGH gesetzt ist. Wennd er Button gedrückt wird, dann wird der Pegel auf LOW gesetzt. Dies ist zunächst nicht ganz intuitiv, da eher zu erwarten werden, wenn der Pegel standardmäßig LOW ist und nur bei Druck auf den Button auf HIGH gesetzt wird. Mit dieser Einstellung ist es so, als würde eine Klingel ständig leuten und nur kurz abgeschaltet sein, während ein Besucher auf den Klingelknopf drückt. Warum habe ich also nicht die umgekehrte Einstellung gewählt? Ganz einfach, der Arduino hat die Funktionalität für INPUT-PULLUP hardwareseitig integriert. Eine entgegengesetzte Funktionalität INPUT_PLLDOWN gibt es hier nicht. Um eine solche Funktion nutzen zu können, müsste sie konstruiert werden, was die Komplexität unnötig erhöhen würde. Diese Besonderheit ist bei der Abfrage und Auswertung des Wertes zu berüksichtigen. Wenn eine Aktion bei gedrückter Taste ausgelöst werden soll, dann muss also entsprechend geprüft werden, ob kein Strom fließt. Um beim Beispiel der Klingel zu bleiben: Wenn die Klingel keinen Krach macht, steht Besuch vor der Tür und bittet um Einlass.

PULLUP- und PULLDOWN-Schaltungen sind notwendig, weil man die Spannungsversorgung, die Masse und die Elektronik nicht einfach ohne Widerstände verbinden sollte. Es kann sonst zu unerwünschten Kurzschlüssen kommen, welche zur Zerstörung der Schaltung führen könnte.

Mehr Informationen hierzu sind in den Weiten des Internets zu finden. beispielsweise hier:

https://circuitdigest.com/tutorial/pull-up-and-pull-down-resistor
https://www.electronics-tutorials.ws/de/logische/pullup-widerstaende.html
https://learn.sparkfun.com/tutorials/pull-up-resistors/all



Nun zum aktuellen Stand des Projektes. Nach der Aufnahme des nachfolgenden Fotos habe ich die Pin-Belegung noch einmal leicht verändert, da der zuvor genutzte Pin 13 eine unerwünschte Besonderheit aufweist. Er ist mit einer auf dem Bord vorhandenen LED verbunden, die dann bei der Nutzung dieses Pins ständig geleuchtet hat. Diese LED oll aber nur kurz Aufleuchten, um anzuzeigen, ob ein Knopf gedrückt wurde (was eigentlich auch unnötig ist).
Ich habe die Belegung somit noch einmal angepasst, so dass Pin 13 nicht mehr genutzt wird.

Arduino Mikrocontroller mit Ein- und Ausgabekomponenten

/*
 * Arduino HID Trim Wheel
 *
 * Author	Jan Wischniowski
 * URL		https://www.metanox.de/
 * Version	0.7.0
 * License	GNU GENERAL PUBLIC LICENSE Version 3
*/
 
// Beim Drehen des Rotary Encoders werden zwischen jedem Einrasten zwei Zustände durchlaufen.
//
// Stufe 1: Drehung im Uhrzeigersinn (rechts)
// rotary_1_clock: 0 -> 1
// rotary_1_data:  1 -> 0
//
// Stufe 2: Drehung gegen Uhrzeigersinn (links)
// rotary_1_clock: 0 -> 1
// rotary_1_data:  0 -> 1
 
#include "HID-Project.h"
 
#define CLOCKWISE true
#define COUNTERCLOCKWISE false
 
// Arduino
const int arduino_led_1		= LED_BUILTIN;
 
// LED 1
const int led_1_blue		= 0;
const int led_1_green		= 1;
const int led_1_red			= 2;
 
// LED 2
const int led_2_blue		= 3;
const int led_2_green		= 4;
const int led_2_red			= 5;
 
// Rotary 1
const int rotary_1_switch	= 6; // Pin 6 to SW (switch pin) on rotary encoder.
const int rotary_1_data		= 7; // Pin 7 to DT (data) on rotary encoder.
const int rotary_1_clock	= 8; // Pin 8 to CLK (clock) on rotary encoder.
int rotary_1_position		= 0;
int rotary_1_x_axis			= 0;
int rotary_1_clock_old;
int rotary_1_clock_new;
boolean rotary_1_direction;
boolean rotary_1_switch_old	= true;
boolean rotary_1_switch_new	= true;
 
// Rotary 2
const int rotary_2_switch	= 9; // Pin 9 to SW (switch pin) on rotary encoder.
const int rotary_2_data		= 10; // Pin 10 to DT (data) on rotary encoder.
const int rotary_2_clock	= 11; // Pin 11 to CLK (clock) on rotary encoder.
int rotary_2_position		= 0;
int rotary_2_y_axis			= 0;
int rotary_2_clock_old;
int rotary_2_clock_new;
boolean rotary_2_direction;
boolean rotary_2_switch_old	= true;
boolean rotary_2_switch_new	= true;
 
void setup()
{
	pinMode(arduino_led_1, OUTPUT);
 
	pinMode(led_1_red, OUTPUT);
	pinMode(led_1_green, OUTPUT);
	pinMode(led_1_blue, OUTPUT);
 
	pinMode(led_2_red, OUTPUT);
	pinMode(led_2_green, OUTPUT);
	pinMode(led_2_blue, OUTPUT);
 
	pinMode(rotary_1_switch, INPUT_PULLUP);
	pinMode(rotary_1_clock, INPUT);
	pinMode(rotary_1_data, INPUT);
 
	pinMode(rotary_2_switch, INPUT_PULLUP);
	pinMode(rotary_2_clock, INPUT);
	pinMode(rotary_2_data, INPUT);
 
	rotary_1_clock_old	= digitalRead(rotary_1_clock);
	rotary_2_clock_old	= digitalRead(rotary_2_clock);
 
	// Sends a clean report to the host. This is important on any Arduino type.
	Gamepad.begin();
 
	// Serial output is used for debugging.
	Serial.begin(9600);
}
 
void loop()
{
	rotary_1();
	rotary_2();
}
 
void rotary_1()
{
	rotary_1_switch_new = digitalRead(rotary_1_switch);
 
	if (rotary_1_switch_new != rotary_1_switch_old)
	{
		if (!rotary_1_switch_new)
		{
			digitalWrite(arduino_led_1, HIGH);
 
			// Button 1 gedrückt. X-Achse auf 0 zurücksetzen.
			Gamepad.press(1);
			rotary_1_x_axis = 0;
			Gamepad.write();
 
			Serial.println ("Rotary 1 Button: Pressed");
 
			digitalWrite(led_1_red, LOW);
			digitalWrite(led_1_green, LOW);
			digitalWrite(led_1_blue, HIGH);
		}
		else
		{
			digitalWrite(arduino_led_1, LOW);
 
			Gamepad.release(1);
 
			Gamepad.write();
 
			Serial.println ("Rotary 1 Button: Released");
 
			digitalWrite(led_1_red, LOW);
			digitalWrite(led_1_green, LOW);
			digitalWrite(led_1_blue, LOW);
		}
	}
 
	rotary_1_switch_old = rotary_1_switch_new;
 
	rotary_1_clock_new = digitalRead(rotary_1_clock);
 
	// Bei jeder Drehung in eine beliebige Richtung soll bei Stufe 2 eine Aktion ausgeführt werden. Stufe 1 wird übersprungen. Andernfalls würden bei jeder Drehung zwei Aktionen ausgeführt werden.
	if ((rotary_1_clock_new != rotary_1_clock_old) && (true == rotary_1_clock_new))
	//if ((rotary_1_clock_new != rotary_1_clock_old))
	{
		//Serial.print("VAR rotary_1_clock: ");
		//Serial.println(digitalRead(rotary_1_clock));
 
		//Serial.print("VAR rotary_1_data: ");
		//Serial.println(digitalRead(rotary_1_data));
 
		if (digitalRead(rotary_1_data) != rotary_1_clock_new)
		{ // Clockwise
			rotary_1_position++;
			rotary_1_direction = CLOCKWISE;
		}
		else
		{ //Counterclockwise
			rotary_1_position--;
			rotary_1_direction = COUNTERCLOCKWISE;
		}
 
		if (CLOCKWISE == rotary_1_direction)
		{
			Serial.println ("Rotary 1 Rotation: Clockwise");
 
			rotary_1_x_axis -= 500;
 
			if (-32767>rotary_1_x_axis)
			{
				rotary_1_x_axis = -32767;
			}
		}
		else
		{
			Serial.println("Rotary 1 Rotation: Counterclockwise");
 
			rotary_1_x_axis += 500;
 
			if (32768<rotary_1_x_axis)
			{
				rotary_1_x_axis = 32768;
			}
		}
 
		if (0>rotary_1_x_axis)
		{
			digitalWrite(led_1_red, HIGH);
			digitalWrite(led_1_green, LOW);
		}
		else if (0<rotary_1_x_axis)
		{
			digitalWrite(led_1_red, LOW);
			digitalWrite(led_1_green, HIGH);
		}
		else
		{
			digitalWrite(led_1_red, LOW);
			digitalWrite(led_1_green, LOW);
		}
 
		Serial.print("VAR rotary_1_position: ");
		Serial.println(rotary_1_position);
 
		Serial.print("VAR rotary_1_x_axis: ");
		Serial.println(rotary_1_x_axis);
	}
 
	rotary_1_clock_old = rotary_1_clock_new;
 
	Gamepad.rxAxis(rotary_1_x_axis);
	Gamepad.write();
}
 
void rotary_2()
{
	rotary_2_switch_new = digitalRead(rotary_2_switch);
 
	if (rotary_2_switch_new != rotary_2_switch_old)
	{
		if (!rotary_2_switch_new)
		{
			digitalWrite(arduino_led_1, HIGH);
 
			// Button 2 gedrückt. Y-Achse auf 0 zurücksetzen.
			Gamepad.press(2);
			rotary_2_y_axis = 0;
			Gamepad.write();
 
			Serial.println ("Rotary 2 Button: Pressed");
 
			digitalWrite(led_2_red, LOW);
			digitalWrite(led_2_green, LOW);
			digitalWrite(led_2_blue, HIGH);
		}
		else
		{
			digitalWrite(arduino_led_1, LOW);
 
			Gamepad.release(2);
 
			Gamepad.write();
 
			Serial.println ("Rotary 2 Button: Released");
 
			digitalWrite(led_2_red, LOW);
			digitalWrite(led_2_green, LOW);
			digitalWrite(led_2_blue, LOW);
		}
	}
 
	rotary_2_switch_old = rotary_2_switch_new;
 
	rotary_2_clock_new = digitalRead(rotary_2_clock);
 
	// Bei jeder Drehung in eine beliebige Richtung soll bei Stufe 2 eine Aktion ausgeführt werden. Stufe 1 wird übersprungen. Andernfalls würden bei jeder Drehung zwei Aktionen ausgeführt werden.
	if ((rotary_2_clock_new != rotary_2_clock_old) && (true == rotary_2_clock_new))
	//if ((rotary_2_clock_new != rotary_2_clock_old))
	{
		//Serial.print("VAR rotary_2_clock: ");
		//Serial.println(digitalRead(rotary_2_clock));
 
		//Serial.print("VAR rotary_2_data: ");
		//Serial.println(digitalRead(rotary_2_data));
 
		if (digitalRead(rotary_2_data) != rotary_2_clock_new)
		{ // Clockwise
			rotary_2_position++;
			rotary_2_direction = CLOCKWISE;
		}
		else
		{ //Counterclockwise
			rotary_2_position--;
			rotary_2_direction = COUNTERCLOCKWISE;
		}
 
		if (CLOCKWISE == rotary_2_direction)
		{
			Serial.println("Rotary 2 Rotation: Counterclockwise");
 
			rotary_2_y_axis += 500;
 
			if (32768<rotary_2_y_axis)
			{
				rotary_2_y_axis = 32768;
			}
		}
		else 
		{
			Serial.println ("Rotary 2 Rotation: Clockwise");
 
			rotary_2_y_axis -= 500;
 
			if (-32767>rotary_2_y_axis)
			{
				rotary_2_y_axis = -32767;
			}
		}
 
		if (0>rotary_2_y_axis)
		{
			digitalWrite(led_2_red, HIGH);
			digitalWrite(led_2_green, LOW);
		}
		else if (0<rotary_2_y_axis)
		{
			digitalWrite(led_2_red, LOW);
			digitalWrite(led_2_green, HIGH);
		}
		else
		{
			digitalWrite(led_2_red, LOW);
			digitalWrite(led_2_green, LOW);
		}
 
		Serial.print("VAR rotary_2_position: ");
		Serial.println(rotary_2_position);
 
		Serial.print("VAR rotary_2_y_axis: ");
		Serial.println(rotary_2_y_axis);
	}
 
	rotary_2_clock_old = rotary_2_clock_new;
 
	Gamepad.ryAxis(rotary_2_y_axis);
	Gamepad.write();
}

Einstellungen im Flugsimulator


Einstellungen im Flugsimulator

Einstellungen im Flugsimulator

Einstellungen im Flugsimulator

Ausblick


Nachdem der Trimm-Controller nun funktioniert, benötigt er natürlich noch ein Gehäuse.