GPIO для чайников (Часть 2).

2
В этой части мы научимся читать из порта GPIO.

В первой части мы научились включать и выключать наш светодиод. Это самое элементарное и простое, на что способен порт GPIO. Но даже это самое простое уже можно использовать в своих целях. За примером далеко ходить ненужно. Например, на плате Raspberry светодиоды управляются точно так же, через подключение к GPIO. Просто на разъём GPIO выведены не все доступные порты. Часть этих портов используется самим Raspbery для своих нужд. Моргает светодиодами, общается с SD-картой, управляет адаптером Ethernet и т.д.
          Пойдём дальше. Что получится, если мы будем повторять последовательность включения и выключения? Давайте попробуем! Слегка подправим нашу написанную программу. Откроем, написанную нами ранее программу:
nano GPIO-test.c
Сохраним её под новым именем:<>ctrl+o
и введём blink.c
Теперь отредактируем её до следующего вида (или просто удалим всё написанное ранее и вставим текст отсюда):

// blink.c
// Программа мигает светодиодом 1 раз в секунду.
// Светодиод подключён к порту Р1_03
// Компиляция: gcc -o blink blink.c -lrt -lbcm2835
// Запуск: sudo ./blink

#include <bcm2835.h>  
#define PIN RPI_GPIO_P1_03 
//#define PIN RPI_V2_GPIO_P1_03  //для плат RPi rev. V2
int main()
{

    if (!bcm2835_init())         // Инициализация GPIO
        return 1;                //Завершение программы, если инициализация не удалась

    bcm2835_gpio_fsel(PIN, BCM2835_GPIO_FSEL_OUTP);          //Устанавливаем порт Р1_03 на вывод

    while(1)   // Повторяем все действия, заключённые в скобки {} бесконечное число раз
    {

        bcm2835_gpio_write(PIN, LOW);   // Устанавливаем порт в 0, светодиод горит
        bcm2835_delay(500);             // Ждём 500 милисекунд
        bcm2835_gpio_write(PIN, HIGH);  // Устанавливаем порт в 1, светодиод не горит
        bcm2835_delay(500);             // Ждём 500 милисекунд

    }
    return 0;     // Выход из программы
}


Проверяем на наличие ошибок и сохраняемся:
Ctrl+o 
Ctrl+x
Компилируем:
gcc -o blink blink.c -lrt -lbcm2835
И запускаем:
sudo ./blink
Ну вот, теперь наш светодиод мигает. В эту программу мы внесли следующие изменения:
Во-первых, изменили временные промежутки включённого и выключенного состояния. Теперь наш светодиод 500 миллисекунд горит и 500 миллисекунд не горит. Т.е. на всё уходит ровно 1 секунда.
Во-вторых, мы заключили функции включения и выключения светодиода в бесконечный цикл. Оператор while(1) будет выполнять всё, что заключено в следующую за while пару скобок {} до тех пор, пока параметром while будет 1. А раз мы эту единичку туда прописали, то она там будет всегда, а следовательно и команды включения-выключения будут тоже выполняться бесконечно.
  Посмотрели, порадовались своим успехам… А светодиод всё ещё мигает. И будет мигать до тех пор, пока не выдернете источник питания из розетки. А ещё можно прекратить выполнение программы «варварским» способом- нажать Ctrl+c. Только если нажать неудачно, то программа завершится, а светодиод продолжит гореть.  Непорядок. Нужно это дело исправлять.
Ставим перед собой задачу:
1. нам нужно предусмотреть возможность прерывать выполнение программы в любой нужный нам момент;
2. нам нужно, чтобы после прерывания, программа завершалась корректно.
Т.е. после выхода из программы светодиод однозначно бы гас.

        Приступим. Для того, чтобы завершить выполнение программы в любое время, мы подключим к нашему Raspberry кнопочку. Пока она не нажата, светодиод будет мигать. Когда нам надоест смотреть на мигающий светодиод, мы нажимаем на кнопочку и наша программа завершается с выключенным светодиодом. Чтобы это реализовать, нам нужно выделить для кнопочки ещё один порт GPIO. Причём этот порт нужно настроить не на Вывод, а на Ввод (или другими словами не на запись, а на чтение). Этот порт будет следить за состоянием подключённой к нему кнопки, и как только на нём появится напряжение (уровень HIGH), наша программа должна завершиться.  Выберем порт для этой цели. Пусть будет Р1_07.
          Так же нам нужен источник сигнала для кнопки, за состояние которой мы будем следить. На этом источнике должна постоянно находиться логическая единица. Можно в качестве такого источника использовать присутствующие на Р1_01 +3,3в. Но к нему неудобно подключаться, он уже использован для питания светодиода. Мы поступим проще. Помните, я в первой части говорил, что процессор bcm2835 умеет подключать свои порты к 0, либо к +3,3в? Так вот, если мы подключим порт процессора к +3,3В посредством установки этого порта в высокое состояние HIGH, то этот порт и можно будет использовать в качестве источника сигнала «1» для кнопки. Выделим под это дело порт P1_05, который находится по соседству с Р1_07. В результате у нас должна получиться вот такая схема:

13606489085119dacc043c2.jpg

       Самые внимательные могут вспомнить, что в предыдущей первой части я говорил, что мы должны всегда следить за тем, чтобы не перегрузить порт по току, и должны устанавливать токоограничивающие резисторы. А в данном случае кнопка подключена безо всего. А так, как кнопка практически не имеет никакого сопротивления, то замкнув кнопкой +3,3В с 0 мы получим короткое замыкание и сожжём нашу Малинку… Но этого не произойдёт. И вот почему. Во-первых, разработчики процессора решили снять часть ответственности с нас и сделали так, что процессор подключает свои порты к +3,3в и 0 через микроскопические резисторы, установленные в самом процессоре. Поэтому короткого замыкания не произойдёт. Ток потечёт через внутренние резисторы. Во-вторых, так же в первой части я говорил, что процессор может вообще никуда не подключать порт. Так вот. Если мы настраиваем порт на Ввод (чтение), то это как раз тот самый случай. Процессор будет читать данные с этого порта, поэтому ему нет необходимости подключать его к источнику питания, или 0. В этом режиме порт имеет достаточно высокое сопротивление. Настолько высокое, что мы в принципе можем безбоязненно подать на него прямиком 3,3 вольта от источника питания, и с ним ничего не случится. Но всё равно, если вы подключаете кнопку напрямую от источника питания, то лучше подстраховаться от всяческих бросков напряжения и поставить небольшой токоограничивающий резистор. Например тот же в 330Ом (не менее 100Ом). На будущее запомним следующие правила:
1. Мы можем спокойно подключать напрямую (без резисторов) к портам GPIO любые входы и выходы микросхем, работающих от того же источника питания, что и наш Raspberry, и имеющих 3-вольтовую логику. Под это же правило попадает и наше подключение кнопки между 2 портами процессора;
2.  Если подключаемое устройство имеет 3-вольтовую логику, но работает от другого источника питания, то нужно подстраховаться и подключать это устройство через ограничительные резисторы сопротивлением 100-330 Ом.
3. Если подключаемое устройство работает на 5-вольтовой логике, то мы обязательно должны делать согласование уровней. На GPIO Raspberry нельзя подключать напряжение выше 3,3В! По этому о непосредственном подключении не может быть и речи!
         Ну и информация для общего развития. Даже в режиме чтения порт может быть подключён к +3,3В, или 0 через внутренний резистор. Это называется подтяжкой. 
         Теперь давайте соберём схему. В принципе у нас она собрана, осталось только подключить к портам Р1_05 и Р1_07 кнопку. Кнопку можно взять абсолютно любую, хоть выключатель от электролампочки. Я взял кнопку Reset из старого системного блока:

13606493735119dc9de177e.jpg

В крайнем случае, можно просто использовать 2 проводочка из разъёма и просто замыкать их между собой.Подключаем всё к RPi таким образом:
13606516815119e5a16f9ec.jpg
Откроем нашу программу blink.c и добавим в неё несколько строк. В результате она должна будет выглядеть вот так:

// button.c
// Программа мигает светодиодом 1 раз в секунду
// и завершается при нажатии на кнопку.
// Светодиод подключён к порту Р1_03
// Кнопка подключена к портам Р1_05 и Р1_07
// Компиляция: gcc -o button button.c -lrt -lbcm2835
// Запуск: sudo ./button

#include <bcm2835.h> 

#define PIN RPI_GPIO_P1_03       // Определяем порт для подключения светодиода
#define PIN_OUT RPI_GPIO_P1_05   // Определяем порт для записи
#define PIN_IN RPI_GPIO_P1_07    // Определяем порт для чтения

int main()
{

    if (!bcm2835_init())         // Инициализация GPIO
        return 1;                // Завершение программы, если инициализация не удалась

    bcm2835_gpio_fsel(PIN, BCM2835_GPIO_FSEL_OUTP);          // Устанавливаем порт PIN на вывод
    bcm2835_gpio_fsel(PIN_OUT, BCM2835_GPIO_FSEL_OUTP);      // Устанавливаем порт PIN_OUT на вывод
    bcm2835_gpio_fsel(PIN_IN, BCM2835_GPIO_FSEL_INPT);       // Устанавливаем порт PIN_IN на ввод
    bcm2835_gpio_pud(PIN_IN, 1);                             // Включаем подтяжку порта PIN_IN к "0"

    bcm2835_gpio_write(PIN_OUT, HIGH);                       // Устанавливаем порт PIN_OUT в состояние "1"  

    while(!bcm2835_gpio_lev(PIN_IN))    // Повторяем все действия, заключённые в скобки {} пока не будет нажата кнопка
    {

        bcm2835_gpio_write(PIN, LOW);   // Устанавливаем порт в 0, светодиод горит
        bcm2835_delay(500);             // Ждём 500 милисекунд
        bcm2835_gpio_write(PIN, HIGH);  // Устанавливаем порт в 1, светодиод не горит
        bcm2835_delay(500);             // Ждём 500 милисекунд

    }

    bcm2835_gpio_pud(PIN_IN, 0);   // Отключаем подтяжку порта PIN_IN к "0"
    return (bcm2835_close ());     // Выход из программы
}


Проверяем программу на ошибки и сохраняем под новым именем:
Ctrl+o
button.c
Ctrl+x
Компилируем и запускаем:
gcc -o button button.c -lrt -lbcm283
sudo ./button
Светодиод начинает мигать, как и раньше. Теперь нажмём нашу кнопку (нажатие должно быть продолжительным).
Программа должна завершиться при выключенном светодиоде. Сейчас разберёмся в том, что мы добавили в программу и как оно работает.
        Первое, что мы сделали, это закрепили свои названия за выбранными портами.
#define PIN_OUT RPI_GPIO_P1_05
#define PIN_IN RPI_GPIO_P1_07
Порт Р1_05, который служит источником питания для нашей кнопки, мы обозвали PIN_OUT.\nПорт Р1_07, который следит за состоянием нашей кнопки, мы обозвали PIN_IN.
Не забываем, что для платы RPi версии rev.V2, необходимо добавить в определение портов V2. Раньше я помещал эти строки в код закомментированными, здесь и далее я этого делать больше не буду, следите за этим пожалуйста сами.
Следующее изменение касается установки новых портов в нужные нам режимы.
bcm2835_gpio_fsel(PIN_OUT, BCM2835_GPIO_FSEL_OUTP);
bcm2835_gpio_fsel(PIN_IN, BCM2835_GPIO_FSEL_INPT);
Т.е. установили PIN_OUT в режим Вывода (он нам даёт +3,3В для кнопки), а PIN_IN установили в режим Ввода (он будет следить за состоянием кнопки).

bcm2835_gpio_pud(PIN_IN, 1);
Этой строкой мы подключили порт PIN_IN через внутренний резистор процессора к «0» (или GND), применили подтяжку порта к 0. Это позволит повысить помехоустойчивость опроса состояния кнопки. В более ранней версии этой статьи этого сделано небыло. И хотя всё работало, но при подключении RPi к сети по Wi-Fi возникали ошибки чтения из-за того, что Wi-Fi донгл наводил в проводниках кнопки потенциал, достаточный для распознавания на порте PIN_IN логической единицы, хотя кнопка была ещё не нажата. В 5-й части цикла статей я ещё коснусь этого вопроса.
Далее, мы установили порт PIN_OUT в высокое состояние:
bcm2835_gpio_write(PIN_OUT, HIGH);

Теперь на выходе порта Р1_05 у нас присутствует логическая единица в виде напряжения в +3,3В.
И ещё мы изменили содержимое функции while()
while(!bcm2835_gpio_lev(PIN_IN)) ;
Мы поместили в условие выполнения цикла функцию чтения состояния порта PIN_IN.
Функция bcm2835_gpio_lev() возвращает значение состояния выбранного порта, т.е. читает его состояние. В нашем случае она читает состояние порта PIN_IN, который в соответствии с нашим определением является портом Р1_07. Если на ножке этого порта присутствует логическая единица, или по другому +3,3В, то эта функция возвращает 1 (помните, в нашей предыдущей программе какраз на этом месте и стояла 1?). А если на ножке порта логический ноль (или напряжение 0В), то эта функция и возвращает 0. Восклицательный знак перед этой функцией обозначает инверсию результата. В булевой математике он соответствует функции «НЕ». Т.е. !1=0, а !0=1. Другими словами изменяет результат на противоположный. Вот и получается, что наш светодиод будет мигать до тех пор, пока результатом чтения состояния порта PIN_IN будет НЕ единица, а собственно Ноль. А НЕ единица будет до тех пор, пока мы не нажмём кнопочку. Как мы только нажмём на кнопку, то логическая единичка с порта PIN_OUT придёт на порт PIN_IN и функция чтения состояния этого порт возвратит единичку. Цикл прервётся и программа завершится. А так, как последним состоянием светодиода в этом цикле было «выключено», то программа завершится именно с выключенным светодиодом. Так же стоит отметить, что чтение порта PIN_IN в нашей программе выполняется 1 раз в секунду. Именно поэтому нажатие кнопки должно быть продолжительным, чтобы гарантировано уложиться в эту 1 секунду и попасть на чтение порта. Разумеется, есть масса способов обойти это неудобство, но пока мы не будем заострять на этом внимание, чтобы не усложнять программу и разобраться в сути чтения информации с порта.

         Ну и появилась ещё одна новая функция, которой мы ранее не пользовались.
bcm2835_close ()
Эта функция закрывает библиотеку, освобождает выделенную под использование GPIO память и приводит состояние портов GPIO к исходному.
Другими словами корректно завершает работу с портами.
Мы её поместили в return потому, что эта функция возвращает значение 1, если всё прошло успешно, и 0, если возникли какие-то проблемы. 
Этой функцией нужно пользоваться всегда при завершении работы с портами. Это, как правило хорошего тона — уходя говорить: «до свидания». Так что эту функцию можно смело ставить и во все ранее написанные нами программы.

         Ну вот, сегодня мы научились читать из порта. И хотя мы читали состояние лишь одной кнопки, но именно подобным образом можно прочитать и последовательность бит от любого другого устройства, подключённого к этому порту и передающего ему какую либо информацию. Но самое главное- это принцип. И мы в нём сегодня разобрались.
           Но на этом мы пока не будем разбирать нашу схему со светодиодом. Он нам поможет разобраться ещё в нескольких интересных и нужных вещах. И об этом речь пойдёт в следующих частях серии «GPIO для чайников».
         А пока продолжайте самостоятельно изучать язык программирования Си. Ну и задание для самостоятельного выполнения- разработайте программу, которая бы включала светодиод при нажатой кнопке и выключала при отпущенной.
  • 0

Комментарии (10)

0
Hi, мое имя Александр Антонович. Я отношу себя к профи. Поэтому, позвольте я дам несколько советов и чуть-чуть покритикую. 1. Пожалуйста, замените варажения типа "питание для кнопки". Во первых, это режет слух, во вторых, это в корне не правильно, а в третьих Вы дуроно воспитаете своих пионеров. 2. К вопросу -- почему разработчики рекомендуют делать так: bcm2835_gpio_fsel(PIN_IN, BCM2835_GPIO_FSEL_OUTP); bcm2835_gpio_fsel(PIN_IN, BCM2835_GPIO_FSEL_INPT); Зайду из далека. Несложно ответить на вопрос -- какое напряжение будет на GPIO4, когда кнопка нажата? Ответ -- такое напряжение, какое в этот момент будет присутствовать на GPIO1. На GPIO1 присутствует (всегда) логическая единица (далее -- "1"), значит, на прои нажатии на кнопку на выводе порта GPIO4 будет присутствовать "1". Ответь на вопрос -- "какое напряжение будет присутствовать на GPIO4, когда кнопка не нажата?" -- очень сложно. Дело в том, что высокоомный вход микросхемы практически остается брошенным на произвол судьбы и помехам. В граммотно спректированных схемах вывод ВСЕГДА "поддтягивают" к земле или к питанию с помощью высокоомных резисторов. В нашем случае, мы должны "подтягивать" вывод к земле. Сопротивление резисторов обычно берут в пределах от 10 до 100 кОм. Но в редких случаях, можно увидеть и 1 кОм и 1 МОм. Подтягивающие резисторы могут внутренними (если таковые имеются в микроконтроллере и удовлетворяют требованиям подтяжки), либо внешние. Когда кнопка не нажата, высокоомный резистор подтягивает вход. В нашем случае -- тянет его к земле. Иначе говоря, на выводе формируется лог."0". Причем следует заметить, формируется весьма слабыми токами -- ведь резистор-то -- относительно высокоомный. Поэтому, как только произойдет нажатие на кнопк, на выводе тут же сформируется лог."1", так как выход порта микросхемы -- это более сильный источник сигнала, чем подтягивающий резистор. Когда кнопку отпустят, на входе снова сформируется лог."0". Поддягивающий резистор делает свое дело. Наша задача, выбрать сопротивление подтягивающего резистора такое, чтобы он, с одной стороны, не сильно нагружал выход порта, когда нажимается кнопка, а с другой -- не слишком большим, чтобы он мого шунтировать наведенные на провода помехи. Далее. Что происходит в нашей схем, ведь в ней нет подтягивающих резисторов? Для ответа на этот вопрос нужно вспмнить, что на входе порта стоят полевые транзисторы. Полевые транзисторы характеризуются тем, что они практически не имеют входных токов, но зато имеют ощутимую входную емкость. Таким образом, выполняя последовательность команд: bcm2835_gpio_fsel(PIN_IN, BCM2835_GPIO_FSEL_OUTP); bcm2835_gpio_fsel(PIN_IN, BCM2835_GPIO_FSEL_INPT); мы сначала переводим вывод порта в режим выхода, тем самым формируем на выводе микросхемы напряжение, соответствующее состоянию порта, а это значит -- ЗАРЯЖАЕМ или РАЗРЯЖАЕМ входные емкости (емкости переходов у входных полевых транзисторов), а потом переводим вывод порта в режим ввода. После этих опрераций на входной емкости порта КАКОЕ-ТО ВРЕМЯ будет сохраняться полученный заряд. Потом, через несколько секунд, а может быть десятков минут, токи утечек разрядят входные емкости и схема (а, следовательно, и программа) начнет вести себя непредсказуемо. В нашем случае всё это вроде бы работает. Во первых, при переводе вывода порта на выхд, мы предполагаем, что на выходе порта у нас лог."0". На самом деле это так: после сброса микроконтроллера состояние портов обычно равно нулю. Но если перед запуском нашей проги, кто-то выведет в порт GPIO1 лог."1", то мы вместо того чтобы сообщить входным емкостям напряжение лог."0", зарядим их. Как поведет себя в этом случае прога, думаю, вы легко сообразите самостоятельно. К стати, проведите эксперимент -- предварительно выведите в порт единичку, а потом выполните эти две команды, и посмотрите, сработает-ли "рекомендации специалистов". 3. Я с большим удовольствием хочу отметить, что не смотря на незначительные огрехи (а укого их нет!) автор цикла статей старается сделать мир лучше. Если мы не будем делиться своими знаниями и опытом, а будем делать на этом деле деньги, мы не станем богаче. Возможно, у нас лично будет больше денег, чем у других окружающих нас людей, но как нация в целом, мы будем беднее и духовно и материально. Автор статей, видимо, познал эту истину, за что ему огромной респект!
0
2.5 Совсем забыл! Я хотел бы порекомендовать сменить наименования портов. Например, следующие имена сделают программу более прозрачной для восприятия: #define LED (RPI_GPIO_P1_03) #define UPPER (RPI_GPIO_P1_05) #define BUTTON (RPI_GPIO_P1_07)
0
У меня такой вопрос, в RPi версии rev.V2 не работает вот это: bcm2835_gpio_pud(PIN_IN, 1); на этой строчке компилятор останавливается и рукается, что слишком много параметров. Немного залез в мануалы, глянул там такие команды как: bcm2835_gpio_pudclc bcm2835_gpio_set_pud Может как-то через гих реализовать?
0
Просто удалите. Автор писал.
0
Для RPI rev.V2 вместо строчки bcm2835_gpio_pud(PIN_IN, 1); нужно написать bcm2835_gpio_set_pud(PIN_IN, BCM2835_GPIO_PUD_DOWN);
0
Небольшая поправка: "Компилируем и запускаем: gcc -o button button.c -lrt -lbcm283" Нужно: gcc -o button button.c -lrt -lbcm2835
0
Еще до того как увидел данную статью поигрался с бесконечным циклом и разным временем работы светодиода. Как будто мысли ваши читал. Почему-то программа с кнопкой у меня не компилируется. Пробовал так как тут и с исправленными пинами и для второй и для первой ревизии. И библиотеку уже установил старую - 1.17, а до этого была самая новая, думал в этом дело... и компилировать пробовал по разному... никак не хочет... не пойму в чем дело... Вот такие ошибки: button.c: In function ‘main’: button.c:23:23: error: ‘RPI_V2_GPIO_P1_06’ undeclared (first use in this function) button.c:23:23: note: each undeclared identifier is reported only once for each function it appears in button.c:24:5: error: too many arguments to function ‘bcm2835_gpio_pud’ /usr/local/include/bcm2835.h:723:17: note: declared here button.c:38:5: error: too many arguments to function ‘bcm2835_gpio_pud’ /usr/local/include/bcm2835.h:723:17: note: declared here
0
[quote] Вот такие ошибки: button.c: In function ‘main’: [/quote] В начале кода: #include <bcm2835.h> #include <stdio.h> Например для ошибки button.c:23:23 можно так для : #define PIN RPI_V2_GPIO_P1_06 для button.c:24:5: ругается что много аргументов которые заданы в скобках () после -- bcm2835_gpio_pud надо было их здесь олицетворить... но наверное должно быть что-то типа этого: bcm2835_gpio_set_pud(PIN, BCM2835_GPIO_PUD_UP) В скобках 2 аргумента, это PIN задефайнен см. вверху и макрос BCM2835_GPIO_PUD_UP для Pull-up/down resistor....
0
Использование bcm2835_gpio_set_pud() выдавало сообщение: ошибка сегментации. В итоге в схему добавил подтягивающий резистор на 10к, как и говорил Александр Антонович. Все заработало. Работаю на RaspberryPi 3.
0
Всем привет.
есть raspberry pi3 нужно собрать монтажную плату и подключить пару кнопок.
за вознаграждение.
пишите если у кого есть желание.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.