Fokus: PWM · ADC · I2C
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.
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.
| Begriff | Symbol | Bedeutung |
|---|---|---|
| Periodendauer | T | Dauer einer kompletten Schwingung (HIGH + LOW) |
| Frequenz | f | Anzahl Schwingungen pro Sekunde, f = 1 / T |
| Einschaltdauer | t_HIGH | Zeit, in der der Pin HIGH ist |
| Tastgrad / Duty Cycle | D | Verhältnis t_HIGH zu T, in % (0 – 100) |
(Werte für U_max = 3,3 V)
Die Frequenz muss hoch genug sein, damit der Verbraucher den Wechsel nicht mehr wahrnimmt:
Wichtig zu unterscheiden:
| Befehl | Bedeutung |
|---|---|
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 |
#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);
}
analogWriteFrequency(1000); // 1 kHz
analogWrite(PC6, 64); // 64/255 ≈ 25 % Tastgrad
Wandelt analoge Spannungen (z. B. von Sensoren oder Potentiometern) in digitale Werte um, mit denen der Mikrocontroller rechnen kann. Gegenstück: DAC.
| Vorteile | Nachteile | |
|---|---|---|
| Analog | Stufenlos, beliebig viele Zwischenwerte | Störanfällig, Verzerrungen, Ablesefehler |
| Digital | Störsicher, einfach speicherbar, flexibel weiterverarbeitbar, leichter ablesbar | Endlich viele Zustände, Informationsverlust beim Wandeln |
Die Wandlung läuft in zwei Schritten ab:
Das Theorem gibt vor, mit welcher Frequenz mindestens abgetastet werden muss, damit das analoge Signal später wieder rekonstruiert werden kann:
f_max ist die höchste Frequenz, die im analogen Signal vorkommt. Wird zu langsam abgetastet, geht Information verloren (Aliasing).
Die Auflösung gibt an, in wie vielen Stufen der ADC die Spannung abbildet. Mehr Bit = feinere Auflösung = genauere Messung.
| Auflösung | Stufen | ΔU bei U_ref = 3,3 V |
|---|---|---|
| 8 Bit | 2⁸ = 256 | ≈ 12,9 mV |
| 10 Bit (Arduino-Default) | 2¹⁰ = 1024 | ≈ 3,22 mV |
| 12 Bit (STM32) | 2¹² = 4096 | ≈ 0,806 mV |
ΔU ist die Spannungsauflösung, also der Abstand zwischen zwei benachbarten Stufen. n ist die Bit-Auflösung des ADC.
Damit lässt sich aus dem digitalen ADC-Wert die ursprüngliche Spannung zurückrechnen.
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:
Bei jedem Takt wird ein Bit angenähert. Bei 12 Bit braucht der ADC also 12 Takte plus etwas Overhead.
Analog-fähige Pins (Nucleo L152RE):
| Befehl | Bedeutung |
|---|---|
analogReadResolution(12); | Auflösung auf 12 Bit setzen (Default 10 Bit) |
int wert = analogRead(PA0); | Spannung an Pin 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
}
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.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;
}
}
Synchroner 2-Draht-Bus mit Adressierung. Ideal, wenn man mehrere Sensoren oder Module mit wenigen Leitungen anschließen will.
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.
| Leitung | Bedeutung |
|---|---|
SDA | Serial Data – die Datenleitung |
SCL | Serial Clock – das Taktsignal vom Master |
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.
| Modus | Geschwindigkeit |
|---|---|
| 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).
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.
| Bit | A6 | A5 | A4 | A3 | A2 | A1 | A0 | R/W |
|---|---|---|---|---|---|---|---|---|
| Inhalt | herstellerfest | einstellbar | 0=W, 1=R | |||||
Ein I2C-Frame zum Schreiben sieht so aus:
Beispiel mit dem PCF8574 (Port-Expander):
| Bus | SDA | SCL |
|---|---|---|
| I2C1 | PB9 | PB8 |
| I2C1 (alt) | PB7 | PB6 |
| I2C2 | PB11 | PB10 |
| Befehl | Bedeutung |
|---|---|
#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 |
Wire.write() schreibt nur in einen Pufferspeicher. Erst Wire.endTransmission() sendet die Daten wirklich auf den Bus.#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();
}
#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);
}
}
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);
}
}
OUTPUT – AusgabeINPUT_PULLUP – low-aktivINPUT_PULLDOWN – high-aktivpinMode(PC0, OUTPUT);
digitalWrite(PC0, HIGH);
int x = digitalRead(PA1);
GPIOx->ODR – schreibenGPIOx->IDR – lesenGPIOC->ODR = 0xFF;
// Nur untere 8 Bit
GPIOC->ODR = (GPIOC->ODR & 0xFF00) | muster;
unsigned char – 0..255 (LED-Muster)int – ±2 Mrdfloat – Kommazahlenvolatile – für ISR-Variablen!x |= (1<<n); // Bit setzen
x &= ~(1<<n); // Bit löschen
x ^= (1<<n); // Bit toggeln
if (x & (1<<n)) // Bit testen
unsigned char m[4] = {0x01, 0x02, 0x04, 0x08};
m[2] = 0x10;
// 2D
unsigned char b[2][3] = { {1,2,3}, {4,5,6} };
int a = 100;
float f = (float) a / 4095;
Ohne Cast → Integer-Division → 0!
if (digitalRead(PA1) == LOW) {
// gedrückt
}
Einfach, aber CPU ständig beschäftigt.
volatile bool flag = false;
void isr() { flag = true; }
attachInterrupt(
digitalPinToInterrupt(PA1),
isr, FALLING);
Trigger: RISING, FALLING, CHANGE
volatile, kein Serial.print, delay(20) nur zum Entprellen.High-aktiv: leuchtet bei HIGH. Low-aktiv: leuchtet bei LOW (z. B. Keypad).
GPIOC->ODR = 0b10000001;
digitalWrite(PC0, !digitalRead(PC0));
4 Spulen, Reihenfolge bestimmt Drehrichtung:
unsigned char s[4] = {0x01, 0x02, 0x04, 0x08};
// Hoch: 0x01 → 0x08
// Runter: 0x08 → 0x01
| Methode | Bedeutung |
|---|---|
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 |
%d int · %4d 4 Stellen · %f float · %.2f 2 Nachkommastellen · %X hex · %s String
platformio.ini ergänzen:build_flags = -D PIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTFZeitsteuerung 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
}
}
| Format | Beispiel |
|---|---|
MICROSEC_FORMAT | 2000000 = 2 s |
HERTZ_FORMAT | 2 = 2 Hz |
write() nur in Puffer, erst endTransmission() sendet(float) davorschreiben