mini segway ESP32 feasibility study

OP #8098129
Lesenswert?

Ich möchte euch mal was zeigen... ich hab mal ein wenig in Hard und Software gebastelt.. siehe hier: https://qlb-harz.de/tbqlb/Video/segway.MOV

Den Code habe ich für Interessenten hier mal rein kopiert... Für mich war das nen cooles Projekt, hat Spaß gemacht darum teile ich es hier gern.

LG Tobias https://qlb-harz.de/Z80/

[Mod: Code ohne Formatierung gelöscht, wird weiter unten nochmal gepostet]

: Bearbeitet durch Moderator
OP #8098174
Lesenswert?

Oh stimmt, na sowas! So besser....

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
// =======================================================
: Bearbeitet durch User
OP #8098206
Lesenswert?

Also bei mir wird der Code jetzt richtig dargestellt! Den ersten Post konnte ich nach 60min nicht mehr ändern. "lesen, verinnerlichen, verstehen, umsetzen" so so ... Aber wenn man hier gleich so welche um die Ohren kriegt, verkneife ich mir das in Zukunft lieber...

LG Tobias

#8098215
Lesenswert?

Tobias B. schrieb:

Aber wenn man hier gleich so welche um die Ohren kriegt, verkneife ich mir das in Zukunft lieber...

Ist das Ziel nicht, neben Lob auch Kritik über ein Projekt zu bekommen?

Das ist der Hinweis solche langen Codes als Datei anzuhängen und nicht die Darstellung im Text zu wählen.

Und in dem Fall bin ich tatsächlich auch dafür, was absolut nicht böse gemeint ist.

Bin auch schon kritisiert worden wenn meine Codefragmente zu lang waren, das muss man ab können!

Moderator (Firma: Titel) Persönliche Seite #8098221
Lesenswert?

Tobias B. schrieb:

Also bei mir wird der Code jetzt richtig dargestellt!

Wenn jeder an sich selber denkt ist auch an jeden gedacht.

Aber wenn du mit deinem Projekt in die Öffentlichkeit willst, dann solltest du dich aufschlauen, wie die Öffentlichkeit das gerne präsentiert haben will:

LangerSourcecode

Und wie du selbst herausgefunden hast, wird mit den richtigen Tags der Code inline syntaxgehighlitet. Da gibt es 2 Möglichkeiten:

  1. Markdown siehe z.B. https://github.com/microsoft/vscode/issues/36069
  2. klassisch mit [ c ] und [ /c ]

C-Tags

1
#define Code
1
#define Code
Angehängte Dateien:
: Bearbeitet durch Moderator
OP #8098354
Lesenswert?

Für die Unwissenden strg a -> strg c -> strg v

Für die Wissenden hätte ich mich hier gern über die Proportional Differenzial Integral Regelung die Wichtung, Kalibrierung etc. unterhalten.

Offensichtlich und leider, ist das hier nicht wirklich mein Forum.

Mit dem auswendig lernen von Überschriften ist man noch lange nicht in der Lage mitreden zu können.

LG Tobias

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren