/* Copyright 2020 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. */ #include "chipset.h" #include "ctn730.h" #include "common.h" #include "console.h" #include "gpio.h" #include "i2c.h" #include "peripheral_charger.h" #include "timer.h" #include "util.h" #include "watchdog.h" /* * Configuration */ /* Print additional data */ #define CTN730_DEBUG /* * When ctn730 is asleep, I2C is ignored but can wake it up. I2C will be resent * after this delay. */ static const int _wake_up_delay_ms = 10; /* Device detection interval */ static const int _detection_interval_ms = 500; /* Buffer size for i2c read & write */ #define CTN730_MESSAGE_BUFFER_SIZE 0x20 /* This driver isn't compatible with big endian. */ BUILD_ASSERT(__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__); #define CPRINTS(fmt, args...) cprints(CC_PCHG, "CTN730: " fmt, ##args) static const char *_text_instruction(uint8_t instruction) { /* TODO: For normal build, use %pb and BINARY_VALUE(res->inst, 6) */ switch (instruction) { case WLC_HOST_CTRL_RESET: return "RESET"; case WLC_HOST_CTRL_DL_OPEN_SESSION: return "DL_OPEN"; case WLC_HOST_CTRL_DL_COMMIT_SESSION: return "DL_COMMIT"; case WLC_HOST_CTRL_DL_WRITE_FLASH: return "DL_WRITE"; case WLC_HOST_CTRL_DUMP_STATUS: return "DUMP_STATUS"; case WLC_HOST_CTRL_GENERIC_ERROR: return "GENERIC_ERROR"; case WLC_HOST_CTRL_BIST: return "BIST"; case WLC_CHG_CTRL_ENABLE: return "ENABLE"; case WLC_CHG_CTRL_DISABLE: return "DISABLE"; case WLC_CHG_CTRL_DEVICE_STATE: return "DEVICE_STATE"; case WLC_CHG_CTRL_CHARGING_STATE: return "CHARGING_STATE"; case WLC_CHG_CTRL_CHARGING_INFO: return "CHARGING_INFO"; default: return "UNDEF"; } } static const char *_text_message_type(uint8_t type) { switch (type) { case CTN730_MESSAGE_TYPE_COMMAND: return "CMD"; case CTN730_MESSAGE_TYPE_RESPONSE: return "RSP"; case CTN730_MESSAGE_TYPE_EVENT: return "EVT"; default: return "BAD"; } } static const char *_text_status_code(uint8_t code) { switch (code) { case WLC_HOST_STATUS_OK: return "OK"; case WLC_HOST_STATUS_PARAMETER_ERROR: return "PARAMETER_ERR"; case WLC_HOST_STATUS_STATE_ERROR: return "STATE_ERR"; case WLC_HOST_STATUS_VALUE_ERROR: return "VALUE_ERR"; case WLC_HOST_STATUS_REJECTED: return "REJECTED"; case WLC_HOST_STATUS_RESOURCE_ERROR: return "RESOURCE_ERR"; case WLC_HOST_STATUS_TXLDO_ERROR: return "TXLDO_ERR"; case WLC_HOST_STATUS_ANTENNA_SELECTION_ERROR: return "ANTENNA_SELECTION_ERR"; case WLC_HOST_STATUS_BIST_FAILED: return "BIST_FAILED"; case WLC_HOST_STATUS_BIST_NO_WLC_CAP: return "BIST_NO_WLC_CAP"; case WLC_HOST_STATUS_BIST_TXLDO_CURRENT_OVERFLOW: return "BIST_TXLDO_CURRENT_OVERFLOW"; case WLC_HOST_STATUS_BIST_TXLDO_CURRENT_UNDERFLOW: return "BIST_TXLDO_CURRENT_UNDERFLOW"; case WLC_HOST_STATUS_FW_VERSION_ERROR: return "FW_VERSION_ERR"; case WLC_HOST_STATUS_FW_VERIFICATION_ERROR: return "FW_VERIFICATION_ERR"; case WLC_HOST_STATUS_NTAG_BLOCK_PARAMETER_ERROR: return "NTAG_BLOCK_PARAMETER_ERR"; case WLC_HOST_STATUS_NTAG_READ_ERROR: return "NTAG_READ_ERR"; default: return "UNDEF"; } } static const char *_text_reset_reason(uint8_t code) { switch (code) { case WLC_HOST_CTRL_RESET_REASON_INTENDED: return "intended"; case WLC_HOST_CTRL_RESET_REASON_CORRUPTED: return "corrupted"; case WLC_HOST_CTRL_RESET_REASON_UNRECOVERABLE: return "unrecoverable"; default: return "unknown"; } } static int _i2c_read(int i2c_port, uint8_t *in, int in_len) { int rv; memset(in, 0, in_len); rv = i2c_xfer(i2c_port, CTN730_I2C_ADDR, NULL, 0, in, in_len); if (rv) { msleep(_wake_up_delay_ms); rv = i2c_xfer(i2c_port, CTN730_I2C_ADDR, NULL, 0, in, in_len); } if (rv) CPRINTS("Failed to read: %d", rv); return rv; } static void _print_header(const struct ctn730_msg *msg) { CPRINTS("%s_%s", _text_instruction(msg->instruction), _text_message_type(msg->message_type)); } static int _send_command(struct pchg *ctx, const struct ctn730_msg *cmd) { int i2c_port = ctx->cfg->i2c_port; int rv; _print_header(cmd); rv = i2c_xfer(i2c_port, CTN730_I2C_ADDR, (void *)cmd, sizeof(*cmd) + cmd->length, NULL, 0); if (rv) { msleep(_wake_up_delay_ms); rv = i2c_xfer(i2c_port, CTN730_I2C_ADDR, (void *)cmd, sizeof(*cmd) + cmd->length, NULL, 0); } if (rv) CPRINTS("Failed to write: %d", rv); return rv; } static int ctn730_reset(struct pchg *ctx) { gpio_set_level(GPIO_WLC_NRST_CONN, 0); /* * Datasheet says minimum is 10 us. This is better not to be a sleep * especially if it's long (e.g. ~1 ms) since the PCHG state machine * may try to access the I2C bus, which is held low by ctn730. */ udelay(15); gpio_set_level(GPIO_WLC_NRST_CONN, 1); return EC_SUCCESS_IN_PROGRESS; } static int ctn730_init(struct pchg *ctx) { uint8_t buf[CTN730_MESSAGE_BUFFER_SIZE]; struct ctn730_msg *cmd = (void *)buf; int rv; cmd->message_type = CTN730_MESSAGE_TYPE_COMMAND; cmd->instruction = WLC_HOST_CTRL_RESET; cmd->length = WLC_HOST_CTRL_RESET_CMD_SIZE; cmd->payload[0] = ctx->mode == PCHG_MODE_NORMAL ? WLC_HOST_CTRL_RESET_CMD_MODE_NORMAL : WLC_HOST_CTRL_RESET_CMD_MODE_DOWNLOAD; /* TODO: Run 1 sec timeout timer. */ rv = _send_command(ctx, cmd); if (rv) return rv; /* WLC-host should send EVT_HOST_CTRL_RESET_EVT shortly. */ return EC_SUCCESS_IN_PROGRESS; } static int ctn730_enable(struct pchg *ctx, bool enable) { uint8_t buf[CTN730_MESSAGE_BUFFER_SIZE]; struct ctn730_msg *cmd = (void *)buf; uint16_t *interval = (void *)cmd->payload; int rv; cmd->message_type = CTN730_MESSAGE_TYPE_COMMAND; if (enable) { cmd->instruction = WLC_CHG_CTRL_ENABLE; cmd->length = WLC_CHG_CTRL_ENABLE_CMD_SIZE; /* Assume core is little endian. Use htole16 for portability. */ *interval = _detection_interval_ms; } else { cmd->instruction = WLC_CHG_CTRL_DISABLE; cmd->length = WLC_CHG_CTRL_DISABLE_CMD_SIZE; } rv = _send_command(ctx, cmd); if (rv) return rv; return EC_SUCCESS_IN_PROGRESS; } static int _process_payload_response(struct pchg *ctx, struct ctn730_msg *res) { uint8_t len = res->length; uint8_t buf[CTN730_MESSAGE_BUFFER_SIZE]; if (sizeof(buf) < len) { CPRINTS("Response size (%d) exceeds buffer", len); return EC_ERROR_OVERFLOW; } if (len > 0) { int rv = _i2c_read(ctx->cfg->i2c_port, buf, len); if (rv) return rv; if (IS_ENABLED(CTN730_DEBUG)) CPRINTS("Payload: %ph", HEX_BUF(buf, len)); } ctx->event = PCHG_EVENT_NONE; /* * Messages with no payload ( == 0) is allowed in the spec. So, * make sure is checked before reading buf[0]. */ switch (res->instruction) { case WLC_HOST_CTRL_RESET: if (len != WLC_HOST_CTRL_RESET_RSP_SIZE) return EC_ERROR_UNKNOWN; if (buf[0] != WLC_HOST_STATUS_OK) ctx->event = PCHG_EVENT_OTHER_ERROR; break; case WLC_HOST_CTRL_DL_OPEN_SESSION: if (len != WLC_HOST_CTRL_DL_OPEN_SESSION_RSP_SIZE) return EC_ERROR_UNKNOWN; if (buf[0] != WLC_HOST_STATUS_OK) { CPRINTS("FW open session failed for %s", _text_status_code(buf[0])); ctx->event = PCHG_EVENT_UPDATE_ERROR; ctx->error |= PCHG_ERROR_MASK(PCHG_ERROR_FW_VERSION); } else { ctx->event = PCHG_EVENT_UPDATE_OPENED; } break; case WLC_HOST_CTRL_DL_COMMIT_SESSION: if (len != WLC_HOST_CTRL_DL_COMMIT_SESSION_RSP_SIZE) return EC_ERROR_UNKNOWN; if (buf[0] != WLC_HOST_STATUS_OK) { CPRINTS("FW commit failed for %s", _text_status_code(buf[0])); ctx->event = PCHG_EVENT_UPDATE_ERROR; ctx->error |= PCHG_ERROR_MASK(PCHG_ERROR_INVALID_FW); } else { ctx->event = PCHG_EVENT_UPDATE_CLOSED; } break; case WLC_HOST_CTRL_DL_WRITE_FLASH: if (len != WLC_HOST_CTRL_DL_WRITE_FLASH_RSP_SIZE) return EC_ERROR_UNKNOWN; if (buf[0] != WLC_HOST_STATUS_OK) { CPRINTS("FW write failed for %s", _text_status_code(buf[0])); ctx->event = PCHG_EVENT_UPDATE_ERROR; ctx->error |= PCHG_ERROR_MASK(PCHG_ERROR_WRITE_FLASH); } else { ctx->event = PCHG_EVENT_UPDATE_WRITTEN; } break; case WLC_CHG_CTRL_ENABLE: if (len != WLC_CHG_CTRL_ENABLE_RSP_SIZE) return EC_ERROR_UNKNOWN; if (buf[0] != WLC_HOST_STATUS_OK) ctx->event = PCHG_EVENT_OTHER_ERROR; else ctx->event = PCHG_EVENT_ENABLED; break; case WLC_CHG_CTRL_DISABLE: if (len != WLC_CHG_CTRL_DISABLE_RSP_SIZE) return EC_ERROR_UNKNOWN; if (buf[0] != WLC_HOST_STATUS_OK) ctx->event = PCHG_EVENT_OTHER_ERROR; else ctx->event = PCHG_EVENT_DISABLED; break; case WLC_CHG_CTRL_CHARGING_INFO: if (len != WLC_CHG_CTRL_CHARGING_INFO_RSP_SIZE) return EC_ERROR_UNKNOWN; if (buf[0] != WLC_HOST_STATUS_OK) { ctx->event = PCHG_EVENT_OTHER_ERROR; } else { ctx->battery_percent = buf[1]; ctx->event = PCHG_EVENT_CHARGE_UPDATE; } break; default: CPRINTS("Received unknown response (%d)", res->instruction); break; } return EC_SUCCESS; } static int _process_payload_event(struct pchg *ctx, struct ctn730_msg *res) { uint8_t len = res->length; uint8_t buf[CTN730_MESSAGE_BUFFER_SIZE]; if (sizeof(buf) < len) { CPRINTS("Response size (%d) exceeds buffer", len); return EC_ERROR_OVERFLOW; } if (len > 0) { int rv = _i2c_read(ctx->cfg->i2c_port, buf, len); if (rv) return rv; if (IS_ENABLED(CTN730_DEBUG)) CPRINTS("Payload: %ph", HEX_BUF(buf, len)); } ctx->event = PCHG_EVENT_NONE; /* * Messages with no payload ( == 0) is allowed in the spec. So, * make sure is checked before reading buf[0]. */ switch (res->instruction) { case WLC_HOST_CTRL_RESET: if (len < WLC_HOST_CTRL_RESET_EVT_MIN_SIZE) return EC_ERROR_INVAL; if (buf[0] == WLC_HOST_CTRL_RESET_EVT_NORMAL_MODE) { if (len != WLC_HOST_CTRL_RESET_EVT_NORMAL_MODE_SIZE) return EC_ERROR_INVAL; ctx->event = PCHG_EVENT_IN_NORMAL; ctx->fw_version = (uint16_t)buf[1] << 8 | buf[2]; CPRINTS("Normal Mode (FW=0x%02x.%02x)", buf[1], buf[2]); /* * ctn730 isn't immediately ready for i2c write after * normal mode initialization (b:178096436). */ msleep(5); } else if (buf[0] == WLC_HOST_CTRL_RESET_EVT_DOWNLOAD_MODE) { if (len != WLC_HOST_CTRL_RESET_EVT_DOWNLOAD_MODE_SIZE) return EC_ERROR_INVAL; CPRINTS("Download Mode (%s)", _text_reset_reason(buf[1])); ctx->event = PCHG_EVENT_RESET; } else { return EC_ERROR_INVAL; } break; case WLC_HOST_CTRL_GENERIC_ERROR: break; case WLC_CHG_CTRL_DISABLE: if (len != WLC_CHG_CTRL_DISABLE_EVT_SIZE) return EC_ERROR_INVAL; ctx->event = PCHG_EVENT_DISABLED; break; case WLC_CHG_CTRL_DEVICE_STATE: if (len < WLC_CHG_CTRL_DEVICE_STATE_EVT_SIZE) return EC_ERROR_INVAL; switch (buf[0]) { case WLC_CHG_CTRL_DEVICE_STATE_DEVICE_DOCKED: if (len != WLC_CHG_CTRL_DEVICE_STATE_EVT_SIZE) return EC_ERROR_INVAL; ctx->event = PCHG_EVENT_DEVICE_DETECTED; break; case WLC_CHG_CTRL_DEVICE_STATE_DEVICE_DETECTED: if (len != WLC_CHG_CTRL_DEVICE_STATE_EVT_SIZE_DETECTED) return EC_ERROR_INVAL; ctx->event = PCHG_EVENT_DEVICE_CONNECTED; break; case WLC_CHG_CTRL_DEVICE_STATE_DEVICE_DEVICE_LOST: case WLC_CHG_CTRL_DEVICE_STATE_DEVICE_UNDOCKED: if (len != WLC_CHG_CTRL_DEVICE_STATE_EVT_SIZE) return EC_ERROR_INVAL; ctx->event = PCHG_EVENT_DEVICE_LOST; break; default: return EC_ERROR_INVAL; } break; case WLC_CHG_CTRL_CHARGING_STATE: if (len != WLC_CHG_CTRL_CHARGING_STATE_EVT_SIZE) return EC_ERROR_INVAL; switch (buf[0]) { case WLC_CHG_CTRL_CHARGING_STATE_CHARGE_STARTED: ctx->event = PCHG_EVENT_CHARGE_STARTED; break; case WLC_CHG_CTRL_CHARGING_STATE_CHARGE_ENDED: ctx->event = PCHG_EVENT_CHARGE_ENDED; break; case WLC_CHG_CTRL_CHARGING_STATE_CHARGE_STOPPED: /* Includes over temp., DISABLE_CMD, device removal. */ ctx->event = PCHG_EVENT_CHARGE_STOPPED; break; default: return EC_ERROR_INVAL; } break; case WLC_CHG_CTRL_CHARGING_INFO: if (len != WLC_CHG_CTRL_CHARGING_INFO_EVT_SIZE || buf[0] > 100) return EC_ERROR_INVAL; ctx->event = PCHG_EVENT_CHARGE_UPDATE; ctx->battery_percent = buf[0]; break; default: CPRINTS("Received unknown event (%d)", res->instruction); break; } return EC_SUCCESS; } static int ctn730_get_event(struct pchg *ctx) { struct ctn730_msg res; int i2c_port = ctx->cfg->i2c_port; int rv; /* Read message header */ rv = _i2c_read(i2c_port, (uint8_t *)&res, sizeof(res)); if (rv) return rv; _print_header(&res); if (res.message_type == CTN730_MESSAGE_TYPE_RESPONSE) { /* TODO: Check 1 sec timeout. */ return _process_payload_response(ctx, &res); } else if (res.message_type == CTN730_MESSAGE_TYPE_EVENT) { return _process_payload_event(ctx, &res); } CPRINTS("Invalid message type (%d)", res.message_type); return EC_ERROR_UNKNOWN; } static int ctn730_get_soc(struct pchg *ctx) { struct ctn730_msg cmd; int rv; cmd.message_type = CTN730_MESSAGE_TYPE_COMMAND; cmd.instruction = WLC_CHG_CTRL_CHARGING_INFO; cmd.length = WLC_CHG_CTRL_CHARGING_INFO_CMD_SIZE; rv = _send_command(ctx, &cmd); if (rv) return rv; return EC_SUCCESS_IN_PROGRESS; } static int ctn730_update_open(struct pchg *ctx) { uint8_t buf[sizeof(struct ctn730_msg) + WLC_HOST_CTRL_DL_OPEN_SESSION_CMD_SIZE]; struct ctn730_msg *cmd = (void *)buf; uint32_t version = ctx->update.version; int rv; cmd->message_type = CTN730_MESSAGE_TYPE_COMMAND; cmd->instruction = WLC_HOST_CTRL_DL_OPEN_SESSION; cmd->length = WLC_HOST_CTRL_DL_OPEN_SESSION_CMD_SIZE; cmd->payload[0] = (version >> 8) & 0xff; cmd->payload[1] = version & 0xff; rv = _send_command(ctx, cmd); if (rv) return rv; return EC_SUCCESS_IN_PROGRESS; } static int ctn730_update_write(struct pchg *ctx) { uint8_t buf[sizeof(struct ctn730_msg) + WLC_HOST_CTRL_DL_WRITE_FLASH_CMD_SIZE]; struct ctn730_msg *cmd = (void *)buf; uint32_t *a = (void *)cmd->payload; uint8_t *d = (void *)&cmd->payload[CTN730_FLASH_ADDR_SIZE]; int rv; /* Address is 3 bytes. FW size must be a multiple of 128 bytes. */ if (ctx->update.addr & GENMASK(31, 24) || ctx->update.size != WLC_HOST_CTRL_DL_WRITE_FLASH_BLOCK_SIZE) return EC_ERROR_INVAL; cmd->message_type = CTN730_MESSAGE_TYPE_COMMAND; cmd->instruction = WLC_HOST_CTRL_DL_WRITE_FLASH; cmd->length = WLC_HOST_CTRL_DL_WRITE_FLASH_CMD_SIZE; /* 4th byte will be overwritten by memcpy below. */ *a = ctx->update.addr; /* Store data in payload with 0-padding for short blocks. */ memset(d, 0, WLC_HOST_CTRL_DL_WRITE_FLASH_BLOCK_SIZE); memcpy(d, ctx->update.data, ctx->update.size); rv = _send_command(ctx, cmd); if (rv) return rv; return EC_SUCCESS_IN_PROGRESS; } static int ctn730_update_close(struct pchg *ctx) { uint8_t buf[sizeof(struct ctn730_msg) + WLC_HOST_CTRL_DL_COMMIT_SESSION_CMD_SIZE]; struct ctn730_msg *cmd = (void *)buf; uint32_t *crc32 = (void *)cmd->payload; int rv; cmd->message_type = CTN730_MESSAGE_TYPE_COMMAND; cmd->instruction = WLC_HOST_CTRL_DL_COMMIT_SESSION; cmd->length = WLC_HOST_CTRL_DL_COMMIT_SESSION_CMD_SIZE; *crc32 = ctx->update.crc32; rv = _send_command(ctx, cmd); if (rv) return rv; return EC_SUCCESS_IN_PROGRESS; } /** * Send command in blocking loop * * @param ctx PCHG port context * @param buf [IN] Command header and payload to send. * [OUT] Response header and payload received. * @param buf_len Size of * @return enum ec_error_list */ static int _send_command_blocking(struct pchg *ctx, uint8_t *buf, int buf_len) { int i2c_port = ctx->cfg->i2c_port; int irq_pin = ctx->cfg->irq_pin; struct ctn730_msg *cmd = (void *)buf; struct ctn730_msg *res = (void *)buf; timestamp_t deadline; int rv; gpio_disable_interrupt(irq_pin); rv = _send_command(ctx, cmd); if (rv) goto exit; deadline.val = get_time().val + CTN730_COMMAND_TIME_OUT; /* Busy loop */ while (gpio_get_level(irq_pin) == 0 && !rv) { udelay(1 * MSEC); rv = timestamp_expired(deadline, NULL); watchdog_reload(); } if (rv) { ccprintf("Response timeout\n"); rv = EC_ERROR_TIMEOUT; goto exit; } rv = _i2c_read(i2c_port, buf, sizeof(*res)); if (rv) goto exit; _print_header(res); if (sizeof(*res) + res->length > buf_len) { ccprintf("RSP size exceeds buffer\n"); rv = EC_ERROR_OVERFLOW; goto exit; } rv = _i2c_read(i2c_port, res->payload, res->length); if (rv) goto exit; exit: gpio_clear_pending_interrupt(irq_pin); gpio_enable_interrupt(irq_pin); return rv; } const struct pchg_drv ctn730_drv = { .reset = ctn730_reset, .init = ctn730_init, .enable = ctn730_enable, .get_event = ctn730_get_event, .get_soc = ctn730_get_soc, .update_open = ctn730_update_open, .update_write = ctn730_update_write, .update_close = ctn730_update_close, }; static int cc_ctn730(int argc, char **argv) { int port; char *end; uint8_t buf[CTN730_MESSAGE_BUFFER_SIZE]; struct ctn730_msg *cmd = (void *)buf; struct ctn730_msg *res = (void *)buf; int rv; if (argc < 4) return EC_ERROR_PARAM_COUNT; port = strtoi(argv[1], &end, 0); if (*end || port < 0 || pchg_count <= port) return EC_ERROR_PARAM2; cmd->message_type = CTN730_MESSAGE_TYPE_COMMAND; if (!strcasecmp(argv[2], "dump")) { int tag = strtoi(argv[3], &end, 0); if (*end || tag < 0 || 0x07 < tag) return EC_ERROR_PARAM3; cmd->instruction = WLC_HOST_CTRL_DUMP_STATUS; cmd->length = WLC_HOST_CTRL_DUMP_STATUS_CMD_SIZE; cmd->payload[0] = tag; } else if (!strcasecmp(argv[2], "bist")) { int id = strtoi(argv[3], &end, 0); if (*end || id < 0) return EC_ERROR_PARAM3; cmd->instruction = WLC_HOST_CTRL_BIST; cmd->payload[0] = id; switch (id) { case 0x01: /* Switch on RF field. Tx driver conf not implemented */ cmd->length = 1; break; case 0x04: /* WLC device activation test */ cmd->length = 1; break; default: return EC_ERROR_PARAM3; } } else { return EC_ERROR_PARAM2; } rv = _send_command_blocking(&pchgs[port], buf, sizeof(buf)); if (rv) return rv; ccprintf("STATUS_%s\n", _text_status_code(res->payload[0])); hexdump(res->payload, res->length); return EC_SUCCESS; } DECLARE_CONSOLE_COMMAND(ctn730, cc_ctn730, " dump/bist " "\n\t dump " "\n\t bist ", "Control ctn730");