MicroPython Skill Builders — #4 Функции с потенциометрами

MicroPython Skill Builders — Функции с потенциометрами

Добро пожаловать в четвёртый выпуск серии MicroPython Skill Builders, автором которой является Тони Гудхью. Цель серии — улучшить навыки программирования на MicroPython, попутно знакомя вас с новыми компонентами и техниками программирования — с использованием Raspberry Pi Pico! В этом выпуске Тони расскажет, что такое функция в MicroPython, зачем она нужна, и покажет несколько примеров применения функций, в том числе — как очищать показания потенциометра.

Нужно наверстать? Загляните в раздел наших обучающих материалов за предыдущими выпусками.

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

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

Мы также предполагаем, что вы прочитали/прошли предыдущие руководства серии (см. ссылку выше).

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

  • Raspberry Pi Pico с контактными гребёнками

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

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

  • Набор перемычек «папа-папа» и «папа-мама» (доступен смешанный набор)

  • Тактильные кнопки (доступен смешанный набор)

  • 3× потенциометр 10 кОм со встроенной ручкой

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

  • 3× светодиод или 10-сегментный светодиодный индикатор (линейка)

Наша отправная точка

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

Схема подключения линейного светодиодного индикатора с Pico

Что такое функции?

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

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

  • Функция — это блок кода, который мы вызываем для выполнения задачи и возврата значений

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

Простой пример функции

Очень простой пример функции — преобразование температуры из градусов Цельсия (°C) в Фаренгейт (°F).

В расчёте мы умножаем °C на 9, делим на 5 и прибавляем 32. Вот как это выглядит в виде функции:

def c_to_f(t):
    f = c * 9 / 5 + 32
    return f

test_temperatures = [0,100,20,39, -40]
for c in test_temperatures:
    f = c_to_f(c)  # Вызов функции
    print(c,"°C equals",f,"°F")

Эта короткая программа выдаст следующий результат:

0 °C equals 32.0 °F

100 °C equals 212.0 °F

20 °C equals 68.0 °F

39 °C equals 102.2 °F

-40 °C equals -40.0 °F

Более сложный пример функции

Следующая программа использует более сложную функцию для вычисления нескольких значений из случайного списка целых чисел. Попробуйте запустить её, а затем читайте объяснение:

# Найти длину, максимум, минимум и среднее из списка
import random

# ==== Определения =========
def get_stats(q):
    minn = 999
    maxx = -999
    total = 0
    l = len(q)
    for v in q:
        total = total + v
        if v < minn:
            minn = v
        if v > maxx:
            maxx = v
    mean = total / l
    return l, mean, maxx, minn
# ==== Конец определений ========

# Генерируем значения
data = []
size = random.randint(7,25)  # Случайное количество элементов от 7 до 25
for i in range(size):
    r = random.randint(-25,100)
    data.append(r)

# Анализируем список
items, average, largest, smallest = get_stats(data)

# Печатаем результаты
print("No of items: ", items)
print("Average: ", average)
print("Smallest: ", smallest)
print("Largest: ", largest)
print(data)

Один из запусков программы дал следующий результат:

No of items: 14

Average: 42.21429

Smallest: -12

Largest: 97

[24, 89, -3, 32, 76, 20, 58, 14, 22, 84, -12, 97, 69, 21]

Как это работает

Мы используем встроенную функцию len(listname) для нахождения длины списка.

Для вычисления минимального значения в списке переменной minn присваивается очень большое значение, 999. Мы проходим по списку, и каждый раз, когда находим значение меньше текущего в minn, заменяем его.

Для вычисления максимального значения в списке переменной maxx присваивается очень маленькое значение, -999. Мы проходим по списку, и каждый раз, когда находим значение больше текущего в maxx, заменяем его.

Для получения среднего значения мы суммируем все элементы списка и делим на количество элементов в списке.

Использование функций для улучшения аналоговых показаний

Мы воспользуемся потенциометром, чтобы показать, как функции могут «очищать» аналоговые показания.

Хорошее введение в аналоговые входы и выходы было дано в нашей статье «День #4 Maker Advent Calendar», поэтому если вы не уверены, что такое аналоговый сигнал и на что он способен, сначала прочитайте ту статью.

Мы используем схему с потенциометром и начальный код для получения некоторых значений, а затем применим функцию для их улучшения.

Простое считывание потенциометра (без функций)

Мы можем продолжить работу с базовой схемой и запустить простую программу с потенциометром (со стандартными недостатками аналогового считывания, которые затем улучшим с помощью функций).

Запустите следующую программу и поворачивайте ручку потенциометра, чтобы изменять яркость светодиода на выводе GP2. Нажмите кнопку, чтобы остановить программу.

Некоторые вещи, которые вы заметите:

  • Яркость меняется при повороте ручки

  • Светодиод тускнеет, но не гаснет полностью в нижнем положении, так как показание никогда не достигает нуля (обычные минимальные значения — 300–500)

  • В верхнем положении он гаснет или мерцает между выключенным состоянием и очень яркими показаниями 65535

  • При нажатии кнопки код очистки выключает ШИМ и светодиод аккуратно гаснет

# Управление яркостью светодиода потенциометром
# 10-сегментный светодиодный индикатор или 1 светодиод на GP2
# Автор: Tony Goodhew, 5 мая 2023
# Кнопка на GP15 — Pull Down
# Потенциометр 10 кОм на ADC0 = GP26

# Подключаем библиотеки
from machine import Pin, ADC, PWM
import time

# Настраиваем кнопку на GP15 с Pull Down
button = Pin(15, Pin.IN, Pin.PULL_DOWN)

# Настраиваем потенциометр на аналоговом выводе 26, ADC0
potentiometer = ADC(Pin(26))
led = PWM(Pin(2))  # Настраиваем 1 светодиод для ШИМ

reading = 0
while button.value() == 0:  # Нажмите кнопку для выхода из цикла
    reading = potentiometer.read_u16()  # Считываем значение потенциометра
    print(reading)  # Выводим значение
    # Устанавливаем рабочий цикл ШИМ светодиода равным значению потенциометра
    # Рабочий цикл регулирует соотношение время ВКЛ : время ВЫКЛ
    led.duty_u16(reading)
    time.sleep(0.2)  # Короткая задержка

# Очистка
# Настраиваем светодиод как выход и выключаем — без ШИМ
led = Pin(2, Pin.OUT)
led.value(0)

Улучшение показаний потенциометра с помощью функций

Давайте попробуем более удачную программу, использующую функции для очистки показаний и исправления перечисленных выше недостатков.

Следующие диапазоны могут быть полезны для начала:

  • Полный 16-битный диапазон: от 0 до 65535 — регулировка яркости светодиода — значение 0 выключит его правильно.

  • Проценты: от 0 до 100

  • Значения в байте: от 0 до 255

  • Указатель или индекс для ряда светодиодов: от 0 до 8 и более

  • Симулированные температуры: от -45 до 110

Следующая программа показывает, как этого можно достичь путём перемасштабирования и корректировки со смещением. Она работает со всеми тремя выводами АЦП. Попробуйте код, а затем читайте дальше, чтобы понять, как он работает:

# Перемасштабированные значения потенциометра
# == Tony Goodhew для thepihut.com == 8 мая 2023 ==
# Кнопка на GP15
# Потенциометры 10 кОм на ADC0, ADC1 и ADC2, если нужно (GP26 -> GP28)
# Светодиод на GP2 с резистором 330 Ом

# Подключаем библиотеки
from machine import ADC, Pin, PWM
import time

# Настраиваем кнопку на GP15 как ВХОД с PULL_DOWN
button = Pin(15, Pin.IN, Pin.PULL_DOWN)

# Настраиваем АЦП списком
adcs = []  # Пустой список выводов АЦП
for i in range(3):
    adc = ADC(26 + i)  # Выводы GP26, GP27 и GP28
    adcs.append(adc)  # Добавляем новый вывод АЦП в список

led = PWM(Pin(2))  # Настраиваем 1 светодиод для ШИМ-вывода
led.freq(1000)

def pot_adj(adc, minn, maxx):
    # Функция для перемасштабирования показания потенциометра
    pot_min = 800      # Считаем нижние показания за 0
    pot_range = 65535 - pot_min
    req_range = maxx - minn
    # Считываем «сырое» значение потенциометра с указанного АЦП
    pot = adcs[adc].read_u16()
    # Перемасштабируем
    result = ((pot - pot_min) * req_range / pot_range) + minn
    result = int(result)  # Убеждаемся, что это целое число
    # Корректируем крайние точки при необходимости
    if result < minn:    # Нижний предел — устанавливаем minn, если слишком мало
        result = minn
    if result > maxx:    # Верхний предел — устанавливаем maxx, если слишком велико
        result = maxx
    return result  # Возвращаем результат в основную программу

# === Цикл до нажатия кнопки ===
while button.value() == 0:  # Останавливаем цикл кнопкой
    # Получаем значение потенциометра (adc, minn, maxx)
    v = pot_adj(0, 0, 65534)
    print(v)
    led.duty_u16(v)  # Яркость светодиода (0 - 65534)
    time.sleep(0.2)

# Очистка
# Настраиваем светодиод как выход и выключаем — без ШИМ
led = Pin(2, Pin.OUT)
led.value(0)

Как это работает

Есть 3 параметра (adc, minn, maxx):

  • adc — указатель вывода АЦП: 0 = GP26, 1 = GP27, 2 = GP28

  • minn — минимальное требуемое значение в нижнем положении при повороте потенциометра

  • maxx — максимальное требуемое значение в верхнем положении

Мы вызываем функцию и получаем ответ одной инструкцией:

v = pot_adj(0, 0, 65534)  # Получаем значение потенциометра (adc, minn, maxx)

Это сохраняет ответ в v. Внутри функции мы используем пропорцию для приведения «сырого» показания потенциометра к нужному размеру, а затем применяем смещение для получения правильного минимального значения:

# Перемасштабируем
result = ((pot - pot_min) * req_range / pot_range) + minn
result = int(result)  # Убеждаемся, что это целое число — целое число

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

Вот несколько идей для самостоятельных экспериментов. Попробуйте, сломайте код, исправьте и учитесь!

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

  • Закомментируйте строку led.duty_u16(v) # Яркость светодиода (0 - 65534) и попробуйте изменять значения аргументов для изменения требуемых максимального и минимального значений, например: (0, 0, 100), (0, 0, 10), (0, 20, 75), (0, -40, 40) — теперь мы можем получить надёжный ноль и значения, уходящие в отрицательную сторону, если необходимо.

  • Переключите провод потенциометра и протестируйте другие выводы АЦП — GP27 и GP28. Измените первый аргумент соответствующим образом.

  • Используйте потенциометр на ADC0 для регулировки времени включения/выключения мигающего светодиода (значения задержки от 0 до 0,75 секунды).

  • Используйте дополнительный потенциометр для изменения яркости того же светодиода во время его мигания.

Заключительные мысли

Мы считаем, что проблема с 65535, выключающим светодиод, является переполнением в MicroPython. Просто используйте 65534 в качестве максимума в led.duty_u16(v), чтобы добиться максимального свечения светодиода.

Если мы пишем длинную программу, в которой нам нужно получать значения с нескольких выводов АЦП в разных местах и с разными диапазонами, мы просто вызываем функцию и не нужно вставлять этот фрагмент кода снова. Это экономит место и делает код значительно легче для чтения, особенно если вы используете осмысленные имена для своих функций.

Разбивка больших проектов на серию проверенных процедур и функций значительно упрощает написание кода и снижает вероятность ошибок. Вы сможете повторно использовать их в новых проектах по мере развития ваших навыков — просто копируйте и вставляйте между программами. Мы будем использовать эту функцию в будущих уроках.

Об авторе

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