/*
* GRUB -- GRand Unified Bootloader
*
* Copyright (C) 2012 Google Inc.
* Copyright (C) 2016 Free Software Foundation, Inc.
*
* This is based on depthcharge code.
*
* GRUB is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* GRUB is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GRUB. If not, see .
*/
#include
#include
#include
#include
#include
static const grub_uint64_t FRAMING_TIMEOUT_MS = 300;
static const grub_uint8_t EC_FRAMING_BYTE = 0xec;
#define EC_CMD_MKBP_STATE 0x60
#define EC_CMD_VERSION0 0xdc
static grub_uint64_t last_transfer;
static void
stop_bus (const struct grub_fdtbus_dev *spi)
{
spi->driver->stop (spi);
last_transfer = grub_get_time_ms ();
}
static int
wait_for_frame (const struct grub_fdtbus_dev *spi)
{
grub_uint64_t start = grub_get_time_ms ();
grub_uint8_t byte;
do
{
if (spi->driver->receive (spi, &byte, 1))
return -1;
if (byte != EC_FRAMING_BYTE &&
grub_get_time_ms () - start > FRAMING_TIMEOUT_MS)
{
grub_dprintf ("cros", "Timeout waiting for framing byte.\n");
return -1;
}
}
while (byte != EC_FRAMING_BYTE);
return 0;
}
/*
* Calculate a simple 8-bit checksum of a data block
*
* @param data Data block to checksum
* @param size Size of data block in bytes
* @return checksum value (0 to 255)
*/
static grub_uint8_t
cros_ec_calc_checksum (const void *data, int size)
{
grub_uint8_t csum;
const grub_uint8_t *bytes = data;
int i;
for (i = csum = 0; i < size; i++)
csum += bytes[i];
return csum & 0xff;
}
enum
{
/* response, arglen */
CROS_EC_SPI_IN_HDR_SIZE = 2,
/* version, cmd, arglen */
CROS_EC_SPI_OUT_HDR_SIZE = 3
};
static grub_uint8_t busbuf[256];
#define MSG_BYTES ((int)sizeof (busbuf))
static int
ec_command (const struct grub_fdtbus_dev *dev, int cmd, int cmd_version,
const void *dout, int dout_len, void *din, int din_len)
{
const struct grub_fdtbus_dev *spi = dev->parent;
grub_uint8_t *bytes;
/* Header + data + checksum. */
grub_uint32_t out_bytes = CROS_EC_SPI_OUT_HDR_SIZE + dout_len + 1;
grub_uint32_t in_bytes = CROS_EC_SPI_IN_HDR_SIZE + din_len + 1;
/*
* Sanity-check I/O sizes given transaction overhead in internal
* buffers.
*/
if (out_bytes > MSG_BYTES)
{
grub_dprintf ("cros", "Cannot send %d bytes\n", dout_len);
return -1;
}
if (in_bytes > MSG_BYTES)
{
grub_dprintf ("cros", "Cannot receive %d bytes\n", din_len);
return -1;
}
/* Prepare the output. */
bytes = busbuf;
*bytes++ = EC_CMD_VERSION0 + cmd_version;
*bytes++ = cmd;
*bytes++ = dout_len;
grub_memcpy (bytes, dout, dout_len);
bytes += dout_len;
*bytes++ = cros_ec_calc_checksum (busbuf,
CROS_EC_SPI_OUT_HDR_SIZE + dout_len);
/* Depthcharge uses 200 us here but GRUB timer resolution is only 1ms,
decrease this when we increase timer resolution. */
while (grub_get_time_ms () - last_transfer < 1)
;
if (spi->driver->start (spi))
return -1;
/* Allow EC to ramp up clock after being awoken. */
/* Depthcharge only waits 100 us here but GRUB timer resolution is only 1ms,
decrease this when we increase timer resolution. */
grub_millisleep (1);
if (spi->driver->send (spi, busbuf, out_bytes))
{
stop_bus (spi);
return -1;
}
/* Wait until the EC is ready. */
if (wait_for_frame (spi))
{
stop_bus (spi);
return -1;
}
/* Read the response code and the data length. */
bytes = busbuf;
if (spi->driver->receive (spi, bytes, 2))
{
stop_bus (spi);
return -1;
}
grub_uint8_t result = *bytes++;
grub_uint8_t length = *bytes++;
/* Make sure there's enough room for the data. */
if (CROS_EC_SPI_IN_HDR_SIZE + length + 1 > MSG_BYTES)
{
grub_dprintf ("cros", "Received length %#02x too large\n", length);
stop_bus (spi);
return -1;
}
/* Read the data and the checksum, and finish up. */
if (spi->driver->receive (spi, bytes, length + 1))
{
stop_bus (spi);
return -1;
}
bytes += length;
int expected = *bytes++;
stop_bus (spi);
/* Check the integrity of the response. */
if (result != 0)
{
grub_dprintf ("cros", "Received bad result code %d\n", result);
return -result;
}
int csum = cros_ec_calc_checksum (busbuf,
CROS_EC_SPI_IN_HDR_SIZE + length);
if (csum != expected)
{
grub_dprintf ("cros", "Invalid checksum rx %#02x, calced %#02x\n",
expected, csum);
return -1;
}
/* If the caller wants the response, copy it out for them. */
if (length < din_len)
din_len = length;
if (din)
{
grub_memcpy (din, (grub_uint8_t *) busbuf + CROS_EC_SPI_IN_HDR_SIZE, din_len);
}
return din_len;
}
int
grub_cros_ec_scan_keyboard (const struct grub_fdtbus_dev *dev, struct grub_cros_ec_keyscan *scan)
{
if (ec_command (dev, EC_CMD_MKBP_STATE, 0, NULL, 0, scan,
sizeof (*scan)) < (int) sizeof (*scan))
return -1;
return 0;
}
int
grub_cros_ec_validate (const struct grub_fdtbus_dev *dev)
{
if (!grub_fdtbus_is_compatible("google,cros-ec-spi", dev))
return 0;
if (!dev->parent)
return 0;
if (!dev->parent->driver)
return 0;
if (!dev->parent->driver->send
|| !dev->parent->driver->receive)
return 0;
return 1;
}