2025-12-26 6:47 AM - edited 2025-12-28 5:48 AM
I am entering the world of NFC with a somewhat complex project, but I took the initiative to start it. The goal was to create a sensor that would work only with Energy Harvesting from the reader.
For this, I chose the ST25R300 chip to be the chip for my reader board and sensor power supply, mainly due to its high transmission power.
I am having difficulty getting it to work properly. Basically, I ran several tests with some DEBUG codes on the reader board. In these tests, I found some inconsistencies. The first is the VDD_A voltages, which come from a reading of the chip itself, which is registering almost 6V, and the other inconsistency is the RSSI giving 0.
I believe the error is in my antenna and matching circuit. I would like someone to help me improve this system by pointing out where I may have made a mistake in their design.
I made the antenna using the ST website (eDesignSuite antenna design tool). I also made the maching circuit using ST's eDesignSuite.
In addition, my circuit was largely based on the core board (X-NUCLEO-NFC12A1).
The main premise was to develop something small that could perform energy harvesting, so I made a 10-turn antenna, which is larger than the TAG antenna. I made a mistake in developing a single-ended antenna instead of a differential one, but I wanted to start with something simpler first before perfecting it.
Could the design of a 10-turn antenna for the ST25R300 prevent the RF driver from oscillating to the point where the RSSI is zero, or does the VDD_A value suggest a hardware failure in the analog stage?
Observations:
Below is the test code:
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
nfc_reader.CS_port = GPIOB;
nfc_reader.CS_pin = ST25_CS_Pin;
nfc_reader.reset_port = GPIOA;
nfc_reader.reset_pin = GPIO_PIN_3;
nfc_reader.IRQ_port = GPIOA;
nfc_reader.IRQ_pin = GPIO_PIN_0;
nfc_reader.hSPIx = &hspi2;
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_I2C2_Init();
MX_SPI1_Init();
MX_SPI2_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
/* USER CODE BEGIN 2 */
if (ST25R300_Init(&nfc_reader)) {
DEBUG_PRINT("ST25R300 inicializado com sucesso!\r\n");
// Configura para modo RX
if (ST25R300_ConfigureRxMode(&nfc_reader)) {
DEBUG_PRINT("Modo RX configurado!\r\n");
// Habilita RX
if (ST25R300_EnableRx(&nfc_reader)) {
DEBUG_PRINT("RX habilitado - Pronto para medir RSSI!\r\n");
}
}
} else {
DEBUG_PRINT("ERRO: Falha na inicialização do ST25R300!\r\n");
}
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1) {
static uint32_t last_read = 0;
// Lê RSSI a cada 100ms
if (HAL_GetTick() - last_read >= 100) {
last_read = HAL_GetTick();
if (ST25R300_ReadRSSI(&nfc_reader, &rssi_data)) {
int db_int = (int)rssi_data.rssi_dbm;
int db_dec = (int)((rssi_data.rssi_dbm - (float)db_int) * 10.0f);
if (db_dec < 0) db_dec = -db_dec;
const char* sign = (rssi_data.rssi_dbm < 0 && db_int == 0) ? "-" : "";
DEBUG_PRINT("RSSI - I: %3d, Q: %3d, Total: %3d, dBm: %s%d.%d\r\n",
rssi_data.rssi_i,
rssi_data.rssi_q,
rssi_data.rssi_total,
sign,
db_int,
db_dec);
} else {
DEBUG_PRINT("ERRO: Falha na leitura do RSSI!\r\n");
}
}
}
/* USER CODE END 3 */
}/*
* st25r300.h
*
* Created on: Dec 25, 2025
* Author: Matheus Markies
*/
#ifndef INC_ST25R300_H_
#define INC_ST25R300_H_
#include "stm32l0xx_hal.h" // Ajuste conforme seu MCU
#include <stdint.h>
#include <stdbool.h>
/* Estrutura de configuração do ST25R300 */
typedef struct ST25R300_setting {
GPIO_TypeDef* CS_port;
uint16_t CS_pin;
GPIO_TypeDef* reset_port;
uint16_t reset_pin;
GPIO_TypeDef* IRQ_port;
uint16_t IRQ_pin;
SPI_HandleTypeDef* hSPIx;
} ST25R300;
/* Estrutura para dados de RSSI */
typedef struct {
uint8_t rssi_i; // RSSI canal I (7 bits)
uint8_t rssi_q; // RSSI canal Q (7 bits)
uint8_t rssi_total; // RSSI combinado
float rssi_dbm; // RSSI em dBm (aproximado)
} ST25R300_RSSI;
/* Definições de registros */
#define ST25R300_REG_OPERATION 0x00
#define ST25R300_REG_GENERAL_CONFIG 0x01
#define ST25R300_REG_RX_ANALOG_SETTINGS_1 0x09
#define ST25R300_REG_RX_ANALOG_SETTINGS_2 0x0A
#define ST25R300_REG_RX_DIGITAL_SETTINGS_1 0x0D
#define ST25R300_REG_PROTOCOL_1 0x14
#define ST25R300_REG_IRQ_STATUS_3 0x3E
#define ST25R300_REG_IC_IDENTITY 0x3F
#define ST25R300_REG_STATUS_1 0x40
#define ST25R300_REG_RSSI_DISPLAY_1 0x4A
#define ST25R300_REG_RSSI_DISPLAY_2 0x4B
/* Bits do Operation Register */
#define ST25R300_OP_EN (1 << 3) // Enable ready mode
#define ST25R300_OP_RX_EN (1 << 5) // Enable RX
#define ST25R300_OP_TX_EN (1 << 6) // Enable TX
#define ST25R300_OP_VDDDR_EN (1 << 4) // Enable VDD_DR regulator
/* Bits do Status Register 1 */
#define ST25R300_STATUS_OSC_OK (1 << 4) // Oscillator stable
/* Comandos diretos */
#define ST25R300_CMD_SET_DEFAULT 0x60
#define ST25R300_CMD_CLEAR_FIFO 0x64
#define ST25R300_CMD_ADJUST_REGULATORS 0x68
#define ST25R300_CMD_CALIBRATE_RC 0xEE
/* Timeouts */
#define ST25R300_TIMEOUT_MS 100
#define ST25R300_RESET_DELAY_MS 10
#define ST25R300_OSC_TIMEOUT_MS 50
/* Protótipos de funções */
bool ST25R300_Init(ST25R300 *dev);
bool ST25R300_Reset(ST25R300 *dev);
bool ST25R300_ConfigureRxMode(ST25R300 *dev);
bool ST25R300_EnableRx(ST25R300 *dev);
bool ST25R300_DisableRx(ST25R300 *dev);
bool ST25R300_ReadRSSI(ST25R300 *dev, ST25R300_RSSI *rssi);
bool ST25R300_ReadRegister(ST25R300 *dev, uint8_t reg_addr, uint8_t *data);
bool ST25R300_WriteRegister(ST25R300 *dev, uint8_t reg_addr, uint8_t data);
bool ST25R300_SendCommand(ST25R300 *dev, uint8_t cmd);
uint8_t ST25R300_ReadID(ST25R300 *dev);
bool ST25R300_WaitOscillatorReady(ST25R300 *dev);
#endif /* INC_ST25R300_H_ *//*
* st25r300.c
*
* Created on: Dec 25, 2025
* Author: Matheus Markies
*/
#include "st25r300.h"
#include <math.h>
/* Macros auxiliares */
#define CS_LOW() HAL_GPIO_WritePin(dev->CS_port, dev->CS_pin, GPIO_PIN_RESET)
#define CS_HIGH() HAL_GPIO_WritePin(dev->CS_port, dev->CS_pin, GPIO_PIN_SET)
#define RESET_LOW() HAL_GPIO_WritePin(dev->reset_port, dev->reset_pin, GPIO_PIN_RESET)
#define RESET_HIGH() HAL_GPIO_WritePin(dev->reset_port, dev->reset_pin, GPIO_PIN_SET)
/**
* @brief Lê um registro do ST25R300
* @PAram dev Ponteiro para estrutura do dispositivo
* @PAram reg_addr Endereço do registro (0x00-0x57)
* @PAram data Ponteiro para armazenar o dado lido
* @return true se sucesso, false se erro
*/
bool ST25R300_ReadRegister(ST25R300 *dev, uint8_t reg_addr, uint8_t *data) {
if (!dev || !data || reg_addr > 0x57) return false;
uint8_t tx_data[2] = {0x80 | reg_addr, 0xFF}; // Bit 7=1 para leitura
uint8_t rx_data[2] = {0};
CS_LOW();
HAL_StatusTypeDef status = HAL_SPI_TransmitReceive(dev->hSPIx, tx_data, rx_data, 2, ST25R300_TIMEOUT_MS);
CS_HIGH();
if (status == HAL_OK) {
*data = rx_data[1];
return true;
}
return false;
}
/**
* @brief Escreve em um registro do ST25R300
* @PAram dev Ponteiro para estrutura do dispositivo
* @PAram reg_addr Endereço do registro (0x00-0x57)
* @PAram data Dado a ser escrito
* @return true se sucesso, false se erro
*/
bool ST25R300_WriteRegister(ST25R300 *dev, uint8_t reg_addr, uint8_t data) {
if (!dev || reg_addr > 0x57) return false;
uint8_t tx_data[2] = {reg_addr, data}; // Bit 7=0 para escrita
CS_LOW();
HAL_StatusTypeDef status = HAL_SPI_Transmit(dev->hSPIx, tx_data, 2, ST25R300_TIMEOUT_MS);
CS_HIGH();
return (status == HAL_OK);
}
/**
* @brief Envia um comando direto ao ST25R300
* @PAram dev Ponteiro para estrutura do dispositivo
* @PAram cmd Código do comando (0x60-0xF9)
* @return true se sucesso, false se erro
*/
bool ST25R300_SendCommand(ST25R300 *dev, uint8_t cmd) {
if (!dev) return false;
CS_LOW();
HAL_StatusTypeDef status = HAL_SPI_Transmit(dev->hSPIx, &cmd, 1, ST25R300_TIMEOUT_MS);
CS_HIGH();
return (status == HAL_OK);
}
/**
* @brief Executa reset via pino RESET
* @PAram dev Ponteiro para estrutura do dispositivo
* @return true se sucesso
*/
bool ST25R300_Reset(ST25R300 *dev) {
if (!dev) return false;
// Pulso de reset (RESET high->low)
RESET_HIGH();
HAL_Delay(ST25R300_RESET_DELAY_MS);
RESET_LOW();
HAL_Delay(ST25R300_RESET_DELAY_MS);
return true;
}
/**
* @brief Lê o ID do chip
* @PAram dev Ponteiro para estrutura do dispositivo
* @return ID do chip (0x16 para ST25R300) ou 0 em caso de erro
*/
uint8_t ST25R300_ReadID(ST25R300 *dev) {
uint8_t id = 0;
if (ST25R300_ReadRegister(dev, ST25R300_REG_IC_IDENTITY, &id)) {
return (id >> 3) & 0x1F; // Bits 7:3 contêm o IC type
}
return 0;
}
/**
* @brief Aguarda o oscilador ficar estável
* @PAram dev Ponteiro para estrutura do dispositivo
* @return true se oscilador estável, false se timeout
*/
bool ST25R300_WaitOscillatorReady(ST25R300 *dev) {
uint32_t start_tick = HAL_GetTick();
uint8_t status;
while ((HAL_GetTick() - start_tick) < ST25R300_OSC_TIMEOUT_MS) {
if (ST25R300_ReadRegister(dev, ST25R300_REG_STATUS_1, &status)) {
if (status & ST25R300_STATUS_OSC_OK) {
return true;
}
}
HAL_Delay(1);
}
return false;
}
/**
* @brief Inicializa o ST25R300 para operação básica
* @PAram dev Ponteiro para estrutura do dispositivo
* @return true se sucesso, false se erro
*/
bool ST25R300_Init(ST25R300 *dev) {
if (!dev || !dev->hSPIx) return false;
// Reset do chip
ST25R300_Reset(dev);
HAL_Delay(10);
// Verifica ID do chip
uint8_t chip_id = ST25R300_ReadID(dev);
if (chip_id != 0x16) { // ST25R300 ID = 0x16
return false;
}
// Comando Set Default
ST25R300_SendCommand(dev, ST25R300_CMD_SET_DEFAULT);
HAL_Delay(5);
// Habilita Ready Mode (oscilador)
ST25R300_WriteRegister(dev, ST25R300_REG_OPERATION, ST25R300_OP_EN);
// Aguarda oscilador estabilizar
if (!ST25R300_WaitOscillatorReady(dev)) {
return false;
}
// Limpa FIFO
ST25R300_SendCommand(dev, ST25R300_CMD_CLEAR_FIFO);
// Calibra RC para AWS
ST25R300_SendCommand(dev, ST25R300_CMD_CALIBRATE_RC);
HAL_Delay(5);
return true;
}
/**
* @brief Configura o ST25R300 para modo RX
* @PAram dev Ponteiro para estrutura do dispositivo
* @return true se sucesso, false se erro
*/
bool ST25R300_ConfigureRxMode(ST25R300 *dev) {
if (!dev) return false;
// Configura Rx Analog Settings 1
// hpf_ctrl = 3 (380kHz), gain_boost = 0
ST25R300_WriteRegister(dev, ST25R300_REG_RX_ANALOG_SETTINGS_1, 0x53);
// Configura Rx Analog Settings 2
// afe_gain_rw = 0 (sem redução de ganho inicial)
ST25R300_WriteRegister(dev, ST25R300_REG_RX_ANALOG_SETTINGS_2, 0x00);
// Configura Rx Digital Settings 1
// AGC habilitado, filtros padrão
ST25R300_WriteRegister(dev, ST25R300_REG_RX_DIGITAL_SETTINGS_1, 0x8F);
// Configura Protocol Register 1 para NFC-A (modo padrão)
// om = 0x1 (ISO14443-A/NFC-A)
ST25R300_WriteRegister(dev, ST25R300_REG_PROTOCOL_1, 0x01);
// Ajusta reguladores
uint8_t op_reg;
ST25R300_ReadRegister(dev, ST25R300_REG_OPERATION, &op_reg);
ST25R300_WriteRegister(dev, ST25R300_REG_OPERATION,
op_reg | ST25R300_OP_VDDDR_EN);
HAL_Delay(1); // Aguarda 10us (1ms é mais que suficiente)
// Ajusta reguladores
ST25R300_SendCommand(dev, ST25R300_CMD_ADJUST_REGULATORS);
HAL_Delay(5);
return true;
}
/**
* @brief Habilita o modo RX
* @PAram dev Ponteiro para estrutura do dispositivo
* @return true se sucesso, false se erro
*/
bool ST25R300_EnableRx(ST25R300 *dev) {
if (!dev) return false;
uint8_t op_reg;
ST25R300_ReadRegister(dev, ST25R300_REG_OPERATION, &op_reg);
// Habilita RX
op_reg |= ST25R300_OP_RX_EN;
return ST25R300_WriteRegister(dev, ST25R300_REG_OPERATION, op_reg);
}
/**
* @brief Desabilita o modo RX
* @PAram dev Ponteiro para estrutura do dispositivo
* @return true se sucesso, false se erro
*/
bool ST25R300_DisableRx(ST25R300 *dev) {
if (!dev) return false;
uint8_t op_reg;
ST25R300_ReadRegister(dev, ST25R300_REG_OPERATION, &op_reg);
// Desabilita RX
op_reg &= ~ST25R300_OP_RX_EN;
return ST25R300_WriteRegister(dev, ST25R300_REG_OPERATION, op_reg);
}
/**
* @brief Lê os valores de RSSI
* @PAram dev Ponteiro para estrutura do dispositivo
* @PAram rssi Ponteiro para estrutura de dados RSSI
* @return true se sucesso, false se erro
*/
bool ST25R300_ReadRSSI(ST25R300 *dev, ST25R300_RSSI *rssi) {
if (!dev || !rssi) return false;
ST25R300_SendCommand(dev, 0xF9);
HAL_Delay(1);
uint8_t rssi_i_raw, rssi_q_raw;
// Lê RSSI do canal I (registro 0x4A, bits 6:0)
if (!ST25R300_ReadRegister(dev, ST25R300_REG_RSSI_DISPLAY_1, &rssi_i_raw)) {
return false;
}
// Lê RSSI do canal Q (registro 0x4B, bits 6:0)
if (!ST25R300_ReadRegister(dev, ST25R300_REG_RSSI_DISPLAY_2, &rssi_q_raw)) {
return false;
}
// Extrai os 7 bits de dados (bit 7 é RFU)
rssi->rssi_i = rssi_i_raw & 0x7F;
rssi->rssi_q = rssi_q_raw & 0x7F;
// Calcula RSSI total (magnitude vetorial)
float i_float = (float)rssi->rssi_i;
float q_float = (float)rssi->rssi_q;
rssi->rssi_total = (uint8_t)sqrtf(i_float * i_float + q_float * q_float);
// Conversão aproximada para dBm
// Fórmula empírica: RSSI_dBm ≈ -90 + (RSSI_total * 0.5)
// Ajuste conforme calibração real
rssi->rssi_dbm = -90.0f + (rssi->rssi_total * 0.5f);
return true;
}Abnormal VDD_A levels: Is it physically possible for the ST25R300's internal VDD_A regulator to report 5.66V when the main V_DD supply is only 3.25V? Could this be a symptom of RF energy reflecting back into the RFI pins due to antenna mismatch, or does it point to a specific fault in the internal LDO/ADC reference?
ADC RSSI saturation: My diagnostics show that the channel I ADC has reached its maximum of 255, but the RSSI remains at 0. This saturation usually indicates that the receiver stage is being “blinded” by self-interference. Is the high Q factor of my 10-turn antenna Q = 66 the likely culprit for this behavior in a single-ended configuration?
Are all these problems reflected by my antenna being small but with many turns? Even when I test without it in the circuit?
Solved! Go to Solution.
2026-01-12 1:40 AM
Hi Matheus,
The ST25 embedded NFC library provides a collection of middleware that runs on the NUCLEO-L476RG board.
The X-CUBE-NFC12 package provides a polling example, which is identical to the polling example from the ST25 embedded NFC library. This example can be ported to any Nucleo board by using STM32CubeMX. For your needs, use the X-CUBE-NFC12 package and follow the X-CUBE-NFC12_GettingStarted guide in the documentation folder of the package.
Rgds
BT
2026-01-05 2:44 AM - edited 2026-01-07 1:18 AM
Hi Matheus,
It is recommended to use the RFAL middleware for application development. The RFAL provides support for various NFC protocols and includes the ST25R300 low-level driver for register access.
For information about the RSSI, see section 5.4.7 of the ST25R300 datasheet. In particular "The RSSI information can be read after a tag reply without any timing constraints."
The nominal voltage of VDD_A is 3.0 V. The conversion factor is 5.9. The typo in the Datasheet DS14655 - Rev 2 has been reported internally and will be fixed in the next revision.
Regarding the Design verification of the antenna, make sure to follow section 9 of AN6092 Antenna design for ST25R500 and ST25R300 devices.
Also I suggest to run you code on X-NUCLEO-NFC12A1.
Rgds
BT
2026-01-10 6:08 AM
Good morning, Brain! Thank you for your reply.
I think my best option right now is to use X-Nucleo instead of starting with my own design right away. That said, I bought an X-NUCLEO-NFC12A1 board to start testing.
I didn't know about RFAL, I would like to use the polling example to test my board. But I don't know how to adapt it to use the example. In my case, instead of NUCLEO-L476RG, I have a NUCLEO F103RB. I tried to run the example located in "...\STMicroelectronics\ST25NFC_Embedded_Lib_ST25R500_1.8. 0\Projects\STM32L476RG-Nucleo\Applications\X-NUCLEO-NFC12A1\polling\STM32CubeIDE."
However, I did not get the logs in the serial to know if it worked or not, but I think I did it wrong, since, as I mentioned, I tried to load the example made for the L476RG directly into my F103RB.
I would like to know how I can better use this middleware, first to make this polling example work on my Nucleo board. And later, how can I put this middleware on my custom board (I don't know how to create a project with it integrated).
2026-01-12 1:40 AM
Hi Matheus,
The ST25 embedded NFC library provides a collection of middleware that runs on the NUCLEO-L476RG board.
The X-CUBE-NFC12 package provides a polling example, which is identical to the polling example from the ST25 embedded NFC library. This example can be ported to any Nucleo board by using STM32CubeMX. For your needs, use the X-CUBE-NFC12 package and follow the X-CUBE-NFC12_GettingStarted guide in the documentation folder of the package.
Rgds
BT
2026-01-12 5:44 AM
Good morning, Brian! Thank you very much! I finally got it working here.
Now I have another question. Please correct me if I should open a new thread for this instead of posting it here.
My problem is increasing the EH voltage of the ST25. As I said earlier, the goal is to power a set of low-power chips with it. I measured it in an improvised way to get an idea and arrived at 10mV on the EH pin. I don't know if I did this measurement correctly.
What can I do to increase this to perhaps 1V, for example? Would it be possible to reach up to 3V in EH?
2026-01-12 5:49 AM
Hi Matheus,
please create a new post and mark this one as replied.
Thanks
Rgds
BT