From eade7cde6558ddf941d32c698d0229493e9c2862 Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Fri, 19 Mar 2021 11:45:01 -0700 Subject: [PATCH] Add QSPI and continuation support This uses the Simple Parallel low level analyzer as input and guesstimates where CS is raised based on transmission pauses. --- README.md | 21 ++++- SPIFlashAnalyzer.py | 217 +++++++++++++++++++++++++++++++++----------- 2 files changed, 186 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 34f7bbb..640cfb4 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,28 @@ Under the three dot, more menu, click `Check for Updates` then install the `SPI 2. Under the three dot, more menu, click `Load Existing Extension...` and then select the local repo location. ## Usage + +### Normal SPI 1. Click the analyzers tab. 2. Add a `SPI` analyzer and configure it for your capture. 3. Add a `SPI Flash` analyzer. 4. Set the `SPI` analyzer as the Input Analyzer. 5. `Min Address`, `Max Address` and `Decode Level` are optional. -6. Change `Address Bytes` to match \ No newline at end of file +6. Change `Address Bytes` to match + +### Quad SPI +1. Click the analyzers tab. +2. Add a `Simple Parallel` analyzer. (You may need to search for it.) +3. Configure it for your capture. + 1. Set D0 to MOSI. + 2. Set D1 to MISO. + 3. Set D2 to IO2 (WP on 8 pin flashes usually.) + 4. Set D3 to IO3 (HOLD on 8 pin flashes usually.) + 5. Set D15 to CS (used to ignore extra clocks.) + 6. Set the clock to the clock pin with the correct edge set. +4. Add a `SPI Flash` analyzer. +5. Set the `Simple Parallel` analyzer as the Input Analyzer. +6. `Min Address`, `Max Address` and `Decode Level` are optional. +7. Change `Address Bytes` to match + +Note: When using Simple Parallel input, it is assumed the CS line goes high between parallel captures that are greater than 4 times the time separation of the closest clocks seen thus far. So, beware of spurious clocks and SPI transmissions that pause between bytes but leave CS low. This analyzer may incorrectly partition the transactions. diff --git a/SPIFlashAnalyzer.py b/SPIFlashAnalyzer.py index 653e53b..6222913 100644 --- a/SPIFlashAnalyzer.py +++ b/SPIFlashAnalyzer.py @@ -5,17 +5,34 @@ import struct +# value is dummy clocks +CONTINUE_COMMANDS = { + 0xe7: 2, + 0xeb: 4, +} + DATA_COMMANDS = {0x03: "Read", 0x0b: "Fast Read", + 0xe7: "Quad Word Read", + 0xeb: "Quad Read", 0x02: "Page Program", 0x32: "Quad Page Program"} CONTROL_COMMANDS = { + 0x01: "Write Status Register 1", 0x06: "Write Enable", 0x05: "Read Status Register", + 0x35: "Read Status Register 2", 0x75: "Program Suspend" } +class FakeFrame: + def __init__(self, t, time=None): + self.type = t + self.start_time = time + self.end_time = time + self.data = {} + # High level analyzers must subclass the HighLevelAnalyzer class. class SPIFlash(HighLevelAnalyzer): # List of settings that a user can set for this High Level Analyzer. @@ -44,12 +61,33 @@ def __init__(self): Settings can be accessed using the same name used above. ''' self._start_time = None - self._address_format = "{:0" + str(2*int(self.address_bytes)) + "x}" + self._address_bytes = 3 + self._address_format = "{:0" + str(2*int(self._address_bytes)) + "x}" self._min_address = int(self.min_address) self._max_address = None if self.max_address: self._max_address = int(self.max_address) + self._miso_data = None + self._mosi_data = None + self._empty_result_count = 0 + + # These are for quad decoding. The input will be a SimpleParallel analyzer + # with the correct clock edge. CS is inferred from a gap in time. + self._last_cs = 1 + self._last_time = None + self._transaction = 0 + self._clock_count = 0 + self._mosi_out = 0 + self._miso_in = 0 + self._quad_data = 0 + self._quad_start = None + self._continuous = False + self._dummy = 0 + + self._fastest_cs = 2000000 + + def decode(self, frame: AnalyzerFrame): ''' Process a frame from the input analyzer, and optionally return a single `AnalyzerFrame` or a list of `AnalyzerFrame`s. @@ -57,56 +95,133 @@ def decode(self, frame: AnalyzerFrame): The type and data values in `frame` will depend on the input analyzer. ''' - if frame.type == "enable": - self._start_time = frame.start_time - self._miso_data = bytearray() - self._mosi_data = bytearray() - elif frame.type == "result": - if self._miso_data is None or self._mosi_data is None: - print(frame) - return - self._miso_data.extend(frame.data["miso"]) - self._mosi_data.extend(frame.data["mosi"]) - elif frame.type == "disable": - if not self._miso_data or not self._mosi_data: - return - command = self._mosi_data[0] - frame_type = None - frame_data = {"command": command} - if command in DATA_COMMANDS: - if len(self._mosi_data) < 1 + int(self.address_bytes): - frame_type = "error" - else: - frame_type = "data_command" - frame_data["command"] = DATA_COMMANDS[command] - frame_address = 0 - for i in range(int(self.address_bytes)): - frame_address <<= 8 - frame_address += self._mosi_data[1+i] - if self.min_address > 0 and frame_address < self._min_address: - frame_type = None - elif self.max_address and frame_address > self.max_address: - frame_type = None - else: - frame_data["address"] = self._address_format.format(frame_address) + # Support getting data from a Simple Parallel and converting it. + frames = [] + if frame.type == "data": + data = frame.data["data"] + cs = data >> 15 + if self._last_time: + diff = frame.start_time - self._last_time else: - if command in CONTROL_COMMANDS: - frame_data["command"] = CONTROL_COMMANDS[command] - frame_type = "control_command" - our_frame = None - if frame_type: - our_frame = AnalyzerFrame(frame_type, - self._start_time, - frame.end_time, - frame_data) - self._miso_data = None - self._mosi_data = None - if self.decode_level == 'Only Data' and frame_type == "control_command": - return None - if self.decode_level == 'Only Errors' and frame_type != "error": - return None - if self.decode_level == "Only Control" and frame_type != "control_command": + diff = self._fastest_cs + diff = float(diff * 1_000_000_000) + + self._fastest_cs = min(diff * 4, self._fastest_cs) + if diff > self._fastest_cs and cs == 0: + if self._transaction > 0: + frames.append(FakeFrame("disable", self._last_time)) + + frames.append(FakeFrame("enable", frame.start_time)) + + self._transaction += 1 + self._clock_count = 0 + if not self._continuous: + self._command = 0 + self._quad_start = None + self._dummy = 0 + else: + self._clock_count = 8 + f = FakeFrame("result") + f.data["mosi"] = [self._command] + f.data["miso"] = [0] + frames.append(f) + + self._last_time = frame.start_time + + # TODO: We could output clock counts when cs is high. + if cs == 1: return None - return our_frame + + if self._quad_start is None or self._clock_count < self._quad_start: + self._mosi_out = self._mosi_out << 1 | (data & 0x1) + self._miso_in = self._miso_in << 1 | ((data >> 1) & 0x1) + if self._clock_count % 8 == 7: + if self._clock_count == 7: + self._command = self._mosi_out + if self._command in CONTINUE_COMMANDS: + self._quad_start = 8 + self._dummy = CONTINUE_COMMANDS[self._command] + + f = FakeFrame("result") + f.data["mosi"] = [self._mosi_out] + f.data["miso"] = [self._miso_in] + frames.append(f) + self._mosi_out = 0 + self._miso_in = 0 + else: + self._quad_data = (self._quad_data << 4 | data & 0xf) + if self._clock_count % 2 == 1: + + f = FakeFrame("result") + if not 15 < self._clock_count <= 15 + self._dummy: + f.data["mosi"] = [self._quad_data] + f.data["miso"] = [0] + else: + f.data["mosi"] = [0] + f.data["miso"] = [self._quad_data] + frames.append(f) + if self._command in CONTINUE_COMMANDS and self._clock_count == 15: + self._continuous = self._quad_data == 0xa0 + self._quad_data = 0 + + self._clock_count += 1 else: - print(frame) + print("non data!") + frames = [frame] + + output = None + for fake_frame in frames: + if fake_frame.type == "enable": + self._start_time = fake_frame.start_time + self._miso_data = bytearray() + self._mosi_data = bytearray() + elif fake_frame.type == "result": + if self._miso_data is None or self._mosi_data is None: + if self._empty_result_count == 0: + print(fake_frame) + self._empty_result_count += 1 + continue + self._miso_data.extend(fake_frame.data["miso"]) + self._mosi_data.extend(fake_frame.data["mosi"]) + elif fake_frame.type == "disable": + if not self._miso_data or not self._mosi_data: + continue + command = self._mosi_data[0] + frame_type = None + frame_data = {"command": command} + if command in DATA_COMMANDS: + if len(self._mosi_data) < 1 + int(self._address_bytes): + frame_type = "error" + else: + frame_type = "data_command" + frame_data["command"] = DATA_COMMANDS[command] + frame_address = 0 + for i in range(int(self._address_bytes)): + frame_address <<= 8 + frame_address += self._mosi_data[1+i] + if self.min_address > 0 and frame_address < self._min_address: + frame_type = None + elif self.max_address and frame_address > self.max_address: + frame_type = None + else: + frame_data["address"] = self._address_format.format(frame_address) + else: + if command in CONTROL_COMMANDS: + frame_data["command"] = CONTROL_COMMANDS[command] + frame_type = "control_command" + our_frame = None + if frame_type: + our_frame = AnalyzerFrame(frame_type, + self._start_time, + fake_frame.end_time, + frame_data) + self._miso_data = None + self._mosi_data = None + if self.decode_level == 'Only Data' and frame_type == "control_command": + continue + if self.decode_level == 'Only Errors' and frame_type != "error": + continue + if self.decode_level == "Only Control" and frame_type != "control_command": + continue + output = our_frame + return output