Пульт для аквариумного шкафа я задумал красивый: восемь кнопок и восемь светящихся клавиш, как на кассовом аппарате. Посчитал пины и поморщился: шестнадцать только на панель, а у Uno их двадцать, из которых половина занята — шина на A4/A5, экран, пищалка. Расширитель PCF8574 из урока про расширитель портов дал мне восемь пинов, и я честно припаял вторую микросхему на соседний адрес. Две микросхемы, два стиля общения, и в каждой пайке — шанс на соплю, а считал я пины уже после пайки — порядок действий, который я не рекомендую.
А потом всплыла грабля, о которой я знал, но недооценил. На PCF8574 светодиод включается нулём: верхний ток пина слабый, сотни микроампер, и «зажечь единицей» он не может физически — анод приходится тянуть к питанию, а пин прижимать к земле. Для восьми светодиодов это вылилось в перевёрнутую логику по всему коду и пару часов на поиски того самого светлячка, который горел при выключенном пульте. Кнопки при этом читались только через внешние резисторы: у чипа нет регистра подтяжек.
MCP23017 решает обе беды и экономит место на плате: одна микросхема даёт шестнадцать полноценных пинов GPIO с регистрами направлений, встроенными подтяжками и выходами, которые толкают и вверх, и вниз. В этом уроке раскатаем карту его регистров, соберём типовой проект «8 кнопок плюс 8 светодиодов на одном чипе» и честно сведём счёт с PCF8574 в таблице — где старый знакомый всё ещё хорош, а где давно пора переехать.
Почему одной PCF8574 упирается в потолок
У PCF8574 нет регистра направлений — пины работают по принципу квази-направлений. Записали в порт единицу — пин подключился к питанию через слабый внутренний источник тока, порядка сотни микроампер: такой пин безболезненно придавливается к земле кнопкой и потому может служить входом. Записали ноль — пин намертво прижат к земле и тянет до двух с половиной десятков миллиампер. Отсюда и перевёрнутые светодиоды, и внешние подтяжки для кнопок, и вилка адресов ровно та же, 0x20-0x27. Мы продирались через эти особенности в уроке про кнопки и реле на PCF8574.
MCP23017 устроен по-взрослому: внутри два восьмибитных банка — GPA0-GPA7 и GPB0-GPB7, и на каждый банк своя пара регистров: направление, подтяжки, выходная защёлка. Выходы симметричные — светодиод можно включить любой полярностью через обычный резистор в 220-330 Ом. Скорость шины тоже другая лига: чип держит до 1,7 МГц при пятитивольтовом питании, тогда как PCF8574 останавливается на 100 кГц. Для панели из кнопок это неважно, а вот для бегущей строки или матрицы — заметно.
Адреса у чипов общие: MCP23017 занимает 0x20-0x27 в зависимости от трёх ножек A0, A1 и A2: каждая на землю или на питание даёт свой бит адреса. Если на шине уже живёт PCF8574 на 0x20, разведите их по адресам или случайно получите двух жильцов в одной квартире; вилка адресов всего раздела собрана в таблице адресов модулей.
Четыре пары регистров и одна вилка
Карта регистров компактная. IODIRA и IODIRB (0x00 и 0x01) задают направления, и здесь сидит первая грабля новичка: единица — это вход, ноль — выход, у многих половина кода написана наоборот. GPPUA и GPPUB (0x0C и 0x0D) включают встроенные подтяжки к питанию порядка 100 кОм на входах — кнопка просто соединяется с землёй, и никакой внешней обвязки. GPIOA и GPIOB (0x12 и 0x13) — чтение фактических уровней пинов и запись в выходы. OLATA и OLATB (0x14 и 0x15) — чтение того, что вы записали в защёлку, независимо от того, что сейчас на пине.
Разница GPIO и OLAT видна на нагруженном выходе: пин задавлен внешней нагрузкой — GPIO покажет правду, OLAT покажет намерение. Для кнопок и светодиодов хватает GPIO, но в диагностике «я же писал единицу» читать OLAT честнее. Общая механика регистров, указатель плюс чтение, разобрана в уроке про карту регистров, а маски для точечных включений битов — в уроке про битовые операции.
Последний персонаж — регистр конфигурации IOCON, в котором живёт бит BANK. При BANK=0 (значение после подачи питания) регистры обеих банков лежат подряд: GPIOA 0x12, GPIOB 0x13, OLATA 0x14, OLATB 0x15, и указатель внутри чипа автоприбавляется — один запрос тянет оба банка сразу. При BANK=1 регистры разъезжаются по половинам, и последовательное чтение ломается. Я всегда оставляю BANK=0 и один раз перепроверяю это чтением двух байт: пришли осмысленные уровни — чип в ожидаемом режиме.
| Что сравниваем | PCF8574 | MCP23017 |
|---|---|---|
| Пинов на одну микросхему | 8 | 16 — два банка по 8 |
| Направления пинов | квази: записал единицу — пин стал входом | регистр IODIR: 1 — вход, 0 — выход |
| Подтяжки входов | внешние резисторы | встроенные GPPUA/B, около 100 кОм |
| Светодиод | только на землю, включается нулём | любой стороной, выход толкает и вверх |
| Скорость шины | 100 кГц | до 1,7 МГц при 5 В |
| Адреса на шине | 0x20-0x27 | 0x20-0x27 — разводите A0-A2 с соседями |
| Ток на пин | 25 мА в низ, микроамперы в верх | десятки миллиампер в обе стороны |
Проект: 8 кнопок и 8 светодиодов на одном чипе
Разводка типового проекта, ради которого чип и берут. Банк A — входы: восемь кнопок, каждая соединяет свой пин GPA с землёй, подтяжки включаются одним байтом в GPPUA, и на макетке не появляется ни одного резистора. Банк B — выходы: восемь светодиодов через резисторы 220-330 Ом, направления прописываются нулями в IODIRB. Вся панель пульта занимает на Arduino два провода шины, и остаётся такой же при расширении до третьей и четвёртой микросхемы.
Чтение панели делается одним ходом: пишем указатель на GPIOA и читаем два байта — первым придёт банк A, вторым банк B; это и есть последовательное чтение при BANK=0. В нажатом состоянии кнопка прижимает пин к земле, так что в прочитанном байте она выглядит нулём — инвертируйте байт, и получите привычную логику «нажата = единица». Светодиоды банка B удобно писать одним байтом в OLATB — он лежит сразу после OLATA, и одна запись зажигает всю восьмёрку разом. Дребезг никуда не делся: механический контакт есть механический контакт, и антидребезг из урока про debounce ставится так же, как на прямых пинах Arduino, просто источник уровней теперь шина.
Ниже — скетч целиком: настройка трёх регистров, чтение обоих банков за один запрос, зеркало кнопок на светодиоды и печать байтов в Serial для отладки. На макетке он поднимается за десять минут и работает годами.
#include <Wire.h> // MCP23017 на адресе 0x20 (A0-A2 на земле). Банк A: 8 кнопок на землю,// подтяжки включены в GPPUA. Банк B: 8 светодиодов через резисторы.// Нажатая кнопка в GPIOA читается нулём (пин прижат к земле). const byte MCP = 0x20;const byte REG_IODIRA = 0x00;const byte REG_IODIRB = 0x01;const byte REG_GPPUA = 0x0C;const byte REG_GPIOA = 0x12; // GPIOA 0x12 и GPIOB 0x13 лежат подрядconst byte REG_OLATA = 0x14; // OLATA 0x14 и OLATB 0x15 тоже подряд void pisat(byte reg, byte znach) { Wire.beginTransmission(MCP); Wire.write(reg); Wire.write(znach); Wire.endTransmission();} void setup() { Wire.begin(); Serial.begin(9600); pisat(REG_IODIRA, 0xFF); // банк A: все восемь — входы pisat(REG_GPPUA, 0xFF); // подтяжка на каждой кнопке pisat(REG_IODIRB, 0x00); // банк B: все восемь — выходы Serial.println("MCP23017: GPA - knopki, GPB - svetodiodi");} void loop() { Wire.beginTransmission(MCP); Wire.write(REG_GPIOA); // указатель на GPIOA Wire.endTransmission(); Wire.requestFrom(MCP, 2); // два байта: сначала A, потом B byte bankA = 0xFF; byte bankB = 0x00; if (Wire.available() >= 2) { bankA = Wire.read(); bankB = Wire.read(); // банк B: уровни светодиодных пинов } byte nizhata = ~bankA; // инверсия: нажата = единица pisat(REG_OLATA, nizhata); // OLATA и OLATB пишутся подряд: pisat(REG_OLATA + 1, nizhata);// светодиод B0 зеркалит кнопку A0 Serial.print("A="); Serial.print(bankA); Serial.print(" B="); Serial.println(bankB); delay(50);}Что здесь честно, а что нет. Микросхемы в движке нет: панель шины пустая, регистры — это три байта-слепка в массивах, и вместо восьми кнопок на банк в сцене их две. Зато логика нулей и единиц подлинная: подтянутый вход читается единицей, нажатый — нулём, инверсия и маски работают ровно так, как они работают в настоящем GPIOA, и светодиод вспыхивает от записи в защёлку, а не от прямой магии. Пощёлкайте кнопки по очереди и подержите обе — OLATB соберёт оба бита, как настоящий банк собрал бы все восемь.
На живой плате к этому проекту напрашиваются две надстройки. Первая — прерывание: MCP23017 умеет дёргать ножку INTA при изменении на любом входе, и панель из восьми кнопок перестаёт сканироваться вообще — тот же механизм слушания вместо опроса, что в уроке про INT-прерывание. Вторая — расширение: третий и четвёртый такие же чипы садятся на соседние адреса, и пульт превращается в пультовую без единого нового провода до Arduino.
- MCP23017 — 16 пинов в двух банках GPA/GPB: регистры направлений, подтяжки и защёлки вместо квази-направлений PCF8574
- Адрес 0x20-0x27 через A0-A2 — вилка та же, что у PCF8574, разводите жильцов по шине заранее
- IODIR: 1 — вход, 0 — выход; GPPUA/B включает подтяжки около 100 кОм; GPIO — уровни пинов, OLAT — то, что записали
- При BANK=0 банки лежат подряд: указатель на GPIOA, requestFrom на два байта — и оба банка у вас
- Нажатая кнопка с подтяжкой читается нулём — инвертируйте байт; дребезг гасится тем же антидребезгом, что и на прямых пинах
- 1. Сколько дополнительных пинов даёт одна микросхема MCP23017?
- 2. Что означает единица в бите регистра IODIRA?
- 3. Как на PCF8574 включают светодиод?
- 4. Как прочитать оба банка GPIO одним запросом?
- 5. Что вернёт чтение OLATB?
Частые вопросы по теме урока
- Сколько дополнительных пинов даёт одна микросхема MCP23017?
- Шестнадцать: два восьмибитных банка, GPA0-GPA7 и GPB0-GPB7. Для 32 пинов понадобится уже пара таких чипов. 16
- Что означает единица в бите регистра IODIRA?
- Бит 1 — вход, 0 — выход. Самая частая путаница новичка: половина кода пишется в обратную сторону, пока это не запомнишь. пин настроен как вход
- Как на PCF8574 включают светодиод?
- Квази-направления: вверх пин тянет микроамперами, вниз — десятками миллиампер. Светодиод включается нулём с анодом на питании. нулём: сильный только прижим к земле, светодиод вешают анодом к питанию
- Как прочитать оба банка GPIO одним запросом?
- После подачи питания бит BANK=0, регистры лежат подряд: GPIOA 0x12, GPIOB 0x13. Указатель плюс чтение двух байт тянет оба банка. поставить указатель на GPIOA при BANK=0 и requestFrom на два байта
- Что вернёт чтение OLATB?
- OLAT — выходная защёлка: она хранит намерение. GPIO показывает правду о пине, OLAT — о том, что писал код; в диагностике «писал же единицу» помогает именно OLAT. то, что вы записали в защёлку, независимо от нагрузки на пине