Lernzettel IT Hardware

Fokus: PWM · ADC · I2C

01 PWM – Pulsweitenmodulation

Pseudo-analoge Ausgabe mit nur einem digitalen Pin. Wird verwendet, um z. B. LEDs zu dimmen, Motoren in der Drehzahl zu regeln oder Servos anzusteuern.

Theorie: Grundidee

Ein Mikrocontroller-Pin kann nur zwei Zustände ausgeben: HIGH (3,3 V) oder LOW (0 V). Trotzdem will man oft eine Spannung dazwischen erzeugen, z. B. um eine LED auf 50 % Helligkeit zu dimmen. Lösung: Der Pin wird so schnell zwischen HIGH und LOW umgeschaltet, dass am Verbraucher im zeitlichen Mittel die gewünschte Leistung ankommt. Das menschliche Auge oder die Trägheit des Motors sieht/spürt nur den Mittelwert.

Kernidee: PWM erzeugt keine echte analoge Spannung. Es schaltet sehr schnell zwischen 0 V und 3,3 V hin und her. Der Verbraucher mittelt das selbst.

Wichtige Begriffe

BegriffSymbolBedeutung
PeriodendauerTDauer einer kompletten Schwingung (HIGH + LOW)
FrequenzfAnzahl Schwingungen pro Sekunde, f = 1 / T
Einschaltdauert_HIGHZeit, in der der Pin HIGH ist
Tastgrad / Duty CycleDVerhältnis t_HIGH zu T, in % (0 – 100)

Wichtige Formeln

Tastgrad D = t_HIGH / T  ·  100 %
U_mittel = D · U_max
f = 1 / T

Visualisierung des Tastgrads

10 %
U ≈ 0,33 V
25 %
U ≈ 0,83 V
50 %
U = 1,65 V
75 %
U ≈ 2,48 V
100 %
U = 3,3 V

(Werte für U_max = 3,3 V)

Frequenzwahl: warum so hoch?

Die Frequenz muss hoch genug sein, damit der Verbraucher den Wechsel nicht mehr wahrnimmt:

Achtung: Wenn die Frequenz zu niedrig gewählt wird, sieht man bei LEDs ein Flackern und hört bei Motoren ein Pfeifen oder Surren.

Tastgrad ≠ Frequenz

Wichtig zu unterscheiden:

Programmierung am STM32 (Arduino)

BefehlBedeutung
analogWrite(pin, wert);PWM ausgeben. Default 8 Bit → Werte 0 bis 255
analogWriteResolution(bit);Auflösung des Tastgrads ändern (z. B. 10 Bit → 0–1023)
analogWriteFrequency(hz);PWM-Frequenz setzen
Umrechnung Tastgrad → Wert:
Bei 8 Bit (Default) entspricht der Wert 0 einem Tastgrad von 0 % und der Wert 255 einem Tastgrad von 100 %. Beispiel: 50 % Tastgrad = Wert 128.

Beispiel: LED dimmen mit Potentiometer

#define POTI PA0
#define LED  PC6

void setup() {
  analogReadResolution(8);          // ADC: 0..255
  analogWriteFrequency(200);        // 200 Hz, kein Flackern
  pinMode(LED, OUTPUT);
}

void loop() {
  int wert = analogRead(POTI);      // Poti einlesen, 0..255
  analogWrite(LED, wert);           // direkt als Tastgrad
  delay(500);
}

Beispiel: festen Tastgrad ausgeben

analogWriteFrequency(1000);   // 1 kHz
analogWrite(PC6, 64);          // 64/255 ≈ 25 % Tastgrad

Vorteile / Nachteile

Vorteile

  • Einfacher als ein DAC
  • Sehr effizient: Transistor ganz an oder ganz aus, kaum Verlustleistung
  • Universell: LEDs, Motoren, Servos, Audio

Nachteile

  • Frequenz muss richtig gewählt werden
  • Kein echtes Analogsignal
  • Kann Störungen (EMV, Pfeifen) erzeugen

02 ADC – Analog Digital Converter

Wandelt analoge Spannungen (z. B. von Sensoren oder Potentiometern) in digitale Werte um, mit denen der Mikrocontroller rechnen kann. Gegenstück: DAC.

Theorie: Warum digital?

VorteileNachteile
AnalogStufenlos, beliebig viele ZwischenwerteStöranfällig, Verzerrungen, Ablesefehler
DigitalStörsicher, einfach speicherbar, flexibel weiterverarbeitbar, leichter ablesbarEndlich viele Zustände, Informationsverlust beim Wandeln

Funktionsprinzip

Die Wandlung läuft in zwei Schritten ab:

  1. Abtastung (Sampling): in regelmäßigen Abständen wird der Spannungswert stichprobenartig gemessen
  2. Digitalisierung (Quantisierung): dem gemessenen Spannungswert wird ein digitaler Wert zugeordnet

Shannon-Abtasttheorem

Das Theorem gibt vor, mit welcher Frequenz mindestens abgetastet werden muss, damit das analoge Signal später wieder rekonstruiert werden kann:

f_abtast > 2 · f_max

f_max ist die höchste Frequenz, die im analogen Signal vorkommt. Wird zu langsam abgetastet, geht Information verloren (Aliasing).

Auflösung

Die Auflösung gibt an, in wie vielen Stufen der ADC die Spannung abbildet. Mehr Bit = feinere Auflösung = genauere Messung.

AuflösungStufenΔU bei U_ref = 3,3 V
8 Bit2⁸ = 256≈ 12,9 mV
10 Bit (Arduino-Default)2¹⁰ = 1024≈ 3,22 mV
12 Bit (STM32)2¹² = 4096≈ 0,806 mV

Wichtige Formeln

ΔU = U_ref / 2ⁿ

ΔU ist die Spannungsauflösung, also der Abstand zwischen zwei benachbarten Stufen. n ist die Bit-Auflösung des ADC.

U_eingang = ADC_wert · U_ref / (2ⁿ − 1)

Damit lässt sich aus dem digitalen ADC-Wert die ursprüngliche Spannung zurückrechnen.

Hinweis aus dem Unterricht: In der Formelsammlung steht für ΔU manchmal U_ref / (2ⁿ − 1). Beide Schreibweisen werden als richtig gewertet.

Beispiel-Rechnungen

Beispiel 1: 12-Bit-ADC, U_ref = 3,3 V → ΔU = 3,3 V / 4096 ≈ 0,806 mV
Beispiel 2: Gemessener ADC-Wert = 2048, U_ref = 3,3 V, 12 Bit → U = 2048 · 3,3 / 4095 ≈ 1,65 V

Wägeverfahren (sukzessive Approximation)

Der STM32-ADC arbeitet nach dem Wägeverfahren. Ein internes Register (SAR = Successive Approximation Register) wird Bit für Bit gefüllt, beginnend mit dem MSB:

  1. Alle Bits werden auf 0 gesetzt
  2. Das MSB wird auf 1 gesetzt, ein interner DAC erzeugt die entsprechende Spannung
  3. Komparator vergleicht: Ist DAC-Spannung < Eingangsspannung → Bit bleibt 1, sonst wird es zurückgesetzt
  4. Verfahren wird mit dem nächsten Bit wiederholt, bis das LSB erreicht ist

Bei jedem Takt wird ein Bit angenähert. Bei 12 Bit braucht der ADC also 12 Takte plus etwas Overhead.

ADC am STM32L152RE

Analog-fähige Pins (Nucleo L152RE):

Programmierung

BefehlBedeutung
analogReadResolution(12);Auflösung auf 12 Bit setzen (Default 10 Bit)
int wert = analogRead(PA0);Spannung an Pin einlesen

Beispiel: Potentiometer einlesen

#define POTI    PA0
#define U_MAX   3.3
#define ADC_MAX 4095

void setup() {
  analogReadResolution(12);
}

void loop() {
  int adc_wert = analogRead(POTI);
  float U = (float) adc_wert * U_MAX / ADC_MAX;
  // adc_wert: 0..4095
  // U: 0..3,3 V
}
Casting nicht vergessen: Bei adc_wert * U_MAX / ADC_MAX ohne (float) rechnet C ganzzahlig und das Ergebnis ist 0 oder 1. Erst durch das Casting auf float wird sauber gerechnet.

Beispiel: ADC mit Hardware-Timer (Übungsaufgabe)

Ein analoger Sensor an PA0 wird alle 2 Sekunden mit einem Timer abgefragt, abhängig vom Wert wird ein LED-Muster ausgegeben:

#include <Arduino.h>
#include <HardwareTimer.h>

#define SENSOR PA0
#define GRENZE_40  1638   // 40 % von 4095
#define GRENZE_60  2457   // 60 % von 4095

HardwareTimer *timer = new HardwareTimer(TIM6);
volatile bool messen = false;

void timer_isr() { messen = true; }

void setup() {
  for (int i = 0; i < 8; i++) pinMode(PC0 + i, OUTPUT);
  analogReadResolution(12);

  timer->setOverflow(2000000, MICROSEC_FORMAT);  // alle 2 s
  timer->attachInterrupt(timer_isr);
  timer->resume();
}

void loop() {
  if (messen) {
    messen = false;
    int wert = analogRead(SENSOR);

    unsigned char muster;
    if (wert < GRENZE_40)       muster = 0x01;
    else if (wert < GRENZE_60)  muster = 0x81;
    else                        muster = 0xff;

    GPIOC->ODR = (GPIOC->ODR & 0xFF00) | muster;
  }
}

03 I2C – Inter Integrated Circuit

Synchroner 2-Draht-Bus mit Adressierung. Ideal, wenn man mehrere Sensoren oder Module mit wenigen Leitungen anschließen will.

Theorie: Grundprinzip

I2C wurde 1982 von Philips entwickelt und ist ein synchroner serieller 2-Draht-Bus. Mehrere Bausteine teilen sich dieselben zwei Leitungen. Master-Slave-Prinzip (neu: Controller / Target). Halbduplex – die beiden Seiten können nicht gleichzeitig senden.

Leitungen

LeitungBedeutung
SDASerial Data – die Datenleitung
SCLSerial Clock – das Taktsignal vom Master

Open Drain und Pull-Ups

Beide Leitungen sind Open Drain. Das heißt: jeder Teilnehmer kann die Leitung nur auf 0 V (LOW) ziehen, nicht aktiv auf HIGH treiben. Damit die Leitung im Ruhezustand HIGH ist, brauchen SDA und SCL jeweils einen Pull-Up-Widerstand.

Warum Open Drain? So können sich mehrere Teilnehmer dieselbe Leitung teilen, ohne dass es zum Kurzschluss kommt, wenn zwei gleichzeitig senden wollen.

Geschwindigkeiten

ModusGeschwindigkeit
Standard Mode (Sm)100 kbit/s
Fast Mode (Fm)400 kbit/s
Fast Mode Plus (Fm+)1 Mbit/s
High Speed Mode (Hs)3,4 Mbit/s

Reichweite: typischerweise nur Platinengröße (ca. 50 cm).

Adressierung

Jeder Slave hat eine 7-Bit-Adresse. Die oberen 4 Bit sind oft vom Hersteller fest, die unteren 3 Bit am Baustein über Pins (A0, A1, A2) einstellbar. Damit lassen sich z. B. bis zu 8 PCF8574 am selben Bus betreiben.

Adressbyte

BitA6A5A4A3A2A1A0R/W
Inhalt herstellerfest einstellbar 0=W, 1=R
Achtung: Mit 7 Bit gibt es nur 128 mögliche Adressen, davon sind 16 reserviert. Adressen sind oft mehrfach vergeben, dann braucht man mehrere I2C-Busse.

Frame-Aufbau

Ein I2C-Frame zum Schreiben sieht so aus:

START
7-Bit Adresse
R/W
ACK
Datenbyte
ACK
STOP
  1. START: Master zieht SDA auf LOW, während SCL HIGH ist
  2. Adresse + R/W-Bit: 7 Adress-Bits + 1 Richtungsbit
  3. ACK: Slave bestätigt mit 0, dass er angesprochen wurde
  4. Datenbytes: jeweils mit ACK des Empfängers
  5. STOP: Master zieht SDA von LOW auf HIGH, während SCL HIGH ist

Ablauf: Schreiben (Output mode)

Beispiel mit dem PCF8574 (Port-Expander):

  1. Master sendet START + Adresse + R/W = 0
  2. Slave sendet ACK
  3. Master sendet Datenbyte (z. B. das LED-Muster)
  4. Slave sendet ACK und schaltet seine Pins entsprechend
  5. Master sendet STOP

Ablauf: Lesen (Input mode)

  1. Master sendet START + Adresse + R/W = 1
  2. Slave sendet ACK
  3. Slave sendet Datenbyte
  4. Master sendet ACK (für weiter lesen) oder NACK (Schluss)
  5. Master sendet STOP

I2C-Pins am Nucleo L152RE

BusSDASCL
I2C1PB9PB8
I2C1 (alt)PB7PB6
I2C2PB11PB10

Programmierung mit Wire-Library

BefehlBedeutung
#include <Wire.h>Bibliothek einbinden
Wire.begin();Bus initialisieren (Master-Mode)
Wire.setClock(400000);Geschwindigkeit setzen (z. B. Fast Mode)
Wire.beginTransmission(addr);Übertragung an Slave starten
Wire.write(byte);Byte in Sendepuffer schreiben
Wire.endTransmission();Sendepuffer übertragen + STOP
Wire.requestFrom(addr, n);n Bytes vom Slave anfordern
Wire.available();Anzahl ungelesener Bytes
Wire.read();Byte aus Empfangspuffer lesen
Wichtig: Wire.write() schreibt nur in einen Pufferspeicher. Erst Wire.endTransmission() sendet die Daten wirklich auf den Bus.

Beispiel: Daten an einen Slave senden

#include <Arduino.h>
#include <Wire.h>

#define SENSOR_ADDRESS 0x4A

void setup() {
  Wire.begin();
}

void loop() {
  // Ein Byte senden
  Wire.beginTransmission(SENSOR_ADDRESS);
  Wire.write(0x9f);
  Wire.endTransmission();

  // Zwei Bytes lesen
  Wire.requestFrom(SENSOR_ADDRESS, 2);
  unsigned char b1 = Wire.read();
  unsigned char b2 = Wire.read();
}

Beispiel: PCF8574 Lauflicht

#define PCF1_ADDRESS 0x22

unsigned char muster[6] = {0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE0};

void loop() {
  for (int i = 0; i < 6; i++) {
    Wire.beginTransmission(PCF1_ADDRESS);
    Wire.write(~muster[i]);   // invertieren, weil LEDs low-aktiv
    Wire.endTransmission();
    delay(200);
  }
}

I2C-Bus-Scanner

Praktisch, um die Adresse unbekannter Bausteine zu finden. Man geht alle Adressen 1 bis 127 durch und prüft, ob endTransmission() 0 (= erfolgreich) zurückgibt:

for (int addr = 1; addr < 128; addr++) {
  Wire.beginTransmission(addr);
  Wire.write(0x00);
  if (Wire.endTransmission() == 0) {
    // Baustein an dieser Adresse gefunden
    lcd.printf("0x%2X gefunden", addr);
  }
}

Vorteile / Nachteile

Vorteile

  • Nur 2 Leitungen für viele Slaves
  • Adressierung statt CS-Pins
  • ACK-Bestätigung jedes Bytes
  • Standardisiert, sehr verbreitet

Nachteile

  • Pull-Up-Widerstände nötig
  • Begrenzte Reichweite (ca. 50 cm)
  • Nur Halbduplex
  • Adressen oft mehrfach vergeben
  • Langsamer als SPI

Pin & Port Refresher

Pin-Modi

  • OUTPUT – Ausgabe
  • INPUT_PULLUP – low-aktiv
  • INPUT_PULLDOWN – high-aktiv
pinMode(PC0, OUTPUT);
digitalWrite(PC0, HIGH);
int x = digitalRead(PA1);

Port-Register

  • GPIOx->ODR – schreiben
  • GPIOx->IDR – lesen
GPIOC->ODR = 0xFF;
// Nur untere 8 Bit
GPIOC->ODR = (GPIOC->ODR & 0xFF00) | muster;

Datentypen, Arrays, Operatoren Refresher

Wichtige Typen

  • unsigned char – 0..255 (LED-Muster)
  • int – ±2 Mrd
  • float – Kommazahlen
  • volatile – für ISR-Variablen!

Bit-Tricks

x |= (1<<n);     // Bit setzen
x &= ~(1<<n);    // Bit löschen
x ^= (1<<n);     // Bit toggeln
if (x & (1<<n))  // Bit testen

Arrays

unsigned char m[4] = {0x01, 0x02, 0x04, 0x08};
m[2] = 0x10;

// 2D
unsigned char b[2][3] = { {1,2,3}, {4,5,6} };

Casting

int a = 100;
float f = (float) a / 4095;

Ohne Cast → Integer-Division → 0!

Taster: Polling vs. Interrupt Refresher

Polling

if (digitalRead(PA1) == LOW) {
  // gedrückt
}

Einfach, aber CPU ständig beschäftigt.

Externer Interrupt

volatile bool flag = false;
void isr() { flag = true; }

attachInterrupt(
  digitalPinToInterrupt(PA1),
  isr, FALLING);

Trigger: RISING, FALLING, CHANGE

ISR-Regeln: Kurz halten, nur Flag setzen, geteilte Variablen volatile, kein Serial.print, delay(20) nur zum Entprellen.

LEDs & Schrittmotor Refresher

LEDs

High-aktiv: leuchtet bei HIGH. Low-aktiv: leuchtet bei LOW (z. B. Keypad).

GPIOC->ODR = 0b10000001;
digitalWrite(PC0, !digitalRead(PC0));

Schrittmotor

4 Spulen, Reihenfolge bestimmt Drehrichtung:

unsigned char s[4] = {0x01, 0x02, 0x04, 0x08};
// Hoch: 0x01 → 0x08
// Runter: 0x08 → 0x01

LCD-Ausgabe Refresher

MethodeBedeutung
lcd.clear();Display löschen
lcd.setCursor(spalte, zeile);Cursor positionieren (0–15, 0–1)
lcd.print("Text");Text ausgeben
lcd.printf("%.2f V", u);Formatierte Ausgabe

printf-Format

%d int · %4d 4 Stellen · %f float · %.2f 2 Nachkommastellen · %X hex · %s String

Float in printf: in platformio.ini ergänzen:
build_flags = -D PIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF

HardwareTimer + Timer-Interrupt Refresher

Zeitsteuerung ohne delay(). Läuft parallel im Hintergrund.

#include <HardwareTimer.h>

HardwareTimer *timer = new HardwareTimer(TIM6);
volatile bool tick = false;

void timer_isr() { tick = true; }

void setup() {
  timer->setOverflow(2000000, MICROSEC_FORMAT);  // alle 2 s
  timer->attachInterrupt(timer_isr);
  timer->resume();
}

void loop() {
  if (tick) {
    tick = false;
    // Aktion
  }
}
FormatBeispiel
MICROSEC_FORMAT2000000 = 2 s
HERTZ_FORMAT2 = 2 Hz

Merksätze