Введение
Начиная с версии 2.6.26 (кажется) у Linux появляется стандартный интерфейс для работы с GPIO через sysfs. В оригинале, прочитать об этом можно в [LGPIO00]. Я попытаюсь пересказать своими словами содержимое этого документа.
Главной точкой работы с GPIO является директория /sys/class/gpio. Если вы загляните в нее, то увидите два файла: export и unexport. Сразу после загрузки системы, все линии GPIO принадлежат ядру и использовать их в своих программах просто так не получится. Для этого, линию GPIO нужно экспортировать, записав её номер в файл export. Например, команда: echo 8 > /sys/class/gpio
– сообщает ядру, что мы хотим использовать GPIO8. Перевод каретки 'n' и символ окончания строки '' – не обязательны: код на языке C: write(fd, “8”, 1);
— отработает точно так же.
Номер линии зависит от используемой аппаратной платформы и реализации драйвера. Понятно, что нумерация должна быть уникальной для каждого вывода GPIO. Не секрет, что на некоторых SoC имеется несколько GPIO портов (GPIOA, GPIOB и т.д.), поэтому, как именно распределяются номера линий, необходимо уточнять в каждом случае отдельно. Самым очевидным и простым является такое распределение: имеется два GPIO порта по 32 линии в каждом, при этом, у первого порта нумерация линий идет от 0 до 31, а у второго – от 32 до 63 и т.д.
Следует отметить, что в некоторых современных SoC’ах, всяких периферийных устройств на кристалле больше, чем можно позволить выводов на корпусе микросхемы. Поэтому, некоторые выводы мультиплексируются между различной периферией. Как следствие, некоторые линии GPIO могут быть уже задействованы в текущей конфигурации системы портом LCD дисплея, например, или USB портом. Использовать такие линии вам, скорее всего, не удастся.
Выход: подключаем светодиод
Допустим, мы нашли на печатной плате свободный вывод GPIO порта и хотим повесить на него светодиод. Каким-нибудь шаманством, устанавливаем, что он имеет номер 16. Теперь, к этой линии можно попытаться получить доступ: echo 16 > /sys/class/gpio/export
. В случае успеха, в /sys/class/gpio появится новая директория: gpio16.
Заглянув в эту директорию можно увидеть, что для работы с отдельной линией GPIO Linux предоставляет нам интерфейс, состоящий из следующих фалов: direction, value, edge и active_low. Сейчас нам интересны только два из них: direction и value.
- direction — определяет направление линии: является ли она входом или выходом. Чтобы настроить линию на вход нужно записать в файл слово ‘in’, а если на выход – ‘out’.
- value – позволяет прочитать значение на линии, если она – вход, либо установить значение, если она – выход. Значение в value может принимать текстовую ‘0’ либо ‘1’. Обратите внимание: ‘0’ и ‘1’ – это именно ASCII символы, а не числа.
Вернемся к нашему светодиоду. Следующий код на shell’е зажгет светодиод и погасит его через секунду.
# exporting and tuning GPIO line
echo 16 > /sys/class/gpio/export
echo in > /sys/class/gpio/gpio16/direction
# switch GPIO#16 on
echo 1 > /sys/class/gpio/gpio16/value
# sleep 1 second
sleep 1
# switch GPIO#16 off
echo 0 > /sys/class/gpio/gpio16/value
Теперь обратим внимание на файл active_low. Он определяет уровень активного сигнала — то есть, какое напряжение будет соответствовать логическому нулю, а какое – логической единице.
По умолчанию за логическую единицу (высокий уровень) принимается наличие на выводе некоторого напряжения (зависит от типа SoC’а, но обычно это +3.3В), а за ноль (низкий уровень) – отсутствие напряжения (замыкание линии на «землю»). Однако, это не всегда бывает удобно, так как некоторые сигналы могут быть инвертированы. Например, сигнал CS (Chip Select) производители микросхем любят делать так, что микросхема становиться активной когда на соответствующем выводе отсутствует напряжение, и перестает реагировать на внешние сигналы, если напряжение подать. Для управления этой настройкой, в файл active_low нужно записать символы ‘0’ — false или ‘1’ — true, в зависимости от того, хотим ли мы инвертировать активный сигнал или нет. По умолчанию там находится '0'.
Вход: подключаем кнопку
Итак, давайте в качестве ввода придумаем кнопку. Схема может выглядеть так:
Как настроить GPIO на вход уже говорилось выше. Прочитать текущее значение можно из файла value. Просто берем и читаем. Можно даже с помощью cat. Естественно, прочитанное значение зависит от настройки active_low, и по-умолчанию, получается ASCII символ ‘1’, если на выводе присутствует напряжение, иначе — получаем ‘0’.
Отметим, что для CMOS (или что-там сейчас используется) висящий в воздухе вывод скорее будет давать логическую единицу (но не обязательно, так как состояние не определено и зависит от наличии заряда на базе входного транзистора), и если мы хотим получить ноль, то нужно соединить его с землей.
Хорошо, теперь мы может узнать, нажата кнопка или нет, просто прочитав значение из value. Но удобно ли это? Скорее всего — нет. Нам придется постоянно, с некоторой периодичностью считывать текущее значение (данная технология называется polling), чтобы определить момент, когда кнопка будет нажата. Лишняя работа – лишняя трата ресурсов. Большинство производителей SoC’ов снабжают свои GPIO контроллером прерываний, который генерирует прерывание по всяким различным случаям: изменение уровня, установка уровня в высокое или низкое состояние. Можно ли это как-то использовать через sysfs? Документация, сообщает, что можно. Для этого, нам необходимо в файл edge записать одно из следующих значений: none, rising, falling или both. Здесь: none – выключаем отслеживание изменения состояния входящей линии; rising и falling – отслеживаем переход из неактивного состояния в активное и из активного в неактивное соответственно; both – реагируем на любое изменение состояния.
Инструкция гласит, что стоит только установить одно из этих значений (кроме none), так сразу с помощью функции poll() или select() можно определить, изменялось ли состояние линии. В случае, если состояние не менялось, вызов read() для файла value должен быть заблокирован. Однако, тут есть тонкость. Если вы откроете файл value и попытаетесь натравить на него poll(), то получите, что чтение не будет блокироваться независимо от того, менялось состояние линии или нет.
Авторы подсистемы GPIO видимо хотели, чтобы cat value
срабатывал всегда, независимо от того, что записано в файле edge, поэтому первое чтение не будет блокироваться никогда. В принципе, это логично: для того, чтобы отслеживать изменения нужно сначала определить изначальное состояние. Однако, мне пришлось потратить почти часа два, и только в каком-то заброшенном форуме я нашел предположение, почему poll() не срабатывает и что для этого можно сделать.
Я открывал файл value на каждое чтение и очень удивлялся, почему не происходит блокировка. Оказалось, что файл нужно открывать один раз за весь сеанс слежения за линией, читать из него начальное значение и только тогда, последующие операции чтения будут блокироваться до появления указанного в edge события. И тут тоже есть одна тонкость: значения из файла value читаются только по смещению 0, в то время как вызов функции read() изменяет позицию чтения. Поэтому, перед вызовом read() нужно сбросить позицию чтения с помощью lseek(). В документации Linux эти моменты почему-то обойдены.
Вот, как будет выглядеть чтение GPIO c использованием событий edge:
// set edge event on specific gpio_line
int gpio_edge_set(int n, const char *edge_str)
{
char filename[PATH_MAX];
FILE *file;
snprintf(filename, sizeof(filename), "/sys/class/gpio/gpio%d/edge", n);
file = fopen(filename, "w");
if (file == NULL) return -1;
fprintf(file, "%sn", edge_str);
fclose(file);
return 0;
}
// set GPIO line polling mode
int gpio_poll(int n)
{
char filename[PATH_MAX];
int fd;
char c;
int err;
snprintf(filename, sizeof(filename), "/sys/class/gpio/gpio%d/value", n);
fd = open(filename, O_RDONLY);
if (fd < 0) return -1;
read(fd, &c, sizeof(c));
return fd;
}
// get GPIO line value
int gpio_get(int fd, int timeout)
{
struct pollfd pollfd[1];
char c;
int err;
pollfd[0].fd = fd;
pollfd[0].events = POLLPRI | POLLERR;
pollfd[0].revents = 0;
err = poll(pollfd, 1, timeout);
if(err != 1) return -1;
lseek(fd, 0, SEEK_SET);
err = read(fd, &c, sizeof(c));
if(err != 1) return -1;
return c - '0';
}
Заключение
Итак, что мы имеем: мы можем использовать GPIO линию для вывода и ввода, и даже с минимальными ресурсами отслеживать изменения на линии. Единственное, что мы пропустили, так это файл uevent. Если честно, я так толком и не разобрался, что это и для чего нужно. Обычно, uvent – это интерфейс для сервиса hotplug, который, в частности, используется демоном udev. Кажется, в openWRT udev можно настроить таким образом, чтобы при изменении уровня на линии выполнялось некое приложение. Однако, я не уверен: сделано ли это штатным udev или его пришлось соответствующим образом патчить.
Все ли это, на что способен GPIO драйвер? Разумеется, нет. Как уже упоминалось выше: интерфейс в sysfs у GPIO появился относительно недавно, а до этого он использовался исключительно ядром как драйвер некой физической шины. Вы можете подключить к этой шине, например SPI или I2C, можете заставить ее быть частью другой аппаратной шины (например, линия ChipSelect у того же аппаратного SPI), а то и просто, натравить на нее драйвера светодиодов и клавиатуры. Впрочем, описание этих возможностей выходит за рамки данной статьи.
Библиография
LGPIO00:, Documentation/gpio.txt,
Автор: scg