Выпуск 18. Август 2014
← Однострочники в Perl | Содержание | Обзор CPAN за июль 2014 г. →Использование портов GPIO в Raspberry Pi. Часть 2
Во второй части статьи рассказано о новой модели B+ и о том, как при работе с Raspberry Pi добиться точности отсчета интервалов порядка одной микросекунды.
Новая модель Raspberry Pi B+
С момента выхода первой части этой статьи разработчики Rasbperry Pi анонсировали и начали продавать обновленную модель своего дивайса, Raspberry B+. В анонсе говорится, что это не новая, а именно обновленная модель. Несмотря на это, изменения очень приятные:
- вместо двух USB-портов теперь четыре;
- слот для SD-карты заменен слотом микро-SD (они называют это USD, видимо выбрав традиционное ASCII-написание греческой буквы
mu
); - разъем видеовыхода совмещен со звуковым (кому вообще может потребоваться видеовыход при наличии HDMI?);
- всесто 26-контактного разъема с портами GPIO P1 теперь установлен 40-контактный;
- вместо двух отверстий для крепления платы теперь четыре :-)
В рамках темы этой статьи самое интересное изменение для нас — увеличение числа выводов GPIO. В новом 40-контактном разъеме J8, который теперь стоит на месте бывшего P1, доступны 26 портов GPIO вместо прежних 17. При этом с платы исчезли контакты для подключения разъема P5, хотя даже с учетом этой потери общее число доступных выводов все равно увеличилось.
Верхняя часть разъема полностью совпадает с разъемом P1 модели B, что позволяет без проблем заменить старое устройство и поставить на его место плату новой модели B+. Разводка нового разъема J8 опубликована на сайте производителя, а новые доступные порты GPIO соответствуют выводам следующим образом:
- GPIO05 — 29
- GPIO06 — 31
- GPIO12 — 32
- GPIO13 — 33
- GPIO16 — 36
- GPIO19 — 35
- GPIO20 — 38
- GPIO21 — 40
- GPIO26 — 37
Все перечисленные GPIO абсолютно новые, а выводы GPIO28—GPIO31, которые были выведены на исчезнувший разъем P5, теперь недоступны.
B+ и libbcm2835
Библиотека libbcm2835 и модуль Device::BCM2835, о которых рассказано в предыдущей части статьи, пользуются константами типа RPI_V2_GPIO_P1_12
, содержащих в своих именах номер разъема (P1 или P5) и номер физического контакта (последние две цифры) соответствующего разъема. При программировании для Raspberry Pi B+ можно по-прежнему применять эти константы, но это неудобно.
Во-первых, они не позволяют обратиться к новым выводам. Констант типа RPI_V2_GPIO_P1_37
не существует, а библиотека по-прежнему не обновлена, и на сегодня доступна версия 1.36, а на официальном форуме не видно никаких обсуждений по поводу того, будет ли это сделано.
Во-вторых, константы для раъема P5 более неактуальны.
Такое положение дел — не причина не использовать новые выводы, а наоборот хороший повод разобраться, что делать. Все константы определены в заголовочном файле bcm2835.h следующим образом:
typedef enum
{
. . .
// RPi Version 2
RPI_V2_GPIO_P1_03 = 2, ///< Version 2, Pin P1-03
RPI_V2_GPIO_P1_05 = 3, ///< Version 2, Pin P1-05
RPI_V2_GPIO_P1_07 = 4, ///< Version 2, Pin P1-07
. . .
} RPiGPIOPin;
Числовое значение констант — это не что иное как номер GPIO, поэтому вместо громоздких имен можно безболезненно подставлять числовые значения. Это не только облагородит код, но и немного уменьшит необходимость во время программирования бесконечного пересчета номеров GPIO в номера физических выводов на плате (хотя от этого не скрыться, когда дойдет дело до подключения реальных устройств).
Например, вместо строк (из которых непонятно, с каким GPIO ведется работа)
Device::BCM2835::gpio_set(RPI_V2_GPIO_P1_12); # Установка в 1
Device::BCM2835::gpio_clr(RPI_V2_GPIO_P1_12); # Сброс в 0
полностью безопасно и корректно писать так (где номер GPIO указан явно):
Device::BCM2835::gpio_set(18); # Установка в 1
Device::BCM2835::gpio_clr(18); # Сброс в 0
При таком подходе появляется возможность программно добраться до всех 26 GPIO, доступных в модели B+. Однако надо проявлять повышенную осторожность, поскольку теперь компилятор (что C, что Perl) не сможет проконтролировать вас, сверяя имена констант с предопределенным списком, и если указать номер несуществующего GPIO (или порта, который имеется в процессоре BCM2835, но не выведен на разъем Raspberry Pi) могут произойти непредвиденные побочные эффекты. Функции библиотеки libbcm2835 не делают никакой проверки переданных аргументов. В моем случаи после попытки записи в порты с неверными номерами Raspberry Pi просто отключалась, к счастью, без физических повреждений.
Работа в реальном времени
В проекте, где я использую Raspberry Pi, мне потребовалось плавно изменять яркость лампы накаливания, причем такой регулятор должен управляться полностью программно и быть многоканальным. В интернетах такого толком ничего не описано: есть много ссылок о том, как регулировать яркость светодиода, парочка видео, где разработчик меняет яркость, передвигая ползунок на экране, но нет никакого описания, наконец, есть одна схема, работающая скорее с Arduino, чем с Raspberry, и одно смешное решение, где компьютер управляет моторчиком, который вращает регулятор яркости.
Кратко регуляторы яркости ламп накаливания (питающихся от сетевого переменного напряжения 220 В) работают так. Одна часть устройства следит за сетевым напряжением и формирует импульс в тот момент, когда оно переходит через нуль. Начиная с этого момента необходимо отсчитать определенное число миллисекунд и подать другой импульс, который включит симистор, который в свою очередь подаст на лампу напряжение. Как только напряжение вновь приблизится к нулю, симистор закроется (сам по себе), и лампа обесточится до следующего программного импульса. Все это происходит дважды за период сетевого напряжения. Его частота 50 Гц, так что управляющий цикл повторяется 100 раз в секунду, а его длина составляет 10 мс. Например, если задержку сделать в 3 мс, то лампа будет светиться примерно на две трети, а если 7 мс, то на четверть.
Казалось бы, при тактовой частоте Raspberry Pi 700 МГц проблем с программным формированием в 700 000 раз более низкочастотных интервалов быть не должно. На практике, однако, все не так. Регулятор яркости работает, но раз в одну-две секунды, а иногда и чаще, лампа помаргивает. Не помогает даже оверклокинг, максимально возможный до 1 ГГц. Я пробовал писать программу и на перле, и на С, но результат остался тем же.
Проблема заключается в том, что Raspberry Pi — это все-таки компьютер с операционной системой, где разрешены и используются прерывания. Работа с сетью, ввод и вывод и кто знает что еще, но в итоге прерывания задерживают работу цикла основной программы, и поэтому часть импульсов, которые должны включить лампу, пропускаются, а другая часть формируется в неправильное время.
Дальше можно пойти тремя путями. Во-первых, попробовать поставить иную операционную систему. Например, среди рекомендованных для Raspberry есть интересная система RISC OS, которая изначально проектировалась с учетом ограниченных возможностей оборудования, но она совсем не Unix-подобная, и как с ней работать, мне осталось неясным. Во-вторых, никто не мешает перейти на Arduino (хоть это и не спортивно) или на UDOO (это более интересно, и есть смысл попробовать). В-третьих, остается разобраться с тем, как избавиться от прерываний.
Регистры контроля прерываний
Контроль за работой прерываний в Raspberry Pi, а точнее, в процессоре BCM2835 описано в седьмой главе «Interrupts» мануала по периферии. Если не вдаваться в подробности, то имеется десяток регистров, доступных по адресам, начиная с 0x2000B000, со следующими смещениями:
- 0x200 — IRQ basic pending
- 0x204 — IRQ pending 1
- 0x208 — IRQ pending 2
- 0x20C — FIQ control
- 0x210 — Enable IRQs 1
- 0x214 — Enable IRQs 2
- 0x218 — Enable Basic IRQs
- 0x21C — Disable IRQs 1
- 0x220 — Disable IRQs 2
- 0x224 — Disable Basic IRQs
На странице Accurate timing for real time control показан пример кода на C, который отключив прерывания, формирует на одном из GPIO сигнал частотой 50 кГц. Этот обнадеживающий код я взял за основу и написал небольшую библиотеку libraspio, которая позволяет программно запрещать и разрешать прерывания. Как только прерывания перестали мешать, удается формировать интервалы времени с точностью до единиц микросекунд, что для моей исходной задачи более чем достаточно.
Вот так выглядит фрагмент функции main()
моей итоговой программы, управляющей светом:
#ifndef DEBUG
disable_interrupts();
#endif
dimmer_loop();
#ifndef DEBUG
enable_interrupts();
#endif
Да, это не перл, но если воспользоваться XS или Inline::C или просто обернуть библиотеку по типу того, как сделан модуль Device::BCM2835, все получится и на перле.
Отдельной строкой замечу, что позже я узнал и про то, что существует пара функций local_irq_disable()
и local_irq_disable()
, доступных практически из коробки, и наверняка есть смысл в следующий раз поэкспериментировать именно с ними.
Важно понимать, что при отключенных прерываниях выводы GPIO остаются полностью управляемыми и работоспособными. Поэтому их можно использовать не только, чтобы выводить сигналы на физические устройства, но и для того, чтобы считывать состояния любых кнопок или датчиков, подключенных к ним. Или даже подключить несколько разрядов GPIO ко второй плате Raspberry, на которой не отключены прерывания, и использовать шину GPIO для передачи команд на ведомую плату с отключенными прерываниями, работающей в режиме реального времени.
Поскольку прерывания более не доступны, временные задержки придется отсчитывать самостоятельно. Полагаться на функции типа sleep
или Device::BCM2835::delay
более нельзя: если их вызвать, то обратно не вернуться, и программа зависнет.
Как работать с регистрами на перле
Процессор BCM2835 содержит внутренний таймер, работающий на частоте 1 МГц, который доступен через регистры, отображаемые на память. В мануале его работа описана в главе 12 «System Timer». В отличие от регистров контроля прерываний, здесь все проще, поэтому я остановлюсь на этом подробнее.
Системное время (время, отсчитываемое с момента подачи питания), хранится в двух 32-разрядных регистрах CLO и CHI, видных, соответственно, по адресам 0x20003004 и 0x20003008. Два беззнаковых регистра обеспечивают 64 бита, что дает возможность отсчитывать время до 2^64 микросекунд, то есть на пятьсот с лишним тысяч лет (32 бит не хватит и на полтора часа).
Прочитать значения системных регистров относительно просто. Для этого в перле удобно воспользоваться функциями модуля Sys::Mmap (он, очевидно, использует POSIX-фукнцию mmap(2)):
my $devmemfh;
my $timer_CLO_reg;
my $timer_CHI_reg;
sysopen($devmemfh, "/dev/mem", O_RDWR|O_SYNC, 0666);
mmap($timer_CLO_reg, 4, PROT_READ, MAP_SHARED, $devmemfh, 0x20003004);
mmap($timer_CHI_reg, 4, PROT_READ, MAP_SHARED, $devmemfh, 0x20003008);
close($devmemfh);
Теперь переменные $timer_CLO_reg
и $timer_CHI_reg
всегда содержат значения из соответствующих регистров системного таймера (эти значения, изменяются без вмешательства программиста, поэтому последовательные чтения могут вернуть разные результаты — поведение, описываемое ключевым словом volatile
в C, C# и Java).
Несмотря на кажущуюся избыточность, я использую все 64 бита, чтобы не получить непрятных сюрпризов в вычислениях после того, как заполнится весь младший регистр. Распаковать число типа unsigned long
, сдвинуть и сложить, на перле это делается так:
my $timer_lo = unpack 'L', $timer_CLO_reg;
my $timer_hi = unpack 'L', $timer_CHI_reg;
my $timer = $timer_lo + ($timer_hi << 32);
Описанная функция реализована в модуле Device::BCM2835::Timer, его использование сводится к вызову функции timer()
:
use Device::BCM2835::Timer;
say Device::BCM2835::Timer::timer();
Другие возможности
Когда не мешают ни прерывания, ни запущенная графическая оболочка, Raspberry Pi становится вполне быстрым и точным устройством, и работать с ним весьма приятно. Помимо описанного выше, GPIO предоставляют и другие интересные возможности для работы в реальном времени, например:
- Формирование широтно-модулированного сигнала (PWM, pulse-width modulation) на выводе GPIO18. Но, к сожалению, такой выход только один, и сделать многоканальное устройство не получится.
- Использование прямого доступа к памяти (DMA, direct memory access), что позволяет с высокой точностью пересылать на выход (опять же, один-единственный) данные, предварительно записанные в некоторую область памяти. В частности, так можно реализовать проигрывание звукового файла в обход процессора.
Самый необычный проект, использующий DMA, — pifm. Это FM-передатчик на Raspberry Pi. Программным путем генерируется сигнал на частоте до 200 МГц, который принимается обычным УКВ-приемником:
sudo ./pifm sound.wav 100.0
Одно из последних обновлений этого проекта (сайт у них давно сломан) позволяло выводить в эфир стереозвук (сам по себе аудиовыход у Raspberry Pi монофонический).
← Однострочники в Perl | Содержание | Обзор CPAN за июль 2014 г. →