Программирование, радиоэлектроника,
саморазвитие и частичка из моей жизни здесь...

RPi.GPIO - работа с входами, выходами и прерываниями в Raspberry Pi, простые примеры

Познакомимся с возможностями модуля RPi.GPIO - разберемся с использованием входов и выходов GPIO в Raspberry Pi, узнаем как использовать прерывания и широтно-импульсную модуляцию (PWM). Приведу простые примеры программ на языке Python, которые помогут выполнить первые эксперименты и понять как все это работает.

Кроме того, из статьи вы узнаете много другой полезной информации, которая может быть полезна для понимания принципов построения программ, написанных специально для Raspberry Pi.

Во всех примерах программ будет использован Python 3-й версии (python3). Для его установки достаточно одной команды в консоли Raspbian:

sudo apt-get install python3

Содержание:

  1. Возможности RPi.GPIO
  2. Установка модуля, узнаем его текущую версию
  3. Режимы нумерации пинов, настройка каналов
  4. Подтягивающие резисторы (Pull-Up, Pull-Down resistors)
  5. Установка и чтение состояний каналов
  6. Сброс (очистка) состояний каналов
  7. Чтение состояния канала (пример с кнопкой)
  8. Установка состояния канала (пример со светодиодом)
  9. Прерывания - что это такое и зачем они нужны
  10. Обнаружение порога срабатывания (Rising/falling edge)
  11. Мультипоточный обратный отклик (threaded callback)
  12. Что такое "дребезг контактов" и как его перебороть
  13. Широтно-импульсная модуляция (PWM)
  14. Узнаем текущий установленный режим для GPIO каналов
  15. В завершение

Возможности RPi.GPIO

RPi.GPIO - модуль на языке Python, который предназначен для управления каналами GPIO в мини-компьютере Raspberry Pi. Важно заметить, что данный модуль нельзя использовать в приложениях реального времени и там, где время выполнения и реакции является одним из важнейших факторов.

Операционная система GNU/Linux является мультизадачной, поэтому какой-либо другой запущенный процесс может отобрать приоритет по ресурсам микропроцессора (CPU) и таким образом, повлиять на появление задержек в выполнении работающей с GPIO программы на Python. Это нужно учитывать при проектировании времязависимых устройств и приложений.

Для проектов, где временный фактор является критичным, лучше использовать AVR микроконтроллеры или что-то в этом духе. К тому же, выполненный на основе микроконтроллера модуль, можно связать с Raspberry Pi и тем самым получить или переслать ему какие-то данные.

Основные возможности модуля RPi.GPIO:

  • Считывание состояния каналов, сконфигурированных на вход (input);
  • Реакция на прерывания, инициируемые любым из каналов в режиме input;
  • Управление состоянием каналов, сконфигурированных на выход (output);
  • PWM (Pulse-Width Modulation) - широтно-импульсная модуляция на каналах в режиме output.
  • Получение информации о платформе и конфигурации пинов.

В одной из своих статей я уже рассказывал об использовании GPIO, там приведено расположение выводов (пинов, pins) на гребенке малинки, а также было продемонстрировано как правильно подключить кнопку и светодиод. В описанных там программах также был использован модуль RPi.GPIO. Здесь мы рассмотрим его очень подробно.

Расположение пинов GPIO на платформе Raspberry Pi

Рис. 1. Расположение пинов GPIO на платформе Raspberry Pi.

Установка модуля, узнаем его текущую версию

Свежую версию модуля RPi.GPIO для Python всегда можно найти на страничке - https://pypi.python.org/pypi/RPi.GPIO

Там же есть описание всех изменений (Change Log) для разных версий пакета, список текущих возможностей и другие полезные данные.

Получим свежий список доступных к установке пакетов из репозитория для Raspbian:

sudo apt-get update

Установим модуль RPi.GPIO для Python версий 3.х и 2.х (если нужно):

sudo apt-get install python3-rpi.gpio
sudo apt-get install python-rpi.gpio

Чтобы узнать текущую установленную версию модуля достаточно выполнить следующую команду:

dpkg --status python3-rpi.gpio | grep Version

Результат будет выглядеть примерно вот так:

Version: 0.6.3~stretch-1

Также, версию модуля можно узнать используя консольный командный интерпретатор Python:

python3

В командном интерфейсе Python вводим следующие команды:

import RPi.GPIO
print(RPi.GPIO.VERSION)

Получим версию модуля одной строчкой:

0.6.3

Для получения информации о платформе из модуля RPi.GPIO введите в интерпретаторе следующую команду:

RPi.GPIO.RPI_INFO

Из вывода можно узнать ревизию и тип платы, количество оперативной памяти, название процессора (пример для Raspberry Pi 3 model B):

{'P1_REVISION': 3, 'TYPE': 'Pi 3 Model B', 'MANUFACTURER': 'Sony', 'RAM': '1024M', 'REVISION': 'a02082', 'PROCESSOR': 'BCM2837'}

Еще одна полезная команда в интерпретаторе, которая позволит вывести на экран содержимое объекта RPi.GPIO:

dir(RPi.GPIO)

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

Пример вывода команды:

['BCM', 'BOARD', 'BOTH', 'FALLING', 'HARD_PWM', 'HIGH', 'I2C', 'IN',
'LOW', 'OUT', 'PUD_DOWN', 'PUD_OFF', 'PUD_UP', 'PWM', 'RISING', 
'RPI_INFO', 'RPI_REVISION', 'SERIAL', 'SPI', 'UNKNOWN', 'VERSION', 
'__builtins__', '__cached__', '__doc__', '__file__', '__loader__', 
'__name__', '__package__', '__path__', '__spec__', 'add_event_callback', 
'add_event_detect', 'cleanup', 'event_detected', 'getmode', 
'gpio_function', 'input', 'output', 'remove_event_detect', 'setmode', 
'setup', 'setwarnings', 'wait_for_edge']

Для выхода из интерпретатора Python 3 вводим команду "exit()".

Все команды, которые мы вводили в консоли интерпретатора, можно использовать в будущих программах на Python.

Режимы нумерации пинов, настройка каналов

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

import RPi.GPIO as GPIO

Теперь нужно определиться с предпочитаемой системой нумерации пинов GPIO, здесь есть два варианта:

  1. по названию каналов, например: 7 (IO7), 21 (IO21)
  2. по номеру пина на гребенке коннектора GPIO, например: 26 (IO7), 40 (IO21)

Для первого варианта инициализация выполняется следующей строчкой кода:

GPIO.setmode(GPIO.BCM)

Для второго варианта:

GPIO.setmode(GPIO.BOARD)

 Получить текущий режим нумерации пинов можно методом "getmode":

pin_mode = GPIO.getmode()   # значением может быть "GPIO.BOARD", "GPIO.BCM" или None

Прежде чем приступить к считыванию и установке уровней на каналах и соответствующих им пинах малинки, нужно выполнить их изначальную конфигурацию (setup). Установка режимов работы каналов выполняется одноименным методом "setup", например установим 7-й канал на вход, а 21-й на вывод:

GPIO.setup(7, GPIO.OUT)
GPIO.setup(21, GPIO.IN)

"GPIO.OUT" и "GPIO.IN" - это константы модуля "RPi.GPIO", содержащие следующие значения: GPIO.OUT = 0, GPIO.IN = 1.

Проверить значения этих констант можно запустив интерпретатор "python3" (как в примере выше) и набрав в его консоли следующую последовательность команд:

import RPi.GPIO as GPIO
GPIO.IN
GPIO.OUT

Зная значения констант можно переписать пример с установкой режимов для пинов 7 и 21 вот так:

GPIO.setup(7, 0)
GPIO.setup(21, 1)

Согласитесь, с такой записью очень легко запутаться, особенно при работе с большим количеством пинов и в большой программе. Поэтому использование констант "GPIO.OUT" и "GPIO.IN" - это более удобное и верное решение.

Для установки начального состояния сконфигурированного на вывод канала (высокий или низкий уровень) можно добавить параметр "initial" с указанием нужного уровня. Например:

GPIO.setup(7, GPIO.OUT, initial=GPIO.HIGH)
GPIO.setup(8, GPIO.OUT, initial=GPIO.LOW)

"GPIO.HIGHT" и "GPIO.LOW" - это константы модуля "RPi.GPIO", которые содержат эквивалентные значения для состояний каналов:

  • HIGH - число 1, высокий уровень (+3,3В);
  • LOW - число 0, низкий уровень (0В).

Также есть возможность установить один и тот же режим работы для списка из нескольких каналов:

GPIO.setup([7, 21, 8], GPIO.OUT)

Подтягивающие резисторы (Pull-Up, Pull-Down resistors)

К каждому из пинов GPIO, сконфигурированным на вход, могут быть подключены внутренние подтягивающие резисторы (Pull-Up, Pull-Down resistors). Они могут пригодиться для установки значения по умолчанию на канале, который соответствует нужному пину. Этот пин может быть никуда не подключен и иметь неопределенное состояние (плавающее, float state).

На таком "плавающем" пине могут хаотично возникать разные состояния (то 0, то 1), что недопустимо в случае если к нему подключена какая-то кнопка или другое подобное устройство, состояние которого планируется отслеживать.

Чтобы "приземлить" этот пин (установить по умолчанию значение канала в 0) или же подтянуть его к высокому уровню (значение 1) можно воспользоваться параметром "pull_up_down".

Например:

GPIO.setup(7, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(21, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

Допустим что к пину, который соответствует каналу 7, подключена кнопка, другой же вывод кнопки подключен через резистор к земле, как на схеме:

Схема подключения к GPIO кнопки, используя внутренний подтягивающий резистор (Pull-Up)

Рис. 2. Схема подключения к GPIO кнопки, используя внутренний подтягивающий резистор (Pull-Up).

Для отслеживания состояния кнопки SW1, канал GPIO IO7 установим в режим ввода (input) и включим подтягивающий к высокому уровню (Pull-Up) резистор Rb. Пока кнопка SW1 не нажата, на пин соответствующий каналу IO7, через подтягивающий резистор поступает напряжение 3,3В и при считывании состояния этого канала мы получим 1 (высокий уровень).

После нажатия кнопки, уровень напряжения (которое поступает через подтягивающий резистор) на пине канала IO7 очень сильно упадет, состояние канала станет - 0 (низкий уровень).

Возможно кто-то спросит: Зачем здесь резистор R1, ведь можно обойтись и без него? - можно, но я считаю что это не безопасно. Сам встречал уже не раз примеры, в которых кнопку подключают напрямую к пинам GPIO и GND - не рекомендую так поступать.

В момент включения малинки и загрузки ОС, а также при не верно сконфигурированном канале для пина GPIO, при переходных процессах и в других случаях, этот пин может быть кратковременно выставлен как выходной (output) и на нем может появиться как высокий, так и низкий уровень.

Если же в этот момент кнопка нажата (такое совпадение мало вероятно, но все же) или же замкнут переключатель (вполне вероятно), то вывод пина GPIO, сконфигурированный на выход (output), будет напрямую подключен к земле (GND). В данном случае достаточно кратковременного появления на этом пине высокого уровня, чтобы спалить GPIO и повредить плату Raspberry Pi.

Если же подобное случится с включенным последовательно кнопке резистором на 300 Ом, то через пин GPIO потечет ток, равный:

I = U/R = 3,3V/300R = 0,011A = 11mA.

Такой ток не причинит никакого вреда каналу GPIO. После загрузки ОС, исправления возможных ошибок в конфигурации и других переходных процессах, все будет работать исправно и канал GPIO останется исправным.

Также можно подсчитать напряжение на входе IO7 (рисунок 2) в момент, когда кнопка SW1 нажата и включен Pull-UP резистор Rb. Используем формулу для расчета резистивного делителя напряжения, который состоит в данном случае из резисторов Rb и R1 и с входным напряжением 3,3В. 

Искомое значение "x" - это напряжение между GND и IO7 в момент, когда включен резистор Rb и нажата кнопка SW1.

R1 = Rb / (3,3V/x - 1), 
300 Ом = 10000 Ом / (3,3V/x - 1),
3,3V/x - 1 = 10000/300 = 33.33333,
3,3V/x = 33.33333 + 1 = 34.33333,
x = 3.3/34.33333 =~ 0.096V.

Как видим, сопротивления резистора R1 в 300 Ом должно хватить чтобы напряжение 3,3В, приходящее через резистор Rb, просело примерно до 0,1В и таким образом, удалось получить низкий уровень на IO7.

Установка и чтение состояний каналов

Для получения состояния сконфигурированного на вход канала служит метод "input". Считать текущее значение 7-го канала в переменную ch7_state можно вот так:

ch7_state = GPIO.input(7)

В зависимости от уровня сигнала на канале, переменная будет содержать одно из чисел:

  • 0 - низкий уровень, соответствует 0В на пине GPIO;
  • 1  - высокий уровень, +3,3В на пине GPIO.

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

if GPIO.input(21):
    ... ваш код
if GPIO.input(21) == GPIO.HIGH:
    ... ваш код

А теперь установим высокий уровень на 7-м канале, который сконфигурирован на вывод (output):

GPIO.output(7, GPIO.HIGH)

Установку одного и того же уровня на множестве каналов  можно выполнить с использованием списка:

GPIO.output([7, 21, 8], GPIO.HIGH)

Или так (используя список, вынесенный в отдельный объект):

leds_rgb = [7, 21, 8]
GPIO.output(leds_rgb, GPIO.HIGH)

Сброс (очистка) состояний каналов

По завершению работы программы, в которой были установлены значения каналов GPIO, желательно выполнить сброс состояний каналов к значениям по умолчанию. Выполнить очистку (cleanup) состояний пинов можно следующим методом:

GPIO.cleanup()

Данная команда выполнит сброс всех использованных в программе (скрипте) каналов в режим по умолчанию - Input (Вход). Важно заметить, что также будет сброшен текущий режим нумерации пинов и будут отключены все подтягивающие резисторы (Pull-UP, Pull-Down) для каналов, которые были использованы в программе. Очистку состояний каналов желательно выполнять по завершению работы каждой программы.

Для очистки состояния одного или нескольких указанных каналов можно воспользоваться следующими вариантами команды:

GPIO.cleanup(7)
GPIO.cleanup([7, 21])

В процессе работы скрипта при выполнении какого-то блока программы можно наткнуться на исключение (Exception). В случае его возникновения, выполнение скрипта прервется и соответственно очистка состояний пинов выполнена не будет, программа не дойдет до завершающих операторов и в конечном итоге к вызову "cleanup". В такой программе нужно учитывать возможность возникновения исключений и постараться отловить их с помощью операторов "try + except".

Допустим что выполнение программы может быть прервано нажатием сочетания клавиш CTRL+C на клавиатуре, в таком случае для уверенного сброса состояний каналов GPIO можно использовать следующую конструкцию:

try:
    #... какой-то код программы
except KeyboardInterrupt:
    GPIO.cleanup()

# Конец программы
GPIO.cleanup()

Чтение состояния канала (пример с кнопкой)

В этом эксперименте мы будем проверять состояние кнопки в бесконечном цикле, нажимать ее и наблюдать за изменением вывода программы. Для этого примера подключим кнопку к каналу IO12, в Raspberry Pi 3 model B ему соответствует вывод (пин) номер 32.

Также, для безопасного подключения, используем два внешних резистора, как на схеме (рисунок 6) из моей первой статьи про GPIO в Raspberry Pi.

Схема соединений для эксперимента с чтением состояния GPIO канала, к которому подключена кнопка

Рис. 3. Схема соединений для эксперимента с чтением состояния GPIO канала, к которому подключена кнопка.

Откроем для редактирования будущий файл скрипта:

nano ~/rpi-gpio-input-switch-test.py

Скопируем в редактор следующий исходный код:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 
# Raspberry Pi RPi.GPIO test: input mode + switch
# https://ph0en1x.net

import RPi.GPIO as GPIO
from time import sleep

GPIO.setmode(GPIO.BCM) 
GPIO.setup(12, GPIO.IN)

try:
    while True:
        if GPIO.input(12):
            print('IO12 = HIGH')
        else:
            print('IO12 = LOW')
        sleep(0.1)
except KeyboardInterrupt:
    GPIO.cleanup()

GPIO.cleanup()

Для выхода из редактора "nano" в GNU/Linux нужно нажать сочетание клавиш CTRL+X и потом подтвердить сохранение изменений в файле клавишей "Y".

Запускаем скрипт:

python3 ~/rpi-gpio-input-switch-test.py

В консоли начнут появляться строчки "IO12 = HIGH", с частотой 10 раз в секунду (с задержкой 0,1с). При нажатии кнопки появится строчка "IO12 = LOW", которая свидетельствует о том, что на канале IO12 появился низкий уровень.

Для выхода из программы достаточно прервать ее выполнение комбинацией клавиш CTRL+C.

Если активировать внутренний Pull-UP резистор для канала IO12, то резистор с сопротивлением 10К можно исключить из схемы. Для активации внутреннего подтягивающего к высокому уровню резистора нужно изменить строчку "GPIO.setup(12, GPIO.IN)" следующим образом:

GPIO.setup(12, GPIO.IN, pull_up_down=GPIO.PUD_UP)

Установка состояния канала (пример со светодиодом)

Суть эксперимента - заставить светодиод мигать, поочередно меняя состояния напряжения на пине (0В и 3,3В), который закреплен за каналом IO12. Подключение светодиода выполним через гасящий резистор сопротивлением 330 Ом (можно также использовать 470 Ом, 620 Ом).

 gpio-on-raspberry-pi-output-test-circuit

Рис. 4. Подключение светодиода к каналу GPIO12.

Готовим новый файл для скрипта:

nano ~/rpi-gpio-output-switch-test.py

Код программы:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 
# Raspberry Pi RPi.GPIO test: output mode + LED
# https://ph0en1x.net

import RPi.GPIO as GPIO
from time import sleep

GPIO.setmode(GPIO.BCM)
GPIO.setup(12, GPIO.OUT)

try:
    while True:
        GPIO.output(12, GPIO.HIGH)
        sleep(0.1)
        GPIO.output(12, GPIO.LOW)
        sleep(0.1)
except KeyboardInterrupt:
    GPIO.cleanup()

GPIO.cleanup()

Запускаем программу:

python3 ~/rpi-gpio-output-switch-test.py

Светодиод должен замигать. Частоту его мигания можно изменить, указав другое время задержки (в секундах) в командах "sleep(0.1)".

Прерывания - что это такое и зачем они нужны

Из примера с кнопкой, который был показан выше, видно что программа ожидает изменения состояния на пине, при этом постоянно и с некоторой задержкой по времени считывая значение состояния его канала командой "input".

Такой метод контроля состояния объекта еще часто называют "polling" (в переводе с английского "опрос"), по сути это процесс продолжительной проверки чего-то, в данном случае - постоянная проверка состояния канала, к которому подключена кнопка.

Это далеко не лучший метод контроля событий на пинах GPIO, он подходит для несложных программ, где зачастую выполняются 1-2 какие-то простые операции, в которых нет конкуренции за приоритет исполнения.

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

К тому же, кратковременное нажатие кнопки может быть проигнорировано, если этот момент совпадет по времени с выполняющейся между опросами состояния канала командой задержки "sleep". Можно конечно уменьшить таймаут между запросами, но это в свою очередь увеличит количество вызовов команды "input" и соответственно потребует дополнительных ресурсов CPU.

Зачем выполнять сотни и тысячи запросов в ожидании нажатия кнопки, теряя при этом время на задержки между запросами? Как и чем заменить поллинг? - здесь на помощь приходят прерывания.

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

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

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

Использование прерываний для GPIO - это способ ожидания и реакции на изменение состояния канала, без необходимости постоянной и многократной проверки его состояния.

Механизм прерываний был добавлен в библиотеку RPi.GPIO начиная с версии 0.5.0а, появились новые доступные функции:

  • add_event_detect();
  • remove_event_detect();
  • add_event_callback();
  • wait_for_edge().

Каждый пин GPIO в Raspberry Pi, установленный в режиме входа (input), может сконфигурирован как источник для вызова прерывания.

Теперь перейдем описанию и примерам использования прерываний для GPIO в Raspberry Pi с применением модуля RPi.GPIO.

Обнаружение порога срабатывания (Rising/falling edge)

Данный метод основан на использовании низкозатратной по ресурсам функции, которая ожидает смены уровня сигнала на канале, применяя для этого систему прерываний. В примере будет использован специальный метод модуля "RPi.GPIO" - "wait_for_edge".

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

Для этого примера используем ту же самую схему подключения кнопки, которая показана на рисунке 3.

nano ~/rpi-gpio-interrupt-edge-test.py

Код программы:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 
# Raspberry Pi RPi.GPIO test: interrupt wait-for-edge
# https://ph0en1x.net

import RPi.GPIO as GPIO
from time import sleep

GPIO.setmode(GPIO.BCM)
GPIO.setup(12, GPIO.IN)

try:
    print('Waiting for IO12 state cahnges ...')
    GPIO.wait_for_edge(12, GPIO.FALLING)  
    print('State change detected.')
except KeyboardInterrupt:
    GPIO.cleanup()

GPIO.cleanup()

 Запуск скрипта:

python3 ~/rpi-gpio-interrupt-edge-test.py

Скрипт выведет на экран сообщение об ожидании изменения состояния IO12. Как только кнопка, подключенная к этому каналу, будет нажата - на экране появится сообщение о том, что зафиксировано изменение состояния канала - "state change detected".

Waiting for IO12 state cahnges...
State change detected.

В этом примере мы ожидаем наступления события по смене уровня сигнала с высокого на низкий - "FALLING" (с англ. - "падение"). Если нужно отслеживать событие со сменой уровня с низкого на высокий (Rising, возрастание), то нужно изменить вызов метода "GPIO.wait_for_edge" вот так:

GPIO.wait_for_edge(12, GPIO.RISING)

В таком случае, прерывание будет активировано если на пине, который соответствует каналу 12, напряжение сменится с низкого уровня на высокий. В примере с кнопкой - это будет момент ее отпускания.

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

Мультипоточный обратный отклик (threaded callback)

Threaded callback (обратная связь в потоке) - специальная функция, которая выполняется в отдельном потоке и следит за наступлением ожидаемого события.

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

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

nano ~/rpi-gpio-interrupt-threaded-callback.py

Программный код:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 
# Raspberry Pi RPi.GPIO test: interrupt threaded-callback
# https://ph0en1x.net

import RPi.GPIO as GPIO
from time import sleep

GPIO.setmode(GPIO.BCM)
GPIO.setup(12, GPIO.IN)

def test_callback(channel):
    print('Event detected.')

print('Waiting for IO12 state cahnges ...')
GPIO.add_event_detect(12, GPIO.FALLING, callback=test_callback)

try:
    while True:
        sleep(2)
        print('.')
except KeyboardInterrupt:
    GPIO.cleanup()

GPIO.cleanup()

Запуск:

python3 ~/rpi-gpio-interrupt-threaded-callback.py

Рассмотрим код программы. После установки канала 12 на вход, создается небольшая функция, при вызове которой в консоль будет выведено сообщение "Event detected" (событие наступило).

С помощью метода "GPIO.add_event_detect" мы как бы приставляем к каналу 12 сторожевого песика (watchdog) и даем ему следующую инструкцию: "если заметишь смену уровня сигнала с высокого на низкий (FALLING) на этом канале - дай об этом знать, запусти функцию указанную в параметре callback - test_callback".

Дальше, мы оставляем песика наедине с его работой (поток 2) и идем заниматься своими делами (поток 1) - будем в бесконечном цикле "while True" через каждые 2 секунды выводить на консоль символ точки ".".

Заметьте, что поток 2 создается методом "add_event_detect", вызванным из потока 1, где мы перед выполнением своей работы (вывод символа точки) выполняем вызов этого метода.

Запустив программу, попробуйте понажимать на кнопочку - сможете наблюдать примерно следующую картину:

.
Event detected.
.
Event detected.
Event detected.
Event detected.
.
.
Event detected.
.
Event detected.
Event detected.
Event detected.
Event detected.
.
Event detected.
Event detected

Как видите, в момент ожидания (2 секунды) между выводом символа точки появляются сообщения о фиксации события "Event detected", при этом "точки" как выводились через две секунды, так и продолжают это делать без какой-либо блокировки выполнения.

Возможно вы спросите: почему при нажатии кнопки бывает что появляется сразу два и больше сообщения от "песика"? - причиной этому является так называемый "дребезг контактов" в используемой кнопке, подробнее о этом явлении и методам его подавления будет рассказано в следующем разделе.

Теперь, давайте сделаем так, чтобы "песик" посторожил некоторое время, а потом ушел со своего поста. По сути, нам нужно с некоторой задержкой прекратить обработку прерывания на канале 12, для этого будем использовать метод "GPIO.remove_event_detect" с указанием нужного канала.

Откроем новый файл:

nano ~/rpi-gpio-interrupt-threaded-callback-remove.py

Исходный код программы для этого примера:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 
# Raspberry Pi RPi.GPIO test: interrupt threaded-callback-rm
# https://ph0en1x.net

import RPi.GPIO as GPIO
from time import sleep

GPIO.setmode(GPIO.BCM)
GPIO.setup(12, GPIO.IN)

def test_callback(channel): 
    print('Event detected on IO=' + str(channel))

print('Waiting for IO12 state cahnges ...')
GPIO.add_event_detect(12, GPIO.FALLING, callback=test_callback)

try:
    for i in range(0,10):
        sleep(2)
        print('.')
        if i == 3:
            GPIO.remove_event_detect(12)
except KeyboardInterrupt:
    GPIO.cleanup()

GPIO.cleanup()

 Запуск скрипта на исполнение:

python3 ~/rpi-gpio-interrupt-threaded-callback-remove.py

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

  • Функция "test_callback" выводит сообщение с номером канала, для которого она была вызвана;
  • Выводим символ точки с задержкой 2 секунды в цикле ровно 10 раз;
  • После вывода трех точек отключаем обработку раньше установленного прерывания на канале 12.

Теперь, нажимая кнопку вы также сможете видеть сообщения из функции "test_callback", но после появления третьей точки с новой строки функция перестанет реагировать на нажатие кнопки.

.
.
Event detected on IO=12
Event detected on IO=12
.
.
.

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

Для реализации подобного сценария нужно использовать связку из методов "add_event_detect" + "add_event_callback".

nano ~/rpi-gpio-interrupt-threaded-callback-2-func.py

Исходный код для примера:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import RPi.GPIO as GPIO
from time import sleep

GPIO.setmode(GPIO.BCM)
GPIO.setup(12, GPIO.IN)

x = 0

def callback_func_1(channel):
    print('Interupt on ' + str(channel))

def callback_func_2(channel):
    global x 
    x = x + 1

GPIO.add_event_detect(12, GPIO.FALLING)
GPIO.add_event_callback(12, callback_func_1)
GPIO.add_event_callback(12, callback_func_2)

try:
    while True:
        sleep(1)
        print(str(x))
except KeyboardInterrupt:
    GPIO.cleanup()

GPIO.cleanup()

 Запуск:

python3 ~/rpi-gpio-interrupt-threaded-callback-2-func.py

В начале программы объявляем переменную х, а также две новые функции, одна из которых печатает на экран сообщение "Interrupt on IO=X" (где Х-номер канала), а вторая - изменяет ранее объявленную глобальную переменную "x".

Методом "add_event_detect" запускаем механизм работы с прерываниями на канале 12, а с помощью методов "add_event_callback" выполняется привязка ранее объявленных функций к прерыванию на том же указанном канале.

Пример вывода программы:

0
Interupt on IO=12
Interupt on IO=12
2
2
Interupt on IO=12
Interupt on IO=12
4
Interupt on IO=12
Interupt on IO=12
Interupt on IO=12
Interupt on IO=12
Interupt on IO=12
Interupt on IO=12
10
Interupt on IO=12
Interupt on IO=12
12
12
Interupt on IO=12
Interupt on IO=12
14

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

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

Что такое "дребезг контактов" и как его перебороть

Дребезг контактов (switch bounce) - это процесс возникновения многократных, кратковременных неконтролируемых замыканий/размыканий в момент соединения контактов кнопок, переключателей и других механических коммутационных устройств.

Причина дребезга контактов - упругость материалов, в следствии чего случается кратковременное отталкивание одного из контактов в момент соприкосновения с другим. Как пример подобного явления можно привести попытку быстро столкнуть вместе два металлических шара.

Способы борьбы с дребезгом контактов:

  • использование RS-триггера;
  • программная задержка;
  • установка RC-цепочки;
  • установка RC-цепочки + программная задержка.

Применение RS-триггера (Reset-Set trigger)  при борьбе с дребезгом контактов целесообразно если используется кнопка с тремя контактами. Здесь же рассмотрим использование программной задержки по времени, а также попробуем установить дополнительный керамический конденсатор к уже имеющейся схеме с кнопкой и резисторами.

Схема подключения кнопки к GPIO (нажатие кнопки - смена уровня с высокого на низкий) с дополнительным конденсатором для подавления дребезга контактов

Рис. 5. Схема подключения кнопки к GPIO (нажатие кнопки - смена уровня с высокого на низкий) с дополнительным конденсатором для подавления дребезга контактов.

Схема подключения кнопки к GPIO (нажатие кнопки - смена уровня с низкого на высокий) с дополнительным конденсатором для подавления дребезга контактов

Рис. 6. Схема подключения кнопки к GPIO (нажатие кнопки - смена уровня с низкого на высокий) с дополнительным конденсатором для подавления дребезга контактов.

Если рассматривать пример из рисунка 3, то керамический конденсатор емкостью 100 нФ (0,1мкФ) нужно установить между входом GPIO и GND (земля, минус). Соберем схему соединений по рисунку 5 (нажатие кнопки - смена уровня с высокого на низкий) и подключим ее к GPIO.

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

Рис. 7. Схема подключения конденсатора к GPIO для подавления дребезга контактов в кнопке.

При вызове метода "add_event_detect" дополнительно укажем ему параметр "bouncetime", которому присвоим числовое значение в миллисекундах. Теперь, при изменении уровня на канале 12, на протяжении указанного отрезка времени будет выполняться подавление дребезга контактов.

nano ~/rpi-gpio-interrupt-threaded-callback-bouncetime.py

Код скрипта:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 
# Raspberry Pi RPi.GPIO test: interrupt threaded-callback + bouncetime
# https://ph0en1x.net

import RPi.GPIO as GPIO
from time import sleep

GPIO.setmode(GPIO.BCM)
GPIO.setup(12, GPIO.IN)

def test_callback(channel):
    print('Event detected.')

print('Waiting for IO12 state cahnges ...')
GPIO.add_event_detect(12, GPIO.FALLING, callback=test_callback, bouncetime=300)

try:
    while True:
        sleep(2)
        print('.')
except KeyboardInterrupt:
    GPIO.cleanup()

GPIO.cleanup()

Старт:

python3 ~/rpi-gpio-interrupt-threaded-callback-bouncetime.py

Теперь, кратковременное нажатие на кнопку будет иметь уверенную реакцию. В примере вывода, что ниже, кнопка была нажата 3 раза на протяжении 2х секунд, потом два раза, а после этого еще по разу.

Waiting for IO12 state cahnges ...
.
.
Event detected.
Event detected.
Event detected.
.
Event detected.
Event detected.
.
Event detected.
.
Event detected.
.

Широтно-импульсная модуляция (PWM)

Широтно-импульсная модуляция (ШИМ) или PWM (Pulse-Width Modulation) -  способ управления подводимой к нагрузке мощностью, при котором питание подается импульсами с постоянной частотой, но при этом с переменной длительностью этих импульсов.

Для ШИМ характерны такие величины как "скважность" и "коэффициент заполнения".

  • Скважность - отношение периода импульса к его длительности.
  • Коэффициент заполнения (Duty Cycle) - обратная к скважности величина.

Данные величины являются безразмерными и очень часто при использовании обозначаются в процентах (%).

Примеры различного коэффициент заполнения для импульсов с одинаковой частотой

Рис. 8. Примеры различного коэффициента заполнения для импульсов с одинаковой частотой.

Диаграмма прямоугольного сигнала, которая изображена по середине рисунка (Duty Cycle = 50% = 0,5), в электронике имеет специальное название - "меандр".

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

Например, ШИМ используется в системах подсветки дисплеев LED-телевизоров, ноутбуков и смартфонов - частота импульсов при подаче питания на светодиоды достаточно большая, поэтому человеческий глаз не видит мерцания, в зависимости от длительности этих импульсов свечение делается темнее или ярче.

Для реализации ШИМ в модуле RPi.GPIO существует специальный метод "GPIO.PWM", с помощью которого можно создать управляемый объект для генерации ШИМ-импульсов на выбранном канале GPIO.

# Создаем объект 'pwm' для работы с PWM на 7-м канале,
# частота импульсов - 100Гц.
pwm = GPIO.PWM(7, 100)

# Запускаем генерацию импульсов на канале 
# с начальным коэффициентом заполнения 50%.
pwm.start(50)

# Изменяем частоту импульсов на 150Гц.
pwm.ChangeFrequency(150)

# Изменяем коэффициент заполнения на 90%.
pwm.ChangeDutyCycle(90)

# Останавливаем генерацию импульсов на канале.
pwm.stop()

Чтобы поэкспериментировать с PWM в Raspberry Pi вы можете собрать схему со светодиодом, подключенным к одному из выводов GPIO - например для 12-го канала, как на рисунке 4.

Откроем файл для создания нового скрипта:

nano ~/rpi-gpio-pwm-test.py

Исходный код скрипта:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 
# Raspberry Pi RPi.GPIO test: PWM with LED
# https://ph0en1x.net

import RPi.GPIO as GPIO
from time import sleep

GPIO.setmode(GPIO.BCM)
GPIO.setup(12, GPIO.OUT)

pwm = GPIO.PWM(12, 50)
pwm.start(0)

try:
    pwm.ChangeDutyCycle(50)
    input('F=50Hz, DC=50%. Press Enter...')
    pwm.ChangeDutyCycle(20)
    input('F=50Hz, DC=20%. Press Enter...')
    pwm.ChangeFrequency(10)
    pwm.ChangeDutyCycle(80)
    input('F=10Hz, DC=80%. Press Enter...')
    pwm.ChangeDutyCycle(10)
    input('F=10Hz, DC=10%. Press Enter to exit...')
except KeyboardInterrupt:
    pass

pwm.stop()
GPIO.cleanup()

Запускаем программу:

python3 ~/rpi-gpio-pwm-test.py

После запуска программы светодиод, подключенный к каналу IO12, начнет мигать с частотой (F) - 50Гц. Если смотреть на этот светодиод и потом медленно переместить поле зрения на какой-то объект, находящийся не далеко от светодиода, то вы возможно заметите небольшое мелькание.

Нажав "ENTER" яркость светодиода понизится (DC=20%), а частота (F) останется такой же как и раньше. При последующем нажатии этой же клавиши, будет изменена частота (на 10Гц) и коэффициент заполненности (на 80%) - светодиод начнет заметно мигать, при этом яркость его свечения будет увеличена.

Еще раз нажав "ENTER" мы уменьшим коэффициент заполненности до 10%, яркость свечения резко упадет. Ну и последнее нажатие этой же клавиши завершит выполнение программы - светодиод погаснет (канал IO12 будет сконфигурирован на вход благодаря "GPIO.cleanup").

Вместо светодиода можно подключить собранный на транзисторе мощный электронный ключ или же H-мост для управления электродвигателем постоянного тока. Таким образом можно будет управлять скоростью вращения двигателей, которые приводят в движение какой-то объект или систему, например - самодельного робота и т.п.

Узнаем текущий установленный режим для GPIO каналов

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

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

Для демонстрации я использовал свой самодельный роутер на базе Raspberry Pi, в котором задействованы пины шины I2C. Запустил на нем в консоли интерпретатор python3, выполнил следующие команды и получил соответствующие им результаты:

>>> import RPi.GPIO as GPIO
>>> GPIO.setmode(GPIO.BOARD)
>>> GPIO.gpio_function(3)
42
>>> GPIO.gpio_function(5)
42
>>> GPIO.I2C
42
>>> GPIO.gpio_function(40)
1

Перед вызовами метода "gpio_function" здесь произведена инициализация нумерации пинов по номеру на гребенке платы (GPIO.BOARD).

Как видим, переданные методу "gpio_function" номера пинов 3 и 5 заставили его вернуть число 42. Если попробовать отыскать соответствующую этому числу константу из модуля "RPi.GPIO", то увидим что это "GPIO.I2C". Из этого можно понять, что данные пины задействованы для шины I2C.

Для пина с номером 40 метод вернул число 1, которое соответствует константе модуля "GPIO.IN", а это в свою очередь означает что пин установлен в режиме на вход (input).

Список некоторых констант модуля "RPi.GPIO" и их значения:

  • GPIO.IN = 1
  • GPIO.OUT = 0
  • GPIO.SPI = 41
  • GPIO.I2C = 42
  • GPIO.HARD_PWM = 43
  • GPIO.SERIAL = 40
  • GPIO.UNKNOWN = -1.

Полученное от метода значение можно присвоить переменной, пример для пина 5:

pin_config = GPIO.gpio_function(5)

Аналогичным способом можно проверить конфигурацию пина на соответствие какому-то из значений в константах, например проверим пин номер 24 на предмет участия его в интерфейсе SPI:

if GPIO.gpio_function(24) == GPIO.SPI:
    print('Used for SPI interface.')

В завершение 

Как видите, нет ничего сложно в использовании модуля RPi.GPIO - все очень просто и удобно. Теперь вы с уверенностью и пониманием сможете строить самые разные проекты на основе Raspberry Pi, в которых используются выводы GPIO. С помощью модуля "RPi.GPIO" и не сложной программы можно генерировать различные по параметрам сигналы, а также выполнять анализ сигналов на любом из доступных каналов GPIO.

Полезные ссылки:

1 429 Железо