diff options
Diffstat (limited to 'chip/stm32/usb_power.c')
-rw-r--r-- | chip/stm32/usb_power.c | 655 |
1 files changed, 655 insertions, 0 deletions
diff --git a/chip/stm32/usb_power.c b/chip/stm32/usb_power.c new file mode 100644 index 0000000000..b1ba698cc7 --- /dev/null +++ b/chip/stm32/usb_power.c @@ -0,0 +1,655 @@ +/* Copyright 2016 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 "common.h" +#include "dma.h" +#include "hooks.h" +#include "i2c.h" +#include "link_defs.h" +#include "registers.h" +#include "timer.h" +#include "usb_descriptor.h" +#include "usb_power.h" +#include "util.h" + +#define CPRINTS(format, args...) cprints(CC_I2C, format, ## args) + + + +static int usb_power_init_inas(struct usb_power_config const *config); +static int usb_power_read(struct usb_power_config const *config); +static int usb_power_write_line(struct usb_power_config const *config); + + +static int8_t usb_power_map_error(int error) +{ + switch (error) { + case EC_SUCCESS: return USB_POWER_SUCCESS; + case EC_ERROR_TIMEOUT: return USB_POWER_ERROR_TIMEOUT; + case EC_ERROR_BUSY: return USB_POWER_ERROR_BUSY; + default: return USB_POWER_ERROR_UNKNOWN | (error & 0x7f); + } +} + +void usb_power_deferred_rx(struct usb_power_config const *config) +{ + int rx_count = rx_ep_pending(config->endpoint); + + /* Handle an incoming command if available */ + if (rx_count) + usb_power_read(config); +} + +void usb_power_deferred_tx(struct usb_power_config const *config) +{ + struct usb_power_state *state = config->state; + + if (!tx_ep_is_ready(config->endpoint)) + return; + + /* We've replied, set up the next read. */ + if (!rx_ep_is_active(config->endpoint)) { + /* Remove any active dma region from output buffer */ + state->reports_xmit_active = state->reports_tail; + + /* Wait for the next command */ + usb_read_ep(config->endpoint, + config->ep->out_databuffer_max, + config->ep->out_databuffer); + return; + } +} + +/* Reset stream */ +void usb_power_reset(struct usb_power_config const *config) +{ + config->ep->out_databuffer = config->state->rx_buf; + config->ep->out_databuffer_max = sizeof(config->state->rx_buf); + config->ep->in_databuffer = config->state->tx_buf; + config->ep->in_databuffer_max = sizeof(config->state->tx_buf); + + epN_reset(config->endpoint); + + /* Flush any queued data */ + hook_call_deferred(config->ep->rx_deferred, 0); + hook_call_deferred(config->ep->tx_deferred, 0); +} + + +/* Write one or more power records to USB */ +static int usb_power_write_line(struct usb_power_config const *config) +{ + struct usb_power_state *state = config->state; + struct usb_power_report *r = (struct usb_power_report *)( + state->reports_data_area + + (USB_POWER_RECORD_SIZE(state->ina_count) + * state->reports_tail)); + /* status + size + timestamps + power list */ + size_t bytes = USB_POWER_RECORD_SIZE(state->ina_count); + + /* Check if queue has active data. */ + if (config->state->reports_head != config->state->reports_tail) { + int recordcount = 1; + + /* We'll concatenate all the upcoming recrds. */ + if (config->state->reports_tail < config->state->reports_head) + recordcount = config->state->reports_head - + config->state->reports_tail; + else + recordcount = state->max_cached - + config->state->reports_tail; + + state->reports_xmit_active = state->reports_tail; + state->reports_tail = (state->reports_tail + recordcount) % + state->max_cached; + + usb_write_ep(config->endpoint, bytes * recordcount, r); + return bytes; + } + + CPRINTS("usb_power_write_line: no data rs: %d, rc: %d", + USB_POWER_RECORD_SIZE(state->ina_count), + USB_POWER_MAX_CACHED(state->ina_count)); + return 0; +} + + +static int usb_power_state_reset(struct usb_power_config const *config) +{ + struct usb_power_state *state = config->state; + + state->state = USB_POWER_STATE_OFF; + state->reports_head = 0; + state->reports_tail = 0; + state->reports_xmit_active = 0; + + CPRINTS("[RESET] STATE -> OFF"); + return USB_POWER_SUCCESS; +} + + +static int usb_power_state_stop(struct usb_power_config const *config) +{ + struct usb_power_state *state = config->state; + + /* Only a valid transition from CAPTURING */ + if (state->state != USB_POWER_STATE_CAPTURING) { + CPRINTS("[STOP] Error not capturing."); + return USB_POWER_ERROR_NOT_CAPTURING; + } + + state->state = USB_POWER_STATE_SETUP; + state->reports_head = 0; + state->reports_tail = 0; + state->reports_xmit_active = 0; + state->stride_bytes = 0; + CPRINTS("[STOP] STATE: CAPTURING -> SETUP"); + return USB_POWER_SUCCESS; +} + + + +static int usb_power_state_start(struct usb_power_config const *config, + union usb_power_command_data *cmd, int count) +{ + struct usb_power_state *state = config->state; + int integration_us = cmd->start.integration_us; + + if (state->state != USB_POWER_STATE_SETUP) { + CPRINTS("[START] Error not setup."); + return USB_POWER_ERROR_NOT_SETUP; + } + + if (count != 6) { + CPRINTS("[START] Error count %d is not 6", (int)count); + return USB_POWER_ERROR_READ_SIZE; + } + + if (integration_us == 0) { + CPRINTS("[START] integration_us cannot be 0"); + return USB_POWER_ERROR_UNKNOWN; + } + + /* Calculate the reports array */ + state->stride_bytes = USB_POWER_RECORD_SIZE(state->ina_count); + state->max_cached = USB_POWER_MAX_CACHED(state->ina_count); + + state->integration_us = integration_us; + usb_power_init_inas(config); + + state->state = USB_POWER_STATE_CAPTURING; + CPRINTS("[START] STATE: SETUP -> CAPTURING %dus", integration_us); + + /* Find our starting time. */ + config->state->base_time = get_time().val; + + hook_call_deferred(config->deferred_cap, state->integration_us); + return USB_POWER_SUCCESS; +} + + +static int usb_power_state_settime(struct usb_power_config const *config, + union usb_power_command_data *cmd, int count) +{ + if (count != sizeof(struct usb_power_command_settime)) { + CPRINTS("[SETTIME] Error: count %d is not %d", + (int)count, sizeof(struct usb_power_command_settime)); + return USB_POWER_ERROR_READ_SIZE; + } + + /* Find the offset between microcontroller clock and host clock. */ + if (cmd->settime.time) + config->state->wall_offset = cmd->settime.time - get_time().val; + else + config->state->wall_offset = 0; + + return EC_SUCCESS; +} + + +static int usb_power_state_addina(struct usb_power_config const *config, + union usb_power_command_data *cmd, int count) +{ + struct usb_power_state *state = config->state; + struct usb_power_ina_cfg *ina; + + /* Only valid from OFF or SETUP */ + if ((state->state != USB_POWER_STATE_OFF) && + (state->state != USB_POWER_STATE_SETUP)) { + CPRINTS("[ADDINA] Error incorrect state."); + return USB_POWER_ERROR_NOT_SETUP; + } + + if (count != sizeof(struct usb_power_command_addina)) { + CPRINTS("[ADDINA] Error count %d is not %d", + (int)count, sizeof(struct usb_power_command_addina)); + return USB_POWER_ERROR_READ_SIZE; + } + + if (state->ina_count >= USB_POWER_MAX_READ_COUNT) { + CPRINTS("[ADDINA] Error INA list full"); + return USB_POWER_ERROR_FULL; + } + + /* Transition to SETUP state if necessary and clear INA data */ + if (state->state == USB_POWER_STATE_OFF) { + state->state = USB_POWER_STATE_SETUP; + state->ina_count = 0; + } + + /* Select INA to configure */ + ina = state->ina_cfg + state->ina_count; + + ina->port = cmd->addina.port; + ina->addr = (cmd->addina.addr) << 1; /* 7 to 8 bit addr. */ + ina->rs = cmd->addina.rs; + + state->ina_count += 1; + return USB_POWER_SUCCESS; +} + +static int usb_power_read(struct usb_power_config const *config) +{ + /* + * If there is a USB packet waiting we process it and generate a + * response. + */ + uint8_t count = rx_ep_pending(config->endpoint); + uint8_t result = USB_POWER_SUCCESS; + union usb_power_command_data *cmd = + (union usb_power_command_data *)config->ep->out_databuffer; + + struct usb_power_state *state = config->state; + struct dwc_usb_ep *ep = config->ep; + + /* Bytes to return */ + int in_msgsize = 1; + + if (count < 2) + return EC_ERROR_INVAL; + + /* State machine. */ + switch (cmd->command) { + case USB_POWER_CMD_RESET: + result = usb_power_state_reset(config); + break; + + case USB_POWER_CMD_STOP: + result = usb_power_state_stop(config); + break; + + case USB_POWER_CMD_START: + result = usb_power_state_start(config, cmd, count); + if (result == USB_POWER_SUCCESS) { + /* Send back actual integration time. */ + ep->in_databuffer[1] = + (state->integration_us >> 0) & 0xff; + ep->in_databuffer[2] = + (state->integration_us >> 8) & 0xff; + ep->in_databuffer[3] = + (state->integration_us >> 16) & 0xff; + ep->in_databuffer[4] = + (state->integration_us >> 24) & 0xff; + in_msgsize += 4; + } + break; + + case USB_POWER_CMD_ADDINA: + result = usb_power_state_addina(config, cmd, count); + break; + + case USB_POWER_CMD_SETTIME: + result = usb_power_state_settime(config, cmd, count); + break; + + case USB_POWER_CMD_NEXT: + if (state->state == USB_POWER_STATE_CAPTURING) { + int ret; + + ret = usb_power_write_line(config); + if (ret) + return EC_SUCCESS; + + CPRINTS("[CAP] busy"); + result = USB_POWER_ERROR_BUSY; + } else { + CPRINTS("[STOP] Error not capturing."); + result = USB_POWER_ERROR_NOT_CAPTURING; + } + break; + + default: + CPRINTS("[ERROR] Unknown command 0x%04x", (int)cmd->command); + result = USB_POWER_ERROR_UNKNOWN; + break; + } + + /* Return result code if applicable. */ + usb_power_map_error(0); + ep->in_databuffer[0] = result; + + usb_write_ep(config->endpoint, in_msgsize, ep->in_databuffer); + + return EC_SUCCESS; +} + + + +/****************************************************************************** + * INA231 interface. + * List the registers and fields here. + * TODO(nsanders): combine with the currently incompatible common INA drivers. + */ + +#define INA231_REG_CONF 0 +#define INA231_REG_RSHV 1 +#define INA231_REG_BUSV 2 +#define INA231_REG_PWR 3 +#define INA231_REG_CURR 4 +#define INA231_REG_CAL 5 +#define INA231_REG_EN 6 + + +#define INA231_CONF_AVG(val) (((int)(val & 0x7)) << 9) +#define INA231_CONF_BUS_TIME(val) (((int)(val & 0x7)) << 6) +#define INA231_CONF_SHUNT_TIME(val) (((int)(val & 0x7)) << 3) +#define INA231_CONF_MODE(val) (((int)(val & 0x7)) << 0) +#define INA231_MODE_OFF 0x0 +#define INA231_MODE_SHUNT 0x5 +#define INA231_MODE_BUS 0x6 +#define INA231_MODE_BOTH 0x7 + + + +uint16_t ina2xx_readagain(uint8_t port, uint8_t addr) +{ + int res; + uint16_t val; + + res = i2c_xfer(port, addr, NULL, 0, (uint8_t *)&val, sizeof(uint16_t), + I2C_XFER_SINGLE); + if (res) { + CPRINTS("INA2XX I2C readagain failed p:%d a:%02x", + (int)port, (int)addr); + return 0x0bad; + } + return (val >> 8) | ((val & 0xff) << 8); +} + + +uint16_t ina2xx_read(uint8_t port, uint8_t addr, uint8_t reg) +{ + int res; + int val; + + res = i2c_read16(port, addr, reg, &val); + if (res) { + CPRINTS("INA2XX I2C read failed p:%d a:%02x, r:%02x", + (int)port, (int)addr, (int)reg); + return 0x0bad; + } + return (val >> 8) | ((val & 0xff) << 8); +} + +int ina2xx_write(uint8_t port, uint8_t addr, uint8_t reg, uint16_t val) +{ + int res; + uint16_t be_val = (val >> 8) | ((val & 0xff) << 8); + + res = i2c_write16(port, addr, reg, be_val); + if (res) + CPRINTS("INA2XX I2C write failed"); + return res; +} + + + +/****************************************************************************** + * Background tasks + * + * Here we setup the INAs and read them at the specified interval. + * INA samples are stored in a ringbuffer that can be fetched using the + * USB commands. + */ + +/* INA231 integration and averaging time presets, indexed by register value */ +static const int average_settings[] = { + 1, 4, 16, 64, 128, 256, 512, 1024}; +static const int conversion_time_us[] = { + 140, 204, 332, 588, 1100, 2116, 4156, 8244}; + +static int usb_power_init_inas(struct usb_power_config const *config) +{ + struct usb_power_state *state = config->state; + int i; + int shunt_time = 0; + int avg = 0; + int target_us = state->integration_us; + + if (state->state != USB_POWER_STATE_SETUP) { + CPRINTS("[ERROR] usb_power_init_inas while not SETUP"); + return -1; + } + + /* Find an INA preset integration time less than specified */ + while (shunt_time < 7) { + if (conversion_time_us[shunt_time + 1] > target_us) + break; + shunt_time++; + } + + /* Find an averaging setting from the INA presets that fits. */ + while (avg < 7) { + if ((conversion_time_us[shunt_time] * + average_settings[avg + 1]) + > target_us) + break; + avg++; + } + + state->integration_us = + conversion_time_us[shunt_time] * average_settings[avg]; + + for (i = 0; i < state->ina_count; i++) { + int value; + int ret; + struct usb_power_ina_cfg *ina = state->ina_cfg + i; + +#ifdef USB_POWER_VERBOSE + { + int conf, cal; + + conf = ina2xx_read(ina->port, ina->addr, INA231_REG_CONF); + cal = ina2xx_read(ina->port, ina->addr, INA231_REG_CAL); + CPRINTS("[CAP] %d (%d,0x%02x): conf:%x, cal:%x", + i, ina->port, ina->addr, conf, cal); + } +#endif + /* + * Calculate INA231 Calibration register + * CurrentLSB = uA per div = 80mV / (Rsh * 2^15) + * CurrentLSB uA = 80000000nV / (Rsh mOhm * 0x8000) + */ + ina->scale = 80000000 / (ina->rs * 0x8000); + + /* + * CAL = .00512 / (CurrentLSB * Rsh) + * CAL = 5120000 / (uA * mOhm) + */ + value = 5120000 / (ina->scale * ina->rs); + ret = ina2xx_write(ina->port, ina->addr, INA231_REG_CAL, value); + if (ret != EC_SUCCESS) { + CPRINTS("[CAP] usb_power_init_inas CAL FAIL: %d", ret); + return ret; + } +#ifdef USB_POWER_VERBOSE + { + int actual; + + actual = ina2xx_read(ina->port, ina->addr, INA231_REG_CAL); + CPRINTS("[CAP] scale: %d uA/div, %d uW/div, cal:%x act:%x", + ina->scale, ina->scale*25, value, actual); + } +#endif + /* Conversion time, shunt + bus, set average. */ + value = INA231_CONF_MODE(INA231_MODE_BOTH) | + INA231_CONF_SHUNT_TIME(shunt_time) | + INA231_CONF_BUS_TIME(shunt_time) | + INA231_CONF_AVG(avg); + ret = ina2xx_write( + ina->port, ina->addr, INA231_REG_CONF, value); + if (ret != EC_SUCCESS) { + CPRINTS("[CAP] usb_power_init_inas CONF FAIL: %d", ret); + return ret; + } +#ifdef USB_POWER_VERBOSE + { + int actual; + + actual = ina2xx_read(ina->port, ina->addr, INA231_REG_CONF); + CPRINTS("[CAP] %d (%d,0x%02x): conf:%x, act:%x", + i, ina->port, ina->addr, value, actual); + } +#endif +#ifdef USB_POWER_VERBOSE + { + int busv_mv = + (ina2xx_read(ina->port, ina->addr, INA231_REG_BUSV) + * 125) / 100; + + CPRINTS("[CAP] %d (%d,0x%02x): busv:%dmv", + i, ina->port, ina->addr, busv_mv); + } +#endif + /* Initialize read from power register. This register address + * will be cached and all ina2xx_readagain() calls will read + * from the same address. + */ + ina2xx_read(ina->port, ina->addr, INA231_REG_PWR); + } + + return EC_SUCCESS; +} + + +/* + * Read each INA's power integration measurement. + * + * INAs recall the most recent address, so no register access write is + * necessary, simply read 16 bits from each INA and fill the result into + * the power record. + * + * If the power record ringbuffer is full, fail with USB_POWER_ERROR_OVERFLOW. + */ +static int usb_power_get_samples(struct usb_power_config const *config) +{ + uint64_t time = get_time().val; + struct usb_power_state *state = config->state; + struct usb_power_report *r = (struct usb_power_report *)( + state->reports_data_area + + (USB_POWER_RECORD_SIZE(state->ina_count) + * state->reports_head)); + struct usb_power_ina_cfg *inas = state->ina_cfg; + int i; + + /* TODO(nsanders): Would we prefer to evict oldest? */ + if (((state->reports_head + 1) % USB_POWER_MAX_CACHED(state->ina_count)) + == state->reports_xmit_active) { + CPRINTS("Overflow! h:%d a:%d t:%d (%d)", + state->reports_head, state->reports_xmit_active, + state->reports_tail, + USB_POWER_MAX_CACHED(state->ina_count)); + return USB_POWER_ERROR_OVERFLOW; + } + + r->status = USB_POWER_SUCCESS; + r->size = state->ina_count; + if (config->state->wall_offset) + time = time + config->state->wall_offset; + else + time -= config->state->base_time; + r->timestamp = time; + + for (i = 0; i < state->ina_count; i++) { + int power; + struct usb_power_ina_cfg *ina = inas + i; + + /* Read INA231. + * ina2xx_read(ina->port, ina->addr, INA231_REG_PWR); + * Readagain cached this address so we'll save an I2C + * transaction. + */ + power = ina2xx_readagain(ina->port, ina->addr); + r->power[i] = power; +#ifdef USB_POWER_VERBOSE + { + int current; + int voltage; + int bvoltage; + + voltage = ina2xx_read(ina->port, ina->addr, INA231_REG_RSHV); + bvoltage = ina2xx_read(ina->port, ina->addr, INA231_REG_BUSV); + current = ina2xx_read(ina->port, ina->addr, INA231_REG_CURR); + power = ina2xx_read(ina->port, ina->addr, INA231_REG_PWR); + } + { + int uV = ((int)voltage * 25) / 10; + int mV = ((int)bvoltage * 125) / 100; + int uA = (uV * 1000) / ina->rs; + int CuA = ((int)current * ina->scale); + int uW = ((int)power * ina->scale*25); + + CPRINTS("[CAP] %d (%d,0x%02x): %dmV / %dmO = %dmA", + i, ina->port, ina->addr, uV/1000, ina->rs, uA/1000); + CPRINTS("[CAP] %duV %dmV %duA %dCuA " + "%duW v:%04x, b:%04x, p:%04x", + uV, mV, uA, CuA, uW, voltage, bvoltage, power); + } +#endif + } + + /* Mark this slot as used. */ + state->reports_head = (state->reports_head + 1) % + USB_POWER_MAX_CACHED(state->ina_count); + + return EC_SUCCESS; +} + +/* + * This function is called every [interval] uS, and reads the accumulated + * values of the INAs, and reschedules itself for the next interval. + * + * It will stop collecting frames if a ringbuffer overflow is + * detected, or a stop request is seen.. + */ +void usb_power_deferred_cap(struct usb_power_config const *config) +{ + int ret; + uint64_t timeout = get_time().val + config->state->integration_us; + uint64_t timein; + + /* Exit if we have stopped capturing in the meantime. */ + if (config->state->state != USB_POWER_STATE_CAPTURING) + return; + + /* Get samples for this timeslice */ + ret = usb_power_get_samples(config); + if (ret == USB_POWER_ERROR_OVERFLOW) { + CPRINTS("[CAP] usb_power_deferred_cap: OVERFLOW"); + return; + } + + /* Calculate time remaining until next slice. */ + timein = get_time().val; + if (timeout > timein) + timeout = timeout - timein; + else + timeout = 0; + + /* Double check if we are still capturing. */ + if (config->state->state == USB_POWER_STATE_CAPTURING) + hook_call_deferred(config->deferred_cap, timeout); +} + |