blob: e15704c271afbb0c2bdc9cfcd3cf6c38a05b4384 [file] [log] [blame] [edit]
/* Copyright 2024 The ChromiumOS Authors
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "include/pd_driver.h"
#include "include/platform.h"
#include "include/ppm.h"
#include "ppm_common.h"
const char *ppm_state_strings[PPM_STATE_MAX] = {
"PPM_STATE_NOT_READY", "PPM_STATE_IDLE",
"PPM_STATE_IDLE_NOTIFY", "PPM_STATE_PROCESSING_COMMAND",
"PPM_STATE_WAITING_CC_ACK", "PPM_STATE_WAITING_ASYNC_EV_ACK",
};
const char *ppm_state_to_string(int state)
{
if (state < PPM_STATE_NOT_READY || state >= PPM_STATE_MAX) {
return "PPM_STATE_Outside_valid_range";
}
return ppm_state_strings[state];
}
const char *ucsi_cmd_strings[UCSI_CMD_VENDOR_CMD + 1] = {
"UCSI_CMD_RESERVED",
"UCSI_CMD_PPM_RESET",
"UCSI_CMD_CANCEL",
"UCSI_CMD_CONNECTOR_RESET",
"UCSI_CMD_ACK_CC_CI",
"UCSI_CMD_SET_NOTIFICATION_ENABLE",
"UCSI_CMD_GET_CAPABILITY",
"UCSI_CMD_GET_CONNECTOR_CAPABILITY",
"UCSI_CMD_SET_CCOM",
"UCSI_CMD_SET_UOR",
"obsolete_UCSI_CMD_SET_PDM",
"UCSI_CMD_SET_PDR",
"UCSI_CMD_GET_ALTERNATE_MODES",
"UCSI_CMD_GET_CAM_SUPPORTED",
"UCSI_CMD_GET_CURRENT_CAM",
"UCSI_CMD_SET_NEW_CAM",
"UCSI_CMD_GET_PDOS",
"UCSI_CMD_GET_CABLE_PROPERTY",
"UCSI_CMD_GET_CONNECTOR_STATUS",
"UCSI_CMD_GET_ERROR_STATUS",
"UCSI_CMD_SET_POWER_LEVEL",
"UCSI_CMD_GET_PD_MESSAGE",
"UCSI_CMD_GET_ATTENTION_VDO",
"UCSI_CMD_reserved_0x17",
"UCSI_CMD_GET_CAM_CS",
"UCSI_CMD_LPM_FW_UPDATE_REQUEST",
"UCSI_CMD_SECURITY_REQUEST",
"UCSI_CMD_SET_RETIMER_MODE",
"UCSI_CMD_SET_SINK_PATH",
"UCSI_CMD_SET_PDOS",
"UCSI_CMD_READ_POWER_LEVEL",
"UCSI_CMD_CHUNKING_SUPPORT",
"UCSI_CMD_VENDOR_CMD",
};
const char *ucsi_command_to_string(uint8_t command)
{
if (command > UCSI_CMD_VENDOR_CMD) {
return "UCSI_CMD_Outside_valid_range";
}
return ucsi_cmd_strings[command];
}
#define DEV_CAST_FROM(v) (struct ppm_common_device *)(v)
static void clear_cci(struct ppm_common_device *dev)
{
platform_memset(&dev->ucsi_data.cci, 0, sizeof(struct ucsi_cci));
}
static void clear_last_error(struct ppm_common_device *dev)
{
dev->last_error = ERROR_LPM;
platform_memset(&dev->ppm_error_result, 0,
sizeof(struct ucsiv3_get_error_status_data));
}
inline static void set_cci_error(struct ppm_common_device *dev)
{
clear_cci(dev);
dev->ucsi_data.cci.error = 1;
dev->ucsi_data.cci.cmd_complete = 1;
}
static bool is_pending_async_event(struct ppm_common_device *dev)
{
return dev->pending.async_event;
}
static int ppm_common_opm_notify(struct ppm_common_device *dev)
{
if (dev->opm_notify) {
uint32_t cci;
platform_memcpy(&cci, &dev->ucsi_data.cci, sizeof(uint32_t));
DLOG("Notifying with CCI = 0x%08x", cci);
dev->opm_notify(dev->opm_context);
return 0;
} else {
ELOG("User error: No notifier!");
}
return -1;
}
static void clear_pending_command(struct ppm_common_device *dev)
{
if (dev->pending.command) {
DLOG("Cleared pending command[0x%x]",
dev->ucsi_data.control.command);
}
dev->pending.command = 0;
}
static void ppm_common_handle_async_event(struct ppm_common_device *dev)
{
uint8_t port;
struct ucsiv3_get_connector_status_data *port_status;
bool alert_port = false;
// Handle any smbus alert.
if (dev->pending.async_event) {
DLOG("PPM: Saw async event and processing.");
// If we are in the not ready or IDLE (no notifications) state,
// we do not bother updating OPM with status. Just clear the
// async event and move on.
if (dev->ppm_state == PPM_STATE_NOT_READY ||
dev->ppm_state == PPM_STATE_IDLE) {
dev->pending.async_event = 0;
return;
}
// Read per-port status if this is a fresh async event from an
// LPM alert.
if (dev->last_connector_alerted != -1) {
DLOG("Calling GET_CONNECTOR_STATUS on port %d",
dev->last_connector_alerted);
struct ucsi_control get_cs_cmd;
platform_memset((void *)&get_cs_cmd, 0,
sizeof(struct ucsi_control));
get_cs_cmd.command = UCSI_CMD_GET_CONNECTOR_STATUS;
get_cs_cmd.data_length = 0x0;
get_cs_cmd.command_specific[0] =
dev->last_connector_alerted;
// Clear port status before reading.
port = dev->last_connector_alerted - 1;
port_status = &dev->per_port_status[port];
platform_memset(
port_status, 0,
sizeof(struct ucsiv3_get_connector_status_data));
if (dev->pd->execute_cmd(dev->pd->dev, &get_cs_cmd,
(uint8_t *)port_status) ==
-1) {
ELOG("Failed to read port %d status. No recovery.",
port + 1);
} else {
DLOG("Port status change on %d: 0x%x", port + 1,
(uint16_t)port_status
->connector_status_change);
}
// We got alerted with a change for a port we already
// sent notifications for but which has not yet acked.
// Resend the notification.
if (port == dev->last_connector_changed) {
alert_port = true;
}
dev->last_connector_alerted = -1;
}
// If we are not already acting on an existing connector change,
// notify the OS if there are any other connector changes.
if (dev->last_connector_changed == -1) {
// Find the first port with any pending change.
for (port = 0; port < dev->num_ports; ++port) {
if (dev->per_port_status[port]
.connector_status_change != 0) {
break;
}
}
// Handle events in order by setting CCI and notifying
// OPM.
if (port < dev->num_ports) {
alert_port = true;
} else {
DLOG("No more ports needing OPM alerting");
}
}
// Should we alert?
if (alert_port) {
DLOG("Notifying async event for port %d and changing state from %d (%s)",
port + 1, dev->ppm_state,
ppm_state_to_string(dev->ppm_state));
// Notify the OPM that we have data for it to read.
clear_cci(dev);
dev->last_connector_changed = port;
dev->ucsi_data.cci.connector_changed = port + 1;
ppm_common_opm_notify(dev);
// Set PPM state to waiting for async event ack
dev->ppm_state = PPM_STATE_WAITING_ASYNC_EV_ACK;
}
// Clear the pending bit.
dev->pending.async_event = 0;
}
}
static bool is_pending_command(struct ppm_common_device *dev)
{
return dev->pending.command;
}
static bool match_pending_command(struct ppm_common_device *dev,
uint8_t command)
{
return dev->pending.command &&
dev->ucsi_data.control.command == command;
}
static int ppm_common_execute_pending_cmd(struct ppm_common_device *dev)
{
struct ucsi_control *control = &dev->ucsi_data.control;
struct ucsi_cci *cci = &dev->ucsi_data.cci;
uint8_t *message_in = (uint8_t *)&dev->ucsi_data.message_in;
uint8_t ucsi_command = control->command;
int ret = -1;
bool ack_ci = false;
if (control->command == 0 || control->command > UCSI_CMD_VENDOR_CMD) {
ELOG("Invalid command 0x%x", control->command);
// Set error condition to invalid command.
clear_last_error(dev);
dev->last_error = ERROR_PPM;
dev->ppm_error_result.error_information.unrecognized_command =
1;
set_cci_error(dev);
return -1;
}
switch (ucsi_command) {
case UCSI_CMD_ACK_CC_CI:
struct ucsiv3_ack_cc_ci_cmd *ack_cmd =
(struct ucsiv3_ack_cc_ci_cmd *)control->command_specific;
// The ack should already validated before we reach here.
ack_ci = ack_cmd->connector_change_ack;
break;
case UCSI_CMD_GET_ERROR_STATUS:
// If the error status came from the PPM, return the cached
// value and skip the |execute_cmd| in the pd_driver.
if (dev->last_error == ERROR_PPM) {
ret = sizeof(struct ucsiv3_get_error_status_data);
platform_memcpy(message_in, &dev->ppm_error_result,
ret);
goto success;
}
break;
default:
break;
}
// Do driver specific execute command.
ret = dev->pd->execute_cmd(dev->pd->dev, control, message_in);
// Clear command since we just executed it.
platform_memset(control, 0, sizeof(struct ucsi_control));
if (ret < 0) {
ELOG("Error with UCSI command 0x%x. Return was %d",
ucsi_command, ret);
clear_last_error(dev);
dev->last_error = ERROR_LPM;
set_cci_error(dev);
return ret;
}
success:
DLOG("Completed UCSI command 0x%x (%s)", ucsi_command,
ucsi_command_to_string(ucsi_command));
clear_cci(dev);
// Post-success command handling
if (ack_ci) {
struct ucsiv3_get_connector_status_data *port_status =
&dev->per_port_status[dev->last_connector_changed];
// Clear port status for acked connector.
port_status->connector_status_change = 0;
dev->last_connector_changed = -1;
// Flag a pending async event to process next event if it
// exists.
dev->pending.async_event = 1;
}
// If we reset, we only surface up the reset completed event after busy.
if (ucsi_command == UCSI_CMD_PPM_RESET) {
cci->reset_completed = 1;
} else {
cci->data_length = ret & 0xFF;
cci->cmd_complete = 1;
}
return 0;
}
inline static bool check_ack_has_valid_bits(struct ucsiv3_ack_cc_ci_cmd *cmd)
{
return cmd->command_complete_ack || cmd->connector_change_ack;
}
inline static bool check_ack_has_valid_ci(struct ucsiv3_ack_cc_ci_cmd *cmd,
struct ppm_common_device *dev)
{
return cmd->connector_change_ack ? dev->last_connector_changed != -1 :
1;
}
inline static bool check_ack_has_valid_cc(struct ucsiv3_ack_cc_ci_cmd *cmd,
struct ppm_common_device *dev)
{
return cmd->command_complete_ack ?
dev->ppm_state == PPM_STATE_WAITING_CC_ACK :
1;
}
inline static bool is_invalid_ack(struct ppm_common_device *dev)
{
struct ucsiv3_ack_cc_ci_cmd *cmd =
(struct ucsiv3_ack_cc_ci_cmd *)
dev->ucsi_data.control.command_specific;
return (!(check_ack_has_valid_bits(cmd) &&
check_ack_has_valid_ci(cmd, dev) &&
check_ack_has_valid_cc(cmd, dev)));
}
static void invalid_ack_notify(struct ppm_common_device *dev)
{
struct ucsiv3_ack_cc_ci_cmd *cmd =
(struct ucsiv3_ack_cc_ci_cmd *)
dev->ucsi_data.control.command_specific;
ELOG("Invalid ack usage (CI=%d CC=%d last_connector_changed=%d) in "
"state %d",
cmd->connector_change_ack, cmd->command_complete_ack,
dev->last_connector_changed, dev->ppm_state);
clear_last_error(dev);
dev->last_error = ERROR_PPM;
dev->ppm_error_result.error_information.invalid_cmd_specific_params = 1;
set_cci_error(dev);
// TODO(UCSI WG): Clarify pending clear behavior in case of PPM error
clear_pending_command(dev);
ppm_common_opm_notify(dev);
}
// Handle pending command. When handling pending commands, it is recommended
// that dev->ppm_state changes or notifications are made only in
// this function. Error bits may be set by other functions.
static void ppm_common_handle_pending_command(struct ppm_common_device *dev)
{
uint8_t next_command = 0;
int ret;
if (dev->pending.command) {
// Check what command is currently pending.
next_command = dev->ucsi_data.control.command;
DLOG("PEND_CMD: Started command processing in state %d (%s), cmd 0x%x (%s)",
dev->ppm_state, ppm_state_to_string(dev->ppm_state),
next_command, ucsi_command_to_string(next_command));
switch (dev->ppm_state) {
case PPM_STATE_IDLE:
case PPM_STATE_IDLE_NOTIFY:
// We are now processing the command. Change state,
// notify OPM and then continue.
dev->ppm_state = PPM_STATE_PROCESSING_COMMAND;
clear_cci(dev);
dev->ucsi_data.cci.busy = 1;
ppm_common_opm_notify(dev);
// Intentional fallthrough since we are now processing.
case PPM_STATE_PROCESSING_COMMAND:
// TODO - Handle the case where we have a command that
// takes multiple smbus calls to process (i.e. firmware
// update). If we were handling something that requires
// processing (i.e. firmware update), we would not
// update to WAITING_CC_ACK until it was completed.
ret = ppm_common_execute_pending_cmd(dev);
if (ret == -1) {
// CCI error bits are handled by
// execute_pending_command
dev->ppm_state = PPM_STATE_IDLE_NOTIFY;
ppm_common_opm_notify(dev);
break;
}
// If we were handling a PPM Reset, we go straight back
// to idle and clear any error indicators.
if (next_command == UCSI_CMD_PPM_RESET) {
dev->ppm_state = PPM_STATE_IDLE;
clear_last_error(dev);
} else if (next_command == UCSI_CMD_ACK_CC_CI) {
// We've received a standalone CI ack after
// completing command loop(s).
dev->ppm_state = PPM_STATE_IDLE_NOTIFY;
clear_cci(dev);
dev->ucsi_data.cci.ack_command = 1;
} else {
dev->ppm_state = PPM_STATE_WAITING_CC_ACK;
}
// Notify OPM to handle result and wait for ack if we're
// not still processing.
if (dev->ppm_state != PPM_STATE_PROCESSING_COMMAND) {
ppm_common_opm_notify(dev);
}
break;
case PPM_STATE_WAITING_CC_ACK:
case PPM_STATE_WAITING_ASYNC_EV_ACK:
// If we successfully ACK, update CCI and notify. On
// error, the CCI will already be set by
// |ppm_common_execute_pending_cmd|.
ret = ppm_common_execute_pending_cmd(dev);
if (ret != -1) {
dev->ppm_state = PPM_STATE_IDLE_NOTIFY;
clear_cci(dev);
dev->ucsi_data.cci.ack_command = 1;
}
ppm_common_opm_notify(dev);
break;
default:
ELOG("Unhandled ppm state (%d) when handling pending command",
dev->ppm_state);
break;
}
DLOG("PEND_CMD: Ended command processing in state %d (%s)",
dev->ppm_state, ppm_state_to_string(dev->ppm_state));
// Last thing is to clear the pending command bit before
// executing the command.
if (dev->ppm_state != PPM_STATE_PROCESSING_COMMAND) {
clear_pending_command(dev);
}
}
}
static void ppm_common_task(void *context)
{
struct ppm_common_device *dev = DEV_CAST_FROM(context);
if (!dev) {
ELOG("Cannot start PPM task without valid device pointer: %p",
dev);
return;
}
DLOG("PPM: Starting the ppm task");
platform_mutex_lock(dev->ppm_lock);
// Initialize the system state.
dev->ppm_state = PPM_STATE_NOT_READY;
// Send PPM reset and set state to IDLE if successful.
platform_memset(&dev->ucsi_data.control, 0,
sizeof(struct ucsi_control));
dev->ucsi_data.control.command = UCSI_CMD_PPM_RESET;
if (dev->pd->execute_cmd(dev->pd->dev, &dev->ucsi_data.control,
dev->ucsi_data.message_in) != -1) {
dev->ppm_state = PPM_STATE_IDLE;
platform_memset(&dev->ucsi_data.cci, 0,
sizeof(struct ucsi_cci));
}
// TODO - Note to self
//
// Smbus function calls are currently done with PPM lock; may need to
// fix that.
do {
// Wait for a task from OPM unless we are already processing a
// command.
if (dev->ppm_state != PPM_STATE_PROCESSING_COMMAND) {
DLOG("Waiting for next command at state %d (%s)...",
dev->ppm_state,
ppm_state_to_string(dev->ppm_state));
platform_condvar_wait(dev->ppm_condvar, dev->ppm_lock);
}
DLOG("Handling next task at state %d (%s)", dev->ppm_state,
ppm_state_to_string(dev->ppm_state));
switch (dev->ppm_state) {
// Idle with notifications enabled.
case PPM_STATE_IDLE:
if (is_pending_command(dev)) {
// Only handle SET_NOTIFICATION_ENABLE or
// PPM_RESET. Otherwise clear the pending
// command.
if (match_pending_command(
dev,
UCSI_CMD_SET_NOTIFICATION_ENABLE) ||
match_pending_command(dev,
UCSI_CMD_PPM_RESET)) {
ppm_common_handle_pending_command(dev);
} else {
clear_pending_command(dev);
}
} else if (is_pending_async_event(dev)) {
ppm_common_handle_async_event(dev);
}
break;
// Idle and waiting for a command or event.
case PPM_STATE_IDLE_NOTIFY:
// Check if you're acking in the right state for
// ACK_CC_CI. Only CI acks are allowed here. i.e. we are
// still waiting for a CI ack after a command loop was
// completed.
if (is_pending_command(dev) &&
match_pending_command(dev, UCSI_CMD_ACK_CC_CI) &&
is_invalid_ack(dev)) {
invalid_ack_notify(dev);
break;
}
if (is_pending_command(dev)) {
ppm_common_handle_pending_command(dev);
} else if (is_pending_async_event(dev)) {
ppm_common_handle_async_event(dev);
}
break;
// Processing a command. We only ever enter this state for
// firmware update (for example if we're breaking up a chunk of
// firmware into multiple transactions).
case PPM_STATE_PROCESSING_COMMAND:
ppm_common_handle_pending_command(dev);
break;
// Waiting for a command completion acknowledge.
case PPM_STATE_WAITING_CC_ACK:
if (!match_pending_command(dev, UCSI_CMD_ACK_CC_CI) ||
is_invalid_ack(dev)) {
invalid_ack_notify(dev);
break;
}
ppm_common_handle_pending_command(dev);
break;
// Waiting for async event ack.
case PPM_STATE_WAITING_ASYNC_EV_ACK:
if (is_pending_command(dev)) {
bool is_ack = match_pending_command(
dev, UCSI_CMD_ACK_CC_CI);
if (is_ack && is_invalid_ack(dev)) {
invalid_ack_notify(dev);
break;
}
// Waiting ASYNC_EV_ACK is a weird state. It can
// directly ACK the CI or it can go into a
// PROCESSING_COMMAND state (in which case it
// should be treated as a IDLE_NOTIFY).
//
// Thus, if we don't get UCSI_CMD_ACK_CC_CI
// here, we just treat this as IDLE_NOTIFY
// state.
if (!is_ack) {
DLOG("ASYNC EV ACK state turned into IDLE_NOTIFY state");
dev->ppm_state = PPM_STATE_IDLE_NOTIFY;
}
ppm_common_handle_pending_command(dev);
}
break;
default:
break;
}
} while (!dev->cleaning_up);
platform_mutex_unlock(dev->ppm_lock);
platform_task_exit();
}
static int ppm_common_init_and_wait(struct ucsi_ppm_device *device,
uint8_t num_ports)
{
#define MAX_TIMEOUT_MS 1000
#define POLL_EVERY_MS 10
struct ppm_common_device *dev = DEV_CAST_FROM(device);
struct ucsi_memory_region *ucsi_data = &dev->ucsi_data;
bool ready_to_exit = false;
// First clear the PPM shared memory region.
platform_memset(ucsi_data, 0, sizeof(*ucsi_data));
// Initialize to UCSI version 3.0
ucsi_data->version.version = 0x0300;
// TODO - Set real lpm address based on smbus driver.
ucsi_data->version.lpm_address = 0x0;
// Init lock to sync PPM task and main task context.
dev->ppm_lock = platform_mutex_init();
if (!dev->ppm_lock) {
return -1;
}
// Init condvar to notify PPM task.
dev->ppm_condvar = platform_condvar_init();
if (!dev->ppm_condvar) {
return -1;
}
// Allocate per port status (used for PPM async event notifications).
dev->num_ports = num_ports;
dev->per_port_status = platform_calloc(
dev->num_ports,
sizeof(struct ucsiv3_get_connector_status_data));
dev->last_connector_changed = -1;
DLOG("Ready to initialize PPM task!");
// Initialize the PPM task.
dev->ppm_task_handle =
platform_task_init((void *)ppm_common_task, (void *)dev);
if (!dev->ppm_task_handle) {
ELOG("No ppm task created.");
return -1;
}
DLOG("PPM is waiting for task to run.");
for (int count = 0; count * POLL_EVERY_MS < MAX_TIMEOUT_MS; count++) {
platform_mutex_lock(dev->ppm_lock);
ready_to_exit = dev->ppm_state != PPM_STATE_NOT_READY;
platform_mutex_unlock(dev->ppm_lock);
if (ready_to_exit) {
break;
}
platform_usleep(POLL_EVERY_MS * 1000);
}
DLOG("PPM initialized result: Success=%b", ready_to_exit);
return (ready_to_exit ? 0 : -1);
}
struct ucsi_memory_region *
ppm_common_get_data_region(struct ucsi_ppm_device *device)
{
struct ppm_common_device *dev = DEV_CAST_FROM(device);
return &dev->ucsi_data;
}
bool ppm_common_get_next_connector_status(
struct ucsi_ppm_device *device, uint8_t *out_port_num,
struct ucsiv3_get_connector_status_data **out_connector_status)
{
struct ppm_common_device *dev = DEV_CAST_FROM(device);
if (dev->last_connector_changed != -1) {
*out_port_num = (uint8_t)dev->last_connector_changed + 1;
*out_connector_status =
&dev->per_port_status[dev->last_connector_changed];
return true;
}
return false;
}
static int ppm_common_read(struct ucsi_ppm_device *device, unsigned int offset,
void *buf, size_t length)
{
struct ppm_common_device *dev = DEV_CAST_FROM(device);
if (!dev) {
return -1;
}
// Validate memory to read and allow any offset for reading.
if (offset + length >= sizeof(struct ucsi_memory_region)) {
ELOG("UCSI read exceeds bounds of memory: offset(0x%x), length(0x%x)",
offset, length);
return -1;
}
platform_memcpy(buf,
(const void *)((uint8_t *)(&dev->ucsi_data) + offset),
length);
return length;
}
static int ppm_common_handle_control_message(struct ppm_common_device *dev,
const void *buf, size_t length)
{
const uint8_t *cmd = (const uint8_t *)buf;
uint8_t prev_cmd;
uint8_t busy = 0;
if (length > sizeof(struct ucsi_control)) {
ELOG("Tried to send control message that is an invalid size (%d)",
(int)length);
return -1;
}
// If we're currently sending a command, we should immediately discard
// this call.
{
platform_mutex_lock(dev->ppm_lock);
busy = dev->pending.command || dev->ucsi_data.cci.busy;
prev_cmd = dev->ucsi_data.control.command;
platform_mutex_unlock(dev->ppm_lock);
}
if (busy) {
ELOG("Tried to send control message (cmd=0x%x) when one is already pending "
"(cmd=0x%x).",
cmd[0], prev_cmd);
return -1;
}
// If we didn't get a full CONTROL message, zero the region before
// copying.
if (length != sizeof(struct ucsi_control)) {
platform_memset(&dev->ucsi_data.control, 0,
sizeof(struct ucsi_control));
}
platform_memcpy(&dev->ucsi_data.control, cmd, length);
DLOG("Got valid control message: 0x%x (%s)", cmd[0],
ucsi_command_to_string(cmd[0]));
// Schedule command send.
{
platform_mutex_lock(dev->ppm_lock);
// Mark command pending.
dev->pending.command = 1;
platform_condvar_signal(dev->ppm_condvar);
DLOG("Signaled pending command");
platform_mutex_unlock(dev->ppm_lock);
}
return 0;
}
/*
Only allow writes into two regions:
- Control (to send commands)
- Message Out (to prepare data to send commands)
A control message will result in an actual UCSI command being called if the
data is valid.
A write into message in doesn't modify the PPM state but is often
a precursor to actually sending a control message. This will be used for fw
updates.
Any writes into non-aligned offsets (except Message IN) will be discarded.
*/
static int ppm_common_write(struct ucsi_ppm_device *device, unsigned int offset,
const void *buf, size_t length)
{
struct ppm_common_device *dev = DEV_CAST_FROM(device);
bool valid_fixed_offset;
if (!buf || length == 0) {
ELOG("Invalid buffer (%p) or length (%x)", buf, length);
return -1;
}
valid_fixed_offset = (offset == UCSI_VERSION_OFFSET) ||
(offset == UCSI_CCI_OFFSET) ||
(offset == UCSI_CONTROL_OFFSET);
if (!valid_fixed_offset &&
!(offset >= UCSI_MESSAGE_OUT_OFFSET &&
offset < UCSI_MESSAGE_OUT_OFFSET + MESSAGE_OUT_SIZE)) {
ELOG("UCSI can't write to invalid offset: 0x%x", offset);
return -1;
}
// Handle control messages
if (offset == UCSI_CONTROL_OFFSET) {
return ppm_common_handle_control_message(dev, buf, length);
}
if (offset >= UCSI_MESSAGE_OUT_OFFSET &&
offset + length >= UCSI_MESSAGE_OUT_OFFSET + MESSAGE_OUT_SIZE) {
ELOG("UCSI write to MESSAGE_OUT exceeds bounds: offset(0x%x) + size(0x%x) "
">= "
"end(0x%x)",
offset, length,
UCSI_MESSAGE_OUT_OFFSET + MESSAGE_OUT_SIZE);
return -1;
}
// Copy from input buffer to offset within MESSAGE_OUT.
platform_memcpy(dev->ucsi_data.message_out +
(offset - UCSI_MESSAGE_OUT_OFFSET),
buf, length);
return 0;
}
static int ppm_common_register_notify(struct ucsi_ppm_device *device,
ucsi_ppm_notify *callback, void *context)
{
struct ppm_common_device *dev = DEV_CAST_FROM(device);
if (!dev) {
return -1;
}
dev->opm_notify = callback;
dev->opm_context = context;
return 0;
}
static void ppm_common_lpm_alert(struct ucsi_ppm_device *device, uint8_t lpm_id)
{
struct ppm_common_device *dev = DEV_CAST_FROM(device);
DLOG("LPM alert seen on port %d!", lpm_id);
platform_mutex_lock(dev->ppm_lock);
if (lpm_id <= dev->num_ports) {
// Set async event and mark port status as not read.
dev->pending.async_event = 1;
dev->last_connector_alerted = lpm_id;
platform_condvar_signal(dev->ppm_condvar);
} else {
ELOG("Alert id out of range: %d (num_ports = %d)", lpm_id,
dev->num_ports);
}
platform_mutex_unlock(dev->ppm_lock);
}
static void ppm_common_cleanup(struct ucsi_ppm_driver *driver)
{
if (driver->dev) {
struct ppm_common_device *dev = DEV_CAST_FROM(driver->dev);
// Signal clean up to waiting thread.
platform_mutex_lock(dev->ppm_lock);
dev->cleaning_up = true;
platform_condvar_signal(dev->ppm_condvar);
platform_mutex_unlock(dev->ppm_lock);
// Wait for task to complete.
platform_task_complete(dev->ppm_task_handle);
platform_free(dev->ppm_condvar);
platform_free(dev->ppm_lock);
platform_free(driver->dev);
driver->dev = NULL;
}
}
struct ucsi_ppm_driver *ppm_open(struct ucsi_pd_driver *pd_driver)
{
struct ppm_common_device *dev = NULL;
struct ucsi_ppm_driver *drv = NULL;
dev = platform_calloc(1, sizeof(struct ppm_common_device));
if (!dev) {
goto handle_error;
}
dev->pd = pd_driver;
drv = platform_calloc(1, sizeof(struct ucsi_ppm_driver));
if (!drv) {
goto handle_error;
}
drv->dev = (struct ucsi_ppm_device *)dev;
drv->init_and_wait = ppm_common_init_and_wait;
drv->get_data_region = ppm_common_get_data_region;
drv->get_next_connector_status = ppm_common_get_next_connector_status;
drv->read = ppm_common_read;
drv->write = ppm_common_write;
drv->register_notify = ppm_common_register_notify;
drv->lpm_alert = ppm_common_lpm_alert;
drv->cleanup = ppm_common_cleanup;
return drv;
handle_error:
platform_free(dev);
platform_free(drv);
return NULL;
}