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

18д. NFC-модуль PN532: кадры по I2C/SPI/UART, ACK, чтение UID и NTAG2xx, эмуляция и отличие от RC522

Раздел 18г разобрал считыватель RC522: прошивка там сама пишет регистры MFRC522, сама ведёт антиколлизию, считает CRC_A и ждёт ответ карты по таймеру. Модуль PN532 работает на том же диапазоне 13,56 МГц и с теми же картами ISO 14443A, но устроен иначе: внутри него свой микроконтроллер со своей прошивкой, и разговор с ним — это команды и ответы в кадрах с контрольными суммами. Здесь — кадр, три шины на выбор, поиск метки и UID, страницы меток NTAG213 и NTAG215, запись и чтение ссылки в формате NDEF, сон, таймауты и что делать, когда PN532 молчит. Метка с двумя входами ST25DV была в разделе 94а, I²C — в разделе 17, SPI — в разделе 18.

Опоры: RM0008 (DocID13902 Rev 17): APB2 — до 72 МГц, APB1 — до 36 МГц, с. 48; бит SPI1EN, с. 112; I2C1EN и USART2EN, с. 115; выводы USART, SPI и I²C — таблицы 24, 25 и 27, с. 165–167; без ремапа SPI1 — PA4–PA7, USART2 — TX/PA2, RX/PA3, I2C1 — SCL/PB6, SDA/PB7, с. 185; SPI_CR1 — LSBFIRST (бит 7) и BR[2:0], с. 743; I2C_CR1.ACK, с. 774; FREQ, с. 776; флаг AF, с. 779; SB и ADDR, с. 781; CCR и TRISE, с. 784; флаги TXE и RXNE USART, с. 819. DocID13587 Rev 17 (STM32F103x8/xB): PA2–PA7 — без допуска 5 В, с. 29; PA9 — с допуском, с. 31; PB6 и PB7 — с допуском 5 В, с. 32. Документа на PN532 в проекте нет, драйвера PN532 нет ни в дереве Zephyr, ни в других исходниках на диске: формат кадра, коды команд и ответов, адрес, байты шины SPI, побудка по UART взяты по документации производителя и не проверены. Команды меток NTAG, их объём и разметка NDEF сверены с исходниками прошивки Flipper Zero (коммит 7f0b6e1). Все числа ниже — прогон драйвера _verify/nfc18f/pn18f.c на компьютере (gcc 12.2.0) со своей моделью PN532 и меток на Python 3.11; прошивка собрана armclang 6.24 под Cortex-M3.

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

Оригинал Перевод
host controller ведущий: наш микроконтроллер
normal / extended information frame обычный / расширенный кадр
LEN, LCS — length checksum длина и её контрольная сумма
TFI — frame identifier направление кадра: D4 или D5
DCS — data checksum контрольная сумма данных
ACK / NACK frame кадр «принял» / «повтори»
error frame кадр ошибки
HSU — high speed UART UART модуля
SAM — security access module внешний модуль безопасности
SENS_RES / SEL_RES ATQA / SAK из раздела 18г
target, initiator метка, считыватель
CC — capability container служебная страница NDEF
TLV — type, length, value тип, длина, значение

Вверху — подключение модуля PN532 к STM32F103 тремя способами, их выбирают перемычки модуля, а микросхема читает их только при включении: I2C1 на PB6 и PB7, адрес 0x24, 100 кГц; SPI1 на PA4–PA7, младшим битом вперёд, 2,25 МГц; USART2 на PA2 и PA3, 115 200 бод; PB1 — вывод IRQ, ноль — ответ готов; внутри модуля 80C51 со своей прошивкой, антенна на 13,56 МГц и метка NTAG213 с UID 04 6F 1C 2A 5B 3D 80 и страницей CC E1 10 12 00; PA2–PA7 без допуска 5 В, PB6 и PB7 — с допуском. Посередине — кадр: преамбула 00, начало 00 FF, LEN и LCS, TFI D4 к PN532 или D5 от него, данные, DCS и 00 в конце; LEN + LCS и TFI + данные + DCS дают 0 по модулю 256; ACK — 00 00 FF 00 FF 00, NACK — 00 00 FF FF 00 00. Внизу — обмен командой GetFirmwareVersion: кадр 00 00 FF 02 FE D4 02 2A 00, ответ ACK, затем ответ 00 00 FF 06 FA D5 03 32 01 06 07 E8 00 — IC 32, версия 1.6; готовность — байт 01 по I2C или SPI или вывод IRQ; при неверной DCS ведущий шлёт NACK, если ответа нет — ACK, и PN532 бросает команду


18д.1 Чем PN532 отличается от RC522

У MFRC522 из раздела 18г наружу смотрят регистры: FIFO, флаги, таймер, сопроцессор CRC. Всё остальное — REQA, антиколлизию, каскады UID, SELECT, проверку CRC_A — делает прошивка нашего микроконтроллера. В PN532 внутри своё ядро 80C51 с прошивкой в ПЗУ (по документации производителя, не проверено), и всю эту работу делает оно. Ведущий даёт команду InListPassiveTarget — и получает готовый UID, ATQA и SAK одним ответом.

Цена — другой протокол. Регистров нет, есть кадры: команда уходит в кадре с длиной и двумя контрольными суммами, PN532 сначала подтверждает приём кадром ACK, потом, когда закончит работу, отдаёт ответ. Пока ответа нет, ведущий ждёт: опрашивает байт готовности или смотрит на вывод IRQ.

RC522 (раздел 18г) PN532
с чем говорит прошивка регистры MFRC522 команды прошивки PN532
антиколлизия, CRC_A в нашей прошивке внутри PN532
шины SPI на модулях I²C, SPI или UART — перемычками
подтверждение приёма нет кадр ACK
эмуляция карты нет TgInitAsTarget

Один и тот же кадр ходит по всем трём шинам. Меняется только обёртка: по I²C перед ответом приходит байт готовности, по SPI каждую транзакцию начинает служебный байт, по UART микросхему сначала будят. Драйвер этого раздела pn18f.c прячет обёртку в трёх маленьких функциях, а команды над ними общие.


18д.2 Кадр: LEN, LCS, TFI, DCS, ACK и NACK

Кадр — 00 (преамбула), 00 FF (начало), LEN, LCS, TFI, данные, DCS, 00 (конец). LEN считает TFI и данные. LCS подбирают так, чтобы LEN + LCS = 0 по модулю 256; DCS — так, чтобы TFI + данные + DCS = 0. TFI говорит, куда идёт кадр: D4 — от ведущего к PN532, D5 — обратно. Первый байт данных — код команды, в ответе — код плюс один.

uint32_t pn_frame(uint8_t *out, uint8_t tfi, const uint8_t *d, uint32_t n)
{
    uint32_t len = n + 1u, k = 0;       /* LEN считает TFI и данные */
    uint8_t sum = tfi;
    out[k++] = 0x00;                    /* преамбула */
    out[k++] = 0x00;                    /* код начала: 00 FF */
    out[k++] = 0xFF;
    if (len <= 254u) {
        out[k++] = (uint8_t)len;
        out[k++] = (uint8_t)(0x100u - len);              /* LCS: LEN + LCS = 0 по модулю 256 */
    } else {                            /* расширенный кадр: FF FF LENM LENL LCS */
        out[k++] = 0xFF;
        out[k++] = 0xFF;
        out[k++] = (uint8_t)(len >> 8);
        out[k++] = (uint8_t)len;
        out[k++] = (uint8_t)(0x100u - ((len >> 8) + (len & 0xFFu)));
    }
    out[k++] = tfi;
    for (uint32_t i = 0; i < n; i++) {
        out[k++] = d[i];
        sum = (uint8_t)(sum + d[i]);
    }
    out[k++] = (uint8_t)(0x100u - sum);                  /* DCS: TFI + данные + DCS = 0 по модулю 256 */
    out[k++] = 0x00;                    /* постамбула */
    return k;
}

Примеры из прогона — драйвер на Си и независимый расчёт на Python дали одно и то же:

команда кадр
GetFirmwareVersion 00 00 FF 02 FE D4 02 2A 00
SAMConfiguration 00 00 FF 05 FB D4 14 01 14 01 02 00
InListPassiveTarget 00 00 FF 04 FC D4 4A 01 00 E1 00
ответ на GetFirmwareVersion 00 00 FF 06 FA D5 03 32 01 06 07 E8 00

У GetFirmwareVersion LEN = 2 (TFI и код 02), LCS = FE; D4 + 02 = D6, и DCS = 2A. Ещё 200 случайных кадров с данными от 1 до 259 байт Си и Python собрали одинаково, и драйвер разобрал их обратно — все 200.

ACK и NACK. ACK — 00 00 FF 00 FF 00: PN532 так говорит «кадр принят», а ведущий — «брось текущую команду». NACK — 00 00 FF FF 00 00: ведущий просит повторить последний ответ. Это особые кадры: у обоих LEN + LCS = FF, а не 0, и разборщик обычных кадров отвергает их как кадры с неверной суммой. Поэтому драйвер узнаёт ACK по всем шести байтам сразу. Кадр ошибки — 00 00 FF 01 FF 7F 81 00: один байт данных 7F без TFI; в модели так PN532 отвечает на незнакомую команду (FE — кадр 00 00 FF 02 FE D4 FE 2E 00).

Расширенный кадр. LEN в обычном кадре — один байт. Когда данных больше, после 00 FF идут FF FF, два байта длины (старший первым) и LCS над ними. Драйвер переходит на расширенный кадр, когда LEN больше 254 (граница — по документации производителя, не проверено): кадр с 260 байтами данных — LEN 261 = 01 05, начало 00 00 FF FF FF 01 05 FA D4, всего 271 байт.

Проверка ответа. Кадр ответа с одним испорченным битом драйвер ловит по DCS, с испорченной длиной — по LCS. Оба случая в прогоне дали «неверная сумма» — и драйвер просит повтор кадром NACK (18д.6).


18д.3 Три шины: I²C, SPI и HSU

Шину выбирают перемычки или переключатели на модуле; микросхема читает их один раз, при включении (по документации производителя, не проверено).

I²C. Адрес — 0x24, в линии 0x48 при записи и 0x49 при чтении (о сдвинутом адресе — раздел 17.2). Команда — обычная запись кадра. Ответ читают иначе, чем у датчиков: первый байт любого чтения — байт готовности, 01 — кадр готов. Драйвер читает один байт, пока не увидит 01, потом читает байт готовности и кадр одним чтением. I2C1 F103 — на PB6 и PB7 (с. 185), открытый сток (таблица 27, с. 167), 100 кГц: FREQ = 36, CCR = 180, TRISE = 37 при APB1 36 МГц.

SPI. Каждую транзакцию начинает служебный байт: 01 — запись кадра (DW), 02 — чтение байта готовности (SR), 03 — чтение кадра (DR). Байты идут младшим битом вперёд. В SPI F103 для этого есть бит LSBFIRST (бит 7 SPI_CR1, с. 743), и порядок бит меняет сам блок — драйвер его не видит. Режим 0, делитель 32 (BR = 100): 72 МГц / 32 = 2,25 МГц — с запасом от 5 МГц, предела SPI PN532 по документации производителя (не проверено).

    SPI1->CR1 = SPI_CR1_MSTR | SPI_BR | SPI_CR1_LSBFIRST | SPI_CR1_SSM | SPI_CR1_SSI;   /* режим 0 */
    SPI1->CR1 |= SPI_CR1_SPE;

HSU — UART модуля, 115 200 бод по умолчанию. Байта готовности нет: кадры просто приходят потоком, и драйвер читает пять байт заголовка, а потом столько, сколько сказал LEN. Зато после включения и после сна PN532 по UART надо разбудить: перед первой командой драйвер шлёт 55 55 и 14 нулей. USART2 F103 — PA2 и PA3 (с. 185), на APB1 36 МГц BRR = 313.

Готовность. Одна функция на две шины и на вывод IRQ:

static int ready(pn_t *p)
{
    uint8_t tx[2] = {PN_SPI_SR, 0}, rx[2] = {0, 0}, st = 0;
    if (p->use_irq)
        return pn_irq() == 0;           /* IRQ вниз — ответ готов */
    if (p->bus == PN_BUS_I2C)
        return pn_i2c_read(p->i2c_addr, &st, 1) == 0 && (st & 0x01u);
    pn_spi(tx, rx, 2);
    return rx[1] & 0x01u;
}

Одна команда GetFirmwareVersion на трёх шинах в модели (ACK готов через 0,5 мс после кадра, ответ — ещё через 1 мс; это допущения модели):

шина операций байт на шине время
I²C, 100 кГц, опрос 6 39 4,63 мс
I²C, IRQ 3 33 4,03 мс
SPI, 2,25 МГц, опрос 7 39 2,139 мс
SPI, IRQ 3 31 2,11 мс
HSU, 115 200 бод 5 28 3,41 мс

По I²C 39 байт — это запись кадра с адресом (10), три опроса по 2 байта, ACK с байтом готовности и адресом (8) и ответ (15). Вывод IRQ убирает опросы с шины: те же 3 проверки готовности идут по выводу PB1, и на шине остаются 33 байта.


18д.4 Поиск метки: SAMConfiguration, GetFirmwareVersion, InListPassiveTarget

pn_begin даёт две команды. SAMConfiguration 14 01 14 01 — обычный режим без внешнего модуля безопасности и вывод IRQ включён (по документации производителя, не проверено); ответ — код 15. GetFirmwareVersion 02 — ответ 03 32 01 06 07: микросхема 32 (PN532), версия 1.6, 07 — какие типы меток поддержаны. Так модель повторяет описание производителя (не проверено); настоящий чип может отдать другую версию. Если ответа нет совсем — дальше идти бессмысленно: шина, перемычки или питание (18д.7).

InListPassiveTarget 4A 01 00 — найти одну метку на 106 кбит/с, тип A. PN532 сам шлёт REQA, ведёт антиколлизию по каскадам, проверяет CRC_A и выбирает метку — всё то, что в разделе 18г было нашим кодом. Ответ на метку NTAG213 в модели:

00 00 FF 0F F1 D5 4B 01 01 00 44 00 07 04 6F 1C 2A 5B 3D 80 C2 00

После D5 4B: одна метка (01), её номер для следующих команд (01), ATQA 00 44, SAK 00, длина UID 07 и сам UID 04 6F 1C 2A 5B 3D 80. ATQA PN532 отдаёт старшим байтом вперёд — в эфире, как в 18г, это 44 00 (порядок — по документации производителя, не проверено). Поиск занял 6,217 мс по SPI. Карта MIFARE Classic 1K из раздела 18г по I²C дала ATQA 00 04, SAK 08 и UID 3A 9F 12 C4 из 4 байт за 9,49 мс.

Сколько искать. По умолчанию PN532 ищет метку без конца: параметр MaxRetries пассивной активации равен FF (по документации производителя, не проверено; в модели так). Команда RFConfiguration 32 05 FF 01 nn ограничивает число попыток: прошивка раздела ставит 10. Что бывает без этого — в 18д.6.


18д.5 NTAG213 и NTAG215: страницы, READ и WRITE, ссылка в NDEF

Метки NTAG21x — потомки MIFARE Ultralight: память из страниц по 4 байта, без ключей и шифра. У NTAG213 45 страниц, у NTAG215 — 135 (так в исходниках Flipper Zero, mf_ultralight.c). Команды метки идут через InDataExchange 40 01 …: PN532 передаёт их метке номер 1 и возвращает ответ с байтом состояния, 00 — успех.

Страницы 0–2 — UID, две контрольные суммы BCC и байты блокировок: 04 6F 1C FF | 2A 5B 3D 80 | CC 48 00 00. BCC0 = 88 ⊕ 04 ⊕ 6F ⊕ 1C = FF, BCC1 = 2A ⊕ 5B ⊕ 3D ⊕ 80 = CC — та же формула, что в Flipper Zero. Страница 3 — CC: E1 10 12 00. E1 — метка размечена под NDEF, 10 — версия 1.0, 12 — размер области данных в восьмёрках байт: 18 · 8 = 144 байта, страницы 4–39. У NTAG215 CC — E1 10 3E 00, 62 · 8 = 496 байт.

Один READ с адреса 3 приносит и CC, и начало данных — ntag_read за 4,21 мс:

E1 10 12 00 01 03 A0 0C 34 03 00 FE 00 00 00 00

Это заводская разметка NTAG213 (такая же — в генераторе меток Flipper Zero): TLV 01 длиной 3 — где лежат биты блокировок, пустое сообщение NDEF 03 00 и конец FE. У NTAG215 — сразу 03 00 FE.

Ссылка в NDEF. Сообщение NDEF лежит в TLV 03. Запись ссылки — заголовок D1 (первая и последняя запись, короткая, тип NFC Forum), длина типа 1, длина данных, тип U, код приставки и остаток адреса. Приставки кодируются числом: 01 — http://www., 02 — https://www., 03 — http://, 04 — https://. Для адреса https://example.com/pn532 ndef_uri дала 25 байт:

03 16 D1 01 12 55 04 65 78 61 6D 70 6C 65 2E 63 6F 6D 2F 70 6E 35 33 32 FE

Длина записи 16 = 22: 4 байта заголовка и 18 байт данных — код 04 и 17 букв example.com/pn532.

    while (i < have && m[i] != 0x03u && m[i] != 0xFEu) {   /* пропустить TLV блокировок и памяти */
        if (m[i] == 0x00u) {
            i++;
            continue;
        }
        i += 2u + m[i + 1u];
    }
    at = i;                             /* сюда ляжет новое сообщение; TLV до него не трогаем */

ntag_write_url читает CC, пропускает TLV 01 и кладёт сообщение со смещения 5 — со второго байта страницы 5. Первый байт этой страницы, 34 от TLV блокировок, драйвер переписывает тем же. Записано 7 страниц (5–11) за 8 команд — один READ и семь WRITE, — 54,529 мс; в модели запись страницы занимает 6 мс, это допущение. Обратно ntag_read_url прочитала ссылку тремя READ за 12,629 мс, и независимый разбор памяти метки на Python нашёл тот же адрес.

Длинная ссылка. Адрес из 300 знаков не влезает в короткую запись: данных 293 байта, больше 255. Тогда заголовок — C1 (без признака короткой записи), а длина данных — 4 байта; запись — 300 байт, и длина TLV тоже не влезает в байт: 03 FF 01 2C. На NTAG215 вышло 305 байт, 77 страниц, 557,72 мс; чтение назад — 20 команд, 84,196 мс. На NTAG213 та же ссылка не влезает в 144 байта, и драйвер отказывается писать, не тронув метку.


18д.6 MIFARE Classic, эмуляция карты, таймауты и сон

MIFARE Classic через PN532. Те же команды карты, что в разделе 18г, идут через InDataExchange: аутентификация 60 с номером блока, 6 байтами ключа и UID, потом READ 30 — 16 байт. Шифр Crypto1 PN532 ведёт сам, как и MFRC522. Всё сказанное в 18г.6 остаётся в силе: MIFARE Classic и UID не годятся для защиты, какой бы считыватель ни стоял. Модель стенда Classic только находит, но не читает.

Эмуляция карты. Командой TgInitAsTarget (8C) PN532 сам притворяется меткой: отвечает считывателю, например телефону, и обменивается с ним данными командами TgGetData и TgSetData. Так делают обмен «телефон — прибор» без отдельной метки. По документации производителя часть UID при этом задаёт сама микросхема, и копию чужой карты так не сделать (не проверено). В драйвере раздела эмуляции нет, и она не моделировалась.

Таймауты. Каждая команда ждёт ACK 10 мс, ответ — сколько ей отведено. Если ответа нет, драйвер шлёт ACK: PN532 бросает команду и готов к следующей. В модели поиск без метки с заводским числом попыток не кончился никогда: драйвер ждал 100 мс, отправил ACK — 124,09 мс и 103 опроса, — и следующая команда GetFirmwareVersion прошла, как обычно. С RFConfiguration на 5 повторов тот же поиск вернул «меток нет» за 16,69 мс.

Неверная сумма. В модели один бит ответа на GetFirmwareVersion перевернулся на линии: D5 13 вместо D5 03. DCS не сошлась, драйвер отправил NACK, PN532 повторил ответ — и команда прошла с одним повтором за 3,228 мс вместо 2,139 мс.

Сон. PowerDown 16 B0 усыпляет PN532 с побудкой по I²C, SPI и UART (биты побудки — по документации производителя, не проверено). Как он просыпается в модели:

шина как будит драйвер GetFirmwareVersion после сна без побудки
I²C первый адрес не подтверждён — повтор через 1 мс 6,55 мс ошибка шины: адрес без ACK
SPI пустая транзакция и пауза 1 мс 3,146 мс нет ACK, 10,114 мс
HSU 55 55 и нули перед кадром 4,799 мс нет ACK, 10,781 мс

Поведение при побудке по I²C и SPI — допущение модели: документа нет. Вывод из таблицы не зависит от деталей: после сна первый обмен теряется, и драйвер должен это ждать.


18д.7 Грабли

PN532 молчит: не та шина. Перемычки стоят на HSU, а прошивка говорит по I²C — адрес не подтверждается, pn_begin возвращает ошибку шины. Переставить перемычки на работающем модуле мало: в модели после перестановки ответ тот же, и только после выключения и включения pn_begin — SAMConfiguration и GetFirmwareVersion — проходит за 9,17 мс.

PN532 молчит: не тот порядок бит SPI. Без LSBFIRST блок SPI шлёт байты старшим битом вперёд, и PN532 видит вместо DW 01 байт 80, вместо SR 02 — 40. Так и вышло в модели: ни одной понятной транзакции, нет ACK через 10,124 мс. Если блок SPI не умеет младшим битом вперёд, байты переворачивают программно — на F103 бит есть.

Неверный адрес I²C. 0x48 — это уже сдвинутый адрес. С ним драйвер трижды получил NACK на адрес — 5,76 мс и ошибка шины; с 0x24 — ответ за 4,63 мс.

Модуль от 5 В. PN532 — микросхема на 3,3 В. Многие модули ставят свой стабилизатор и подтягивают I²C к своему питанию (по описаниям продавцов, не проверено). PB6 и PB7 F103 выдержат 5 В (с. 32), а PA2–PA7 — нет (с. 29): по SPI и UART модуль — только от 3,3 В.

Длинные провода. Сбой на линии ловят LCS и DCS; на ответ с неверной суммой драйвер отвечает NACK, а не принимает его (18д.6). Но не принятая команда выглядит как «нет ACK», и после десятков сантиметров проводов на I²C и SPI лучше снизить частоту.

Пропущенный ACK. Если не прочитать ACK, а сразу ждать ответ, по I²C и SPI ведущий прочитает ACK вместо ответа. Зависшую команду — например, поиск без метки — снимают кадром ACK от ведущего; без него PN532 занят ею (по документации производителя, не проверено).

Поиск без конца. Без RFConfiguration InListPassiveTarget без метки не отвечает никогда: прошивка ждёт до своего таймаута, а без него зависает.


18д.8 Итоги

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

pn18f.h — команды и ответы PN532, кадр, интерфейс драйвера и платформы.

/* pn18f.h — раздел 18д: драйвер NFC-модуля PN532 без регистров STM32.
 * Драйвер говорит с микросхемой кадрами через функции платформы: I2C, SPI или UART (HSU) — что выбрано перемычками.
 * В прошивке fw18f.c их даёт STM32F103, в стенде host18f.c — модель model18f.py.
 * Формат кадра, коды команд и ответов PN532 — по документации производителя (в проекте её нет, не проверено);
 * команды NTAG2xx и разметка NDEF сверены с исходниками прошивки Flipper Zero (выдержки — flipper_ntag.txt). */
#ifndef PN18F_H
#define PN18F_H
#include <stdint.h>

/* ── кадр ─────────────────────────────────────────────────────────────── */
#define PN_TFI_HOST   0xD4u             /* кадр от ведущего к PN532 */
#define PN_TFI_CHIP   0xD5u             /* кадр от PN532 к ведущему */
#define PN_I2C_ADDR   0x24u             /* семибитный адрес: в линии 0x48 — запись, 0x49 — чтение */
#define PN_SPI_DW     0x01u             /* первый байт SPI: запись данных */
#define PN_SPI_SR     0x02u             /* чтение байта готовности */
#define PN_SPI_DR     0x03u             /* чтение данных */
#define PN_MAXDATA    265u              /* TFI и данные расширенного кадра — с запасом */
#define PN_FRAME_MAX  (PN_MAXDATA + 10u)

/* ── команды PN532 ────────────────────────────────────────────────────── */
#define PN_GETFW      0x02u             /* GetFirmwareVersion */
#define PN_SAMCONFIG  0x14u             /* SAMConfiguration */
#define PN_POWERDOWN  0x16u
#define PN_RFCONFIG   0x32u             /* RFConfiguration */
#define PN_INEXCHANGE 0x40u             /* InDataExchange */
#define PN_INLIST     0x4Au             /* InListPassiveTarget */
#define PN_INRELEASE  0x52u
#define PN_TGINIT     0x8Cu             /* TgInitAsTarget — эмуляция карты, в драйвере не используется */

/* ── команды метки NTAG2xx ────────────────────────────────────────────── */
#define NTAG_READ     0x30u             /* 16 байт: четыре страницы по 4 байта */
#define NTAG_WRITE    0xA2u             /* одна страница: 4 байта */

/* ── ответ драйвера ───────────────────────────────────────────────────── */
enum { PN_OK, PN_TIMEOUT, PN_NOACK, PN_FRAME, PN_CHECKSUM, PN_ERRFRAME, PN_BUS, PN_STATUS, PN_NOTAG, PN_NOSPACE, PN_NDEF };
enum { PN_BUS_I2C = 1, PN_BUS_SPI, PN_BUS_HSU };

extern const uint8_t PN_ACK[6], PN_NACK[6];

typedef struct {
    uint8_t sens_res[2];                /* ATQA — так, как его отдаёт PN532 */
    uint8_t sel_res;                    /* SAK */
    uint8_t uid[10];
    uint8_t uid_len;
} pn_tag_t;

typedef struct {
    uint8_t bus;                        /* PN_BUS_I2C, PN_BUS_SPI или PN_BUS_HSU */
    uint8_t i2c_addr;                   /* 0x24; другое — опыт с неверным адресом */
    uint8_t use_irq;                    /* 1 — ждать готовность по выводу IRQ, а не опросом шины */
    uint8_t no_wake;                    /* 1 — не будить после сна (опыт «PN532 молчит») */
    uint8_t asleep;                     /* драйвер считает, что микросхема спит */
    uint8_t fw[4];                      /* IC, Ver, Rev, Support из GetFirmwareVersion */
    uint8_t status;                     /* байт состояния последнего InDataExchange */
    uint32_t polls, nacks, retries, aborts, wakes, reads, writes;
} pn_t;

/* ── платформа: даёт прошивка или стенд ───────────────────────────────── */
int pn_i2c_write(uint8_t addr7, const uint8_t *d, uint32_t n);   /* 0 — адрес и все байты подтверждены */
int pn_i2c_read(uint8_t addr7, uint8_t *d, uint32_t n);          /* 0 — адрес подтверждён */
void pn_spi(const uint8_t *tx, uint8_t *rx, uint32_t n);         /* NSS вниз, n байт младшим битом вперёд, NSS вверх */
void pn_uart_write(const uint8_t *d, uint32_t n);
uint32_t pn_uart_read(uint8_t *d, uint32_t n, uint32_t timeout_ms);   /* сколько байт пришло */
int pn_irq(void);                                                /* уровень IRQ: 0 — есть что читать */
void pn_delay_ms(uint32_t ms);

/* ── кадры ────────────────────────────────────────────────────────────── */
uint32_t pn_frame(uint8_t *out, uint8_t tfi, const uint8_t *d, uint32_t n);   /* -> длина кадра */
int pn_unframe(const uint8_t *f, uint32_t n, uint8_t *d, uint32_t *dn);       /* d — TFI и данные */

/* ── команды ──────────────────────────────────────────────────────────── */
int pn_cmd(pn_t *p, const uint8_t *cmd, uint32_t n, uint8_t *resp, uint32_t *rn, uint32_t rmax, uint32_t timeout_ms);
void pn_abort(pn_t *p);
int pn_begin(pn_t *p, uint8_t bus);
int pn_getfw(pn_t *p);
int pn_sam(pn_t *p);
int pn_retries(pn_t *p, uint8_t passive);
int pn_list(pn_t *p, pn_tag_t *t, uint32_t timeout_ms);
int pn_exchange(pn_t *p, const uint8_t *tx, uint32_t n, uint8_t *rx, uint32_t *rn, uint32_t rmax);
int pn_release(pn_t *p);
int pn_sleep(pn_t *p);

/* ── NTAG2xx и NDEF ───────────────────────────────────────────────────── */
int ntag_read(pn_t *p, uint8_t page, uint8_t out[16]);
int ntag_write(pn_t *p, uint8_t page, const uint8_t d[4]);
uint32_t ndef_uri(uint8_t *out, uint32_t cap, const char *url);              /* TLV 03 … FE -> длина или 0 */
int ndef_parse(const uint8_t *m, uint32_t n, char *url, uint32_t cap, uint32_t *tlv_at);
int ntag_write_url(pn_t *p, const char *url, uint32_t *pages);
int ntag_read_url(pn_t *p, char *url, uint32_t cap);

#endif

pn18f.c — драйвер без регистров STM32: общая часть прошивки и стенда.

/* pn18f.c — раздел 18д: драйвер PN532 — кадры с LCS и DCS, ACK и NACK, обёртки I2C, SPI и HSU, ожидание
 * готовности опросом или по IRQ, команды SAMConfiguration, GetFirmwareVersion, RFConfiguration,
 * InListPassiveTarget, InDataExchange, InRelease, PowerDown; страницы NTAG2xx и запись URL в NDEF. Без регистров STM32. */
#include <string.h>
#include "pn18f.h"

#define ACK_MS 10u                      /* ждать ACK, мс */
#define RESEND 2u                       /* сколько раз просить повтор (NACK) при неверной сумме */

const uint8_t PN_ACK[6] = {0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00};
const uint8_t PN_NACK[6] = {0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00};
static const uint8_t WAKE[16] = {0x55, 0x55};          /* HSU: 55 55 и нули — разбудить перед первой командой */

/* ── кадр: 00 00 FF LEN LCS TFI данные DCS 00 ─────────────────────────── */
uint32_t pn_frame(uint8_t *out, uint8_t tfi, const uint8_t *d, uint32_t n)
{
    uint32_t len = n + 1u, k = 0;       /* LEN считает TFI и данные */
    uint8_t sum = tfi;
    out[k++] = 0x00;                    /* преамбула */
    out[k++] = 0x00;                    /* код начала: 00 FF */
    out[k++] = 0xFF;
    if (len <= 254u) {
        out[k++] = (uint8_t)len;
        out[k++] = (uint8_t)(0x100u - len);              /* LCS: LEN + LCS = 0 по модулю 256 */
    } else {                            /* расширенный кадр: FF FF LENM LENL LCS */
        out[k++] = 0xFF;
        out[k++] = 0xFF;
        out[k++] = (uint8_t)(len >> 8);
        out[k++] = (uint8_t)len;
        out[k++] = (uint8_t)(0x100u - ((len >> 8) + (len & 0xFFu)));
    }
    out[k++] = tfi;
    for (uint32_t i = 0; i < n; i++) {
        out[k++] = d[i];
        sum = (uint8_t)(sum + d[i]);
    }
    out[k++] = (uint8_t)(0x100u - sum);                  /* DCS: TFI + данные + DCS = 0 по модулю 256 */
    out[k++] = 0x00;                    /* постамбула */
    return k;
}

int pn_unframe(const uint8_t *f, uint32_t n, uint8_t *d, uint32_t *dn)
{
    uint32_t i = 0, len;
    while (i + 1u < n && !(f[i] == 0x00 && f[i + 1u] == 0xFF))
        i++;                            /* преамбул бывает сколько угодно: ищем 00 FF */
    i += 2u;
    if (i + 2u > n)
        return PN_FRAME;
    if (f[i] == 0xFF && f[i + 1u] == 0xFF) {             /* расширенный */
        if (i + 5u > n || (uint8_t)(f[i + 2u] + f[i + 3u] + f[i + 4u]) != 0)
            return PN_CHECKSUM;
        len = (uint32_t)f[i + 2u] << 8 | f[i + 3u];
        i += 5u;
    } else {
        if ((uint8_t)(f[i] + f[i + 1u]) != 0)
            return PN_CHECKSUM;         /* LCS не сошёлся (у NACK тоже: FF + 00) */
        len = f[i];
        i += 2u;
    }
    if (len == 0u || len > PN_MAXDATA || i + len + 1u > n)
        return PN_FRAME;
    uint8_t sum = 0;
    for (uint32_t k = 0; k <= len; k++)
        sum = (uint8_t)(sum + f[i + k]);
    if (sum != 0)
        return PN_CHECKSUM;             /* DCS не сошёлся */
    memcpy(d, f + i, len);
    *dn = len;
    if (len == 1u && d[0] == 0x7Fu)
        return PN_ERRFRAME;             /* кадр ошибки 00 00 FF 01 FF 7F 81 00 */
    return PN_OK;
}

/* ── шина ─────────────────────────────────────────────────────────────── */
static int bus_write(pn_t *p, const uint8_t *f, uint32_t n)
{
    static uint8_t tx[PN_FRAME_MAX + 1u], rx[PN_FRAME_MAX + 1u];
    p->writes++;
    if (p->bus == PN_BUS_I2C) {
        for (uint32_t t = 0; t < 3u; t++) {                /* после сна первый адрес будит и теряется */
            if (pn_i2c_write(p->i2c_addr, f, n) == 0)
                return PN_OK;
            if (p->no_wake)
                break;
            p->retries++;
            pn_delay_ms(1);
        }
        return PN_BUS;
    }
    if (p->bus == PN_BUS_SPI) {
        tx[0] = PN_SPI_DW;
        memcpy(tx + 1, f, n);
        pn_spi(tx, rx, n + 1u);
        return PN_OK;
    }
    pn_uart_write(f, n);
    return PN_OK;
}

static void wake(pn_t *p)
{
    uint8_t tx[2] = {PN_SPI_SR, 0}, rx[2];
    if (!p->asleep)
        return;
    p->asleep = 0;
    if (p->no_wake)
        return;
    p->wakes++;
    if (p->bus == PN_BUS_HSU) {
        pn_uart_write(WAKE, sizeof WAKE);
    } else if (p->bus == PN_BUS_SPI) {
        pn_spi(tx, rx, 2);              /* NSS вниз будит; эта транзакция теряется */
        pn_delay_ms(1);
    }                                   /* I2C: будит первый адрес, его повторяет bus_write */
}

static int ready(pn_t *p)
{
    uint8_t tx[2] = {PN_SPI_SR, 0}, rx[2] = {0, 0}, st = 0;
    if (p->use_irq)
        return pn_irq() == 0;           /* IRQ вниз — ответ готов */
    if (p->bus == PN_BUS_I2C)
        return pn_i2c_read(p->i2c_addr, &st, 1) == 0 && (st & 0x01u);
    pn_spi(tx, rx, 2);
    return rx[1] & 0x01u;
}

static int wait_ready(pn_t *p, uint32_t timeout_ms)
{
    for (uint32_t ms = 0;; ms++) {
        p->polls++;
        if (ready(p))
            return PN_OK;
        if (ms >= timeout_ms)
            return PN_TIMEOUT;
        pn_delay_ms(1);
    }
}

/* Прочитать кадр: I2C и SPI — want байт после готовности, HSU — по потоку, сколько скажет LEN. */
static int read_frame(pn_t *p, uint8_t *f, uint32_t want, uint32_t timeout_ms, uint32_t *got)
{
    static uint8_t tx[PN_FRAME_MAX + 1u], rx[PN_FRAME_MAX + 1u];
    p->reads++;
    *got = 0;
    if (p->bus == PN_BUS_HSU) {
        uint32_t k = pn_uart_read(f, 5, timeout_ms), more;
        if (k < 5u)
            return PN_TIMEOUT;
        if (f[3] == 0x00 && f[4] == 0xFF)
            more = 1;                   /* ACK: остался байт 00 */
        else if (f[3] == 0xFF && f[4] == 0xFF) {
            if (pn_uart_read(f + 5, 3, ACK_MS) < 3u)
                return PN_TIMEOUT;
            k = 8;
            more = ((uint32_t)f[5] << 8 | f[6]) + 2u;
        } else
            more = (uint32_t)f[3] + 2u;
        if (k + more > PN_FRAME_MAX || pn_uart_read(f + k, more, ACK_MS) < more)
            return PN_TIMEOUT;
        *got = k + more;
        return PN_OK;
    }
    if (wait_ready(p, timeout_ms) != PN_OK)
        return PN_TIMEOUT;
    if (p->bus == PN_BUS_I2C) {
        if (pn_i2c_read(p->i2c_addr, rx, want + 1u) != 0 || !(rx[0] & 0x01u))
            return PN_BUS;
        memcpy(f, rx + 1, want);        /* первый байт — снова байт готовности */
    } else {
        memset(tx, 0, want + 1u);
        tx[0] = PN_SPI_DR;
        pn_spi(tx, rx, want + 1u);
        memcpy(f, rx + 1, want);
    }
    *got = want;
    return PN_OK;
}

void pn_abort(pn_t *p)
{
    p->aborts++;
    bus_write(p, PN_ACK, 6);            /* ACK от ведущего — «бросить текущую команду» */
}

int pn_cmd(pn_t *p, const uint8_t *cmd, uint32_t n, uint8_t *resp, uint32_t *rn, uint32_t rmax, uint32_t timeout_ms)
{
    static uint8_t f[PN_FRAME_MAX], d[PN_MAXDATA];
    uint32_t got = 0, dn = 0, len = pn_frame(f, PN_TFI_HOST, cmd, n);
    *rn = 0;
    wake(p);
    if (bus_write(p, f, len) != PN_OK)
        return PN_BUS;
    if (read_frame(p, f, 6, ACK_MS, &got) != PN_OK || memcmp(f, PN_ACK, 6) != 0)
        return PN_NOACK;                /* кадр не принят: нет связи, не тот режим, неверная сумма */
    for (uint32_t t = 0; t <= RESEND; t++) {
        int st = read_frame(p, f, rmax + 8u, timeout_ms, &got);
        if (st == PN_TIMEOUT) {
            pn_abort(p);
            return PN_TIMEOUT;
        }
        st = st == PN_OK ? pn_unframe(f, got, d, &dn) : st;
        if (st == PN_ERRFRAME)
            return PN_ERRFRAME;
        if (st != PN_OK) {
            p->nacks++;
            bus_write(p, PN_NACK, 6);   /* NACK — «пришли ответ ещё раз» */
            continue;
        }
        if (dn < 2u || d[0] != PN_TFI_CHIP || d[1] != (uint8_t)(cmd[0] + 1u))
            return PN_FRAME;
        memcpy(resp, d + 1, dn - 1u);   /* код ответа (команда + 1) и данные */
        *rn = dn - 1u;
        return PN_OK;
    }
    return PN_CHECKSUM;
}

/* ── команды ──────────────────────────────────────────────────────────── */
int pn_begin(pn_t *p, uint8_t bus)
{
    memset(p, 0, sizeof *p);
    p->bus = bus;
    p->i2c_addr = PN_I2C_ADDR;
    p->asleep = bus == PN_BUS_HSU;      /* по UART микросхему будят и после включения */
    int st = pn_sam(p);
    return st != PN_OK ? st : pn_getfw(p);
}

int pn_getfw(pn_t *p)
{
    uint8_t c = PN_GETFW, r[8];
    uint32_t n = 0;
    int st = pn_cmd(p, &c, 1, r, &n, 5, 20);
    if (st != PN_OK)
        return st;
    if (n != 5u)
        return PN_FRAME;
    memcpy(p->fw, r + 1, 4);            /* IC, Ver, Rev, Support */
    return PN_OK;
}

int pn_sam(pn_t *p)
{
    uint8_t c[4] = {PN_SAMCONFIG, 0x01, 0x14, 0x01}, r[4];   /* обычный режим, 0x14 × 50 мс, IRQ включён */
    uint32_t n = 0;
    return pn_cmd(p, c, 4, r, &n, 1, 20);
}

int pn_retries(pn_t *p, uint8_t passive)
{
    uint8_t c[5] = {PN_RFCONFIG, 0x05, 0xFF, 0x01, passive}, r[4];   /* MaxRetries: ATR, PSL, пассивная активация */
    uint32_t n = 0;
    return pn_cmd(p, c, 5, r, &n, 1, 20);
}

int pn_list(pn_t *p, pn_tag_t *t, uint32_t timeout_ms)
{
    uint8_t c[3] = {PN_INLIST, 0x01, 0x00}, r[24];          /* одна метка, 106 кбит/с, тип A */
    uint32_t n = 0;
    int st = pn_cmd(p, c, 3, r, &n, 17, timeout_ms);
    if (st != PN_OK)
        return st;
    if (n < 2u || r[1] == 0u)
        return PN_NOTAG;
    if (n < 7u || r[6] > 10u || n < 7u + r[6])
        return PN_FRAME;
    t->sens_res[0] = r[3];
    t->sens_res[1] = r[4];
    t->sel_res = r[5];
    t->uid_len = r[6];
    memcpy(t->uid, r + 7, r[6]);
    return PN_OK;
}

int pn_exchange(pn_t *p, const uint8_t *tx, uint32_t n, uint8_t *rx, uint32_t *rn, uint32_t rmax)
{
    uint8_t c[64], r[64];
    uint32_t k = 0;
    if (n + 2u > sizeof c || rmax + 2u > sizeof r)
        return PN_FRAME;
    c[0] = PN_INEXCHANGE;
    c[1] = 0x01;                        /* номер метки из InListPassiveTarget */
    memcpy(c + 2, tx, n);
    int st = pn_cmd(p, c, n + 2u, r, &k, rmax + 2u, 100);
    *rn = 0;
    if (st != PN_OK)
        return st;
    if (k < 2u)
        return PN_FRAME;
    p->status = r[1];
    if (r[1] & 0x3Fu)
        return PN_STATUS;               /* метка не ответила или отказала */
    memcpy(rx, r + 2, k - 2u);
    *rn = k - 2u;
    return PN_OK;
}

int pn_release(pn_t *p)
{
    uint8_t c[2] = {PN_INRELEASE, 0x00}, r[4];
    uint32_t n = 0;
    return pn_cmd(p, c, 2, r, &n, 2, 20);
}

int pn_sleep(pn_t *p)
{
    uint8_t c[2] = {PN_POWERDOWN, 0xB0}, r[4];               /* будить по I2C, SPI и HSU */
    uint32_t n = 0;
    int st = pn_cmd(p, c, 2, r, &n, 2, 20);
    if (st == PN_OK)
        p->asleep = 1;
    return st;
}

/* ── NTAG2xx ──────────────────────────────────────────────────────────── */
int ntag_read(pn_t *p, uint8_t page, uint8_t out[16])
{
    uint8_t c[2] = {NTAG_READ, page}, r[20];
    uint32_t n = 0;
    int st = pn_exchange(p, c, 2, r, &n, 16);
    if (st != PN_OK)
        return st;
    if (n != 16u)
        return PN_FRAME;
    memcpy(out, r, 16);
    return PN_OK;
}

int ntag_write(pn_t *p, uint8_t page, const uint8_t d[4])
{
    uint8_t c[6] = {NTAG_WRITE, page, d[0], d[1], d[2], d[3]}, r[4];
    uint32_t n = 0;
    return pn_exchange(p, c, 6, r, &n, 0);
}

/* ── NDEF: TLV 03, запись URI (тип «U»), TLV FE ───────────────────────── */
static const char *const PREFIX[5] = {"", "http://www.", "https://www.", "http://", "https://"};

uint32_t ndef_uri(uint8_t *out, uint32_t cap, const char *url)
{
    uint32_t code = 0, best = 0, k = 0;
    for (uint32_t i = 1; i < 5u; i++) {
        uint32_t l = (uint32_t)strlen(PREFIX[i]);
        if (l > best && strncmp(url, PREFIX[i], l) == 0) {
            best = l;
            code = i;
        }
    }
    const char *rest = url + best;
    uint32_t pl = 1u + (uint32_t)strlen(rest);           /* поле полезной нагрузки: код приставки и остаток */
    uint32_t rec = (pl <= 255u ? 4u : 7u) + pl;          /* заголовок, длина типа, длина нагрузки, тип «U» */
    uint32_t tlv = (rec <= 254u ? 2u : 4u) + rec + 1u;
    if (tlv > cap)
        return 0;
    out[k++] = 0x03;                    /* TLV: сообщение NDEF */
    if (rec <= 254u) {
        out[k++] = (uint8_t)rec;
    } else {
        out[k++] = 0xFF;                /* длинная длина: FF и два байта */
        out[k++] = (uint8_t)(rec >> 8);
        out[k++] = (uint8_t)rec;
    }
    out[k++] = pl <= 255u ? 0xD1u : 0xC1u;               /* MB, ME, (SR), TNF = 1 — тип NFC Forum */
    out[k++] = 0x01;                    /* длина типа */
    if (pl <= 255u) {
        out[k++] = (uint8_t)pl;
    } else {
        out[k++] = 0;
        out[k++] = 0;
        out[k++] = (uint8_t)(pl >> 8);
        out[k++] = (uint8_t)pl;
    }
    out[k++] = 'U';
    out[k++] = (uint8_t)code;
    memcpy(out + k, rest, pl - 1u);
    k += pl - 1u;
    out[k++] = 0xFE;                    /* TLV: конец */
    return k;
}

int ndef_parse(const uint8_t *m, uint32_t n, char *url, uint32_t cap, uint32_t *tlv_at)
{
    uint32_t i = 0;
    while (i < n) {
        uint8_t t = m[i];
        if (t == 0x00) {                /* заполнитель: без длины */
            i++;
            continue;
        }
        if (t == 0xFE || i + 2u > n)
            return PN_NDEF;             /* конец, а сообщения NDEF не было */
        uint32_t len = m[i + 1u], h = 2;
        if (len == 0xFFu) {
            if (i + 4u > n)
                return PN_NDEF;
            len = (uint32_t)m[i + 2u] << 8 | m[i + 3u];
            h = 4;
        }
        if (t != 0x03) {                /* 01 — блокировки, 02 — память, FD — своё: пропустить */
            i += h + len;
            continue;
        }
        *tlv_at = i;
        const uint8_t *r = m + i + h;
        if (i + h + len > n || len < 4u || (r[0] & 0x07u) != 0x01u || r[1] != 1u)
            return PN_NDEF;             /* нужен тип NFC Forum с однобуквенным именем */
        uint32_t pl, j;
        if (r[0] & 0x10u) {             /* SR: длина нагрузки — один байт */
            pl = r[2];
            j = 3;
        } else {
            pl = (uint32_t)r[2] << 24 | (uint32_t)r[3] << 16 | (uint32_t)r[4] << 8 | r[5];
            j = 6;
        }
        if (r[0] & 0x08u)
            j++;                        /* IL: есть длина идентификатора (пропускаем; у нас 0) */
        if (j + 1u + pl > len || r[j] != 'U' || pl < 1u || r[j + 1u] > 4u)
            return PN_NDEF;
        const char *pre = PREFIX[r[j + 1u]];
        uint32_t lp = (uint32_t)strlen(pre);
        if (lp + pl > cap)
            return PN_NOSPACE;
        memcpy(url, pre, lp);
        memcpy(url + lp, r + j + 2u, pl - 1u);
        url[lp + pl - 1u] = 0;
        return PN_OK;
    }
    return PN_NDEF;
}

/* Память метки с 4-й страницы: читает по 16 байт, пока не найдёт TLV FE или не дойдёт до конца области. */
static int user_area(pn_t *p, uint8_t *m, uint32_t *size, uint32_t *have)
{
    uint8_t b[16];
    int st = ntag_read(p, 3, b);        /* страница 3 — CC, за ней сразу страницы 4–6 */
    if (st != PN_OK)
        return st;
    if (b[0] != 0xE1u || b[1] != 0x10u)
        return PN_NDEF;                 /* метка не размечена под NDEF */
    *size = b[2] * 8u;                  /* NTAG213: 0x12 × 8 = 144 байта */
    memcpy(m, b + 4, 12);
    *have = 12;
    while (*have < *size && !memchr(m, 0xFE, *have)) {
        if ((st = ntag_read(p, (uint8_t)(4u + *have / 4u), b)) != PN_OK)
            return st;
        memcpy(m + *have, b, 16);
        *have += 16u;
    }
    if (*have > *size)
        *have = *size;
    return PN_OK;
}

int ntag_read_url(pn_t *p, char *url, uint32_t cap)
{
    static uint8_t m[1024];
    uint32_t size = 0, have = 0, at = 0;
    int st = user_area(p, m, &size, &have);
    return st != PN_OK ? st : ndef_parse(m, have, url, cap, &at);
}

int ntag_write_url(pn_t *p, const char *url, uint32_t *pages)
{
    static uint8_t m[1024], t[1024];
    uint32_t size = 0, have = 0, at = 0, i = 0;
    *pages = 0;
    int st = user_area(p, m, &size, &have);
    if (st != PN_OK)
        return st;
    while (i < have && m[i] != 0x03u && m[i] != 0xFEu) {   /* пропустить TLV блокировок и памяти */
        if (m[i] == 0x00u) {
            i++;
            continue;
        }
        i += 2u + m[i + 1u];
    }
    at = i;                             /* сюда ляжет новое сообщение; TLV до него не трогаем */
    uint32_t n = ndef_uri(t, sizeof t, url);
    if (n == 0u || at + n > size)
        return PN_NOSPACE;
    uint32_t first = at / 4u, last = (at + n - 1u) / 4u;
    memcpy(m + at, t, n);               /* байты страницы до at остаются прежними */
    memset(m + at + n, 0, (last + 1u) * 4u - (at + n));
    for (uint32_t pg = first; pg <= last; pg++) {
        if ((st = ntag_write(p, (uint8_t)(4u + pg), m + pg * 4u)) != PN_OK)
            return st;
        (*pages)++;
    }
    return PN_OK;
}

fw18f.c — прошивка STM32F103: I2C1, SPI1 или USART2, вывод в USART1.

/* fw18f.c — раздел 18д: NFC-модуль PN532 на STM32F103 (BluePill), 72 МГц. Шина — макрос FW_BUS:
 *   1 — I2C1 без ремапа: PB6 — SCL, PB7 — SDA, 100 кГц, открытый сток (подтяжки — на модуле);
 *   2 — SPI1 без ремапа: PA4 — NSS (ведём сами), PA5 — SCK, PA6 — MISO, PA7 — MOSI;
 *       режим 0, МЛАДШИМ битом вперёд (LSBFIRST), 72 МГц / 32 = 2,25 МГц;
 *   3 — USART2 (HSU): PA2 — TX, PA3 — RX, 115 200 бод.
 * PB1 — IRQ модуля (вход с подтяжкой; драйвер по умолчанию опрашивает шину, use_irq = 1 — ждёт IRQ).
 * Модуль питается от 3,3 В. UID метки и адрес из NDEF — в USART1 (PA9, 115 200).
 * Тактирование до 72 МГц — как в разделе 6 (здесь не повторяется); APB2 = 72 МГц, APB1 = 36 МГц. */
#include "stm32f10x.h"
#include "pn18f.h"

#ifndef FW_BUS
#define FW_BUS PN_BUS_SPI
#endif
#define CPU_HZ  72000000u
#define APB1_HZ 36000000u
#define SPI_BR  SPI_CR1_BR_2                    /* BR = 100: fPCLK / 32 */

_Static_assert(SPI_BR == 0x20u, "BR[2:0] = 100 — биты 5:3 SPI_CR1");
_Static_assert(SPI_CR1_LSBFIRST == 0x80u, "LSBFIRST — бит 7 SPI_CR1");
_Static_assert(APB1_HZ / (2u * 100000u) == 180u, "I2C1: CCR = 180 — 100 кГц");
_Static_assert((APB1_HZ + 57600u) / 115200u == 313u, "USART2: BRR = 313 — 115 200 бод");
_Static_assert(CPU_HZ / 115200u == 625u, "USART1: BRR = 625 — 115 200 бод");

static uint32_t now(void)
{
    return DWT->CYCCNT;
}

void pn_delay_ms(uint32_t ms)
{
    uint32_t t0 = now();
    while (now() - t0 < ms * (CPU_HZ / 1000u)) {
    }
}

int pn_irq(void)
{
    return (GPIOB->IDR >> 1) & 1u;              /* PB1: ноль — PN532 приготовил кадр */
}

/* ── I2C1: старый блок F1, ожидания с таймаутом 10 мс ─────────────────── */
static int i2c_wait(uint32_t flag)
{
    uint32_t t0 = now();
    while (!(I2C1->SR1 & flag)) {
        if (I2C1->SR1 & I2C_SR1_AF)
            return -1;                          /* NACK: на адрес никто не ответил */
        if (now() - t0 > CPU_HZ / 100u)
            return -2;
    }
    return 0;
}

static int i2c_fail(void)
{
    I2C1->CR1 |= I2C_CR1_STOP;
    I2C1->SR1 &= (uint16_t)~I2C_SR1_AF;
    return -1;
}

int pn_i2c_write(uint8_t addr7, const uint8_t *d, uint32_t n)
{
    I2C1->CR1 |= I2C_CR1_START;
    if (i2c_wait(I2C_SR1_SB))
        return i2c_fail();
    I2C1->DR = (uint8_t)(addr7 << 1);           /* 0x24 -> 0x48: запись */
    if (i2c_wait(I2C_SR1_ADDR))
        return i2c_fail();
    (void)I2C1->SR1;
    (void)I2C1->SR2;
    for (uint32_t i = 0; i < n; i++) {
        if (i2c_wait(I2C_SR1_TXE))
            return i2c_fail();
        I2C1->DR = d[i];
    }
    if (i2c_wait(I2C_SR1_BTF))
        return i2c_fail();
    I2C1->CR1 |= I2C_CR1_STOP;
    return 0;
}

int pn_i2c_read(uint8_t addr7, uint8_t *d, uint32_t n)
{
    I2C1->CR1 |= I2C_CR1_ACK | I2C_CR1_START;
    if (i2c_wait(I2C_SR1_SB))
        return i2c_fail();
    I2C1->DR = (uint8_t)((addr7 << 1) | 1u);    /* 0x49: чтение */
    if (i2c_wait(I2C_SR1_ADDR))
        return i2c_fail();
    if (n == 1u)
        I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;    /* один байт: снять ACK до сброса ADDR */
    (void)I2C1->SR1;
    (void)I2C1->SR2;
    if (n == 1u)
        I2C1->CR1 |= I2C_CR1_STOP;
    for (uint32_t i = 0; i < n; i++) {
        if (i2c_wait(I2C_SR1_RXNE))
            return i2c_fail();
        d[i] = (uint8_t)I2C1->DR;
        if (n > 1u && i == n - 2u) {
            I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;    /* последний байт — без подтверждения */
            I2C1->CR1 |= I2C_CR1_STOP;
        }
    }
    return 0;
}

/* ── SPI1: байты младшим битом вперёд делает сам блок (LSBFIRST) ──────── */
void pn_spi(const uint8_t *tx, uint8_t *rx, uint32_t n)
{
    GPIOA->BRR = 1u << 4;                       /* NSS вниз */
    for (uint32_t i = 0; i < n; i++) {
        while (!(SPI1->SR & SPI_SR_TXE)) {
        }
        SPI1->DR = tx[i];
        while (!(SPI1->SR & SPI_SR_RXNE)) {
        }
        rx[i] = (uint8_t)SPI1->DR;
    }
    while (SPI1->SR & SPI_SR_BSY) {
    }
    GPIOA->BSRR = 1u << 4;                      /* NSS вверх — конец транзакции */
}

/* ── USART2 (HSU) ─────────────────────────────────────────────────────── */
void pn_uart_write(const uint8_t *d, uint32_t n)
{
    for (uint32_t i = 0; i < n; i++) {
        while (!(USART2->SR & USART_SR_TXE)) {
        }
        USART2->DR = d[i];
    }
    while (!(USART2->SR & USART_SR_TC)) {
    }
}

uint32_t pn_uart_read(uint8_t *d, uint32_t n, uint32_t timeout_ms)
{
    uint32_t k = 0, t0 = now();
    while (k < n && now() - t0 < timeout_ms * (CPU_HZ / 1000u)) {
        if (USART2->SR & USART_SR_RXNE)
            d[k++] = (uint8_t)USART2->DR;
    }
    return k;
}

/* ── вывод в USART1 ───────────────────────────────────────────────────── */
static void put(char c)
{
    while (!(USART1->SR & USART_SR_TXE)) {
    }
    USART1->DR = (uint8_t)c;
}

static void puts_(const char *s)
{
    while (*s)
        put(*s++);
}

static void hex(const uint8_t *d, uint32_t n)
{
    static const char h[] = "0123456789ABCDEF";
    for (uint32_t i = 0; i < n; i++) {
        put(h[d[i] >> 4]);
        put(h[d[i] & 15u]);
    }
}

static void board_init(void)
{
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_AFIOEN
                  | RCC_APB2ENR_SPI1EN | RCC_APB2ENR_USART1EN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN | RCC_APB1ENR_USART2EN;
    GPIOA->BSRR = 1u << 4;                      /* NSS вверх до настройки вывода */
    GPIOA->CRL = (GPIOA->CRL & 0x000000FFu)
               | 0xBu << 8                      /* PA2 — USART2_TX, альтернативная функция */
               | 0x4u << 12                     /* PA3 — USART2_RX, вход */
               | 0x3u << 16                     /* PA4 — выход: NSS */
               | 0xBu << 20                     /* PA5 — SCK */
               | 0x4u << 24                     /* PA6 — MISO, вход */
               | 0xBu << 28;                    /* PA7 — MOSI */
    GPIOA->CRH = (GPIOA->CRH & ~(0xFu << 4)) | 0xBu << 4;        /* PA9 — USART1_TX */
    GPIOB->ODR |= 1u << 1;                      /* PB1 — подтяжка вверх */
    GPIOB->CRL = (GPIOB->CRL & 0x00FFFF0Fu)
               | 0x8u << 4                      /* PB1 — IRQ, вход с подтяжкой */
               | 0xFu << 24                     /* PB6 — SCL, альтернативная функция, открытый сток */
               | 0xFu << 28;                    /* PB7 — SDA */
    GPIOC->CRH = (GPIOC->CRH & ~(0xFu << 20)) | 0x2u << 20;      /* PC13 — светодиод платы */
    SPI1->CR1 = SPI_CR1_MSTR | SPI_BR | SPI_CR1_LSBFIRST | SPI_CR1_SSM | SPI_CR1_SSI;   /* режим 0 */
    SPI1->CR1 |= SPI_CR1_SPE;
    I2C1->CR2 = APB1_HZ / 1000000u;             /* FREQ = 36 */
    I2C1->CCR = APB1_HZ / (2u * 100000u);       /* 180: стандартный режим, 100 кГц */
    I2C1->TRISE = APB1_HZ / 1000000u + 1u;      /* 37 */
    I2C1->CR1 = I2C_CR1_PE;
    USART2->BRR = (APB1_HZ + 57600u) / 115200u;
    USART2->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
    USART1->BRR = CPU_HZ / 115200u;
    USART1->CR1 = USART_CR1_UE | USART_CR1_TE;
}

int main(void)
{
    static pn_t p;
    static char url[160];
    pn_tag_t t;
    board_init();
    pn_delay_ms(10);
    if (pn_begin(&p, FW_BUS) != PN_OK) {
        puts_("PN532: no answer\r\n");
        for (;;) {
            GPIOC->ODR ^= 1u << 13;             /* частое мигание: перемычки, шина, питание */
            pn_delay_ms(100);
        }
    }
    puts_("PN532 IC=");
    hex(p.fw, 1);
    puts_(" v");
    hex(p.fw + 1, 2);
    puts_("\r\n");
    pn_retries(&p, 0x10);                       /* без этого поиск без метки не кончается никогда */
    for (;;) {
        if (pn_list(&p, &t, 200) == PN_OK) {
            puts_("UID ");
            hex(t.uid, t.uid_len);
            puts_(" SAK ");
            hex(&t.sel_res, 1);
            if (t.sel_res == 0x00u && ntag_read_url(&p, url, sizeof url) == PN_OK) {
                puts_(" URL ");
                puts_(url);
            }
            puts_("\r\n");
            pn_release(&p);
            GPIOC->ODR ^= 1u << 13;
        }
        pn_delay_ms(300);
    }
}

Дальше: раздел 18е «DDS-генератор AD9833 по SPI: фазовый аккумулятор, частота и фаза, синус/треугольник/меандр, свип и FSK» — готовая микросхема DDS: частота, фаза и форма сигнала словами по SPI, свип и FSK.

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