utils: bitstream_analyzer: Add initial version of bitstream analyzer

Signed-off-by: Tomasz Michalak <tmichalak@antmicro.com>
diff --git a/utils/bitstream_analyzer.py b/utils/bitstream_analyzer.py
new file mode 100755
index 0000000..8411445
--- /dev/null
+++ b/utils/bitstream_analyzer.py
@@ -0,0 +1,417 @@
+#!/usr/bin/env python3
+'''
+Spartan 6 bitstream analyzer tool.
+
+This script reads a Spartan6 bitstream and prints out some useful information.
+It can also create a frames file with the configuration data words.
+The bitstream is analyzed word by word and interpreted according to
+the UG380 Configuration User Guide.
+
+The tool can be used to derive the initialization, startup and finalization
+sequence as well as the configuration data. The latter is written to a frames
+file which can be used by the bitstream tools such as frames2bit to generate
+a valid bitstream.
+'''
+
+import argparse
+from io import StringIO
+
+conf_regs = {
+    0: "CRC",
+    1: "FAR",
+    2: "FDRI",
+    3: "FDRO",
+    4: "CMD",
+    5: "CTL0",
+    6: "MASK",
+    7: "STAT",
+    8: "LOUT",
+    9: "COR0",
+    10: "MFWR",
+    11: "CBC",
+    12: "IDCODE",
+    13: "AXSS",
+    14: "COR1",
+    16: "WBSTAR",
+    17: "TIMER",
+    22: "BOOTSTS",
+    24: "CTL1",
+    31: "BSPI"
+}
+
+cmd_reg_codes = {
+    0: "NULL",
+    1: "WCFG",
+    2: "MFW",
+    3: "LFRM",
+    4: "RCFG",
+    5: "START",
+    7: "RCRC",
+    8: "AGHIGH",
+    9: "SWITCH",
+    10: "GRESTORE",
+    11: "SHUTDOWN",
+    13: "DESYNC",
+    15: "IPROG",
+    16: "CRCC",
+    17: "LTIMER",
+    18: "BSPI_READ",
+    19: "FALL_EDGE"
+}
+
+opcodes = ("NOP", "READ", "WRITE", "UNKNOWN")
+
+
+def KnuthMorrisPratt(text, pattern):
+    '''
+    Yields all starting positions of copies of the pattern in the text.
+    Calling conventions are similar to string.find, but its arguments can be
+    lists or iterators, not just strings, it returns all matches, not just
+    the first one, and it does not need the whole text in memory at once.
+    Whenever it yields, it will have read the text exactly up to and including
+    the match that caused the yield.
+    '''
+
+    # allow indexing into pattern and protect against change during yield
+    pattern = list(pattern)
+
+    # build table of shift amounts
+    shifts = [1] * (len(pattern) + 1)
+    shift = 1
+    for pos in range(len(pattern)):
+        while shift <= pos and pattern[pos] != pattern[pos - shift]:
+            shift += shifts[pos - shift]
+        shifts[pos + 1] = shift
+
+    # do the actual search
+    startPos = 0
+    matchLen = 0
+    for c in text:
+        while matchLen == len(pattern) or \
+              matchLen >= 0 and pattern[matchLen] != c:
+            startPos += shifts[matchLen]
+            matchLen -= shifts[matchLen]
+        matchLen += 1
+        if matchLen == len(pattern):
+            yield startPos
+
+
+class Bitstream:
+    def __init__(self, file_name, verbose=False):
+        self.frame_data = []
+        self.idcode = 0
+        self.exp_sign = 0
+        self.far_min = 0
+        self.far_maj = 0
+        self.curr_fdri_write_len = 0
+        self.curr_crc_check = 0
+        self.fdri_in_progress = False
+        self.words = []
+        with open(file_name, "rb") as f:
+            word = f.read(4)
+            while word:
+                self.words.append(int.from_bytes(word, byteorder = 'big'))
+                word = f.read(4)
+        pos, self.header = self.get_header()
+        self.body = self.words[pos:]
+        self.parse_bitstream(verbose)
+
+    def get_header(self):
+        pos = next(KnuthMorrisPratt(self.words, [0xaa995566]))
+        return pos + 1, self.words[:pos + 1]
+
+    def parse_bitstream(self, verbose):
+        payload_len = 0
+        for word in self.body:
+            if payload_len > 0:
+                if verbose:
+                    print("\tWord: ", hex(word))
+                #payload_len = self.parse_reg(
+                #    reg_addr, word, payload_len, verbose)
+                payload_len -= 1
+                continue
+            else:
+                packet_header = self.parse_packet_header(word)
+                opcode = packet_header["opcode"]
+                reg_addr = packet_header["reg_addr"]
+                words = packet_header["word_count"]
+                type = packet_header["type"]
+                if verbose:
+                    print(
+                        "\tWord: ", hex(word),
+                        'Type: {}, Op: {}, Addr: {} ({}), Words: {}'.format(
+                            type, opcodes[opcode], conf_regs[reg_addr] if reg_addr in conf_regs else "UNKNOWN", reg_addr, words))
+                if opcode and reg_addr in conf_regs:
+                    payload_len = words
+                    continue
+
+    def parse_packet_header(self, word):
+        type = (word >> 29) & 0x7
+        opcode = (word >> 27) & 0x3
+        reg_addr = (word >> 13) & 0x1F
+        if type == 1:
+            word_count = word & 0x7FF
+        elif type == 2:
+            word_count = word & 0x7FFFFFF
+        else:
+            word_count = 0
+        return {
+            "type": type,
+            "opcode": opcode,
+            "reg_addr": reg_addr,
+            "word_count": word_count
+        }
+
+    def parse_command(self, word):
+        return cmd_reg_codes[word]
+
+    def parse_cor1(self, word):
+        return word
+
+    def parse_cor2(self, word):
+        return word
+
+    def parse_ctl(self, word):
+        #decryption
+        dec = (word >> 6) & 1
+        #security bits
+        sb = (word >> 4) & 3
+        #persist
+        p = (word >> 3) & 1
+        #use efuse
+        efuse = (word >> 2) & 1
+        #crc extstat disable
+        crc = (word >> 1) & 1
+        return {
+            "decryption": dec,
+            "security bits": sb,
+            "pesist": p,
+            "use efuse": efuse,
+            "crc extstat disable": crc
+        }
+
+    def parse_cclk_freq(self, word):
+        ext_mclk = (word >> 14) & 1
+        mclk_freq = word & 0x3FF
+        return (ext_mclk, mclk_freq)
+
+    def parse_pwrdn(self, word):
+        en_eyes = (word >> 14) & 1
+        filter_b = (word >> 5) & 1
+        en_pgsr = (word >> 4) & 1
+        en_pwrdn = (word >> 2) & 1
+        keep_sclk = word & 1
+        return {
+            "en_eyes": en_eyes,
+            "filter_b": filter_b,
+            "en_pgsr": en_pgsr,
+            "en_pwrdn": en_pwrdn,
+            "keep_sclk": keep_sclk
+        }
+
+    def parse_eye_mask(self, word):
+        return word & 0xFF
+
+    def parse_hc_opt(self, word):
+        return (word >> 6) & 1
+
+    def parse_cwdt(self, word):
+        return word
+
+    def parse_pu_gwe(self, word):
+        return word & 0x3FF
+
+    def parse_pu_gts(self, word):
+        return word & 0x3FF
+
+    def parse_mode(self, word):
+        new_mode = (word >> 13) & 0x1
+        buswidth = (word >> 11) & 0x3
+        bootmode = (word >> 8) & 0x7
+        bootvsel = word & 0xFF
+        return {
+            "new_mode": new_mode,
+            "buswidth": buswidth,
+            "bootmode": bootmode,
+            "bootvsel": bootvsel
+        }
+
+    def parse_seu(self, word):
+        seu_freq = (word >> 4) & 0x3FF
+        seu_run_on_err = (word >> 3) & 0x1
+        glut_mask = (word >> 1) & 0x1
+        seu_enable = word & 0x1
+        return {
+            "seu_freq": seu_freq,
+            "seu_run_on_err": seu_run_on_err,
+            "glut_mask": glut_mask,
+            "seu_enable": seu_enable
+        }
+
+    def parse_reg(self, reg_addr, word, payload_len, verbose):
+        reg = conf_regs[reg_addr]
+        if reg == "CMD":
+            command = self.parse_command(word)
+            if verbose:
+                print("Command: {}\n".format(command))
+        elif reg == "FLR":
+            frame_length = word
+            if verbose:
+                print("Frame length: {}\n".format(frame_length))
+        elif reg == "COR1":
+            conf_options = self.parse_cor1(word)
+            if verbose:
+                print("COR1 options: {}\n".format(conf_options))
+        elif reg == "COR2":
+            conf_options = self.parse_cor2(word)
+            if verbose:
+                print("COR2 options: {}\n".format(conf_options))
+        elif reg == "IDCODE":
+            assert payload_len < 3
+            if payload_len == 2:
+                self.idcode = word << 16
+            elif payload_len == 1:
+                self.idcode |= word
+                if verbose:
+                    print("IDCODE: {}\n".format(hex(self.idcode)))
+        elif reg == "MASK":
+            mask = word
+            if verbose:
+                print("Mask value: {}\n".format(mask))
+        elif reg == "CTL":
+            ctl_options = self.parse_ctl(word)
+            if verbose:
+                print("CTL options: {}\n".format(ctl_options))
+        elif reg == "CCLK_FREQ":
+            cclk_freq_options = self.parse_cclk_freq(word)
+            if verbose:
+                print("CCLK_FREQ options: {}\n".format(cclk_freq_options))
+        elif reg == "PWRDN_REG":
+            suspend_reg_options = self.parse_pwrdn(word)
+            if verbose:
+                print("{} options: {}\n".format(reg, suspend_reg_options))
+        elif reg == "EYE_MASK":
+            eye_mask = self.parse_eye_mask(word)
+            if verbose:
+                print("{} options: {}\n".format(reg, eye_mask))
+        elif reg == "HC_OPT_REG":
+            hc_options = self.parse_hc_opt(word)
+            if verbose:
+                print("{} options: {}\n".format(reg, hc_options))
+        elif reg == "CWDT":
+            cwdt_options = self.parse_cwdt(word)
+            if verbose:
+                print("{} options: {}\n".format(reg, cwdt_options))
+        elif reg == "PU_GWE":
+            pu_gwe_sequence = self.parse_pu_gwe(word)
+            if verbose:
+                print("{} options: {}\n".format(reg, pu_gwe_sequence))
+        elif reg == "PU_GTS":
+            pu_gts_sequence = self.parse_pu_gts(word)
+            if verbose:
+                print("{} options: {}\n".format(reg, pu_gts_sequence))
+        elif reg == "MODE_REG":
+            mode_options = self.parse_mode(word)
+            if verbose:
+                print("{} options: {}\n".format(reg, mode_options))
+        elif reg == "GENERAL1" or reg == "GENERAL2" \
+             or reg == "GENERAL3" or reg == "GENERAL4" \
+             or reg == "GENERAL5":
+            general_options = word
+            if verbose:
+                print("{} options: {}\n".format(reg, general_options))
+        elif reg == "SEU_OPT":
+            seu_options = self.parse_seu(word)
+            if verbose:
+                print("{} options: {}\n".format(reg, seu_options))
+        elif reg == "EXP_SIGN":
+            if payload_len == 2:
+                self.exp_sign = word << 16
+            elif payload_len == 1:
+                self.exp_sign |= word
+                if verbose:
+                    print("{}: {}\n".format(reg, self.exp_sign))
+        elif reg == "FAR":
+            if payload_len == 2:
+                self.current_far_maj = word
+            elif payload_len == 1:
+                self.current_far_min = word
+                if verbose:
+                    print(
+                        "{}: {} FAR_MIN: {}\n".format(
+                            reg, self.far_maj, self.far_min))
+        elif reg == "FDRI":
+            if self.fdri_in_progress:
+                self.frame_data.append(word)
+                if payload_len == 1:
+                    self.fdri_in_progress = False
+                    return 0
+            elif payload_len == 2:
+                self.curr_fdri_write_len = (word & 0xFFF) << 16
+            elif payload_len == 1:
+                self.curr_fdri_write_len |= word
+                self.fdri_in_progress = True
+                # Check if 0 words actually means read something
+                payload_len = self.curr_fdri_write_len + 2
+                if verbose:
+                    print("{}: {}\n".format(reg, self.curr_fdri_write_len))
+                return payload_len
+        elif reg == "CRC":
+            if payload_len == 2:
+                self.curr_crc_check = (word & 0xFFF) << 16
+            elif payload_len == 1:
+                self.curr_crc_check |= word
+                if verbose:
+                    print("{}: {}\n".format(reg, self.curr_crc_check))
+        payload_len -= 1
+        return payload_len
+
+    def write_frames_txt(self, file_name):
+        '''Write frame data in a more readable format'''
+        frame_stream = StringIO()
+        for i in range(len(self.frame_data)):
+            if i % 65 == 0:
+                frame_stream.write("\nFrame {:4}\n".format(i // 65))
+            #IOB word
+            if i % 65 == 32:
+                frame_stream.write(
+                    "\n#{:3}:{:6}\n".format(i % 65, hex(self.frame_data[i])))
+            else:
+                frame_stream.write(
+                    "#{:3}:{:6},".format(i % 65, hex(self.frame_data[i])))
+        with open(file_name, "w") as f:
+            print(frame_stream.getvalue(), file=f)
+
+    def write_frames(self, file_name):
+        '''Write configuration data to frames file'''
+        frame_stream = StringIO()
+        for i in range(len(self.frame_data)):
+            if i % 65 == 0:
+                frame_stream.write("0x{:08x} ".format(i // 65))
+            frame_stream.write("0x{:04x}".format(self.frame_data[i]))
+            if i % 65 == 64:
+                frame_stream.write("\n")
+            elif i < len(self.frame_data) - 1:
+                frame_stream.write(",")
+        with open(file_name, "w") as f:
+            print(frame_stream.getvalue(), file=f)
+
+
+def main(args):
+    verbose = not args.silent
+    bitstream = Bitstream(args.bitstream, verbose)
+    print("Frame data length: ", len(bitstream.frame_data))
+    if args.frames_out:
+        bitstream.write_frames(args.frames_out)
+        if verbose:
+            bitstream.write_frames_txt(args.frames_out + ".txt")
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument('--bitstream', help='Input bitstream')
+    parser.add_argument('--frames_out', help='Output frames file')
+    parser.add_argument(
+        '--silent', help="Don't print analysis details", action='store_true')
+    args = parser.parse_args()
+    main(args)