1# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""A utility to program Chrome OS devices' firmware using servo.
6
7This utility expects the DUT to be connected to a servo device. This allows us
8to put the DUT into the required state and to actually program the DUT's
9firmware using FTDI, USB and/or serial interfaces provided by servo.
10
11Servo state is preserved across the programming process.
12"""
13
14import glob
15import logging
16import os
17import re
18import site
19import time
20import xml.etree.ElementTree
21
22from autotest_lib.client.common_lib import error
23from autotest_lib.server.cros.faft.utils.config import Config as FAFTConfig
24
25
26# Number of seconds for program EC/BIOS to time out.
27FIRMWARE_PROGRAM_TIMEOUT_SEC = 1800
28
29class ProgrammerError(Exception):
30    """Local exception class wrapper."""
31    pass
32
33
34class _BaseProgrammer(object):
35    """Class implementing base programmer services.
36
37    Private attributes:
38      _servo: a servo object controlling the servo device
39      _servo_host: a host object running commands like 'flashrom'
40      _servo_prog_state: a tuple of strings of "<control>:<value>" pairs,
41                         listing servo controls and their required values for
42                         programming
43      _servo_prog_state_delay: time in second to wait after changing servo
44                               controls for programming.
45      _servo_saved_state: a list of the same elements as _servo_prog_state,
46                          those which need to be restored after programming
47      _program_cmd: a string, the shell command to run on the servo host
48                    to actually program the firmware. Dependent on
49                    firmware/hardware type, set by subclasses.
50    """
51
52    def __init__(self, servo, req_list, servo_host=None):
53        """Base constructor.
54        @param servo: a servo object controlling the servo device
55        @param req_list: a list of strings, names of the utilities required
56                         to be in the path for the programmer to succeed
57        @param servo_host: a host object to execute commands. Default to None,
58                           using the host object from the above servo object
59        """
60        self._servo = servo
61        self._servo_prog_state = ()
62        self._servo_prog_state_delay = 0
63        self._servo_saved_state = []
64        self._program_cmd = ''
65        self._servo_host = servo_host
66        if self._servo_host is None:
67            self._servo_host = self._servo._servo_host
68
69        try:
70            self._servo_host.run('which %s' % ' '.join(req_list))
71        except error.AutoservRunError:
72            # TODO: We turn this exception into a warn since the fw programmer
73            # is not working right now, and some systems do not package the
74            # required utilities its checking for.
75            # We should reinstate this exception once the programmer is working
76            # to indicate the missing utilities earlier in the test cycle.
77            # Bug chromium:371011 filed to track this.
78            logging.warn("Ignoring exception when verify required bins : %s",
79                         ' '.join(req_list))
80
81
82    def _set_servo_state(self):
83        """Set servo for programming, while saving the current state."""
84        logging.debug("Setting servo state for programming")
85        for item in self._servo_prog_state:
86            key, value = item.split(':')
87            try:
88                present = self._servo.get(key)
89            except error.TestFail:
90                logging.warn('Missing servo control: %s', key)
91                continue
92            if present == 'not_applicable':
93                # control is has no bearing in this servo config so ignore it.
94                logging.debug('Servo control %s is NA .. skipping', key)
95                continue
96            if present != value:
97                self._servo_saved_state.append('%s:%s' % (key, present))
98            self._servo.set(key, value)
99        time.sleep(self._servo_prog_state_delay)
100
101
102    def _restore_servo_state(self):
103        """Restore previously saved servo state."""
104        logging.debug("Restoring servo state after programming")
105        self._servo_saved_state.reverse()  # Do it in the reverse order.
106        for item in self._servo_saved_state:
107            key, value = item.split(':')
108            self._servo.set(key, value)
109
110
111    def program(self):
112        """Program the firmware as configured by a subclass."""
113        self._set_servo_state()
114        try:
115            logging.debug("Programmer command: %s", self._program_cmd)
116            self._servo_host.run(self._program_cmd,
117                                 timeout=FIRMWARE_PROGRAM_TIMEOUT_SEC)
118        finally:
119            self._restore_servo_state()
120
121
122class FlashromProgrammer(_BaseProgrammer):
123    """Class for programming AP flashrom."""
124
125    def __init__(self, servo, keep_ro=False):
126        """Configure required servo state.
127
128        @param servo: a servo object controlling the servo device
129        @param keep_ro: True to keep the RO portion unchanged
130        """
131        super(FlashromProgrammer, self).__init__(servo, ['flashrom',])
132        self._keep_ro = keep_ro
133        self._fw_path = None
134        self.init_section_paths('/tmp')
135        self._servo_version = self._servo.get_servo_version(active=True)
136        self._servo_serials = self._servo._server.get_servo_serials()
137
138
139    def init_section_paths(self, tmp_path):
140        """Update section paths to use the tmp directory"""
141        self._tmp_path = tmp_path
142        self._fw_main = os.path.join(self._tmp_path, 'fw_main')
143        self._wp_ro = os.path.join(self._tmp_path, 'wp_ro')
144        self._ro_vpd = os.path.join(self._tmp_path, 'ro_vpd')
145        self._rw_vpd = os.path.join(self._tmp_path, 'rw_vpd')
146        self._gbb = os.path.join(self._tmp_path, 'gbb')
147
148
149    def program(self):
150        """Program the firmware but preserve VPD and HWID."""
151        assert self._fw_path is not None
152        self._set_servo_state()
153        try:
154            wp_ro_section = [('WP_RO', self._wp_ro)]
155            rw_vpd_section = [('RW_VPD', self._rw_vpd)]
156            ro_vpd_section = [('RO_VPD', self._ro_vpd)]
157            gbb_section = [('GBB', self._gbb)]
158            if self._keep_ro:
159                # Keep the whole RO portion
160                preserved_sections = wp_ro_section + rw_vpd_section
161            else:
162                preserved_sections = ro_vpd_section + rw_vpd_section
163
164            servo_v2_programmer = 'ft2232_spi:type=google-servo-v2'
165            servo_v3_programmer = 'linux_spi'
166            servo_v4_with_micro_programmer = 'raiden_debug_spi'
167            servo_v4_with_ccd_programmer = 'raiden_debug_spi:target=AP'
168
169            if self._servo_version == 'servo_v2':
170                programmer = servo_v2_programmer
171                servo_serial = self._servo_serials.get('main')
172                if servo_serial:
173                    programmer += ',serial=%s' % servo_serial
174            elif self._servo_version == 'servo_v3':
175                programmer = servo_v3_programmer
176            elif self._servo_version == 'servo_v4_with_servo_micro':
177                # When a uServo is connected to a DUT with CCD support, the
178                # firmware programmer will always use the uServo to program.
179                servo_micro_serial = self._servo_serials.get('servo_micro')
180                programmer = servo_v4_with_micro_programmer
181                programmer += ':serial=%s' % servo_micro_serial
182            elif self._servo_version == 'servo_v4_with_ccd_cr50':
183                ccd_serial = self._servo_serials.get('ccd')
184                programmer = servo_v4_with_ccd_programmer
185                programmer += ',serial=%s' % ccd_serial
186            else:
187                raise Exception('Servo version %s is not supported.' %
188                                self._servo_version)
189            # Save needed sections from current firmware
190            for section in preserved_sections + gbb_section:
191                self._servo_host.run(' '.join([
192                    'flashrom', '-V', '-p', programmer, '-r',
193                    '-i', '%s:%s' % section]),
194                    timeout=FIRMWARE_PROGRAM_TIMEOUT_SEC)
195
196            # Pack the saved VPD into new firmware
197            self._servo_host.run('cp %s %s' % (self._fw_path, self._fw_main))
198            img_size = self._servo_host.run_output(
199                    "stat -c '%%s' %s" % self._fw_main)
200            pack_cmd = ['flashrom',
201                    '-p', 'dummy:image=%s,size=%s,emulate=VARIABLE_SIZE' % (
202                        self._fw_main, img_size),
203                    '-w', self._fw_main]
204            for section in preserved_sections:
205                pack_cmd.extend(['-i', '%s:%s' % section])
206            self._servo_host.run(' '.join(pack_cmd),
207                                 timeout=FIRMWARE_PROGRAM_TIMEOUT_SEC)
208
209            # HWID is inside the RO portion. Don't preserve HWID if we keep RO.
210            if not self._keep_ro:
211                # Read original HWID. The output format is:
212                #    hardware_id: RAMBI TEST A_A 0128
213                gbb_hwid_output = self._servo_host.run_output(
214                        'futility gbb -g --hwid %s' % self._gbb)
215                original_hwid = gbb_hwid_output.split(':', 1)[1].strip()
216
217                # Write HWID to new firmware
218                self._servo_host.run("futility gbb -s --hwid='%s' %s" %
219                        (original_hwid, self._fw_main))
220
221            # Flash the new firmware
222            self._servo_host.run(' '.join([
223                    'flashrom', '-V', '-p', programmer,
224                    '-w', self._fw_main]), timeout=FIRMWARE_PROGRAM_TIMEOUT_SEC)
225        finally:
226            self._servo.get_power_state_controller().reset()
227            self._restore_servo_state()
228
229
230    def prepare_programmer(self, path):
231        """Prepare programmer for programming.
232
233        @param path: a string, name of the file containing the firmware image.
234        """
235        self._fw_path = path
236        self.init_section_paths(os.path.dirname(path))
237
238        # If servo is running with servo v4, there may be two programming
239        # devices. Determine the programmer based on the active one.
240        self._servo_version = self._servo.get_servo_version(active=True)
241
242        # CCD takes care holding AP/EC. Don't need the following steps.
243        if self._servo_version != 'servo_v4_with_ccd_cr50':
244            faft_config = FAFTConfig(self._servo.get_board())
245            self._servo_prog_state_delay = faft_config.servo_prog_state_delay
246            self._servo_prog_state = (
247                'spi2_vref:%s' % faft_config.spi_voltage,
248                'spi2_buf_en:on',
249                'spi2_buf_on_flex_en:on',
250                'spi_hold:off',
251                'cold_reset:on',
252                'usbpd_reset:on',
253                )
254
255
256class FlashECProgrammer(_BaseProgrammer):
257    """Class for programming AP flashrom."""
258
259    def __init__(self, servo, host=None, ec_chip=None):
260        """Configure required servo state.
261
262        @param servo: a servo object controlling the servo device
263        @param host: a host object to execute commands. Default to None,
264                     using the host object from the above servo object, i.e.
265                     a servo host. A CrOS host object can be passed here
266                     such that it executes commands on the CrOS device.
267        @param ec_chip: a string of EC chip. Default to None, using the
268                        EC chip name reported by servo, the primary EC.
269                        Can pass a different chip name, for the case of
270                        the base EC.
271
272        """
273        super(FlashECProgrammer, self).__init__(servo, ['flash_ec'], host)
274        self._servo_version = self._servo.get_servo_version()
275        self._ec_chip = ec_chip
276
277    def prepare_programmer(self, image):
278        """Prepare programmer for programming.
279
280        @param image: string with the location of the image file
281        """
282        if self._ec_chip is None:
283            self._ec_chip = self._servo.get('ec_chip')
284
285        # If servo is running with servo v4, there may be two programming
286        # devices. Determine the programmer based on the active one.
287        self._servo_version = self._servo.get_servo_version(active=True)
288
289        # Get the port of servod. flash_ec may use it to talk to servod.
290        port = self._servo._servo_host.servo_port
291        self._program_cmd = ('flash_ec --chip=%s --image=%s --port=%d' %
292                             (self._ec_chip, image, port))
293        if self._ec_chip == 'stm32':
294            self._program_cmd += ' --bitbang_rate=57600'
295        self._program_cmd += ' --verify'
296        self._program_cmd += ' --verbose'
297
298
299class ProgrammerV2(object):
300    """Main programmer class which provides programmer for BIOS and EC with
301    servo V2."""
302
303    def __init__(self, servo):
304        self._servo = servo
305        self._valid_boards = self._get_valid_v2_boards()
306        self._bios_programmer = self._factory_bios(self._servo)
307        self._ec_programmer = self._factory_ec(self._servo)
308
309
310    @staticmethod
311    def _get_valid_v2_boards():
312        """Greps servod config files to look for valid v2 boards.
313
314        @return A list of valid board names.
315        """
316        site_packages_paths = site.getsitepackages()
317        SERVOD_CONFIG_DATA_DIR = None
318        for p in site_packages_paths:
319            servo_data_path = os.path.join(p, 'servo', 'data')
320            if os.path.exists(servo_data_path):
321                SERVOD_CONFIG_DATA_DIR = servo_data_path
322                break
323        if not SERVOD_CONFIG_DATA_DIR:
324            raise ProgrammerError(
325                    'Unable to locate data directory of Python servo module')
326        SERVOFLEX_V2_R0_P50_CONFIG = 'servoflex_v2_r0_p50.xml'
327        SERVO_CONFIG_GLOB = 'servo_*_overlay.xml'
328        SERVO_CONFIG_REGEXP = 'servo_(?P<board>.+)_overlay.xml'
329
330        def is_v2_compatible_board(board_config_path):
331            """Check if the given board config file is v2-compatible.
332
333            @param board_config_path: Path to a board config XML file.
334
335            @return True if the board is v2-compatible; False otherwise.
336            """
337            configs = []
338            def get_all_includes(config_path):
339                """Get all included XML config names in the given config file.
340
341                @param config_path: Path to a servo config file.
342                """
343                root = xml.etree.ElementTree.parse(config_path).getroot()
344                for element in root.findall('include'):
345                    include_name = element.find('name').text
346                    configs.append(include_name)
347                    get_all_includes(os.path.join(
348                            SERVOD_CONFIG_DATA_DIR, include_name))
349
350            get_all_includes(board_config_path)
351            return True if SERVOFLEX_V2_R0_P50_CONFIG in configs else False
352
353        result = []
354        board_overlays = glob.glob(
355                os.path.join(SERVOD_CONFIG_DATA_DIR, SERVO_CONFIG_GLOB))
356        for overlay_path in board_overlays:
357            if is_v2_compatible_board(overlay_path):
358                result.append(re.search(SERVO_CONFIG_REGEXP,
359                                        overlay_path).group('board'))
360        return result
361
362
363    def _get_flashrom_programmer(self, servo):
364        """Gets a proper flashrom programmer.
365
366        @param servo: A servo object.
367
368        @return A programmer for flashrom.
369        """
370        return FlashromProgrammer(servo)
371
372
373    def _factory_bios(self, servo):
374        """Instantiates and returns (bios, ec) programmers for the board.
375
376        @param servo: A servo object.
377
378        @return A programmer for ec. If the programmer is not supported
379            for the board, None will be returned.
380        """
381        _bios_prog = None
382        _board = servo.get_board()
383
384        logging.debug('Setting up BIOS programmer for board: %s', _board)
385        if _board in self._valid_boards:
386            _bios_prog = self._get_flashrom_programmer(servo)
387        else:
388            logging.warning('No BIOS programmer found for board: %s', _board)
389
390        return _bios_prog
391
392
393    def _factory_ec(self, servo):
394        """Instantiates and returns ec programmer for the board.
395
396        @param servo: A servo object.
397
398        @return A programmer for ec. If the programmer is not supported
399            for the board, None will be returned.
400        """
401        _ec_prog = None
402        _board = servo.get_board()
403
404        logging.debug('Setting up EC programmer for board: %s', _board)
405        if _board in self._valid_boards:
406            _ec_prog = FlashECProgrammer(servo)
407        else:
408            logging.warning('No EC programmer found for board: %s', _board)
409
410        return _ec_prog
411
412
413    def program_bios(self, image):
414        """Programs the DUT with provide bios image.
415
416        @param image: (required) location of bios image file.
417
418        """
419        self._bios_programmer.prepare_programmer(image)
420        self._bios_programmer.program()
421
422
423    def program_ec(self, image):
424        """Programs the DUT with provide ec image.
425
426        @param image: (required) location of ec image file.
427
428        """
429        self._ec_programmer.prepare_programmer(image)
430        self._ec_programmer.program()
431
432
433class ProgrammerV2RwOnly(ProgrammerV2):
434    """Main programmer class which provides programmer for only updating the RW
435    portion of BIOS with servo V2.
436
437    It does nothing on EC, as EC software sync on the next boot will
438    automatically overwrite the EC RW portion, using the EC RW image inside
439    the BIOS RW image.
440
441    """
442
443    def _get_flashrom_programmer(self, servo):
444        """Gets a proper flashrom programmer.
445
446        @param servo: A servo object.
447
448        @return A programmer for flashrom.
449        """
450        return FlashromProgrammer(servo, keep_ro=True)
451
452
453    def program_ec(self, image):
454        """Programs the DUT with provide ec image.
455
456        @param image: (required) location of ec image file.
457
458        """
459        # Do nothing. EC software sync will update the EC RW.
460        pass
461
462
463class ProgrammerV3(object):
464    """Main programmer class which provides programmer for BIOS and EC with
465    servo V3.
466
467    Different from programmer for servo v2, programmer for servo v3 does not
468    try to validate if the board can use servo V3 to update firmware. As long as
469    the servod process running in beagblebone with given board, the program will
470    attempt to flash bios and ec.
471
472    """
473
474    def __init__(self, servo):
475        self._servo = servo
476        self._bios_programmer = FlashromProgrammer(servo)
477        self._ec_programmer = FlashECProgrammer(servo)
478
479
480    def program_bios(self, image):
481        """Programs the DUT with provide bios image.
482
483        @param image: (required) location of bios image file.
484
485        """
486        self._bios_programmer.prepare_programmer(image)
487        self._bios_programmer.program()
488
489
490    def program_ec(self, image):
491        """Programs the DUT with provide ec image.
492
493        @param image: (required) location of ec image file.
494
495        """
496        self._ec_programmer.prepare_programmer(image)
497        self._ec_programmer.program()
498
499
500class ProgrammerV3RwOnly(ProgrammerV3):
501    """Main programmer class which provides programmer for only updating the RW
502    portion of BIOS with servo V3.
503
504    It does nothing on EC, as EC software sync on the next boot will
505    automatically overwrite the EC RW portion, using the EC RW image inside
506    the BIOS RW image.
507
508    """
509
510    def __init__(self, servo):
511        self._servo = servo
512        self._bios_programmer = FlashromProgrammer(servo, keep_ro=True)
513
514
515    def program_ec(self, image):
516        """Programs the DUT with provide ec image.
517
518        @param image: (required) location of ec image file.
519
520        """
521        # Do nothing. EC software sync will update the EC RW.
522        pass
523