2026-01-15 7:53 AM - edited 2026-01-15 8:48 AM
Hello ST Community,
We are developing an iOS application using the ST25SDK (Swift) to interact with a device equipped with the ST25DV02K-W1 (Dual Interface EEPROM).
Our Goal: We need to configure the PWM parameters to control an Adjustable Output Current (AOC). Based on our system design (ported from a working Android implementation), we need to write 4 bytes to Block 62 (Address 0xF8 / 248bytes).
The Data Structure: The 4 bytes represent:
Bytes 0-1: Period (Little Endian)
Bytes 2-3: Pulse Width (Little Endian), where Bit 7 of MSB is set to 1 to indicate "NFC Update".
The Issue: The iOS app calculates the correct payload (e.g., [B1 3B 70 AF]) and executes writeSingleBlock without throwing any errors. The console logs confirm the command is sent. However, the value on the device does not change. We suspect the Microcontroller (MCU) on the other side is overwriting the value immediately because it cannot access the I2C bus while the RF field is still active.
What we tried:
We force presentPassword (default 0) before writing, even if not strictly required, to ensure access.
We implemented a "Fire & Forget" strategy: immediately after writeSingleBlock, we invalidate the NFC session (invalidate()) to kill the RF field and free the I2C bus for the MCU.
We added a small usleep (200ms) before closing the session to allow EEPROM write time.
Despite this, the update is inconsistent.
Here is our writing logic. We calculate the pulse width based on a "Real vs Nominal" conversion logic and write to Block 62.
// ============================================================
// 1️⃣ Main Function
// ============================================================
private func performCustomWrite() throws {
guard let tag = mTag as? ST25DVPwmTag else {
throw NSError(domain: "NFC", code: -1)
}
// If we have a saved password, present it
if let pwd = self.mST25PwmPwd {
do {
try (mTag as! ST25DVPwmTag).presentPassword(passwordNumber: UInt8(ST25DVPwmTag.ST25DVPWM_PWM_PASSWORD_ID), password: pwd)
} catch {
// Ignore error here to avoid blocking flow,
// as we might already be authenticated or on an unprotected tag
}
}
// --- 1. DATA PREPARATION ---
let period = (mPwm1Period != nil && mPwm1Period! > 0) ? mPwm1Period! : 4000
let safeInput = clamp(valueToWriteFromUI, min: minNominal, max: maxNominal)
let targetPulseFloat: Float
if thereAreRealValues && minReal > 0 && maxReal > 0 {
// "Real" Logic (BinRev >= 2)
let deltaReal = Float(maxReal - minReal)
let deltaNom = Float(maxNominal - minNominal)
let scale = deltaNom != 0 ? deltaReal / deltaNom : 0
let mapped = scale * Float(safeInput - minNominal) + Float(minReal)
targetPulseFloat = mapped * Float(period) / Float(maxNominal)
} else {
// "Legacy" Logic
targetPulseFloat = Float(safeInput) * (Float(period) / Float(maxNominal))
}
let safePulseInt = clamp(Int(targetPulseFloat), min: 0, max: period)
// --- 2. WRITING ---
try writeAOC(
tag: tag,
address: ADDR_AOC_PWM,
binRevision: binRevision,
decRis: safePulseInt,
period: period,
nfcBit: true
)
// --- 3. IMMEDIATE CLOSE (Hit & Run) ---
// Close immediately to free the I2C bus for the microcontroller.
// We assume "OK" with isError:false triggers invalidate() without alertMessage.
if let session = miOSReaderSession {
session.stopTagReaderSession("OK", isError: false)
}
// --- 4. UI UPDATE ---
DispatchQueue.main.async {
self.txtCurrentValue.text = "\(safeInput)"
self.sliderAOC.value = Float(safeInput)
self.currentAOCRaw = safePulseInt
self.decAOCFinal = Float(safeInput)
}
}
// ============================================================
// 2️⃣ writeAOC Function
// ============================================================
func writeAOC(
tag: ST25DVPwmTag,
address: Int,
binRevision: Int,
decRis: Int,
period: Int?,
nfcBit: Bool
) throws {
let periodVal = (period ?? 4000) & 0xFFFF
let periodLSB = UInt8(periodVal & 0xFF)
let periodMSB = UInt8((periodVal >> 8) & 0xFF)
let pulseVal = decRis & 0xFFFF
let pulseLSB = UInt8(pulseVal & 0xFF)
var pulseMSB = UInt8((pulseVal >> 8) & 0x7F)
if nfcBit { pulseMSB |= 0x80 }
let payload: [UInt8] = [periodLSB, periodMSB, pulseLSB, pulseMSB]
let iosArray = makeIOSByteArray(payload)
// Execute writing
_ = try tag.writeSingleBlock(
with: Int32(address),
with: iosArray
)
}
// ============================================================
// 3️⃣ Helper makeIOSByteArray
// ============================================================
func makeIOSByteArray(_ data: [UInt8]) -> IOSByteArray {
guard let arr = IOSByteArray.newArray(withLength: UInt(data.count)) else {
fatalError("Unable to create IOSByteArray")
}
for i in 0..<data.count {
arr.replaceByte(at: UInt(i), withByte: Int8(bitPattern: data[i]))
}
return arr
}Questions:
Is there a specific timing or command sequence required by the ST25DV02K-W1 to trigger the GPO interrupt correctly without blocking the I2C read from the MCU?
On iOS, does writeSingleBlock guarantee the EEPROM programming cycle is complete before returning, or should we poll for completion?
Are there known issues with NFCTagReaderSession keeping the field alive too long even after invalidate() is called?
Any help or insight on the RF/I2C arbitration for this specific chip would be greatly appreciated.
Thank you!