ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
st25r.cpp
Go to the documentation of this file.
1#include "st25r.h"
3#include "esphome/core/log.h"
4#include <algorithm>
5#include <iterator>
6
7namespace esphome::st25r {
8
9ESPHOME_LOG_TAG(TAG, "st25r");
10
11// ISO14443A SEL command for each cascade level
12static constexpr uint8_t SEL_CMDS[] = {0x93, 0x95, 0x97};
13
14// RFAL NFC-A 106 kbps register profile, as {register, value} pairs in write order
15static const uint8_t NFCA_PROFILE_RX[][2] PROGMEM = {
19};
20static const uint8_t NFCA_PROFILE_TX[][2] PROGMEM = {
23 {ST25R_REG_AUX_MOD, 0x10},
28 {ST25R_REG_PT_MOD, 0x51},
34};
35static const uint8_t NFCA_PROFILE_CORR[][2] PROGMEM = {
36 {ST25R_REG_RX_CONF4, 0x00},
39};
40
41void IRAM_ATTR ST25R::isr(ST25R *arg) {
42 arg->irq_fired_ = true;
44}
45
47 if (this->reset_pin_ != nullptr) {
48 this->reset_pin_->setup();
49 this->pulse_reset_();
50 }
51
52 if (this->irq_pin_ != nullptr) {
53 this->irq_pin_->setup();
55 }
56
57 if (!this->reset_chip_())
58 this->mark_failed(LOG_STR("Failed to reset chip"));
59}
60
62 if (this->is_failed() || this->state_ != STATE_IDLE)
63 return;
64
65 this->enable_loop();
66 // The field was off between scans, so every tag in range restarts in the IDLE state and
67 // answers the WUPA; a tag left on the reader is found again without needing a HLTA first.
68 this->write_register(ST25R_REG_OP_CONTROL, 0xC8); // en=1, rx_en=1, tx_en=1
69 this->tag_found_this_scan_ = false;
70 this->state_ = STATE_FIELD_ON;
72}
73
75 if (this->state_ == STATE_IDLE) {
76 this->disable_loop();
77 return;
78 }
79 // With an IRQ pin the status is only fetched after the chip raised the line; the
80 // state timeouts still run on every pass
81 if (this->irq_pin_ == nullptr || this->irq_fired_) {
82 this->irq_fired_ = false;
84 // The line only drops once every pending source is read, or the next edge is missed
87 } else {
88 this->irq_status_ = 0;
89 }
90 this->process_state_();
91}
92
94 this->reset_pin_->digital_write(true);
95 delay(10);
96 this->reset_pin_->digital_write(false);
97 delay(10);
98}
99
106
107void ST25R::write_register_table_(const uint8_t (*table)[2], size_t count) {
108 for (size_t i = 0; i < count; i++)
109 this->write_register(progmem_read_byte(&table[i][0]), progmem_read_byte(&table[i][1]));
110}
111
112bool ST25R::transceive_ex_(const uint8_t *data, size_t len, uint8_t *resp, uint8_t resp_cap, uint8_t &resp_len,
113 uint32_t timeout_ms) {
114 this->clear_irqs_();
116
117 // The byte count spans both registers: ntx[12:5] in the first, ntx[4:0] plus the bit count in the second
118 this->write_register(ST25R_REG_NUM_TX_BYTES1, (len >> 5) & 0xFF);
119 this->write_register(ST25R_REG_NUM_TX_BYTES2, (len & 0x1F) << 3);
120
121 this->write_fifo(data, len);
123
124 uint32_t start = millis();
125 resp_len = 0;
126 bool tx_done = false;
127
128 while (millis() - start < timeout_ms) {
129 uint8_t irq = this->read_register(ST25R_REG_IRQ_MAIN);
130 this->irq_status_ = irq;
131
132 if (irq & IRQ_TXE)
133 tx_done = true;
134
135 if (tx_done) {
136 uint8_t f1 = this->read_register(ST25R_REG_FIFO_STATUS1);
137 if (f1 > 0) {
138 uint8_t to_read = std::min<uint8_t>(resp_cap - resp_len, f1);
139 this->read_fifo(resp + resp_len, to_read);
140 resp_len += to_read;
141 if (resp_len >= resp_cap)
142 return true;
143 start = millis();
144 }
145 if (irq & IRQ_RXE)
146 return resp_len > 0;
147 }
148 delay(1);
149 }
150 return resp_len > 0;
151}
152
155 switch (this->state_) {
156 case STATE_IDLE:
157 break;
158
159 case STATE_FIELD_ON: {
160 // ISO 14443-3 guard time: a tag needs 5 ms of field before the first command
161 if (now - this->last_state_change_ < FIELD_GUARD_MS)
162 break;
163 this->clear_irqs_();
166 this->state_ = STATE_WUPA;
167 this->last_state_change_ = now;
168 break;
169 }
170
171 case STATE_WUPA: {
172 if (this->irq_status_ & (IRQ_RXE | IRQ_COL)) {
173 this->cascade_level_ = 0;
174 this->scan_uid_.clear();
175 this->send_anticol_frame_();
176 this->state_ = STATE_ANTICOL;
177 this->last_state_change_ = now;
178 } else if (now - this->last_state_change_ > 100) {
179 this->finish_scan_();
180 }
181 break;
182 }
183
184 case STATE_ANTICOL: {
185 if (!(this->irq_status_ & (IRQ_RXE | IRQ_COL | IRQ_TXE))) {
186 if (now - this->last_state_change_ > 20)
187 this->finish_scan_();
188 break;
189 }
190
191 uint8_t f1 = this->read_register(ST25R_REG_FIFO_STATUS1);
192
193 if (this->irq_status_ & IRQ_COL) {
194 if (f1 > 0) {
195 uint8_t tmp[8];
196 this->read_fifo(tmp, std::min<uint8_t>(f1, sizeof(tmp)));
197 }
198 this->finish_scan_();
199 break;
200 }
201
202 if (f1 < 5) {
203 if (now - this->last_state_change_ > 20)
204 this->finish_scan_();
205 break;
206 }
207
208 uint8_t resp[5];
209 this->read_fifo(resp, 5);
210 uint8_t bcc = xor_bytes(resp, 4);
211 if (bcc != resp[4]) {
212 ESP_LOGV(TAG, "Anticollision BCC mismatch");
213 this->finish_scan_();
214 break;
215 }
216 uint8_t sel_pk[7] = {SEL_CMDS[this->cascade_level_], 0x70, resp[0], resp[1], resp[2], resp[3], bcc};
217
218 // Skip the cascade tag (0x88)
219 const uint8_t *uid_part = resp[0] == 0x88 ? resp + 1 : resp;
220 const uint8_t *uid_end = resp + 4;
221 for (; uid_part < uid_end && this->scan_uid_.size() < nfc::NFC_UID_MAX_LENGTH; uid_part++)
222 this->scan_uid_.push_back(*uid_part);
223
224 this->write_register(ST25R_REG_ISO14443A_CONF, 0x00); // antcl=0
225
226 uint8_t sak_buf[3];
227 uint8_t sak_len = 0;
228 if (!this->transceive_ex_(sel_pk, sizeof(sel_pk), sak_buf, sizeof(sak_buf), sak_len, 150)) {
229 ESP_LOGW(TAG, "SELECT failed (no SAK)");
230 this->finish_scan_();
231 break;
232 }
233 uint8_t sak = sak_buf[0];
234
235 if (sak & 0x04) { // Cascade bit
236 this->cascade_level_++;
237 if (this->cascade_level_ >= std::size(SEL_CMDS)) {
238 ESP_LOGE(TAG, "Too many cascade levels");
239 this->finish_scan_();
240 break;
241 }
242 this->send_anticol_frame_();
243 this->state_ = STATE_ANTICOL;
244 this->last_state_change_ = now;
245 break;
246 }
247
248 size_t uid_len = this->scan_uid_.size();
249 if (uid_len != 4 && uid_len != 7 && uid_len != 10) {
250 ESP_LOGW(TAG, "Discarding invalid UID len=%u", static_cast<unsigned>(uid_len));
251 this->finish_scan_();
252 break;
253 }
254
255#ifdef ESPHOME_LOG_HAS_VERBOSE
256 char uid_buf[nfc::FORMAT_UID_BUFFER_SIZE];
257 ESP_LOGV(TAG, "Tag selected: %s (SAK=0x%02X)", nfc::format_uid_to(uid_buf, this->scan_uid_), sak);
258#endif
259 this->tag_found_this_scan_ = true;
260 this->finish_scan_();
261 break;
262 }
263 }
264}
265
267 this->write_register(ST25R_REG_OP_CONTROL, 0x80); // en=1: field off until the next scan
268 this->state_ = STATE_IDLE;
269 this->irq_status_ = 0;
270 this->finalize_scan_();
271 this->disable_loop();
272}
273
275 if (this->current_tag_ != nullptr) {
276 const auto &uid = this->current_tag_->get_uid();
277 if (this->tag_found_this_scan_ && uid.size() == this->scan_uid_.size() &&
278 std::equal(uid.begin(), uid.end(), this->scan_uid_.begin())) {
279 this->miss_count_ = 0;
280 return;
281 }
282 if (!this->tag_found_this_scan_ && ++this->miss_count_ < MISS_THRESHOLD)
283 return;
284 this->remove_tag_();
285 }
286
287 if (!this->tag_found_this_scan_)
288 return;
289
290 this->current_tag_ = make_unique<nfc::NfcTag>(this->scan_uid_);
291 this->miss_count_ = 0;
292 char uid_buf[nfc::FORMAT_UID_BUFFER_SIZE];
293 nfc::format_uid_to(uid_buf, this->scan_uid_);
294 ESP_LOGI(TAG, "Tag detected: %s", uid_buf);
295 if (!this->on_tag_callback_.empty())
296 this->on_tag_callback_.call(std::string(uid_buf), *this->current_tag_);
297#ifdef NFC_TAG_LISTENER_COUNT
298 for (auto *listener : this->tag_listeners_)
299 listener->tag_on(*this->current_tag_);
300#endif
301}
302
304 char uid_buf[nfc::FORMAT_UID_BUFFER_SIZE];
305 nfc::format_uid_to(uid_buf, this->current_tag_->get_uid());
306 ESP_LOGI(TAG, "Tag removed: %s", uid_buf);
307 if (!this->on_tag_removed_callback_.empty())
308 this->on_tag_removed_callback_.call(std::string(uid_buf), *this->current_tag_);
309#ifdef NFC_TAG_LISTENER_COUNT
310 for (auto *listener : this->tag_listeners_)
311 listener->tag_off(*this->current_tag_);
312#endif
313 this->current_tag_.reset();
314}
315
317 uint8_t ic_identity = this->read_register(ST25R_REG_IC_IDENTITY);
318 uint8_t chip_type = ic_identity & 0xF8;
319 if (chip_type != 0x28 && chip_type != 0x30) {
320 ESP_LOGE(TAG, "IC identity mismatch: expected 0x28/0x30, got 0x%02X", chip_type);
321 return false;
322 }
323
325 delay(10);
326 const bool is_b_version = chip_type == 0x30;
327 ESP_LOGI(TAG, "IC: 0x%02X (ST25R3916%s)", ic_identity, is_b_version ? LOG_STR_LITERAL("B") : LOG_STR_LITERAL(""));
328
329 this->write_register(ST25R_REG_OP_CONTROL, 0x80); // Enable oscillator
330 delay(10);
331
332 // Measure VDD for supply voltage auto-detection
333 uint8_t reg_ctrl = this->read_register(ST25R_REG_REGULATOR_CONTROL);
334 this->write_register(ST25R_REG_REGULATOR_CONTROL, reg_ctrl & ~0x07);
336 delay(5);
337 uint8_t vdd_raw = this->read_register(ST25R_REG_AD_CONV_RESULT);
338 bool sup3v;
339 if (vdd_raw == 0) {
340 sup3v = true;
341 } else {
342 uint16_t vdd_mv = (uint16_t) vdd_raw * 23U + (((uint16_t) vdd_raw * 4U + 5U) / 10U);
343 sup3v = (vdd_mv < 3600);
344 ESP_LOGI(TAG, "VDD: %u mV (sup3V=%s)", vdd_mv, sup3v ? LOG_STR_LITERAL("3.3V") : LOG_STR_LITERAL("5V"));
345 }
346
348 // SPI pull-downs and the AAT module enabled
349 this->write_register(ST25R_REG_IO_CONF2, (sup3v ? 0x80 : 0x00) | 0x18 | 0x20);
350 this->write_register_table_(NFCA_PROFILE_RX, std::size(NFCA_PROFILE_RX));
351 this->write_register(ST25R_REG_TX_DRIVER_CONF, (15 - this->rf_power_) & 0x0F);
352 this->write_register_table_(NFCA_PROFILE_TX, std::size(NFCA_PROFILE_TX));
353 uint8_t aux_val = this->read_register(ST25R_REG_AUX);
354 this->write_register(ST25R_REG_AUX, aux_val & ~0x04);
355 this->write_register_table_(NFCA_PROFILE_CORR, std::size(NFCA_PROFILE_CORR));
356
357 this->field_on_();
358 delay(10);
359 return true;
360}
361
363 uint8_t frame[2] = {SEL_CMDS[this->cascade_level_], 0x20};
364
365 this->write_register(ST25R_REG_ISO14443A_CONF, 0x01); // antcl=1
366 this->clear_irqs_();
367 this->write_fifo(frame, sizeof(frame));
371}
372
374 this->write_register(ST25R_REG_OP_CONTROL, 0x88); // en=1, tx_en=1
375 delay(10);
377 delay(10);
378 this->write_register(ST25R_REG_OP_CONTROL, 0xC8); // en=1, rx_en=1, tx_en=1
380}
381
383 ESP_LOGCONFIG(TAG,
384 "ST25R:\n"
385 " RF Power: %u",
386 this->rf_power_);
387 LOG_PIN(" IRQ Pin: ", this->irq_pin_);
388 LOG_PIN(" Reset Pin: ", this->reset_pin_);
389 LOG_UPDATE_INTERVAL(this);
390}
391
392} // namespace esphome::st25r
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:274
void enable_loop_soon_any_context()
Thread and ISR-safe version of enable_loop() that can be called from any context.
void enable_loop()
Enable this component's loop.
Definition component.h:248
void disable_loop()
Disable this component's loop.
virtual void setup()=0
virtual void digital_write(bool value)=0
size_t size() const
Definition helpers.h:355
void push_back(const T &value)
Definition helpers.h:325
void attach_interrupt(void(*func)(T *), T *arg, gpio::InterruptType type) const
Definition gpio.h:29
StaticVector< NfcTagListener *, NFC_TAG_LISTENER_COUNT > tag_listeners_
Definition nfc.h:90
uint32_t last_state_change_
Definition st25r.h:125
void write_register_table_(const uint8_t(*table)[2], size_t count)
Definition st25r.cpp:107
virtual void read_fifo(uint8_t *data, size_t len)=0
bool tag_found_this_scan_
Definition st25r.h:131
static constexpr uint8_t IRQ_COL
Definition st25r.h:121
static constexpr uint8_t IRQ_RXE
Definition st25r.h:119
static constexpr uint8_t MISS_THRESHOLD
Definition st25r.h:115
static constexpr uint8_t IRQ_TXE
Definition st25r.h:120
virtual void write_command(uint8_t command)=0
uint8_t miss_count_
Definition st25r.h:128
LazyCallbackManager< void(const std::string &, const nfc::NfcTag &)> on_tag_callback_
Definition st25r.h:134
bool transceive_ex_(const uint8_t *data, size_t len, uint8_t *resp, uint8_t resp_cap, uint8_t &resp_len, uint32_t timeout_ms)
Definition st25r.cpp:112
uint8_t cascade_level_
Definition st25r.h:130
void update() override
Definition st25r.cpp:61
void loop() override
Definition st25r.cpp:74
void send_anticol_frame_()
Definition st25r.cpp:362
volatile bool irq_fired_
Definition st25r.h:132
virtual void write_register(uint8_t reg, uint8_t value)=0
static constexpr uint32_t FIELD_GUARD_MS
Definition st25r.h:116
virtual void write_fifo(const uint8_t *data, size_t len)=0
LazyCallbackManager< void(const std::string &, const nfc::NfcTag &)> on_tag_removed_callback_
Definition st25r.h:135
void dump_config() override
Definition st25r.cpp:382
virtual uint8_t read_register(uint8_t reg)=0
InternalGPIOPin * irq_pin_
Definition st25r.h:112
static void isr(ST25R *arg)
Definition st25r.cpp:41
GPIOPin * reset_pin_
Definition st25r.h:111
nfc::NfcTagUid scan_uid_
Definition st25r.h:124
std::unique_ptr< nfc::NfcTag > current_tag_
Definition st25r.h:123
uint8_t irq_status_
Definition st25r.h:129
void setup() override
Definition st25r.cpp:46
unsigned count
Definition gif.cpp:51
constexpr float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM
Definition bme680.cpp:27
@ INTERRUPT_RISING_EDGE
Definition gpio_pin.h:52
char * format_uid_to(char *buffer, std::span< const uint8_t > uid)
Format UID to buffer with '-' separator (e.g., "04-11-22-33"). Returns buffer for inline use.
Definition nfc.cpp:10
ESPHOME_LOG_TAG(TAG, "st25r")
@ ST25R_REG_OP_CONTROL
Definition st25r.h:17
@ ST25R_REG_FIFO_STATUS1
Definition st25r.h:32
@ ST25R_REG_BIT_RATE
Definition st25r.h:19
@ ST25R_REG_UNDERSHOOT_CONF2
Definition st25r.h:52
@ ST25R_REG_FIELD_THRESHOLD_DEACTV
Definition st25r.h:41
@ ST25R_REG_RX_CONF2
Definition st25r.h:24
@ ST25R_REG_IRQ_ERROR
Definition st25r.h:31
@ ST25R_REG_NUM_TX_BYTES2
Definition st25r.h:34
@ ST25R_REG_UNDERSHOOT_CONF1
Definition st25r.h:51
@ ST25R_REG_AUX_MOD
Definition st25r.h:47
@ ST25R_REG_RX_CONF1
Definition st25r.h:23
@ ST25R_REG_PASSIVE_TARGET
Definition st25r.h:21
@ ST25R_REG_MASK_MAIN
Definition st25r.h:27
@ ST25R_REG_OVERSHOOT_CONF1
Definition st25r.h:49
@ ST25R_REG_TX_DRIVER_CONF
Definition st25r.h:38
@ ST25R_REG_RX_CONF4
Definition st25r.h:26
@ ST25R_REG_OVERSHOOT_CONF2
Definition st25r.h:50
@ ST25R_REG_PT_MOD
Definition st25r.h:39
@ ST25R_REG_EMD_SUP_CONF
Definition st25r.h:44
@ ST25R_REG_IC_IDENTITY
Definition st25r.h:43
@ ST25R_REG_IRQ_TIMER
Definition st25r.h:30
@ ST25R_REG_REGULATOR_CONTROL
Definition st25r.h:42
@ ST25R_REG_NUM_TX_BYTES1
Definition st25r.h:33
@ ST25R_REG_AUX
Definition st25r.h:22
@ ST25R_REG_IO_CONF1
Definition st25r.h:15
@ ST25R_REG_IRQ_MAIN
Definition st25r.h:29
@ ST25R_REG_RES_AM_MOD
Definition st25r.h:48
@ ST25R_REG_AD_CONV_RESULT
Definition st25r.h:35
@ ST25R_REG_FIELD_THRESHOLD_ACTV
Definition st25r.h:40
@ ST25R_REG_CORR_CONF2
Definition st25r.h:46
@ ST25R_REG_ANT_TUNE_B
Definition st25r.h:37
@ ST25R_REG_ANT_TUNE_A
Definition st25r.h:36
@ ST25R_REG_RX_CONF3
Definition st25r.h:25
@ ST25R_REG_ISO14443A_CONF
Definition st25r.h:20
@ ST25R_REG_MODE
Definition st25r.h:18
@ ST25R_REG_MASK_TIMER
Definition st25r.h:28
@ ST25R_REG_CORR_CONF1
Definition st25r.h:45
@ ST25R_REG_IO_CONF2
Definition st25r.h:16
@ ST25R_CMD_FIELD_ON
Definition st25r.h:61
@ ST25R_CMD_TRANSMIT_WUPA
Definition st25r.h:60
@ ST25R_CMD_TRANSMIT_WITHOUT_CRC
Definition st25r.h:59
@ ST25R_CMD_TRANSMIT_WITH_CRC
Definition st25r.h:58
@ ST25R_CMD_SET_DEFAULT
Definition st25r.h:56
@ ST25R_CMD_CLEAR_FIFO
Definition st25r.h:57
@ ST25R_CMD_ADJUST_REGULATORS
Definition st25r.h:63
@ ST25R_CMD_MEASURE_VDD
Definition st25r.h:64
@ ST25R_CMD_RESET_RX_GAIN
Definition st25r.h:62
constexpr uint8_t xor_bytes(const uint8_t *data, size_t len, uint8_t init=0)
XOR the bytes of data with size len, starting from init.
Definition helpers.h:904
const void size_t len
Definition hal.h:68
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.
uint8_t progmem_read_byte(const uint8_t *addr)
Definition hal.h:51
static void uint32_t