| /* SPDX-License-Identifier: GPL-2.0 */ |
| /* |
| * Copyright (C) 2023, Google Inc |
| * |
| * MAX77779 firmware updater |
| */ |
| |
| #include <linux/i2c.h> |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/mutex.h> |
| #include <linux/device.h> |
| #include <linux/firmware.h> |
| #include <linux/platform_device.h> |
| #include <linux/of_platform.h> |
| #include <linux/regmap.h> |
| #include <linux/debugfs.h> |
| |
| #include "google_bms.h" |
| #include "max77779_regs.h" |
| #include "max77779.h" |
| #include "maxfg_common.h" |
| #include "max77779_charger.h" |
| |
| #define MAX77779_FIRMWARE_BINARY_PREFIX "batt_fw_adi_79" |
| #define MAX77779_REASON_FIRMWARE "FW_UPDATE" |
| |
| #define FW_UPDATE_RETRY_CPU_RESET 100 |
| #define FW_UPDATE_RETRY_FW_UPDATE 1000 |
| #define FW_UPDATE_RETRY_RISCV_REBOOT 20 |
| #define FW_UPDATE_RETRY_ONCE 1 |
| #define FW_UPDATE_WAIT_INTERVAL_MS 50 |
| #define FW_UPDATE_WAIT_LOAD_BIN_MS 50 |
| #define FW_UPDATE_TIMER_CHECK_INTERVAL_MS 1000 |
| #define FW_UPDATE_CONDITION_CHECK_INTERVAL_MS (60 * 1000) |
| |
| #define FW_UPDATE_MAXIMUM_PAGE_SIZE (PAGE_SIZE*10) |
| |
| /* b/308445917: adding device tree for voltage threshold in micro volts */ |
| #define MAX77779_FW_UPDATE_MIN_VOLTAGE 4000000 |
| |
| #define MAX77779_FW_IMG_SZ_HEADER 8 |
| #define MAX77779_FW_IMG_SZ_PACKET 42 |
| #define MAX77779_FW_IMG_SZ_FRAME (MAX77779_FW_IMG_SZ_PACKET * 20) |
| |
| |
| #define MAX77779_FG_SECUPDATE_STATUS_REG 0x6F |
| #define MAX77779_FG_SECUPDATE_STATUS_SUCCESS 0x03 |
| |
| /* PMIC.0x62 can be set 0xFF if previous firmware update fails */ |
| #define MAX77779_FW_INVALID_FW_VER 0xFF |
| |
| #define MAX77779_REV_PASS_1_5 0x1 |
| #define MAX77779_REV_PASS_2_0 0x2 |
| |
| #define MAX77779_REV_PASS_1_5_FIRMWARE 2 |
| #define MAX77779_REV_PASS_2_0_FIRMWARE 3 |
| |
| /* vimon's memory mapped to 0x80 */ |
| #define MAX77779_VIMON_MEM_BASE_ADDR 0x80 |
| #define MAX77779_VIMON_PG_SIZE 256 |
| #define MAX77779_VIMON_PG3_SIZE (MAX77779_VIMON_PG_SIZE - 32) |
| |
| #define MAX77779_OFFSET_VER_MAJOR 7 |
| #define MAX77779_OFFSET_VER_MINOR 6 |
| |
| #define MAX77779_FW_UPDATE_STRING_MAX 32 |
| |
| #define MAX77779_FW_UPDATE_RETRY_MAX 10 |
| |
| #define MAX77779_GET_DATA_FRAME_SIZE(filelen) \ |
| (filelen - MAX77779_FW_IMG_SZ_HEADER - 3*MAX77779_FW_IMG_SZ_PACKET) |
| |
| #define MAX77779_ABORT_ON_ERROR(result, name, err_op) { \ |
| if (result) { \ |
| dev_err(fwu->dev, "[%s] failed: %s (%d)\n", name, err_op, result); \ |
| return result; \ |
| } \ |
| } |
| |
| #define MARK_IN_PROGRESS() do {} while (0); |
| |
| #define MAX77779_FW_HIST_OFFSET_TAG 16 |
| #define MAX77779_FW_HIST_VER_MASK 0xFFFF |
| |
| |
| enum max77779_fwupdate_fg_lock { |
| FG_ST_LOCK_ALL_SECTION = 0x0e, |
| FG_ST_UNLOCK_ALL_SECTION = 0x00, |
| }; |
| |
| enum max77779_fg_operation_status { |
| FGST_NOT_CACHED = 0x01, |
| FGST_FWUPDATE = 0x02, |
| FGST_BASEFW = 0x03, |
| FGST_ERR_READTAG = 0x10, |
| FGST_NORMAL = 0xff, |
| }; |
| |
| enum max77779_fwupdate_intr { |
| MAX77779_INTR_CLEAR = 0x00, |
| MAX77779_INTR_SESSION_START = 0x70, |
| MAX77779_INTR_TRANSFER_FRAMES = 0x72, |
| MAX77779_INTR_APP_VALID = 0x77, |
| MAX77779_INTR_SESSION_END = 0x74, |
| }; |
| |
| enum max77779_fwupdate_rsp_code { |
| MAX77779_RSP_CODE_OK = 0x00, |
| MAX77779_RSP_CODE_UNEXPECTED = 0xF0, |
| MAX77779_RSP_CODE_CMD_SEC_FAIL = 0xF1, |
| MAX77779_RSP_CODE_INVALID_PARAM = 0xF4, |
| MAX77779_RSP_CODE_NOT_READY = 0xFF, |
| }; |
| |
| enum max77779_fwupdate_cmd { |
| MAX77779_CMD_CLEAR_ALL = 0x00, |
| MAX77779_CMD_REBOOT_FG = 0x0F, |
| MAX77779_CMD_REBOOT_RISCV = 0x080F, |
| }; |
| |
| enum max77779_fwupdate_status { |
| MAX77779_FWU_OK = 0x0, |
| MAX77779_FWU_RUNNING_UPDATE, |
| MAX77779_FWU_REG_ACCESS_ERR, |
| MAX77779_FWU_UPDATE_FAIL, |
| MAX77779_FWU_BOOT_ERR, |
| MAX77779_FWU_TIMER_ERR, |
| }; |
| |
| enum max77779_fwupdate_err_code { |
| MAX77779_FWU_ERR_NONE = 0, |
| MAX77779_FWU_ERR_PREPARE, |
| MAX77779_FWU_ERR_DATA_TRANSFER, |
| MAX77779_FWU_ERR_POST_STATUS_CHECK, |
| }; |
| |
| struct max77779_version_info { |
| u8 major; |
| u8 minor; |
| }; |
| |
| /* saved as GBMS_TAG_FWSF */ |
| struct max77779_fwupdate_stats { |
| s16 count; |
| s8 success; |
| s8 fail; |
| }__attribute__((packed)); |
| |
| struct max77779_fwupdate_custom_data { |
| ssize_t size; |
| char* data; |
| }; |
| |
| struct max77779_fwupdate_info { |
| int new_tag; |
| int retry_cnt; |
| bool force_update; |
| bool reboot_on_failure; |
| }; |
| |
| struct max77779_fwupdate { |
| struct device *dev; |
| struct dentry *de; |
| |
| struct delayed_work update_work; |
| |
| struct device *pmic; |
| struct device *fg; |
| struct device *vimon; |
| struct device *chg; |
| |
| struct platform_device *batt; |
| |
| bool can_update; |
| bool running_update; |
| struct max77779_fwupdate_info update_info; |
| |
| struct max77779_version_info v_cur; |
| struct max77779_version_info v_new; |
| |
| char fw_name[MAX77779_FW_UPDATE_STRING_MAX]; |
| |
| size_t data_frame_size; |
| u32 crc_val; |
| |
| u8 op_st; |
| |
| u8* scratch_buffer; |
| u8* zero_filled_buffer; |
| |
| int minimum_voltage; |
| |
| struct max77779_version_info minimum; |
| |
| struct max77779_fwupdate_custom_data debug_image; |
| |
| struct wakeup_source *fwupdate_wake_lock; |
| struct mutex status_lock; |
| |
| struct max77779_fwupdate_stats stats; |
| |
| struct logbuffer *lb; |
| |
| struct gvotable_election *mode_votable; |
| }; |
| |
| /* Defined at max77779_fg.c */ |
| int max77779_external_fg_reg_write_nolock(struct device *, uint16_t, uint16_t); |
| |
| static int get_firmware_update_tag(const struct max77779_fwupdate *fwu) |
| { |
| int ret, ver_tag = 0; |
| uint32_t fw_tag = 0, cur_ver; |
| |
| ret = gbms_storage_read(GBMS_TAG_FWHI, &fw_tag, sizeof(fw_tag)); |
| if (ret < 0) |
| gbms_logbuffer_prlog(fwu->lb, LOGLEVEL_WARNING, 0, LOGLEVEL_INFO, |
| "failed to read GBMS_TAG_FWHI (%d)\n", ret); |
| |
| cur_ver = (fwu->v_cur.major << 8) | fwu->v_cur.minor; |
| |
| if ((fw_tag & MAX77779_FW_HIST_VER_MASK) == cur_ver) |
| ver_tag = (fw_tag >> MAX77779_FW_HIST_OFFSET_TAG); |
| |
| return ver_tag; |
| } |
| |
| static void set_firmware_update_tag(const struct max77779_fwupdate *fwu, int tag) |
| { |
| int ret; |
| uint32_t fw_tag; |
| |
| fw_tag = (tag << MAX77779_FW_HIST_OFFSET_TAG); |
| fw_tag |= (fwu->v_cur.major << 8) | fwu->v_cur.minor; |
| |
| ret= gbms_storage_write(GBMS_TAG_FWHI, &fw_tag, sizeof(fw_tag)); |
| if (ret < 0) |
| gbms_logbuffer_prlog(fwu->lb, LOGLEVEL_WARNING, 0, LOGLEVEL_INFO, |
| "failed to write GBMS_TAG_FWHI (%d)\n", ret); |
| } |
| |
| static inline void read_fwupdate_stats(struct max77779_fwupdate *fwu) |
| { |
| int ret; |
| |
| ret = gbms_storage_read(GBMS_TAG_FWSF, &fwu->stats, sizeof(fwu->stats)); |
| if (ret < 0) |
| gbms_logbuffer_prlog(fwu->lb, LOGLEVEL_WARNING, 0, LOGLEVEL_INFO, |
| "failed to read GBMS_TAG_FWSF (%d)\n", ret); |
| |
| if (ret < 0 || fwu->stats.count < 0 || fwu->stats.success < 0 || fwu->stats.fail < 0 |
| || fwu->stats.count != fwu->stats.success + fwu->stats.fail) { |
| /* invalid stats values. reset all counter to 0 */ |
| fwu->stats.count = 0; |
| fwu->stats.success = 0; |
| fwu->stats.fail = 0; |
| } |
| } |
| |
| static inline void update_fwupdate_stats(struct max77779_fwupdate *fwu, |
| struct max77779_fwupdate_stats* stats) |
| { |
| int ret; |
| |
| ret = gbms_storage_write(GBMS_TAG_FWSF, stats, sizeof(*stats)); |
| if (ret < 0) |
| gbms_logbuffer_prlog(fwu->lb, LOGLEVEL_WARNING, 0, LOGLEVEL_INFO, |
| "failed to write GBMS_TAG_FWSF (%d)\n", ret); |
| } |
| |
| /* TODO: b/326472325 it needs to handle counter reaches MAX77779_FW_UPDATE_RETRY_MAX */ |
| static inline int max77779_schedule_update(struct max77779_fwupdate* fwu) |
| { |
| int ret = -1; |
| |
| mutex_lock(&fwu->status_lock); |
| |
| if (fwu->update_info.retry_cnt < MAX77779_FW_UPDATE_RETRY_MAX) { |
| fwu->update_info.retry_cnt++; |
| ret = 0; |
| } |
| |
| mutex_unlock(&fwu->status_lock); |
| |
| if (ret == 0) { |
| dev_info(fwu->dev, "will schedule firmware update for [%s]\n", fwu->fw_name); |
| schedule_delayed_work(&fwu->update_work, |
| msecs_to_jiffies(FW_UPDATE_CONDITION_CHECK_INTERVAL_MS)); |
| } |
| |
| return ret; |
| } |
| |
| static int max77779_fwupdate_init(struct max77779_fwupdate *fwu) |
| { |
| struct device* dev = fwu->dev; |
| struct device_node *dn; |
| int val = 0; |
| |
| if (!dev) |
| return -EINVAL; |
| |
| fwu->update_info.force_update = false; |
| fwu->running_update = false; |
| fwu->minimum_voltage = MAX77779_FW_UPDATE_MIN_VOLTAGE; |
| |
| fwu->debug_image.data = NULL; |
| fwu->debug_image.size = 0; |
| |
| mutex_init(&fwu->status_lock); |
| |
| fwu->pmic = max77779_get_dev(fwu->dev, MAX77779_PMIC_OF_NAME); |
| if (!fwu->pmic) { |
| dev_err(dev, "Error finding pmic\n"); |
| return -EPROBE_DEFER; |
| } |
| |
| fwu->fg = max77779_get_dev(fwu->dev, "max77779,fg"); |
| if (!fwu->fg) { |
| dev_err(dev, "Error finding fg\n"); |
| return -EPROBE_DEFER; |
| } |
| |
| fwu->vimon = max77779_get_dev(fwu->dev, "max77779,vimon"); |
| if (!fwu->vimon) { |
| dev_err(dev, "Error finding vimon\n"); |
| return -EPROBE_DEFER; |
| } |
| |
| fwu->chg = max77779_get_dev(fwu->dev, "max77779,chg"); |
| if (!fwu->chg) { |
| dev_err(dev, "Error finding chg\n"); |
| return -EPROBE_DEFER; |
| } |
| |
| if (!fwu->batt) { |
| dn = of_parse_phandle(dev->of_node, "google,battery", 0); |
| if (!dn) |
| return -ENXIO; |
| |
| fwu->batt = of_find_device_by_node(dn); |
| if (!fwu->batt) |
| return -EPROBE_DEFER; |
| } |
| |
| val = gbms_storage_read(GBMS_TAG_FGST, &fwu->op_st, sizeof(fwu->op_st)); |
| if (val < 0) |
| gbms_logbuffer_prlog(fwu->lb, LOGLEVEL_WARNING, 0, LOGLEVEL_INFO, |
| "failed to read FGST tag (%d)\n", val); |
| |
| if (of_property_read_u32(dev->of_node, "fwu,enabled", &val) == 0) |
| fwu->can_update = val; |
| |
| if (of_property_read_u32(dev->of_node, "minimum-voltage", &val) == 0) |
| fwu->minimum_voltage = val; |
| |
| if (of_property_read_u32(dev->of_node, "version-major", &val) == 0) |
| fwu->minimum.major = (u8)val; |
| |
| if (of_property_read_u32(dev->of_node, "version-minor", &val) == 0) |
| fwu->minimum.minor = (u8)val; |
| |
| fwu->lb = logbuffer_register("max77779_fwupdate"); |
| if (IS_ERR(fwu->lb)) { |
| val = PTR_ERR(fwu->lb); |
| dev_err(dev, "failed to obtain logbuffer, ret=%d\n", val); |
| fwu->lb = NULL; |
| return val; |
| } |
| |
| read_fwupdate_stats(fwu); |
| |
| return 0; |
| } |
| |
| static int max77779_wait_cpu_reset(struct max77779_fwupdate *fwu) |
| { |
| int ret; |
| int cnt = 0; |
| uint8_t val; |
| |
| dev_info(fwu->dev, "waiting for cpu reset\n"); |
| |
| while (cnt < FW_UPDATE_RETRY_CPU_RESET) { |
| msleep(FW_UPDATE_WAIT_INTERVAL_MS); |
| ret = max77779_external_pmic_reg_read(fwu->pmic, MAX77779_PMIC_RISCV_AP_DATAIN0, &val); |
| if (ret == 0) { |
| if (val != MAX77779_RSP_CODE_UNEXPECTED) { |
| MARK_IN_PROGRESS(); |
| } else { |
| dev_info(fwu->dev, "cpu reset completed\n"); |
| return 0; |
| } |
| } |
| cnt++; |
| } |
| |
| dev_err(fwu->dev, "timeout for max77779_wait_cpu_reset\n"); |
| return -ETIMEDOUT; |
| } |
| |
| static int max77779_wait_fw_update(struct max77779_fwupdate *fwu) |
| { |
| int ret; |
| int cnt = 0; |
| uint8_t val; |
| |
| dev_info(fwu->dev, "waiting for firmware update\n"); |
| |
| do { |
| msleep(FW_UPDATE_WAIT_INTERVAL_MS); |
| cnt++; |
| |
| ret = max77779_external_pmic_reg_read(fwu->pmic, MAX77779_PMIC_RISCV_AP_DATAIN0, &val); |
| if (ret != 0 || val == MAX77779_RSP_CODE_NOT_READY) { |
| MARK_IN_PROGRESS(); |
| continue; |
| } else if (val == MAX77779_RSP_CODE_UNEXPECTED) { |
| dev_err(fwu->dev, "failed to firmware update rsp %02x\n", val); |
| return -EBADFD; |
| } |
| |
| dev_info(fwu->dev, "firmware update completed: rsp %02x\n", val); |
| return 0; |
| |
| } while (cnt < FW_UPDATE_RETRY_FW_UPDATE); |
| |
| dev_err(fwu->dev, "timeout for max77779_wait_fw_update\n"); |
| return -ETIMEDOUT; |
| } |
| |
| static int max77779_wait_riscv_reboot(struct max77779_fwupdate *fwu) |
| { |
| int ret; |
| int cnt = 0; |
| uint16_t val; |
| |
| dev_info(fwu->dev, "waiting for riscv reboot\n"); |
| |
| while (cnt < FW_UPDATE_RETRY_RISCV_REBOOT) { |
| msleep(FW_UPDATE_WAIT_INTERVAL_MS); |
| ret = max77779_external_fg_reg_read(fwu->fg, MAX77779_FG_FG_INT_STS, &val); |
| if (!ret && (val & MAX77779_FG_FG_INT_MASK_POR_m_MASK)) { |
| dev_info(fwu->dev, "wait_risc_reboot POR interrupt received\n"); |
| return 0; |
| } |
| |
| cnt++; |
| } |
| |
| dev_err(fwu->dev, "timeout for POR interrupt\n"); |
| return -ETIMEDOUT; |
| } |
| |
| /* b/328083603: Even POR triggered, RISC-V may not be ready */ |
| static int check_boot_completed(struct max77779_fwupdate *fwu, int max_retry) |
| { |
| int ret, retry; |
| uint16_t val; |
| |
| for (retry = 0; retry < max_retry; retry++) { |
| ret = max77779_external_fg_reg_read(fwu->fg, MAX77779_FG_BOOT_CHECK_REG, &val); |
| if (ret) { |
| dev_err(fwu->dev, "failed to read %02x (%d) in check boot completed\n", |
| MAX77779_FG_BOOT_CHECK_REG, ret); |
| return ret; |
| } |
| |
| /* b/323382370 */ |
| if ((val & MAX77779_FG_BOOT_CHECK_SUCCESS) == MAX77779_FG_BOOT_CHECK_SUCCESS) |
| break; |
| |
| msleep(FW_UPDATE_WAIT_INTERVAL_MS); |
| } |
| |
| if (retry == max_retry) { |
| dev_err(fwu->dev, "Boot NOT completed successfully: %04x\n", val); |
| return -EIO; |
| } |
| |
| dev_info(fwu->dev, "Boot completed successfully\n"); |
| return 0; |
| } |
| |
| static int max77779_check_timer_refresh(struct max77779_fwupdate *fwu) |
| { |
| int ret; |
| uint16_t val0, val1; |
| |
| dev_info(fwu->dev, "check for timer refresh\n"); |
| |
| ret = max77779_external_fg_reg_read(fwu->fg, MAX77779_FG_Timer, &val0); |
| if (ret) |
| goto check_timer_error; |
| |
| msleep(FW_UPDATE_TIMER_CHECK_INTERVAL_MS); |
| |
| ret = max77779_external_fg_reg_read(fwu->fg, MAX77779_FG_Timer, &val1); |
| if (ret) |
| goto check_timer_error; |
| |
| if (val1 <= val0) { |
| dev_err(fwu->dev, "Timer NOT updating correctly\n"); |
| return -EIO; |
| } |
| |
| dev_info(fwu->dev, "Timer updating correctly\n"); |
| return 0; |
| |
| check_timer_error: |
| dev_err(fwu->dev, "failed to read %02x (%d) in max77779_check_timer_refresh\n", |
| MAX77779_FG_Timer, ret); |
| return ret; |
| } |
| |
| static int max77779_send_command(struct max77779_fwupdate *fwu, |
| enum max77779_fwupdate_cmd cmd) |
| { |
| int ret; |
| |
| ret = max77779_external_fg_reg_write_nolock(fwu->fg, MAX77779_FG_Command_fw, cmd); |
| if (ret) |
| dev_err(fwu->dev, "failed to write fg reg %02x (%d) in max77779_send_command\n", |
| MAX77779_FG_Command_fw, ret); |
| |
| return ret; |
| } |
| |
| static int max77779_trigger_interrupt(struct max77779_fwupdate *fwu, |
| enum max77779_fwupdate_intr intr) |
| { |
| int ret; |
| |
| ret = max77779_external_pmic_reg_write(fwu->pmic, MAX77779_PMIC_RISCV_AP_DATAOUT_OPCODE, |
| intr); |
| if (ret) |
| dev_err(fwu->dev, "failed to write pmic reg %02x (%d) in trigger_interrupt\n", |
| MAX77779_PMIC_RISCV_AP_DATAOUT_OPCODE, ret); |
| |
| return ret; |
| } |
| |
| static int max77779_get_firmware_version(struct max77779_fwupdate *fwu, |
| struct max77779_version_info *ver) |
| { |
| int ret; |
| uint8_t major, minor; |
| |
| ret = max77779_external_pmic_reg_read(fwu->pmic, MAX77779_PMIC_RISCV_FW_REV, &major); |
| if (ret) { |
| dev_err(fwu->dev, "failed to read pmic reg %02x (%d) in read firmware version\n", |
| MAX77779_PMIC_RISCV_FW_REV, ret); |
| goto max77779_get_firmware_version_done; |
| } |
| |
| ret = max77779_external_pmic_reg_read(fwu->pmic, MAX77779_PMIC_RISCV_FW_SUB_REV, &minor); |
| if (ret) |
| dev_err(fwu->dev, "failed to read pmic reg %02x (%d) in read firmware version\n", |
| MAX77779_PMIC_RISCV_FW_SUB_REV, ret); |
| |
| ver->major = major; |
| ver->minor = minor; |
| |
| max77779_get_firmware_version_done: |
| return ret; |
| } |
| |
| |
| static int max77779_change_fg_lock(struct max77779_fwupdate *fwu, |
| const enum max77779_fwupdate_fg_lock st) |
| { |
| int ret; |
| |
| /* |
| * change FG's lock status |
| * - write status_value 2 times to MAX77779_FG_USR |
| */ |
| ret = max77779_external_fg_reg_write_nolock(fwu->fg, MAX77779_FG_USR, st); |
| if (ret == 0) |
| ret = max77779_external_fg_reg_write_nolock(fwu->fg, MAX77779_FG_USR, st); |
| |
| if (ret) |
| dev_err(fwu->dev, "failed to write fg reg %02x (%d) in change lock status\n", |
| MAX77779_FG_USR, ret); |
| |
| return ret; |
| } |
| |
| static int max77779_copy_to_vimon_mem(struct max77779_fwupdate *fwu, |
| const u16 page, const u8* data, |
| const size_t data_len) |
| { |
| int ret; |
| |
| ret = max77779_external_vimon_reg_write(fwu->vimon, MAX77779_BVIM_PAGE_CTRL, |
| (u8*)&page, 2); |
| if (ret) { |
| dev_err(fwu->dev, "failed to set page %x (%d)\n", page, ret); |
| goto max77779_copy_to_vimon_mem_done; |
| } |
| |
| ret = max77779_external_vimon_reg_write(fwu->vimon, MAX77779_VIMON_MEM_BASE_ADDR, |
| data, data_len); |
| if (ret) { |
| dev_err(fwu->dev, |
| "failed to write data to vimon's memory page %x (%d)\n", |
| page, ret); |
| goto max77779_copy_to_vimon_mem_done; |
| } |
| |
| max77779_copy_to_vimon_mem_done: |
| return ret; |
| } |
| |
| static int max77779_load_fw_binary(struct max77779_fwupdate *fwu, |
| const u8* data, const size_t data_len) |
| { |
| u16 page; |
| int ret; |
| size_t cp_len; |
| ssize_t remains = (ssize_t)data_len; |
| |
| /* copy firmware binary to vimon's memory */ |
| for (page = 0; (page < 4) && remains > 0; page++) { |
| if (remains > MAX77779_VIMON_PG_SIZE) |
| cp_len = MAX77779_VIMON_PG_SIZE; |
| else |
| cp_len = remains; |
| |
| ret = max77779_copy_to_vimon_mem(fwu, page, data, cp_len); |
| if (ret) { |
| dev_err(fwu->dev, "failed load binary in copy data in page %d\n", |
| (int)page); |
| goto max77779_load_fw_binary_done; |
| } |
| |
| data += MAX77779_VIMON_PG_SIZE; |
| remains -= MAX77779_VIMON_PG_SIZE; |
| } |
| |
| max77779_load_fw_binary_done: |
| return ret; |
| } |
| |
| static inline int max77779_clear_state_for_update(struct max77779_fwupdate *fwu) |
| { |
| int ret; |
| uint16_t val; |
| |
| /* clear POR bits*/ |
| ret = max77779_external_fg_reg_read(fwu->fg, MAX77779_FG_FG_INT_STS, &val); |
| if (ret) { |
| dev_err(fwu->dev, |
| "failed to read reg %02x (%d) in max77779_clear_state_for_update\n", |
| MAX77779_FG_FG_INT_STS, ret); |
| return ret; |
| } |
| |
| ret = max77779_external_fg_reg_write_nolock(fwu->fg, MAX77779_FG_FG_INT_STS, val); |
| if (ret) { |
| dev_err(fwu->dev, |
| "failed to write reg %02x (%d) in max77779_clear_state_for_update\n", |
| MAX77779_FG_FG_INT_STS, ret); |
| return ret; |
| } |
| |
| /* clear commands */ |
| max77779_send_command(fwu, MAX77779_CMD_CLEAR_ALL); |
| |
| /* corner case, handles commands still present in AP_REQUEST_OPCODE */ |
| ret = max77779_trigger_interrupt(fwu, MAX77779_INTR_CLEAR); |
| return ret; |
| } |
| |
| static inline int max77779_session_start(struct max77779_fwupdate *fwu, |
| const u8* fw_binary_data, |
| const char* name) |
| { |
| int ret; |
| |
| dev_info(fwu->dev, "[%s] begins\n", name); |
| |
| ret = max77779_trigger_interrupt(fwu, MAX77779_INTR_SESSION_START); |
| MAX77779_ABORT_ON_ERROR(ret, name, "interrupt trigger"); |
| |
| max77779_wait_cpu_reset(fwu); |
| |
| ret = max77779_load_fw_binary(fwu, fw_binary_data, MAX77779_FW_IMG_SZ_PACKET); |
| MAX77779_ABORT_ON_ERROR(ret, name, "load_binary"); |
| |
| ret = max77779_wait_fw_update(fwu); |
| MAX77779_ABORT_ON_ERROR(ret, name, "max77779_wait_fw_update"); |
| |
| dev_info(fwu->dev, "[%s] ends\n", name); |
| |
| return ret; |
| } |
| |
| static int max77779_transfer_binary_data(struct max77779_fwupdate *fwu, |
| const u8* fw_binary_data, |
| const size_t data_size, |
| enum max77779_fwupdate_intr intr, |
| const char* name) |
| { |
| int ret; |
| ssize_t frame_len; |
| ssize_t remains = (ssize_t)data_size; |
| |
| dev_info(fwu->dev, "[%s] begins\n", name); |
| |
| while (remains > 0) { |
| frame_len = (remains > MAX77779_FW_IMG_SZ_FRAME)? |
| MAX77779_FW_IMG_SZ_FRAME : remains; |
| |
| ret = max77779_load_fw_binary(fwu, fw_binary_data, frame_len); |
| MAX77779_ABORT_ON_ERROR(ret, name, "load_binary"); |
| |
| msleep(FW_UPDATE_WAIT_LOAD_BIN_MS); |
| |
| ret = max77779_trigger_interrupt(fwu, intr); |
| MAX77779_ABORT_ON_ERROR(ret, name, "max77779_trigger_interrupt"); |
| |
| ret = max77779_wait_fw_update(fwu); |
| MAX77779_ABORT_ON_ERROR(ret, name, "max77779_wait_fw_update"); |
| |
| fw_binary_data += frame_len; |
| remains -= frame_len; |
| |
| dev_info(fwu->dev, "transferred data (%zu/%zu)\n", (data_size - remains), |
| data_size); |
| } |
| |
| dev_info(fwu->dev, "[%s] ends\n", name); |
| |
| return ret; |
| } |
| |
| /* TODO: b/303731272 condtion check */ |
| static int max77779_can_update(struct max77779_fwupdate *fwu, struct max77779_version_info* target) |
| { |
| /* compatibility check: major version should match */ |
| if (target->major != fwu->v_cur.major) |
| return -EINVAL; |
| |
| /* Is this device ellgabie to update firmware? */ |
| if (!fwu->can_update) |
| return -EACCES; |
| |
| /* check version */ |
| if (target->minor <= fwu->v_cur.minor && !fwu->update_info.force_update) |
| return -EINVAL; |
| |
| return 0; |
| } |
| |
| static inline int max77779_set_firmwarename(struct max77779_fwupdate *fwu) |
| { |
| uint8_t val; |
| int ret, fw_ver; |
| |
| fw_ver = fwu->v_cur.major; |
| |
| /* b/322967969 version value can be 0xFF */ |
| if (fw_ver == MAX77779_FW_INVALID_FW_VER) { |
| ret = max77779_external_pmic_reg_read(fwu->pmic, MAX77779_PMIC_REVISION, &val); |
| if (ret) { |
| dev_err(fwu->dev, "faild to read pmic reg %02x (%d)\n", |
| MAX77779_PMIC_REVISION, ret); |
| return ret; |
| } |
| |
| if (val == MAX77779_REV_PASS_1_5) |
| fw_ver = MAX77779_REV_PASS_1_5_FIRMWARE; |
| else if (val == MAX77779_REV_PASS_2_0) |
| fw_ver = MAX77779_REV_PASS_2_0_FIRMWARE; |
| else |
| return -EINVAL; |
| } |
| |
| scnprintf(fwu->fw_name, MAX77779_FW_UPDATE_STRING_MAX, "%s_%d.bin", |
| MAX77779_FIRMWARE_BINARY_PREFIX, fw_ver); |
| |
| return 0; |
| } |
| |
| static inline int max77779_fwupdate_chip_reset(struct max77779_fwupdate *fwu) |
| { |
| int ret; |
| |
| /* non zero opcode may distrub chip reset */ |
| ret = max77779_external_pmic_reg_write(fwu->pmic, MAX77779_PMIC_RISCV_AP_DATAOUT_OPCODE, |
| 0x0); |
| if (ret) |
| dev_err(fwu->dev, "failed to clear opcode (%d)\n", ret); |
| |
| ret = max77779_external_pmic_reg_write(fwu->pmic, MAX77779_PMIC_RISCV_COMMAND_HW, |
| MAX77779_CMD_REBOOT_FG); |
| if (ret) |
| dev_err(fwu->dev, "failed to reset chip (%d)\n", ret); |
| |
| return ret; |
| } |
| |
| static int max77779_fwl_prepare(struct max77779_fwupdate *fwu, |
| const u8 *data, u32 size) |
| { |
| int ret; |
| size_t data_frame_size; |
| struct gvotable_election *mode_votable; |
| |
| fwu->zero_filled_buffer = kzalloc(MAX77779_VIMON_PG_SIZE, GFP_KERNEL); |
| fwu->scratch_buffer = kmalloc(MAX77779_VIMON_PG_SIZE, GFP_KERNEL); |
| if (!fwu->zero_filled_buffer || !fwu->scratch_buffer) { |
| dev_err(fwu->dev, "failed to allocate temporay work buffer\n"); |
| return -ENOMEM; |
| } |
| |
| dev_info(fwu->dev, "prepare firmware update (image size: %d)\n", size); |
| |
| data_frame_size = MAX77779_GET_DATA_FRAME_SIZE(size); |
| if (data_frame_size % MAX77779_FW_IMG_SZ_PACKET) { |
| dev_err(fwu->dev, "incorrect image size (data section size: %zu)\n", |
| data_frame_size); |
| return -EINVAL; |
| } |
| |
| ret = max77779_get_firmware_version(fwu, &fwu->v_cur); |
| MAX77779_ABORT_ON_ERROR(ret, __func__, "failed to read version information\n"); |
| |
| fwu->data_frame_size = data_frame_size; |
| fwu->op_st = (u8)FGST_FWUPDATE; |
| |
| ret = gbms_storage_write(GBMS_TAG_FGST, &fwu->op_st, sizeof(fwu->op_st)); |
| if (ret != sizeof(fwu->op_st)) { |
| fwu->op_st = (u8)FGST_ERR_READTAG; |
| gbms_logbuffer_prlog(fwu->lb, LOGLEVEL_WARNING, 0, LOGLEVEL_INFO, |
| "failed to update eeprom:GBMS_TAG_FGST (%d)\n", ret); |
| } |
| |
| if (!fwu->mode_votable) { |
| mode_votable = gvotable_election_get_handle(GBMS_MODE_VOTABLE); |
| if (!mode_votable) { |
| dev_err(fwu->dev, "failed to get %s(%ld)\n", GBMS_MODE_VOTABLE, |
| PTR_ERR(mode_votable)); |
| return -ENODEV; |
| } |
| |
| fwu->mode_votable = mode_votable; |
| } |
| |
| ret = gvotable_cast_long_vote(fwu->mode_votable, MAX77779_REASON_FIRMWARE, |
| GBMS_CHGR_MODE_FWUPDATE_BOOST_ON, true); |
| MAX77779_ABORT_ON_ERROR(ret, __func__, "failed to set mode BOOST_ON"); |
| |
| ret = max77779_fg_enable_firmware_update(fwu->fg, true); |
| MAX77779_ABORT_ON_ERROR(ret, __func__, "failed to set fg_enable_firmware_update"); |
| |
| dev_info(fwu->dev, "the current installed firmware version %u.%u\n", |
| (unsigned int)fwu->v_cur.major, |
| (unsigned int)fwu->v_cur.minor); |
| |
| ret = max77779_change_fg_lock(fwu, FG_ST_UNLOCK_ALL_SECTION); |
| MAX77779_ABORT_ON_ERROR(ret, __func__, "failed unlock FG"); |
| |
| ret = max77779_clear_state_for_update(fwu); |
| MAX77779_ABORT_ON_ERROR(ret, __func__, "failed clear command / POR interrupt"); |
| |
| ret = max77779_send_command(fwu, MAX77779_CMD_REBOOT_RISCV); |
| MAX77779_ABORT_ON_ERROR(ret, __func__, "failed send command CMD_REBOOT_RISCV"); |
| |
| /* wait_riscv_reboot might timeout but subsequent updates will be ok */ |
| max77779_wait_riscv_reboot(fwu); |
| |
| return ret; |
| } |
| |
| /* TODO: b/303132973 - consider: "offset" */ |
| static int max77779_fwl_write(struct max77779_fwupdate *fwu, const u8 *fw_binary_data, u32 offset, |
| u32 size, u32 *written) |
| { |
| int ret; |
| uint8_t val; |
| |
| dev_info(fwu->dev, "perform firmware update\n"); |
| |
| /* skip header*/ |
| fw_binary_data += MAX77779_FW_IMG_SZ_HEADER; |
| *written += MAX77779_FW_IMG_SZ_HEADER; |
| |
| /* Session Start */ |
| ret = max77779_session_start(fwu, fw_binary_data, "Session Start"); |
| MAX77779_ABORT_ON_ERROR(ret, __func__, "Session Start"); |
| |
| fw_binary_data += MAX77779_FW_IMG_SZ_PACKET; |
| *written += MAX77779_FW_IMG_SZ_PACKET; |
| |
| /* Transfer Frame */ |
| ret = max77779_transfer_binary_data(fwu, fw_binary_data, fwu->data_frame_size, |
| MAX77779_INTR_TRANSFER_FRAMES, "Transfer Frame"); |
| MAX77779_ABORT_ON_ERROR(ret, __func__, "Transfer Frame"); |
| |
| fw_binary_data += fwu->data_frame_size; |
| *written += fwu->data_frame_size; |
| |
| /* App Valid: CRC check */ |
| ret = max77779_transfer_binary_data(fwu, fw_binary_data, MAX77779_FW_IMG_SZ_PACKET, |
| MAX77779_INTR_APP_VALID, "App Valid"); |
| MAX77779_ABORT_ON_ERROR(ret, __func__, "App Valid"); |
| |
| fw_binary_data += MAX77779_FW_IMG_SZ_PACKET; |
| *written += MAX77779_FW_IMG_SZ_PACKET; |
| |
| fwu->crc_val = 0; |
| ret = max77779_external_pmic_reg_read(fwu->pmic, MAX77779_PMIC_RISCV_AP_DATAIN0, &val); |
| MAX77779_ABORT_ON_ERROR(ret, __func__, "failed to read crc information"); |
| dev_info(fwu->dev, "RISCV lock status: %x\n", val); |
| |
| ret = max77779_external_pmic_reg_read(fwu->pmic, MAX77779_PMIC_RISCV_AP_DATAIN2, &val); |
| MAX77779_ABORT_ON_ERROR(ret, __func__, "failed to read crc information"); |
| fwu->crc_val = val; |
| |
| ret = max77779_external_pmic_reg_read(fwu->pmic, MAX77779_PMIC_RISCV_AP_DATAIN3, &val); |
| MAX77779_ABORT_ON_ERROR(ret, __func__, "failed to read crc information"); |
| fwu->crc_val |= (val << 8); |
| |
| |
| /* Session End */ |
| ret = max77779_transfer_binary_data(fwu, fw_binary_data, MAX77779_FW_IMG_SZ_PACKET, |
| MAX77779_INTR_SESSION_END, "Session End"); |
| MAX77779_ABORT_ON_ERROR(ret, __func__, "Session End"); |
| |
| *written += MAX77779_FW_IMG_SZ_PACKET; |
| |
| return ret; |
| } |
| |
| static int max77779_fwl_poll_complete(struct max77779_fwupdate *fwu) |
| { |
| int ret; |
| uint16_t val; |
| |
| dev_info(fwu->dev, "max77779_fwl_poll_complete\n"); |
| |
| /* check firmware update status */ |
| dev_info(fwu->dev, "firmware update CRC: %x\n", fwu->crc_val); |
| if (fwu->crc_val == 0) { |
| dev_info(fwu->dev, "bad CRC value returns"); |
| return -EIO; |
| } |
| |
| ret = max77779_external_fg_reg_read(fwu->fg, MAX77779_FG_SECUPDATE_STATUS_REG, &val); |
| MAX77779_ABORT_ON_ERROR(ret, __func__, "failed to read MAX77779_FG_SECUPDATE_STATUS_REG"); |
| if (val != MAX77779_FG_SECUPDATE_STATUS_SUCCESS) { |
| dev_err(fwu->dev, "firmware update fail: MAX77779_FG_SECUPDATE_STATUS_REG:%02x\n", |
| val); |
| return -EAGAIN; |
| } |
| |
| /* b/310710147: risc-v is not operational state. requires reboot */ |
| max77779_fwupdate_chip_reset(fwu); |
| max77779_wait_riscv_reboot(fwu); |
| |
| ret = check_boot_completed(fwu, FW_UPDATE_RETRY_CPU_RESET); |
| MAX77779_ABORT_ON_ERROR(ret, __func__, "failed on check_boot_completed\n"); |
| |
| ret = max77779_get_firmware_version(fwu, &fwu->v_new); |
| MAX77779_ABORT_ON_ERROR(ret, __func__, "failed to get firmware version\n"); |
| dev_info(fwu->dev, "updated firmware version: %u.%u\n", |
| fwu->v_new.major, fwu->v_new.minor); |
| |
| ret = max77779_check_timer_refresh(fwu); |
| MAX77779_ABORT_ON_ERROR(ret, __func__, "failed on max77779_check_timer_refresh\n"); |
| |
| mutex_lock(&fwu->status_lock); |
| |
| fwu->op_st = FGST_NORMAL; |
| fwu->stats.count++; |
| fwu->stats.success++; |
| |
| update_fwupdate_stats(fwu, &fwu->stats); |
| |
| mutex_unlock(&fwu->status_lock); |
| |
| if (fwu->v_cur.major != fwu->v_new.major || fwu->v_cur.minor != fwu->v_new.minor) |
| fwu->v_cur = fwu->v_new; |
| |
| set_firmware_update_tag(fwu, fwu->update_info.new_tag); |
| |
| fwu->update_info.force_update = false; |
| fwu->update_info.retry_cnt = 0; |
| |
| return ret; |
| } |
| |
| static void max77779_fwl_cleanup(struct max77779_fwupdate *fwu) |
| { |
| int ret; |
| |
| dev_info(fwu->dev, "max77779_fwl_cleanup\n"); |
| |
| if (fwu->zero_filled_buffer) |
| kfree(fwu->zero_filled_buffer); |
| if (fwu->scratch_buffer) |
| kfree(fwu->scratch_buffer); |
| |
| ret = max77779_fg_enable_firmware_update(fwu->fg, false); |
| if (ret) |
| dev_err(fwu->dev, "failed to restore FG from update mode (%d)\n", ret); |
| |
| if (!fwu->mode_votable) |
| return; |
| |
| ret = gvotable_cast_long_vote(fwu->mode_votable, MAX77779_REASON_FIRMWARE, |
| GBMS_CHGR_MODE_FWUPDATE_BOOST_ON, false); |
| if (ret) |
| dev_err(fwu->dev, "failed to restore CHG from update mode (%d)\n", ret); |
| } |
| |
| static inline int update_running_state(struct max77779_fwupdate *fwu, bool running) |
| { |
| bool ret = false; |
| |
| mutex_lock(&fwu->status_lock); |
| |
| if (fwu->running_update != running) { |
| fwu->running_update = running; |
| ret = true; |
| } |
| |
| mutex_unlock(&fwu->status_lock); |
| |
| return ret; |
| } |
| |
| static inline int perform_firmware_update(struct max77779_fwupdate *fwu, const char* data, |
| const size_t count) |
| { |
| u32 written = 0; |
| enum max77779_fwupdate_err_code err_code = MAX77779_FWU_ERR_NONE; |
| int ret, ret_st; |
| struct max77779_fwupdate_stats stats_backup; |
| |
| /* if previous update is not completed yet, stop at here */ |
| if (!update_running_state(fwu, true)) |
| return -EBUSY; |
| |
| __pm_stay_awake(fwu->fwupdate_wake_lock); |
| |
| logbuffer_log(fwu->lb, "perform_firmware_update: %s", fwu->fw_name); |
| |
| /* |
| * increase failure count upfront |
| * - update can be distrubed without cleanup |
| * - store with new value inside of max77779_fwl_poll_complete when everything is OK |
| */ |
| mutex_lock(&fwu->status_lock); |
| |
| stats_backup.count = fwu->stats.count + 1; |
| stats_backup.success = fwu->stats.success; |
| stats_backup.fail = fwu->stats.fail + 1; |
| |
| update_fwupdate_stats(fwu, &stats_backup); |
| |
| mutex_unlock(&fwu->status_lock); |
| |
| ret = max77779_fwl_prepare(fwu, data, count); |
| if (ret) { |
| err_code = MAX77779_FWU_ERR_PREPARE; |
| goto perform_firmware_update_cleanup; |
| } |
| |
| ret = max77779_fwl_write(fwu, data, 0, count, &written); |
| if (ret || written != count) { |
| err_code = MAX77779_FWU_ERR_DATA_TRANSFER; |
| goto perform_firmware_update_cleanup; |
| } |
| |
| if (max77779_fwl_poll_complete(fwu) != 0) |
| err_code = MAX77779_FWU_ERR_POST_STATUS_CHECK; |
| |
| perform_firmware_update_cleanup: |
| max77779_fwl_cleanup(fwu); |
| |
| /* force reboot RISC-V for the case of update failure*/ |
| if (ret || written != count) { |
| max77779_fwupdate_chip_reset(fwu); |
| |
| mutex_lock(&fwu->status_lock); |
| |
| fwu->op_st = FGST_BASEFW; |
| fwu->stats.count++; |
| fwu->stats.fail++; |
| |
| mutex_unlock(&fwu->status_lock); |
| } |
| |
| ret_st = gbms_storage_write(GBMS_TAG_FGST, &fwu->op_st, sizeof(fwu->op_st)); |
| if (ret_st != sizeof(fwu->op_st)) |
| gbms_logbuffer_prlog(fwu->lb, LOGLEVEL_WARNING, 0, LOGLEVEL_INFO, |
| "failed to update eeprom:GBMS_TAG_FGST (%d)\n", ret_st); |
| |
| gbms_logbuffer_prlog(fwu->lb, LOGLEVEL_INFO, 0, LOGLEVEL_INFO, |
| "complete_firmware_update: %d %d %d (%d)", fwu->stats.count, |
| fwu->stats.success, fwu->stats.fail, (int)err_code); |
| |
| __pm_relax(fwu->fwupdate_wake_lock); |
| update_running_state(fwu, false); |
| |
| kobject_uevent(&fwu->fg->kobj, KOBJ_CHANGE); |
| |
| return ret; |
| } |
| |
| static void firmware_update_work(struct work_struct* work) |
| { |
| int ret; |
| struct max77779_version_info target_version; |
| const struct firmware* fw_data; |
| struct max77779_fwupdate *fwu = NULL; |
| |
| fwu = container_of(work, struct max77779_fwupdate, update_work.work); |
| if (!fwu) |
| return; |
| |
| ret = request_firmware(&fw_data, fwu->fw_name, fwu->dev); |
| if (ret) { |
| dev_warn(fwu->dev, "fails on request_firmware %d\n", ret); |
| goto firmware_update_work_cleanup; |
| } |
| |
| target_version.major = fw_data->data[MAX77779_OFFSET_VER_MAJOR]; |
| target_version.minor = fw_data->data[MAX77779_OFFSET_VER_MINOR]; |
| |
| ret = max77779_can_update(fwu, &target_version); |
| if (ret) { |
| dev_info(fwu->dev, "can not update firmware %d\n", ret); |
| goto firmware_update_work_cleanup; |
| } |
| |
| ret = perform_firmware_update(fwu, fw_data->data, fw_data->size); |
| if (ret) |
| dev_err(fwu->dev, "firmware update failed (retry:%d) %d\n", |
| fwu->update_info.retry_cnt, ret); |
| |
| firmware_update_work_cleanup: |
| release_firmware(fw_data); |
| |
| if (ret) |
| max77779_schedule_update(fwu); |
| } |
| |
| static inline bool max77779_can_charge(struct device* chg) |
| { |
| struct max77779_chgr_data *data = dev_get_drvdata(chg); |
| int ret; |
| uint8_t chg_detail; |
| |
| ret = max77779_external_chg_reg_read(chg, MAX77779_CHG_DETAILS_00, &chg_detail); |
| if (ret) |
| return false; |
| |
| /* check usb: 0x0 or 0x1 means VBUS is invalid */ |
| if (_max77779_chg_details_00_chgin_dtls_get(chg_detail) >= 2 && !data->chgin_input_suspend) |
| return true; |
| |
| /* check wireless: 0x0 or 0x1 means VWCIN is invalid */ |
| if ((_max77779_chg_details_00_wcin_dtls_get(chg_detail) >= 2 && !data->wcin_input_suspend) |
| || data->wlc_spoof) |
| return true; |
| |
| return false; |
| } |
| |
| /* |
| * trigger firmware update with override version tag |
| * - echo xxx > update_firmware |
| */ |
| static ssize_t trigger_update_firmware(struct device *dev, |
| struct device_attribute *attr, |
| const char *options, size_t count) |
| { |
| int ret, current_ver, read_cnt, target_ver; |
| int bypass_check = 0, override_ver = 0; |
| uint16_t voltage; |
| struct max77779_fwupdate *fwu = dev_get_drvdata(dev); |
| |
| if (!fwu) |
| return -EAGAIN; |
| |
| if (!fwu->can_update) { |
| dev_err(fwu->dev, "not allowed to update firmware\n"); |
| return -EACCES; |
| } |
| |
| read_cnt = sscanf(options, "%d %d %d", &target_ver, &override_ver, &bypass_check); |
| if (read_cnt < 1) { |
| dev_err(fwu->dev, "incorrect input: expects override_tag(number) and reset_tag" |
| "(optional)\n"); |
| return -EINVAL; |
| } |
| |
| if (!bypass_check) { |
| /* check chgin/wcin */ |
| if (!max77779_can_charge(fwu->chg)) { |
| dev_err(fwu->dev, "charger is not plugged. connect charger required\n"); |
| return -EBUSY; |
| } |
| |
| /* check current voltage */ |
| ret = max77779_external_fg_reg_read(fwu->fg, MAX77779_FG_AvgVCell, &voltage); |
| if (ret || reg_to_micro_volt(voltage) < fwu->minimum_voltage) { |
| dev_err(fwu->dev, "low voltage for update\n"); |
| return -ERANGE; |
| } |
| } |
| |
| current_ver = get_firmware_update_tag(fwu); |
| if (!override_ver && (target_ver <= current_ver)) { |
| dev_info(fwu->dev, "ver %d already installed: update request will be skipped", |
| target_ver); |
| return count; |
| } |
| |
| if (max77779_set_firmwarename(fwu) < 0) { |
| dev_err(fwu->dev, "can't set proper firmware file\n"); |
| return -EINVAL; |
| } |
| |
| mutex_lock(&fwu->status_lock); |
| |
| fwu->update_info.new_tag = target_ver; |
| fwu->update_info.force_update = true; |
| fwu->update_info.reboot_on_failure = true; |
| fwu->update_info.retry_cnt = 0; |
| |
| mutex_unlock(&fwu->status_lock); |
| |
| schedule_delayed_work(&fwu->update_work, |
| msecs_to_jiffies(FW_UPDATE_TIMER_CHECK_INTERVAL_MS)); |
| |
| return count; |
| } |
| |
| static DEVICE_ATTR(update_firmware, 0220, NULL, trigger_update_firmware); |
| |
| |
| static ssize_t enable_update_show(struct device *dev, struct device_attribute *attr, char *buf) |
| { |
| struct max77779_fwupdate *fwu = dev_get_drvdata(dev); |
| if (!fwu) |
| return -EAGAIN; |
| |
| return scnprintf(buf, PAGE_SIZE, "%d\n", fwu->can_update); |
| } |
| |
| static ssize_t enable_update_store(struct device *dev, struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| struct max77779_fwupdate *fwu = dev_get_drvdata(dev); |
| if (!fwu) |
| return -EAGAIN; |
| |
| if (kstrtobool(buf, &fwu->can_update)) |
| return -EINVAL; |
| |
| return count; |
| } |
| |
| static DEVICE_ATTR_RW(enable_update); |
| |
| static ssize_t update_status_show(struct device *dev, struct device_attribute *attr, char *buf) |
| { |
| int ret; |
| uint16_t val; |
| enum max77779_fwupdate_status st = MAX77779_FWU_OK; |
| struct max77779_fwupdate *fwu = dev_get_drvdata(dev); |
| |
| if (!fwu) |
| return -EAGAIN; |
| |
| mutex_lock(&fwu->status_lock); |
| if (fwu->running_update) { |
| st = MAX77779_FWU_RUNNING_UPDATE; |
| goto update_status_show_exit; |
| } |
| |
| ret = max77779_external_fg_reg_read(fwu->fg, MAX77779_FG_SECUPDATE_STATUS_REG, &val); |
| if (ret < 0){ |
| st = MAX77779_FWU_REG_ACCESS_ERR; |
| goto update_status_show_exit; |
| } |
| |
| if (val != MAX77779_FG_SECUPDATE_STATUS_SUCCESS) { |
| dev_err(fwu->dev, "firmware update fail: %X:%02x\n", |
| MAX77779_FG_SECUPDATE_STATUS_REG, val); |
| st = MAX77779_FWU_UPDATE_FAIL; |
| goto update_status_show_exit; |
| } |
| |
| ret = check_boot_completed(fwu, FW_UPDATE_RETRY_ONCE); |
| if (ret < 0) { |
| st = MAX77779_FWU_BOOT_ERR; |
| goto update_status_show_exit; |
| } |
| |
| ret = max77779_check_timer_refresh(fwu); |
| if (ret < 0) |
| st = MAX77779_FWU_TIMER_ERR; |
| |
| update_status_show_exit: |
| mutex_unlock(&fwu->status_lock); |
| return scnprintf(buf, PAGE_SIZE, "%d\n", st); |
| } |
| |
| static const DEVICE_ATTR_RO(update_status); |
| |
| static ssize_t chip_reset_store(struct device *dev, struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| bool trigger; |
| int rt = -EBUSY; |
| struct max77779_fwupdate *fwu = dev_get_drvdata(dev); |
| |
| if (!fwu) |
| return -EAGAIN; |
| |
| if (kstrtobool(buf, &trigger) && !trigger) |
| return -EINVAL; |
| |
| mutex_lock(&fwu->status_lock); |
| /* if there is no on-going fwupdate, trigger reset */ |
| if (!fwu->running_update && max77779_fwupdate_chip_reset(fwu) == 0) |
| rt = count; |
| |
| mutex_unlock(&fwu->status_lock); |
| |
| return rt; |
| } |
| |
| static DEVICE_ATTR_WO(chip_reset); |
| |
| static ssize_t update_stats_show(struct device *dev, struct device_attribute *attr, char *buf) |
| { |
| ssize_t ret; |
| struct max77779_fwupdate *fwu = dev_get_drvdata(dev); |
| if (!fwu) |
| return -EAGAIN; |
| |
| mutex_lock(&fwu->status_lock); |
| |
| ret = scnprintf(buf, PAGE_SIZE, "%d %d %d\n", fwu->stats.count, fwu->stats.success, |
| fwu->stats.fail); |
| |
| mutex_unlock(&fwu->status_lock); |
| |
| return ret; |
| } |
| |
| static ssize_t update_stats_store(struct device *dev, struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| struct max77779_fwupdate *fwu = dev_get_drvdata(dev); |
| int value, ret; |
| |
| if (!fwu) |
| return -EAGAIN; |
| |
| ret = kstrtoint(buf, 0, &value); |
| if (ret < 0) |
| return ret; |
| |
| mutex_lock(&fwu->status_lock); |
| |
| if (value == 0 && !fwu->running_update) { |
| fwu->stats.count = 0; |
| fwu->stats.success = 0; |
| fwu->stats.fail = 0; |
| |
| update_fwupdate_stats(fwu, &fwu->stats); |
| |
| ret = count; |
| } else { |
| ret = -EBUSY; |
| } |
| |
| mutex_unlock(&fwu->status_lock); |
| |
| return ret; |
| } |
| |
| static DEVICE_ATTR_RW(update_stats); |
| |
| /* |
| * Using the same pattern as FW_LOADER |
| * echo 1 > loading |
| * cat FW_IMG > data |
| * echo 0 > loading |
| */ |
| static int debug_update_firmware_loading(void *data, u64 val) |
| { |
| struct max77779_fwupdate *fwu = data; |
| int ret = 0; |
| |
| if (!fwu) |
| return -EAGAIN; |
| |
| if (val) { |
| if (!fwu->debug_image.data) |
| fwu->debug_image.data = kzalloc(FW_UPDATE_MAXIMUM_PAGE_SIZE, GFP_KERNEL); |
| |
| if (!fwu->debug_image.data) |
| return -ENOMEM; |
| |
| fwu->debug_image.size = 0; |
| } else { |
| if (fwu->debug_image.size > 0) |
| ret = perform_firmware_update(fwu, fwu->debug_image.data, fwu->debug_image.size); |
| |
| if (fwu->debug_image.data) { |
| kfree(fwu->debug_image.data); |
| fwu->debug_image.data = NULL; |
| fwu->debug_image.size = 0; |
| } |
| } |
| |
| fwu->can_update = (val != 0); |
| |
| return ret; |
| } |
| |
| DEFINE_SIMPLE_ATTRIBUTE(debug_update_firmware_loading_fops, NULL, debug_update_firmware_loading, |
| "%llu\n"); |
| |
| |
| static ssize_t debug_update_firmware_data(struct file *filp, const char __user *user_buf, |
| size_t count, loff_t *ppos) |
| { |
| struct max77779_fwupdate *fwu; |
| ssize_t ret; |
| |
| fwu = (struct max77779_fwupdate *)filp->private_data; |
| if (!fwu) |
| return -EAGAIN; |
| |
| if (!fwu->debug_image.data) |
| return -EINVAL; |
| |
| if ((FW_UPDATE_MAXIMUM_PAGE_SIZE - fwu->debug_image.size) < count) |
| return -EFBIG; |
| |
| ret = simple_write_to_buffer(fwu->debug_image.data, FW_UPDATE_MAXIMUM_PAGE_SIZE, ppos, |
| user_buf, count); |
| |
| if (ret >= 0) |
| fwu->debug_image.size += ret; |
| else |
| fwu->debug_image.size = -EINVAL; |
| |
| return ret; |
| } |
| |
| BATTERY_DEBUG_ATTRIBUTE(debug_update_firmware_data_fops, NULL, debug_update_firmware_data); |
| |
| |
| static int max77779_fwupdate_probe(struct platform_device *pdev) |
| { |
| int ret = 0; |
| struct dentry *de; |
| struct max77779_fwupdate *fwu; |
| |
| fwu = devm_kzalloc(&pdev->dev, sizeof(*fwu), GFP_KERNEL); |
| if (!fwu) |
| return -ENOMEM; |
| |
| fwu->dev = &pdev->dev; |
| platform_set_drvdata(pdev, fwu); |
| |
| ret = max77779_fwupdate_init(fwu); |
| if (ret) { |
| dev_err(fwu->dev, "error to set max77779_fwupdate\n"); |
| return ret; |
| } |
| |
| ret = max77779_get_firmware_version(fwu, &fwu->v_cur); |
| if (ret) |
| dev_err(fwu->dev, "failed to read version information\n"); |
| |
| ret = max77779_set_firmwarename(fwu); |
| if (ret) |
| dev_err(fwu->dev, "failed to set proper firmware file\n"); |
| |
| INIT_DELAYED_WORK(&fwu->update_work, firmware_update_work); |
| |
| ret = device_create_file(fwu->dev, &dev_attr_update_firmware); |
| if (ret != 0) { |
| pr_err("Failed to create update_firmware files, ret=%d\n", ret); |
| return ret; |
| } |
| |
| ret = device_create_file(fwu->dev, &dev_attr_enable_update); |
| if (ret != 0) { |
| pr_err("Failed to create enable_update files, ret=%d\n", ret); |
| return ret; |
| } |
| |
| ret = device_create_file(fwu->dev, &dev_attr_update_status); |
| if (ret != 0) { |
| pr_err("Failed to create update_status files, ret=%d\n", ret); |
| return ret; |
| } |
| |
| ret = device_create_file(fwu->dev, &dev_attr_chip_reset); |
| if (ret != 0) { |
| pr_err("Failed to create chip_reset files, ret=%d\n", ret); |
| return ret; |
| } |
| |
| ret = device_create_file(fwu->dev, &dev_attr_update_stats); |
| if (ret != 0) { |
| pr_err("Failed to create update_stats files, ret=%d\n", ret); |
| return ret; |
| } |
| |
| fwu->fwupdate_wake_lock = wakeup_source_register(NULL, "max77779-fwupdate"); |
| if (!fwu->fwupdate_wake_lock) { |
| dev_err(fwu->dev, "failed to register wakeup source\n"); |
| return -ENODEV; |
| } |
| |
| de = debugfs_create_dir("max77779_fwupdate", 0); |
| if (!de) |
| return 0; |
| |
| debugfs_create_file("loading", 0400, de, fwu, &debug_update_firmware_loading_fops); |
| debugfs_create_file("data", 0444, de, fwu, &debug_update_firmware_data_fops); |
| |
| fwu->de = de; |
| |
| /* the chip is running with base firmware: need to be updated */ |
| if (fwu->op_st == FGST_BASEFW) { |
| fwu->update_info.retry_cnt = 0; |
| fwu->update_info.force_update = true; |
| fwu->update_info.reboot_on_failure = false; |
| max77779_schedule_update(fwu); |
| } |
| |
| return ret; |
| } |
| |
| static int max77779_fwupdate_remove(struct platform_device *pdev) |
| { |
| struct max77779_fwupdate *fwu = platform_get_drvdata(pdev); |
| if (!fwu) |
| return 0; |
| |
| if (fwu->lb) { |
| logbuffer_unregister(fwu->lb); |
| fwu->lb = NULL; |
| } |
| |
| mutex_destroy(&fwu->status_lock); |
| |
| if (fwu->debug_image.data) |
| kfree(fwu->debug_image.data); |
| |
| if (fwu->fwupdate_wake_lock) |
| wakeup_source_unregister(fwu->fwupdate_wake_lock); |
| |
| if (fwu->de) |
| debugfs_remove(fwu->de); |
| |
| return 0; |
| } |
| |
| static const struct of_device_id max77779_fwupdate_of_match[] = { |
| {.compatible = "maxim,max77779fwu"}, |
| {}, |
| }; |
| MODULE_DEVICE_TABLE(of, max77779_fwupdate_of_match); |
| |
| |
| static const struct platform_device_id max77779_fwupdate_id[] = { |
| {"max77779_fwupdate", 0}, |
| {} |
| }; |
| MODULE_DEVICE_TABLE(platform, max77779_fwupdate_id); |
| |
| static struct platform_driver max77779_fwupdate_driver = { |
| .driver = { |
| .name = "max77779_fwupdate", |
| .owner = THIS_MODULE, |
| .of_match_table = max77779_fwupdate_of_match, |
| .probe_type = PROBE_PREFER_ASYNCHRONOUS, |
| }, |
| .id_table = max77779_fwupdate_id, |
| .probe = max77779_fwupdate_probe, |
| .remove = max77779_fwupdate_remove, |
| }; |
| |
| module_platform_driver(max77779_fwupdate_driver); |
| |
| MODULE_DESCRIPTION("MAX77779 Firmware Update Driver"); |
| MODULE_AUTHOR("Chungro Lee <[email protected]>"); |
| MODULE_LICENSE("GPL"); |