Files
kidsbox/device/components/esp-idf-rc522/rc522.c
T
Claude 4f9946bd17 Vendor esp-idf-rc522 as plain source instead of submodule
The library is modified (NDEF reading, cascade anticollision) so vendoring
it directly avoids needing push access to the upstream fork.

https://claude.ai/code/session_01MhAnh68DPNnTjcKqjNYDmk
2026-06-12 19:46:21 +00:00

747 lines
20 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "driver/spi_master.h"
#include "soc/gpio_struct.h"
#include "driver/gpio.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "rc522.h"
static const char* TAG = "ESP-RC522";
struct rc522 {
bool running;
rc522_config_t* config;
spi_device_handle_t spi;
TaskHandle_t task_handle;
bool scan_started;
uint8_t* lastSerialNumber;
};
typedef struct rc522* rc522_handle_t;
static rc522_handle_t hndl = NULL;
#define rc522_fw_version() rc522_read(0x37)
bool rc522_is_inited() {
return hndl != NULL;
}
static esp_err_t rc522_spi_init() {
if(! hndl || ! hndl->config) {
ESP_LOGE(TAG, "Fail to init SPI. Invalid handle");
return ESP_ERR_INVALID_STATE;
}
if(hndl->spi) {
ESP_LOGW(TAG, "SPI already initialized");
return ESP_ERR_INVALID_STATE;
}
spi_bus_config_t buscfg = {
.miso_io_num = hndl->config->miso_io,
.mosi_io_num = hndl->config->mosi_io,
.sclk_io_num = hndl->config->sck_io,
.quadwp_io_num = -1,
.quadhd_io_num = -1
};
spi_device_interface_config_t devcfg = {
.clock_speed_hz = 5000000,
.mode = 0,
.spics_io_num = hndl->config->sda_io,
.queue_size = 7,
.flags = SPI_DEVICE_HALFDUPLEX
};
esp_err_t err = spi_bus_initialize(hndl->config->spi_host_id, &buscfg, 0);
if(err != ESP_OK) {
return err;
}
err = spi_bus_add_device(hndl->config->spi_host_id, &devcfg, &hndl->spi);
if(err != ESP_OK) {
spi_bus_free(hndl->config->spi_host_id);
hndl->spi = NULL;
}
return err;
}
static esp_err_t rc522_write_n(uint8_t addr, uint8_t n, uint8_t *data) {
uint8_t* buffer = (uint8_t*) malloc(n + 1);
buffer[0] = (addr << 1) & 0x7E;
for (uint8_t i = 1; i <= n; i++) {
buffer[i] = data[i-1];
}
spi_transaction_t t;
memset(&t, 0, sizeof(t));
t.length = 8 * (n + 1);
t.tx_buffer = buffer;
esp_err_t ret = spi_device_transmit(hndl->spi, &t);
free(buffer);
return ret;
}
static esp_err_t rc522_write(uint8_t addr, uint8_t val) {
return rc522_write_n(addr, 1, &val);
}
static uint8_t* rc522_read_n(uint8_t addr, uint8_t n) {
if (n <= 0) {
return NULL;
}
spi_transaction_t t;
memset(&t, 0, sizeof(t));
uint8_t* buffer = (uint8_t*) malloc(n);
t.flags = SPI_TRANS_USE_TXDATA;
t.length = 8;
t.tx_data[0] = ((addr << 1) & 0x7E) | 0x80;
t.rxlength = 8 * n;
t.rx_buffer = buffer;
esp_err_t ret = spi_device_transmit(hndl->spi, &t);
assert(ret == ESP_OK);
return buffer;
}
static uint8_t rc522_read(uint8_t addr) {
uint8_t* buffer = rc522_read_n(addr, 1);
uint8_t res = buffer[0];
free(buffer);
return res;
}
static esp_err_t rc522_set_bitmask(uint8_t addr, uint8_t mask) {
return rc522_write(addr, rc522_read(addr) | mask);
}
static esp_err_t rc522_clear_bitmask(uint8_t addr, uint8_t mask) {
return rc522_write(addr, rc522_read(addr) & ~mask);
}
static esp_err_t rc522_antenna_on() {
esp_err_t ret;
if(~ (rc522_read(0x14) & 0x03)) {
ret = rc522_set_bitmask(0x14, 0x03);
if(ret != ESP_OK) {
return ret;
}
}
return rc522_write(0x26, 0x60); // 43dB gain
}
static void rc522_task(void* arg);
esp_err_t rc522_init(rc522_config_t* config) {
if(! config) {
return ESP_ERR_INVALID_ARG;
}
if(hndl) {
ESP_LOGW(TAG, "Already initialized");
return ESP_ERR_INVALID_STATE;
}
if(! (hndl = calloc(1, sizeof(struct rc522)))) {
return ESP_ERR_NO_MEM;
}
if(! (hndl->config = calloc(1, sizeof(rc522_config_t)))) {
rc522_destroy();
return ESP_ERR_NO_MEM;
}
// copy config considering defaults
hndl->config->callback = config->callback;
hndl->config->ndef_callback = config->ndef_callback;
hndl->config->miso_io = config->miso_io == 0 ? RC522_DEFAULT_MISO : config->miso_io;
hndl->config->mosi_io = config->mosi_io == 0 ? RC522_DEFAULT_MOSI : config->mosi_io;
hndl->config->sck_io = config->sck_io == 0 ? RC522_DEFAULT_SCK : config->sck_io;
hndl->config->sda_io = config->sda_io == 0 ? RC522_DEFAULT_SDA : config->sda_io;
hndl->config->spi_host_id = config->spi_host_id == 0 ? RC522_DEFAULT_SPI_HOST : config->spi_host_id;
hndl->config->scan_interval_ms = config->scan_interval_ms < 50 ? RC522_DEFAULT_SCAN_INTERVAL_MS : config->scan_interval_ms;
hndl->config->task_stack_size = config->task_stack_size == 0 ? RC522_DEFAULT_TACK_STACK_SIZE : config->task_stack_size;
hndl->config->task_priority = config->task_priority == 0 ? RC522_DEFAULT_TACK_STACK_PRIORITY : config->task_priority;
esp_err_t err = rc522_spi_init();
if(err != ESP_OK) {
rc522_destroy();
return err;
}
// ---------- RW test ------------
const uint8_t test_addr = 0x24, test_val = 0x25;
for(uint8_t i = test_val; i < test_val + 2; i++) {
if((err = rc522_write(test_addr, i)) != ESP_OK || rc522_read(test_addr) != i) {
ESP_LOGE(TAG, "RW test fail");
rc522_destroy();
return err;
}
}
// ------- End of RW test --------
rc522_write(0x01, 0x0F);
rc522_write(0x2A, 0x8D);
rc522_write(0x2B, 0x3E);
rc522_write(0x2D, 0x1E);
rc522_write(0x2C, 0x00);
rc522_write(0x15, 0x40);
rc522_write(0x11, 0x3D);
rc522_antenna_on();
hndl->running = true;
if (xTaskCreate(rc522_task, "rc522_task", hndl->config->task_stack_size, NULL, hndl->config->task_priority, &hndl->task_handle) != pdTRUE) {
ESP_LOGE(TAG, "Fail to create rc522 task");
rc522_destroy();
return err;
}
if(err != ESP_OK) {
ESP_LOGE(TAG, "Fail to create timer");
rc522_destroy();
return err;
}
ESP_LOGI(TAG, "Initialized (firmware: 0x%x)", rc522_fw_version());
return ESP_OK;
}
uint64_t rc522_sn_to_u64(uint8_t* sn) {
if(!sn) {
return 0;
}
uint64_t result = 0;
for(int i = 4; i >= 0; i--) {
result |= ((uint64_t) sn[i] << (i * 8));
}
return result;
}
/* Returns pointer to dynamically allocated array of two element */
static uint8_t* rc522_calculate_crc(uint8_t *data, uint8_t n) {
rc522_clear_bitmask(0x05, 0x04);
rc522_set_bitmask(0x0A, 0x80);
rc522_write_n(0x09, n, data);
rc522_write(0x01, 0x03);
uint8_t i = 255;
uint8_t nn = 0;
for(;;) {
nn = rc522_read(0x05);
i--;
if(! (i != 0 && ! (nn & 0x04))) {
break;
}
}
uint8_t* res = (uint8_t*) malloc(2);
res[0] = rc522_read(0x22);
res[1] = rc522_read(0x21);
return res;
}
static uint8_t* rc522_card_write(uint8_t cmd, uint8_t *data, uint8_t n, uint8_t* res_n) {
uint8_t *result = NULL;
uint8_t irq = 0x00;
uint8_t irq_wait = 0x00;
uint8_t last_bits = 0;
uint8_t nn = 0;
if(cmd == 0x0E) {
irq = 0x12;
irq_wait = 0x10;
}
else if(cmd == 0x0C) {
irq = 0x77;
irq_wait = 0x30;
}
rc522_write(0x02, irq | 0x80);
rc522_clear_bitmask(0x04, 0x80);
rc522_set_bitmask(0x0A, 0x80);
rc522_write(0x01, 0x00);
rc522_write_n(0x09, n, data);
rc522_write(0x01, cmd);
if(cmd == 0x0C) {
rc522_set_bitmask(0x0D, 0x80);
}
uint16_t i = 1000;
for(;;) {
nn = rc522_read(0x04);
i--;
if(! (i != 0 && (((nn & 0x01) == 0) && ((nn & irq_wait) == 0)))) {
break;
}
}
rc522_clear_bitmask(0x0D, 0x80);
if(i != 0) {
if((rc522_read(0x06) & 0x1B) == 0x00) {
if(cmd == 0x0C) {
nn = rc522_read(0x0A);
last_bits = rc522_read(0x0C) & 0x07;
if (last_bits != 0) {
*res_n = (nn - 1) + last_bits;
} else {
*res_n = nn;
}
result = (uint8_t*) malloc(*res_n);
for(i = 0; i < *res_n; i++) {
result[i] = rc522_read(0x09);
}
}
}
}
return result;
}
static uint8_t* rc522_request(uint8_t* res_n) {
uint8_t* result = NULL;
rc522_write(0x0D, 0x07);
uint8_t req_mode = 0x26;
result = rc522_card_write(0x0C, &req_mode, 1, res_n);
if(*res_n * 8 != 0x10) {
free(result);
return NULL;
}
return result;
}
static uint8_t* rc522_anticoll() {
uint8_t res_n;
rc522_write(0x0D, 0x00);
uint8_t* result = rc522_card_write(0x0C, (uint8_t[]) { 0x93, 0x20 }, 2, &res_n);
if(result && res_n != 5) { // all cards/tags serial numbers is 5 bytes long (?)
free(result);
return NULL;
}
return result;
}
/* Send HALT and clear crypto bit. Card will only respond to WUPA after this. */
static void rc522_halt_card() {
uint8_t buf[] = { 0x50, 0x00, 0x00, 0x00 };
uint8_t* crc = rc522_calculate_crc(buf, 2);
buf[2] = crc[0];
buf[3] = crc[1];
free(crc);
uint8_t res_n;
uint8_t* res = rc522_card_write(0x0C, buf, 4, &res_n);
free(res);
rc522_clear_bitmask(0x08, 0x08);
}
static uint8_t* rc522_get_tag() {
uint8_t* result = NULL;
uint8_t* res_data = NULL;
uint8_t res_data_n;
res_data = rc522_request(&res_data_n);
if(res_data != NULL) {
free(res_data);
result = rc522_anticoll();
/* NOTE: card is NOT halted here — caller must call rc522_halt_card() */
}
return result;
}
/* URI prefix table for NDEF URI records (RFC 3986 shortened forms) */
static const char* const RC522_URI_PREFIXES[] = {
"", /* 0x00 no prefix */
"http://www.", /* 0x01 */
"https://www.", /* 0x02 */
"http://", /* 0x03 */
"https://", /* 0x04 */
"tel:", /* 0x05 */
"mailto:", /* 0x06 */
"ftp://anonymous:anonymous@", /* 0x07 */
"ftp://ftp.", /* 0x08 */
"ftps://", /* 0x09 */
"sftp://", /* 0x0A */
"smb://", /* 0x0B */
"nfs://", /* 0x0C */
"ftp://", /* 0x0D */
"dav://", /* 0x0E */
"news:", /* 0x0F */
"telnet://", /* 0x10 */
"imap:", /* 0x11 */
"rtsp://", /* 0x12 */
"urn:", /* 0x13 */
"pop:", /* 0x14 */
"sip:", /* 0x15 */
"sips:", /* 0x16 */
"tftp:", /* 0x17 */
"btspp://", /* 0x18 */
"btl2cap://", /* 0x19 */
"btgoep://", /* 0x1A */
"tcpobex://", /* 0x1B */
"irdaobex://", /* 0x1C */
"file://", /* 0x1D */
"urn:epc:id:", /* 0x1E */
"urn:epc:tag:", /* 0x1F */
"urn:epc:pat:", /* 0x20 */
"urn:epc:raw:", /* 0x21 */
"urn:epc:", /* 0x22 */
"urn:nfc:", /* 0x23 */
};
#define RC522_URI_PREFIX_COUNT (sizeof(RC522_URI_PREFIXES) / sizeof(RC522_URI_PREFIXES[0]))
/* Parse an NDEF message TLV block and extract the first URI record URL.
* Returns heap-allocated null-terminated URL string, or NULL if not found. */
static char* rc522_parse_ndef_url(const uint8_t* data, int len) {
int i = 0;
while(i < len) {
uint8_t tlv_type = data[i++];
if(tlv_type == 0xFE) break; /* Terminator TLV */
if(tlv_type == 0x00) continue; /* Null TLV — no length byte */
if(i >= len) break;
/* Read TLV length (1 or 3 bytes) */
uint16_t tlv_len;
if(data[i] == 0xFF) {
i++;
if(i + 1 >= len) break;
tlv_len = ((uint16_t)data[i] << 8) | data[i + 1];
i += 2;
} else {
tlv_len = data[i++];
}
if(tlv_type != 0x03 || tlv_len == 0 || i + (int)tlv_len > len) {
i += tlv_len;
continue;
}
/* Parse NDEF record header */
uint8_t header = data[i++];
uint8_t tnf = header & 0x07;
bool sr = (header & 0x10) != 0; /* Short Record */
bool il = (header & 0x08) != 0; /* ID Length present */
if(i >= len) break;
uint8_t type_len = data[i++];
uint32_t payload_len;
if(sr) {
if(i >= len) break;
payload_len = data[i++];
} else {
if(i + 3 >= len) break;
payload_len = ((uint32_t)data[i] << 24) | ((uint32_t)data[i+1] << 16)
| ((uint32_t)data[i+2] << 8) | data[i+3];
i += 4;
}
uint8_t id_len = 0;
if(il) {
if(i >= len) break;
id_len = data[i++];
}
/* Only handle Well-Known URI records (TNF=0x01, Type='U') */
if(tnf == 0x01 && type_len == 1 && i < len && data[i] == 0x55) {
i++; /* skip type byte 'U' */
i += id_len; /* skip optional ID */
if(payload_len > 0 && i < len) {
uint8_t prefix_code = data[i++];
payload_len--;
const char* prefix = (prefix_code < RC522_URI_PREFIX_COUNT) ? RC522_URI_PREFIXES[prefix_code] : "";
size_t prefix_len = strlen(prefix);
size_t body_len = (payload_len < (uint32_t)(len - i)) ? payload_len : (uint32_t)(len - i);
if(prefix_len + body_len > 255) body_len = 255 - prefix_len;
char* url = (char*) malloc(prefix_len + body_len + 1);
if(!url) return NULL;
memcpy(url, prefix, prefix_len);
memcpy(url + prefix_len, data + i, body_len);
url[prefix_len + body_len] = '\0';
return url;
}
}
/* Skip over this record */
i += type_len + id_len + (int)payload_len;
}
return NULL;
}
/* Wake a halted card with WUPA (0x52), perform cascade anticollision (CL1 + optional CL2),
* SELECT the card, read pages 4–15 of user memory (NDEF area), parse, then halt.
* Returns heap-allocated URL string or NULL. Card is always left in HALT state on return. */
static char* rc522_read_ndef_url() {
uint8_t res_n;
/* WUPA: wake halted card — identical bit-framing to REQA but byte is 0x52 */
rc522_write(0x0D, 0x07);
uint8_t wupa_cmd = 0x52;
uint8_t* atqa = rc522_card_write(0x0C, &wupa_cmd, 1, &res_n);
if(!atqa || res_n * 8 != 0x10) {
free(atqa);
return NULL;
}
free(atqa);
/* CL1 anti-collision */
rc522_write(0x0D, 0x00);
uint8_t* cl1 = rc522_card_write(0x0C, (uint8_t[]){ 0x93, 0x20 }, 2, &res_n);
if(!cl1 || res_n != 5) {
free(cl1);
rc522_halt_card();
return NULL;
}
bool cascade = (cl1[0] == 0x88); /* cascade tag byte indicates 7-byte UID */
/* CL1 SELECT */
uint8_t sel1[9] = { 0x93, 0x70, cl1[0], cl1[1], cl1[2], cl1[3], cl1[4] };
uint8_t* crc = rc522_calculate_crc(sel1, 7);
sel1[7] = crc[0]; sel1[8] = crc[1]; free(crc);
uint8_t sel_n;
uint8_t* sak1 = rc522_card_write(0x0C, sel1, 9, &sel_n);
free(cl1);
if(!sak1) {
rc522_halt_card();
return NULL;
}
free(sak1);
if(cascade) {
/* CL2 anti-collision */
rc522_write(0x0D, 0x00);
uint8_t* cl2 = rc522_card_write(0x0C, (uint8_t[]){ 0x95, 0x20 }, 2, &res_n);
if(!cl2 || res_n != 5) {
free(cl2);
rc522_halt_card();
return NULL;
}
/* CL2 SELECT */
uint8_t sel2[9] = { 0x95, 0x70, cl2[0], cl2[1], cl2[2], cl2[3], cl2[4] };
crc = rc522_calculate_crc(sel2, 7);
sel2[7] = crc[0]; sel2[8] = crc[1]; free(crc);
uint8_t* sak2 = rc522_card_write(0x0C, sel2, 9, &sel_n);
free(cl2);
if(!sak2) {
rc522_halt_card();
return NULL;
}
free(sak2);
}
/* Card is now SELECTED. Verify NDEF capability container at page 3.
* For NTAG/MIFARE Ultralight the NDEF magic byte is 0xE1 at CC[0]. */
uint8_t read_cc[4] = { 0x30, 0x03 };
crc = rc522_calculate_crc(read_cc, 2);
read_cc[2] = crc[0]; read_cc[3] = crc[1]; free(crc);
uint8_t* cc_data = rc522_card_write(0x0C, read_cc, 4, &res_n);
if(!cc_data || res_n < 4 || cc_data[0] != 0xE1) {
free(cc_data);
rc522_halt_card();
return NULL;
}
free(cc_data);
/* Read NDEF user pages 4–15 (12 pages × 4 bytes = 48 bytes).
* READ command (0x30) returns 16 bytes (4 pages) per call. */
uint8_t ndef_buf[48];
int ndef_len = 0;
for(uint8_t page = 4; page <= 12; page += 4) { /* 3 reads × 16 bytes = 48 bytes */
uint8_t read_cmd[4] = { 0x30, page };
crc = rc522_calculate_crc(read_cmd, 2);
read_cmd[2] = crc[0]; read_cmd[3] = crc[1]; free(crc);
uint8_t* page_data = rc522_card_write(0x0C, read_cmd, 4, &res_n);
if(!page_data || res_n < 16) {
free(page_data);
break;
}
int copy = (48 - ndef_len < 16) ? 48 - ndef_len : 16;
memcpy(ndef_buf + ndef_len, page_data, copy);
ndef_len += copy;
free(page_data);
}
rc522_halt_card();
if(ndef_len == 0) return NULL;
return rc522_parse_ndef_url(ndef_buf, ndef_len);
}
esp_err_t rc522_start(rc522_start_args_t start_args) {
esp_err_t err = rc522_init(&start_args);
return err != ESP_OK ? err : rc522_start2();
}
esp_err_t rc522_start2() {
if(! hndl) { return ESP_ERR_INVALID_STATE; }
hndl->scan_started = true;
return ESP_OK;
}
esp_err_t rc522_pause() {
if(! hndl) { return ESP_ERR_INVALID_STATE; }
if(! hndl->scan_started) {
return ESP_OK;
}
hndl->scan_started = false;
return ESP_OK;
}
void rc522_destroy() {
if(! hndl) { return; }
rc522_pause(); // stop timer
hndl->running = false; // task will delete itself
if(hndl->spi) {
spi_bus_remove_device(hndl->spi);
spi_bus_free(hndl->config->spi_host_id);
hndl->spi = NULL;
}
free(hndl->config);
hndl->config = NULL;
free(hndl);
hndl = NULL;
}
static bool isSameSerialNumber(uint8_t* one, uint8_t* other) {
if (one == NULL && other == NULL) {
return true;
}
if (one == NULL || other == NULL) {
return false;
}
return one[0] == other[0]
&& one[1] == other[1]
&& one[2] == other[2]
&& one[3] == other[3]
&& one[4] == other[4];
}
static void rc522_task(void* arg) {
while(hndl->running) {
if(!hndl->scan_started) {
vTaskDelay(100 / portTICK_PERIOD_MS);
continue;
}
uint8_t* serial_no = rc522_get_tag(); /* card NOT halted yet */
if(!isSameSerialNumber(serial_no, hndl->lastSerialNumber)) {
rc522_tag_callback_t cb = hndl->config->callback;
rc522_ndef_callback_t ndef_cb = hndl->config->ndef_callback;
if(serial_no == NULL) {
free(hndl->lastSerialNumber);
hndl->lastSerialNumber = NULL;
if(cb) cb(NULL);
if(ndef_cb) ndef_cb(NULL, NULL);
} else {
if(!hndl->lastSerialNumber) {
hndl->lastSerialNumber = malloc(5);
}
memcpy(hndl->lastSerialNumber, serial_no, 5);
if(cb) cb(serial_no);
if(ndef_cb) {
/* rc522_read_ndef_url() handles HALT internally after reading */
char* url = rc522_read_ndef_url();
ndef_cb(serial_no, url);
if(url) free(url);
} else {
rc522_halt_card();
}
}
} else if(serial_no != NULL) {
/* Same card still present — halt it again to prevent continuous REQA responses */
rc522_halt_card();
}
int delay_ms = hndl->config->scan_interval_ms;
if(serial_no != NULL) {
free(serial_no);
serial_no = NULL;
delay_ms *= 2;
}
vTaskDelay(delay_ms / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}