# Copyright (c) 2014 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """A utility to program Chrome OS devices' firmware using servo. This utility expects the DUT to be connected to a servo device. This allows us to put the DUT into the required state and to actually program the DUT's firmware using FTDI, USB and/or serial interfaces provided by servo. Servo state is preserved across the programming process. """ import glob import logging import os import re import site import time import xml.etree.ElementTree from autotest_lib.client.common_lib import error from autotest_lib.server.cros.faft.utils.config import Config as FAFTConfig # Number of seconds for program EC/BIOS to time out. FIRMWARE_PROGRAM_TIMEOUT_SEC = 1800 class ProgrammerError(Exception): """Local exception class wrapper.""" pass class _BaseProgrammer(object): """Class implementing base programmer services. Private attributes: _servo: a servo object controlling the servo device _servo_host: a host object running commands like 'flashrom' _servo_prog_state: a tuple of strings of ":" pairs, listing servo controls and their required values for programming _servo_prog_state_delay: time in second to wait after changing servo controls for programming. _servo_saved_state: a list of the same elements as _servo_prog_state, those which need to be restored after programming _program_cmd: a string, the shell command to run on the servo host to actually program the firmware. Dependent on firmware/hardware type, set by subclasses. """ def __init__(self, servo, req_list, servo_host=None): """Base constructor. @param servo: a servo object controlling the servo device @param req_list: a list of strings, names of the utilities required to be in the path for the programmer to succeed @param servo_host: a host object to execute commands. Default to None, using the host object from the above servo object """ self._servo = servo self._servo_prog_state = () self._servo_prog_state_delay = 0 self._servo_saved_state = [] self._program_cmd = '' self._servo_host = servo_host if self._servo_host is None: self._servo_host = self._servo._servo_host try: self._servo_host.run('which %s' % ' '.join(req_list)) except error.AutoservRunError: # TODO: We turn this exception into a warn since the fw programmer # is not working right now, and some systems do not package the # required utilities its checking for. # We should reinstate this exception once the programmer is working # to indicate the missing utilities earlier in the test cycle. # Bug chromium:371011 filed to track this. logging.warn("Ignoring exception when verify required bins : %s", ' '.join(req_list)) def _set_servo_state(self): """Set servo for programming, while saving the current state.""" logging.debug("Setting servo state for programming") for item in self._servo_prog_state: key, value = item.split(':') try: present = self._servo.get(key) except error.TestFail: logging.warn('Missing servo control: %s', key) continue if present == 'not_applicable': # control is has no bearing in this servo config so ignore it. logging.debug('Servo control %s is NA .. skipping', key) continue if present != value: self._servo_saved_state.append('%s:%s' % (key, present)) self._servo.set(key, value) time.sleep(self._servo_prog_state_delay) def _restore_servo_state(self): """Restore previously saved servo state.""" logging.debug("Restoring servo state after programming") self._servo_saved_state.reverse() # Do it in the reverse order. for item in self._servo_saved_state: key, value = item.split(':') self._servo.set(key, value) def program(self): """Program the firmware as configured by a subclass.""" self._set_servo_state() try: logging.debug("Programmer command: %s", self._program_cmd) self._servo_host.run(self._program_cmd, timeout=FIRMWARE_PROGRAM_TIMEOUT_SEC) finally: self._restore_servo_state() class FlashromProgrammer(_BaseProgrammer): """Class for programming AP flashrom.""" def __init__(self, servo, keep_ro=False): """Configure required servo state. @param servo: a servo object controlling the servo device @param keep_ro: True to keep the RO portion unchanged """ super(FlashromProgrammer, self).__init__(servo, ['flashrom',]) self._keep_ro = keep_ro self._fw_path = None self.init_section_paths('/tmp') self._servo_version = self._servo.get_servo_version(active=True) self._servo_serials = self._servo._server.get_servo_serials() def init_section_paths(self, tmp_path): """Update section paths to use the tmp directory""" self._tmp_path = tmp_path self._fw_main = os.path.join(self._tmp_path, 'fw_main') self._wp_ro = os.path.join(self._tmp_path, 'wp_ro') self._ro_vpd = os.path.join(self._tmp_path, 'ro_vpd') self._rw_vpd = os.path.join(self._tmp_path, 'rw_vpd') self._gbb = os.path.join(self._tmp_path, 'gbb') def program(self): """Program the firmware but preserve VPD and HWID.""" assert self._fw_path is not None self._set_servo_state() try: wp_ro_section = [('WP_RO', self._wp_ro)] rw_vpd_section = [('RW_VPD', self._rw_vpd)] ro_vpd_section = [('RO_VPD', self._ro_vpd)] gbb_section = [('GBB', self._gbb)] if self._keep_ro: # Keep the whole RO portion preserved_sections = wp_ro_section + rw_vpd_section else: preserved_sections = ro_vpd_section + rw_vpd_section servo_v2_programmer = 'ft2232_spi:type=google-servo-v2' servo_v3_programmer = 'linux_spi' servo_v4_with_micro_programmer = 'raiden_debug_spi' servo_v4_with_ccd_programmer = 'raiden_debug_spi:target=AP' if self._servo_version == 'servo_v2': programmer = servo_v2_programmer servo_serial = self._servo_serials.get('main') if servo_serial: programmer += ',serial=%s' % servo_serial elif self._servo_version == 'servo_v3': programmer = servo_v3_programmer elif self._servo_version == 'servo_v4_with_servo_micro': # When a uServo is connected to a DUT with CCD support, the # firmware programmer will always use the uServo to program. servo_micro_serial = self._servo_serials.get('servo_micro') programmer = servo_v4_with_micro_programmer programmer += ':serial=%s' % servo_micro_serial elif self._servo_version == 'servo_v4_with_ccd_cr50': ccd_serial = self._servo_serials.get('ccd') programmer = servo_v4_with_ccd_programmer programmer += ',serial=%s' % ccd_serial else: raise Exception('Servo version %s is not supported.' % self._servo_version) # Save needed sections from current firmware for section in preserved_sections + gbb_section: self._servo_host.run(' '.join([ 'flashrom', '-V', '-p', programmer, '-r', '-i', '%s:%s' % section]), timeout=FIRMWARE_PROGRAM_TIMEOUT_SEC) # Pack the saved VPD into new firmware self._servo_host.run('cp %s %s' % (self._fw_path, self._fw_main)) img_size = self._servo_host.run_output( "stat -c '%%s' %s" % self._fw_main) pack_cmd = ['flashrom', '-p', 'dummy:image=%s,size=%s,emulate=VARIABLE_SIZE' % ( self._fw_main, img_size), '-w', self._fw_main] for section in preserved_sections: pack_cmd.extend(['-i', '%s:%s' % section]) self._servo_host.run(' '.join(pack_cmd), timeout=FIRMWARE_PROGRAM_TIMEOUT_SEC) # HWID is inside the RO portion. Don't preserve HWID if we keep RO. if not self._keep_ro: # Read original HWID. The output format is: # hardware_id: RAMBI TEST A_A 0128 gbb_hwid_output = self._servo_host.run_output( 'futility gbb -g --hwid %s' % self._gbb) original_hwid = gbb_hwid_output.split(':', 1)[1].strip() # Write HWID to new firmware self._servo_host.run("futility gbb -s --hwid='%s' %s" % (original_hwid, self._fw_main)) # Flash the new firmware self._servo_host.run(' '.join([ 'flashrom', '-V', '-p', programmer, '-w', self._fw_main]), timeout=FIRMWARE_PROGRAM_TIMEOUT_SEC) finally: self._servo.get_power_state_controller().reset() self._restore_servo_state() def prepare_programmer(self, path): """Prepare programmer for programming. @param path: a string, name of the file containing the firmware image. """ self._fw_path = path self.init_section_paths(os.path.dirname(path)) # If servo is running with servo v4, there may be two programming # devices. Determine the programmer based on the active one. self._servo_version = self._servo.get_servo_version(active=True) # CCD takes care holding AP/EC. Don't need the following steps. if self._servo_version != 'servo_v4_with_ccd_cr50': faft_config = FAFTConfig(self._servo.get_board()) self._servo_prog_state_delay = faft_config.servo_prog_state_delay self._servo_prog_state = ( 'spi2_vref:%s' % faft_config.spi_voltage, 'spi2_buf_en:on', 'spi2_buf_on_flex_en:on', 'spi_hold:off', 'cold_reset:on', 'usbpd_reset:on', ) class FlashECProgrammer(_BaseProgrammer): """Class for programming AP flashrom.""" def __init__(self, servo, host=None, ec_chip=None): """Configure required servo state. @param servo: a servo object controlling the servo device @param host: a host object to execute commands. Default to None, using the host object from the above servo object, i.e. a servo host. A CrOS host object can be passed here such that it executes commands on the CrOS device. @param ec_chip: a string of EC chip. Default to None, using the EC chip name reported by servo, the primary EC. Can pass a different chip name, for the case of the base EC. """ super(FlashECProgrammer, self).__init__(servo, ['flash_ec'], host) self._servo_version = self._servo.get_servo_version() self._ec_chip = ec_chip def prepare_programmer(self, image): """Prepare programmer for programming. @param image: string with the location of the image file """ if self._ec_chip is None: self._ec_chip = self._servo.get('ec_chip') # If servo is running with servo v4, there may be two programming # devices. Determine the programmer based on the active one. self._servo_version = self._servo.get_servo_version(active=True) # Get the port of servod. flash_ec may use it to talk to servod. port = self._servo._servo_host.servo_port self._program_cmd = ('flash_ec --chip=%s --image=%s --port=%d' % (self._ec_chip, image, port)) if self._ec_chip == 'stm32': self._program_cmd += ' --bitbang_rate=57600' self._program_cmd += ' --verify' self._program_cmd += ' --verbose' class ProgrammerV2(object): """Main programmer class which provides programmer for BIOS and EC with servo V2.""" def __init__(self, servo): self._servo = servo self._valid_boards = self._get_valid_v2_boards() self._bios_programmer = self._factory_bios(self._servo) self._ec_programmer = self._factory_ec(self._servo) @staticmethod def _get_valid_v2_boards(): """Greps servod config files to look for valid v2 boards. @return A list of valid board names. """ site_packages_paths = site.getsitepackages() SERVOD_CONFIG_DATA_DIR = None for p in site_packages_paths: servo_data_path = os.path.join(p, 'servo', 'data') if os.path.exists(servo_data_path): SERVOD_CONFIG_DATA_DIR = servo_data_path break if not SERVOD_CONFIG_DATA_DIR: raise ProgrammerError( 'Unable to locate data directory of Python servo module') SERVOFLEX_V2_R0_P50_CONFIG = 'servoflex_v2_r0_p50.xml' SERVO_CONFIG_GLOB = 'servo_*_overlay.xml' SERVO_CONFIG_REGEXP = 'servo_(?P.+)_overlay.xml' def is_v2_compatible_board(board_config_path): """Check if the given board config file is v2-compatible. @param board_config_path: Path to a board config XML file. @return True if the board is v2-compatible; False otherwise. """ configs = [] def get_all_includes(config_path): """Get all included XML config names in the given config file. @param config_path: Path to a servo config file. """ root = xml.etree.ElementTree.parse(config_path).getroot() for element in root.findall('include'): include_name = element.find('name').text configs.append(include_name) get_all_includes(os.path.join( SERVOD_CONFIG_DATA_DIR, include_name)) get_all_includes(board_config_path) return True if SERVOFLEX_V2_R0_P50_CONFIG in configs else False result = [] board_overlays = glob.glob( os.path.join(SERVOD_CONFIG_DATA_DIR, SERVO_CONFIG_GLOB)) for overlay_path in board_overlays: if is_v2_compatible_board(overlay_path): result.append(re.search(SERVO_CONFIG_REGEXP, overlay_path).group('board')) return result def _get_flashrom_programmer(self, servo): """Gets a proper flashrom programmer. @param servo: A servo object. @return A programmer for flashrom. """ return FlashromProgrammer(servo) def _factory_bios(self, servo): """Instantiates and returns (bios, ec) programmers for the board. @param servo: A servo object. @return A programmer for ec. If the programmer is not supported for the board, None will be returned. """ _bios_prog = None _board = servo.get_board() logging.debug('Setting up BIOS programmer for board: %s', _board) if _board in self._valid_boards: _bios_prog = self._get_flashrom_programmer(servo) else: logging.warning('No BIOS programmer found for board: %s', _board) return _bios_prog def _factory_ec(self, servo): """Instantiates and returns ec programmer for the board. @param servo: A servo object. @return A programmer for ec. If the programmer is not supported for the board, None will be returned. """ _ec_prog = None _board = servo.get_board() logging.debug('Setting up EC programmer for board: %s', _board) if _board in self._valid_boards: _ec_prog = FlashECProgrammer(servo) else: logging.warning('No EC programmer found for board: %s', _board) return _ec_prog def program_bios(self, image): """Programs the DUT with provide bios image. @param image: (required) location of bios image file. """ self._bios_programmer.prepare_programmer(image) self._bios_programmer.program() def program_ec(self, image): """Programs the DUT with provide ec image. @param image: (required) location of ec image file. """ self._ec_programmer.prepare_programmer(image) self._ec_programmer.program() class ProgrammerV2RwOnly(ProgrammerV2): """Main programmer class which provides programmer for only updating the RW portion of BIOS with servo V2. It does nothing on EC, as EC software sync on the next boot will automatically overwrite the EC RW portion, using the EC RW image inside the BIOS RW image. """ def _get_flashrom_programmer(self, servo): """Gets a proper flashrom programmer. @param servo: A servo object. @return A programmer for flashrom. """ return FlashromProgrammer(servo, keep_ro=True) def program_ec(self, image): """Programs the DUT with provide ec image. @param image: (required) location of ec image file. """ # Do nothing. EC software sync will update the EC RW. pass class ProgrammerV3(object): """Main programmer class which provides programmer for BIOS and EC with servo V3. Different from programmer for servo v2, programmer for servo v3 does not try to validate if the board can use servo V3 to update firmware. As long as the servod process running in beagblebone with given board, the program will attempt to flash bios and ec. """ def __init__(self, servo): self._servo = servo self._bios_programmer = FlashromProgrammer(servo) self._ec_programmer = FlashECProgrammer(servo) def program_bios(self, image): """Programs the DUT with provide bios image. @param image: (required) location of bios image file. """ self._bios_programmer.prepare_programmer(image) self._bios_programmer.program() def program_ec(self, image): """Programs the DUT with provide ec image. @param image: (required) location of ec image file. """ self._ec_programmer.prepare_programmer(image) self._ec_programmer.program() class ProgrammerV3RwOnly(ProgrammerV3): """Main programmer class which provides programmer for only updating the RW portion of BIOS with servo V3. It does nothing on EC, as EC software sync on the next boot will automatically overwrite the EC RW portion, using the EC RW image inside the BIOS RW image. """ def __init__(self, servo): self._servo = servo self._bios_programmer = FlashromProgrammer(servo, keep_ro=True) def program_ec(self, image): """Programs the DUT with provide ec image. @param image: (required) location of ec image file. """ # Do nothing. EC software sync will update the EC RW. pass