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 подавление дребезга
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 Итоги
- MCP23017: 16 линий в двух портах, адрес 0x20 + A2…A0 (до 8 микросхем на
шине); при
BANK= 0 регистры портов A и B идут парами, с 0x00 по 0x15; после включенияIODIR= 0xFF, остальное — 0. - Подключение к F103: I2C1 на PB6/PB7, 400 кГц; INTA на PB8 — EXTI8 по
спаду; RESET — PB9;
IOCON= 0x44 —MIRRORи открытый сток INT. - Запуск — 4 обмена, 362,5 мкс: сначала 0 в 0x05 на случай
BANK= 1, иначе скан с одной кнопкой показывает 0xFFFF. - Выходы — через копию
OLAT: 1 обмен, 72,5 мкс; через чтениеGPIO— 2 обмена, 215 мкс, и перегруженный выход теряет свой бит. INTCAP— снимок порта в момент прерывания; INT снимает чтениеINTCAPилиGPIO, а неINTF: без этого 1 нажатие из 10, с ним — 10 из 10 и 164 прерывания от дребезга.- Клавиатура: скан — 9 обменов, 301 такт: 752,5 мкс на 400 кГц, 15,05 % шины при скане раз в 5 мс (60,2 % на 100 кГц); антидребезг на 4 скана убирает 7 лишних нажатий из 100.
- По прерыванию касание видно через 0,17 мс, но событие — через 18,65 мс против 19,93 мс опросом: выигрыш — в тишине на шине, 853 обмена в секунду вместо 1800.
- Без диодов — 144 ложных тройки из 560, с диодами — 0.
Полные исходники
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, ®, 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, что и дисплей, калибровка и жесты.