1 |
// =======================================================
|
2 |
// ESP32-S3 Segway-Balancer v4 — Telemetrie-kalibriert
|
3 |
// =======================================================
|
4 |
// Korrekturen gegenüber v3 (basierend auf Feldtest-Telemetrie):
|
5 |
//
|
6 |
// FIX 1: setpoint = -2.5°
|
7 |
// Telemetrie: pitch konstant -0.7° bis -3.4°, Mittel ~-2.5°.
|
8 |
// Mechanischer Balancepunkt ≠ 0°. Ohne Korrektur läuft I-Term
|
9 |
// immer voll auf → Wegrollen garantiert.
|
10 |
//
|
11 |
// FIX 2: Kp 18→10, Kd 0.70→2.5
|
12 |
// Kp=18 → bei 2.5° Fehler cmd=45 → starkes Übersteuern mit Getriebespiel.
|
13 |
// Kd=0.70 dämpfte rate-Sprünge von ±22°/s nicht ausreichend.
|
14 |
//
|
15 |
// FIX 3: Ki 15→3, iLimit 120→35
|
16 |
// Ki=15: I-Term lief in 4s von -11 auf +25 unkontrolliert.
|
17 |
// Ki=3 + iLimit=35 gibt Zeit zum Einpendeln ohne zu dominieren.
|
18 |
//
|
19 |
// FIX 4: CMD_DEADBAND 8→5
|
20 |
// Zu groß: kleine Korrekturen wurden verschluckt → ruckartige Reaktion.
|
21 |
//
|
22 |
// FIX 5: PULSE_MIN_US 250→400
|
23 |
// Kleinere Pulse überbrücken Getriebespiel nicht zuverlässig.
|
24 |
//
|
25 |
// WICHTIG beim ersten Start:
|
26 |
// - Roboter beim Booten RUHIG & AUFRECHT halten (Kalibrierung)
|
27 |
// - Fährt er in die falsche Richtung → INVERT_MOTORS auf 1
|
28 |
// - Reagiert er "verkehrt herum" → INVERT_ANGLE auf 1
|
29 |
// - setpoint live mit "s-2.0", "s-3.0" etc. feinabstimmen
|
30 |
// =======================================================
|
31 |
|
32 |
#include <Arduino.h>
|
33 |
#include <Wire.h>
|
34 |
#include <Adafruit_NeoPixel.h>
|
35 |
|
36 |
// -------------------------------------------------------
|
37 |
// Pin-Belegung
|
38 |
// -------------------------------------------------------
|
39 |
#define SDA_PIN 8
|
40 |
#define SCL_PIN 9
|
41 |
#define NEOPIXEL_PIN 21
|
42 |
#define NEOPIXEL_COUNT 1
|
43 |
#define MPU_ADDR 0x68
|
44 |
|
45 |
#define L_IN1 4
|
46 |
#define L_IN2 5
|
47 |
#define R_IN1 6
|
48 |
#define R_IN2 7
|
49 |
|
50 |
// -------------------------------------------------------
|
51 |
// Vorzeichen-Korrekturen (bei Bedarf auf 1 setzen)
|
52 |
// -------------------------------------------------------
|
53 |
#define INVERT_ANGLE 0 // 1 = Neigungs-Vorzeichen drehen
|
54 |
#define INVERT_MOTORS 1 // Feldtest: Motoren fuhren in Sturzrichtung → gedreht
|
55 |
#define INVERT_LEFT 0 // 1 = nur linken Motor drehen
|
56 |
#define INVERT_RIGHT 0 // 1 = nur rechten Motor drehen
|
57 |
|
58 |
// -------------------------------------------------------
|
59 |
// Regel-Timing
|
60 |
// -------------------------------------------------------
|
61 |
#define CONTROL_HZ 600
|
62 |
const unsigned long CONTROL_PERIOD_US = 1000000UL / CONTROL_HZ;
|
63 |
|
64 |
// -------------------------------------------------------
|
65 |
// Puls-Parameter (Hardware-Timer-ISR)
|
66 |
// ISR-Takt = 100 µs (10 kHz)
|
67 |
// Eine Pulsperiode = PULSE_PERIOD_US (z.B. 3000 µs ≈ 333 Hz)
|
68 |
// Innerhalb der Periode wird für "pulseWidth" angetrieben,
|
69 |
// danach Coast (Freilauf) bis zur nächsten Periode.
|
70 |
// -------------------------------------------------------
|
71 |
#define ISR_TICK_US 100
|
72 |
#define PULSE_PERIOD_US 3000 // 333 Hz Pulsrate
|
73 |
#define PULSE_MIN_US 400 // Mindest-Kick (Getriebespiel überwinden, Feldtest: 250 zu klein)
|
74 |
#define PULSE_MAX_US PULSE_PERIOD_US // bei cmd=255 → Dauerstrich
|
75 |
|
76 |
const int PERIOD_TICKS = PULSE_PERIOD_US / ISR_TICK_US; // 30
|
77 |
const int MIN_TICKS = PULSE_MIN_US / ISR_TICK_US; // 2-3
|
78 |
const int MAX_TICKS = PULSE_MAX_US / ISR_TICK_US; // 30
|
79 |
|
80 |
// Hysterese-Totzone: verhindert schnelles Richtungswechseln bei Getriebespiel
|
81 |
// Motor bleibt aktiv bis cmd unter DEADBAND_OFF fällt (nicht erst bei 0)
|
82 |
// Motor startet erst wenn cmd über DEADBAND_ON steigt
|
83 |
#define CMD_DEADBAND_ON 6.0f // Einschaltschwelle
|
84 |
#define CMD_DEADBAND_OFF 3.0f // Ausschaltschwelle (Hysterese)
|
85 |
|
86 |
// -------------------------------------------------------
|
87 |
// Filter / Regler-Parameter (STARTWERTE — live tunen!)
|
88 |
// -------------------------------------------------------
|
89 |
float alpha = 0.98f; // Komplementärfilter: Gyro-Gewichtung (0.97–0.99)
|
90 |
|
91 |
// PID — Fehler & Winkel in GRAD, Stellgröße in 0..255
|
92 |
// Werte aus Feldtest-Telemetrie abgeleitet:
|
93 |
float Kp = 38.0f; // erhöht: mehr Masse braucht frühere/stärkere Reaktion
|
94 |
float Kd = 1.2f; // leicht erhöht: mehr Dämpfung gegen Trägheitspendeln
|
95 |
float Ki = 2.0f; // unverändert
|
96 |
|
97 |
// Telemetrie: pitch pendelte präzise um -1.8°
|
98 |
float setpoint = -1.8f; // Grad — live mit "s" in 0.2°-Schritten feinabstimmen
|
99 |
|
100 |
const float iLimit = 35.0f; // war 120 → viel zu groß, dominierte alles
|
101 |
const float tipLimit = 30.0f; // Grad: Notabschaltung
|
102 |
const float reArmLimit = 3.0f; // Grad: Re-Arm-Fenster
|
103 |
|
104 |
// -------------------------------------------------------
|
105 |
// Zustand
|
106 |
// -------------------------------------------------------
|
107 |
float pitch = 0.0f; // gefilterter Winkel (Grad)
|
108 |
float gyroRateDeg = 0.0f; // Winkelgeschwindigkeit (Grad/s)
|
109 |
float gyroBiasDeg = 0.0f; // Gyro-Bias (Grad/s)
|
110 |
float accPitchOff = 0.0f; // Accel-Nullpunkt (Grad)
|
111 |
float iTerm = 0.0f; // Integralspeicher
|
112 |
|
113 |
unsigned long lastControlUs = 0;
|
114 |
unsigned long lastTeleMs = 0;
|
115 |
bool telemetry = true;
|
116 |
|
117 |
// Arm-Logik
|
118 |
bool armed = false;
|
119 |
unsigned long stableSinceMs = 0;
|
120 |
|
121 |
// ---- ISR-geteilte Variablen (volatile!) ----
|
122 |
volatile int pulseTicksL = 0; // 0..PERIOD_TICKS (0 = Coast)
|
123 |
volatile int pulseTicksR = 0;
|
124 |
volatile int8_t dirL = 0; // -1, 0, +1
|
125 |
volatile int8_t dirR = 0;
|
126 |
volatile bool isrArmed = false;
|
127 |
|
128 |
Adafruit_NeoPixel pixel(NEOPIXEL_COUNT, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
|
129 |
hw_timer_t *pulseTimer = NULL;
|
130 |
|
131 |
// =======================================================
|
132 |
// Pulse-Timer-ISR (10 kHz) — nur Pins schalten, keine Mathematik!
|
133 |
// =======================================================
|
134 |
void ARDUINO_ISR_ATTR onPulseTimer() {
|
135 |
static int counter = 0;
|
136 |
counter++;
|
137 |
if (counter >= PERIOD_TICKS) counter = 0;
|
138 |
|
139 |
// --- Linker Motor ---
|
140 |
if (!isrArmed || dirL == 0 || pulseTicksL <= 0) {
|
141 |
digitalWrite(L_IN1, LOW); digitalWrite(L_IN2, LOW); // Coast
|
142 |
} else if (counter < pulseTicksL) {
|
143 |
if (dirL > 0) { digitalWrite(L_IN1, HIGH); digitalWrite(L_IN2, LOW); }
|
144 |
else { digitalWrite(L_IN1, LOW); digitalWrite(L_IN2, HIGH); }
|
145 |
} else {
|
146 |
digitalWrite(L_IN1, LOW); digitalWrite(L_IN2, LOW); // Coast (Pause)
|
147 |
}
|
148 |
|
149 |
// --- Rechter Motor ---
|
150 |
if (!isrArmed || dirR == 0 || pulseTicksR <= 0) {
|
151 |
digitalWrite(R_IN1, LOW); digitalWrite(R_IN2, LOW);
|
152 |
} else if (counter < pulseTicksR) {
|
153 |
if (dirR > 0) { digitalWrite(R_IN1, HIGH); digitalWrite(R_IN2, LOW); }
|
154 |
else { digitalWrite(R_IN1, LOW); digitalWrite(R_IN2, HIGH); }
|
155 |
} else {
|
156 |
digitalWrite(R_IN1, LOW); digitalWrite(R_IN2, LOW);
|
157 |
}
|
158 |
}
|
159 |
|
160 |
// =======================================================
|
161 |
// Stellgröße → Puls-Parameter (im Regelloop berechnet)
|
162 |
// cmd: -255 .. +255
|
163 |
// =======================================================
|
164 |
void setMotorCommand(float cmdLeft, float cmdRight) {
|
165 |
|
166 |
#if INVERT_MOTORS
|
167 |
cmdLeft = -cmdLeft; cmdRight = -cmdRight;
|
168 |
#endif
|
169 |
#if INVERT_LEFT
|
170 |
cmdLeft = -cmdLeft;
|
171 |
#endif
|
172 |
#if INVERT_RIGHT
|
173 |
cmdRight = -cmdRight;
|
174 |
#endif
|
175 |
|
176 |
auto compute = [](float cmd, volatile int8_t &dir, volatile int &ticks) {
|
177 |
static float lastCmd = 0;
|
178 |
float a = fabs(cmd);
|
179 |
float threshold = (fabs(lastCmd) > CMD_DEADBAND_OFF) ? CMD_DEADBAND_OFF : CMD_DEADBAND_ON;
|
180 |
if (a < threshold) {
|
181 |
dir = 0; ticks = 0; lastCmd = 0; return;
|
182 |
}
|
183 |
lastCmd = cmd;
|
184 |
dir = (cmd > 0) ? 1 : -1;
|
185 |
float ratio = (a - CMD_DEADBAND_ON) / (255.0f - CMD_DEADBAND_ON);
|
186 |
ratio = constrain(ratio, 0.0f, 1.0f);
|
187 |
ticks = MIN_TICKS + (int)(ratio * (MAX_TICKS - MIN_TICKS));
|
188 |
};
|
189 |
|
190 |
compute(cmdLeft, dirL, pulseTicksL);
|
191 |
compute(cmdRight, dirR, pulseTicksR);
|
192 |
}
|
193 |
|
194 |
// =======================================================
|
195 |
// I2C / MPU6050
|
196 |
// =======================================================
|
197 |
void i2cWrite(uint8_t reg, uint8_t val) {
|
198 |
Wire.beginTransmission(MPU_ADDR);
|
199 |
Wire.write(reg); Wire.write(val);
|
200 |
Wire.endTransmission();
|
201 |
}
|
202 |
|
203 |
void readMPU(float &ax, float &ay, float &az, float &gyDeg) {
|
204 |
Wire.beginTransmission(MPU_ADDR);
|
205 |
Wire.write(0x3B);
|
206 |
Wire.endTransmission(false);
|
207 |
Wire.requestFrom(MPU_ADDR, 14, true);
|
208 |
|
209 |
int16_t axr = (Wire.read() << 8) | Wire.read();
|
210 |
int16_t ayr = (Wire.read() << 8) | Wire.read();
|
211 |
int16_t azr = (Wire.read() << 8) | Wire.read();
|
212 |
Wire.read(); Wire.read(); // Temp
|
213 |
Wire.read(); Wire.read(); // gx (ungenutzt)
|
214 |
int16_t gyr = (Wire.read() << 8) | Wire.read();
|
215 |
Wire.read(); Wire.read(); // gz (ungenutzt)
|
216 |
|
217 |
ax = axr / 16384.0f;
|
218 |
ay = ayr / 16384.0f;
|
219 |
az = azr / 16384.0f;
|
220 |
gyDeg = gyr / 16.4f; // ±2000°/s → Grad/s
|
221 |
}
|
222 |
|
223 |
// =======================================================
|
224 |
// Setup
|
225 |
// =======================================================
|
226 |
void setup() {
|
227 |
Serial.begin(115200);
|
228 |
delay(300);
|
229 |
Serial.println("\n=== Balancer v4 (Telemetrie-kalibriert) ===");
|
230 |
|
231 |
pinMode(L_IN1, OUTPUT); pinMode(L_IN2, OUTPUT);
|
232 |
pinMode(R_IN1, OUTPUT); pinMode(R_IN2, OUTPUT);
|
233 |
digitalWrite(L_IN1, LOW); digitalWrite(L_IN2, LOW);
|
234 |
digitalWrite(R_IN1, LOW); digitalWrite(R_IN2, LOW);
|
235 |
|
236 |
Wire.begin(SDA_PIN, SCL_PIN, 400000);
|
237 |
i2cWrite(0x6B, 0x00); // Sleep aus
|
238 |
i2cWrite(0x1B, 0x18); // Gyro ±2000°/s
|
239 |
i2cWrite(0x1C, 0x00); // Acc ±2g
|
240 |
i2cWrite(0x1A, 0x02); // DLPF 94 Hz
|
241 |
|
242 |
pixel.begin();
|
243 |
pixel.setPixelColor(0, pixel.Color(80,80,0)); pixel.show(); // Gelb
|
244 |
|
245 |
// ---- Gyro-Bias (Roboter ruhig halten) ----
|
246 |
Serial.println("Kalibriere Gyro-Bias...");
|
247 |
const int N = 2000;
|
248 |
double s = 0;
|
249 |
for (int i=0;i<N;i++){ float ax,ay,az,gy; readMPU(ax,ay,az,gy); s+=gy; delayMicroseconds(700);}
|
250 |
gyroBiasDeg = (float)(s/N);
|
251 |
Serial.printf("Gyro-Bias: %.4f Grad/s\n", gyroBiasDeg);
|
252 |
|
253 |
// ---- Accel-Nullpunkt (Roboter AUFRECHT halten) ----
|
254 |
Serial.println("Kalibriere Nullpunkt (aufrecht halten!)...");
|
255 |
s = 0;
|
256 |
for (int i=0;i<400;i++){
|
257 |
float ax,ay,az,gy; readMPU(ax,ay,az,gy);
|
258 |
s += atan2f(-ax, sqrtf(ay*ay+az*az)) * 180.0f/PI;
|
259 |
delay(4);
|
260 |
}
|
261 |
accPitchOff = (float)(s/400.0);
|
262 |
Serial.printf("Nullpunkt: %.3f Grad\n", accPitchOff);
|
263 |
|
264 |
// Filter initialisieren (kein Startsprung)
|
265 |
{
|
266 |
float ax,ay,az,gy; readMPU(ax,ay,az,gy);
|
267 |
pitch = atan2f(-ax, sqrtf(ay*ay+az*az)) * 180.0f/PI - accPitchOff;
|
268 |
#if INVERT_ANGLE
|
269 |
pitch = -pitch;
|
270 |
#endif
|
271 |
}
|
272 |
|
273 |
// ---- Hardware-Timer für Pulsansteuerung ----
|
274 |
pulseTimer = timerBegin(1000000); // 1 MHz → 1 Tick = 1 µs
|
275 |
timerAttachInterrupt(pulseTimer, &onPulseTimer);
|
276 |
timerAlarm(pulseTimer, ISR_TICK_US, true, 0); // alle 100 µs
|
277 |
|
278 |
pixel.setPixelColor(0, pixel.Color(0,80,0)); pixel.show(); // Grün
|
279 |
Serial.println("Bereit. Befehle: p/i/d/a/s <wert>, m=arm-toggle, t=telemetrie, r=reset-I, ?=status");
|
280 |
lastControlUs = micros();
|
281 |
}
|
282 |
|
283 |
// =======================================================
|
284 |
// Serielle Befehle (Live-Tuning)
|
285 |
// =======================================================
|
286 |
void handleSerial() {
|
287 |
static char buf[24];
|
288 |
static uint8_t idx = 0;
|
289 |
while (Serial.available()) {
|
290 |
char c = Serial.read();
|
291 |
if (c=='\n' || c=='\r') {
|
292 |
if (idx==0) return;
|
293 |
buf[idx] = 0; idx = 0;
|
294 |
char cmd = buf[0];
|
295 |
float val = atof(buf+1);
|
296 |
switch (cmd) {
|
297 |
case 'p': Kp = val; Serial.printf(">> Kp=%.2f\n", Kp); break;
|
298 |
case 'i': Ki = val; iTerm=0; Serial.printf(">> Ki=%.2f (I reset)\n", Ki); break;
|
299 |
case 'd': Kd = val; Serial.printf(">> Kd=%.3f\n", Kd); break;
|
300 |
case 'a': alpha = constrain(val,0.90f,0.999f); Serial.printf(">> alpha=%.3f\n", alpha); break;
|
301 |
case 's': setpoint = val; Serial.printf(">> setpoint=%.2f Grad\n", setpoint); break;
|
302 |
case 'r': iTerm = 0; Serial.println(">> I-Term reset"); break;
|
303 |
case 't': telemetry = !telemetry; Serial.printf(">> telemetry=%d\n", telemetry); break;
|
304 |
case 'm': armed = !armed; if(!armed){iTerm=0;} Serial.printf(">> armed=%d\n", armed); break;
|
305 |
case '?':
|
306 |
Serial.printf(">> Kp=%.2f Ki=%.2f Kd=%.3f alpha=%.3f setpoint=%.2f armed=%d\n",
|
307 |
Kp,Ki,Kd,alpha,setpoint,armed);
|
308 |
break;
|
309 |
default: Serial.println(">> ? unbekannt"); break;
|
310 |
}
|
311 |
} else if (idx < sizeof(buf)-1) {
|
312 |
buf[idx++] = c;
|
313 |
}
|
314 |
}
|
315 |
}
|
316 |
|
317 |
// =======================================================
|
318 |
// Hauptschleife
|
319 |
// =======================================================
|
320 |
void loop() {
|
321 |
handleSerial();
|
322 |
|
323 |
unsigned long now = micros();
|
324 |
if (now - lastControlUs < CONTROL_PERIOD_US) return; // nicht-blockierendes Pacing
|
325 |
|
326 |
// Echtes dt messen (robust gegen Jitter), begrenzen
|
327 |
float dt = (now - lastControlUs) / 1e6f;
|
328 |
if (dt > 0.01f) dt = 0.01f; // nach Stall nicht explodieren
|
329 |
lastControlUs = now;
|
330 |
|
331 |
// ---- Sensorik ----
|
332 |
float ax, ay, az, gyDeg;
|
333 |
readMPU(ax, ay, az, gyDeg);
|
334 |
gyroRateDeg = gyDeg - gyroBiasDeg;
|
335 |
|
336 |
float pitchAcc = atan2f(-ax, sqrtf(ay*ay+az*az)) * 180.0f/PI - accPitchOff;
|
337 |
|
338 |
#if INVERT_ANGLE
|
339 |
pitchAcc = -pitchAcc;
|
340 |
gyroRateDeg = -gyroRateDeg;
|
341 |
#endif
|
342 |
|
343 |
// ---- Komplementärfilter (KORREKT: Ausgang rückgekoppelt → driftfrei) ----
|
344 |
pitch = alpha * (pitch + gyroRateDeg * dt) + (1.0f - alpha) * pitchAcc;
|
345 |
|
346 |
// ---- Arm-Logik ----
|
347 |
if (armed && fabs(pitch) > tipLimit) { // umgekippt → abschalten
|
348 |
armed = false; iTerm = 0;
|
349 |
}
|
350 |
if (!armed) {
|
351 |
isrArmed = false;
|
352 |
setMotorCommand(0, 0);
|
353 |
// Auto-Re-Arm wenn aufrecht & ruhig gehalten
|
354 |
if (fabs(pitch) < reArmLimit && fabs(gyroRateDeg) < 30.0f) {
|
355 |
if (stableSinceMs == 0) stableSinceMs = millis();
|
356 |
if (millis() - stableSinceMs > 400) { armed = true; iTerm = 0; }
|
357 |
} else {
|
358 |
stableSinceMs = 0;
|
359 |
}
|
360 |
pixel.setPixelColor(0, pixel.Color(80,0,0)); pixel.show(); // Rot = disarmed
|
361 |
if (telemetry && millis()-lastTeleMs > 50) {
|
362 |
lastTeleMs = millis();
|
363 |
Serial.printf("DISARM pitch:%.2f rate:%.1f (aufrecht halten zum Re-Arm)\n", pitch, gyroRateDeg);
|
364 |
}
|
365 |
return;
|
366 |
}
|
367 |
isrArmed = true;
|
368 |
|
369 |
// ---- PID-Regler (Grad) ----
|
370 |
float e = setpoint - pitch;
|
371 |
float P = Kp * e;
|
372 |
iTerm += Ki * e * dt;
|
373 |
iTerm = constrain(iTerm, -iLimit, iLimit);
|
374 |
float D = Kd * gyroRateDeg; // Derivative on Measurement (Gyrorate)
|
375 |
|
376 |
float u = P + iTerm - D;
|
377 |
float cmd = constrain(u, -255.0f, 255.0f);
|
378 |
|
379 |
setMotorCommand(cmd, cmd);
|
380 |
|
381 |
// ---- Status-LED ----
|
382 |
if (fabs(pitch) > tipLimit*0.6f)
|
383 |
pixel.setPixelColor(0, pixel.Color(80,40,0)); // Orange
|
384 |
else
|
385 |
pixel.setPixelColor(0, pixel.Color(0,80,0)); // Grün
|
386 |
pixel.show();
|
387 |
|
388 |
// ---- Telemetrie (~20 Hz, copy-paste-freundlich & parsebar) ----
|
389 |
if (telemetry && millis() - lastTeleMs > 50) {
|
390 |
lastTeleMs = millis();
|
391 |
Serial.printf("pitch:%.2f rate:%.1f e:%.2f P:%.1f I:%.1f D:%.1f u:%.0f cmd:%.0f kp:%.1f ki:%.1f kd:%.2f a:%.3f dt:%lu\n",
|
392 |
pitch, gyroRateDeg, e, P, iTerm, D, u, cmd,
|
393 |
Kp, Ki, Kd, alpha, (unsigned long)(dt*1e6));
|
394 |
}
|
395 |
}
|
396 |
|
397 |
// =======================================================
|
398 |
// TUNING-WORKFLOW (Stand v4 — Startwerte aus Telemetrie)
|
399 |
// =======================================================
|
400 |
//
|
401 |
// setpoint = -2.5° ist der aus Telemetrie gemessene Balancepunkt.
|
402 |
// Falls er nach dem Flash noch systematisch rollt:
|
403 |
// vorwärts rollt → "s-3.0" oder "s-3.5" probieren
|
404 |
// rückwärts rollt → "s-2.0" oder "s-1.5" probieren
|
405 |
// In 0.5°-Schritten tasten bis er nahezu steht.
|
406 |
//
|
407 |
// PID-Feintuning danach:
|
408 |
// Schwingt er (regelmäßiges Vor-Rück)? → Kp runter ("p8") oder Kd hoch ("d3.0")
|
409 |
// Zittert er hochfrequent? → Kd runter ("d2.0")
|
410 |
// Pendelt er langsam in eine Richtung? → Ki leicht hoch ("i4") oder setpoint trimmen
|
411 |
// Reagiert er zu träge / fällt um? → Kp hoch ("p12")
|
412 |
//
|
413 |
// Serielle Befehle (kein Neuflashen nötig):
|
414 |
// p10 → Kp setzen d2.5 → Kd setzen
|
415 |
// i3 → Ki setzen a0.98 → alpha setzen
|
416 |
// s-2.5 → setpoint r → I-Term reset
|
417 |
// m → arm/disarm t → telemetrie an/aus
|
418 |
// ? → aktuelle Werte
|
419 |
//
|
420 |
// Telemetrie-Spalten:
|
421 |
// pitch = Ist-Winkel (Grad) rate = Gyrorate (Grad/s)
|
422 |
// e = Fehler (setpoint-pitch)
|
423 |
// P/I/D = Regleranteile u = Summe cmd = an Motor (−255..255)
|
424 |
// dt = Loopzeit µs (sollte ~1666 sein)
|
425 |
//
|
426 |
// Puls-Parameter (nur bei Bedarf, dann neu flashen):
|
427 |
// PULSE_MIN_US hoch wenn Motor bei kleinen Korrekturen nicht anspricht
|
428 |
// PULSE_PERIOD_US runter für feinere Regelung, hoch bei H-Brücken-Wärme
|
429 |
// =======================================================
|