Sistema de passagens com Módulo Leitor Rfid e Arduino Deixe um comentário

Uma das aplicações mais comuns de Rfid no dia-a-dia é o controle de passagens (bilhetagem). Neste post vamos ver como isto pode ser feito com os cartões MIFARE e simular esta operação usando o Módulo Leitor RfId Mfrc522 ligado a um Arduino.

Imagem 1 - Sistema de passagens com Módulo Leitor Rfid e Arduino

Material Necessário

Para a parte prática você precisará:

IMPORTANTE: Como vamos escrever no cartão e reconfigurá-lo, use somente um cartão vazio e que você possa descartar caso algo ocorra errado.

Conhecendo um pouco o cartão RFID Mifare

MIFARE é a marca da NXP (que já foi uma divisão da Philips) para uma série de integrados desenvolvidos para uso em cartões de proximidade ou smartcards sem contato.

O Mifare Classic (no qual vamos nos focar) é basicamente um dispositivo de memória no qual podem ser feitas leituras e escritas sem contato a curta distância, condicionadas a recursos de segurança. A memória disponível é dividida em setores, numerados a partir de zero. Cada setor pode ser protegido de uma forma independente dos demais. O setor, por sua vez, é dividido em blocos de 16 bytes. O último bloco de cada setor é usado para configurar a proteção do setor. O primeiro bloco do primeiro setor é o “bloco do fabricante” e (em teoria) não pode ser alterado. Ele contem a identificação única do cartão ou tag. O cartão e o tag que vem no kit possuem 1K de memória, o que corresponde a 16 setores cada um com 4 blocos.

O acesso a cada setor é controlado por uma configuração e duas chaves de 48 bits (chamadas A e B), armazenadas no último bloco do setor. A forma como a configuração é armazenada neste bloco é um pouco confusa. Temos três bits para cada bloco e, para maior segurança, estes bits são gravados duas vezes (o valor normal e o invertido):

Imagem 2 -Setores

A interpretação dos três bits é diferente entre os blocos de dados e o bloco de configuração:

Imagem 3 - Controle de Acesso de Blocos de Dados

Imagem 4 - Controle de Acesso do Último Bloco do Setor

Antes de qualquer operação de leitura ou escrita, é necessário fazer uma autenticação, usando uma das chaves. Uma pegadinha é que uma chave que possa ser lida não pode ser usada para autenticação. A chave A nunca pode ser lida, mas a leitura da chave B pode ser permitida (três primeiras linhas da primeira tabela acima), neste caso a chave B passa a ser um dado ao invés de chave. Em todo acesso o cartão verifica se o bloco de configuração é valido, se não for todo o setor é bloqueado de forma definitiva (uma forma fácil de perder um setor).

Os cartões/tags novos vem com o acesso configurado para permitir ler e escrever nos blocos de dados com a chave A ou B. Porém, o bloco de configuração só pode ser lido ou escrito usando a chave A e a chave B pode ser lida. Portanto, apenas a chave A pode ser usada para autenticação; tipicamente vem de fábrica como FFFFFFFFFFFF.

Usando um cartão RFID Mifare para um sistema de passagens

Na aplicação de bilhetagem (ou de “carteira eletrônica”), o cartão (ou tag) é carregado com uma certa quantidade de “créditos” que vão sendo consumidos em troca de serviços ou bens. Olhando novamente a tabela de controle de blocos de dados, repare que existem dois usos possíveis: dados e valor. Você poderia implementar a bilhetagem usando o modo dados, mas o modo valor existe exatamente para que as operações de carga e consumo sejam feitas de forma mais segura.

No uso como dados o cartão não se preocupa com o que você escreve nos blocos, para uso como valor o conteúdo do bloco precisa estar em um formato específico:

Imagem 5 - Conteúdo do bloco

Na figura acima value é o valor (saldo); para maior segurança ele é repetido três vezes no bloco (uma delas negado). O valor é um número com sinal de quatro bytes (no formato complemento de 2); ou seja de -2.147.483.648 a 2.147.483.647. adr é o endereço de um outro bloco do setor para “gerenciamento de backup” (segundo o datasheet, não encontrei explicações ou exemplo de uso).

Uma vez configurado um bloco para uso como valor, o seu acesso fica condicionado a possuir o formato correto e existem quatro operações básicas que podem ser feitas com ele:

  • incremento, onde um “crédito” é somado ao valor; o resultado fica em um registrador temporário interno ao cartão
  • decremento, onde um “débito” é subtraído do valor; o resultado fica em um registrador temporário interno ao cartão
  • restauração, onde o valor atual é copiado para o registrador temporário
  • transferência, onde o valor no registrador temporário é gravado no bloco

Portanto um carga é feita combinando um incremento com transferência e um consumo é feito combinando um decremento com transferência.

Olhando novamente a tabela do controle de acesso, percebemos que no modo “001” qualquer uma das chaves pode ser usada para fazer um consumo, mas não há como fazer uma carga (isto é útil para cartões pré-carregados e descartáveis). No modo “110” qualquer chave pode ser usada para fazer um consumo mas apenas a B pode fazer uma carga (os pontos onde o cartão é usado conhecem apenas a chave A; a chave B fica somente nos poucos pontos de recarga).

Montagem do Sistema de Passagem com leitor RFID e Arduino

A conexão do módulo Rfid ao Arduino deve ser feita conforme a figura abaixo:

Imagem 6 - Ciricuito do sistema de passagens com Módulo Leitor Rfid e Arduino

Código para o Sistema de Passagem com leitor RFID e Arduino

Para o nosso programa exemplo vamos usar a biblioteca MFRC522 que pode ser instalada diretamente da IDE:

Imagem 7 - Instalação da biblioteca

Neste exemplo vamos usar o Serial Monitor da IDE para interagir com o programa. Para isto certifique-se que ele está configurado como abaixo:

Imagem 8 - Configuração para o Serial Monitor

O programa permite as seguintes operações, cada uma disparada por uma letra:

  • (I)niciar o cartão: reconfigura o bloco x do setor y para o modo dados (usando a chave de fábrica ou a nossa chave B), grava um valor inicial de 20 e o reconfigura para o modo valor com as nossas chaves A e B
  • (R)ecarga: acrescenta 20 “créditos” ao saldo no cartão
  • (C)onsumo: tenta fazer uma “compra” com o custo de 3 “créditos”
  • (S)aldo: apresenta o saldo no cartão
  • (F)abrica: volta o setor para a configuração de fábrica

Abaixo o código:

#include <SPI.h>
#include <MFRC522.h>

// Acesso ao módulo Rfid
const byte RST_PIN = 9;   // pino de reset
const byte SS_PIN = 10;   // pino de seleção
MFRC522 mfrc522(SS_PIN, RST_PIN);

// Chaves
typedef enum { FABRICA, NOSSA, OUTRA } CHAVE;
MFRC522::MIFARE_Key chaveFabrica;
MFRC522::MIFARE_Key chaveA;
MFRC522::MIFARE_Key chaveB;
CHAVE chave;

// Vamos usar o setor #4, que contem os blocos 16 a 19
const byte SETOR = 4;
const byte BOLOCO_VAL = 17;
const byte BLOCO_CFG = 19;

// Valores de recarga e consumo
const int32_t VALOR_RECARGA = 10;
const int32_t VALOR_CONSUMO = 3;

// Iniciação
void setup() {
  // Inicia a porta serial
  Serial.begin (115200);

  // Inicia o acesso ao módulo
  SPI.begin();
  mfrc522.PCD_Init();

  // Inicia as chaves
  for (byte i = 0; i < 6; i++) {
    chaveFabrica.keyByte[i] = 0xFF;
  }
  chaveA.keyByte[0] = 'D';  chaveA.keyByte[1] = 'Q';  chaveA.keyByte[2] = 'S';
  chaveA.keyByte[3] = 'o';  chaveA.keyByte[4] = 'f';  chaveA.keyByte[5] = 't';
  chaveB.keyByte[0] = '1';  chaveB.keyByte[1] = '2';  chaveB.keyByte[2] = '3';
  chaveB.keyByte[3] = '4';  chaveB.keyByte[4] = '5';  chaveB.keyByte[5] = '6';
}

// Laço principal
void loop() {
  // Solicita uma opção
  Serial.print ("(I)nicia, (R)ecarrega, (C)onsome, (S)aldo ou (F)abrica?");
  int c;
  while (Serial.read() != -1) {
    delay(10);
  }
  do {
    c = Serial.read();
    c = toupper(c);
  } while (strchr("IRCSF", c) == NULL);
  char cmd[2] = "";
  cmd[0] = (char) c;
  Serial.println (cmd);

  // Pede para aproximar um cartão
  if (aguardaCartao()) {
    // Trata a opção
    if (c == 'I') {
      inicia();
    } else if (c == 'R') {
      recarrega();
    } else if (c == 'C') {
      consome();
    } else if (c == 'S') {
      saldo();
    } else if (c == 'F') {
      fabrica();
    }
    mfrc522.PICC_HaltA();
    mfrc522.PCD_StopCrypto1();
  }
  Serial.println();
}

// Inicia o cartão para uso
void inicia() {
  MFRC522::StatusCode status;
  
  // Se autentica com a chave B
  status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_B, BLOCO_CFG, 
      (chave == FABRICA)? &chaveFabrica : &chaveB, &(mfrc522.uid));
  if (status != MFRC522::STATUS_OK) {
    Serial.println ("Erro na autenticacao!");
    return;
  }

  // Grava o setor de valor com o formato correto
  if (!iniciaValor(BOLOCO_VAL)) {
    return;
  }

  // Reconfigura o setor para usar nossas chaves e as condições de acesso desejadas
  // 1o bloco 000 configuração padrão, pode acessar com chaves A ou B
  // 2o bloco 110 valor, leitura/decremento com chave A ou B, escrita/incremento só com B
  // 3o bloco 000 configuração padrão, pode acessar com chaves A ou B
  // 4o bloco 011 escrita só com B, chave A só lê bits de acesso
  byte cfgBuffer[16];
  memset (cfgBuffer, 0, 16);
  memcpy (cfgBuffer, &chaveA, 6);
  memcpy (cfgBuffer+10, &chaveB, 6);
  mfrc522.MIFARE_SetAccessBits(&cfgBuffer[6], 0, 6, 0, 3);
  status = mfrc522.MIFARE_Write(BLOCO_CFG, cfgBuffer, 16);
  if (status != MFRC522::STATUS_OK) {
    Serial.println ("Erro ao configurar!");
  }
  Serial.println ("Cartao iniciado");
}

// Recarrega o cartão, acrescentando VALOR_RECARGA "créditos"
void recarrega() {
  MFRC522::StatusCode status;

  if (chave != NOSSA) {
    Serial.println ("Cartao nao iniciado!");
    return;
  }
  
  // Se autentica com a chave B
  status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_B, BLOCO_CFG, 
      &chaveB, &(mfrc522.uid));
  if (status != MFRC522::STATUS_OK) {
    Serial.println ("Erro na autenticacao!");
    return;
  }

  status = mfrc522.MIFARE_Increment(BOLOCO_VAL, VALOR_RECARGA);
  if (status != MFRC522::STATUS_OK) {
    Serial.println ("Erro ao recarregar!");
    return;
  }
  status = mfrc522.MIFARE_Transfer(BOLOCO_VAL);
  if (status != MFRC522::STATUS_OK) {
    Serial.println ("Erro na salvar novo saldo!");
    return;
  }

  // Mostra o novo saldo
  saldo();
}

// Consome VALOR_CONSUMO "créditos"
void consome() {
  MFRC522::StatusCode status;

  if (chave != NOSSA) {
    Serial.println ("Cartao nao iniciado!");
    return;
  }
  
  // Se autentica com a chave A
  status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, BLOCO_CFG, 
      &chaveA, &(mfrc522.uid));
  if (status != MFRC522::STATUS_OK) {
    Serial.println ("Erro na autenticacao!");
    return;
  }

  // Vê se tem saldo suficente
  int32_t valor;
  status = mfrc522.MIFARE_GetValue(BOLOCO_VAL, &valor);
  if (status != MFRC522::STATUS_OK) {
    Serial.println ("Erro na leitura!");
    return;
  }
  if (valor < VALOR_CONSUMO) {
    Serial.println ("Saldo insuficiente!");
    return;
  }

  // Atualiza o saldo
  status = mfrc522.MIFARE_Decrement(BOLOCO_VAL, VALOR_CONSUMO);
  if (status != MFRC522::STATUS_OK) {
    Serial.println ("Erro ao debitar!");
    return;
  }
  status = mfrc522.MIFARE_Transfer(BOLOCO_VAL);
  if (status != MFRC522::STATUS_OK) {
    Serial.println ("Erro na salvar novo saldo!");
    return;
  }

  // Mostra o novo saldo
  saldo();
}

// Informa o saldo
void saldo() {
  MFRC522::StatusCode status;

  if (chave != NOSSA) {
    Serial.println ("Cartao nao iniciado!");
    return;
  }
  
  // Se autentica com a chave A
  status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, BLOCO_CFG, 
      &chaveA, &(mfrc522.uid));
  if (status != MFRC522::STATUS_OK) {
    Serial.println ("Erro na autenticacao!");
    return;
  }

  // Le o Valor
  int32_t valor;
  status = mfrc522.MIFARE_GetValue(BOLOCO_VAL, &valor);
  if (status != MFRC522::STATUS_OK) {
    Serial.println ("Erro na leitura!");
    return;
  }
  Serial.print ("Saldo: ");
  Serial.println (valor);
}

// Recoloca o cartão na condição de fábrica
void fabrica() {
  MFRC522::StatusCode status;

  // Se autentica com a chave B
  status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_B, BLOCO_CFG, 
      (chave == FABRICA)? &chaveFabrica : &chaveB, &(mfrc522.uid));
  if (status != MFRC522::STATUS_OK) {
    Serial.println ("Erro na autenticacao!");
    return;
  }
  
  // Reconfigura o setor para a configuração padrão
  byte cfgBuffer[16];
  memset (cfgBuffer, 0, 16);
  memcpy (cfgBuffer, &chaveFabrica, 6);
  memcpy (cfgBuffer+10, &chaveFabrica, 6);
  mfrc522.MIFARE_SetAccessBits(&cfgBuffer[6], 0, 0, 0, 1);
  status = mfrc522.MIFARE_Write(BLOCO_CFG, cfgBuffer, 16);
  if (status != MFRC522::STATUS_OK) {
    Serial.println ("Erro ao configurar!");
  }
  Serial.println ("Cartao de volta a condicao de fabrica");
}

// Aguarda apresentar um cartão ou digitar algo
bool aguardaCartao() {
  Serial.println ("Aproxime um cartao (digite qualquer tecla para desistir)");
  while (true) {
    if (Serial.read() != -1) {
      return false;
    }
    if (!mfrc522.PICC_IsNewCardPresent())
      continue;
    if (mfrc522.PICC_ReadCardSerial())
      break;
  }
  Serial.print ("ID: ");
  dump_buffer (mfrc522.uid.uidByte, mfrc522.uid.size);
  Serial.println();
  
  Serial.print(F("Tipo: "));
  MFRC522::PICC_Type tipo = mfrc522.PICC_GetType(mfrc522.uid.sak);
  Serial.println(mfrc522.PICC_GetTypeName(tipo));

  // Verifica se tipo aceito
  if ( (tipo == MFRC522::PICC_TYPE_MIFARE_MINI) ||
       (tipo == MFRC522::PICC_TYPE_MIFARE_1K) ||
       (tipo == MFRC522::PICC_TYPE_MIFARE_4K) ) {
    chave = testaChave();
    return chave != OUTRA;
  }
  Serial.println ("Cartao incompativel!");
  return false;
}

// Verifica qual a chave usada no nosso setor
CHAVE testaChave() {
  MFRC522::StatusCode status;

  status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, BLOCO_CFG, 
      &chaveFabrica, &(mfrc522.uid));
  if (status == MFRC522::STATUS_OK) {
    Serial.println ("Usa chave da fábrica");
    return FABRICA;
  }

  // Reinicia a comunicação, para poder testar outra chave
  mfrc522.PICC_HaltA();
  mfrc522.PCD_StopCrypto1();
  mfrc522.PICC_IsNewCardPresent();
  mfrc522.PICC_ReadCardSerial();
  
  status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, BLOCO_CFG, 
      &chaveA, &(mfrc522.uid));
  if (status == MFRC522::STATUS_OK) {
    Serial.println ("Usa a nossa chave");
    return NOSSA;
  }
  
  Serial.println ("Chave desconhecida!");
  return OUTRA;
}

// Inicia um bloco no formato apropriado para valor
bool iniciaValor(byte bloco) {
  MFRC522::StatusCode status;
  byte valueBlock[] = {
      0, 0, 0, 0,
      255, 255, 255, 255,
      0, 0, 0, 0,
      bloco, ~bloco, bloco, ~bloco };
  status = mfrc522.MIFARE_Write(bloco, valueBlock, 16);
  if (status != MFRC522::STATUS_OK) {
      Serial.print("Erro ao gravar bloco!");
      return false;
  }
  return true;
}

// Mostra o conteudo de um buffer em hexa
void dump_buffer(byte *buffer, byte tam) {
  for (byte i = 0; i < tam; i++) {
      Serial.print(buffer[i] < 0x10 ? " 0" : " ");
      Serial.print(buffer[i], HEX);
  }
}

Resultado do Sistema de Passagem com leitor RFID e Arduino

A imagem abaixo mostra um exemplo de funcionamento.

Imagem 9 - Resultado do sistema de passagens com Módulo Leitor Rfid e Arduino

Você pode experimentar com outros valores de recarga e consumo alterando as linhas que definem as constantes VALOR_RECARGA e VALOR_CONSUMO. Você pode também mudar as chaves em setup(), porém tome o cuidado de retornar o cartão à condição de fábrica antes de iniciá-lo com uma nova chave.

Sugestões para Projetos

Neste post vimos o básico de como usar o cartão com bilhetagem, disparando as operações através do monitor serial. Algumas ideias de projeto usando estes conhecimentos:

  • Conectar um display e botões ao Arduino para operar sem conexão a um micro.
  • Após um consumo bem sucedido, acionar um servo motor para liberar uma recompensa
  • Montar uma mini “vending machine“, onde um produto é selecionado e, após registrar o consumo, um motor de passo ou servo é usado para entregar o produto.

Gostou do post Sistema de passagens com Módulo Leitor Rfid e Arduino? Ajude-nos a melhorar o blog comentando abaixo sobre este tutorial. Não se esqueça de visitar nosso Fórum para dúvidas e compartilhar suas ideias de projetos com a comunidade.

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *