From deb96755fa946c791f4660cc073de124f169143c Mon Sep 17 00:00:00 2001 From: Karl Palsson Date: Wed, 13 Aug 2025 14:27:40 +0000 Subject: [PATCH 01/11] spiflash: consistently sort flash chip choices Previously dropdown was random sorted each run. Signed-off-by: Karl Palsson --- decoders/spiflash/pd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/decoders/spiflash/pd.py b/decoders/spiflash/pd.py index c9c3edf4..a8b51ecf 100644 --- a/decoders/spiflash/pd.py +++ b/decoders/spiflash/pd.py @@ -90,8 +90,8 @@ class Decoder(srd.Decoder): ('warnings', 'Warnings', (L + 2,)), ) options = ( - {'id': 'chip', 'desc': 'Chip', 'default': tuple(chips.keys())[0], - 'values': tuple(chips.keys())}, + {'id': 'chip', 'desc': 'Chip', 'default': tuple(sorted(chips.keys()))[0], + 'values': tuple(sorted(chips.keys()))}, {'id': 'format', 'desc': 'Data format', 'default': 'hex', 'values': ('hex', 'ascii')}, ) From 19c013831e0a474df6fe41620b96f0e72a116c94 Mon Sep 17 00:00:00 2001 From: Karl Palsson Date: Wed, 13 Aug 2025 09:43:00 +0000 Subject: [PATCH 02/11] spiflash: RDID now decodes extension characters properly Old code assumed that RDID only ever returned 3 bytes. Depending on the vendor page assigned in JEDEC, you will actually get extension codes of 0x7f to reach the correct page. Tested with an Infineon FRAM part, "The JEDEC-assigned manufacturer ID places the Cypress (Ramtron) identifier in bank 7; therefore, there are six bytes of the continuation code 7Fh followed by the single byte C2h." Signed-off-by: Karl Palsson --- decoders/spiflash/lists.py | 5 +++++ decoders/spiflash/pd.py | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/decoders/spiflash/lists.py b/decoders/spiflash/lists.py index e31daf75..77b74167 100644 --- a/decoders/spiflash/lists.py +++ b/decoders/spiflash/lists.py @@ -118,6 +118,11 @@ 'sector_size': 4 * 1024, 'block_size': 64 * 1024, }, + 'infineon_fm25v02a': { + 'vendor': 'Infineon', + 'model': 'FM25V02A', + 'rdid_id': 0xc22208, + }, # Macronix 'macronix_mx25l1605d': { 'vendor': 'Macronix', diff --git a/decoders/spiflash/pd.py b/decoders/spiflash/pd.py index a8b51ecf..ac23fab5 100644 --- a/decoders/spiflash/pd.py +++ b/decoders/spiflash/pd.py @@ -184,6 +184,10 @@ def handle_rdid(self, mosi, miso): if self.cmdstate == 1: # Byte 1: Master sends command ID. self.emit_cmd_byte() + # Skip forward for continuation characters + elif self.cmdstate == 2 and miso == 0x7f: + self.putx([Ann.FIELD, ['Extension Byte: 0x7f']]) + return elif self.cmdstate == 2: # Byte 2: Slave sends the JEDEC manufacturer ID. self.putx([Ann.FIELD, ['Manufacturer ID: 0x%02x' % miso]]) From 618cbc72294d0816e7ee470e374c1dd51dc6ada2 Mon Sep 17 00:00:00 2001 From: Karl Palsson Date: Wed, 13 Aug 2025 11:52:39 +0000 Subject: [PATCH 03/11] spiflash: support 2/3/4 byte addressing Some (many?) larger flash chips support being switched into 4 byte mode, and then using the same "regular" 3 byte addressing commands. Some parts also use "regular" addressing commands with only 2 byte addressing. Sometimes this can be set by selecting a device, or interpreting the commands as they are seen, but you cannot guarantee that a trace will include mode switch commands, so allow it to be set up front when needed. Tested with a Micron MT25QL512 and an Infineon FM25V02A. (4 byte variable and 2 byte fixed) TODO: this doesn't change the formatting of addresses at this point. TODO: some of the two bit modes aren't handled either. Signed-off-by: Karl Palsson --- decoders/spiflash/pd.py | 75 ++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/decoders/spiflash/pd.py b/decoders/spiflash/pd.py index ac23fab5..2df21c66 100644 --- a/decoders/spiflash/pd.py +++ b/decoders/spiflash/pd.py @@ -94,6 +94,9 @@ class Decoder(srd.Decoder): 'values': tuple(sorted(chips.keys()))}, {'id': 'format', 'desc': 'Data format', 'default': 'hex', 'values': ('hex', 'ascii')}, + # Some/many flash parts can switch to different addressing modes + {'id': 'addr_size', 'desc': 'Count of address bytes (-1 for auto/detect)', 'default': -1, + 'values': (-1, 2, 3, 4)}, ) def __init__(self): @@ -126,6 +129,18 @@ def start(self): self.out_ann = self.register(srd.OUTPUT_ANN) self.chip = chips[self.options['chip']] self.vendor = self.options['chip'].split('_')[0] + # Does the chip have a forced, fixed size? + chip_addr_size = self.chip.get('addr_size') + opt_addr_size = self.options['addr_size'] + if opt_addr_size == -1 and chip_addr_size is None: + self.addr_size = 3 + elif chip_addr_size is not None: + self.addr_size = chip_addr_size + else: + self.addr_size = opt_addr_size + + self.addr_chunks = tuple((n for n in range(2, 2 + self.addr_size))) + self.data_offs = self.addr_chunks[-1] + 1 def putx(self, data): # Simplification, most annotations span exactly one SPI byte/packet. @@ -159,15 +174,16 @@ def emit_cmd_byte(self): self.addr = 0 def emit_addr_bytes(self, mosi): - self.addr |= (mosi << ((4 - self.cmdstate) * 8)) - b = ((3 - (self.cmdstate - 2)) * 8) - 1 + last_addr = self.addr_chunks[-1] + self.addr |= (mosi << ((last_addr - self.cmdstate) * 8)) + b = (((last_addr-1) - (self.cmdstate - 2)) * 8) - 1 self.putx([Ann.BIT, ['Address bits %d..%d: 0x%02x' % (b, b - 7, mosi), 'Addr bits %d..%d: 0x%02x' % (b, b - 7, mosi), 'Addr bits %d..%d' % (b, b - 7), 'A%d..A%d' % (b, b - 7)]]) if self.cmdstate == 2: self.ss_field = self.ss - if self.cmdstate == 4: + if self.cmdstate == last_addr: self.es_field = self.es self.putf([Ann.FIELD, ['Address: 0x%06x' % self.addr, 'Addr: 0x%06x' % self.addr, '0x%06x' % self.addr]]) @@ -266,17 +282,16 @@ def handle_wrsr(self, mosi, miso): def handle_read(self, mosi, miso): # Read data bytes: Master asserts CS#, sends READ command, sends - # 3-byte address, reads >= 1 data bytes, de-asserts CS#. + # address, reads >= 1 data bytes, de-asserts CS#. if self.cmdstate == 1: # Byte 1: Master sends command ID. self.emit_cmd_byte() - elif self.cmdstate in (2, 3, 4): - # Bytes 2/3/4: Master sends read address (24bits, MSB-first). + elif self.cmdstate in self.addr_chunks: self.emit_addr_bytes(mosi) - elif self.cmdstate >= 5: - # Bytes 5-x: Master reads data bytes (until CS# de-asserted). + elif self.cmdstate >= self.data_offs: + # Bytes ..-x: Master reads data bytes (until CS# de-asserted). self.es_field = self.es # Will be overwritten for each byte. - if self.cmdstate == 5: + if self.cmdstate == self.data_offs: self.ss_field = self.ss self.on_end_transaction = lambda: self.output_data_block('Data', Ann.READ) self.data.append(miso) @@ -284,19 +299,18 @@ def handle_read(self, mosi, miso): def handle_write_common(self, mosi, miso, ann): # Write data bytes: Master asserts CS#, sends WRITE command, sends - # 3-byte address, writes >= 1 data bytes, de-asserts CS#. + # address, writes >= 1 data bytes, de-asserts CS#. if self.cmdstate == 1: # Byte 1: Master sends command ID. self.emit_cmd_byte() if self.writestate == 0: self.putc([Ann.WARN, ['Warning: WREN might be missing']]) - elif self.cmdstate in (2, 3, 4): - # Bytes 2/3/4: Master sends write address (24bits, MSB-first). + elif self.cmdstate in self.addr_chunks: self.emit_addr_bytes(mosi) - elif self.cmdstate >= 5: - # Bytes 5-x: Master writes data bytes (until CS# de-asserted). + elif self.cmdstate >= self.data_offs: + # Bytes ..-x: Master writes data bytes (until CS# de-asserted). self.es_field = self.es # Will be overwritten for each byte. - if self.cmdstate == 5: + if self.cmdstate == self.data_offs: self.ss_field = self.ss self.on_end_transaction = lambda: self.output_data_block('Data', ann) self.data.append(mosi) @@ -310,19 +324,18 @@ def handle_write2(self, mosi, miso): def handle_fast_read(self, mosi, miso): # Fast read: Master asserts CS#, sends FAST READ command, sends - # 3-byte address + 1 dummy byte, reads >= 1 data bytes, de-asserts CS#. + # address + 1 dummy byte, reads >= 1 data bytes, de-asserts CS#. if self.cmdstate == 1: # Byte 1: Master sends command ID. self.emit_cmd_byte() - elif self.cmdstate in (2, 3, 4): - # Bytes 2/3/4: Master sends read address (24bits, MSB-first). + elif self.cmdstate in self.addr_chunks: self.emit_addr_bytes(mosi) - elif self.cmdstate == 5: + elif self.cmdstate == self.data_offs: self.putx([Ann.BIT, ['Dummy byte: 0x%02x' % mosi]]) - elif self.cmdstate >= 6: - # Bytes 6-x: Master reads data bytes (until CS# de-asserted). + elif self.cmdstate >= self.data_offs+1: + # Bytes ..-x: Master reads data bytes (until CS# de-asserted). self.es_field = self.es # Will be overwritten for each byte. - if self.cmdstate == 6: + if self.cmdstate == self.data_offs+1: self.ss_field = self.ss self.on_end_transaction = lambda: self.output_data_block('Data', Ann.FAST_READ) self.data.append(miso) @@ -333,6 +346,7 @@ def handle_2read(self, mosi, miso): # command, sends 3-byte address + 1 dummy byte, reads >= 1 data bytes, # de-asserts CS#. All data after the command is sent via two I/O pins. # MOSI = SIO0 = even bits, MISO = SIO1 = odd bits. + # TODO - no 2/3 byte address handling here :( if self.cmdstate != 1: b1, b2 = decode_dual_bytes(mosi, miso) if self.cmdstate == 1: @@ -380,11 +394,10 @@ def handle_se(self, mosi, miso): self.emit_cmd_byte() if self.writestate == 0: self.putx([Ann.WARN, ['Warning: WREN might be missing']]) - elif self.cmdstate in (2, 3, 4): - # Bytes 2/3/4: Master sends sector address (24bits, MSB-first). + elif self.cmdstate in self.addr_chunks: self.emit_addr_bytes(mosi) - if self.cmdstate == 4: + if self.cmdstate == self.addr_chunks[-1]: self.es_cmd = self.es d = 'Erase sector %d (0x%06x)' % (self.addr, self.addr) self.putc([Ann.SE, [d]]) @@ -410,18 +423,18 @@ def handle_ce2(self, mosi, miso): self.putx([Ann.WARN, ['Warning: WREN might be missing']]) def handle_pp(self, mosi, miso): - # Page program: Master asserts CS#, sends PP command, sends 3-byte + # Page program: Master asserts CS#, sends PP command, sends # page address, sends >= 1 data bytes, de-asserts CS#. if self.cmdstate == 1: # Byte 1: Master sends command ID. self.emit_cmd_byte() - elif self.cmdstate in (2, 3, 4): - # Bytes 2/3/4: Master sends page address (24bits, MSB-first). + elif self.cmdstate in self.addr_chunks: + # Master sends page address self.emit_addr_bytes(mosi) - elif self.cmdstate >= 5: - # Bytes 5-x: Master sends data bytes (until CS# de-asserted). + elif self.cmdstate >= self.data_offs: + # Bytes ..-x: Master sends data bytes (until CS# de-asserted). self.es_field = self.es # Will be overwritten for each byte. - if self.cmdstate == 5: + if self.cmdstate == self.data_offs: self.ss_field = self.ss self.on_end_transaction = lambda: self.output_data_block('Data', Ann.PP) self.data.append(mosi) From e88ec9fc8f9bdb09f28da2e6f462f65049644dd3 Mon Sep 17 00:00:00 2001 From: Karl Palsson Date: Wed, 13 Aug 2025 14:21:39 +0000 Subject: [PATCH 04/11] spiflash: mark infineon fram as fixed 2 byte addressing Signed-off-by: Karl Palsson --- decoders/spiflash/lists.py | 1 + 1 file changed, 1 insertion(+) diff --git a/decoders/spiflash/lists.py b/decoders/spiflash/lists.py index 77b74167..c1df64f5 100644 --- a/decoders/spiflash/lists.py +++ b/decoders/spiflash/lists.py @@ -122,6 +122,7 @@ 'vendor': 'Infineon', 'model': 'FM25V02A', 'rdid_id': 0xc22208, + 'addr_size': 2 }, # Macronix 'macronix_mx25l1605d': { From 722c3e1cf21e6ff7b73aff74dd7101dcb1cab624 Mon Sep 17 00:00:00 2001 From: Karl Palsson Date: Wed, 13 Aug 2025 14:20:58 +0000 Subject: [PATCH 05/11] spiflash: support vendor specific command decoding It's not really very pretty, but it's an attempt at least. Signed-off-by: Karl Palsson --- decoders/spiflash/lists.py | 24 +++++++++++++++- decoders/spiflash/pd.py | 58 ++++++++++++++++++++++++++++++++++---- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/decoders/spiflash/lists.py b/decoders/spiflash/lists.py index c1df64f5..08e4f42d 100644 --- a/decoders/spiflash/lists.py +++ b/decoders/spiflash/lists.py @@ -19,9 +19,14 @@ from collections import OrderedDict +# Annotations are created from cmds, and can't be changed after the fact, based on chip +# So we must prefill all plausible flash commands, then override. +cmds = OrderedDict([(n, ('CMD%02Xh' % n, 'Flash Command 0x%02X' % n)) for n in range(256)]) + # OrderedDict which maps command IDs to their names and descriptions. # Please keep this sorted by command ID. -cmds = OrderedDict([ +# FIXME: this contains quite a few vendor specific codepoints +base_cmds = OrderedDict([ (0x01, ('WRSR', 'Write status register')), (0x02, ('PP', 'Page program')), (0x03, ('READ', 'Read data')), @@ -51,6 +56,7 @@ (0xd8, ('BE', 'Block erase')), (0xef, ('REMS2', 'Read ID for 2x I/O mode')), ]) +cmds.update(base_cmds) device_name = { 'adesto': { @@ -70,6 +76,15 @@ }, } +# At least applies to MT25QL512 +cmds_micron = OrderedDict([ + (0x66, ('RESET_ENABLE', 'Reset Enable')), + (0x70, ('RFSR', 'Read Flag status register')), + (0x99, ('RESET_MEMORY', 'Reset Memory')), + (0xb7, ('ENTER4B', 'Enter 4-Byte addressing mode')), + (0xe9, ('EXIT4B', 'Exit 4-Byte addressing mode')), +]) + chips = { # Adesto 'adesto_at45db161e': { @@ -169,6 +184,13 @@ 'sector_size': 4 * 1024, 'block_size': 64 * 1024, }, + # Micron + 'micron_mt25ql512': { + 'vendor': 'Micron', + 'model': 'MT25QL512', + 'rdid_id': 0x20ba20, + 'extra_cmds': cmds_micron, + }, # Winbond 'winbond_w25q80dv': { 'vendor': 'Winbond', diff --git a/decoders/spiflash/pd.py b/decoders/spiflash/pd.py index 2df21c66..c523eaaf 100644 --- a/decoders/spiflash/pd.py +++ b/decoders/spiflash/pd.py @@ -68,6 +68,44 @@ def decode_status_reg(data): return ret + +class VendorDecoder: + def __init__(self, srd: srd.Decoder, vcmds): + """ + A vendor decoder doesn't _inherit_ the top level decoder, it just has access to it... + vcmds are a map of vendor commands... + """ + self.srd = srd + self.vcmds = vcmds + + def get_handler(cmd): + s = 'vhandle_%s' % vcmds[cmd][0].lower().replace('/', '_') + v_default = getattr(self, "default_vhandle", None) + if v_default is not None: + return getattr(self, s, v_default) + else: + return getattr(self, s) + self.cmd_handlers = dict((cmd, get_handler(cmd)) for cmd in vcmds.keys()) + +class VendorDecoderMicron(VendorDecoder): + def vhandle_rfsr(self, mosi, miso): + if self.srd.cmdstate == 1: + # Byte 1: Master sends command ID. + self.srd.putx([0x70, [self.vcmds[mosi][1], self.vcmds[mosi][0]]]) + elif self.srd.cmdstate >= 2: + # Bytes 2-x: Slave sends status register as long as master clocks. + self.srd.putx([Ann.BIT, ["undecoded FIXME"]]) + self.srd.putx([Ann.FIELD, ['Flag Status register']]) + self.srd.putx([0x70, ['Flag status value: 0x%02x' % miso, '0x%02x' % miso]]) + self.srd.cmdstate += 1 + + def default_vhandle(self, mosi, miso): + """ + We can use this for simple one commands were the info in the command table is sufficient + """ + self.srd.putx([mosi, [self.vcmds[mosi][1], self.vcmds[mosi][0]]]) + + class Decoder(srd.Decoder): api_version = 3 id = 'spiflash' @@ -107,13 +145,14 @@ def reset(self): self.on_end_transaction = None self.end_current_transaction() self.writestate = 0 + self.vhandler = None # Build dict mapping command keys to handler functions. Each # command in 'cmds' (defined in lists.py) has a matching # handler self.handle_. def get_handler(cmd): s = 'handle_%s' % cmds[cmd][0].lower().replace('/', '_') - return getattr(self, s) + return getattr(self, s, self.default_handle) self.cmd_handlers = dict((cmd, get_handler(cmd)) for cmd in cmds.keys()) def end_current_transaction(self): @@ -129,6 +168,11 @@ def start(self): self.out_ann = self.register(srd.OUTPUT_ANN) self.chip = chips[self.options['chip']] self.vendor = self.options['chip'].split('_')[0] + extra_cmds = self.chip.get('extra_cmds') + if extra_cmds: + # FIXME - no sure yet how best to have "lists" refer to classes without falling back to strings :| + self.vhandler = VendorDecoderMicron(self, extra_cmds) + # Does the chip have a forced, fixed size? chip_addr_size = self.chip.get('addr_size') opt_addr_size = self.options['addr_size'] @@ -518,6 +562,10 @@ def handle_esry(self, mosi, miso): def handle_dsry(self, mosi, miso): pass # TODO + def default_handle(self, mosi, miso): + self.putx([Ann.BIT, ['Unhandled CMD: 0x%02x' % mosi, 'CMD%02xh' % mosi]]) + self.state = None + def output_data_block(self, label, idx): # Print accumulated block of data # (called on CS# de-assert via self.on_end_transaction callback). @@ -547,8 +595,8 @@ def decode(self, ss, es, data): self.cmdstate = 1 # Handle commands. - try: + # Look for chip specific first... + if self.vhandler and self.vhandler.cmd_handlers.get(self.state): + self.vhandler.cmd_handlers[self.state](mosi, miso) + else: self.cmd_handlers[self.state](mosi, miso) - except KeyError: - self.putx([Ann.BIT, ['Unknown command: 0x%02x' % mosi]]) - self.state = None From bcffd304237a4d4ba701ffee5680cc6284a79f72 Mon Sep 17 00:00:00 2001 From: Karl Palsson Date: Thu, 14 Aug 2025 13:50:26 +0000 Subject: [PATCH 06/11] spiflash: PageProgram should use the common write handling Less duplication, less chances for copy pasta bugs. Signed-off-by: Karl Palsson --- decoders/spiflash/pd.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/decoders/spiflash/pd.py b/decoders/spiflash/pd.py index c523eaaf..0de77385 100644 --- a/decoders/spiflash/pd.py +++ b/decoders/spiflash/pd.py @@ -366,6 +366,9 @@ def handle_write1(self, mosi, miso): def handle_write2(self, mosi, miso): self.handle_write_common(mosi, miso, Ann.WRITE2) + def handle_pp(self, mosi, miso): + self.handle_write_common(mosi, miso, Ann.PP) + def handle_fast_read(self, mosi, miso): # Fast read: Master asserts CS#, sends FAST READ command, sends # address + 1 dummy byte, reads >= 1 data bytes, de-asserts CS#. @@ -466,24 +469,6 @@ def handle_ce2(self, mosi, miso): if self.writestate == 0: self.putx([Ann.WARN, ['Warning: WREN might be missing']]) - def handle_pp(self, mosi, miso): - # Page program: Master asserts CS#, sends PP command, sends - # page address, sends >= 1 data bytes, de-asserts CS#. - if self.cmdstate == 1: - # Byte 1: Master sends command ID. - self.emit_cmd_byte() - elif self.cmdstate in self.addr_chunks: - # Master sends page address - self.emit_addr_bytes(mosi) - elif self.cmdstate >= self.data_offs: - # Bytes ..-x: Master sends data bytes (until CS# de-asserted). - self.es_field = self.es # Will be overwritten for each byte. - if self.cmdstate == self.data_offs: - self.ss_field = self.ss - self.on_end_transaction = lambda: self.output_data_block('Data', Ann.PP) - self.data.append(mosi) - self.cmdstate += 1 - def handle_cp(self, mosi, miso): pass # TODO From 029a36654c7669ec932356da90991907d5fd2997 Mon Sep 17 00:00:00 2001 From: Karl Palsson Date: Thu, 14 Aug 2025 13:51:26 +0000 Subject: [PATCH 07/11] spiflash: correctly output WREN missing warnings Original code would try and output an annotation with es well in the past, resulting in a sliver that couldn't be zoomed in on. Instead, correctly update the end of the command as is done with other commands, and wait to output the warning until we've finished with the command entirely. Signed-off-by: Karl Palsson --- decoders/spiflash/pd.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/decoders/spiflash/pd.py b/decoders/spiflash/pd.py index 0de77385..3e348b1c 100644 --- a/decoders/spiflash/pd.py +++ b/decoders/spiflash/pd.py @@ -347,16 +347,17 @@ def handle_write_common(self, mosi, miso, ann): if self.cmdstate == 1: # Byte 1: Master sends command ID. self.emit_cmd_byte() - if self.writestate == 0: - self.putc([Ann.WARN, ['Warning: WREN might be missing']]) elif self.cmdstate in self.addr_chunks: self.emit_addr_bytes(mosi) elif self.cmdstate >= self.data_offs: # Bytes ..-x: Master writes data bytes (until CS# de-asserted). self.es_field = self.es # Will be overwritten for each byte. if self.cmdstate == self.data_offs: + self.es_cmd = self.ss self.ss_field = self.ss self.on_end_transaction = lambda: self.output_data_block('Data', ann) + if self.writestate == 0: + self.putc([Ann.WARN, ['Warning: WREN might be missing']]) self.data.append(mosi) self.cmdstate += 1 From d67a0e31e981ea4af64d41be40cdebfae68ee37b Mon Sep 17 00:00:00 2001 From: Karl Palsson Date: Fri, 15 Aug 2025 10:43:09 +0000 Subject: [PATCH 08/11] spiflash: check RDID against provided This overhauls the data tables to make them a little bit easier to work with in the future, by using classes, instead of big string dicts. The old code was a little mixed between showing the RDID results, or showing what had been selected, so make that clear, and use warnings if things aren't as we expect. We also warn if the selected part doesn't provide IDs, but we saw one anyway. Decoding _may_ be just fine, but it may be wrong. Checked also with sigrok-dumps for parts without rdid. For "manufacturer" id, we've gone with keeping tables in the style of memtest86, using an upper byte for the page number, rather than trying to keep literals like "0x7f7f7f7f7f7fc2" or similar ala flashrom. TODO: this does _not_ overhaul the related rems/res/rdp style ID decoding, but that really needs similar treatment. Signed-off-by: Karl Palsson --- decoders/spiflash/lists.py | 65 +++++++++++++++++++++++++++----------- decoders/spiflash/pd.py | 42 ++++++++++++++++++------ 2 files changed, 79 insertions(+), 28 deletions(-) diff --git a/decoders/spiflash/lists.py b/decoders/spiflash/lists.py index 08e4f42d..1b7efb20 100644 --- a/decoders/spiflash/lists.py +++ b/decoders/spiflash/lists.py @@ -58,24 +58,6 @@ ]) cmds.update(base_cmds) -device_name = { - 'adesto': { - 0x00: 'AT45Dxxx family, standard series', - }, - 'fidelix': { - 0x15: 'FM25Q32', - }, - 'macronix': { - 0x13: 'MX25L8006', - 0x14: 'MX25L1605D', - 0x15: 'MX25L3205D', - 0x16: 'MX25L6405D', - }, - 'winbond': { - 0x13: 'W25Q80DV', - }, -} - # At least applies to MT25QL512 cmds_micron = OrderedDict([ (0x66, ('RESET_ENABLE', 'Reset Enable')), @@ -85,7 +67,52 @@ (0xe9, ('EXIT4B', 'Exit 4-Byte addressing mode')), ]) -chips = { +class FlashChip: + def __init__(self, vendor, model, jedec_manu=None, deviceid=None, write_enable=None, **kwargs): + self.vendor = vendor + self.model = model + self.jedec_manu = jedec_manu + self.deviceid = deviceid + self.write_enable = write_enable + # known: addr_size and extra_cmds ... + self.opts = kwargs + + def key(self): + return "%s_%s" % (self.vendor, self.model) + + def idstr(self): + if self.has_ids(): + return "%04x:%04x" % (self.jedec_manu, self.deviceid) + return "" + + def has_ids(self): + return self.jedec_manu is not None and self.deviceid is not None + + def matches(self, manu, devid): + return self.has_ids() and self.jedec_manu == manu and self.deviceid == devid + + def __repr__(self): + return "FlashChip<%s_%s>" % (self.vendor, self.model) + +# page/sector/block sizes have never been used in this PD, but keep them for now if people provided them... +# remsids are relegated to kwargs, largely superseded by jedec rdid... +# Uses a "page << 8 | manuf_id" ala memtest86, rather than expanded 7f7f7f style notation ala flashrom +# however, put in whatever the device _does_ not what it "should" be doing. +chips_list = [ + FlashChip("Adesto", "AT45DB161E", jedec_manu=0x001f, deviceid=0x2600, write_enable=False, sz_p=528, sz_s=128*1024, sz_b=4*1024), + FlashChip("Atmel", "AT25xx", sz_p=64), + FlashChip("Fidelix", "FM25Q32", jedec_manu=0x07a1, deviceid=0x4016, sz_p=256, sz_s=4*1024, sz_b=64*1024, rems_id=0x15), + FlashChip("Infineon", "FM25V02A", jedec_manu=0x06c2, deviceid=0x2208, addr_size=2), + FlashChip("Macronix", "MX25L8006", jedec_manu=0x00c2, deviceid=0x2014, sz_p=256, sz_s=4*1024, sz_b=64*1024, rems_id=0x13), + FlashChip("Macronix", "MX25L1605D", jedec_manu=0x00c2, deviceid=0x2015, sz_p=256, sz_s=4*1024, sz_b=64*1024, rems_id=0x14), + FlashChip("Macronix", "MX25L3205D", jedec_manu=0x00c2, deviceid=0x2016, sz_p=256, sz_s=4*1024, sz_b=64*1024, rems_id=0x15), + FlashChip("Macronix", "MX25L6405D", jedec_manu=0x00c2, deviceid=0x2017, sz_p=256, sz_s=4*1024, sz_b=64*1024, rems_id=0x16), + FlashChip("Micron", "MT25QL512", jedec_manu=0x0020, deviceid=0xba20, extra_cmds=cmds_micron), + FlashChip("Winbond", "W25Q80DV", jedec_manu=0x00ef, deviceid=0x4014, sz_p=256, sz_s=4*1024, sz_b=64*1024, rems_id=0x13), +] +chips = {chip.key(): chip for chip in chips_list} + +chips_legacy = { # Adesto 'adesto_at45db161e': { 'vendor': 'Adesto', diff --git a/decoders/spiflash/pd.py b/decoders/spiflash/pd.py index 3e348b1c..29aca54a 100644 --- a/decoders/spiflash/pd.py +++ b/decoders/spiflash/pd.py @@ -146,6 +146,7 @@ def reset(self): self.end_current_transaction() self.writestate = 0 self.vhandler = None + self.state_rdid = None # Build dict mapping command keys to handler functions. Each # command in 'cmds' (defined in lists.py) has a matching @@ -167,14 +168,14 @@ def end_current_transaction(self): def start(self): self.out_ann = self.register(srd.OUTPUT_ANN) self.chip = chips[self.options['chip']] - self.vendor = self.options['chip'].split('_')[0] - extra_cmds = self.chip.get('extra_cmds') + self.vendor = self.chip.vendor + extra_cmds = self.chip.opts.get('extra_cmds') if extra_cmds: # FIXME - no sure yet how best to have "lists" refer to classes without falling back to strings :| self.vhandler = VendorDecoderMicron(self, extra_cmds) # Does the chip have a forced, fixed size? - chip_addr_size = self.chip.get('addr_size') + chip_addr_size = self.chip.opts.get('addr_size') opt_addr_size = self.options['addr_size'] if opt_addr_size == -1 and chip_addr_size is None: self.addr_size = 3 @@ -196,11 +197,8 @@ def putf(self, data): def putc(self, data): self.put(self.ss_cmd, self.es_cmd, self.out_ann, data) - def device(self): - return device_name[self.vendor].get(self.device_id, 'Unknown') - def vendor_device(self): - return '%s %s' % (self.chip['vendor'], self.device()) + return '%s %s' % (self.chip.vendor, self.chip.model) def cmd_ann_list(self): x, s = cmds[self.state][0], cmds[self.state][1] @@ -244,24 +242,50 @@ def handle_rdid(self, mosi, miso): if self.cmdstate == 1: # Byte 1: Master sends command ID. self.emit_cmd_byte() + self.state_rdid = {'exbytes': 0, 'manu': None, 'dtype': None, 'did': None} # Skip forward for continuation characters elif self.cmdstate == 2 and miso == 0x7f: self.putx([Ann.FIELD, ['Extension Byte: 0x7f']]) + self.state_rdid['exbytes'] += 1 return elif self.cmdstate == 2: # Byte 2: Slave sends the JEDEC manufacturer ID. self.putx([Ann.FIELD, ['Manufacturer ID: 0x%02x' % miso]]) + self.state_rdid['manu'] = miso elif self.cmdstate == 3: # Byte 3: Slave sends the memory type. self.putx([Ann.FIELD, ['Memory type: 0x%02x' % miso]]) + self.state_rdid['dtype'] = miso elif self.cmdstate == 4: # Byte 4: Slave sends the device ID. - self.device_id = miso self.putx([Ann.FIELD, ['Device ID: 0x%02x' % miso]]) + self.state_rdid['did'] = miso if self.cmdstate == 4: self.es_cmd = self.es - self.putc([Ann.RDID, self.cmd_vendor_dev_list()]) + seen_manu = self.state_rdid['exbytes'] << 8 | self.state_rdid['manu'] + seen_devid = self.state_rdid['dtype'] << 8 | self.state_rdid['did'] + # four cases + # * specified chip has an ID, and it matches, (show, no warn) + # * specified chip has an ID and it _doesn't_ match -> warn + # * specified chip does not have an ID, but we got one anyway (show what was seen, warn about bad data) + # * specified chip does not have an ID -> we shouldn't get here... + seen_id = "%04x:%04x" % (seen_manu, seen_devid) + if self.chip.has_ids(): + if self.chip.matches(seen_manu, seen_devid): + # TODO can make more positive "match" statement here. + self.putc([Ann.RDID, self.cmd_vendor_dev_list()]) + else: + self.putc([Ann.WARN, [ + "Selected chip %s (%s) doesn't match seen: %s, other decoding may be incorrect" % + (self.chip.key(), self.chip.idstr(), seen_id), + "RDID Mismatch %s != %s" % (self.chip.idstr(), seen_id) + ]]) + else: + # Either missing data for the chip selected, or wrong chip selected + self.putc([Ann.WARN, [ + "Chip %s has no IDs listed. Either wrong selection, or bad data" % (self.chip.key()) + ]]) self.state = None else: self.cmdstate += 1 From fe3a80801e8e057e6c4e930ada78246b4d11560f Mon Sep 17 00:00:00 2001 From: Karl Palsson Date: Fri, 15 Aug 2025 11:32:57 +0000 Subject: [PATCH 09/11] spiflash: add flash id from dumps repo This was in the dumps repo, so add it for decoding purposes. Signed-off-by: Karl Palsson --- decoders/spiflash/lists.py | 1 + 1 file changed, 1 insertion(+) diff --git a/decoders/spiflash/lists.py b/decoders/spiflash/lists.py index 1b7efb20..102c4f56 100644 --- a/decoders/spiflash/lists.py +++ b/decoders/spiflash/lists.py @@ -99,6 +99,7 @@ def __repr__(self): # Uses a "page << 8 | manuf_id" ala memtest86, rather than expanded 7f7f7f style notation ala flashrom # however, put in whatever the device _does_ not what it "should" be doing. chips_list = [ + FlashChip("Adesto", "AT25SF041", jedec_manu=0x001f, deviceid=0x8401), FlashChip("Adesto", "AT45DB161E", jedec_manu=0x001f, deviceid=0x2600, write_enable=False, sz_p=528, sz_s=128*1024, sz_b=4*1024), FlashChip("Atmel", "AT25xx", sz_p=64), FlashChip("Fidelix", "FM25Q32", jedec_manu=0x07a1, deviceid=0x4016, sz_p=256, sz_s=4*1024, sz_b=64*1024, rems_id=0x15), From 93ffa561c1d33a2baa990aa5990ca120f20423a1 Mon Sep 17 00:00:00 2001 From: Karl Palsson Date: Fri, 15 Aug 2025 11:33:47 +0000 Subject: [PATCH 10/11] spiflash: RDID: don't fail on trailing EDI data We don't have a good way of passing this to specific implementations yet, but at least don't choke on it, and present it properly. Tested on the https://github.com/sigrokproject/sigrok-dumps/tree/master/spi/spiflash/adesto_at45db161e test files Signed-off-by: Karl Palsson --- decoders/spiflash/pd.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/decoders/spiflash/pd.py b/decoders/spiflash/pd.py index 29aca54a..93ebc148 100644 --- a/decoders/spiflash/pd.py +++ b/decoders/spiflash/pd.py @@ -260,8 +260,6 @@ def handle_rdid(self, mosi, miso): # Byte 4: Slave sends the device ID. self.putx([Ann.FIELD, ['Device ID: 0x%02x' % miso]]) self.state_rdid['did'] = miso - - if self.cmdstate == 4: self.es_cmd = self.es seen_manu = self.state_rdid['exbytes'] << 8 | self.state_rdid['manu'] seen_devid = self.state_rdid['dtype'] << 8 | self.state_rdid['did'] @@ -286,9 +284,13 @@ def handle_rdid(self, mosi, miso): self.putc([Ann.WARN, [ "Chip %s has no IDs listed. Either wrong selection, or bad data" % (self.chip.key()) ]]) - self.state = None - else: - self.cmdstate += 1 + elif self.cmdstate == 5: + # Optionally, you can keep clocking, and receive N bytes of EDI. + # First byte says how much more to expect... + self.putx([Ann.FIELD, ['EDI Length: %0d' % miso]]) + elif self.cmdstate > 5: + self.putx([Ann.FIELD, ['Unhandled EDI: 0x%02x' % miso]]) + self.cmdstate += 1 def handle_rdsr(self, mosi, miso): # Read status register: Master asserts CS#, sends RDSR command, From 645dfe0f69d4fb716a232c012671b72907e4f2f5 Mon Sep 17 00:00:00 2001 From: Karl Palsson Date: Fri, 15 Aug 2025 12:50:20 +0000 Subject: [PATCH 11/11] spiflash: properly match res/rem ids as well as RDID While overhauling the data structure, we postponed fully checking the alternative "ID" commands. This updates them, tested against various dumps in the dumsp repo: * spi/spiflash/fidelix_fm25q32/cmd/0xab.sr * spi/spiflash/macronix_mx25l1605d/cmd/0x90.sr Signed-off-by: Karl Palsson --- decoders/spiflash/lists.py | 1 + decoders/spiflash/pd.py | 66 ++++++++++++++++++++++++++++++-------- 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/decoders/spiflash/lists.py b/decoders/spiflash/lists.py index 102c4f56..713aa59f 100644 --- a/decoders/spiflash/lists.py +++ b/decoders/spiflash/lists.py @@ -96,6 +96,7 @@ def __repr__(self): # page/sector/block sizes have never been used in this PD, but keep them for now if people provided them... # remsids are relegated to kwargs, largely superseded by jedec rdid... +# Is rems_id _always_ the jedec device id -1? # Uses a "page << 8 | manuf_id" ala memtest86, rather than expanded 7f7f7f style notation ala flashrom # however, put in whatever the device _does_ not what it "should" be doing. chips_list = [ diff --git a/decoders/spiflash/pd.py b/decoders/spiflash/pd.py index 93ebc148..5f20eca9 100644 --- a/decoders/spiflash/pd.py +++ b/decoders/spiflash/pd.py @@ -512,10 +512,22 @@ def handle_rdp_res(self, mosi, miso): elif self.cmdstate == 5: # Byte 5: Slave sends device ID. self.es_cmd = self.es - self.device_id = miso - self.putx([Ann.FIELD, ['Device ID: %s' % self.device()]]) - d = 'Device = %s' % self.vendor_device() - self.putc([Ann.RDP_RES, self.cmd_vendor_dev_list()]) + self.putx([Ann.FIELD, ['Device ID: %02x' % miso]]) + c_rid = self.chip.opts.get('rems_id') + if c_rid: + if c_rid == miso: + # TODO can make more positive "match" statement here. + self.putc([Ann.RDP_RES, self.cmd_vendor_dev_list()]) + else: + self.putc([Ann.WARN, [ + "Selected chip %s (%02x) doesn't match seen: %02x, other decoding may be incorrect" % + (self.chip.key(), c_rid, miso), + "REMS Mismatch %s != %s" % (c_rid, miso) + ]]) + else: + self.putc([Ann.WARN, [ + "Chip %s has no 'rems_id' listed. Either wrong selection, or bad data" % (self.chip.key()) + ]]) self.state = None self.cmdstate += 1 @@ -523,6 +535,7 @@ def handle_rems(self, mosi, miso): if self.cmdstate == 1: # Byte 1: Master sends command ID. self.emit_cmd_byte() + self.state_rdid = {'manu': None, 'did': None} elif self.cmdstate in (2, 3): # Bytes 2/3: Master sends two dummy bytes. self.putx([Ann.FIELD, ['Dummy byte: 0x%02x' % mosi]]) @@ -535,20 +548,47 @@ def handle_rems(self, mosi, miso): self.putx([Ann.FIELD, ['Master wants %s ID first' % d]]) elif self.cmdstate == 5: # Byte 5: Slave sends manufacturer ID (or device ID). - self.ids = [miso] - d = 'Manufacturer' if self.manufacturer_id_first else 'Device' - self.putx([Ann.FIELD, ['%s ID: 0x%02x' % (d, miso)]]) + #self.ids = [miso] + if self.manufacturer_id_first: + self.state_rdid['manu'] = miso + fn = 'Manufacturer' + else: + self.state_rdid['did'] = miso + fn = 'Device' + self.putx([Ann.FIELD, ['%s ID: 0x%02x' % (fn, miso)]]) elif self.cmdstate == 6: # Byte 6: Slave sends device ID (or manufacturer ID). - self.ids.append(miso) - d = 'Device' if self.manufacturer_id_first else 'Manufacturer' - self.putx([Ann.FIELD, ['%s ID: 0x%02x' % (d, miso)]]) + #self.ids.append(miso) + if self.manufacturer_id_first: + self.state_rdid['did'] = miso + fn = 'Device' + else: + self.state_rdid['manu'] = miso + fn = 'Manufacturer' + self.putx([Ann.FIELD, ['%s ID: 0x%02x' % (fn, miso)]]) if self.cmdstate == 6: - id_ = self.ids[1] if self.manufacturer_id_first else self.ids[0] - self.device_id = id_ + #id_ = self.ids[1] if self.manufacturer_id_first else self.ids[0] + #self.device_id = id_ self.es_cmd = self.es - self.putc([Ann.REMS, self.cmd_vendor_dev_list()]) + # rems gives both manufacturer and remsid, so you need both the jedec manuf, + the "remsid" + c_rid = self.chip.opts.get('rems_id') + if self.chip.has_ids() and c_rid: + c_manu = self.chip.jedec_manu & 0xff + seen_manu = self.state_rdid['manu'] + seen_did = self.state_rdid['did'] + if c_manu == seen_manu and c_rid == seen_did: + self.putc([Ann.REMS, self.cmd_vendor_dev_list()]) + else: + self.putc([Ann.WARN, [ + "Selected chip %s (%02x:%02x) doesn't match seen: %02x:%02x, other decoding may be incorrect" % + (self.chip.key(), c_manu, c_rid, seen_manu, seen_did), + "REMS Mismatch %02x:%02x != %02x:%02x" % (c_manu, c_rid, seen_manu, seen_did) + ]]) + else: + self.putc([Ann.WARN, [ + "Chip %s has no 'rems_id' listed. Either wrong selection, or bad data" % (self.chip.key()) + ]]) self.state = None else: self.cmdstate += 1