Feu de camp LED avec Raspberry Pi Pico (Matrice NeoPixel 8x8 + Potentiomètre + Buzzer)
Un feu de camp qui ne brûle pas, ne chauffe pas, mais crépite ! Le **Raspberry Pi Pico** dessine sur une **matrice NeoPixel 8x8** une flamme réaliste recalculée à chaque image : des étincelles naissent à la base, la chaleur refroidit en montant et passe du rouge au blanc. Le **potentiomètre** règle la hauteur de la flamme, le **bouton** alterne entre Feu, Gaz, Magie et Améthyste, et le **buzzer** ajoute le crépitement du bois. Animation procédurale, ADC, PWM et MicroPython dans un seul projet.

Vidéo
Schéma du circuit

Code source
1# ─────────────────────────────────────────
2# BlueGrays · bluegrays.com
3# Feu de camp LED avec Raspberry Pi Pico (Matrice NeoPixel 8x8 + Potentiomètre + Buzzer)
4# ─────────────────────────────────────────
5# Animation réaliste de feu sur une matrice NeoPixel 8x8. Le potentiomètre
6# règle la hauteur de la flamme, un appui court change le thème (Feu, Gaz,
7# Magie, Améthyste) et un appui long active/désactive le son de
8# crépitement.
9#
10# Broches :
11# GP2 -> DIN de la matrice (VDD -> VBUS, VSS -> GND)
12# GP26 -> Curseur du potentiomètre (ADC0)
13# GP14 -> Bouton (autre patte sur GND)
14# GP17 -> Buzzer (+)
15
16from machine import Pin, PWM, ADC
17from neopixel import NeoPixel
18import time
19import random
20
21# ============ RÉGLAGES ============
22W = 8 # Colonnes
23H = 8 # Rangées
24SERPENTINE = False # True pour les panneaux câblés en zigzag
25BRIGHTNESS = 0.4 # Wokwi dessine les couleurs telles quelles, 0.4 montre bien la flamme
26 # 0.15-0.2 recommandé sur USB 2.0 (500 mA), utilise du 5V externe pour plus
27 # de luminosité
28FRAME_MS = 35 # Durée d'une image (~28 FPS)
29LONG_PRESS_MS = 900 # Durée de maintien pour activer/désactiver le son
30
31# ============ MATÉRIEL ============
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# ============ ÉTAT ============
39# heat[x][y] : colonne x, y = 0 est la rangée du bas ; 0 = froid, 255 = le plus chaud
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# ============ PALETTE DE COULEURS ============
47def heat_color(t):
48 # Associer la chaleur (0-255) à une couleur selon le thème : noir -> foncé -> vif -> blanc
49 t192 = (t * 191) // 255
50 ramp = (t192 & 0x3F) << 2 # Rampe de 0-252 par bande
51 if t192 & 0x80:
52 r, g, b = 255, 255, ramp # Le plus chaud : jaune -> blanc
53 elif t192 & 0x40:
54 r, g, b = 255, ramp, 0 # Milieu : rouge -> jaune
55 else:
56 r, g, b = ramp, 0, 0 # Froid : noir -> rouge
57 if theme == 1:
58 r, b = b, r # Gaz : flamme bleue
59 elif theme == 2:
60 r, g = g, r # Magie : flamme verte
61 elif theme == 3:
62 b = max(b, r * 3 // 4) # Améthyste : flamme violette
63 g = g // 2
64 return (int(r * BRIGHTNESS), int(g * BRIGHTNESS), int(b * BRIGHTNESS))
65
66
67def pixel_index(x, row):
68 # rangée 0 = rangée du haut
69 if SERPENTINE and row % 2 == 1:
70 return row * W + (W - 1 - x)
71 return row * W + x
72
73
74# ============ SIMULATION DE FEU ============
75def fire_step(intensity):
76 # intensité 0-1 : hauteur de la flamme. Haute = moins de refroidissement + plus d'étincelles
77 cooling = int(115 - 55 * intensity) # 115 (court) -> 60 (grand)
78 sparking = int(70 + 150 * intensity) # 70 -> 220 (probabilité)
79 max_cool = (cooling * 10) // H + 2
80
81 for x in range(W):
82 col = heat[x]
83 # Chaque cellule refroidit un peu
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 # La chaleur monte et se diffuse
88 for y in range(H - 1, 1, -1):
89 col[y] = (col[y - 1] + col[y - 2] + col[y - 2]) // 3
90 # Étincelles aléatoires à la base
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) Mélange de lumière avec les colonnes voisines : la flamme ondule plus naturellement
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# ============ SON DE CRÉPITEMENT ============
112def crackle(intensity):
113 # De très courts "tics" aléatoires = bois qui crépite
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# ============ BOUTON ============
131btn_state = 1
132btn_raw = 1
133btn_changed = time.ticks_ms()
134press_start = 0
135long_done = False
136
137
138def handle_button():
139 # Appui court = thème, appui long = son activé/désactivé
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# ============ BOUCLE PRINCIPALE ============
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 # Lisser la valeur du potentiomètre (la gigue ne doit pas faire sauter la flamme)
173 smooth += (pot.read_u16() / 65535 - smooth) * 0.2
174 fire_step(smooth)
175 render()
176 crackle(smooth)
177
178 # Fréquence d'images constante
179 spent = time.ticks_diff(time.ticks_ms(), start)
180 if spent < FRAME_MS:
181 time.sleep_ms(FRAME_MS - spent)
182FAQ
Pourquoi baisser la luminosité sur le vrai montage ?▾
Le code utilise `BRIGHTNESS = 0.4` car Wokwi affiche les couleurs telles quelles. Une vraie matrice peut tirer environ 1 A avec une grande flamme, alors qu'un port USB 2.0 ne fournit que 500 mA. Sur USB, mets 0.15–0.2 ; pour plus de lumière, alimente la matrice en 5V 2A et relie les masses (GND).
Comment changer la hauteur et la couleur de la flamme ?▾
Tourne le potentiomètre à gauche pour des braises, à droite pour des flammes qui montent jusqu'en haut de la matrice. Un appui court sur le bouton passe de Feu à Gaz, Magie puis Améthyste. Sur Wokwi, clique sur le potentiomètre et utilise les flèches ; la touche T change le thème.
Comment couper le crépitement ou le rendre plus rare ?▾
Maintiens le bouton environ une seconde : un bip aigu signifie son activé, un bip grave son coupé. Pour un crépitement plus rare, diminue le 10 et le 50 de `10 + 50 * intensity` dans `crackle()`. Utilise un buzzer passif pour le son.
Faut-il installer une bibliothèque ?▾
Non. Les modules `neopixel`, `machine` et `random` sont inclus dans MicroPython pour le Pico. Installe MicroPython sur le Pico, puis enregistre le code dessus sous le nom `main.py` avec Thonny ; le feu démarre tout seul à chaque mise sous tension.
