From: bvernoux Date: Tue, 14 Jul 2020 21:02:35 +0000 (+0200) Subject: st25r39xx_spi: Add st25r39xx PD using spi X-Git-Url: https://sigrok.org/gitaction?a=commitdiff_plain;h=a43eba6af2ad18c7dc535f1c2b8764d13b5cd21a;p=libsigrokdecode.git st25r39xx_spi: Add st25r39xx PD using spi This NFC interface chip is used in the HydraNFC Shield v2. See also dumps used to validate it on https://github.com/sigrokproject/sigrok-dumps/pull/21 --- diff --git a/decoders/st25r39xx_spi/__init__.py b/decoders/st25r39xx_spi/__init__.py new file mode 100644 index 0000000..d02dd7d --- /dev/null +++ b/decoders/st25r39xx_spi/__init__.py @@ -0,0 +1,28 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2019-2020 Benjamin Vernoux +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, see . +## + +''' +This decoder stacks on top of the 'spi' PD and decodes st25r3916 High performance NFC universal device and EMVCo reader +(SPI mode) protocol. + +Details: +https://www.st.com/resource/en/datasheet/st25r3916.pdf +''' + +from .pd import Decoder diff --git a/decoders/st25r39xx_spi/lists.py b/decoders/st25r39xx_spi/lists.py new file mode 100644 index 0000000..1429765 --- /dev/null +++ b/decoders/st25r39xx_spi/lists.py @@ -0,0 +1,231 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2019-2020 Benjamin Vernoux +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, see . +## +## v0.1 - 17 September 2019 B.VERNOUX using ST25R3916 Datasheet DS12484 Rev 1 (January 2019) +## v0.2 - 28 April 2020 B.VERNOUX using ST25R3916 Datasheet DS12484 Rev 2 (December 2019) https://www.st.com/resource/en/datasheet/st25r3916.pdf +## v0.3 - 17 June 2020 B.VERNOUX using ST25R3916 Datasheet DS12484 Rev 3 (04 June 2020) https://www.st.com/resource/en/datasheet/st25r3916.pdf + +## ST25R3916 Datasheet DS12484 Rev 3 (04 June 2020) §4.4 Direct commands +dir_cmd = { +# addr: 'name' +# Set Default + 0xC0: 'SET_DEFAULT', + 0xC1: 'SET_DEFAULT', +# Stop All Activities + 0xC2: 'STOP', + 0xC3: 'STOP', +# Transmit With CRC + 0xC4: 'TXCRC', +# Transmit Without CRC + 0xC5: 'TXNOCRC', +# Transmit REQA + 0xC6: 'TXREQA', +# Transmit WUPA + 0xC7: 'TXWUPA', +# NFC Initial Field ON + 0xC8: 'NFCINITFON', +# NFC Response Field ON + 0xC9: 'NFCRESFON', +# Go to Sense (Idle) + 0xCD: 'GOIDLE', +# Go to Sleep (Halt) + 0xCE: 'GOHALT', +# Mask Receive Data / Stops receivers and RX decoders + 0xD0: 'STOPRX', +# Unmask Receive Data / Starts receivers and RX decoders + 0xD1: 'STARRX', +# Change AM Modulation state + 0xD2: 'SETAMSTATE', +# Measure Amplitude + 0xD3: 'MAMP', +# Reset RX Gain + 0xD5: 'RSTRXGAIN', +# Adjust Regulators + 0xD6: 'ADJREG', +# Calibrate Driver Timing + 0xD8: 'CALDRVTIM', +# Measure Phase + 0xD9: 'MPHASE', +# Clear RSSI + 0xDA: 'CLRRSSI', +# Clear FIFO + 0xDB: 'CLRFIFO', +# Enter Transparent Mode + 0xDC: 'TRMODE', +# Calibrate Capacitive Sensor + 0xDD: 'CALCAPA', +# Measure Capacitance + 0xDE: 'MCAPA', +# Measure Power Supply + 0xDF: 'MPOWER', +# Start General Purpose Timer + 0xE0: 'STARGPTIM', +# Start Wake-up Timer + 0xE1: 'STARWTIM', +# Start Mask-receive Timer + 0xE2: 'STARMSKTIM', +# Start No-response Timer + 0xE3: 'STARNRESPTIM', +# Start PPON2 Timer + 0xE4: 'STARPPON2TIM', +# Stop No-response Timer + 0xE8: 'STOPNRESTIM', +# RFU / Not Used + 0xFA: 'RFU', +# Register Space-B Access + 0xFB: 'REGSPACEB', +# Register Test access + 0xFC: 'TESTACCESS' +# Other codes => RFU / Not Used +} + +## ST25R3916 Datasheet DS12484 Rev 2 (December 2019) §4.5 Registers Table 17. List of registers - Space A +## ST25R3916 Datasheet DS12484 Rev 2 (December 2019) §4.3.3 Serial peripheral interface (SPI) Table 11. SPI operation modes +regsSpaceA = { +# addr: 'name' +# §4.5 Registers Table 17. List of registers - Space A +# IO configuration + 0x00: 'IOCFG1', + 0x01: 'IOCFG2', +# Operation control and mode definition + 0x02: 'OPCTRL', + 0x03: 'MODEDEF', + 0x04: 'BITRATE', +# Protocol configuration + 0x05: 'TYPEA', + 0x06: 'TYPEB', + 0x07: 'TYPEBF', + 0x08: 'NFCIP1', + 0x09: 'STREAM', + 0x0A: 'AUX', +# Receiver configuration + 0x0B: 'RXCFG1', + 0x0C: 'RXCFG2', + 0x0D: 'RXCFG3', + 0x0E: 'RXCFG4', +# Timer definition + 0x0F: 'MSKRXTIM', + 0x10: 'NRESPTIM1', + 0x11: 'NRESPTIM2', + 0x12: 'TIMEMV', + 0x13: 'GPTIM1', + 0x14: 'GPTIM2', + 0x15: 'PPON2', +# Interrupt and associated reporting + 0x16: 'MSKMAINIRQ', + 0x17: 'MSKTIMNFCIRQ', + 0x18: 'MSKERRWAKEIRQ', + 0x19: 'TARGIRQ', + 0x1A: 'MAINIRQ', + 0x1B: 'TIMNFCIRQ', + 0x1C: 'ERRWAKEIRQ', + 0x1D: 'TARGIRQ', + 0x1E: 'FIFOSTAT1', + 0x1F: 'FIFOSTAT2', + 0x20: 'COLLDISP', + 0x21: 'TARGDISP', +# Definition of number of transmitted bytes + 0x22: 'NBTXB1', + 0x23: 'NBTXB2', + 0x24: 'BITRATEDET', +# A/D converter output + 0x25: 'ADCONVOUT', +# Antenna calibration + 0x26: 'ANTTUNECTRL1', + 0x27: 'ANTTUNECTRL2', +# Antenna driver and modulation + 0x28: 'TXDRV', + 0x29: 'TARGMOD', +# External field detector threshold + 0x2A: 'EXTFIELDON', + 0x2B: 'EXTFIELDOFF', +# Regulator + 0x2C: 'REGVDDCTRL', +# Receiver state display + 0x2D: 'RSSIDISP', + 0x2E: 'GAINSTATE', +# Capacitive sensor + 0x2F: 'CAPACTRL', + 0x30: 'CAPADISP', +# Auxiliary display + 0x31: 'AUXDISP', +# Wake-up + 0x32: 'WAKETIMCTRL', + 0x33: 'AMPCFG', + 0x34: 'AMPREF', + 0x35: 'AMPAAVGDISP', + 0x36: 'AMPDISP', + 0x37: 'PHASECFG', + 0x38: 'PHASEREF', + 0x39: 'PHASEAAVGDISP', + 0x3A: 'PHASEDISP', + 0x3B: 'CAPACFG', + 0x3C: 'CAPAREF', + 0x3D: 'CAPAAAVGDISP', + 0x3E: 'CAPADISP', +# IC identity + 0x3F: 'ICIDENT', +## ST25R3916 Datasheet DS12484 Rev 2 (December 2019) §4.3.3 Serial peripheral interface (SPI) Table 11. SPI operation modes + 0xA0: 'PT_memLoadA', + 0xA8: 'PT_memLoadF', + 0xAC: 'PT_memLoadTSN', + 0xBF: 'PT_memRead' +} + +## ST25R3916 Datasheet DS12484 Rev 2 (December 2019) §4.5 Registers Table 18. List of registers - Space B +regsSpaceB = { +# addr: 'name' +# §4.5 Registers Table 18. List of registers - Space B +# Protocol configuration + 0x05: 'EMDSUPPRCONF', + 0x06: 'SUBCSTARTIM', +# Receiver configuration + 0x0B: 'P2PRXCONF', + 0x0C: 'CORRCONF1', + 0x0D: 'CORRCONF2', +# Timer definition + 0x0F: 'SQUELSHTIM', + 0x15: 'NFCGUARDTIM', +# Antenna driver and modulation + 0x28: 'AUXMODSET', + 0x29: 'TXDRVTIM', +# External field detector threshold + 0x2A: 'RESAMMODE', + 0x2B: 'TXDRVTIMDISP', +# Regulator + 0x2C: 'REGDISP', +# Protection + 0x30: 'OSHOOTCONF1', + 0x31: 'OSHOOTCONF2', + 0x32: 'USHOOTCONF1', + 0x33: 'USHOOTCONF2' +} + +## ST25R3916 Datasheet DS12484 Rev 2 (December 2019) §4.4.17 Test access +regsTest = { +# addr: 'name' +# §4.4.17 Test access (Typo in datasheet it is not register 0x00 but 0x01) + 0x01: 'ANTSTOBS' +} + +## Optional TODO add important status bit fields / ANN_STATUS +## Interrupt and associated reporting => Registers Space A from Address (hex) 0x16 to 0x21 +## §4.5.58 RSSI display register +## §4.5.59 Gain reduction state register +## ... + diff --git a/decoders/st25r39xx_spi/pd.py b/decoders/st25r39xx_spi/pd.py new file mode 100644 index 0000000..714fdbc --- /dev/null +++ b/decoders/st25r39xx_spi/pd.py @@ -0,0 +1,353 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2019-2020 Benjamin Vernoux +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, see . +## +## v0.1 - 17 September 2019 B.VERNOUX using ST25R3916 Datasheet DS12484 Rev 1 (January 2019) +## v0.2 - 28 April 2020 B.VERNOUX using ST25R3916 Datasheet DS12484 Rev 2 (December 2019) https://www.st.com/resource/en/datasheet/st25r3916.pdf +## v0.3 - 17 June 2020 B.VERNOUX using ST25R3916 Datasheet DS12484 Rev 3 (04 June 2020) https://www.st.com/resource/en/datasheet/st25r3916.pdf + +import sigrokdecode as srd +from collections import namedtuple +from common.srdhelper import SrdIntEnum +from .lists import * + +Ann = SrdIntEnum.from_str('Ann', 'BURST_READ BURST_WRITE \ + BURST_READB BURST_WRITEB BURST_READT BURST_WRITET \ + DIRECTCMD FIFO_WRITE FIFO_READ STATUS WARN') + +Pos = namedtuple('Pos', ['ss', 'es']) +Data = namedtuple('Data', ['mosi', 'miso']) + +class Decoder(srd.Decoder): + api_version = 3 + id = 'st25r39xx_spi' + name = 'ST25R39xx' + longname = 'STMicroelectronics ST25R39xx' + desc = 'High performance NFC universal device and EMVCo reader.' + license = 'gplv2+' + inputs = ['spi'] + outputs = [] + tags = ['IC', 'Wireless/RF', 'NFC'] + annotations = ( + ('Read', 'Burst register read'), + ('Write', 'Burst register write'), + ('ReadB', 'Burst register SpaceB read'), + ('WriteB', 'Burst register SpaceB write'), + ('ReadT', 'Burst register Test read'), + ('WriteT', 'Burst register Test write'), + ('Cmd', 'Direct command'), + ('FIFOW', 'FIFO write'), + ('FIFOR', 'FIFO read'), + ('status_reg', 'Status register'), + ('warning', 'Warning'), + ) + annotation_rows = ( + ('reg', 'Regs', (Ann.prefixes('BURST_'))), + ('cmds', 'Commands', (Ann.DIRECTCMD,)), + ('data', 'Data', (Ann.prefixes('FIFO_'))), + ('status', 'Status register', (Ann.STATUS,)), + ('warnings', 'Warnings', (Ann.WARN,)), + ) + + def __init__(self): + self.reset() + + def reset(self): + self.next() + self.requirements_met = True + self.cs_was_released = False + + def start(self): + self.out_ann = self.register(srd.OUTPUT_ANN) + + def warn(self, pos, msg): + '''Put a warning message 'msg' at 'pos'.''' + self.put(pos.ss, pos.es, self.out_ann, [Ann.WARN, [msg]]) + + def putp(self, pos, ann, msg): + '''Put an annotation message 'msg' at 'pos'.''' + self.put(pos.ss, pos.es, self.out_ann, [ann, [msg]]) + + def putp2(self, pos, ann, msg1, msg2): + '''Put an annotation message 'msg' at 'pos'.''' + self.put(pos.ss, pos.es, self.out_ann, [ann, [msg1, msg2]]) + + def next(self): + '''Resets the decoder after a complete command was decoded.''' + # 'True' for the first byte after CS# went low. + self.first = True + + # The current command, and the minimum and maximum number + # of data bytes to follow. + self.cmd = None + self.min = 0 + self.max = 0 + + # Used to collect the bytes after the command byte + # (and the start/end sample number). + self.mb = [] + self.ss_mb = -1 + self.es_mb = -1 + + def mosi_bytes(self): + '''Returns the collected MOSI bytes of a multi byte command.''' + return [b.mosi for b in self.mb] + + def miso_bytes(self): + '''Returns the collected MISO bytes of a multi byte command.''' + return [b.miso for b in self.mb] + + def decode_command(self, pos, b): + '''Decodes the command byte 'b' at position 'pos' and prepares + the decoding of the following data bytes.''' + c = self.parse_command(b) + if c is None: + self.warn(pos, 'unknown command') + return + + self.cmd, self.dat, self.min, self.max = c + + if self.cmd == 'Cmd': + self.putp(pos, Ann.DIRECTCMD, self.format_command()) + else: + # Don't output anything now, the command is merged with + # the data bytes following it. + self.ss_mb = pos.ss + + def format_command(self): + '''Returns the label for the current command.''' + if self.cmd in ('Write', 'Read', 'WriteB', 'ReadB', 'WriteT', 'ReadT', 'FIFO Write', 'FIFO Read'): + return self.cmd + if self.cmd == 'Cmd': + reg = dir_cmd.get(self.dat, 'unknown direct command') + return '{} {}'.format(self.cmd, reg) + else: + return 'TODO Cmd {}'.format(self.cmd) + + def parse_command(self, b): + '''Parses the command byte. + Returns a tuple consisting of: + - the name of the command + - additional data needed to dissect the following bytes + - minimum number of following bytes + - maximum number of following bytes (None for infinite) + ''' + addr = b & 0x3F + # previous command was 'Space B' + if self.cmd == 'Space B': + if (b & 0xC0) == 0x00: + return ('WriteB', addr, 1, 99999) + if (b & 0xC0) == 0x40: + return ('ReadB', addr, 1, 99999) + else: + self.warn(pos, 'unknown address/command combination') + # previous command was 'TestAccess' + elif self.cmd == 'TestAccess': + if (b & 0xC0) == 0x00: + return ('WriteT', addr, 1, 99999) + if (b & 0xC0) == 0x40: + return ('ReadT', addr, 1, 99999) + else: + self.warn(pos, 'unknown address/command combination') + else: + # Space A regs or other operation modes (except Space B) + # Register Write 0b00xxxxxx 0x00 to 0x3F => 'Write' + # Register Read 0b01xxxxxx 0x40 to 0x7F => 'Read' + if (b <= 0x7F): + if (b & 0xC0) == 0x00: + return ('Write', addr, 1, 99999) + if (b & 0xC0) == 0x40: + return ('Read', addr, 1, 99999) + else: + self.warn(pos, 'unknown address/command combination') + else: + # FIFO Load 0b10000000 0x80 => 'FIFO Write' + # PT_memory loadA-config 0b10100000 0xA0 => 'Write' + # PT_memory loadF-config 0b10101000 0xA8 => 'Write' + # PT_memory loadTSN data 0b10101100 0xAC => 'Write' + # PT_memory Read 0b10111111 0xBF => 'Read' + # FIFO Read 0b10011111 0x9F => 'FIFO Read' + # Direct Command 0b11xxx1xx 0xC0 to 0xE8 => 'Cmd' + # Register Space-B Access 0b11111011 0xFB => 'Space B' + # Register Test Access 0b11111100 0xFC => 'TestAccess' + if b == 0x80: + return ('FIFO Write', b, 1, 99999) + if b == 0xA0: + return ('Write', b, 1, 99999) + if b == 0xA8: + return ('Write', b, 1, 99999) + if b == 0xAC: + return ('Write', b, 1, 99999) + if b == 0xBF: + return ('Read', b, 1, 99999) + if b == 0x9F: + return ('FIFO Read', b, 1, 99999) + if (b >= 0x0C and b <= 0xE8) : + return ('Cmd', b, 0, 0) + if b == 0xFB: + return ('Space B', b, 0, 0) + if b == 0xFC: + return ('TestAccess', b, 0, 0) + else: + self.warn(pos, 'unknown address/command combination') + + def decode_reg(self, pos, ann, regid, data): + '''Decodes a register. + pos -- start and end sample numbers of the register + ann -- the annotation number that is used to output the register. + regid -- may be either an integer used as a key for the 'regs' + dictionary, or a string directly containing a register name.' + data -- the register content. + ''' + if type(regid) == int: + if (ann == Ann.FIFO_READ) or (ann == Ann.FIFO_WRITE): + name = '' + elif (ann == Ann.BURST_READB) or (ann == Ann.BURST_WRITEB): + # Get the name of the register. + if regid not in regsSpaceB: + self.warn(pos, 'unknown register SpaceB') + return + name = '{} ({:02X})'.format(regsSpaceB[regid], regid) + elif (ann == Ann.BURST_READT) or (ann == Ann.BURST_WRITET): + # Get the name of the register. + if regid not in regsTest: + self.warn(pos, 'unknown register Test') + return + name = '{} ({:02X})'.format(regsTest[regid], regid) + else: + # Get the name of the register. + if regid not in regsSpaceA: + self.warn(pos, 'unknown register SpaceA') + return + name = '{} ({:02X})'.format(regsSpaceA[regid], regid) + else: + name = regid + + if regid == 'STATUS' and ann == Ann.STATUS: + label = 'Status' + self.decode_status_reg(pos, ann, data, label) + else: + label = '{}: {}'.format(self.format_command(), name) + self.decode_mb_data(pos, ann, data, label) + + def decode_status_reg(self, pos, ann, data, label): + '''Decodes the data bytes 'data' of a status register at position + 'pos'. The decoded data is prefixed with 'label'.''' + + def decode_mb_data(self, pos, ann, data, label): + '''Decodes the data bytes 'data' of a multibyte command at position + 'pos'. The decoded data is prefixed with 'label'.''' + + def escape(b): + return '{:02X}'.format(b) + + data = ' '.join([escape(b) for b in data]) + if (ann == Ann.FIFO_WRITE) or (ann == Ann.FIFO_READ): + text = '{}{}'.format(label, data) + else: + text = '{} = {}'.format(label, data) + self.putp(pos, ann, text) + + def finish_command(self, pos): + '''Decodes the remaining data bytes at position 'pos'.''' + if self.cmd == 'Write': + self.decode_reg(pos, Ann.BURST_WRITE, self.dat, self.mosi_bytes()) + elif self.cmd == 'Read': + self.decode_reg(pos, Ann.BURST_READ, self.dat, self.miso_bytes()) + elif self.cmd == 'WriteB': + self.decode_reg(pos, Ann.BURST_WRITEB, self.dat, self.mosi_bytes()) + elif self.cmd == 'ReadB': + self.decode_reg(pos, Ann.BURST_READB, self.dat, self.miso_bytes()) + elif self.cmd == 'WriteT': + self.decode_reg(pos, Ann.BURST_WRITET, self.dat, self.mosi_bytes()) + elif self.cmd == 'ReadT': + self.decode_reg(pos, Ann.BURST_READT, self.dat, self.miso_bytes()) + elif self.cmd == 'FIFO Write': + self.decode_reg(pos, Ann.FIFO_WRITE, self.dat, self.mosi_bytes()) + elif self.cmd == 'FIFO Read': + self.decode_reg(pos, Ann.FIFO_READ, self.dat, self.miso_bytes()) + elif self.cmd == 'Cmd': + self.decode_reg(pos, Ann.DIRECTCMD, self.dat, self.mosi_bytes()) + else: + self.warn(pos, 'unhandled command') + + def decode(self, ss, es, data): + if not self.requirements_met: + return + + ptype, data1, data2 = data + + if ptype == 'CS-CHANGE': + if data1 is None: + if data2 is None: + self.requirements_met = False + raise ChannelError('CS# pin required.') + elif data2 == 1: + self.cs_was_released = True + + if data1 == 0 and data2 == 1: + # Rising edge, the complete command is transmitted, process + # the bytes that were sent after the command byte. + if self.cmd: + # Check if we got the minimum number of data bytes + # after the command byte. + if len(self.mb) < self.min: + self.warn((ss, ss), 'missing data bytes') + elif self.mb: + self.finish_command(Pos(self.ss_mb, self.es_mb)) + + self.next() + self.cs_was_released = True + + elif ptype == 'DATA' and self.cs_was_released: + mosi, miso = data1, data2 + pos = Pos(ss, es) + + if miso is None or mosi is None: + self.requirements_met = False + raise ChannelError('Both MISO and MOSI pins are required.') + + if self.first: + # Register Space-B Access 0b11111011 0xFB => 'Space B' + if mosi == 0xFB: + self.first = True + # First MOSI byte 'Space B' command. + self.decode_command(pos, mosi) + # First MISO byte is always the status register. + #self.decode_reg(pos, ANN_STATUS, 'STATUS', [miso]) + # Register TestAccess Access 0b11111100 0xFC => 'TestAccess' + elif mosi == 0xFC: + self.first = True + # First MOSI byte 'TestAccess' command. + self.decode_command(pos, mosi) + # First MISO byte is always the status register. + #self.decode_reg(pos, ANN_STATUS, 'STATUS', [miso]) + else: + self.first = False + # First MOSI byte is always the command. + self.decode_command(pos, mosi) + # First MISO byte is always the status register. + #self.decode_reg(pos, ANN_STATUS, 'STATUS', [miso]) + else: + if not self.cmd or len(self.mb) >= self.max: + self.warn(pos, 'excess byte') + else: + # Collect the bytes after the command byte. + if self.ss_mb == -1: + self.ss_mb = ss + self.es_mb = es + self.mb.append(Data(mosi, miso))