blob: 5e987375651b1206d19f58df6487829334c385d3 [file] [log] [blame]
/* Copyright 2019 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.
*/
/*
* IDT P9221-R7 Wireless Power Receiver driver.
*/
#include "p9221.h"
#include "charge_manager.h"
#include "chipset.h"
#include "common.h"
#include "console.h"
#include "gpio.h"
#include "hooks.h"
#include "power.h"
#include "tcpm.h"
#include "timer.h"
#include "usb_charge.h"
#include "usb_pd.h"
#include "util.h"
#include <stdbool.h>
#include "printf.h"
#define CPRINTS(format, args...) cprints(CC_USBPD, "WPC " format, ## args)
#define P9221_TX_TIMEOUT_MS (20 * 1000*1000)
#define P9221_DCIN_TIMEOUT_MS (2 * 1000*1000)
#define P9221_VRECT_TIMEOUT_MS (2 * 1000*1000)
#define P9221_NOTIFIER_DELAY_MS (80*1000)
#define P9221R7_ILIM_MAX_UA (1600 * 1000)
#define P9221R7_OVER_CHECK_NUM 3
#define OVC_LIMIT 1
#define OVC_THRESHOLD 1400000
#define OVC_BACKOFF_LIMIT 900000
#define OVC_BACKOFF_AMOUNT 100000
/* P9221 parameters */
static struct wpc_charger_info p9221_charger_info = {
.online = false,
.i2c_port = I2C_PORT_WPC,
.pp_buf_valid = false,
};
static struct wpc_charger_info *wpc = &p9221_charger_info;
static void p9221_set_offline(void);
static const uint32_t p9221_ov_set_lut[] = {
17000000, 20000000, 15000000, 13000000,
11000000, 11000000, 11000000, 11000000
};
static int p9221_reg_is_8_bit(uint16_t reg)
{
switch (reg) {
case P9221_CHIP_REVISION_REG:
case P9221R7_VOUT_SET_REG:
case P9221R7_ILIM_SET_REG:
case P9221R7_CHARGE_STAT_REG:
case P9221R7_EPT_REG:
case P9221R7_SYSTEM_MODE_REG:
case P9221R7_COM_CHAN_RESET_REG:
case P9221R7_COM_CHAN_SEND_SIZE_REG:
case P9221R7_COM_CHAN_SEND_IDX_REG:
case P9221R7_COM_CHAN_RECV_SIZE_REG:
case P9221R7_COM_CHAN_RECV_IDX_REG:
case P9221R7_DEBUG_REG:
case P9221R7_EPP_Q_FACTOR_REG:
case P9221R7_EPP_TX_GUARANTEED_POWER_REG:
case P9221R7_EPP_TX_POTENTIAL_POWER_REG:
case P9221R7_EPP_TX_CAPABILITY_FLAGS_REG:
case P9221R7_EPP_RENEGOTIATION_REG:
case P9221R7_EPP_CUR_RPP_HEADER_REG:
case P9221R7_EPP_CUR_NEGOTIATED_POWER_REG:
case P9221R7_EPP_CUR_MAXIMUM_POWER_REG:
case P9221R7_EPP_CUR_FSK_MODULATION_REG:
case P9221R7_EPP_REQ_RPP_HEADER_REG:
case P9221R7_EPP_REQ_NEGOTIATED_POWER_REG:
case P9221R7_EPP_REQ_MAXIMUM_POWER_REG:
case P9221R7_EPP_REQ_FSK_MODULATION_REG:
case P9221R7_VRECT_TARGET_REG:
case P9221R7_VRECT_KNEE_REG:
case P9221R7_FOD_SECTION_REG:
case P9221R7_VRECT_ADJ_REG:
case P9221R7_ALIGN_X_ADC_REG:
case P9221R7_ALIGN_Y_ADC_REG:
case P9221R7_ASK_MODULATION_DEPTH_REG:
case P9221R7_OVSET_REG:
case P9221R7_EPP_TX_SPEC_REV_REG:
return true;
default:
return false;
}
}
static int p9221_read8(uint16_t reg, int *val)
{
return i2c_read_offset16(wpc->i2c_port, P9221_R7_ADDR_FLAGS,
reg, val, 1);
}
static int p9221_write8(uint16_t reg, int val)
{
return i2c_write_offset16(wpc->i2c_port, P9221_R7_ADDR_FLAGS,
reg, val, 1);
}
static int p9221_read16(uint16_t reg, int *val)
{
return i2c_read_offset16(wpc->i2c_port, P9221_R7_ADDR_FLAGS,
reg, val, 2);
}
static int p9221_write16(uint16_t reg, int val)
{
return i2c_write_offset16(wpc->i2c_port, P9221_R7_ADDR_FLAGS,
reg, val, 2);
}
static int p9221_block_read(uint16_t reg, uint8_t *data, int len)
{
return i2c_read_offset16_block(wpc->i2c_port, P9221_R7_ADDR_FLAGS,
reg, data, len);
}
static int p9221_block_write(uint16_t reg, uint8_t *data, int len)
{
return i2c_write_offset16_block(wpc->i2c_port, P9221_R7_ADDR_FLAGS,
reg, data, len);
}
static int p9221_set_cmd_reg(uint8_t cmd)
{
int cur_cmd;
int retry;
int ret;
for (retry = 0; retry < P9221_COM_CHAN_RETRIES; retry++) {
ret = p9221_read8(P9221_COM_REG, &cur_cmd);
if (ret == EC_SUCCESS && cur_cmd == 0)
break;
msleep(25);
}
if (retry >= P9221_COM_CHAN_RETRIES) {
CPRINTS("Failed to wait for cmd free %02x", cur_cmd);
return EC_ERROR_TIMEOUT;
}
ret = p9221_write8(P9221_COM_REG, cmd);
if (ret)
CPRINTS("Failed to set cmd reg %02x: %d", cmd, ret);
return ret;
}
/* Convert a register value to uV, Hz, or uA */
static int p9221_convert_reg_r7(uint16_t reg, uint16_t raw_data, uint32_t *val)
{
switch (reg) {
case P9221R7_ALIGN_X_ADC_REG: /* raw */
case P9221R7_ALIGN_Y_ADC_REG: /* raw */
*val = raw_data;
break;
case P9221R7_VOUT_ADC_REG: /* 12-bit ADC raw */
case P9221R7_IOUT_ADC_REG: /* 12-bit ADC raw */
case P9221R7_DIE_TEMP_ADC_REG: /* 12-bit ADC raw */
case P9221R7_EXT_TEMP_REG:
*val = raw_data & 0xFFF;
break;
case P9221R7_VOUT_SET_REG: /* 0.1V -> uV */
*val = raw_data * 100 * 1000;
break;
case P9221R7_IOUT_REG: /* mA -> uA */
case P9221R7_VRECT_REG: /* mV -> uV */
case P9221R7_VOUT_REG: /* mV -> uV */
case P9221R7_OP_FREQ_REG: /* kHz -> Hz */
case P9221R7_TX_PINGFREQ_REG: /* kHz -> Hz */
*val = raw_data * 1000;
break;
case P9221R7_ILIM_SET_REG: /* 100mA -> uA, 200mA offset */
*val = ((raw_data * 100) + 200) * 1000;
break;
case P9221R7_OVSET_REG: /* uV */
raw_data &= P9221R7_OVSET_MASK;
*val = p9221_ov_set_lut[raw_data];
break;
default:
return -2;
}
return 0;
}
static int p9221_reg_read_converted(uint16_t reg, uint32_t *val)
{
int ret;
int data;
if (p9221_reg_is_8_bit(reg))
ret = p9221_read8(reg, &data);
else
ret = p9221_read16(reg, &data);
if (ret)
return ret;
return p9221_convert_reg_r7(reg, data, val);
}
static int p9221_is_online(void)
{
int chip_id;
if (p9221_read16(P9221_CHIP_ID_REG, &chip_id)
|| chip_id != P9221_CHIP_ID)
return false;
else
return true;
}
int wpc_chip_is_online(void)
{
return p9221_is_online();
}
void p9221_interrupt(enum gpio_signal signal)
{
task_wake(TASK_ID_WPC);
}
static int p9221r7_clear_interrupts(uint16_t mask)
{
int ret;
ret = p9221_write16(P9221R7_INT_CLEAR_REG, mask);
if (ret) {
CPRINTS("Failed to clear INT reg: %d", ret);
return ret;
}
ret = p9221_set_cmd_reg(P9221_COM_CLEAR_INT_MASK);
if (ret)
CPRINTS("Failed to reset INT: %d", ret);
return ret;
}
/*
* Enable interrupts on the P9221 R7, note we don't really need to disable
* interrupts since when the device goes out of field, the P9221 is reset.
*/
static int p9221_enable_interrupts_r7(void)
{
uint16_t mask = 0;
int ret;
CPRINTS("Enable interrupts");
mask = P9221R7_STAT_LIMIT_MASK | P9221R7_STAT_CC_MASK
| P9221_STAT_VRECT;
p9221r7_clear_interrupts(mask);
ret = p9221_write8(P9221_INT_ENABLE_REG, mask);
if (ret)
CPRINTS("Failed to enable INTs: %d", ret);
return ret;
}
static int p9221_send_csp(uint8_t status)
{
int ret;
CPRINTS("Send CSP=%d", status);
mutex_lock(&wpc->cmd_lock);
ret = p9221_write8(P9221R7_CHARGE_STAT_REG, status);
if (ret == EC_SUCCESS)
ret = p9221_set_cmd_reg(P9221R7_COM_SENDCSP);
mutex_unlock(&wpc->cmd_lock);
return ret;
}
static int p9221_send_eop(uint8_t reason)
{
int rv;
CPRINTS("Send EOP reason=%d", reason);
mutex_lock(&wpc->cmd_lock);
rv = p9221_write8(P9221R7_EPT_REG, reason);
if (rv == EC_SUCCESS)
rv = p9221_set_cmd_reg(P9221R7_COM_SENDEPT);
mutex_unlock(&wpc->cmd_lock);
return rv;
}
static void print_current_samples(uint32_t *iout_val, int count)
{
int i;
char temp[P9221R7_OVER_CHECK_NUM * 9 + 1] = { 0 };
for (i = 0; i < count ; i++)
snprintf(temp + i * 9, sizeof(temp) - i * 9,
"%08x ", iout_val[i]);
CPRINTS("OVER IOUT_SAMPLES: %s", temp);
}
/*
* Number of times to poll the status to see if the current limit condition
* was transient or not.
*/
static void p9221_limit_handler_r7(uint16_t orign_irq_src)
{
uint8_t reason;
int i;
int ret;
int ovc_count = 0;
uint32_t iout_val[P9221R7_OVER_CHECK_NUM] = { 0 };
int irq_src = (int)orign_irq_src;
CPRINTS("OVER INT: %02x", irq_src);
if (irq_src & P9221R7_STAT_OVV) {
reason = P9221_EOP_OVER_VOLT;
goto send_eop;
}
if (irq_src & P9221R7_STAT_OVT) {
reason = P9221_EOP_OVER_TEMP;
goto send_eop;
}
if ((irq_src & P9221R7_STAT_UV) && !(irq_src & P9221R7_STAT_OVC))
return;
reason = P9221_EOP_OVER_CURRENT;
for (i = 0; i < P9221R7_OVER_CHECK_NUM; i++) {
ret = p9221r7_clear_interrupts(
irq_src & P9221R7_STAT_LIMIT_MASK);
msleep(50);
if (ret)
continue;
ret = p9221_reg_read_converted(P9221R7_IOUT_REG, &iout_val[i]);
if (ret) {
CPRINTS("Failed to read IOUT[%d]: %d", i, ret);
continue;
} else if (iout_val[i] > OVC_THRESHOLD) {
ovc_count++;
}
ret = p9221_read16(P9221_STATUS_REG, &irq_src);
if (ret) {
CPRINTS("Failed to read status: %d", ret);
continue;
}
if ((irq_src & P9221R7_STAT_OVC) == 0) {
print_current_samples(iout_val, i + 1);
CPRINTS("OVER condition %04x cleared after %d tries",
irq_src, i);
return;
}
CPRINTS("OVER status is still %04x, retry", irq_src);
}
if (ovc_count < OVC_LIMIT) {
print_current_samples(iout_val, P9221R7_OVER_CHECK_NUM);
CPRINTS("ovc_threshold=%d, ovc_count=%d, ovc_limit=%d",
OVC_THRESHOLD, ovc_count, OVC_LIMIT);
return;
}
send_eop:
CPRINTS("OVER is %04x, sending EOP %d", irq_src, reason);
ret = p9221_send_eop(reason);
if (ret)
CPRINTS("Failed to send EOP %d: %d", reason, ret);
}
static void p9221_abort_transfers(void)
{
wpc->tx_busy = false;
wpc->tx_done = true;
wpc->rx_done = true;
wpc->rx_len = 0;
}
/* Handler for r7 and R7 chips */
static void p9221r7_irq_handler(uint16_t irq_src)
{
int res;
if (irq_src & P9221R7_STAT_LIMIT_MASK)
p9221_limit_handler_r7(irq_src);
/* Receive complete */
if (irq_src & P9221R7_STAT_CCDATARCVD) {
int rxlen = 0;
res = p9221_read8(P9221R7_COM_CHAN_RECV_SIZE_REG, &rxlen);
if (res)
CPRINTS("Failed to read len: %d", res);
if (rxlen) {
res = p9221_block_read(P9221R7_DATA_RECV_BUF_START,
wpc->rx_buf, rxlen);
if (res) {
CPRINTS("Failed to read CC data: %d", res);
rxlen = 0;
}
wpc->rx_len = rxlen;
wpc->rx_done = true;
}
}
/* Send complete */
if (irq_src & P9221R7_STAT_CCSENDBUSY) {
wpc->tx_busy = false;
wpc->tx_done = true;
}
/* Proprietary packet */
if (irq_src & P9221R7_STAT_PPRCVD) {
res = p9221_block_read(P9221R7_DATA_RECV_BUF_START,
wpc->pp_buf, sizeof(wpc->pp_buf));
if (res) {
CPRINTS("Failed to read PP: %d", res);
wpc->pp_buf_valid = false;
return;
}
/* We only care about PP which come with 0x4F header */
wpc->pp_buf_valid = (wpc->pp_buf[0] == 0x4F);
hexdump(wpc->pp_buf, sizeof(wpc->pp_buf));
}
/* CC Reset complete */
if (irq_src & P9221R7_STAT_CCRESET)
p9221_abort_transfers();
}
static int p9221_is_epp(void)
{
int ret, reg;
uint32_t vout_uv;
if (p9221_read8(P9221R7_SYSTEM_MODE_REG, &reg) == EC_SUCCESS)
return reg & P9221R7_SYSTEM_MODE_EXTENDED_MASK;
/* Check based on power supply voltage */
ret = p9221_reg_read_converted(P9221R7_VOUT_ADC_REG, &vout_uv);
if (ret) {
CPRINTS("Failed to read VOUT_ADC: %d", ret);
return false;
}
CPRINTS("Voltage is %duV", vout_uv);
if (vout_uv > P9221_EPP_THRESHOLD_UV)
return true;
return false;
}
static void p9221_config_fod(void)
{
int epp;
uint8_t *fod;
int fod_len;
int ret;
int retries = 3;
CPRINTS("Config FOD");
epp = p9221_is_epp();
fod_len = epp ? board_get_epp_fod(&fod) : board_get_fod(&fod);
if (!fod_len || !fod) {
CPRINTS("FOD data not found");
return;
}
while (retries) {
uint8_t fod_read[fod_len];
CPRINTS("Writing %s FOD (n=%d try=%d)",
epp ? "EPP" : "BPP", fod_len, retries);
ret = p9221_block_write(P9221R7_FOD_REG, fod, fod_len);
if (ret)
goto no_fod;
/* Verify the FOD has been written properly */
ret = p9221_block_read(P9221R7_FOD_REG, fod_read, fod_len);
if (ret)
goto no_fod;
if (memcmp(fod, fod_read, fod_len) == 0)
return;
hexdump(fod_read, fod_len);
retries--;
msleep(100);
}
no_fod:
CPRINTS("Failed to set FOD. retries:%d ret:%d", retries, ret);
}
static void p9221_set_online(void)
{
int ret;
CPRINTS("Set online");
wpc->online = true;
wpc->tx_busy = false;
wpc->tx_done = true;
wpc->rx_done = false;
wpc->charge_supplier = CHARGE_SUPPLIER_WPC_BPP;
ret = p9221_enable_interrupts_r7();
if (ret)
CPRINTS("Failed to enable INT: %d", ret);
/* NOTE: depends on _is_epp() which is not valid until DC_IN */
p9221_config_fod();
}
static void p9221_vbus_check_timeout(void)
{
CPRINTS("Timeout VBUS, online=%d", wpc->online);
if (wpc->online)
p9221_set_offline();
}
DECLARE_DEFERRED(p9221_vbus_check_timeout);
static void p9221_set_offline(void)
{
CPRINTS("Set offline");
wpc->online = false;
/* Reset PP buf so we can get a new serial number next time around */
wpc->pp_buf_valid = false;
p9221_abort_transfers();
hook_call_deferred(&p9221_vbus_check_timeout_data, -1);
}
/* P9221_NOTIFIER_DELAY_MS from VRECTON */
static int p9221_notifier_check_det(void)
{
if (wpc->online)
goto done;
/* send out a FOD but is_epp() is still invalid */
p9221_set_online();
/* Give the vbus 2 seconds to come up. */
CPRINTS("Waiting VBUS");
hook_call_deferred(&p9221_vbus_check_timeout_data, -1);
hook_call_deferred(&p9221_vbus_check_timeout_data,
P9221_DCIN_TIMEOUT_MS);
done:
wpc->p9221_check_det = false;
return 0;
}
static int p9221_get_charge_supplier(void)
{
if (!wpc->online)
return EC_ERROR_UNKNOWN;
if (p9221_is_epp()) {
uint32_t tx_id;
int txmf_id;
int ret;
wpc->charge_supplier = CHARGE_SUPPLIER_WPC_EPP;
ret = p9221_read16(P9221R7_EPP_TX_MFG_CODE_REG, &txmf_id);
if (ret || txmf_id != P9221_GPP_TX_MF_ID)
return ret;
ret = p9221_block_read(P9221R7_PROP_TX_ID_REG,
(uint8_t *) &tx_id,
P9221R7_PROP_TX_ID_SIZE);
if (ret)
return ret;
if (tx_id & P9221R7_PROP_TX_ID_GPP_MASK)
wpc->charge_supplier = CHARGE_SUPPLIER_WPC_GPP;
CPRINTS("txmf_id=0x%04x tx_id=0x%08x supplier=%d",
txmf_id, tx_id, wpc->charge_supplier);
} else {
wpc->charge_supplier = CHARGE_SUPPLIER_WPC_BPP;
CPRINTS("supplier=%d", wpc->charge_supplier);
}
return EC_SUCCESS;
}
static int p9221_get_icl(int charge_supplier)
{
switch (charge_supplier) {
case CHARGE_SUPPLIER_WPC_EPP:
case CHARGE_SUPPLIER_WPC_GPP:
return P9221_DC_ICL_EPP_MA;
case CHARGE_SUPPLIER_WPC_BPP:
default:
return P9221_DC_ICL_BPP_MA;
}
}
static int p9221_get_ivl(int charge_supplier)
{
switch (charge_supplier) {
case CHARGE_SUPPLIER_WPC_EPP:
case CHARGE_SUPPLIER_WPC_GPP:
return P9221_DC_IVL_EPP_MV;
case CHARGE_SUPPLIER_WPC_BPP:
default:
return P9221_DC_IVL_BPP_MV;
}
}
static void p9221_update_charger(int type, struct charge_port_info *chg)
{
if (!chg)
charge_manager_update_dualrole(0, CAP_UNKNOWN);
else
charge_manager_update_dualrole(0, CAP_DEDICATED);
charge_manager_update_charge(type, 0, chg);
}
static int p9221_reg_write_converted_r7(uint16_t reg, uint32_t val)
{
int ret = 0;
uint16_t data;
int i;
/* Do the appropriate conversion */
switch (reg) {
case P9221R7_ILIM_SET_REG:
/* uA -> 0.1A, offset 0.2A */
if ((val < 200000) || (val > 1600000))
return -EC_ERROR_INVAL;
data = (val / (100 * 1000)) - 2;
break;
case P9221R7_VOUT_SET_REG:
/* uV -> 0.1V */
val /= 1000;
if (val < 3500 || val > 9000)
return -EC_ERROR_INVAL;
data = val / 100;
break;
case P9221R7_OVSET_REG:
/* uV */
for (i = 0; i < ARRAY_SIZE(p9221_ov_set_lut); i++) {
if (val == p9221_ov_set_lut[i])
break;
}
if (i == ARRAY_SIZE(p9221_ov_set_lut))
return -EC_ERROR_INVAL;
data = i;
break;
default:
return -EC_ERROR_INVAL;
}
if (p9221_reg_is_8_bit(reg))
ret = p9221_write8(reg, data);
else
ret = p9221_write16(reg, data);
return ret;
}
static int p9221_set_dc_icl(void)
{
/* Increase the IOUT limit */
if (p9221_reg_write_converted_r7(P9221R7_ILIM_SET_REG,
P9221R7_ILIM_MAX_UA))
CPRINTS("%s set rx_iout limit fail.", __func__);
return EC_SUCCESS;
}
static void p9221_notifier_check_vbus(void)
{
struct charge_port_info chg;
wpc->p9221_check_vbus = false;
CPRINTS("%s online:%d vbus:%d", __func__, wpc->online,
wpc->vbus_status);
/*
* We now have confirmation from DC_IN, kill the timer, p9221_online
* will be set by this function.
*/
hook_call_deferred(&p9221_vbus_check_timeout_data, -1);
if (wpc->vbus_status) {
/* WPC VBUS on ,Always write FOD, check dc_icl, send CSP */
p9221_set_dc_icl();
p9221_config_fod();
p9221_send_csp(1);
/* when wpc vbus attached after 2s, set wpc online */
if (!wpc->online)
p9221_set_online();
/* WPC VBUS on , update charge voltage and current */
p9221_get_charge_supplier();
chg.voltage = p9221_get_ivl(wpc->charge_supplier);
chg.current = p9221_get_icl(wpc->charge_supplier);
p9221_update_charger(wpc->charge_supplier, &chg);
} else {
/*
* Vbus detached, set wpc offline and update wpc charge voltage
* and current to zero.
*/
if (wpc->online) {
p9221_set_offline();
p9221_update_charger(wpc->charge_supplier, NULL);
}
}
CPRINTS("check_vbus changed on:%d vbus:%d", wpc->online,
wpc->vbus_status);
}
static void p9221_detect_work(void)
{
CPRINTS("%s online:%d check_vbus:%d check_det:%d vbus:%d", __func__,
wpc->online, wpc->p9221_check_vbus, wpc->p9221_check_det,
wpc->vbus_status);
/* Step 1 */
if (wpc->p9221_check_det)
p9221_notifier_check_det();
/* Step 2 */
if (wpc->p9221_check_vbus)
p9221_notifier_check_vbus();
}
DECLARE_DEFERRED(p9221_detect_work);
void p9221_notify_vbus_change(int vbus)
{
wpc->p9221_check_vbus = true;
wpc->vbus_status = vbus;
hook_call_deferred(&p9221_detect_work_data, P9221_NOTIFIER_DELAY_MS);
}
void wireless_power_charger_task(void *u)
{
while (1) {
int ret, irq_src;
task_wait_event(-1);
ret = p9221_read16(P9221_INT_REG, &irq_src);
if (ret) {
CPRINTS("Failed to read INT REG");
continue;
}
CPRINTS("INT SRC 0x%04x", irq_src);
if (p9221r7_clear_interrupts(irq_src))
continue;
if (irq_src & P9221_STAT_VRECT) {
CPRINTS("VRECTON, online=%d", wpc->online);
if (!wpc->online) {
wpc->p9221_check_det = true;
hook_call_deferred(&p9221_detect_work_data,
P9221_NOTIFIER_DELAY_MS);
}
}
p9221r7_irq_handler(irq_src);
}
}