MicroPython Skill Builders — #3 Светодиоды и процедуры

MicroPython Skill Builders #3 Светодиоды и процедуры

Это третий выпуск серии MicroPython Skill Builders от Тони Гудью, цель которой — улучшить ваши навыки программирования на MicroPython, попутно знакомя вас с новыми компонентами и техниками написания кода — с использованием Raspberry Pi Pico!

В этом выпуске Тони продемонстрирует использование процедур на примере 10-сегментного светодиодного индикатора.

Не читали предыдущие выпуски? Ознакомьтесь с первым и вторым эпизодами, в которых рассматривается использование циклов и списков в MicroPython.

Что вам понадобится

Предполагается, что вы уже установили Thonny на свой компьютер и настроили Raspberry Pi Pico с актуальной прошивкой MicroPython (UF2). Если нет, ознакомьтесь с руководством по началу работы с Raspberry Pi Pico, где это описано подробно.

Также предполагается, что вы прочитали и прошли первые два руководства серии (см. ссылки выше, если нет).

Вам понадобится:

  • Raspberry Pi Pico с контактными штырями

  • Макетная плата полного размера

  • Кабель Micro-USB (для питания и программирования Pico)

  • Набор соединительных проводов папа-папа и папа-мама

  • Потенциометр 10 кОм

  • 10 токоограничивающих резисторов (330 Ом)

  • Тактовые кнопки

  • 10 светодиодов ИЛИ 10-сегментный светодиодный индикатор

Начальная точка

Мы продолжим работу с той же схемой со светодиодным индикатором, что и в предыдущем руководстве. Если вам нужны инструкции по сборке схемы, вернитесь ко второму руководству серии.

Схема подключения индикатора

Демонстрация процедур

Давайте сразу перейдём к примеру кода, который покажет несколько процедур в действии.

Когда схема готова, скопируйте приведённый ниже код в Thonny, запустите его, а затем прочитайте ниже объяснение того, как он работает:

# Демонстрирует ПРОЦЕДУРЫ
# 10-сегментный светодиодный индикатор или 10 светодиодов
# Автор: Tony Goodhew, 2 мая 2023

# Светодиоды с резисторами 330 Ом на контактах GP2–GP11
# Кнопка на GP15 - Pull Down
# Потенциометр 10 кОм на ADC0 = GP26

# Импорт библиотек
from machine import Pin, ADC
import time
import random

# Настройка кнопки на GP 15 с Pull Down
button = Pin(15,Pin.IN, Pin.PULL_DOWN)

# Настройка светодиодов с помощью списка
leds = [] # Пустой список

for i in range(10):
    led = Pin(2 + i, Pin.OUT) # Настройка 1 светодиода
    leds.append(led)          # Добавление нового светодиода в список
    leds[i].value(0)          # Выключить его

# ====== Начало определений ======
def on(): # Включить ВСЕ светодиоды
    for i in range(10):
        leds[i].value(1)

def off(): # Выключить ВСЕ светодиоды
    for i in range(10):
        leds[i].value(0)

def odd(): # Включить светодиоды с НЕЧЁТНЫМИ номерами
    for i in range(0,10,2):
        leds[i].value(1)

def even(): # Включить светодиоды с ЧЁТНЫМИ номерами
    for i in range(1,10,2):
        leds[i].value(1)

def on_n_L(n): # Включить n светодиодов слева
    for i in range(n):
            leds[i].value(1)

def on_n_R(n): # Включить n светодиодов справа
    for i in range(n):
            leds[9-i].value(1)

def flash(pp, delay): # pp — указатель на светодиод, delay — время включения
    leds[pp].value(1) # Включить светодиод
    time.sleep(delay)
    leds[pp].value(0) # Выключить светодиод

def random_LEDs(n,delay): # Мигнуть n случайными светодиодами
    for c in range(n):
        p = random.randint(1,9)
        flash(p,delay)

# ========= Конец определений =========
# Мигать всеми светодиодами
while button.value() == 0:
    on()
    time.sleep(0.3)
    off()
    time.sleep(0.3)

while button.value() == 1:continue

# Покачивание дисплея светодиодов
while button.value() == 0:
    odd()
    time.sleep(0.2)
    off()
    even()
    time.sleep(0.2)
    off()

while button.value() == 1:continue

random_LEDs(30,0.2)

on_n_R(4) # 4 светодиода включены у ПРАВОГО конца индикатора
time.sleep(0.9)
off()
on_n_L(2) # светодиоды включены у ЛЕВОГО конца индикатора
time.sleep(0.9)
off()

Первый блок программы настраивает список светодиодов и кнопку для управления циклами — это мы разбирали в двух предыдущих руководствах, поэтому повторять не будем.

Следующий блок определяет несколько простых процедур. Но что они из себя представляют?

Что такое процедура в MicroPython?

Процедура — это набор инструкций для выполнения одной задачи.

Она начинается с ключевого слова def, за которым следуют имя процедуры, пара скобок и двоеточие. Строки с отступом после строки def — это обычный код с отступами, такой же, какой мы пишем в цикле.

Возьмём первую процедуру из демонстрационного кода выше в качестве примера. Она очень простая и просто запускает цикл, включающий светодиоды по одному с максимальной скоростью:

def on(): # Включить ВСЕ светодиоды
    for i in range(10):
        leds[i].value(1)

Это стандартная простая задача со светодиодами, которую теперь можно запрограммировать одной инструкцией on(), не прописывая код каждый раз.

Аналогично можно определить ещё более полезную процедуру off(), чтобы выключать все светодиоды:

def off(): # Выключить ВСЕ светодиоды
    for i in range(10):
        leds[i].value(0)

Больше вариаций этого паттерна вы найдёте в нашем демонстрационном коде. odd() включает светодиоды с нечётными номерами, а even() — только чётные (мы разберём, как они работают, в другой раз, когда подробнее рассмотрим функцию range).

Переменные внутри процедур

Следующие процедуры из нашей демонстрации включают переменную в части в скобках. Это называется параметром и используется для внесения изменений внутри процедуры при каждом её вызове.

def on_n_L(n): # Включить n светодиодов слева
    for i in range(n):
            leds[i].value(1)

Она позволяет включить нужное количество светодиодов, начиная с левого конца индикатора. Посмотрим на пример вызова:

on_n_L(3)

Это включит только три левых светодиода на индикаторе.

Параметры и аргументы в MicroPython

Когда мы вызываем процедуру с аргументом (или значением) между скобками, это значение используется как параметр при выполнении процедуры:

  • Параметр — это переменная, указанная внутри скобок в определении функции

  • Аргумент — это значение, которое передаётся в функцию при её вызове

Хороший пример этого — оператор print(). Когда вы используете print(), вы используете процедуру. В примере ниже print — это имя процедуры, а «Answer is» и x — аргументы:

print("Answer is", x)

Последние два определения имеют параметры и при вызове потребуют передачи аргументов:

  • def flash(pp, delay):

  • def random_LEDs(n,delay):

Использование наших определений

В нашем примере кода, ниже определений, находится ОСНОВНАЯ часть программы, которая вызывает заранее определённые процедуры для выполнения полезных задач (помните, что определения ничего не делают сами по себе — их нужно вызвать в коде):

  • Мигать всеми светодиодами до нажатия кнопки

  • «Качать» светодиоды до нажатия кнопки

  • Случайным образом зажигать светодиоды по всему индикатору

  • Зажигать ряд светодиодов с любого конца

Ещё один пример определений в MicroPython

Давайте добавим ещё несколько определений и немного изменим код, чтобы программа делала с светодиодами что-то новое!

Добавьте эти процедуры в конец блока определений:

def zero_L(delay):    # 1.1.......
    leds[0].value(1)
    leds[2].value(1)
    time.sleep(delay)

def zero_R(delay):    # .......1.1
    leds[7].value(1)
    leds[9].value(1)
    time.sleep(delay)

Эти новые определения добавляют два светодиодных паттерна для обозначения нуля на каждом конце индикатора.

Чтобы использовать эти определения, нам нужно их вызвать. Замените основной блок кода в конце программы приведённым ниже кодом, затем сохраните программу как proc_counting1.py:

# ========= Конец определений =========
wait = 0.7
# Считаем от 0 до 35 на светодиодном индикаторе
for count in range(36):
    tens = count // 10
    units = count % 10
    print(tens, units) # Только для проверки
    if tens == 0:
        zero_L(wait)
        off()
    else:
        on_n_L(tens)
        time.sleep(wait)
    off()
    if units == 0:
        zero_R(wait)
        off()
    else:
        on_n_R(units)
        time.sleep(wait+0.2)
    off()
    time.sleep(wait)
    # Нажмите кнопку, чтобы выйти из цикла
    if button.value() == 1:
        print("Остановлено кнопкой")
        break

Запустите программу! Она считает от нуля до 35 довольно необычным способом: два горящих светодиода с пробелом между ними обозначают ноль — но это работает. Обратите внимание, как мы извлекаем значения десятков и единиц из значения count с помощью // и %.

Почему процедуры так полезны?

  • Повторное использование — Если у вас есть проверенная процедура для выполнения конкретной задачи, вы можете с уверенностью использовать её в других программах, не «изобретая велосипед» заново и не перетестируя код. Просто скопируйте и вставьте из старой программы в новую и вызовите её.

  • Структура и ясность — Вы можете разбить очень сложную задачу на серию более мелких задач — процедур, которые можно проектировать, писать и тестировать отдельно. Затем они объединяются вместе и вызываются по очереди из основной программы.

  • Иерархия — Вы можете выстроить иерархию процедур, где процедуры высокого уровня вызывают процедуры низкого уровня. Мы продемонстрируем это в следующей программе.

Иерархия процедур в MicroPython

Здесь мы создаём процедуру высокого уровня def show_number(z): для отображения двузначных значений на 10-сегментном светодиодном индикаторе. Для этого она вызывает процедуры низкого уровня.

Вернёмся к примеру кода в Thonny. Измените конец программы так, чтобы он выглядел следующим образом:

def zero_R(delay):
    leds[7].value(1)
    leds[9].value(1)
    time.sleep(delay)

def show_number(z): # Показать 1 число: Диапазон: от 0 до 99 включительно
    wait = 1
    tens = z // 10
    units = z % 10
    print(tens, units) # Только для проверки
    if tens == 0:
        zero_L(wait)
        off()
    else:
        on_n_L(tens)
        time.sleep(wait)
    off()
    time.sleep(0.2)
    if units == 0:
        zero_R(wait)
        off()
    else:
        on_n_R(units)
        time.sleep(wait+0.2)
    off()
    time.sleep(wait)

# ========= Конец определений =========
# Список тестовых значений
test_values = [33, 19, 0, 20, 13, 91, 6, 24, 66, 99]

for v in test_values:
    show_number(v)
    time.sleep(0.5)

    # Нажмите кнопку, чтобы досрочно выйти из цикла
    if button.value() == 1:
        print("Остановлено кнопкой")
        break

print("Готово")

На что стоит обратить внимание

  • Мы можем использовать список для предоставления тестовых значений. При тестировании нового кода всегда полезно включать граничные значения: ноль и 99 в данном случае.

  • for v in test_values: извлекает значения из списка и настраивает цикл, не требуя от нас знания длины списка.

Что попробовать

Почему бы не попробовать использовать датчик температуры вместе с процедурами для отображения уровня температуры через светодиоды?

  • Если у вас есть датчик температуры, совместимый с DS18B20, подключите его к GP1 и протестируйте. Они достаточно точны, водонепроницаемы и очень полезны.

  • В противном случае смоделируйте набор показаний температуры (от 0 до 42 — возможно, нас ждёт ещё одно жаркое лето!) с помощью списка и протестируйте свой код.

  • Создайте процедуру для отображения температуры с помощью нескольких вспышек на каждом конце светодиодного индикатора. 14 градусов — это одна вспышка слева и 4 вспышки справа. 7 градусов — это просто 7 вспышек справа, а десятки — одна вспышка слева. Подсказка: def flash_temp(t): — это первая строка определения процедуры.

  • Как насчёт чисел выше 99?

  • Как можно обозначить отрицательную температуру? Добавьте ваше решение в код.

В следующем руководстве мы рассмотрим использование другого типа подпрограмм — функции, и устраним незначительную проблему при чтении значений, близких к нулю, с помощью до трёх потенциометров на 10 кОм.

Об авторе

Эта статья написана Тони Гудью. Тони — учитель информатики на пенсии, начавший писать код ещё в 1968 году, когда это называлось программированием — он начинал с FORTRAN IV на IBM 1130! Активный участник сообщества Raspberry Pi, его главные интересы сейчас — программирование на MicroPython, путешествия и фотография.