Lettore RFID per accessi con ESP32-C6 e RC522 (MFRC522)

Lettore RFID per accessi con ESP32-C6 e RC522 (MFRC522)

Un lettore di chiavette RFID è il classico progetto “porta di casa”: avvicini il portachiavi, il relè scatta e la serratura si apre. In questo tutorial lo costruisci con l’ESP32-C6 e il diffusissimo modulo RC522 (chip NXP MFRC522, 13,56 MHz), scrivendo da zero un piccolo driver SPI che esegue la sequenza ISO/IEC 14443-A fino a leggere l’UID della carta. Le chiavette autorizzate sono salvate in NVS, e un relè, due LED e un buzzer danno il riscontro.

Alla fine c’è una sezione che non devi saltare: leggere il solo UID è comodo per imparare, ma non è sicuro per un controllo accessi vero. Vedremo perché e cosa usare al suo posto.

Cosa serve

Componente Note
ESP32-C6-DevKitC-1 Qualsiasi scheda ESP32-C6, adatta i pin
Modulo RC522 (MFRC522) Alimentazione 3,3 V; di solito arriva con una carta e un portachiavi MIFARE Classic 1K
Modulo relè con transistor e diodo di ricircolo Mai pilotare la bobina direttamente dal GPIO
Buzzer attivo + transistor NPN Il buzzer attivo suona con un semplice livello alto
LED verde e rosso con resistori da 330 Ω Accesso consentito / negato
Pulsante normalmente aperto Tenuto premuto durante la lettura, registra la chiavetta
Chiavette MIFARE (o DESFire, vedi sicurezza) 13,56 MHz, ISO/IEC 14443-A

Come funziona

RFID a 13,56 MHz e ISO/IEC 14443-A

Il lettore (in gergo PCD) genera un campo magnetico a 13,56 MHz con la sua antenna a spira. La carta (PICC) non ha batteria: si alimenta dal campo e risponde modulandone il carico. Il dialogo segue lo standard ISO/IEC 14443-3 tipo A:

  1. REQA (0x26, un frame corto da soli 7 bit): “c’è qualcuno?”. Ogni carta in stato di riposo risponde con due byte, l’ATQA.
  2. ANTICOLLISION (0x93 0x20): la carta invia 4 byte di UID più un byte di controllo BCC, lo XOR dei quattro.
  3. SELECT (0x93 0x70 + UID + BCC + CRC_A): la carta selezionata risponde con il SAK, che dice se l’UID è completo e che tipo di carta è.
  4. Se il primo byte ricevuto è il cascade tag 0x88, l’UID è più lungo di 4 byte: si ripete anticollisione e selezione al livello 2 (0x95) ed eventualmente al livello 3 (0x97). Gli UID possono essere di 4, 7 o 10 byte.
  5. HLTA (0x50 0x00 + CRC): mette la carta in stato HALT. Finché resta nel campo non risponde più a REQA, e così non la rileggi dieci volte al secondo.

RC522 o PN532?

RC522 (MFRC522) PN532
Interfacce SPI (sui moduli comuni), I2C e UART previsti dal chip I2C, SPI, HSU (UART)
Standard ISO/IEC 14443-A, MIFARE ISO/IEC 14443-A/B, FeliCa, NFC peer-to-peer, emulazione di carta (MIFARE Classic, FeliCa)
Livello di astrazione Registri e FIFO: il protocollo lo scrivi tu Microcontrollore interno con comandi di alto livello (per esempio InListPassiveTarget)
ISO 14443-4 (necessario per DESFire) Da implementare nel firmware Il datasheet non lo cita: dichiara il framing completo ISO/IEC 14443A (parità e CRC) e, con il firmware adatto, l’intero protocollo 14443A/MIFARE
Costo e diffusione Bassissimo, ovunque Un po’ più alto

Per imparare il protocollo l’RC522 è perfetto, perché ti obbliga a vedere ogni passaggio. Per un prodotto che deve parlare con carte DESFire il PN532 (o un lettore dedicato) ti risparmia molto lavoro grazie ai comandi di alto livello; come gestisca il livello ISO 14443-4, però, va verificato nella documentazione NXP del firmware, perché il datasheet non lo descrive.

Collegamenti

Modulo Pin ESP32-C6
RC522 SDA (è il chip select SPI) GPIO10
RC522 SCK GPIO6
RC522 MOSI GPIO7
RC522 MISO GPIO2
RC522 RST GPIO11
RC522 IRQ non collegato (usiamo il polling)
RC522 3.3V / GND 3V3 / GND (non 5 V)
Relè IN GPIO18
LED verde / rosso anodo (via 330 Ω) GPIO19 / GPIO20
Buzzer base del transistor (via 1 kΩ) GPIO21
Pulsante registrazione verso GND GPIO23 (pull-up interno)

I pin evitano i GPIO di strapping (8, 9, 15), l’USB (12, 13) e la UART della console (16, 17). GPIO2, 6 e 7 sono i pin IO_MUX dello SPI2 sul C6.

La periferica dell’ESP32

L’MFRC522 si comanda leggendo e scrivendo registri a 8 bit. Via SPI ogni accesso è una transazione di due byte: il primo contiene l’indirizzo del registro spostato di un bit a sinistra, con il bit 7 a 1 per la lettura e a 0 per la scrittura; il secondo è il dato (in lettura, il byte che torna indietro). Il driver SPI master di ESP-IDF (driver/spi_master.h) gestisce il chip select e il clock: con SPI_TRANS_USE_TXDATA e SPI_TRANS_USE_RXDATA i due byte stanno direttamente nella struttura della transazione, senza buffer esterni, e spi_device_polling_transmit() è la scelta più veloce per transazioni così brevi. Il bus gira a 4 MHz, sotto il limite di 10 Mbit/s del datasheet e con margine per cavi volanti.

Per i comandi che richiedono il CRC (SELECT e HLTA) calcoliamo il CRC_A in software: è un CRC a 16 bit con valore iniziale 0x6363 definito nella ISO/IEC 14443-3. Un controllo facile: il CRC di 50 00 deve dare 57 CD, i due byte finali della frase HLTA.

Il codice

Il progetto usa solo componenti di ESP-IDF 5.4: basta questo main.c, target esp32c6.

/*
 * Lettore chiavette RFID per accessi - ESP32-C6 + MFRC522 (RC522), ESP-IDF 5.4
 * SPI2: SCK=GPIO6, MOSI=GPIO7, MISO=GPIO2, CS(SDA)=GPIO10, RST=GPIO11
 * Relè=GPIO18, LED verde=GPIO19, LED rosso=GPIO20, buzzer=GPIO21, tasto registra=GPIO23
 * www.webappportal.com
 */
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_timer.h"
#include "esp_log.h"

static const char *TAG = "rfid";

/* ---------- Pin ---------- */
#define PIN_SCK         GPIO_NUM_6
#define PIN_MOSI        GPIO_NUM_7
#define PIN_MISO        GPIO_NUM_2
#define PIN_CS          GPIO_NUM_10
#define PIN_RST         GPIO_NUM_11
#define PIN_RELE        GPIO_NUM_18
#define PIN_LED_OK      GPIO_NUM_19
#define PIN_LED_KO      GPIO_NUM_20
#define PIN_BUZZER      GPIO_NUM_21
#define PIN_REGISTRA    GPIO_NUM_23     /* tenuto premuto = registra la chiavetta */

/* ---------- Registri MFRC522 (datasheet NXP, cap. 9) ---------- */
#define REG_COMMAND     0x01
#define REG_COM_IRQ     0x04
#define REG_ERROR       0x06
#define REG_FIFO_DATA   0x09
#define REG_FIFO_LEVEL  0x0A
#define REG_CONTROL     0x0C
#define REG_BIT_FRAMING 0x0D
#define REG_COLL        0x0E
#define REG_MODE        0x11
#define REG_TX_MODE     0x12
#define REG_RX_MODE     0x13
#define REG_TX_CONTROL  0x14
#define REG_TX_ASK      0x15
#define REG_MOD_WIDTH   0x24
#define REG_T_MODE      0x2A
#define REG_T_PRESCALER 0x2B
#define REG_T_RELOAD_H  0x2C
#define REG_T_RELOAD_L  0x2D
#define REG_VERSION     0x37

#define CMD_IDLE        0x00
#define CMD_TRANSCEIVE  0x0C
#define CMD_SOFT_RESET  0x0F

/* ---------- Comandi ISO/IEC 14443-3 tipo A ---------- */
#define PICC_REQA       0x26
#define PICC_HLTA       0x50
#define PICC_CASCADE    0x88

#define MAX_UID         10
#define MAX_AUTORIZZATI 32

typedef struct {
    uint8_t len;
    uint8_t uid[MAX_UID];
} chiave_t;

static spi_device_handle_t s_spi;
static chiave_t s_lista[MAX_AUTORIZZATI];
static size_t s_num_autorizzati;

/* ================= Accesso ai registri via SPI ================= */
static void rc522_scrivi(uint8_t reg, uint8_t val)
{
    spi_transaction_t t = {
        .flags = SPI_TRANS_USE_TXDATA,
        .length = 16,
        .tx_data = { (uint8_t)((reg << 1) & 0x7E), val },
    };
    ESP_ERROR_CHECK(spi_device_polling_transmit(s_spi, &t));
}

static uint8_t rc522_leggi(uint8_t reg)
{
    spi_transaction_t t = {
        .flags = SPI_TRANS_USE_TXDATA | SPI_TRANS_USE_RXDATA,
        .length = 16,
        .tx_data = { (uint8_t)(0x80 | ((reg << 1) & 0x7E)), 0x00 },
    };
    ESP_ERROR_CHECK(spi_device_polling_transmit(s_spi, &t));
    return t.rx_data[1];
}

static void rc522_set_bit(uint8_t reg, uint8_t mask)
{
    rc522_scrivi(reg, rc522_leggi(reg) | mask);
}

static void rc522_clr_bit(uint8_t reg, uint8_t mask)
{
    rc522_scrivi(reg, rc522_leggi(reg) & ~mask);
}

/* CRC_A di ISO/IEC 14443-3 (polinomio x^16+x^12+x^5+1, valore iniziale 0x6363) */
static void crc_a(const uint8_t *dati, size_t len, uint8_t out[2])
{
    uint16_t crc = 0x6363;
    for (size_t i = 0; i < len; i++) {
        uint8_t ch = dati[i] ^ (uint8_t)(crc & 0xFF);
        ch ^= (uint8_t)(ch << 4);
        crc = (crc >> 8) ^ ((uint16_t)ch << 8) ^ ((uint16_t)ch << 3) ^ (ch >> 4);
    }
    out[0] = crc & 0xFF;
    out[1] = crc >> 8;
}

static void rc522_init(void)
{
    gpio_set_level(PIN_RST, 0);                 /* reset hardware */
    vTaskDelay(pdMS_TO_TICKS(2));
    gpio_set_level(PIN_RST, 1);
    vTaskDelay(pdMS_TO_TICKS(50));
    rc522_scrivi(REG_COMMAND, CMD_SOFT_RESET);
    vTaskDelay(pdMS_TO_TICKS(50));

    rc522_scrivi(REG_TX_MODE, 0x00);            /* 106 kbit/s, CRC gestito da noi */
    rc522_scrivi(REG_RX_MODE, 0x00);
    rc522_scrivi(REG_MOD_WIDTH, 0x26);
    /* timer interno: 13,56 MHz / (2*169+1) = 40 kHz, ricarica 1000 -> 25 ms */
    rc522_scrivi(REG_T_MODE, 0x80);             /* TAuto: parte a fine trasmissione */
    rc522_scrivi(REG_T_PRESCALER, 0xA9);
    rc522_scrivi(REG_T_RELOAD_H, 0x03);
    rc522_scrivi(REG_T_RELOAD_L, 0xE8);
    rc522_scrivi(REG_TX_ASK, 0x40);             /* modulazione ASK 100% */
    rc522_scrivi(REG_MODE, 0x3D);               /* preset CRC 0x6363 */
    rc522_set_bit(REG_TX_CONTROL, 0x03);        /* accende l'antenna (TX1, TX2) */
}

/* Invia 'tx' alla carta e riceve la risposta in 'rx' */
static esp_err_t rc522_transceive(const uint8_t *tx, uint8_t tx_len, uint8_t bit_framing,
                                  uint8_t *rx, uint8_t *rx_len)
{
    rc522_scrivi(REG_COMMAND, CMD_IDLE);
    rc522_scrivi(REG_COM_IRQ, 0x7F);            /* azzera i flag di interrupt */
    rc522_scrivi(REG_FIFO_LEVEL, 0x80);         /* svuota la FIFO */
    for (uint8_t i = 0; i < tx_len; i++) {
        rc522_scrivi(REG_FIFO_DATA, tx[i]);
    }
    rc522_scrivi(REG_BIT_FRAMING, bit_framing);
    rc522_scrivi(REG_COMMAND, CMD_TRANSCEIVE);
    rc522_set_bit(REG_BIT_FRAMING, 0x80);       /* StartSend */

    int64_t scadenza = esp_timer_get_time() + 40000;
    while (1) {
        uint8_t irq = rc522_leggi(REG_COM_IRQ);
        if (irq & 0x30) {                       /* RxIRq o IdleIRq: finito */
            break;
        }
        if ((irq & 0x01) || esp_timer_get_time() > scadenza) {
            return ESP_ERR_TIMEOUT;             /* nessuna carta ha risposto */
        }
    }
    uint8_t err = rc522_leggi(REG_ERROR);
    if (err & 0x13) {                           /* BufferOvfl, ParityErr, ProtocolErr */
        return ESP_FAIL;
    }
    if (err & 0x08) {                           /* CollErr: più carte nel campo */
        return ESP_ERR_INVALID_STATE;
    }
    uint8_t n = rc522_leggi(REG_FIFO_LEVEL);
    if (n > *rx_len) {
        return ESP_ERR_NO_MEM;
    }
    for (uint8_t i = 0; i < n; i++) {
        rx[i] = rc522_leggi(REG_FIFO_DATA);
    }
    *rx_len = n;
    return ESP_OK;
}

/* REQA -> anticollisione -> SELECT (fino a 3 livelli) -> UID completo */
static esp_err_t rc522_leggi_uid(chiave_t *uid)
{
    uint8_t buf[10];
    uint8_t len = sizeof(buf);
    uint8_t reqa = PICC_REQA;

    rc522_clr_bit(REG_COLL, 0x80);
    /* REQA è un frame corto da 7 bit: BitFramingReg = 0x07 */
    esp_err_t ret = rc522_transceive(&reqa, 1, 0x07, buf, &len);
    if (ret != ESP_OK || len != 2) {
        return ESP_ERR_NOT_FOUND;               /* ATQA assente: nessuna carta */
    }

    uid->len = 0;
    const uint8_t sel[3] = { 0x93, 0x95, 0x97 };
    for (int livello = 0; livello < 3; livello++) {
        uint8_t ac[2] = { sel[livello], 0x20 };     /* ANTICOLLISION */
        uint8_t risp[5];
        len = sizeof(risp);
        ret = rc522_transceive(ac, 2, 0x00, risp, &len);
        if (ret != ESP_OK || len != 5) {
            return ESP_FAIL;
        }
        if ((risp[0] ^ risp[1] ^ risp[2] ^ risp[3]) != risp[4]) {
            return ESP_ERR_INVALID_CRC;             /* BCC errato */
        }
        uint8_t s[9] = { sel[livello], 0x70, risp[0], risp[1], risp[2], risp[3], risp[4] };
        crc_a(s, 7, &s[7]);                          /* SELECT con CRC_A */
        uint8_t sak[3];
        len = sizeof(sak);
        ret = rc522_transceive(s, 9, 0x00, sak, &len);
        if (ret != ESP_OK || len != 3) {
            return ESP_FAIL;
        }
        if (risp[0] == PICC_CASCADE) {              /* UID lungo: continua */
            memcpy(&uid->uid[uid->len], &risp[1], 3);
            uid->len += 3;
        } else {
            memcpy(&uid->uid[uid->len], risp, 4);
            uid->len += 4;
            ESP_LOGD(TAG, "SAK 0x%02X", sak[0]);
            break;
        }
    }
    /* HLTA: la carta non risponderà più a REQA finché resta nel campo */
    uint8_t hlta[4] = { PICC_HLTA, 0x00 };
    crc_a(hlta, 2, &hlta[2]);
    len = sizeof(buf);
    rc522_transceive(hlta, 4, 0x00, buf, &len);     /* timeout atteso */
    return ESP_OK;
}

/* ================= Lista autorizzati in NVS ================= */
static void lista_carica(void)
{
    nvs_handle_t h;
    size_t dim = sizeof(s_lista);
    s_num_autorizzati = 0;
    if (nvs_open("accessi", NVS_READONLY, &h) != ESP_OK) {
        return;                                 /* primo avvio: lista vuota */
    }
    if (nvs_get_blob(h, "uid", s_lista, &dim) == ESP_OK) {
        s_num_autorizzati = dim / sizeof(chiave_t);
    }
    nvs_close(h);
}

static esp_err_t lista_salva(void)
{
    nvs_handle_t h;
    esp_err_t err = nvs_open("accessi", NVS_READWRITE, &h);
    if (err != ESP_OK) {
        return err;
    }
    err = nvs_set_blob(h, "uid", s_lista, s_num_autorizzati * sizeof(chiave_t));
    if (err == ESP_OK) {
        err = nvs_commit(h);
    }
    nvs_close(h);
    return err;
}

static bool lista_contiene(const chiave_t *u)
{
    for (size_t i = 0; i < s_num_autorizzati; i++) {
        if (s_lista[i].len == u->len && memcmp(s_lista[i].uid, u->uid, u->len) == 0) {
            return true;
        }
    }
    return false;
}

/* ================= Feedback ================= */
static void segnala(bool ok)
{
    gpio_num_t led = ok ? PIN_LED_OK : PIN_LED_KO;
    gpio_set_level(led, 1);
    gpio_set_level(PIN_BUZZER, 1);
    vTaskDelay(pdMS_TO_TICKS(ok ? 100 : 600));      /* bip corto = OK, lungo = negato */
    gpio_set_level(PIN_BUZZER, 0);
    if (ok) {
        gpio_set_level(PIN_RELE, 1);                 /* apre la serratura per 3 s */
        vTaskDelay(pdMS_TO_TICKS(3000));
        gpio_set_level(PIN_RELE, 0);
    } else {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
    gpio_set_level(led, 0);
}

static void gpio_init(void)
{
    const gpio_config_t out = {
        .pin_bit_mask = (1ULL << PIN_RST) | (1ULL << PIN_RELE) | (1ULL << PIN_LED_OK) |
                        (1ULL << PIN_LED_KO) | (1ULL << PIN_BUZZER),
        .mode = GPIO_MODE_OUTPUT,
    };
    ESP_ERROR_CHECK(gpio_config(&out));
    const gpio_config_t in = {
        .pin_bit_mask = 1ULL << PIN_REGISTRA,
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
    };
    ESP_ERROR_CHECK(gpio_config(&in));
}

static void spi_init(void)
{
    const spi_bus_config_t bus = {
        .sclk_io_num = PIN_SCK,
        .mosi_io_num = PIN_MOSI,
        .miso_io_num = PIN_MISO,
        .quadwp_io_num = -1,
        .quadhd_io_num = -1,
    };
    ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &bus, SPI_DMA_DISABLED));
    const spi_device_interface_config_t dev = {
        .clock_speed_hz = 4 * 1000 * 1000,     /* MFRC522: max 10 Mbit/s */
        .mode = 0,
        .spics_io_num = PIN_CS,
        .queue_size = 1,
    };
    ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, &dev, &s_spi));
}

void app_main(void)
{
    esp_err_t err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        err = nvs_flash_init();
    }
    ESP_ERROR_CHECK(err);

    gpio_init();
    spi_init();
    rc522_init();

    uint8_t ver = rc522_leggi(REG_VERSION);
    ESP_LOGI(TAG, "MFRC522 VersionReg = 0x%02X (0x91 = v1.0, 0x92 = v2.0)", ver);
    if (ver == 0x00 || ver == 0xFF) {
        ESP_LOGE(TAG, "Nessuna risposta: controlla cablaggio SPI e alimentazione 3,3 V");
    }
    lista_carica();
    ESP_LOGI(TAG, "Chiavette autorizzate in NVS: %u", (unsigned)s_num_autorizzati);

    while (1) {
        chiave_t uid;
        if (rc522_leggi_uid(&uid) == ESP_OK) {
            char hex[3 * MAX_UID + 1] = { 0 };
            for (int i = 0; i < uid.len; i++) {
                sprintf(&hex[i * 3], "%02X ", uid.uid[i]);
            }
            bool registra = (gpio_get_level(PIN_REGISTRA) == 0);

            if (registra && !lista_contiene(&uid) && s_num_autorizzati < MAX_AUTORIZZATI) {
                s_lista[s_num_autorizzati++] = uid;
                ESP_LOGW(TAG, "REGISTRATA UID %s(%d byte) -> %s", hex, uid.len,
                         esp_err_to_name(lista_salva()));
                segnala(true);
            } else if (lista_contiene(&uid)) {
                ESP_LOGI(TAG, "ACCESSO CONSENTITO  UID %s", hex);
                segnala(true);
            } else {
                ESP_LOGW(TAG, "ACCESSO NEGATO      UID %s", hex);
                segnala(false);
            }
        }
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

Come funziona il codice

  • rc522_init() fa un reset hardware e software, poi imposta il timer interno (prescaler 0xA9 e ricarica 1000: circa 25 ms di timeout per ogni risposta), la modulazione ASK al 100% richiesta dal tipo A, il preset del CRC e accende i due driver d’antenna. Sono i valori usati dalle librerie MFRC522 più diffuse; il significato di ogni bit è nel datasheet NXP.
  • rc522_transceive() è il cuore: svuota la FIFO, ci carica i byte da trasmettere, avvia il comando Transceive e aspetta i flag RxIRq o IdleIRq in ComIrqReg. Se scatta il timer (nessuna carta) torna ESP_ERR_TIMEOUT; poi controlla ErrorReg per errori di protocollo, parità e collisione, e legge la risposta dalla FIFO.
  • rc522_leggi_uid() esegue REQA (con BitFramingReg = 0x07 per trasmettere solo 7 bit), anticollisione e SELECT per ogni livello di cascata, verifica il BCC e chiude con HLTA. Il driver è volutamente minimo: con due carte nel campo segnala collisione invece di risolverla bit per bit.
  • Lista autorizzati: un array di strutture {lunghezza, UID} salvato come blob nel namespace NVS accessi. Sopravvive ai riavvii e agli aggiornamenti firmware.
  • Registrazione: se tieni premuto il pulsante su GPIO23 mentre avvicini una chiavetta nuova, questa viene aggiunta alla lista. In un impianto vero proteggeresti questa funzione (pulsante interno alla scatola, carta master).
  • segnala(): bip corto, LED verde e relè attivo per 3 secondi se l’accesso è consentito; bip lungo e LED rosso se è negato.

Prova e risultati attesi

Al primo avvio la lista è vuota: tieni premuto il pulsante e avvicina la carta per registrarla, poi rilascia e riprova. Il log sarà simile a questo (gli UID sono di esempio):

I (298) rfid: MFRC522 VersionReg = 0x92 (0x91 = v1.0, 0x92 = v2.0)
I (298) rfid: Chiavette autorizzate in NVS: 0
W (5120) rfid: REGISTRATA UID 3A 7F 21 C5 (4 byte) -> ESP_OK
I (9870) rfid: ACCESSO CONSENTITO  UID 3A 7F 21 C5
W (14210) rfid: ACCESSO NEGATO      UID 04 5B 92 1A 6C 11 90

L’ultima riga mostra un UID da 7 byte, tipico delle carte NTAG e DESFire: la cascata funziona.

Problemi comuni

  • VersionReg vale 0x00 o 0xFF: il chip non risponde. Controlla MISO/MOSI (spesso invertiti), il chip select su SDA e che RST sia alto. Alcuni cloni del chip restituiscono valori diversi da 0x91/0x92 ma funzionano lo stesso.
  • Nessuna carta rilevata: la distanza utile dei moduli economici è di pochi centimetri. Metalli vicini all’antenna (viti, scatole di alluminio) la riducono ancora.
  • Letture intermittenti: alimentazione debole o cavi lunghi. Aggiungi un condensatore da 10 µF vicino al modulo e accorcia i fili SPI.
  • Il relè resetta l’ESP32: disturbi sulla bobina. Usa un modulo con diodo di ricircolo e, meglio ancora, un’alimentazione separata per il relè.

Sicurezza: perché l’UID non basta

Questo è il punto più importante dell’articolo. L’UID viene trasmesso in chiaro, senza alcuna autenticazione, e chiunque lo può leggere con uno smartphone NFC. Esistono poi le cosiddette magic card, carte compatibili MIFARE Classic in cui il blocco 0 (quello che contiene l’UID) è riscrivibile: con pochi euro si ottiene un clone perfetto della tua chiavetta, e gli strumenti di analisi RFID possono anche emulare un UID arbitrario. Anche la crittografia Crypto1 delle MIFARE Classic è violata da anni: proteggere i settori con le chiavi A/B non basta a renderla sicura.

Per un controllo accessi reale usa carte MIFARE DESFire EV2 o EV3:

  • autenticazione reciproca AES-128 tra lettore e carta: la chiave non viaggia mai sul canale;
  • applicazioni e file separati sulla stessa carta, ognuno con le sue chiavi;
  • opzione di UID casuale, che rende inutile tracciare o copiare l’UID;
  • la decisione di aprire si basa sull’autenticazione riuscita con una chiave diversificata per ogni carta, non sul numero di serie.

È la scelta fatta nel progetto Antifurto del sito, che usa carte DESFire EV3. Lato ESP32 ricordati anche di proteggere le chiavi: attiva flash encryption e secure boot, altrimenti chi ha accesso fisico alla scheda può leggerle dalla flash.

Va oltre

  • Passa a DESFire con un PN532, sfruttando i suoi comandi di alto livello per lo scambio dati con la carta (verifica nella documentazione NXP come tratta il livello ISO 14443-4), e implementa l’autenticazione AES seguendo la documentazione NXP.
  • Aggiungi il registro accessi: salva data, ora (via SNTP) e UID di ogni evento e inviali via Wi-Fi o MQTT.
  • Usa l’interrupt: collega il pin IRQ e configura ComIEnReg per non fare polling continuo e risparmiare energia.
  • Altri progetti con ESP32 e domotica: sezione ESP32 e sezione domotica.

Imprenditore, maker, MBSE enthusiast e co-autore di Web App Portal. Collegati su LinkedIn.