| /* |
| * Copyright 2017 Google Inc. |
| * |
| * See file CREDITS for list of people who contributed to this |
| * project. |
| * |
| * 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 2 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. |
| */ |
| |
| #include <endian.h> |
| #include <libpayload.h> |
| |
| #include "base/container_of.h" |
| #include "drivers/ec/ps8751/ps8751.h" |
| |
| #define PS8751_DEBUG 0 |
| |
| #if (PS8751_DEBUG > 0) |
| #define debug(msg, args...) printf("%s: " msg, __func__, ##args) |
| #else |
| #define debug(msg, args...) |
| #endif |
| |
| #ifndef __must_check |
| #define __must_check __attribute__((warn_unused_result)) |
| #endif |
| |
| #define SLAVE0 (0x10 >> 1) /* a.k.a. "page 0" */ |
| #define SLAVE1 (0x12 >> 1) /* a.k.a. "page 1" */ |
| #define SLAVE2 (0x14 >> 1) /* a.k.a. "page 2" */ |
| #define I2C_MASTER (0x16 >> 1) /* a.k.a. "page 3" */ |
| |
| #define PS8751_P1_SPI_WP 0x4b |
| /* NOTE: on 8751 A3 silicon, P1_SPI_WP_EN reads back inverted! */ |
| #define PS8751_P1_SPI_WP_EN 0x10 /* WP enable bit */ |
| #define PS8805_P2_SPI_WP 0x2a |
| #define PS8805_P2_SPI_WP_EN 0x10 /* WP enable bit */ |
| #define P1_CHIP_REV_LO 0xf0 /* the 0x03 in "A3" */ |
| #define P1_CHIP_REV_HI 0xf1 /* the 0x0a in "A3" */ |
| #define P1_CHIP_ID_LO 0xf2 /* 0x50 */ |
| #define P1_CHIP_ID_HI 0xf3 /* 0x87 */ |
| |
| #define P2_ALERT_LOW 0x10 |
| #define P2_ALERT_HIGH 0x11 |
| #define P2_WR_FIFO 0x90 |
| #define P2_RD_FIFO 0x91 |
| #define P2_SPI_LEN 0x92 |
| #define P2_SPI_CTRL 0x93 |
| #define P2_SPI_CTRL_NOREAD 0x04 |
| #define P2_SPI_CTRL_FIFO_RESET 0x02 |
| #define P2_SPI_CTRL_TRIGGER 0x01 |
| #define P2_SPI_STATUS 0x9e |
| #define P2_CLK_CTRL 0xd6 |
| |
| #define P3_VENDOR_ID_LOW 0x00 |
| #define P3_I2C_DEBUG 0xa0 |
| #define P3_I2C_DEBUG_DEFAULT 0x31 |
| #define P3_I2C_DEBUG_ENABLE 0x30 |
| #define P3_I2C_DEBUG_DISABLE P3_I2C_DEBUG_DEFAULT |
| |
| /* |
| * bytes of SPI FIFO depth after command overhead |
| */ |
| |
| #define PS_FW_RD_CHUNK 16 |
| #define PS_FW_WR_CHUNK 12 |
| |
| #define SPI_CMD_WRITE_STATUS_REG 0x01 |
| #define SPI_CMD_PROG_PAGE 0x02 |
| #define SPI_CMD_READ_DATA 0x03 |
| #define SPI_CMD_WRITE_DISABLE 0x04 |
| #define SPI_CMD_READ_STATUS_REG 0x05 |
| #define SPI_CMD_WRITE_ENABLE 0x06 |
| |
| /* |
| * EN25F20: |
| * 64 x 4KB erase sectors |
| * 4 x 64KB erase blocks |
| * 1024 x 256B write pages |
| */ |
| |
| #define SPI_CMD_ERASE_SECTOR 0x20 /* sector erase, 4KB */ |
| #define SPI_CMD_READ_DEVICE_ID 0x90 |
| #define SPI_PAGE_SIZE (1 << 8) /* 256B, always 2^n */ |
| #define SPI_PAGE_MASK (SPI_PAGE_SIZE - 1) |
| |
| #define SPI_STATUS_WIP 0x01 |
| #define SPI_STATUS_WEL 0x02 |
| #define SPI_STATUS_BP 0x0c |
| #define SPI_STATUS_SRP 0x80 |
| |
| /* |
| * period of issuing reads on the I2C master to keep the chip awake |
| * 1 sec is OK |
| * 2 secs is too long |
| * chip goes to sleep when I2C is idle for 2 secs |
| */ |
| |
| #define USEC_TO_SEC(us) ((us) / 1000000) |
| #define USEC_TO_MSEC(us) ((us) / 1000) |
| #define PS_REFRESH_INTERVAL_US (500 * 1000) /* 500 ms */ |
| #define PS_SPI_TIMEOUT_US (1 * 1000 * 1000) /* 1s */ |
| #define PS_WIP_TIMEOUT_US (1 * 1000 * 1000) /* 1s */ |
| #define PS_MPU_BOOT_DELAY_MS (50) |
| #define PS_RESTART_DELAY_CS (6) /* 6cs / 60 ms */ |
| |
| #define PARADE_BINVERSION_OFFSET 0x501c |
| #define PARADE_CHIPVERSION_OFFSET 0x503a |
| |
| #if (PS8751_DEBUG >= 2) |
| #define PARADE_FW_START 0x38000 |
| #else |
| #define PARADE_FW_START 0x30000 |
| #endif |
| #define PARADE_FW_END 0x40000 |
| #define PARADE_FW_SECTOR 0x1000 /* erase sector size */ |
| |
| #define PARADE_TEST_FW_SIZE 0x1000 |
| |
| #define PARADE_VENDOR_ID 0x1DA0 |
| #define PARADE_PS8751_PRODUCT_ID 0x8751 |
| #define PARADE_PS8805_PRODUCT_ID 0x8805 |
| |
| enum ps8751_device_state { |
| PS8751_DEVICE_MISSING = -2, |
| PS8751_DEVICE_NOT_PARADE = -1, |
| PS8751_DEVICE_PRESENT = 0, |
| }; |
| |
| static const uint8_t erased_bytes[] = { |
| 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
| 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
| }; |
| _Static_assert(sizeof(erased_bytes) == PS_FW_RD_CHUNK, |
| "erased_bytes initializer size mismatch"); |
| |
| /** |
| * write a single byte to a register of an i2c target |
| * |
| * @param me device context |
| * @param slave i2c device address |
| * @param reg i2c register on target device |
| * @param data byte to write to register |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int __must_check write_reg(Ps8751 *me, |
| uint8_t slave, uint8_t reg, uint8_t data) |
| { |
| return i2c_writeb(&me->bus->ops, slave, reg, data); |
| } |
| |
| /** |
| * issue a series of i2c writes |
| * |
| * @param me device context |
| * @param cmds vector of i2c reg write commands |
| * @param count number of elements in ops vector |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int __must_check write_regs(Ps8751 *me, uint8_t chip, |
| const I2cWriteVec *cmds, const size_t count) |
| { |
| return i2c_write_regs(&me->bus->ops, chip, cmds, count); |
| } |
| |
| /** |
| * read a single byte from a register of a SPI target |
| * |
| * @param me device context |
| * @param slave i2c device address |
| * @param reg i2c register on target device |
| * @param data pointer to return byte |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int __must_check read_reg(Ps8751 *me, |
| uint8_t slave, uint8_t reg, uint8_t *data) |
| { |
| return i2c_readb(&me->bus->ops, slave, reg, data); |
| } |
| |
| /** |
| * issue a series of i2c reads |
| * |
| * @param me device context |
| * @param regs vector of i2c regs to read read |
| * @param count number of elements in regs array |
| * @param data pointer to read bytes |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int __must_check read_regs(Ps8751 *me, |
| uint8_t chip, |
| const uint8_t *regs, |
| const size_t count, |
| uint8_t *data) |
| { |
| return i2c_read_regs(&me->bus->ops, chip, regs, count, data); |
| } |
| |
| /** |
| * wait for SPI interface FIFOs to become ready |
| * this means the requested command bytes have been written and |
| * result bytes have been captured |
| * |
| * SPI bus timeout can happen if the SPI CLK isn't |
| * running as expected. |
| * |
| * @param me device context |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int __must_check ps8751_spi_fifo_wait_busy(Ps8751 *me) |
| { |
| uint8_t status; |
| uint64_t t0_us; |
| |
| t0_us = timer_us(0); |
| do { |
| if (read_reg(me, SLAVE2, P2_SPI_CTRL, &status) != 0) |
| return -1; |
| if (timer_us(t0_us) >= PS_SPI_TIMEOUT_US) { |
| printf("%s: SPI bus timeout after %ums\n", |
| me->chip_name, USEC_TO_MSEC(PS_SPI_TIMEOUT_US)); |
| return -1; |
| } |
| } while (status & P2_SPI_CTRL_TRIGGER); |
| return 0; |
| } |
| |
| /** |
| * reset the SPI interface FIFOs |
| * |
| * @param me device context |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int __must_check ps8751_spi_fifo_reset(Ps8751 *me) |
| { |
| if (write_reg(me, SLAVE2, P2_SPI_CTRL, P2_SPI_CTRL_FIFO_RESET) != 0) |
| return -1; |
| return 0; |
| } |
| |
| /** |
| * wake up the chip and enable all the i2c targets (i.e. "pages") |
| * - also reset SPI interface FIFOs for good measure |
| * |
| * @param me device context |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int __must_check ps8751_wake_i2c(Ps8751 *me) |
| { |
| int status; |
| uint8_t dummy; |
| |
| debug("call...\n"); |
| |
| status = read_reg(me, I2C_MASTER, P3_I2C_DEBUG, &dummy); |
| if (status != 0) { |
| /* wait for device to wake up... */ |
| mdelay(10); |
| } |
| |
| /* |
| * this enables 7 additional i2c slave addrs: |
| * 0x10, 0x12, 0x14, 0x18, 0x1a, 0x1c, 0x1f |
| */ |
| status = write_reg(me, I2C_MASTER, P3_I2C_DEBUG, P3_I2C_DEBUG_ENABLE); |
| if (status == 0) |
| status = ps8751_spi_fifo_reset(me); |
| if (status != 0) |
| printf("%s: chip did not wake up!\n", me->chip_name); |
| return status; |
| } |
| |
| /** |
| * turn off extra i2c targets (pages) |
| * |
| * @param me device context |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int __must_check ps8751_hide_i2c(Ps8751 *me) |
| { |
| int status; |
| uint8_t dummy; |
| |
| /* make sure chip is awake (is this needed?) */ |
| status = read_reg(me, I2C_MASTER, P3_I2C_DEBUG, &dummy); |
| if (status != 0) { |
| /* wait for device to wake up... */ |
| mdelay(10); |
| } |
| status = write_reg(me, I2C_MASTER, P3_I2C_DEBUG, P3_I2C_DEBUG_DISABLE); |
| return status; |
| } |
| |
| /** |
| * clear the ps8751 TCPCI alerts. after the MPU has been stopped, the |
| * TCPCI regs at slave0 (0x10) need to be used instead of i2c_master |
| * (0x16). |
| * |
| * @param me device context |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int __must_check ps8751_clear_alerts(Ps8751 *me) |
| { |
| const I2cWriteVec am[] = { |
| { P2_ALERT_LOW, 0xff }, |
| { P2_ALERT_HIGH, 0xff }, |
| }; |
| /* yes, SLAVE0 */ |
| return write_regs(me, SLAVE0, am, ARRAY_SIZE(am)); |
| } |
| |
| /* |
| * stop the MPU, reset SPI clock |
| * |
| * if the MPU isn't running properly (i.e. corrupted flash), |
| * we need to disable both clocks, then re-enable the SPI clock |
| * to be able to access the SPI FIFO to recover the chip. |
| * |
| * ? disabling the MPU may keep the chip awake! |
| */ |
| |
| static int __must_check ps8751_disable_mpu(Ps8751 *me) |
| { |
| /* turn off SPI|MPU clocks */ |
| if (write_reg(me, SLAVE2, P2_CLK_CTRL, 0xc0) != 0) |
| return -1; |
| /* SPI clock on, MPU clock stays off */ |
| if (write_reg(me, SLAVE2, P2_CLK_CTRL, 0x40) != 0) |
| return -1; |
| /* clear residual alerts */ |
| if (ps8751_clear_alerts(me) != 0) |
| return -1; |
| return 0; |
| } |
| |
| /** |
| * re-enable the MPU clock and reboot it |
| * |
| * SPI clock stays on |
| * we tune MUX_DP_EQ_CONFIGURATION when initializing the chip from the ec |
| * parade confirmed this tuning is preserved across this reboot |
| * |
| * the chip needs about 50ms to come out of reset, so we'll assume it |
| * takes a similar amount of time for the MPU to boot up. |
| * |
| * @param me device context |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int __must_check ps8751_enable_mpu(Ps8751 *me) |
| { |
| /* SPI|MPU clk on */ |
| if (write_reg(me, SLAVE2, P2_CLK_CTRL, 0x00) != 0) |
| return -1; |
| mdelay(PS_MPU_BOOT_DELAY_MS); |
| if (ps8751_wake_i2c(me) != 0) |
| return -1; |
| return 0; |
| } |
| |
| /** |
| * send a SPI write-enable cmd |
| * |
| * WRITE_ENABLE must be issued before every |
| * PROGRAM, ERASE, WRITE_STATUS_REGISTER command |
| * |
| * @param me device context |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int __must_check ps8751_spi_cmd_enable_writes(Ps8751 *me) |
| { |
| static const I2cWriteVec we[] = { |
| { P2_WR_FIFO, SPI_CMD_WRITE_ENABLE }, |
| { P2_SPI_LEN, 0x00 }, |
| { P2_SPI_CTRL, P2_SPI_CTRL_NOREAD|P2_SPI_CTRL_TRIGGER }, |
| }; |
| |
| if (write_regs(me, SLAVE2, we, ARRAY_SIZE(we)) != 0) |
| return -1; |
| if (ps8751_spi_fifo_wait_busy(me) != 0) |
| return -1; |
| return 0; |
| } |
| |
| /* |
| * wait for "erase/program command finished" as seen by the ps8751 |
| */ |
| |
| static int __must_check ps8751_spi_wait_prog_cmd(Ps8751 *me) |
| { |
| uint8_t busy; |
| uint64_t t0_us; |
| |
| t0_us = timer_us(0); |
| do { |
| if (read_reg(me, SLAVE2, P2_SPI_STATUS, &busy) != 0) |
| return -1; |
| if ((busy & 0x3f) == 0x00) { |
| /* {chip,sector} erase, program cmd finished */ |
| return 0; |
| } |
| } while (timer_us(t0_us) < PS_WIP_TIMEOUT_US); |
| printf("%s: flash prog/erase timeout after %ums\n", |
| me->chip_name, USEC_TO_MSEC(PS_SPI_TIMEOUT_US)); |
| return -1; |
| } |
| |
| /** |
| * read SPI flash status register |
| * |
| * @param me device context |
| * @return status if ok, -1 on error |
| */ |
| |
| static int __must_check ps8751_spi_cmd_read_status(Ps8751 *me, uint8_t *status) |
| { |
| static const I2cWriteVec rs[] = { |
| { P2_WR_FIFO, SPI_CMD_READ_STATUS_REG }, |
| { P2_SPI_LEN, 0x00 }, |
| { P2_SPI_CTRL, P2_SPI_CTRL_TRIGGER }, |
| }; |
| |
| if (write_regs(me, SLAVE2, rs, ARRAY_SIZE(rs)) != 0) |
| return -1; |
| if (ps8751_spi_fifo_wait_busy(me) != 0) |
| return -1; |
| if (read_reg(me, SLAVE2, P2_RD_FIFO, status) != 0) |
| return -1; |
| return 0; |
| } |
| |
| /** |
| * wait for SPI flash status WIP (write-in-progress) bit to clear |
| * this is needed after write-status-reg, any prog, any erase command |
| * |
| * @param me device context |
| * @return status if ok, -1 on error |
| */ |
| |
| static int __must_check ps8751_spi_wait_wip(Ps8751 *me) |
| { |
| uint8_t status; |
| uint64_t t0_us; |
| |
| t0_us = timer_us(0); |
| do { |
| if (ps8751_spi_cmd_read_status(me, &status) != 0) |
| return -1; |
| if (timer_us(t0_us) >= PS_WIP_TIMEOUT_US) { |
| printf("%s: WIP timeout after %ums\n", |
| me->chip_name, USEC_TO_MSEC(PS_WIP_TIMEOUT_US)); |
| return -1; |
| } |
| } while (status & SPI_STATUS_WIP); |
| return 0; |
| } |
| |
| /** |
| * write SPI flash status register |
| * |
| * @param me device context |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int __must_check ps8751_spi_cmd_write_status(Ps8751 *me, uint8_t val) |
| { |
| if (ps8751_spi_cmd_enable_writes(me) < 0) |
| return -1; |
| |
| const I2cWriteVec ws[] = { |
| { P2_WR_FIFO, SPI_CMD_WRITE_STATUS_REG }, |
| { P2_WR_FIFO, val }, |
| { P2_SPI_LEN, 0x01 }, |
| { P2_SPI_CTRL, P2_SPI_CTRL_NOREAD|P2_SPI_CTRL_TRIGGER }, |
| }; |
| |
| if (write_regs(me, SLAVE2, ws, ARRAY_SIZE(ws)) != 0) |
| return -1; |
| if (ps8751_spi_fifo_wait_busy(me) != 0) |
| return -1; |
| if (ps8751_spi_wait_wip(me) != 0) |
| return -1; |
| return 0; |
| } |
| |
| /* |
| * lock the SPI flash |
| * |
| * @param me device context |
| * @return 0 if ok, -1 on error |
| * |
| * NOTE: keep in sync with ps8751_spi_flash_unlock() |
| */ |
| |
| static int __must_check ps8751_spi_flash_lock(Ps8751 *me) |
| { |
| int status = 0; |
| uint8_t slave; |
| uint8_t wp_reg; |
| uint8_t wp_en; |
| |
| if (ps8751_spi_cmd_write_status(me, SPI_STATUS_SRP|SPI_STATUS_BP) != 0) |
| status = -1; |
| |
| switch (me->chip_type) { |
| case CHIP_PS8751: |
| slave = SLAVE1; |
| wp_reg = PS8751_P1_SPI_WP; |
| wp_en = PS8751_P1_SPI_WP_EN; |
| break; |
| case CHIP_PS8805: |
| slave = SLAVE2; |
| wp_reg = PS8805_P2_SPI_WP; |
| wp_en = PS8805_P2_SPI_WP_EN; |
| break; |
| default: |
| return -1; |
| } |
| /* assert SPI flash WP# */ |
| if (write_reg(me, slave, wp_reg, wp_en) != 0) |
| status = -1; |
| |
| return status; |
| } |
| |
| /* |
| * reset SPI bus cmd FIFOs and unlock the SPI flash... |
| * |
| * @param me device context |
| * |
| * NOTE: call ps8751_disable_mpu() before this so we have |
| * a functional SPI bus. |
| */ |
| |
| static int __must_check ps8751_spi_flash_unlock(Ps8751 *me) |
| { |
| uint8_t status; |
| uint8_t slave; |
| uint8_t wp_reg; |
| |
| if (ps8751_spi_fifo_reset(me) != 0) |
| return -1; |
| |
| switch (me->chip_type) { |
| case CHIP_PS8751: |
| slave = SLAVE1; |
| wp_reg = PS8751_P1_SPI_WP; |
| break; |
| case CHIP_PS8805: |
| slave = SLAVE2; |
| wp_reg = PS8805_P2_SPI_WP; |
| break; |
| default: |
| return -1; |
| } |
| /* deassert SPI flash WP# */ |
| if (write_reg(me, slave, wp_reg, 0x00) != 0) |
| return -1; |
| |
| /* clear the SRP, BP bits */ |
| if (ps8751_spi_cmd_write_status(me, 0x00) != 0) |
| return -1; |
| |
| if (ps8751_spi_cmd_read_status(me, &status) != 0) |
| return -1; |
| |
| if ((status & (SPI_STATUS_SRP|SPI_STATUS_BP)) != 0) { |
| printf("%s: could not clear flash status " |
| "SRP|BP (0x%02x)\n", me->chip_name, status); |
| return -1; |
| } |
| return 0; |
| } |
| |
| /* |
| * wait for "erase/program command finished" as seen by the ps8751 |
| * then, wait for the WIP (write-in-progress) bit to clear on the |
| * flash part itself. |
| * |
| * @param me device context |
| */ |
| |
| static int __must_check ps8751_spi_wait_rom_ready(Ps8751 *me) |
| { |
| if (ps8751_spi_wait_prog_cmd(me) != 0) |
| return -1; |
| if (ps8751_spi_wait_wip(me) != 0) |
| return -1; |
| return 0; |
| } |
| |
| /** |
| * query the flash ID and see if we support it |
| * |
| * @param me device context |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int __must_check ps8751_spi_flash_identify(Ps8751 *me) |
| { |
| uint8_t buf[2] = {0, 0}; |
| uint16_t flash_id; |
| |
| static const I2cWriteVec read_id[] = { |
| { P2_WR_FIFO, SPI_CMD_READ_DEVICE_ID }, |
| { P2_WR_FIFO, 0x00 }, |
| { P2_WR_FIFO, 0x00 }, |
| { P2_WR_FIFO, 0x00 }, |
| { P2_SPI_LEN, 0x13 }, |
| { P2_SPI_CTRL, P2_SPI_CTRL_TRIGGER }, |
| }; |
| if (write_regs(me, SLAVE2, read_id, ARRAY_SIZE(read_id)) != 0) |
| return -1; |
| if (ps8751_spi_fifo_wait_busy(me) != 0) |
| return -1; |
| |
| static const uint8_t get_id[ARRAY_SIZE(buf)] = { |
| P2_RD_FIFO, |
| P2_RD_FIFO, |
| }; |
| if (read_regs(me, SLAVE2, get_id, ARRAY_SIZE(buf), buf) != 0) |
| return -1; |
| flash_id = be16dec(buf); |
| |
| printf("%s: found SPI flash ID 0x%04x\n", me->chip_name, flash_id); |
| |
| /* |
| * these devices must use |
| * SPI_CMD_ERASE_SECTOR of 0x20 with a 4KB erase block |
| * list extracted from parade reference code |
| * (PS8751_PanelConfig.py) |
| */ |
| switch (flash_id) { |
| case 0x1c11: /* "EN25F20" */ |
| case 0xbf43: /* "25LF020A" */ |
| case 0xef11: /* "W25X20" */ |
| case 0xef15: /* "W25X32" */ |
| case 0xc211: /* "25L2005" */ |
| case 0xc205: /* "25L512" */ |
| case 0x1f44: /* "25DF041A" */ |
| case 0x1f43: /* "25DF021A" */ |
| break; |
| default: |
| printf("%s: SPI flash ID 0x%04x not recognized\n", |
| me->chip_name, flash_id); |
| return -1; |
| } |
| return 0; |
| } |
| |
| /** |
| * access the chip periodically so it doesn't fall asleep. a simple |
| * i2c read every second or so from I2C_MASTER is sufficient. |
| * |
| * @param me device context |
| * @param deadline pointer to time of next needed access |
| * use deadline of 0 for first call |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int __must_check ps8751_keep_awake(Ps8751 *me, uint64_t *deadline) |
| { |
| uint64_t now_us = timer_us(0); |
| uint8_t dummy; |
| |
| if (now_us >= *deadline) { |
| *deadline = now_us + PS_REFRESH_INTERVAL_US; |
| int status = read_reg(me, I2C_MASTER, P3_I2C_DEBUG, &dummy); |
| if (status != 0) { |
| printf("%s: chip dozed off!\n", me->chip_name); |
| return -1; |
| } |
| } |
| return 0; |
| } |
| |
| /** |
| * get chip hardware version. result of 0xa3 means it's an A3 chip. |
| * |
| * @param me device context |
| * @param version pointer to result version byte |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int __must_check ps8751_get_hw_version(Ps8751 *me, uint8_t *version) |
| { |
| int status; |
| uint8_t low; |
| uint8_t high; |
| |
| status = read_reg(me, SLAVE1, P1_CHIP_REV_LO, &low); |
| if (status == 0) |
| status = read_reg(me, SLAVE1, P1_CHIP_REV_HI, &high); |
| if (status < 0) { |
| printf("%s: read P1_CHIP_REV_* failed\n", me->chip_name); |
| return status; |
| } |
| *version = (high << 4) | low; |
| return 0; |
| } |
| |
| static int is_corrupted_tcpc(const struct ec_response_pd_chip_info *const info) |
| { |
| return info->vendor_id == 0 && info->product_id == 0; |
| } |
| |
| static int is_parade_chip(const struct ec_response_pd_chip_info *const info, |
| const enum ParadeChipType chip) |
| { |
| if (info->vendor_id != PARADE_VENDOR_ID) |
| return 0; |
| |
| switch (chip) { |
| case CHIP_PS8751: |
| return info->product_id == PARADE_PS8751_PRODUCT_ID; |
| case CHIP_PS8805: |
| return info->product_id == PARADE_PS8805_PRODUCT_ID; |
| default: |
| printf("Unknown Parade chip type: 0x%x\n", info->product_id); |
| return 0; |
| } |
| } |
| |
| /** |
| * capture chip (vendor, product, device, rev) IDs |
| * |
| * @param me device context |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static enum ps8751_device_state __must_check ps8751_capture_device_id( |
| Ps8751 *me, int renew) |
| { |
| struct ec_params_pd_chip_info p; |
| struct ec_response_pd_chip_info r; |
| |
| if (me->chip.vendor != 0 && !renew) |
| return PS8751_DEVICE_PRESENT; |
| |
| p.port = me->ec_pd_id; |
| p.renew = renew; |
| int status = ec_command(me->bus->ec, EC_CMD_PD_CHIP_INFO, 0, |
| &p, sizeof(p), &r, sizeof(r)); |
| if (status < 0) { |
| printf("%s: could not get chip info!\n", me->chip_name); |
| return PS8751_DEVICE_MISSING; |
| } |
| |
| uint16_t vendor = r.vendor_id; |
| uint16_t product = r.product_id; |
| uint16_t device = r.device_id; |
| uint8_t fw_rev = r.fw_version_number; |
| |
| printf("%s: vendor 0x%04x product 0x%04x " |
| "device 0x%04x fw_rev 0x%02x\n", |
| me->chip_name, vendor, product, device, fw_rev); |
| if (is_corrupted_tcpc(&r)) { |
| /* vendor 0 likely due to "missing/corrupted" firmware */ |
| printf("%s: MCU must be down!\n", me->chip_name); |
| } else if (!is_parade_chip(&r, me->chip_type)) { |
| return PS8751_DEVICE_NOT_PARADE; |
| } |
| |
| me->chip.vendor = vendor; |
| me->chip.product = product; |
| me->chip.device = device; |
| me->chip.fw_rev = fw_rev; |
| return PS8751_DEVICE_PRESENT; |
| } |
| |
| /** |
| * extract the chip version compatibility info from a firmware blob |
| * |
| * @fw_blob firmware blob |
| * @return chip version (e.g. 0xa3 is an A3 chip) |
| */ |
| |
| static uint8_t __must_check ps8751_blob_hw_version(const uint8_t *fw_blob) |
| { |
| char buf[3]; |
| |
| buf[0] = fw_blob[PARADE_CHIPVERSION_OFFSET]; |
| buf[1] = fw_blob[PARADE_CHIPVERSION_OFFSET + 1]; |
| buf[2] = '\0'; |
| return strtol(buf, 0, 16); |
| } |
| |
| /** |
| * determine if a firmware blob is compatible with the hardware |
| * |
| * @param me device context |
| * @param fw proposed firmware blob |
| * @return compatibility status (boolean) |
| */ |
| |
| static int __must_check ps8751_is_fw_compatible(Ps8751 *me, const uint8_t *fw) |
| { |
| uint8_t hw_rev; |
| uint8_t fw_chip_version; |
| |
| if (ps8751_get_hw_version(me, &hw_rev) < 0) |
| return 0; |
| |
| switch (me->chip_type) { |
| case CHIP_PS8751: |
| fw_chip_version = ps8751_blob_hw_version(fw); |
| break; |
| case CHIP_PS8805: |
| fw_chip_version = me->blob_hw_version; |
| break; |
| default: |
| return 0; |
| } |
| if (hw_rev == fw_chip_version) |
| return 1; |
| printf("%s: chip rev 0x%02x but firmware for 0x%02x\n", |
| me->chip_name, hw_rev, fw_chip_version); |
| return 0; |
| } |
| |
| /** |
| * write a SPI command plus a 24 bit addr to the SPI interface FIFO |
| * |
| * @param me device context |
| * @cmd SPI command to issue |
| * @param a24 SPI command address bits (24) |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int __must_check ps8751_spi_setup_cmd24(Ps8751 *me, |
| uint8_t cmd, uint32_t a24) |
| { |
| const I2cWriteVec sa[] = { |
| { P2_WR_FIFO, cmd }, |
| { P2_WR_FIFO, a24 >> 16 }, |
| { P2_WR_FIFO, a24 >> 8 }, |
| { P2_WR_FIFO, a24 }, |
| }; |
| return write_regs(me, SLAVE2, sa, ARRAY_SIZE(sa)); |
| } |
| |
| /** |
| * issue a single flash sector erase command to |
| * erase PARADE_FW_SECTOR (4KB) bytes. |
| * |
| * @param me device context |
| * @param offset device byte offset, but containing |
| * sector is erased |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int __must_check ps8751_sector_erase(Ps8751 *me, uint32_t offset) |
| { |
| if (ps8751_spi_cmd_enable_writes(me) != 0) |
| return -1; |
| if (ps8751_spi_setup_cmd24(me, SPI_CMD_ERASE_SECTOR, offset) != 0) |
| return -1; |
| |
| static const I2cWriteVec se[] = { |
| { P2_SPI_LEN, 0x03 }, |
| { P2_SPI_CTRL, P2_SPI_CTRL_NOREAD|P2_SPI_CTRL_TRIGGER }, |
| }; |
| if (write_regs(me, SLAVE2, se, ARRAY_SIZE(se)) != 0) |
| return -1; |
| if (ps8751_spi_fifo_wait_busy(me) != 0) |
| return -1; |
| if (ps8751_spi_wait_rom_ready(me) != 0) |
| return -1; |
| return 0; |
| } |
| |
| /** |
| * erase a chunk of flash. offset and data_size must be sector aligned, |
| * which is PARADE_FW_SECTOR (4KB). |
| * |
| * assumes SPI interface has been enabled for programming. |
| * |
| * @param me device context |
| * @param offset device offset, 1st byte to erase |
| * @param data_size number of bytes to erase |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int __must_check ps8751_erase(Ps8751 *me, |
| uint32_t offset, uint32_t data_size) |
| { |
| uint32_t end = offset + data_size; |
| int rval = 0; |
| uint64_t t0_us; |
| |
| debug("offset 0x%06x size %u\n", offset, data_size); |
| |
| t0_us = timer_us(0); |
| for (; offset < end; offset += PARADE_FW_SECTOR) { |
| if (ps8751_sector_erase(me, offset) != VBERROR_SUCCESS) |
| rval = -1; |
| } |
| printf("%s: erased %uKB in %ums\n", |
| me->chip_name, |
| data_size >> 10, |
| (unsigned)USEC_TO_MSEC(timer_us(t0_us))); |
| return rval; |
| } |
| |
| /** |
| * program flash with new data |
| * |
| * flash is assumed to be erased |
| * the MPU is assumed to be stopped (highly recommended) |
| * the SPI bus and flash are write-enabled |
| * |
| * @param me device context |
| * @param fw_start flash device offset to program |
| * @param data addr of data to write |
| * @param data_size size of data to write |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int __must_check ps8751_program(Ps8751 *me, |
| const uint32_t fw_start, |
| const uint8_t *data, int data_size) |
| { |
| uint32_t data_offset; |
| uint64_t t0_us; |
| int chunk; |
| |
| printf("%s: programming %uKB...\n", me->chip_name, data_size >> 10); |
| |
| t0_us = timer_us(0); |
| for (data_offset = 0; |
| data_offset < data_size; |
| data_offset += chunk) { |
| chunk = MIN(PS_FW_WR_CHUNK, data_size - data_offset); |
| /* clip at flash page boundary */ |
| int page_offset = (fw_start + data_offset) & SPI_PAGE_MASK; |
| chunk = MIN(chunk, SPI_PAGE_SIZE - page_offset); |
| |
| if (ps8751_spi_cmd_enable_writes(me) != 0) |
| return -1; |
| |
| if (ps8751_spi_setup_cmd24(me, |
| SPI_CMD_PROG_PAGE, |
| fw_start + data_offset) != 0) { |
| return -1; |
| } |
| for (int i = 0; i < chunk; ++i) { |
| if (write_reg(me, SLAVE2, |
| P2_WR_FIFO, data[data_offset + i]) != 0) |
| return -1; |
| } |
| |
| const I2cWriteVec wr[] = { |
| { P2_SPI_LEN, (4 + chunk - 1) }, |
| { P2_SPI_CTRL, |
| P2_SPI_CTRL_NOREAD|P2_SPI_CTRL_TRIGGER }, |
| }; |
| if (write_regs(me, SLAVE2, wr, ARRAY_SIZE(wr)) != 0) |
| return -1; |
| if (ps8751_spi_fifo_wait_busy(me) != 0) |
| return -1; |
| if (ps8751_spi_wait_rom_ready(me) != 0) |
| return -1; |
| } |
| printf("%s: programmed %uKB in %us\n", |
| me->chip_name, |
| data_size >> 10, |
| (unsigned)USEC_TO_SEC(timer_us(t0_us))); |
| return 0; |
| } |
| |
| /** |
| * verify flash content matches given data |
| * |
| * @param me device context |
| * @param fw_start flash device offset to verify |
| * @param data addr of data to match |
| * @param data_size size of data to match |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int __must_check ps8751_verify(Ps8751 *me, |
| const uint32_t fw_addr, |
| const uint8_t *data, size_t data_size) |
| { |
| uint64_t deadline = 0; |
| uint8_t readback; |
| uint64_t t0_us; |
| uint32_t data_offset; |
| int chunk; |
| |
| debug("offset 0x%06x size %u\n", fw_addr, data_size); |
| |
| t0_us = timer_us(0); |
| for (data_offset = 0; |
| data_offset < data_size; |
| data_offset += chunk) { |
| chunk = MIN(PS_FW_RD_CHUNK, data_size - data_offset); |
| |
| if (ps8751_keep_awake(me, &deadline) != 0) |
| return -1; |
| if (ps8751_spi_setup_cmd24(me, SPI_CMD_READ_DATA, |
| fw_addr + data_offset) != 0) { |
| return -1; |
| } |
| |
| const I2cWriteVec rd[] = { |
| { P2_SPI_LEN, |
| ((chunk - 1) << 4) | (4 - 1) }, |
| { P2_SPI_CTRL, P2_SPI_CTRL_TRIGGER }, |
| }; |
| if (write_regs(me, SLAVE2, rd, ARRAY_SIZE(rd)) != 0) |
| return -1; |
| if (ps8751_spi_fifo_wait_busy(me) != 0) |
| return -1; |
| for (int i = 0; i < chunk; ++i) { |
| if (read_reg(me, SLAVE2, P2_RD_FIFO, &readback) != 0) |
| return -1; |
| if (readback != data[data_offset + i]) { |
| printf("%s: mismatch at offset 0x%06x " |
| "0x%02x != 0x%02x (expected)\n", |
| me->chip_name, |
| fw_addr + data_offset + i, |
| readback, data[data_offset + i]); |
| return -1; |
| } |
| } |
| } |
| printf("%s: verified %zuKB in %us\n", |
| me->chip_name, |
| data_size >> 10, |
| (unsigned)USEC_TO_SEC(timer_us(t0_us))); |
| return 0; |
| } |
| |
| static VbError_t ps8751_ec_tunnel_status(const VbootAuxFwOps *vbaux, |
| int *protected) |
| { |
| Ps8751 *const me = container_of(vbaux, Ps8751, fw_ops); |
| |
| if (cros_ec_tunnel_i2c_protect_status(me->bus, protected) < 0) { |
| printf("%s: could not get EC I2C tunnel status!\n", |
| me->chip_name); |
| return VBERROR_UNKNOWN; |
| } |
| return VBERROR_SUCCESS; |
| } |
| |
| static int ps8751_ec_pd_suspend(Ps8751 *me) |
| { |
| int status; |
| |
| status = cros_ec_pd_control(me->ec_pd_id, PD_SUSPEND); |
| if (status == -EC_RES_BUSY) |
| printf("%s: PD_SUSPEND busy! Could be only power source.\n", |
| me->chip_name); |
| else if (status != 0) |
| printf("%s: PD_SUSPEND failed!\n", me->chip_name); |
| return status; |
| } |
| |
| static int ps8751_ec_pd_resume(Ps8751 *me) |
| { |
| int status; |
| |
| status = cros_ec_pd_control(me->ec_pd_id, PD_RESUME); |
| if (status != 0) |
| printf("%s: PD_RESUME failed!\n", me->chip_name); |
| return status; |
| } |
| |
| static void ps8751_dump_flash(Ps8751 *me, |
| const uint32_t offset_start, |
| const uint32_t size) |
| { |
| printf("================================" |
| "================================\n{\n"); |
| |
| uint32_t offset_end = offset_start + size; |
| uint8_t prev_buf[PS_FW_RD_CHUNK]; |
| uint8_t buf[PS_FW_RD_CHUNK]; |
| uint64_t deadline = 0; |
| uint32_t offset; |
| int dot3 = 0; |
| int i; |
| |
| for (offset = offset_start; |
| offset < offset_end; |
| offset += sizeof(buf)) { |
| if (ps8751_keep_awake(me, &deadline) != 0) |
| break; |
| |
| /* construct a SPI read PS_FW_CHUNK bytes @ offset command */ |
| if (ps8751_spi_setup_cmd24(me, |
| SPI_CMD_READ_DATA, offset) != 0) { |
| debug("could not set up addr\n"); |
| break; |
| } |
| |
| static const I2cWriteVec trig[] = { |
| { P2_SPI_LEN, |
| ((PS_FW_RD_CHUNK - 1) << 4) | (4 - 1) }, |
| { P2_SPI_CTRL, P2_SPI_CTRL_TRIGGER }, |
| }; |
| if (write_regs(me, SLAVE2, trig, ARRAY_SIZE(trig)) != 0) { |
| debug("could not issue SPI_CTRL\n"); |
| break; |
| } |
| if (ps8751_spi_fifo_wait_busy(me) != 0) |
| return; |
| |
| for (i = 0; i < sizeof(buf); ++i) { |
| if (read_reg(me, SLAVE2, P2_RD_FIFO, &buf[i]) != 0) |
| break; |
| } |
| if (i != sizeof(buf)) |
| break; |
| |
| if (offset > offset_start && |
| memcmp(buf, prev_buf, sizeof(buf)) == 0) { |
| if (!dot3) { |
| printf("...\n"); |
| dot3 = 1; |
| } |
| } else { |
| memcpy(prev_buf, buf, sizeof(prev_buf)); |
| if (offset < offset_start + 0x1000) { |
| printf("0x%06x: ", offset); |
| for (i = 0; i < sizeof(buf); ++i) |
| printf(" %02x", buf[i]); |
| printf("\n"); |
| } else { |
| printf("."); |
| } |
| dot3 = 0; |
| } |
| } |
| |
| printf("\n}\n================================" |
| "================================\n"); |
| } |
| |
| /** |
| * install a new firmware image, replacing whatever was there before, |
| * then verify installed data. |
| * |
| * the MPU is assumed to be stopped (highly recommended) |
| * the SPI bus and flash are write-enabled |
| * |
| * @param me device context |
| * @param data addr of firmware blob to install |
| * @param data_size size of firmware blob to install |
| * @return 0 if ok, -1 on error |
| */ |
| |
| static int ps8751_reflash(Ps8751 *me, const uint8_t *data, size_t data_size) |
| { |
| int status; |
| |
| debug("data %8p len %u\n", data, data_size); |
| |
| status = ps8751_erase(me, PARADE_FW_START, data_size); |
| if (status != 0) { |
| printf("%s: chip erase failed\n", me->chip_name); |
| return -1; |
| } |
| /* |
| * quick sanity check to see if we modified flash |
| * we'll do a full verify after programming |
| */ |
| status = ps8751_verify(me, PARADE_FW_START, |
| erased_bytes, |
| MIN(data_size, sizeof(erased_bytes))); |
| if (status != 0) { |
| printf("%s: chip erase verify failed\n", me->chip_name); |
| return -1; |
| } |
| |
| if (PS8751_DEBUG > 0) { |
| debug("start post erase 7s delay...\n"); |
| mdelay(7 * 1000); |
| debug("end post erase delay\n"); |
| } |
| |
| if (PS8751_DEBUG >= 2) |
| ps8751_dump_flash(me, PARADE_FW_START, |
| PARADE_FW_END - PARADE_FW_START); |
| status = ps8751_program(me, PARADE_FW_START, data, data_size); |
| if (PS8751_DEBUG >= 2) |
| ps8751_dump_flash(me, PARADE_FW_START, PARADE_TEST_FW_SIZE); |
| if (status != 0) { |
| printf("%s: chip program failed\n", me->chip_name); |
| return -1; |
| } |
| |
| return ps8751_verify(me, PARADE_FW_START, data, data_size); |
| } |
| |
| /* |
| * reading the firmware takes about 15-20 secs, so we'll just use the |
| * firmware rev as a trivial hash. |
| */ |
| |
| static VbError_t ps8751_check_hash(const VbootAuxFwOps *vbaux, |
| const uint8_t *hash, size_t hash_size, |
| VbAuxFwUpdateSeverity_t *severity) |
| { |
| Ps8751 *me = container_of(vbaux, Ps8751, fw_ops); |
| enum ps8751_device_state status; |
| |
| debug("call...\n"); |
| |
| if (hash_size != 2) { |
| debug("hash_size %u unexpected\n", hash_size); |
| return VBERROR_INVALID_PARAMETER; |
| } |
| |
| status = ps8751_capture_device_id(me, 0); |
| if (status == PS8751_DEVICE_MISSING) { |
| *severity = VB_AUX_FW_NO_DEVICE; |
| printf("Skipping upgrade for %s\n", me->chip_name); |
| return VBERROR_SUCCESS; |
| } else if (status == PS8751_DEVICE_NOT_PARADE) { |
| *severity = VB_AUX_FW_NO_UPDATE; |
| printf("No update required for %s\n", me->chip_name); |
| return VBERROR_SUCCESS; |
| } |
| |
| me->blob_hw_version = hash[0]; |
| if (hash[1] == me->chip.fw_rev) |
| *severity = VB_AUX_FW_NO_UPDATE; |
| else |
| *severity = VB_AUX_FW_SLOW_UPDATE; |
| return VBERROR_SUCCESS; |
| } |
| |
| static int ps8751_halt_and_flash(Ps8751 *me, |
| const uint8_t *image, size_t image_size) |
| { |
| int status = -1; |
| |
| if (ps8751_disable_mpu(me) != 0) |
| return -1; |
| if (ps8751_spi_flash_unlock(me) != 0) |
| goto enable_mpu; |
| debug("unlock_spi_bus returned\n"); |
| if (ps8751_spi_flash_identify(me) == 0 && |
| ps8751_reflash(me, image, image_size) == 0) |
| status = 0; |
| |
| if (ps8751_spi_flash_lock(me) != 0) |
| status = -1; |
| |
| enable_mpu: |
| if (ps8751_enable_mpu(me) != 0) |
| return -1; |
| return status; |
| } |
| |
| static VbError_t ps8751_update_image(const VbootAuxFwOps *vbaux, |
| const uint8_t *image, size_t image_size) |
| { |
| Ps8751 *me = container_of(vbaux, Ps8751, fw_ops); |
| VbError_t status = VBERROR_UNKNOWN; |
| int protected; |
| int timeout; |
| |
| debug("call...\n"); |
| |
| if (ps8751_ec_tunnel_status(vbaux, &protected) != 0) |
| return VBERROR_UNKNOWN; |
| if (protected) |
| return VBERROR_EC_REBOOT_TO_RO_REQUIRED; |
| |
| if (image == NULL || image_size == 0) |
| return VBERROR_INVALID_PARAMETER; |
| |
| switch (ps8751_ec_pd_suspend(me)) { |
| case -EC_RES_BUSY: |
| return VBERROR_PERIPHERAL_BUSY; |
| case EC_RES_SUCCESS: |
| /* Continue onward */ |
| break; |
| default: |
| return VBERROR_UNKNOWN; |
| } |
| |
| if (ps8751_wake_i2c(me) != 0) |
| goto pd_resume; |
| |
| if (!ps8751_is_fw_compatible(me, image)) |
| goto hide_i2c; |
| if (ps8751_halt_and_flash(me, image, image_size) == 0) |
| status = VBERROR_SUCCESS; |
| |
| hide_i2c: |
| if (ps8751_hide_i2c(me) != 0) |
| status = VBERROR_UNKNOWN; |
| |
| pd_resume: |
| if (ps8751_ec_pd_resume(me) != 0) |
| status = VBERROR_UNKNOWN; |
| |
| /* Wait at most ~60ms for reset to occur. */ |
| timeout = PS_RESTART_DELAY_CS; |
| do { |
| if (ps8751_capture_device_id(me, 1) == PS8751_DEVICE_PRESENT) |
| break; |
| |
| mdelay(10); |
| timeout--; |
| } while (timeout > 0); |
| |
| if (timeout == 0) |
| status = VBERROR_UNKNOWN; |
| |
| return status; |
| } |
| |
| static const VbootAuxFwOps ps8751_fw_ops = { |
| .fw_image_name = "ps8751_a3.bin", |
| .fw_hash_name = "ps8751_a3.hash", |
| .check_hash = ps8751_check_hash, |
| .update_image = ps8751_update_image, |
| }; |
| |
| static const VbootAuxFwOps ps8751_fw_canary_ops = { |
| .fw_image_name = "ps8751_a3_canary.bin", |
| .fw_hash_name = "ps8751_a3_canary.hash", |
| .check_hash = ps8751_check_hash, |
| .update_image = ps8751_update_image, |
| }; |
| |
| static const VbootAuxFwOps ps8805_fw_ops = { |
| .fw_image_name = "ps8805_a2.bin", |
| .fw_hash_name = "ps8805_a2.hash", |
| .check_hash = ps8751_check_hash, |
| .update_image = ps8751_update_image, |
| }; |
| |
| Ps8751 *new_ps8751(CrosECTunnelI2c *bus, int ec_pd_id) |
| { |
| Ps8751 *me = xzalloc(sizeof(*me)); |
| |
| me->bus = bus; |
| me->ec_pd_id = ec_pd_id; |
| me->fw_ops = ps8751_fw_ops; |
| me->chip_type = CHIP_PS8751; |
| snprintf(me->chip_name, sizeof(me->chip_name), "ps8751.%d", ec_pd_id); |
| |
| return me; |
| } |
| |
| Ps8751 *new_ps8751_canary(CrosECTunnelI2c *bus, int ec_pd_id) |
| { |
| Ps8751 *me = xzalloc(sizeof(*me)); |
| |
| me->bus = bus; |
| me->ec_pd_id = ec_pd_id; |
| me->fw_ops = ps8751_fw_canary_ops; |
| me->chip_type = CHIP_PS8751; |
| snprintf(me->chip_name, sizeof(me->chip_name), "ps8751.%d", ec_pd_id); |
| |
| return me; |
| } |
| |
| Ps8751 *new_ps8805(CrosECTunnelI2c *bus, int ec_pd_id) |
| { |
| Ps8751 *me = xzalloc(sizeof(*me)); |
| |
| me->bus = bus; |
| me->ec_pd_id = ec_pd_id; |
| me->fw_ops = ps8805_fw_ops; |
| me->chip_type = CHIP_PS8805; |
| snprintf(me->chip_name, sizeof(me->chip_name), "ps8805.%d", ec_pd_id); |
| |
| return me; |
| } |