Выпуск 17. Июль 2014

Асинхронный ввод/вывод с IO::AIO | Содержание | Обзор CPAN за июнь 2014 г.

Использование портов GPIO в Raspberry Pi. Часть 1

В этой статье рассказано о том, как устроены универсальные порты ввода-вывода (GPIO) и о том, как работать с ними на перле.

Одноплатный компьютер Raspberry Pi примечателен не только своими размером и ценой, но и наличием порта ввода-вывода общего назначения (General Purpose Input-Output, GPIO). Эдакая современная версия параллельного LPT-порта, которая вместе с компьютером занимает меньше места, чем плата PCI с контроллером LPT.

Базовая информация о портах

В базовом комплекте второй версии Raspberry Pi установлен один 26-контактный разьем (он обозначен на плате как P1), в котором доступно 17 портов ввода-вывода (тех самых GPIO). Остальные восемь контактов подключены к земле или к питанию +3,3 В и +5 В. Выводы, отданные под GPIO, могут быть программно переконфигурированы для работы в качестве последовательных портов, вывода с широтно-импульсной модуляцией и еще как-то (вот это все называется альтернативными режимами). Но по умолчанию после включения питания все контакты работают в режиме «один контакт — один бит». Каждый из них может быть либо входом, либо выходом (по умолчанию включен режим ввода).

Кроме упомянутого разъема P1 на плате есть отверстия для установки восьмиконтактного разъема P5, который дает возможность получить еще четыре порта GPIO. Итого в распоряжении программиста оказывается 21 бинарный порт.

Наличие GPIO позволяет относительно легко связывать компьютер с устройствами из реального мира и управлять ими программно. Разумеется, здесь потребуется знание не только программирования, но и хотя бы основ электроники (детям рекоменую книгу В. Т. Полякова «Посвящение в радиоэлектронику», взрослым — книгу П. Хоровица и У. Хилла «Искусство схемотехники»).

При работе с GPIO важно учитывать пару моментов:

  1. Рабочее напряжение всех выводов — 3,3 В. Случайная подача на вход GPIO большего напряжения (даже 5 В с соседнего штырька разъема) приводит к выходу из строя не только этого вывода, но и вообще всего Raspberry Pi (подтверждаю экспериментально).
  2. Контакты разъема P1 и нумерация портов GPIO не совпадает, поэтому при программировании надо всегда помнить, какая из нумераций используется. Еще более они не совпадают в первой версии Raspberry (надеюсь, сейчас, если не прилагать дополнительных усилий, купить удастся только новую модель).
  3. Дополнительным пунктом надо отметить, что нумерация самих GPIO в Raspberry Pi идет с пропусками.
  4. Raspberry Pi построен на ARM-процессоре BCM2835, поэтому иногда полезнее гуглить BCM2835, а не Raspberry GPIO (то же самое действительно для поиска на CPAN).

Хорошее практическое описание опубликовано на странице elinux.org/RPi_Low-level_peripherals.

Для справки, вот так разведены порты GPIO на контакты разъемов P1 и P5 (контакты традиционно обозначаются в формате PX-NN, где X — номер раъема, а NN — двузначный номер контакта):

  1. GPIO02 — P1-03
  2. GPIO03 — P1-05
  3. GPIO04 — P1-07
  4. GPIO07 — P1-26
  5. GPIO08 — P1-24
  6. GPIO09 — P1-21
  7. GPIO10 — P1-19
  8. GPIO11 — P1-23
  9. GPIO14 — P1-08
  10. GPIO15 — P1-10
  11. GPIO17 — P1-11
  12. GPIO18 — P1-12
  13. GPIO22 — P1-15
  14. GPIO23 — P1-16
  15. GPIO24 — P1-18
  16. GPIO25 — P1-22
  17. GPIO27 — P1-13
  18. GPIO28 — P5-03
  19. GPIO29 — P5-04
  20. GPIO30 — P5-05
  21. GPIO31 — P5-06

Каждый битовый порт способен работать в режиме ввода или вывода. Кроме того, в режиме ввода может быть дополнительно включен подтягивающий резистор, что поможет максимально упростить способ подключения выключателей — их достаточно подключить между соответствующим выводом GPIO и общим проводом (если включен резистор pull up) или между GPIO и источником питания +3,3 В (если выход сконфигурирован в режиме pull down).

За более детальными продробностями о внутреннем устройстве портов GPIO я отсылаю читателя к шестой главе «General Purpose I/O (GPIO)» мануала по процессору BCM2835.

Регистры для работы с GPIO

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

Процессор BCM2835 имеет 41 32-разрядный регистр, которые полностью определяют режим и состояние портов GPIO. В частности, для установки единичного значения на выводе, запрограммированном на работу как выход, необходимо записать единичный бит в соответствующий разряд одного из двух регистров установки битов GPIO Pin Output Set Registers (GPSETn). Чтобы установить выход в ноль, следует выставить единичный бит в регистрах сброса битов GPIO Pin Output Clear Registers (GPCLRn). Такая на первый взгляд странная схема позволяет независимо устанавливать и сбрасывать любой бит GPIO без необходимости чтения текущего состояния выводов.

Аналогично, когда разряды GPIO работают на чтение, то узнать уровень входного сигнала можно, прочитав значение одного из двух портов GPIO Pin Level Registers (GPLEVn), каждый бит которого отображает текущее состояние входного разряда.

Программирование портов ввода-вывода

Регистры, отвечающие за работу с GPIO, расположены по адресам 0x7E200000—0x7E2000B0, которые отображаются на физическую память с адресами, начинающимися с 0x20200000. В принципе, в этом месте уже можно было бы начать управлять портами, программируя чтение и запись нужных битов в эти регистры (на перле это вполне возможно, если воспользоваться мапингом переменных на области памяти, используя модуль Sys::Mmap).

Но более практично еще немного усложнить систему, чтобы программировать стало легче.

Модуль Device::BCM2835

Для управления портами ввода-вывода на перле удобно воспользоваться модулем Device::BCM2835. Он является Perl-оберткой над C-библиотекой того же автора bcm2835 и один в один повторяет все ее функции (поэтому документацию может оказаться полезнее почитать в оригинале).

Установка библиотеки bcm2835 не вызывает сложностей:

./configure
make
sudo make install

Равно как и модуль со CPAN:

perl Makefile.PL
make
sudo make install

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

Все дальнейшие действия необходимо выполнять от имени суперпользователя.

Перед началом работы следует вызвать функцию init():

use Device::BCM2835;
Device::BCM2835::init() || die "Could not init library";

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

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

bcm2835_init: Unable to open /dev/mem: Permission denied

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

use Device::BCM2835;
. . .
Device::BCM2835::gpio_fsel(
    &Device::BCM2835::RPI_V2_GPIO_P1_05,
    &Device::BCM2835::BCM2835_GPIO_FSEL_OUTP
);

записывать:

use Device::BCM2835 ':all';
. . .
Device::BCM2835::gpio_fsel(RPI_V2_GPIO_P1_05, BCM2835_GPIO_FSEL_OUTP);

Вывод

Чтобы переключить один из разрядов порта GPIO для работы в режиме вывода, надо вызвать функцию установки режима gpio_fsel($pin, $mode) и передать ей номер вывода (константу, соответствующую номеру физического вывода нужного разъема) и режим (другую определенную константу). Например, чтобы перевести выход P1-12 в режим вывода, следует выполнить следующее:

Device::BCM2835::gpio_fsel(RPI_V2_GPIO_P1_12, BCM2835_GPIO_FSEL_OUTP);

Запись нуля или единицы выполняют, либо вызывая функцию gpio_write($pin, $value):

Device::BCM2835::gpio_write(RPI_V2_GPIO_P1_12, 1);
Device::BCM2835::gpio_write(RPI_V2_GPIO_P1_12, 0);

Либо используя пару функций gpio_set($pin) и gpio_clr($pin), которым достаточно передать номер вывода:

Device::BCM2835::gpio_set(RPI_V2_GPIO_P1_12); # Установка в 1
Device::BCM2835::gpio_clr(RPI_V2_GPIO_P1_12); # Сброс в 0

Обратите внимание, что в обоих примерах физический вывод P1-12 (RPI_V2_GPIO_P1_12) является логическим выходом GPIO18. А еще обратите внимание на наличие V2 в именах констант. Для первой версии Raspberry Pi следует использовать константы типа RPI_GPIO_P1_12, которые за парой исключений совпадают с вариантами с V2. Но следует иметь в виду, что порт P5 доступен только во второй версии, например: RPI_V2_GPIO_P5_04 для GPIO29.

Ввод

Чтение данных с портов ввода-вывода также прост, как и запись в них. Прежде всего, необходимо перевести соответствующие разряды GPIO в режим чтения (BCM2835_GPIO_FSEL_INPT):

Device::BCM2835::gpio_fsel(RPI_V2_GPIO_P1_10, BCM2835_GPIO_FSEL_INPT);

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

# Подтягиваем к питанию
Device::BCM2835::gpio_set_pud(RPI_V2_GPIO_P1_10, BCM2835_GPIO_PUD_UP);

# Подтягиваем к земле
Device::BCM2835::gpio_set_pud(RPI_V2_GPIO_P1_10, BCM2835_GPIO_PUD_DOWN);

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

Чтобы прочитать значение со входа, достаточно вызвать функцию gpio_lev($pin), передав ей номер нужного входа:

say Device::BCM2835::gpio_lev(RPI_V2_GPIO_P1_10);

Функция возвращает либо нуль, либо единицу.

Примеры

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

Код из первого примера раз в секунду включает и выключает светодиод, подключенный (через резистор сопротивлением 300…1000 Ом) к выводу GPIO03 (контакт P1-05).

use v5.12;
use Device::BCM2835;

Device::BCM2835::init() || die "Could not init library";

Device::BCM2835::gpio_fsel(RPI_V2_GPIO_P1_05, BCM2835_GPIO_FSEL_OUTP);

while (1) {
    Device::BCM2835::gpio_set(RPI_V2_GPIO_P1_05);
    Device::BCM2835::delay(500);

    Device::BCM2835::gpio_clr(RPI_V2_GPIO_P1_05);
    Device::BCM2835::delay(500);
}

Обратите внимание на использование функции Device::BCM2835::delay($n) из того же модуля, которая выполняет задержку на $n миллисекунд. Доступна и функция Device::BCM8235::delayMicroseconds($n) для задержки в микросекундах. Обе, однако, не гарантируют точности отсчитанного времени. На практике следует быть осторожным, если требуется получить более или менее точные задержки меньше 20-50 миллисекунд.

Второй пример включает тот же светодиод при нажатии на кнопку, подключенную ко входу GPIO15 (P1-10).

use v5.12;
use Device::BCM2835;

Device::BCM2835::init() || die "Could not init library";

Device::BCM2835::gpio_fsel(RPI_V2_GPIO_P1_05, BCM2835_GPIO_FSEL_OUTP);

Device::BCM2835::gpio_fsel(RPI_V2_GPIO_P1_10, BCM2835_GPIO_FSEL_INPT);
Device::BCM2835::gpio_set_pud(RPI_V2_GPIO_P1_10, BCM2835_GPIO_PUD_UP);

while (1) {
    Device::BCM2835::gpio_write(
        RPI_V2_GPIO_P1_05, 
        !Device::BCM2835::gpio_lev(RPI_V2_GPIO_P1_10)
    );
    Device::BCM2835::delay(50);
}

Здесь интересно отметить, что в отличие от предыдущего примера удобнее воспользоваться функцией gpio_write, а не парой gpio_set и gpio_clr.

Андрей Шитов


Асинхронный ввод/вывод с IO::AIO | Содержание | Обзор CPAN за июнь 2014 г.
Нас уже 1380. Больше подписчиков — лучше выпуски!

Комментарии к статье

Чат