Raspberry Pi Pico LED-Lagerfeuer (8x8-NeoPixel-Matrix + Potentiometer + Summer)
Ein Lagerfeuer, das weder brennt noch wärmt, aber knistert! Der **Raspberry Pi Pico** zeichnet auf einer **8x8-NeoPixel-Matrix** eine realistische Flamme, die in jedem Frame neu entsteht: Am Boden sprühen Funken, die Hitze kühlt nach oben ab und glüht von Rot bis Weiß. Das **Potentiometer** regelt die Flammenhöhe, der **Taster** wechselt zwischen Feuer, Gas, Magie und Amethyst, der **Summer** liefert das Knistern von Holz. Prozedurale Animation, ADC, PWM und MicroPython in einem Projekt.

Video
Schaltplan

Quellcode
1# ─────────────────────────────────────────
2# BlueGrays · bluegrays.com
3# Raspberry Pi Pico LED-Lagerfeuer (8x8-NeoPixel-Matrix + Potentiometer + Summer)
4# ─────────────────────────────────────────
5# Realistische Feueranimation auf einer 8x8-NeoPixel-Matrix. Das
6# Potentiometer stellt die Flammenhöhe ein, ein kurzer Tastendruck
7# wechselt das Thema (Feuer, Gas, Magie, Amethyst) und ein langer Druck
8# schaltet den Knistersound um.
9#
10# Pins:
11# GP2 -> Matrix DIN (VDD -> VBUS, VSS -> GND)
12# GP26 -> Schleifer des Potentiometers (ADC0)
13# GP14 -> Taste (anderes Beinchen an GND)
14# GP17 -> Buzzer (+)
15
16from machine import Pin, PWM, ADC
17from neopixel import NeoPixel
18import time
19import random
20
21# ============ EINSTELLUNGEN ============
22W = 8 # Spalten
23H = 8 # Zeilen
24SERPENTINE = False # True für im Zickzack verdrahtete Panels
25BRIGHTNESS = 0.4 # Wokwi zeichnet Farben unverändert, 0.4 zeigt die Flamme deutlich
26 # 0.15-0.2 empfohlen an USB 2.0 (500 mA), für mehr Helligkeit externes 5V
27 # verwenden
28FRAME_MS = 35 # Frame-Zeit (~28 FPS)
29LONG_PRESS_MS = 900 # Haltezeit zum Umschalten des Sounds
30
31# ============ HARDWARE ============
32matrix = NeoPixel(Pin(2), W * H)
33pot = ADC(26) # ADC0
34button = Pin(14, Pin.IN, Pin.PULL_UP)
35buzzer = PWM(Pin(17))
36buzzer.duty_u16(0)
37
38# ============ ZUSTAND ============
39# heat[x][y]: Spalte x, y = 0 ist die unterste Reihe; 0 = kalt, 255 = am heißesten
40heat = [[0] * H for _ in range(W)]
41theme = 0
42THEME_NAMES = ["Ates / Fire", "Gaz / Gas", "Buyu / Magic", "Ametist / Amethyst"]
43sound_on = True
44
45
46# ============ FARBPALETTE ============
47def heat_color(t):
48 # Hitze (0-255) je nach Thema auf eine Farbe abbilden: schwarz -> tief -> hell -> weiß
49 t192 = (t * 191) // 255
50 ramp = (t192 & 0x3F) << 2 # 0-252 Rampe pro Band
51 if t192 & 0x80:
52 r, g, b = 255, 255, ramp # Am heißesten: Gelb -> Weiß
53 elif t192 & 0x40:
54 r, g, b = 255, ramp, 0 # Mitte: Rot -> Gelb
55 else:
56 r, g, b = ramp, 0, 0 # Kalt: Schwarz -> Rot
57 if theme == 1:
58 r, b = b, r # Gas: blaue Flamme
59 elif theme == 2:
60 r, g = g, r # Magie: grüne Flamme
61 elif theme == 3:
62 b = max(b, r * 3 // 4) # Amethyst: lila Flamme
63 g = g // 2
64 return (int(r * BRIGHTNESS), int(g * BRIGHTNESS), int(b * BRIGHTNESS))
65
66
67def pixel_index(x, row):
68 # Zeile 0 = oberste Zeile
69 if SERPENTINE and row % 2 == 1:
70 return row * W + (W - 1 - x)
71 return row * W + x
72
73
74# ============ FEUERSIMULATION ============
75def fire_step(intensity):
76 # Intensität 0-1: Flammenhöhe. Hoch = weniger Kühlung + mehr Funken
77 cooling = int(115 - 55 * intensity) # 115 (kurz) -> 60 (hoch)
78 sparking = int(70 + 150 * intensity) # 70 -> 220 (Wahrscheinlichkeit)
79 max_cool = (cooling * 10) // H + 2
80
81 for x in range(W):
82 col = heat[x]
83 # Jede Zelle kühlt etwas ab
84 for y in range(H):
85 c = col[y] - random.getrandbits(8) % max_cool
86 col[y] = c if c > 0 else 0
87 # Wärme driftet nach oben und breitet sich aus
88 for y in range(H - 1, 1, -1):
89 col[y] = (col[y - 1] + col[y - 2] + col[y - 2]) // 3
90 # Zufällige Funken an der Basis
91 if random.getrandbits(8) < sparking:
92 y = random.getrandbits(1)
93 col[y] = min(255, col[y] + 160 + random.getrandbits(6))
94
95 # 4) Lichtmischung mit Nachbarspalten: Die Flamme wogt natürlicher
96 for y in range(2, H):
97 row_vals = [heat[x][y] for x in range(W)]
98 for x in range(W):
99 left = row_vals[x - 1] if x > 0 else row_vals[x]
100 right = row_vals[x + 1] if x < W - 1 else row_vals[x]
101 heat[x][y] = (row_vals[x] * 2 + left + right) // 4
102
103
104def render():
105 for x in range(W):
106 for y in range(H):
107 matrix[pixel_index(x, H - 1 - y)] = heat_color(heat[x][y])
108 matrix.write()
109
110
111# ============ KNISTERSOUND ============
112def crackle(intensity):
113 # Zufällige sehr kurze "Ticks" = knisterndes Holz
114 if not sound_on:
115 return
116 if random.getrandbits(8) < int(10 + 50 * intensity):
117 buzzer.freq(1500 + random.getrandbits(12)) # 1500-5595 Hz
118 buzzer.duty_u16(6000)
119 time.sleep_ms(2 + random.getrandbits(2))
120 buzzer.duty_u16(0)
121
122
123def beep(freq, ms):
124 buzzer.freq(freq)
125 buzzer.duty_u16(20000)
126 time.sleep_ms(ms)
127 buzzer.duty_u16(0)
128
129
130# ============ TASTE ============
131btn_state = 1
132btn_raw = 1
133btn_changed = time.ticks_ms()
134press_start = 0
135long_done = False
136
137
138def handle_button():
139 # Kurzer Druck = Thema, langer Druck = Sound an/aus
140 global btn_state, btn_raw, btn_changed, press_start, long_done, theme, sound_on
141 now = time.ticks_ms()
142 raw = button.value()
143 if raw != btn_raw:
144 btn_raw = raw
145 btn_changed = now
146 if time.ticks_diff(now, btn_changed) > 25 and raw != btn_state:
147 btn_state = raw
148 if raw == 0:
149 press_start = now
150 long_done = False
151 elif not long_done:
152 theme = (theme + 1) % len(THEME_NAMES)
153 print("Tema / Theme:", THEME_NAMES[theme])
154 beep(1200, 30)
155 if btn_state == 0 and not long_done and time.ticks_diff(now, press_start) > LONG_PRESS_MS:
156 long_done = True
157 sound_on = not sound_on
158 print("Ses / Sound:", "ACIK / ON" if sound_on else "KAPALI / OFF")
159 beep(1600 if sound_on else 500, 80)
160
161
162# ============ HAUPTSCHLEIFE ============
163print("== BlueGrays · bluegrays.com ==")
164print("Pico 8x8 Kamp Atesi / Campfire")
165print("Pot = alev boyu / flame height | Kisa basis = tema / short press = theme | Uzun basis = ses / long press = sound")
166
167smooth = pot.read_u16() / 65535
168while True:
169 start = time.ticks_ms()
170 handle_button()
171
172 # Den Potiwert glätten (Jitter soll die Flamme nicht springen lassen)
173 smooth += (pot.read_u16() / 65535 - smooth) * 0.2
174 fire_step(smooth)
175 render()
176 crackle(smooth)
177
178 # Konstante Bildrate
179 spent = time.ticks_diff(time.ticks_ms(), start)
180 if spent < FRAME_MS:
181 time.sleep_ms(FRAME_MS - spent)
182FAQ
Warum sollte ich die Helligkeit in der echten Schaltung senken?▾
Im Code steht `BRIGHTNESS = 0.4`, weil Wokwi die Farben unverändert zeichnet. Eine echte Matrix zieht bei großer Flamme rund 1 A, ein USB-2.0-Port liefert nur 500 mA. Über USB also 0.15–0.2 einstellen; für mehr Helligkeit die Matrix mit einem 5-V-Netzteil (2 A) versorgen und die GNDs verbinden.
Wie ändere ich Flammenhöhe und Farbe?▾
Dreh das Potentiometer nach links für Glut und nach rechts für Flammen bis zur obersten Reihe. Ein kurzer Tastendruck wechselt zwischen Feuer, Gas, Magie und Amethyst. In Wokwi klickst du auf das Poti und nutzt die Pfeiltasten; das Thema wechselst du mit T.
Wie schalte ich das Knistern aus oder mache es seltener?▾
Halte den Taster etwa eine Sekunde gedrückt: Ein hoher Piepton heißt Ton an, ein tiefer heißt Ton aus. Für seltenere Klicks verkleinerst du die Zahlen 10 und 50 im Ausdruck `10 + 50 * intensity` in `crackle()`. Für den Ton brauchst du einen passiven Summer.
Muss ich zusätzliche Bibliotheken installieren?▾
Nein. Die Module `neopixel`, `machine` und `random` sind in MicroPython für den Pico bereits enthalten. Spiele MicroPython auf den Pico und speichere den Code in Thonny als `main.py` darauf; dann startet das Feuer bei jedem Einschalten von selbst.
