Embedded HandbookПриложения
Все разделы › III. Периферия
STM32F103 · уровень средний · 37 мин чтения
Открыть в интерактивном справочнике

22и. Расширитель портов MCP23017 по I2C: 16 линий, прерывания по изменению, клавиатура и дребезг

В разделе 22 выводы добавляли сдвиговыми регистрами: 74HC595 на выход (22.1), 74HC165 на вход (22.2). Они быстрые, но глухие: направление линии задано микросхемой, а о нажатой кнопке программа узнаёт, только когда сама спросит. MCP23017 — расширитель другого рода: 16 линий, каждая сама по себе вход или выход, с подтяжкой, с инверсией и с прерыванием по изменению, и всё это по двум проводам I2C. Здесь — как он устроен, как подключить его к STM32F103, почему выходы меняют через OLAT, а не через GPIO, что такое INTCAP и почему прерывание может «застрять», как сделать на одном его порту клавиатуру 4×4 с антидребезгом и сколько это стоит времени на шине.

Опоры: RM0008 (DocID13902 Rev 17): таблица режимов выводов, с. 160; AFIO_EXTICR3, с. 191; EXTI_FTSR и срабатывание по фронту, с. 211; I2C — до 100 кГц в обычном режиме и до 400 кГц в быстром, с. 752; приём ведущим больше двух байт, с. 763, одного и двух, с. 764; I2C_CCR, с. 783–784; I2C_TRISE, с. 784. DocID13587 Rev 17 (STM32F103x8/xB): PB6 — I2C1_SCL, PB7 — I2C1_SDA, оба FT, с. 32; PB8 и PB9 — FT, с. 33; FT — допуск 5 В, с. 33. Документа на MCP23017 в проекте нет. Номера регистров, бит MIRROR, значения после включения, активный ноль на INT и RESET, снятие прерывания чтением INTCAP и импульс сброса 2 мкс сверены с драйвером gpio_mcp23xxx из Zephyr 4.4.2; адрес 0x20, номера GPPU, GPIO и OLAT и пометка «запись в GPIO меняет OLAT» — с драйверами клавиатур QMK на диске. Остальные биты IOCON, карта регистров при BANK = 1, ход номера регистра при SEQOP и поведение прерываний — по документации производителя, не проверено: в модели это допущения. Все числа ниже — прогон драйвера _verify/gpx22j/mcp22j.c на компьютере (gcc 12.2.0) со своей моделью MCP23017 как ведомого I2C и клавиатуры с дребезгом на Python 3.11; прошивка собрана armclang 6.24 под Cortex-M3.

Как это называется в документации

Оригинал Перевод
I/O expander расширитель портов
IODIR (I/O direction) направление: 1 — вход, 0 — выход
IPOL (input polarity) инверсия входа при чтении
GPPU (pull-up) подтяжка входа к питанию
OLAT (output latch) защёлка выходов: что записано
GPIO (port register) уровни на выводах: что на самом деле
GPINTEN (interrupt-on-change enable) разрешение прерывания по линии
DEFVAL, INTCON образец и выбор: по изменению или по отличию от образца
INTF (interrupt flag) какая линия вызвала прерывание
INTCAP (interrupt captured value) снимок порта в момент прерывания
IOCON: BANK, MIRROR, SEQOP, ODR, INTPOL раскладка регистров, общий INT, автоприращение номера, открытый сток INT, активный уровень INT
ghosting ложное нажатие в матрице
debouncing подавление дребезга

Вверху — подключение MCP23017 к STM32F103: I2C1 на 400 кГц, PB6 — SCL, PB7 — SDA, PB8 — вход INTA на EXTI8 по спаду, PB9 — RESET, выводы A2…A0 на GND задают адрес 0x20, IOCON = 0x44 — MIRROR и ODR; порт A — клавиатура 4×4: строки GPA4…GPA7, столбцы GPA0…GPA3 с подтяжкой GPPU и диодом в каждой кнопке, порт B — восемь светодиодов; SCL, SDA и INT — открытый сток с подтяжкой, PB6–PB9 терпят 5 В. Внизу — нажатие с дребезгом по времени: INTA падает с первым касанием, чтение INTF и INTCAP одной транзакцией занимает 165 мкс и снимает INT, дальше сканы раз в 5 мс по 9 обменов и 752,5 мкс; первый скан идёт во время дребезга, сканы 2–5 одинаковы, и после четвёртого одинакового приходит событие «нажата»; в модели от нажатия до события в среднем 18,65 мс по прерыванию и 19,93 мс опросом


22и.1 Микросхема: два порта по восемь линий

У MCP23017 два порта, A и B, по восемь линий: GPA0…GPA7 и GPB0…GPB7. У каждого порта свой набор из десяти регистров и общий IOCON; у каждого свой вывод прерывания — INTA и INTB. Адрес на шине — 0x20 плюс три вывода A2…A0. В модели при перемычках 000 ответил 0x20 (байт в линии 0x40 на запись и 0x41 на чтение, 17.2), при 001 — 0x21, при 111 — 0x27 (0x4E и 0x4F): на одну шину садятся восемь микросхем, 128 линий. Чужой адрес — MCP_E_NACK.

При BANK = 0 (так после включения) регистры двух портов идут вперемежку: у порта A чётный номер, у порта B — следующий. Zephyr держит номера восьмилинейной MCP23008 (от 0x00 до 0x0A) и для 16-линейной умножает их на два — та же таблица:

/* ── регистры при BANK = 0: у порта A чётный номер, у порта B — следующий ─ */
#define MCP_IODIRA    0x00u             /* 1 — вход, 0 — выход; после сброса 0xFF */
#define MCP_IPOLA     0x02u             /* 1 — читать вход инверсным */
#define MCP_GPINTENA  0x04u             /* 1 — линия может вызвать прерывание */
#define MCP_DEFVALA   0x06u             /* с чем сравнивать при INTCON = 1 */
#define MCP_INTCONA   0x08u             /* 0 — по изменению, 1 — по отличию от DEFVAL */
#define MCP_IOCON     0x0Au             /* один регистр на два номера: 0x0A и 0x0B */
#define MCP_GPPUA     0x0Cu             /* 1 — подтяжка к питанию на входе */
#define MCP_INTFA     0x0Eu             /* какая линия вызвала прерывание (только чтение) */
#define MCP_INTCAPA   0x10u             /* состояние порта в момент прерывания (только чтение) */
#define MCP_GPIOA     0x12u             /* чтение — уровни на выводах, запись — в OLAT */
#define MCP_OLATA     0x14u             /* защёлка выходов: что записано, а не что на выводе */
#define MCP_PORTB     1u                /* прибавить к номеру — регистр порта B */
/* ── при BANK = 1 порт A — 0x00…0x0A, порт B — 0x10…0x1A ─────────────── */
#define MCP_B1_IOCON  0x05u
#define MCP_B1_GPIOA  0x09u
#define MCP_B1_OLATA  0x0Au
Регистр A / B Что делает После включения
IODIR 0x00 / 0x01 1 — вход, 0 — выход 0xFF — все входы
IPOL 0x02 / 0x03 1 — вход читается инверсным 0
GPINTEN 0x04 / 0x05 линия может вызвать прерывание 0
DEFVAL 0x06 / 0x07 образец для сравнения 0
INTCON 0x08 / 0x09 0 — по изменению, 1 — по отличию от DEFVAL 0
IOCON 0x0A и 0x0B настройки микросхемы, один регистр на два номера 0
GPPU 0x0C / 0x0D подтяжка входа к питанию 0
INTF 0x0E / 0x0F какая линия вызвала прерывание 0
INTCAP 0x10 / 0x11 снимок порта в момент прерывания 0
GPIO 0x12 / 0x13 чтение — уровни на выводах, запись — в OLAT —
OLAT 0x14 / 0x15 защёлка выходов 0

Модель после включения прочитала IODIR = 0xFFFF, а все остальные регистры, кроме GPIO, — нули; те же значения Zephyr записывает в свою копию регистров «по документации». В IOCON по документации производителя (не проверено): бит 7 BANK — раскладка, бит 6 MIRROR — INTA и INTB общие, бит 5 SEQOP — номер регистра после байта не растёт, бит 4 DISSLW — крутизна фронта SDA, бит 3 HAEN — адресные выводы у MCP23S17, SPI-сестры, бит 2 ODR — INT открытым стоком, бит 1 INTPOL — активный уровень INT. Из этих битов в Zephyr есть только MIRROR (бит 6).

Чем это лучше 74HC165 из 22.2: направление и подтяжка — у каждой линии свои, о нажатии микросхема сообщает сама, а провода — те же два, что у датчиков на I2C. Чем хуже: каждое обращение — транзакция I2C в десятки микросекунд (22и.6), а не такт SPI.


22и.2 Подключение к F103: I2C1, INTA на EXTI, сброс

Вывод MCP23017 STM32F103 Назначение
SCL, SDA PB6, PB7 I2C1, 400 кГц
INTA PB8 спад — EXTI8; INT открытым стоком (ODR = 1)
RESET PB9 ноль — сброс микросхемы
A2, A1, A0 GND адрес 0x20
VDD, VSS 3,3 В и земля общая земля с микроконтроллером

PB6 и PB7 — выводы I2C1, оба FT (DocID13587, с. 32), PB8 и PB9 тоже FT (с. 33): если расширитель питается от 5 В, подтяжки SDA, SCL и INT к 5 В их не сожгут. Сам I2C1 — как в 15б.2: быстрый режим, 36 МГц / (3 × 30) = 400 кГц, TRISE = 11 (с. 783–784); F103 больше 400 кГц не умеет (с. 752). INT настраивается открытым стоком (ODR = 1), подтяжку даёт сам PB8 — вход с подтяжкой, код 1000 в таблице режимов (с. 160). MIRROR = 1 сводит оба порта на INTA, и хватает одного провода. RESET у MCP23017 — активный ноль (так он описан в Zephyr); Zephyr держит его в нуле 2 мкс, прошивка — столько же. Линия EXTI8 привязана к порту B битами 3:0 регистра AFIO_EXTICR3 = 0001 (с. 191), спад включает EXTI_FTSR (с. 211):

static void pins_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    /* PB6, PB7 — альтернативная функция, открытый сток, 50 МГц (1111) */
    GPIOB->CRL = (GPIOB->CRL & 0x00FFFFFFu) | 0xFF000000u;
    /* PB8 — вход с подтяжкой (1000), PB9 — выход 2 МГц (0010), RESET отпущен */
    GPIOB->BSRR = 1u << 8 | 1u << 9;
    GPIOB->CRH = (GPIOB->CRH & ~0xFFu) | 0x28u;
}

static void i2c1_init(void)
{
    I2C1->CR1 = 0;                                      /* настраивать при PE = 0 */
    I2C1->CR2 = APB1_MHZ;
    I2C1->CCR = I2C_CCR_FS | 30u;                       /* DUTY = 0: 36 МГц / (3 × 30) = 400 кГц */
    I2C1->TRISE = APB1_MHZ * 300u / 1000u + 1u;         /* 300 нс × 36 МГц = 10,8 → 10 + 1 = 11 */
    I2C1->CR1 = I2C_CR1_PE;
}

static void exti_init(void)
{
    AFIO->EXTICR[2] = (AFIO->EXTICR[2] & ~0xFu) | 0x1u;    /* EXTI8 ← PB8 */
    EXTI->FTSR |= 1u << 8;
    EXTI->IMR |= 1u << 8;
    NVIC_EnableIRQ(EXTI9_5_IRQn);
}

Обработчик EXTI только ставит флаг: обмен по I2C длится сотни микросекунд, и ему место в основном цикле (7.7). EXTI срабатывает по фронту, а не по уровню (RM0008, с. 211), — это станет важно в 22и.4.


22и.3 Запись и чтение: OLAT, а не GPIO; последовательный режим и BANK

Первый байт записи — номер регистра, дальше — значения; номер после каждого байта растёт (SEQOP = 0), поэтому оба порта пишутся и читаются одной транзакцией: номер порта A, потом байт A и байт B.

Запуск. После сброса микроконтроллера микросхема могла остаться в BANK = 1 — например, от прошлой прошивки, если её RESET не трогали. Тогда номер 0x0A у неё — OLATA, а IOCON живёт по номеру 0x05. Драйвер начинает с записи нуля в 0x05: при BANK = 1 это IOCON, при BANK = 0 — GPINTENB, который после запуска и так нулевой:

/* IOCON с проверкой. Если микросхема пережила сброс микроконтроллера с BANK = 1, номер 0x0A у неё —
 * OLATA, а IOCON — 0x05. Поэтому сначала 0 в 0x05: при BANK = 1 это IOCON (BANK станет 0), при
 * BANK = 0 — GPINTENB, который всё равно должен быть 0 после запуска. */
int mcp_init(mcp_t *d, uint8_t addr7, uint8_t iocon)
{
    uint8_t v;
    int rc;
    d->addr = addr7;
    d->xfers = d->errors = 0;
    iocon &= (uint8_t)~MCP_BANK;
    if ((rc = mcp_wr(d, MCP_B1_IOCON, 0x00u)) != MCP_OK)       /* заодно: отвечает ли кто-то по адресу */
        return rc;
    if ((rc = mcp_wr(d, MCP_IOCON, iocon)) != MCP_OK)
        return rc;
    if ((rc = mcp_rd(d, MCP_IOCON, &v)) != MCP_OK)
        return rc;
    if (v != iocon)
        return MCP_E_CHIP;                                      /* по адресу не MCP23017 или он не в BANK = 0 */
    d->iocon = iocon;
    return mcp_rd16(d, MCP_OLATA, &d->olat);                    /* копия защёлки — из самой микросхемы */
}

В модели mcp_init(&dev, 0x20, 0x44) — 4 обмена: 05 00, 0A 44, чтение IOCON (0x44) и чтение двух OLAT (00 00); на шине 1450 мкс на 100 кГц и 362,5 мкс на 400 кГц. Наивный запуск — сразу 0x44 в 0x0A и проверка чтением того же номера — при оставшемся BANK = 1 тоже прочитал 0x44 и решил, что всё хорошо. На деле IOCON остался 0x80, а 0x44 легли в OLATA. Дальше всё мимо: номер 0x12, который драйвер считает GPIOA, при BANK = 1 — GPINTENB, читается нулём, и скан клавиатуры при одной нажатой кнопке 5 вернул 0xFFFF — нажаты все 16. С записью в 0x05 тот же запуск дал IOCON = 0x44, OLATA = 0x00 и скан 0x0020 — кнопка 5.

Выходы — через копию OLAT. Чтение GPIO отдаёт уровни на выводах, а не то, что записано. У перегруженного выхода — светодиод без резистора, ёмкость, чужой выход на той же линии — уровень может не совпасть с защёлкой. Опыт в модели: порт B — выходы, B7 = 1, но нагрузка держит его в нуле. Включаем B0 «как обычно»: прочитать GPIO, поставить бит, записать в GPIO. Чтение вернуло 0x00, запись положила в OLATB 0x01 вместо 0x81: B7 выключен навсегда, и когда нагрузку сняли, он так и остался нулём. На это ушло 2 обмена, 215 мкс на 400 кГц. Драйвер держит копию OLAT в памяти и пишет только защёлку — 1 обмен, 72,5 мкс, OLATB = 0x81:

/* Выходы меняются по копии OLAT: чтение GPIO вернуло бы уровни на выводах, а не то, что записано. */
int mcp_set(mcp_t *d, uint16_t mask, uint16_t val)
{
    uint16_t n = (uint16_t)((d->olat & ~mask) | (val & mask));
    int rc;
    if ((mask & 0xFF00u) && (mask & 0x00FFu))
        rc = mcp_wr16(d, MCP_OLATA, n);
    else if (mask & 0xFF00u)
        rc = mcp_wr(d, MCP_OLATA + MCP_PORTB, (uint8_t)(n >> 8));
    else
        rc = mcp_wr(d, MCP_OLATA, (uint8_t)n);
    if (rc == MCP_OK)
        d->olat = n;
    return rc;
}

QMK в своих номерах регистров так и пишет: запись в GPIO меняет OLAT. Zephyr тоже не читает GPIO перед записью — он меняет бит в своей копии.

Последовательный режим и BANK. Чтение четырёх байт с 0x12 (порт A — входы с подтяжкой, 0xFF; порт B — выходы, 0x5A) в модели дало:

IOCON Номер Четыре байта Что читалось
0x00 0x12 FF 5A 00 5A GPIOA, GPIOB, OLATA, OLATB
0x20 (SEQOP) 0x12 FF 5A FF 5A номер ходит внутри пары A/B
0xA0 (BANK, SEQOP) 0x09 FF FF FF FF номер стоит: GPIOA раз за разом

Это поведение — по документации производителя, в модели допущение. Польза последнего режима — частые отсчёты одного порта: одна транзакция на 16 байт заняла 435 мкс, 27,2 мкс на отсчёт, против 97,5 мкс у отдельного чтения. Драйвер этого раздела работает при BANK = 0 и SEQOP = 0.


22и.4 Прерывания: по изменению, по DEFVAL, INTCAP и MIRROR

Линия с GPINTEN = 1 вызывает прерывание порта: при INTCON = 0 — когда её уровень меняется, при INTCON = 1 — пока он отличается от DEFVAL. Сработав, порт записывает в INTF линию, в INTCAP — уровни всего порта в этот момент и держит INT, пока не прочитают его GPIO или INTCAP. Пока INT держится, новые изменения не записываются. Zephyr снимает прерывание чтением INTCAP; остальное — по документации производителя, в модели допущение.

Опыт в модели, клавиатура в покое (строки в нуле, столбцы с подтяжкой и GPINTEN): нажата кнопка 0 — INTA упал, INTF = 0x01, INTCAP = 0x0E, GPIO = 0x0E (столбец 0 в нуле). До чтения кнопку 0 отпустили и нажали кнопку 1: GPIO уже 0x0D, а INTCAP по-прежнему 0x0E — снимок того мгновения, когда случилось первое изменение. Чтение INTF и INTCAP одной транзакцией вернуло 0x0001 и 0x000E, и INTA отпустился. Отпускание снова уронило INTA (INTF = 0x02), а чтение GPIOA (0x0F) снова его сняло. INTFA, INTFB, INTCAPA и INTCAPB лежат подряд — драйвер берёт их четырьмя байтами:

/* INTFA, INTFB, INTCAPA, INTCAPB лежат подряд с 0x0E: четыре байта одной транзакцией.
 * Чтение INTCAP снимает прерывание. */
int mcp_irq(mcp_t *d, uint16_t *intf, uint16_t *intcap)
{
    uint8_t b[4];
    int rc = xfer_done(d, mcp_port_read(d->addr, MCP_INTFA, b, 4));
    if (rc == MCP_OK) {
        *intf = (uint16_t)(b[0] | b[1] << 8);
        *intcap = (uint16_t)(b[2] | b[3] << 8);
    }
    return rc;
}

Сравнение с DEFVAL. При INTCON = 0x0F и DEFVAL = 0x0F (столбцы в покое — единицы) прерывание означает «не в покое», а не «изменилось». Пока кнопка 11 держится, чтение INTCAP (INTF = 0x0008, INTCAP = 0x0007) снимает прерывание, и оно тут же встаёт снова: после трёх чтений INTA всё ещё в нуле, прерывание вставало 3 раза. Отпустили — следующее чтение отпустило INTA. INT в нуле весь срок удержания, и EXTI по спаду увидит одно прерывание на нажатие. Zephyr так и пишет: одного фронта MCP23017 аппаратно не различает, «нажатие» и «отпускание» драйвер выделяет сам по INTCAP.

MIRROR, ODR, INTPOL. Событие на входе B0 при IOCON = 0x04 уронило только INTB, при 0x44 — и INTA, и INTB. Уровни INT в модели:

IOCON INT в покое INT при прерывании
0x00 1 (двухтактный) 0
0x02 (INTPOL) 0 1
0x04 (ODR) отпущен 0
0x06 (ODR и INTPOL) отпущен 0

С ODR = 1 активный уровень — ноль при любом INTPOL. Открытый сток нужен, когда INT нескольких микросхем сведены на один вывод: двухтактный выход той, что в покое, держит единицу против нуля соседки.


22и.5 Клавиатура 4×4 на одном порту: скан, антидребезг, призраки

Матрица — как в 22.5, только на порту A расширителя: столбцы GPA0…GPA3 — входы с подтяжкой GPPU, строки GPA4…GPA7 с нулём в OLAT. Строку «включают», делая её выходом, остальные строки — входы: неактивная строка отпущена, как открытый сток в 22.5. Скан — 4 пары «записать IODIRA, прочитать GPIOA» и возврат всех строк в ноль, 9 обменов:

/* Один скан: 4 строки по очереди, на каждую — запись IODIRA и чтение GPIOA; в конце все строки снова
 * выходы с нулём. raw: бит 4 × строка + столбец, 1 — нажата. */
int kp_scan(mcp_t *d, uint16_t *raw)
{
    uint16_t keys = 0;
    uint8_t v;
    int rc;
    for (uint32_t r = 0; r < 4u; r++) {
        uint8_t dir = (uint8_t)(0xFFu & ~(0x10u << r));          /* выход — только строка r */
        if ((rc = mcp_wr(d, MCP_IODIRA, dir)) != MCP_OK)
            return rc;
        if ((rc = mcp_rd(d, MCP_GPIOA, &v)) != MCP_OK)
            return rc;
        keys |= (uint16_t)((~v & KP_COLS) << (4u * r));          /* нажатая кнопка — ноль на столбце */
    }
    if ((rc = mcp_wr(d, MCP_IODIRA, KP_COLS)) != MCP_OK)
        return rc;
    *raw = keys;
    return MCP_OK;
}

Дребезг. Модель клавиатуры нажимает 100 кнопок за 43,3 с; у каждого нажатия и отпускания дребезг от 1,00 до 7,98 мс: контакт переключается через 50…1000 мкс (допущение модели, порядок — как в 8.5). Скан раз в 5 мс без всякой обработки — каждое изменение сырого скана считается событием — дал 7 лишних нажатий на 100: скан попадал на отскок контакта. Антидребезг принимает новое состояние, когда 4 скана подряд одинаковы, — это не меньше 15 мс без перемен:

/* Антидребезг: новое состояние принимается, когда KP_STABLE сканов подряд одинаковы.
 * Возвращает 1, если принятое состояние сменилось; down и up — какие кнопки нажаты и отпущены. */
int kp_debounce(kp_t *k, uint16_t raw, uint16_t *down, uint16_t *up)
{
    uint16_t ch;
    *down = *up = 0;
    if (raw == k->last) {
        if (k->same < KP_STABLE)
            k->same++;
    } else {
        k->last = raw;
        k->same = 1;
    }
    if (k->same < KP_STABLE || raw == k->stable)
        return 0;
    ch = (uint16_t)(raw ^ k->stable);
    *down = (uint16_t)(ch & raw);
    *up = (uint16_t)(ch & k->stable);
    k->stable = raw;
    return 1;
}

С ним лишних нажатий 0 из 100, а от нажатия до события — в среднем 19,93 мс, от 15,43 до 27,91 мс (22и.6).

Призраки и диоды. Скан драйвера по всем наборам нажатых кнопок без диодов: пары — 0 ошибок из 120, тройки — 144 из 560, ровно как в 22.5; кнопки 0, 1 и 4 показали ещё и 5. Четвёрки — 1152 ошибки из 1820. С диодом в каждой кнопке (ток только из столбца в строку) — 0 ошибок у пар, троек и четвёрок.


22и.6 Время на шине и реакция: прерывание против опроса

Такты SCL на транзакцию в модели: байт — 9 тактов, старт, повторный старт и стоп — по одному; пауз между байтами нет (допущение):

Обмен Тактов SCL 100 кГц 400 кГц
запись одного регистра 29 290 мкс 72,5 мкс
чтение одного регистра 39 390 мкс 97,5 мкс
чтение двух портов (GPIOA и GPIOB) 48 480 мкс 120 мкс
INTF и INTCAP обоих портов 66 660 мкс 165 мкс
скан клавиатуры, 9 обменов 301 3010 мкс 752,5 мкс

Скан раз в 5 мс занимает шину на 60,2 % на 100 кГц и на 15,05 % на 400 кГц. Режим прерываний убирает сканы, пока ничего не нажато: в покое все строки в нуле, первое касание роняет столбец, INT вызывает шаг клавиатуры, и дальше сканы идут раз в 5 мс, пока всё не отпущено и не устоялось. В конце драйвер читает GPIOA: сканы сами дёргали столбцы и могли снова поставить INT.

/* Шаг клавиатуры: вызывать раз в KP_SCAN_MS, а в режиме прерываний ещё и по спаду INT (irq = 1).
 * В режиме прерываний в покое шина молчит: шаг без irq сразу возвращает KP_IDLE. */
int kp_step(mcp_t *d, kp_t *k, int irq, uint16_t *down, uint16_t *up)
{
    uint16_t raw, f, cap;
    uint8_t v;
    int rc;
    *down = *up = 0;
    if (k->use_irq && !k->active) {
        if (!irq)
            return KP_IDLE;
        if ((rc = mcp_irq(d, &f, &cap)) != MCP_OK)              /* что случилось — и снять прерывание */
            return rc;
        k->active = 1;
    }
    if ((rc = kp_scan(d, &raw)) != MCP_OK)
        return rc;
    (void)kp_debounce(k, raw, down, up);
    if (k->use_irq && k->stable == 0u && raw == 0u && k->same >= KP_STABLE) {
        /* всё отпущено и устоялось: сканы сами дёргали столбцы и могли снова поставить INT — снять его,
         * и если столбцы все в единице, вернуться в покой */
        if ((rc = mcp_rd(d, MCP_GPIOA, &v)) != MCP_OK)
            return rc;
        if ((v & KP_COLS) == KP_COLS)
            k->active = 0;
    }
    return k->use_irq && !k->active ? KP_IDLE : KP_ACTIVE;
}

В прошивке шаг зовёт основной цикл: по флагу EXTI, пока клавиатура в покое, и раз в 5 мс по SysTick, пока идут сканы:

    for (;;) {
        int irq, rc, due = (int32_t)(ms - next) >= 0;
        if (kp.active ? !due : !irq_flag) {
            __WFI();                                    /* SysTick или EXTI разбудят */
            continue;
        }
        __disable_irq();
        irq = irq_flag;
        irq_flag = 0;
        __enable_irq();
        if (!kp.active)
            next = ms;                                  /* первый скан — сразу по INT */
        next += KP_SCAN_MS;
        rc = kp_step(&mcp, &kp, irq, &down, &up);
        if (rc < 0)
            bus_errors++;
        if (down) {
            uint16_t key = 0;
            while (!(down >> key & 1u))
                key++;
            presses++;
            (void)mcp_set(&mcp, 0xFF00u, (uint16_t)((key + 1u) << 8));
        }
    }

Те же 100 нажатий на 400 кГц:

Способ Лишних нажатий Первый признак нажатия: среднее / худшее От нажатия до события: среднее (от … до) Обменов в секунду Шина
опрос, без антидребезга 7 4,48 / 11,78 мс 4,83 мс (0,43 … 12,02) 1800 15,05 %
опрос, антидребезг 0 4,48 / 12,32 мс 19,93 мс (15,43 … 27,91) 1800 15,05 %
прерывание, антидребезг 0 0,17 / 0,17 мс 18,65 мс (16,67 … 26,67) 853 7,16 %

По прерыванию программа узнаёт о касании через 0,17 мс — это чтение INTF и INTCAP, — а опрос в среднем через 4,48 мс. Но событие «нажата» в обоих случаях ждёт четыре одинаковых скана, и выигрыш прерывания — 1,28 мс в среднем: время съедает антидребезг. Главный выигрыш в другом — в покое шина молчит. За эти 43,3 с прерывания сделали 36 756 обменов вместо 77 886.


22и.7 Грабли

Прерывание пришло один раз и больше не приходит. INT держится, пока не прочитают GPIO или INTCAP; чтение одного INTF его не снимает. EXTI ловит фронт (RM0008, с. 211), а фронта больше нет. В модели кнопка на B0 нажата 10 раз: программа, которая по спаду читает только INTF, увидела 1 прерывание и 1 нажатие из 10, INTA остался в нуле. Программа с чтением INTF и INTCAP увидела все 10 — и 164 прерывания: дребезг роняет INT много раз за нажатие, антидребезг нужен и здесь.

Нажата кнопка, а читаются все шестнадцать. Микросхема пережила сброс микроконтроллера с BANK = 1, и драйвер пишет и читает не те регистры (22и.3). Первая запись — 0 в 0x05, проверка IOCON чтением, а лучше ещё и импульс на RESET при запуске.

Выход сам выключился. Выход меняли через чтение GPIO: уровень перегруженного вывода попал в OLAT (22и.3). Писать надо в OLAT по своей копии.

Микросхема то отвечает, то нет. Вывод A0 не подключён. В модели висящий A0 в каждой транзакции читается случайно, и из 1000 обменов по адресу 0x20 без ответа остались 494. A2…A0 — только на GND или питание. По документации производителя RESET тоже надо держать резистором к питанию, если он не идёт к микроконтроллеру (не проверено).

Входы показывают мусор. Нет подтяжки: после включения GPPU = 0, и отпущенная кнопка оставляет вход висящим. Включить GPPU у столбцов или поставить резисторы.

Две микросхемы на одном INT мешают друг другу. После включения INT двухтактный: в покое одна держит единицу, другая тянет ноль. ODR = 1 и одна подтяжка на линию.

Выводы микроконтроллера под 5 В. Расширитель на 5 В подтягивает SDA, SCL и INT к 5 В. PB6–PB9 это терпят (FT, DocID13587, с. 32–33), выводы без FT — нет; диапазон питания MCP23017 — по документации производителя, не проверено.

Лишние нажатия. Скан без антидребезга — 7 лишних на 100 (22и.5).

Ложная кнопка при трёх нажатых. Нет диодов — 144 ошибки из 560 троек.

Прошивка «зависла» в обмене. Ведомый держит SDA после сбоя посреди байта — BUSY не снимается. Восстановление шины — 17.6.


22и.8 Итоги

Полные исходники

mcp22j.h — регистры, биты IOCON, порт, драйвер и клавиатура: типы и прототипы.

/* mcp22j.h — раздел 22и: драйвер расширителя портов MCP23017 (16 линий по I2C) и клавиатура 4×4 на его
 * порту A, без регистров микроконтроллера. Драйвер говорит с микросхемой через функции порта mcp_port_*:
 * в прошивке fw22j.c их делает I2C1 STM32F103, в стенде host22j.c — модель model22j.py.
 * Номера регистров при BANK = 0, бит MIRROR и сброс INTCAP чтением совпадают с драйверами Zephyr
 * (gpio_mcp23xxx.h) и QMK; остальные биты IOCON, карта при BANK = 1 и поведение прерываний — по
 * документации производителя, не проверено: документа в проекте нет. */
#ifndef MCP22J_H
#define MCP22J_H
#include <stdint.h>

/* ── регистры при BANK = 0: у порта A чётный номер, у порта B — следующий ─ */
#define MCP_IODIRA    0x00u             /* 1 — вход, 0 — выход; после сброса 0xFF */
#define MCP_IPOLA     0x02u             /* 1 — читать вход инверсным */
#define MCP_GPINTENA  0x04u             /* 1 — линия может вызвать прерывание */
#define MCP_DEFVALA   0x06u             /* с чем сравнивать при INTCON = 1 */
#define MCP_INTCONA   0x08u             /* 0 — по изменению, 1 — по отличию от DEFVAL */
#define MCP_IOCON     0x0Au             /* один регистр на два номера: 0x0A и 0x0B */
#define MCP_GPPUA     0x0Cu             /* 1 — подтяжка к питанию на входе */
#define MCP_INTFA     0x0Eu             /* какая линия вызвала прерывание (только чтение) */
#define MCP_INTCAPA   0x10u             /* состояние порта в момент прерывания (только чтение) */
#define MCP_GPIOA     0x12u             /* чтение — уровни на выводах, запись — в OLAT */
#define MCP_OLATA     0x14u             /* защёлка выходов: что записано, а не что на выводе */
#define MCP_PORTB     1u                /* прибавить к номеру — регистр порта B */
/* ── при BANK = 1 порт A — 0x00…0x0A, порт B — 0x10…0x1A ─────────────── */
#define MCP_B1_IOCON  0x05u
#define MCP_B1_GPIOA  0x09u
#define MCP_B1_OLATA  0x0Au

/* ── IOCON ────────────────────────────────────────────────────────────── */
#define MCP_BANK      0x80u             /* 1 — регистры портов раздельно */
#define MCP_MIRROR    0x40u             /* 1 — INTA и INTB сигналят о любом порте */
#define MCP_SEQOP     0x20u             /* 1 — номер регистра после байта не растёт */
#define MCP_DISSLW    0x10u
#define MCP_HAEN      0x08u             /* для MCP23S17 (SPI); у MCP23017 адрес задают A2…A0 всегда */
#define MCP_ODR       0x04u             /* 1 — INT открытый сток */
#define MCP_INTPOL    0x02u             /* активный уровень INT при ODR = 0: 0 — ноль */

#define MCP_ADDR_BASE 0x20u             /* A2…A0 на GND; 0x20…0x27 */
#define MCP_RESET_US  2u                /* RESET в нуле — столько держит драйвер Zephyr */

enum { MCP_OK = 0, MCP_E_NACK = -1, MCP_E_BUS = -2, MCP_E_CHIP = -3 };

typedef struct {
    uint8_t addr;                       /* семибитный адрес: 0x20 + A2…A0 */
    uint8_t iocon;                      /* что записано в IOCON */
    uint16_t olat;                      /* копия OLAT: порт A — младший байт, B — старший */
    uint32_t xfers, errors;             /* обменов по I2C и ошибок */
} mcp_t;

/* ── порт: в прошивке — I2C1, в стенде — строки к модели ──────────────── */
/* записать n байт одной транзакцией: START, адрес, байты, STOP; первый байт — номер регистра */
int mcp_port_write(uint8_t addr7, const uint8_t *tx, uint32_t n);
/* прочитать n байт с регистра reg: запись номера, повторный старт, чтение, STOP */
int mcp_port_read(uint8_t addr7, uint8_t reg, uint8_t *rx, uint32_t n);
void mcp_port_delay_us(uint32_t us);

/* ── драйвер ──────────────────────────────────────────────────────────── */
int mcp_wr(mcp_t *d, uint8_t reg, uint8_t v);
int mcp_wr16(mcp_t *d, uint8_t reg, uint16_t v);           /* порт A, потом B — одной транзакцией */
int mcp_rd(mcp_t *d, uint8_t reg, uint8_t *v);
int mcp_rd16(mcp_t *d, uint8_t reg, uint16_t *v);
int mcp_init(mcp_t *d, uint8_t addr7, uint8_t iocon);
int mcp_set(mcp_t *d, uint16_t mask, uint16_t val);        /* выходы: чтение-модификация-запись по копии OLAT */
int mcp_irq(mcp_t *d, uint16_t *intf, uint16_t *intcap);   /* INTF и INTCAP одной транзакцией — прерывание снято */

/* ── клавиатура 4×4 на порту A: столбцы GPA0…GPA3 — входы с подтяжкой, строки GPA4…GPA7 ── */
#define KP_COLS       0x0Fu
#define KP_ROWS       0xF0u
#define KP_SCAN_MS    5u                /* шаг сканирования */
#define KP_STABLE     4u                /* столько одинаковых сканов подряд — состояние принято */
#define KP_SCAN_XFERS 9u                /* обменов на один скан: 4 × (IODIR + GPIO) + возврат строк */

typedef struct {
    uint16_t stable;                    /* принятое состояние: бит 4 × строка + столбец */
    uint16_t last;                      /* прошлый скан */
    uint8_t same;                       /* сколько сканов подряд last не менялся */
    uint8_t use_irq;                    /* 1 — в покое ждать INT, 0 — сканировать всегда */
    uint8_t active;                     /* режим прерываний: идут сканы */
} kp_t;

enum { KP_IDLE = 0, KP_ACTIVE = 1 };

int kp_init(mcp_t *d, kp_t *k, int use_irq);
int kp_scan(mcp_t *d, uint16_t *raw);
int kp_debounce(kp_t *k, uint16_t raw, uint16_t *down, uint16_t *up);
int kp_step(mcp_t *d, kp_t *k, int irq, uint16_t *down, uint16_t *up);

#endif

mcp22j.c — драйвер MCP23017 и клавиатура 4×4 с антидребезгом — общий для прошивки и стенда.

/* mcp22j.c — раздел 22и: драйвер MCP23017 и клавиатура 4×4. Один и тот же файл собирается в прошивку
 * (armclang, Cortex-M3) и в стенд на компьютере (gcc): микроконтроллера он не знает, только порт mcp_port_*. */
#include "mcp22j.h"

static int xfer_done(mcp_t *d, int rc)
{
    d->xfers++;
    if (rc)
        d->errors++;
    return rc;
}

int mcp_wr(mcp_t *d, uint8_t reg, uint8_t v)
{
    uint8_t b[2] = {reg, v};
    return xfer_done(d, mcp_port_write(d->addr, b, 2));
}

int mcp_wr16(mcp_t *d, uint8_t reg, uint16_t v)
{
    uint8_t b[3] = {reg, (uint8_t)v, (uint8_t)(v >> 8)};    /* SEQOP = 0: номер растёт, A → B */
    return xfer_done(d, mcp_port_write(d->addr, b, 3));
}

int mcp_rd(mcp_t *d, uint8_t reg, uint8_t *v)
{
    return xfer_done(d, mcp_port_read(d->addr, reg, v, 1));
}

int mcp_rd16(mcp_t *d, uint8_t reg, uint16_t *v)
{
    uint8_t b[2];
    int rc = xfer_done(d, mcp_port_read(d->addr, reg, b, 2));
    if (rc == MCP_OK)
        *v = (uint16_t)(b[0] | b[1] << 8);
    return rc;
}

/* IOCON с проверкой. Если микросхема пережила сброс микроконтроллера с BANK = 1, номер 0x0A у неё —
 * OLATA, а IOCON — 0x05. Поэтому сначала 0 в 0x05: при BANK = 1 это IOCON (BANK станет 0), при
 * BANK = 0 — GPINTENB, который всё равно должен быть 0 после запуска. */
int mcp_init(mcp_t *d, uint8_t addr7, uint8_t iocon)
{
    uint8_t v;
    int rc;
    d->addr = addr7;
    d->xfers = d->errors = 0;
    iocon &= (uint8_t)~MCP_BANK;
    if ((rc = mcp_wr(d, MCP_B1_IOCON, 0x00u)) != MCP_OK)       /* заодно: отвечает ли кто-то по адресу */
        return rc;
    if ((rc = mcp_wr(d, MCP_IOCON, iocon)) != MCP_OK)
        return rc;
    if ((rc = mcp_rd(d, MCP_IOCON, &v)) != MCP_OK)
        return rc;
    if (v != iocon)
        return MCP_E_CHIP;                                      /* по адресу не MCP23017 или он не в BANK = 0 */
    d->iocon = iocon;
    return mcp_rd16(d, MCP_OLATA, &d->olat);                    /* копия защёлки — из самой микросхемы */
}

/* Выходы меняются по копии OLAT: чтение GPIO вернуло бы уровни на выводах, а не то, что записано. */
int mcp_set(mcp_t *d, uint16_t mask, uint16_t val)
{
    uint16_t n = (uint16_t)((d->olat & ~mask) | (val & mask));
    int rc;
    if ((mask & 0xFF00u) && (mask & 0x00FFu))
        rc = mcp_wr16(d, MCP_OLATA, n);
    else if (mask & 0xFF00u)
        rc = mcp_wr(d, MCP_OLATA + MCP_PORTB, (uint8_t)(n >> 8));
    else
        rc = mcp_wr(d, MCP_OLATA, (uint8_t)n);
    if (rc == MCP_OK)
        d->olat = n;
    return rc;
}

/* INTFA, INTFB, INTCAPA, INTCAPB лежат подряд с 0x0E: четыре байта одной транзакцией.
 * Чтение INTCAP снимает прерывание. */
int mcp_irq(mcp_t *d, uint16_t *intf, uint16_t *intcap)
{
    uint8_t b[4];
    int rc = xfer_done(d, mcp_port_read(d->addr, MCP_INTFA, b, 4));
    if (rc == MCP_OK) {
        *intf = (uint16_t)(b[0] | b[1] << 8);
        *intcap = (uint16_t)(b[2] | b[3] << 8);
    }
    return rc;
}

/* Столбцы — входы с подтяжкой и прерыванием по изменению; строки — выходы с нулём в OLAT.
 * Строку «включают», делая её выходом (IODIR = 0), остальные строки — входы без подтяжки: так
 * неактивная строка не держит единицу, и две кнопки в одном столбце не замыкают выход на выход.
 * В покое все строки — выходы с нулём: любая нажатая кнопка роняет свой столбец, и приходит INT. */
int kp_init(mcp_t *d, kp_t *k, int use_irq)
{
    int rc;
    k->stable = k->last = 0;
    k->same = 0;
    k->use_irq = (uint8_t)(use_irq != 0);
    k->active = 0;
    if ((rc = mcp_set(d, KP_ROWS, 0x00u)) != MCP_OK)
        return rc;
    if ((rc = mcp_wr(d, MCP_GPPUA, KP_COLS)) != MCP_OK)
        return rc;
    if ((rc = mcp_wr(d, MCP_IODIRA, KP_COLS)) != MCP_OK)        /* все строки — выходы с нулём */
        return rc;
    if ((rc = mcp_wr(d, MCP_INTCONA, 0x00u)) != MCP_OK)         /* по изменению, DEFVAL не нужен */
        return rc;
    if ((rc = mcp_wr(d, MCP_GPINTENA, use_irq ? KP_COLS : 0x00u)) != MCP_OK)
        return rc;
    {
        uint8_t v;
        return mcp_rd(d, MCP_GPIOA, &v);                        /* снять прерывание, если оно уже стоит */
    }
}

/* Один скан: 4 строки по очереди, на каждую — запись IODIRA и чтение GPIOA; в конце все строки снова
 * выходы с нулём. raw: бит 4 × строка + столбец, 1 — нажата. */
int kp_scan(mcp_t *d, uint16_t *raw)
{
    uint16_t keys = 0;
    uint8_t v;
    int rc;
    for (uint32_t r = 0; r < 4u; r++) {
        uint8_t dir = (uint8_t)(0xFFu & ~(0x10u << r));          /* выход — только строка r */
        if ((rc = mcp_wr(d, MCP_IODIRA, dir)) != MCP_OK)
            return rc;
        if ((rc = mcp_rd(d, MCP_GPIOA, &v)) != MCP_OK)
            return rc;
        keys |= (uint16_t)((~v & KP_COLS) << (4u * r));          /* нажатая кнопка — ноль на столбце */
    }
    if ((rc = mcp_wr(d, MCP_IODIRA, KP_COLS)) != MCP_OK)
        return rc;
    *raw = keys;
    return MCP_OK;
}

/* Антидребезг: новое состояние принимается, когда KP_STABLE сканов подряд одинаковы.
 * Возвращает 1, если принятое состояние сменилось; down и up — какие кнопки нажаты и отпущены. */
int kp_debounce(kp_t *k, uint16_t raw, uint16_t *down, uint16_t *up)
{
    uint16_t ch;
    *down = *up = 0;
    if (raw == k->last) {
        if (k->same < KP_STABLE)
            k->same++;
    } else {
        k->last = raw;
        k->same = 1;
    }
    if (k->same < KP_STABLE || raw == k->stable)
        return 0;
    ch = (uint16_t)(raw ^ k->stable);
    *down = (uint16_t)(ch & raw);
    *up = (uint16_t)(ch & k->stable);
    k->stable = raw;
    return 1;
}

/* Шаг клавиатуры: вызывать раз в KP_SCAN_MS, а в режиме прерываний ещё и по спаду INT (irq = 1).
 * В режиме прерываний в покое шина молчит: шаг без irq сразу возвращает KP_IDLE. */
int kp_step(mcp_t *d, kp_t *k, int irq, uint16_t *down, uint16_t *up)
{
    uint16_t raw, f, cap;
    uint8_t v;
    int rc;
    *down = *up = 0;
    if (k->use_irq && !k->active) {
        if (!irq)
            return KP_IDLE;
        if ((rc = mcp_irq(d, &f, &cap)) != MCP_OK)              /* что случилось — и снять прерывание */
            return rc;
        k->active = 1;
    }
    if ((rc = kp_scan(d, &raw)) != MCP_OK)
        return rc;
    (void)kp_debounce(k, raw, down, up);
    if (k->use_irq && k->stable == 0u && raw == 0u && k->same >= KP_STABLE) {
        /* всё отпущено и устоялось: сканы сами дёргали столбцы и могли снова поставить INT — снять его,
         * и если столбцы все в единице, вернуться в покой */
        if ((rc = mcp_rd(d, MCP_GPIOA, &v)) != MCP_OK)
            return rc;
        if ((v & KP_COLS) == KP_COLS)
            k->active = 0;
    }
    return k->use_irq && !k->active ? KP_IDLE : KP_ACTIVE;
}

fw22j.c — прошивка STM32F103: I2C1, INTA на EXTI8, RESET, клавиатура и светодиоды на порту B.

/* fw22j.c — раздел 22и: MCP23017 на STM32F103 (BluePill), 72 МГц, APB1 = 36 МГц.
 * I2C1: PB6 — SCL, PB7 — SDA (открытый сток, 400 кГц). A2…A0 на GND — адрес 0x20.
 * INTA → PB8: вход с подтяжкой, EXTI8 по спаду; у MCP23017 INT — открытый сток (ODR = 1), MIRROR = 1.
 * RESET MCP23017 ← PB9. Клавиатура 4×4 на порту A (столбцы GPA0…GPA3, строки GPA4…GPA7, кнопки с диодами),
 * восемь светодиодов на порту B показывают номер последней нажатой кнопки. SysTick — 1 мс.
 * Тактирование до 72 МГц — как в разделе 6. */
#include "stm32f10x.h"
#include "mcp22j.h"

#define CPU_HZ          72000000u
#define APB1_MHZ        36u
#define I2C_TIMEOUT_US  1000u           /* самая длинная транзакция — 66 тактов SCL, 165 мкс; ждём каждый флаг отдельно */

static mcp_t mcp;
static kp_t kp;
static volatile uint32_t ms;
static volatile uint8_t irq_flag;
volatile uint32_t bus_errors, presses;

static int expired(uint32_t t0, uint32_t us)
{
    return DWT->CYCCNT - t0 >= us * (CPU_HZ / 1000000u);
}

void mcp_port_delay_us(uint32_t us)
{
    uint32_t t0 = DWT->CYCCNT;
    while (!expired(t0, us)) {
    }
}

void SysTick_Handler(void)
{
    ms++;
}

/* INTA: спад на PB8 — только флаг, обмен по I2C идёт в основном цикле */
void EXTI9_5_IRQHandler(void)
{
    EXTI->PR = 1u << 8;
    irq_flag = 1;
}

static void pins_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    /* PB6, PB7 — альтернативная функция, открытый сток, 50 МГц (1111) */
    GPIOB->CRL = (GPIOB->CRL & 0x00FFFFFFu) | 0xFF000000u;
    /* PB8 — вход с подтяжкой (1000), PB9 — выход 2 МГц (0010), RESET отпущен */
    GPIOB->BSRR = 1u << 8 | 1u << 9;
    GPIOB->CRH = (GPIOB->CRH & ~0xFFu) | 0x28u;
}

static void i2c1_init(void)
{
    I2C1->CR1 = 0;                                      /* настраивать при PE = 0 */
    I2C1->CR2 = APB1_MHZ;
    I2C1->CCR = I2C_CCR_FS | 30u;                       /* DUTY = 0: 36 МГц / (3 × 30) = 400 кГц */
    I2C1->TRISE = APB1_MHZ * 300u / 1000u + 1u;         /* 300 нс × 36 МГц = 10,8 → 10 + 1 = 11 */
    I2C1->CR1 = I2C_CR1_PE;
}

static void exti_init(void)
{
    AFIO->EXTICR[2] = (AFIO->EXTICR[2] & ~0xFu) | 0x1u;    /* EXTI8 ← PB8 */
    EXTI->FTSR |= 1u << 8;
    EXTI->IMR |= 1u << 8;
    NVIC_EnableIRQ(EXTI9_5_IRQn);
}

static void mcp_reset(void)
{
    GPIOB->BRR = 1u << 9;
    mcp_port_delay_us(MCP_RESET_US);
    GPIOB->BSRR = 1u << 9;
}

static int sr1_wait(uint16_t mask)
{
    uint32_t t0 = DWT->CYCCNT;
    while (!(I2C1->SR1 & mask)) {
        if (I2C1->SR1 & I2C_SR1_AF)
            return MCP_E_NACK;                          /* адрес или байт не подтверждён */
        if (expired(t0, I2C_TIMEOUT_US))
            return MCP_E_BUS;
    }
    return MCP_OK;
}

static int fail(int rc)
{
    I2C1->CR1 |= I2C_CR1_STOP;
    I2C1->CR1 &= (uint16_t)~(I2C_CR1_POS | I2C_CR1_ACK);
    I2C1->SR1 = (uint16_t)~I2C_SR1_AF;                  /* AF снимается записью нуля */
    return rc;
}

/* START, адрес на запись и номер регистра (или все байты записи) */
static int start_write(uint8_t addr7, const uint8_t *tx, uint32_t n)
{
    int rc;
    uint32_t t0 = DWT->CYCCNT;
    while (I2C1->SR2 & I2C_SR2_BUSY) {                  /* SDA держит ведомый — раздел 17.6 */
        if (expired(t0, I2C_TIMEOUT_US))
            return MCP_E_BUS;
    }
    I2C1->CR1 |= I2C_CR1_START;
    if ((rc = sr1_wait(I2C_SR1_SB)) != MCP_OK)
        return rc;
    I2C1->DR = (uint8_t)(addr7 << 1);
    if ((rc = sr1_wait(I2C_SR1_ADDR)) != MCP_OK)
        return rc;
    (void)I2C1->SR2;
    for (uint32_t i = 0; i < n; i++) {
        if ((rc = sr1_wait(I2C_SR1_TXE)) != MCP_OK)
            return rc;
        I2C1->DR = tx[i];
    }
    return sr1_wait(I2C_SR1_BTF);
}

int mcp_port_write(uint8_t addr7, const uint8_t *tx, uint32_t n)
{
    int rc = start_write(addr7, tx, n);
    if (rc != MCP_OK)
        return fail(rc);
    I2C1->CR1 |= I2C_CR1_STOP;
    return MCP_OK;
}

/* Приём ведущим по RM0008, с. 763–764: один байт, два байта и больше двух — три разные процедуры */
int mcp_port_read(uint8_t addr7, uint8_t reg, uint8_t *rx, uint32_t n)
{
    int rc;
    if (n == 0u)
        return MCP_E_BUS;
    if ((rc = start_write(addr7, &reg, 1)) != MCP_OK)
        return fail(rc);
    I2C1->CR1 |= I2C_CR1_START;                         /* повторный старт */
    if ((rc = sr1_wait(I2C_SR1_SB)) != MCP_OK)
        return fail(rc);
    I2C1->CR1 |= n == 2u ? (I2C_CR1_POS | I2C_CR1_ACK) : I2C_CR1_ACK;
    I2C1->DR = (uint8_t)(addr7 << 1 | 1u);
    if ((rc = sr1_wait(I2C_SR1_ADDR)) != MCP_OK)
        return fail(rc);
    if (n == 1u) {
        I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;            /* ACK снять до сброса ADDR */
        (void)I2C1->SR2;
        I2C1->CR1 |= I2C_CR1_STOP;
        if ((rc = sr1_wait(I2C_SR1_RXNE)) != MCP_OK)
            return fail(rc);
        rx[0] = (uint8_t)I2C1->DR;
        return MCP_OK;
    }
    (void)I2C1->SR2;
    if (n == 2u) {
        I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;
        if ((rc = sr1_wait(I2C_SR1_BTF)) != MCP_OK)
            return fail(rc);
        I2C1->CR1 |= I2C_CR1_STOP;
        rx[0] = (uint8_t)I2C1->DR;
        rx[1] = (uint8_t)I2C1->DR;
        I2C1->CR1 &= (uint16_t)~I2C_CR1_POS;
        return MCP_OK;
    }
    for (uint32_t i = 0; i + 3u < n; i++) {             /* все, кроме трёх последних */
        if ((rc = sr1_wait(I2C_SR1_RXNE)) != MCP_OK)
            return fail(rc);
        rx[i] = (uint8_t)I2C1->DR;
    }
    if ((rc = sr1_wait(I2C_SR1_BTF)) != MCP_OK)         /* N−2 в DR, N−1 в сдвиговом регистре */
        return fail(rc);
    I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;
    rx[n - 3u] = (uint8_t)I2C1->DR;
    if ((rc = sr1_wait(I2C_SR1_BTF)) != MCP_OK)
        return fail(rc);
    I2C1->CR1 |= I2C_CR1_STOP;
    rx[n - 2u] = (uint8_t)I2C1->DR;
    if ((rc = sr1_wait(I2C_SR1_RXNE)) != MCP_OK)
        return fail(rc);
    rx[n - 1u] = (uint8_t)I2C1->DR;
    return MCP_OK;
}

int main(void)
{
    uint16_t down, up;
    uint32_t next = 0;
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    SysTick_Config(CPU_HZ / 1000u);
    pins_init();
    i2c1_init();
    mcp_reset();
    while (mcp_init(&mcp, MCP_ADDR_BASE, MCP_MIRROR | MCP_ODR) != MCP_OK)
        mcp_port_delay_us(100000u);                     /* нет ответа: провод, адрес, питание */
    (void)mcp_wr(&mcp, MCP_IODIRA + MCP_PORTB, 0x00u);  /* порт B — выходы на светодиоды */
    (void)kp_init(&mcp, &kp, 1);
    exti_init();
    for (;;) {
        int irq, rc, due = (int32_t)(ms - next) >= 0;
        if (kp.active ? !due : !irq_flag) {
            __WFI();                                    /* SysTick или EXTI разбудят */
            continue;
        }
        __disable_irq();
        irq = irq_flag;
        irq_flag = 0;
        __enable_irq();
        if (!kp.active)
            next = ms;                                  /* первый скан — сразу по INT */
        next += KP_SCAN_MS;
        rc = kp_step(&mcp, &kp, irq, &down, &up);
        if (rc < 0)
            bus_errors++;
        if (down) {
            uint16_t key = 0;
            while (!(down >> key & 1u))
                key++;
            presses++;
            (void)mcp_set(&mcp, 0xFF00u, (uint16_t)((key + 1u) << 8));
        }
    }
}

Дальше: раздел 22к «Резистивный тачскрин и контроллер XPT2046 по SPI: измерение X/Y, давление, калибровка по трём точкам и фильтрация дребезга» — от кнопок к пальцу на экране: резистивный тачскрин на том же SPI, что и дисплей, калибровка и жесты.

Открыть в интерактивном справочнике