summarylogtreecommitdiffstats
path: root/cyusb-fw-extract-py2.py
blob: 9cb25dc3efe778bc46af0b335a26ee654e3134be (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
#!/usr/bin/python
# -*- coding: ascii -*-
########################################################################
# cyusb-fw-extract.py - Extract firmware from Cypress USB script files 
# Copyright (C) 2008  Dwayne C. Litzenberger <dlitz@dlitz.net>
#
# 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 3 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 <http://www.gnu.org/licenses/>.
########################################################################
#
# This program attempts to create fxload-compatible firmware images by
# analyzing a supplied Cypress USB script file.  Cypress USB script files
# typically have the ".spt" file extension, and are used in devices that
# incorporate the Cypress Semiconductor EZ-USB (CY7C68xxx) series
# microcontrollers.  These script files are executed on Windows by the Cypress
# Generic USB Driver (CyUsb.sys).
#
# This program is known to work for the following firmware(s):
#   - ADS Tech Instant Video-To-Go RDX-160 (hardware H.264 encoder)
#       + "vtogold.spt" on the CD "Instant Video To-Go CD, Ver. 1.2"
#

import sys
import os.path
import struct
import getopt

PROGRAM_VERSION = "cyusb-fw-extract 0.1"

def exit_usage():
    print >>sys.stderr, """Usage: %s [-v] -oPREFIX SPTFILE

Read SPTFILE and output PREFIX_1.ihx, PREFIX_2.ihx...
Use -v for verbose output.
""" % (sys.argv[0],)
    sys.exit(2)

class CSPTChunk(object):
    @classmethod
    def fromfile(cls, file):
        chunk = file.read(4)
        if not chunk:
            return None     # EOF
        if chunk[:4] != "CSPT":
            raise ValueError("Error reading chunk: expected 'CSPT', got %r" % (chunk[:4],))
        chunk += file.read(4)
        (length,) = struct.unpack("<L", chunk[4:8])
        if length < 8:
            raise ValueError("Error reading chunk: chunk length %d too small" % (length,))
        chunk += file.read(length - 8)
        if len(chunk) < length:
            raise EOFError("Error reading chunk: file truncated")

        fmt = "<LLLLBBHLLL"
        hdr_len = struct.calcsize(fmt)
        (magic,         # 0:  Long word
         length,        # 4:  Long word
         dummy8,        # 8:  Long word
         dummy12,       # 12: Long word
         request,       # 16: Byte
         dummy17,       # 17: Byte
         addr,          # 18: Half word
         dummy20,       # 20: long word
         dummy24,       # 24: long word
         data_len,      # 28: long word
        ) = struct.unpack(fmt, chunk[:hdr_len])
        data = chunk[hdr_len:]
        if len(data) != data_len:
            raise ValueError("Error reading chunk: data length mismatch (%d vs %d)" % (len(data), data_len))

        if len(data) < data_len:
            raise EOFError("Error reading chunk: file truncated")
        
        obj = cls()
        obj.raw_chunk = chunk
        obj.dummies = (dummy8, dummy12, dummy17, dummy20, dummy24)
        obj.request = request
        obj.address = addr
        obj.data = data

        return obj

    def __repr__(self):
        return """<%s request=0x%02x address=0x%04x length=%d dummies=%r>""" % (
            self.__class__.__name__, self.request, self.address,
            len(self.data), self.dummies)

def make_ihx_data_lines(addr, data, width=16):
    """Generate a list of Intel Hex format lines"""
    if not 1 <= width < 256:
        raise ValueError("width must be in range(1,256)")
    if not 0 <= addr <= 0xffff or not 0 <= addr + len(data) - 1 <= 0xffff:
        raise NotImplementedError("Addresses greater than 16 bits not supported")
    for i in xrange(0, len(data), width):
        a = addr + i
        d = data[i:i+width]
        assert 0 <= a <= 0xffff
        assert 0 <= a+width-1 <= 0xffff
        octets = []
        octets.append(chr(len(d)))   # Load record length
        octets.append(chr(a >> 8))   # high address bits
        octets.append(chr(a & 0xff)) # low address bits
        octets.append("\x00")        # record type = 00 (data record)
        octets += list(d)

        # checksum
        checksum = 0
        for c in octets:
            checksum = (checksum + ord(c)) & 0xff
        checksum = (0x100 - checksum) & 0xff
        octets.append(chr(checksum))

        line = ":%s\n" % ("".join(octets).encode('hex').upper(),)
        yield line

def main():
    # Parse arguments
    verbose = False
    output_prefix = None

    try:
        optlist, args = getopt.getopt(sys.argv[1:], 'vo:')
    except getopt.GetoptError, exc:
        print >>sys.stderr, "error: %s" % (str(exc),)
        exit_usage()

    if len(args) != 1:
        print >>sys.stderr, "error: this program takes exactly one argument"
        exit_usage()
    (input_path,) = args
    
    for (opt, optarg) in optlist:
        if opt == '-v':
            verbose = True
        elif opt == "-o":
            output_prefix = optarg
        else:
            assert 0    # This should never happen

    if output_prefix is None:
        print >>sys.stderr, "error: no output prefix specified"
        exit_usage()

    warning_count = 0
    cpucs_address = None    # Address of the CPUCS register
    cpu_reset = 0           # Value of the 8051RES bit (1 = CPU in RESET)
    stage_number = 0
    input_file = open(input_path, "rb")
    chunk = CSPTChunk.fromfile(input_file)
    output_file = None
    while chunk is not None:
#        if verbose:
#            print repr(chunk)

        if chunk.request != 0xa0:
            print >>sys.stderr, "warning: skipping unrecognized device request 0x%02x" % (chunk.request,)
            warning_count += 1
            continue

        # FIXME: What do these bits mean?  Do they need to be set this way?
        if chunk.dummies != (0x20, 0x40000000, 0x75, 0x900000, 0xf):
            print >>sys.stderr, "warning: file format not entirely understood: %r" % (chunk.dummies,)
            warning_count += 1

        # The first request in the input file should be to write a single byte
        # to the CPUCS register.  Prior to uploading the firmware, the 8051 CPU
        # core must be put into a RESET state by setting CPUCS.0 = 1.  When the
        # upload is complete, the 8051 core must be restarted by setting
        # CPUCS.0 = 0.  This process is done once for a single-stage loader, or
        # twice for a two-stage loader.

        if cpucs_address is None:   # First write
            if len(chunk.data) != 1:
                print >>sys.stderr, "warning: skipping multi-byte write past CPUCS" % (chunk.request,)
                warning_count += 1
                continue
            
            if not ord(chunk.data[0]) & 1:      # The reset bit should be
                print >>sys.stderr, "warning: first write clears CPUCS.0 instead of setting it" % (chunk.request,)
                warning_count += 1

            cpucs_address = chunk.address
            if verbose:
                print "Detected CPUCS address: 0x%04x" % (cpucs_address,)
        
        if chunk.address == cpucs_address:
            if len(chunk.data) != 1:
                print >>sys.stderr, "warning: skipping multi-byte write past CPUCS" % (chunk.request,)
                warning_count += 1
                continue
            
            new_reset_bit = ord(chunk.data[0]) & 1      # 8051RES = CPUCS.0
            if new_reset_bit == cpu_reset:
                print >>sys.stderr, "warning: skipping write to CPUCS that doesn't change CPUCS.0" % (chunk.request,)
                warning_count += 1
                continue
            
            if not cpu_reset:
                # Open a new file
                stage_number += 1
                filename = output_prefix + "_%d.ihx" % (stage_number,)
                if verbose:
                    print "Writing stage %d to %s" % (stage_number, filename,)
                output_file = open(filename, "w")
                output_file.write("# Extracted using %s\n" % (PROGRAM_VERSION,)) 
                output_file.write("# Stage %d from %s\n" % (stage_number, os.path.basename(input_path).replace("\n", " ")))
            else:
                # Write the end-of-file marker and close current file
                output_file.write(":00000001FF\n")
                output_file.close()
                output_file = None

            cpu_reset = new_reset_bit
        
        else:

            for line in make_ihx_data_lines(chunk.address, chunk.data):
                output_file.write(line)

        chunk = CSPTChunk.fromfile(input_file)

    if cpu_reset:
        print >>sys.stderr, "warning: input file finished without clearing CPUCS.0"
        warning_count += 1

    input_file.close()
    
    if warning_count > 0:
        print >>sys.stderr, "Finished with %d warnings." % (warning_count,)
        sys.exit(1)
    else:
        if verbose:
            print "Finished with no warnings."
        sys.exit(0)

if __name__ == '__main__':
    main()


# vim:set ts=4 sw=4 sts=4 expandtab: