#169Средний

Светодиодный костёр на Raspberry Pi Pico (матрица NeoPixel 8x8 + потенциометр + зуммер)

Костёр, который ничего не жжёт и не греет, но всё равно потрескивает! Raspberry Pi Pico рисует реалистичное пламя на матрице NeoPixel 8x8, пересчитывая его в каждом кадре: внизу появляются искры, жар остывает по мере подъёма и светится от красного до белого. Потенциометр задаёт высоту пламени, кнопка переключает темы «Огонь», «Газ», «Магия» и «Аметист», а зуммер добавляет треск горящих дров. Процедурная анимация, ADC, PWM и MicroPython в одном проекте.

0.0(0)
1 собрано
Светодиодный костёр на Raspberry Pi Pico (матрица NeoPixel 8x8 + потенциометр + зуммер)
Костёр, который помещается на столе и никогда ничего не поджигает. На светодиодной матрице 8x8 языки пламени рождаются из искр внизу и по мере подъёма меняют цвет с красного на жёлтый и белый, а поворотом ручки ты делаешь огонь меньше или больше. Короткое нажатие кнопки делает пламя синим, зелёным или фиолетовым, а долгое включает или выключает треск дров из зуммера.

Видео

Схема

Светодиодный костёр на Raspberry Pi Pico (матрица NeoPixel 8x8 + потенциометр + зуммер) - Circuit

Исходный код

MicroPython
1# ─────────────────────────────────────────
2#   BlueGrays · bluegrays.com
3#   Светодиодный костёр на Raspberry Pi Pico (матрица NeoPixel 8x8 + потенциометр + зуммер)
4# ─────────────────────────────────────────
5# Реалистичная анимация огня на матрице NeoPixel 8x8. Потенциометр задаёт
6# высоту пламени, короткое нажатие кнопки меняет тему (огонь, газ, магия,
7# аметист), а долгое включает и выключает звук потрескивания.
8#
9# Пины:
10#   GP2  -> DIN матрицы (VDD -> VBUS, VSS -> GND)
11#   GP26 -> Средний вывод потенциометра (ADC0)
12#   GP14 -> Кнопка (другая ножка к GND)
13#   GP17 -> Зуммер (+)
14
15from machine import Pin, PWM, ADC
16from neopixel import NeoPixel
17import time
18import random
19
20# ============ НАСТРОЙКИ ============
21W = 8                  # Столбцы
22H = 8                  # Строки
23SERPENTINE = False     # True для панелей с подключением зигзагом
24BRIGHTNESS = 0.4       # Wokwi рисует цвета как есть, при 0.4 пламя хорошо видно
25                       # На USB 2.0 (500 mA) рекомендуется 0.15-0.2, для большей яркости используй
26                       # внешние 5V
27FRAME_MS = 35          # Время кадра (~28 FPS)
28LONG_PRESS_MS = 900    # Время удержания для переключения звука
29
30# ============ ЖЕЛЕЗО ============
31matrix = NeoPixel(Pin(2), W * H)
32pot = ADC(26)                           # ADC0
33button = Pin(14, Pin.IN, Pin.PULL_UP)
34buzzer = PWM(Pin(17))
35buzzer.duty_u16(0)
36
37# ============ СОСТОЯНИЕ ============
38# heat[x][y]: столбец x, y = 0 — нижняя строка; 0 = холодно, 255 = горячее всего
39heat = [[0] * H for _ in range(W)]
40theme = 0
41THEME_NAMES = ["Ates / Fire", "Gaz / Gas", "Buyu / Magic", "Ametist / Amethyst"]
42sound_on = True
43
44
45# ============ ЦВЕТОВАЯ ПАЛИТРА ============
46def heat_color(t):
47    # Перевести жар (0-255) в цвет по теме: чёрный -> глубокий -> яркий -> белый
48    t192 = (t * 191) // 255
49    ramp = (t192 & 0x3F) << 2      # Нарастание 0-252 на полосу
50    if t192 & 0x80:
51        r, g, b = 255, 255, ramp   # Самое горячее: жёлтый -> белый
52    elif t192 & 0x40:
53        r, g, b = 255, ramp, 0     # Середина: красный -> жёлтый
54    else:
55        r, g, b = ramp, 0, 0       # Холодное: чёрный -> красный
56    if theme == 1:
57        r, b = b, r                # Газ: синее пламя
58    elif theme == 2:
59        r, g = g, r                # Магия: зелёное пламя
60    elif theme == 3:
61        b = max(b, r * 3 // 4)     # Аметист: фиолетовое пламя
62        g = g // 2
63    return (int(r * BRIGHTNESS), int(g * BRIGHTNESS), int(b * BRIGHTNESS))
64
65
66def pixel_index(x, row):
67    # row 0 = верхняя строка
68    if SERPENTINE and row % 2 == 1:
69        return row * W + (W - 1 - x)
70    return row * W + x
71
72
73# ============ СИМУЛЯЦИЯ ОГНЯ ============
74def fire_step(intensity):
75    # intensity 0-1: высота пламени. Высокая = меньше остывания + больше искр
76    cooling = int(115 - 55 * intensity)    # 115 (низкое) -> 60 (высокое)
77    sparking = int(70 + 150 * intensity)   # 70 -> 220 (вероятность)
78    max_cool = (cooling * 10) // H + 2
79
80    for x in range(W):
81        col = heat[x]
82        # Каждая ячейка немного остывает
83        for y in range(H):
84            c = col[y] - random.getrandbits(8) % max_cool
85            col[y] = c if c > 0 else 0
86        # Жар поднимается и рассеивается
87        for y in range(H - 1, 1, -1):
88            col[y] = (col[y - 1] + col[y - 2] + col[y - 2]) // 3
89        # Случайные искры у основания
90        if random.getrandbits(8) < sparking:
91            y = random.getrandbits(1)
92            col[y] = min(255, col[y] + 160 + random.getrandbits(6))
93
94    # 4) Лёгкое смешивание с соседними столбцами: пламя колышется естественнее
95    for y in range(2, H):
96        row_vals = [heat[x][y] for x in range(W)]
97        for x in range(W):
98            left = row_vals[x - 1] if x > 0 else row_vals[x]
99            right = row_vals[x + 1] if x < W - 1 else row_vals[x]
100            heat[x][y] = (row_vals[x] * 2 + left + right) // 4
101
102
103def render():
104    for x in range(W):
105        for y in range(H):
106            matrix[pixel_index(x, H - 1 - y)] = heat_color(heat[x][y])
107    matrix.write()
108
109
110# ============ ЗВУК ПОТРЕСКИВАНИЯ ============
111def crackle(intensity):
112    # Случайные очень короткие «щелчки» = треск дров
113    if not sound_on:
114        return
115    if random.getrandbits(8) < int(10 + 50 * intensity):
116        buzzer.freq(1500 + random.getrandbits(12))   # 1500-5595 Hz
117        buzzer.duty_u16(6000)
118        time.sleep_ms(2 + random.getrandbits(2))
119        buzzer.duty_u16(0)
120
121
122def beep(freq, ms):
123    buzzer.freq(freq)
124    buzzer.duty_u16(20000)
125    time.sleep_ms(ms)
126    buzzer.duty_u16(0)
127
128
129# ============ КНОПКА ============
130btn_state = 1
131btn_raw = 1
132btn_changed = time.ticks_ms()
133press_start = 0
134long_done = False
135
136
137def handle_button():
138    # Короткое нажатие = тема, долгое = звук вкл/выкл
139    global btn_state, btn_raw, btn_changed, press_start, long_done, theme, sound_on
140    now = time.ticks_ms()
141    raw = button.value()
142    if raw != btn_raw:
143        btn_raw = raw
144        btn_changed = now
145    if time.ticks_diff(now, btn_changed) > 25 and raw != btn_state:
146        btn_state = raw
147        if raw == 0:
148            press_start = now
149            long_done = False
150        elif not long_done:
151            theme = (theme + 1) % len(THEME_NAMES)
152            print("Tema / Theme:", THEME_NAMES[theme])
153            beep(1200, 30)
154    if btn_state == 0 and not long_done and time.ticks_diff(now, press_start) > LONG_PRESS_MS:
155        long_done = True
156        sound_on = not sound_on
157        print("Ses / Sound:", "ACIK / ON" if sound_on else "KAPALI / OFF")
158        beep(1600 if sound_on else 500, 80)
159
160
161# ============ ГЛАВНЫЙ ЦИКЛ ============
162print("== BlueGrays · bluegrays.com ==")
163print("Pico 8x8 Kamp Atesi / Campfire")
164print("Pot = alev boyu / flame height | Kisa basis = tema / short press = theme | Uzun basis = ses / long press = sound")
165
166smooth = pot.read_u16() / 65535
167while True:
168    start = time.ticks_ms()
169    handle_button()
170
171    # Сгладить значение потенциометра (дрожание не должно заставлять пламя прыгать)
172    smooth += (pot.read_u16() / 65535 - smooth) * 0.2
173    fire_step(smooth)
174    render()
175    crackle(smooth)
176
177    # Постоянная частота кадров
178    spent = time.ticks_diff(time.ticks_ms(), start)
179    if spent < FRAME_MS:
180        time.sleep_ms(FRAME_MS - spent)
181

Частые вопросы

Почему на реальной схеме нужно уменьшить яркость?

В коде стоит `BRIGHTNESS = 0.4`, потому что Wokwi рисует цвета ровно так, как они отправлены. Реальная матрица при большом пламени может потреблять около 1 A, а порт USB 2.0 даёт 500 mA. При питании от USB поставь 0.15–0.2; для большей яркости питай матрицу от адаптера 5V 2A и соедини земли.

Как изменить высоту и цвет пламени?

Поверни потенциометр влево — получатся тлеющие угли, вправо — пламя достанет до верха матрицы. Короткое нажатие кнопки по кругу переключает «Огонь», «Газ», «Магия» и «Аметист». В Wokwi щёлкни по потенциометру и используй клавиши со стрелками, а тема меняется клавишей T.

Как выключить треск или сделать его реже?

Удерживай кнопку около секунды: высокий сигнал означает, что звук включён, низкий — что выключен. Чтобы треска было меньше, уменьши 10 и 50 в `10 + 50 * intensity` внутри `crackle()`. Для звука используй пассивный зуммер.

Нужно ли устанавливать какие-то библиотеки?

Нет. Модули `neopixel`, `machine` и `random` входят в MicroPython для Pico. Прошей MicroPython на Pico и сохрани на него код как `main.py` в Thonny; тогда огонь будет запускаться сам при каждом включении Pico.