| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| # | |
| # Copyright (C) 2017-2020 The Project X-Ray Authors. | |
| # | |
| # Use of this source code is governed by a ISC-style | |
| # license that can be found in the LICENSE file or at | |
| # https://opensource.org/licenses/ISC | |
| # | |
| # SPDX-License-Identifier: ISC | |
| ''' | |
| 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 | |
| from prjxray.util import OpenSafeFile | |
| conf_regs = { | |
| 0: "CRC", | |
| 1: "FAR_MAJ", | |
| 2: "FAR_MIN", | |
| 3: "FDRI", | |
| 4: "FDRO", | |
| 5: "CMD", | |
| 6: "CTL", | |
| 7: "MASK", | |
| 8: "STAT", | |
| 9: "LOUT", | |
| 10: "COR1", | |
| 11: "COR2", | |
| 12: "PWRDN_REG", | |
| 13: "FLR", | |
| 14: "IDCODE", | |
| 15: "CWDT", | |
| 16: "HC_OPT_REG", | |
| 18: "CSBO", | |
| 19: "GENERAL1", | |
| 20: "GENERAL2", | |
| 21: "GENERAL3", | |
| 22: "GENERAL4", | |
| 23: "GENERAL5", | |
| 24: "MODE_REG", | |
| 25: "PU_GWE", | |
| 26: "PU_GTS", | |
| 27: "MFWR", | |
| 28: "CCLK_FREQ", | |
| 29: "SEU_OPT", | |
| 30: "EXP_SIGN", | |
| 31: "RDBK_SIGN", | |
| 32: "BOOTSTS", | |
| 33: "EYE_MASK", | |
| 34: "CBC_REG" | |
| } | |
| cmd_reg_codes = { | |
| 0: "NULL", | |
| 1: "WCFG", | |
| 2: "MFW", | |
| 3: "LFRM", | |
| 4: "RCFG", | |
| 5: "START", | |
| 7: "RCRC", | |
| 8: "AGHIGH", | |
| 10: "GRESTORE", | |
| 11: "SHUTDOWN", | |
| 13: "DESYNC", | |
| 14: "IPROG" | |
| } | |
| 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 | |
| with OpenSafeFile(file_name, "rb") as f: | |
| self.bytes = f.read() | |
| pos, self.header = self.get_header() | |
| self.body = [ | |
| (i << 8) | j | |
| for i, j in zip(self.bytes[pos::2], self.bytes[pos + 1::2]) | |
| ] | |
| self.parse_bitstream(verbose) | |
| def get_header(self): | |
| pos = next(KnuthMorrisPratt(self.bytes, [0xaa, 0x99, 0x55, 0x66])) | |
| return pos + 4, self.bytes[:pos + 4] | |
| 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) | |
| 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], reg_addr, words)) | |
| if opcode and reg_addr in conf_regs: | |
| payload_len = words | |
| continue | |
| def parse_packet_header(self, word): | |
| type = (word >> 13) & 0x7 | |
| opcode = (word >> 11) & 0x3 | |
| reg_addr = (word >> 5) & 0x3F | |
| if type == 1: | |
| word_count = word & 0x1F | |
| elif type == 2: | |
| word_count = 2 | |
| 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_MAJ": | |
| 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 OpenSafeFile(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 OpenSafeFile(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) |