GPIO — это набор входов/выходов общего назначения, с помощью которых микроконтроллер может управлять внешними устройствами и принимать от них сигналы. В одном из первых уроков мы подключали к Ардуино светодиод, и делали это как раз при помощи GPIO.
Ардуино Уно и её более мелкие собратья имеют 20 штук GPIO контактов, которых хватает для большинства DIY проектов. У версии Mega и того больше — 54. А вот у популярной WiFi платы ESP8266, особенно у версии 01S — свободных контактов крайне мало — их вообще всего 4. Как же быть, если нам не хватает контактов для подключения нужной периферии?
Есть разные способы, которые зависят от особенностей подключаемых устройств. Например, можно применить микросхему дешифратора или сдвиговые регистры. Можно даже соединить вместе несколько микроконтроллерных плат по одноранговой схеме или по схеме ведущий-ведомый, чтобы получить любое количество доступных контактов.
А ещё для увеличения GPIO существуют специальные микросхемы. На этом уроке мы будет работать с модулем расширения GPIO от RobotClass как раз на одной из таких микросхем — MCP23017.
Модуль умеет работать как с напряжением 3,3В, так и 5В. Так что его можно использовать в связке с разного рода платами Ардуино, а также с stm32, esp8266, esp32 и другими современными микроконтроллерами.
В качестве примера, увеличим число GPIO контактов у самой обычной платы Ардуино Уно. Попробуем применить модуль MCP23017 и для передачи сигнала (OUTPUT), и для чтения (INPUT).
Список необходимых компонентов
Для выполнения всех экспериментов в данном уроке, кроме самого расширителя, потребуются: Ардуино-совместимый контроллер, светодиоды, кнопки, резисторы, макетная плата и немного проводов вилка-розетка и вилка-вилка. Необходимые компоненты можно добавить в корзину прямо здесь, и затем оформить заказ в нашем интернет-магазине.
Подключение
Расширитель имеет 22 контакта, 20 из которых расположенны по контуру платы и два чуть ближе к центру. Посмотрим, за что они отвечают:
- SDA, SCL — модуль подключается к микроконтроллеру по I2C шине, эти контакты как раз для этого;
- VCC, GND — ещё два контакта отвечают за питание;
- G0 до G15 — это те самые GPIO, в количестве 16 штук;
- IA и IB — два дополнительных контакта, которые служат для работы с модулем по прерыванию.
Подключим модуль MCP23017 к Ардуино Уно по следующей схеме:
Arduino Uno | Gnd | +5V | A4 | A5 |
Расширитель GPIO | Gnd | Vcc | SDA | SCL |
Чтобы продемонстрировать работу модуля, к ножке с надписью G0 подключим светодиод. Разумеется, светодиод подключаем через резистор 200Ом, всё как в том самом уроке про светодиод и Ардуино.
Этой схемы нам вполне хватит для первого теста, можно переходить к программе.
Программа
Самый простой пример — мигание светодиода.
#include "Adafruit_MCP23017.h"
const byte ledPin = 0; // номер контакта со светодиодом - G0
Adafruit_MCP23017 mcp;
void setup() {
mcp.begin(7); // инициализация модуля с адресом 0x07
mcp.pinMode(ledPin, OUTPUT); // настройка контакта G0 на вывод
}
void loop() {
mcp.digitalWrite(ledPin, HIGH); // зажигаем
delay(200); // пауза 200мс
mcp.digitalWrite(ledPin, LOW); // гасим
delay(200); // пауза 200мс
}
В самом начале программы мы объявляем переменную mcp, которую связываем с программным модулем Adafruit_MCP23017. Говоря языком объектно-ориентированного программирования — мы создаём экземпляр класса Adafruit_MCP23017 и записываем указатель на него в переменную mcp.
Затем, в функции setup мы вызываем метод mcp.begin с числом 7 в качестве единственного аргумента. 7 — это адрес устройства (подробности в последней главе «Адресация»).
Как видим, в программе используется знакомая нам функция digitalWrite. Единственное отличие от варианта программы без расширителя — вызов функции нужно делать от имени переменной mcp (тот самый указатель).
Загружаем программу, убеждаемся, что светодиод мигает. Следующий пример использует все 16 контактов расширителя!
Программа для управления 16 светодиодами
Усложним схему и алгоритм. Подключим 16 светодиодов к выводам модуля с номерами от G0 до G15. Пусть теперь светодиоды зажигаются по очереди с G0 до G7, и синхронно в обратном порядке с другого конца: с G15 до G8.
#include "Adafruit_MCP23017.h"
Adafruit_MCP23017 mcp;
void setup() {
mcp.begin(7);
for(byte i=0; i<16; i++){
mcp.pinMode(i, OUTPUT);
}
}
void loop() {
for(int i=0; i<8; i++){
mcp.digitalWrite(i, HIGH);
mcp.digitalWrite(15-i, HIGH);
delay(30);
if(i>0){
mcp.digitalWrite(i-1, LOW);
mcp.digitalWrite(15-i+1, LOW);
delay(30);
}
}
for(int i=7; i>=0; i--){
mcp.digitalWrite(i, HIGH);
mcp.digitalWrite(15-i, HIGH);
delay(30);
if(i<7){
mcp.digitalWrite(i+1, LOW);
mcp.digitalWrite(15-i-1, LOW);
delay(30);
}
}
}
Загружаем и наблюдаем безудержное веселье!
Далее, на примере с тактовыми кнопками научимся считывать сигналы с помощью расширителя.
Чтение сигнала и digitalRead
Работа с сигналами кнопок при помощи GPIO-расширителя также не отличается сложностью. Для чтения сигнала на контакте используем хорошо знакомую функцию digitalRead. Как и в предыдущем примере, для вызова digitalRead на расширителе используем переменную mcp.
Кнопку подключим по схеме с подтяжкой. А именно, без всяких резисторов одну ножку кнопки соединим с контактом расширителя G1, а другую с землей Gnd. При этом, контакт G1 подтянем к питанию.
При таком подключении, кнопка будет работать инверсно: в свободном состоянии на контакте G1 будет высокий уровень сигнала — HIGH (из-за подтяжки), а если кнопку нажать — уровень станет низким (LOW), так как G1 через контакты кнопки соединится с землей.
Напишем простую программу, которая будет зажигать светодиод G0 в зависимости от нажатия кнопки на контакте G1 расширителя.
#include "Adafruit_MCP23017.h"
const byte ledPin = 0;
const byte btnPin = 1;
Adafruit_MCP23017 mcp;
void setup() {
mcp.begin(7); // инициализация модуля с адресом 0x07
mcp.pinMode(ledPin, OUTPUT); // настройка контакта G0 на вывод
mcp.pinMode(btnPin, INPUT); // настройка контакта G1 на ввод
mcp.pullUp(btnPin, HIGH); // подтяжка контакта G1 к питанию
}
void loop() {
if(mcp.digitalRead(btnPin)==LOW) // если кнопка нажата, то
mcp.digitalWrite(ledPin, HIGH); // зажигаем светодиод
else // иначе
mcp.digitalWrite(ledPin, LOW); // гасим светодиод
}
Функция pullUp осуществляет ту самую подтяжку контакта к питанию через сопротивление 100кОм. Весь остальной код нам хорошо знаком из уроков про кнопки и Ардуино. Загружаем программу на Ардуино и жмем кнопку — видим свет!
На этом урок можно закончить. Теперь у нас есть мощный инструмент, который может легко победить нехватку GPIO контактов а Ардуино. В следующем разделе будут чуть более углубленные сведения о модуле, касающиеся работы с прерываниями.
Прерывания
Как было указано в самом начале, у модуля есть два специальных контакта IA и IB. При смене уровня на контактах расширителя, на IA и IB возникнет импульс, который можно использовать для активации прерывания на микроконтроллере.
Зачем нужны прерывания мы говорили в уроке про прерывания.
Если коротко, то при использовании прерываний контроллер не будет общаться с расширителем постоянно, и все ресурсы будет тратить на основную задачу. Как только сработает прерывание, контроллер прочитает состояние нужного контакта на расширителе по I2C шине, вызовет обработчик этого события и продолжит опять выполнять свои рутинные дела.
Пишем программу.
#include <Adafruit_MCP23017.h>
Adafruit_MCP23017 mcp;
// к этому контакту Arduino подключен встроенный светодиод
const byte ledPin = 13;
// к этому контакту расширителя подключен внешний светодиод
const byte mcpPin = 0;
// к этому контакту Arduino подключен контакт IA расширителя
const byte arduinoIntPin = 3;
// на 3-м контакте Arduino обслуживается прерывание №1
const byte arduinoInterrupt = 1;
// переменная - флаг, которая примет значение true
// при возникновении прерывания
volatile boolean awakenByInterrupt = false;
void setup(){
// настраиваем режим контакта для прерывания Arduino
pinMode(arduinoIntPin,INPUT);
// инициализируем расширитель с адресом 7
mcp.begin(7);
// настраиваем прерывания расширителя
// режим зеркалирования отключен - false
// режим открытого стока отключен - false
// инверсия сигнала включена - LOW
mcp.setupInterrupts(false, false, LOW);
mcp.pinMode(mcpPin, INPUT); // установка режима
mcp.pullUp(mcpPin, HIGH); // подтяжка контакта к питанию
// активируем прерывание расширителя на контакте mcpPin
// в режиме детектирования спада импульса - FALLING
mcp.setupInterruptPin(mcpPin, FALLING);
// настраиваем режим контакта для светодиода
pinMode(ledPin, OUTPUT);
// активируем прерывание Arduino под номером arduinoInterrupt
// с вызовом функции intCallBack
// в режиме детектирования спада импульса - FALLING
attachInterrupt(arduinoInterrupt,intCallBack,FALLING);
}
// обработчик прерывания Arduino
void intCallBack(){
awakenByInterrupt = true;
}
void handleInterrupt(){
// отключаем прерывание на время пока выполняется обработчик
detachInterrupt(arduinoInterrupt);
// мигаем один раз светодиодом
digitalWrite(ledPin,HIGH);
delay(100);
digitalWrite(ledPin,LOW);
delay(100);
// ждём, пока кнопку не отпустят
while( !mcp.digitalRead(mcpPin));
// сбрасываем флаг нажатия
awakenByInterrupt = false;
// включаем прерывание обратно
attachInterrupt(arduinoInterrupt,intCallBack,FALLING);
}
void loop(){
// если прерывание сработало и флаг истина
if(awakenByInterrupt){
// делаем, что задумали
handleInterrupt();
}
}
Загружаем программу на Ардуино и проводим тест: нажимаем кнопку — в отчет, встроенный светодиод должен мигнуть один раз.
Адресация I2C
Этот параграф посвящен случаю, когда требуется подключить сразу несколько расширителей GPIO к одному контроллеру. Можно её пропустить, если такой цели не стоит.
Не все устройства с I2C интерфейсом могут похвастаться возможностью смены адреса. Модуль MCP23017 позволяет менять свой адрес в диапазоне от 0x20 до 0x27. На практике это означает, что мы можем подключить к контроллеру аж 8 таких модулей! С суммарным количеством GPIO = 8*16 = 128 штук. Не хило.
Как менять адрес? На обратной стороне модуля есть три перемычки.
Перемычка тройная. Слева — контакты земли GND, справа — контакты питания VCC, по центру — контакты микросхемы, отвечающие за адресацию I2C.
По-умолчанию, все перемычки соединены с питанием. Если приглядеться, видно, что между центральным контактом и питанием есть дорожка. В таком состоянии адрес устройства равен 0x27.
Если эту дорожку перерезать, а центральный контакт спаять с левым (который GND), то адрес устройства изменится. Ниже приведена таблица всех возможных комбинаций перемычек и соответствующие им адреса.
Адрес микросхемы | A0 | A1 | A2 | Адрес I2C |
000 (0) | GND | GND | GND | 0x20 |
001 (1) | GND | GND | VCC | 0x21 |
010 (2) | GND | VCC | GND | 0x22 |
011 (3) | GND | VCC | VCC | 0x23 |
100 (4) | VCC | GND | GND | 0x24 |
101 (5) | VCC | GND | VCC | 0x25 |
110 (6) | VCC | VCC | GND | 0x26 |
111 (7) | VCC | VCC | VCC | 0x27 |
Адрес микросхемы — это число, которое мы будем указывать во время инициализации модуля в нашей программе для Ардуино. Адрес I2C — это реальный адрес I2C шины, который мы увидим, если запустим сканер I2C. Реальный адрес может понадобиться при использовании какой-нибудь другой библиотеки или если мы решим работать с модулем без всяким библиотек вовсе.