blob: 80e1c0498ce962eff17a5664848d09304cf033ad [file] [log] [blame]
/* 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 "byteorder.h"
#include "common.h"
#include "console.h"
#include "consumer.h"
#include "include/compile_time_macros.h"
#include "queue_policies.h"
#include "shared_mem.h"
#include "system.h"
#include "update_fw.h"
#include "usb-stream.h"
#include "util.h"
#define CPRINTS(format, args...) cprints(CC_USB, format, ## args)
/*
* This file is an adaptation layer between the USB interface and the firmware
* update engine. The engine expects to receive long blocks of data, 1K or so
* in size, prepended by the offset where the data needs to be programmed into
* the flash and a 4 byte integrity check value.
*
* The USB transfer, on the other hand, operates on much shorter chunks of
* data, typically 64 bytes in this case. This module reassembles firmware
* programming blocks from the USB chunks, and invokes the programmer passing
* it the full block.
*
* The programmer reports results by putting the return value of one or four
* bytes into the same buffer where the block was passed in. This wrapper
* retrieves the programmer's return value, normalizes it to 4 bytes and sends
* it back to the host.
*
* In the end of the successful image transfer and programming, the host send
* the reset command, and the device reboots itself.
*/
struct consumer const update_consumer;
struct usb_stream_config const usb_update;
static struct queue const update_to_usb = QUEUE_DIRECT(64, uint8_t,
null_producer,
usb_update.consumer);
static struct queue const usb_to_update = QUEUE_DIRECT(64, uint8_t,
usb_update.producer,
update_consumer);
USB_STREAM_CONFIG_FULL(usb_update,
USB_IFACE_UPDATE,
USB_CLASS_VENDOR_SPEC,
USB_SUBCLASS_GOOGLE_UPDATE,
USB_PROTOCOL_GOOGLE_UPDATE,
USB_STR_UPDATE_NAME,
USB_EP_UPDATE,
USB_MAX_PACKET_SIZE,
USB_MAX_PACKET_SIZE,
usb_to_update,
update_to_usb)
/* The receiver can be in one of the states below. */
enum rx_state {
rx_idle, /* Nothing happened yet. */
rx_inside_block, /* Assembling a block to pass to the programmer. */
rx_outside_block, /* Waiting for the next block to start or for the
reset command. */
rx_awaiting_reset /* Waiting for reset confirmation. */
};
/* This is the format of the header the programmer expects. */
struct update_command {
uint32_t block_digest; /* first 4 bytes of sha1 of the rest of the
block. */
uint32_t block_base; /* Offset of this block into the flash SPI. */
};
/* This is the format of the header the host uses. */
struct update_pdu_header {
uint32_t block_size; /* Total size of the block, including this
field. */
union {
struct update_command cmd;
uint32_t resp; /* The programmer puts response to the same
buffer where the command was. */
};
/* The actual payload goes here. */
};
enum rx_state rx_state_ = rx_idle;
static uint8_t *block_buffer;
static uint32_t block_size;
static uint32_t block_index;
/*
* Verify that the contens of the USB rx queue is a valid transfer start
* message from host, and if so - save its contents in the passed in
* update_pdu_header structure.
*/
static int valid_transfer_start(struct consumer const *consumer, size_t count,
struct update_pdu_header *pupdu)
{
int i;
/*
* Let's just make sure we drain the queue no matter what the contents
* are. This way they won't be in the way during next callback, even
* if these contents are not what's expected.
*/
i = count;
while (i > 0) {
QUEUE_REMOVE_UNITS(consumer->queue, pupdu,
MIN(i, sizeof(*pupdu)));
i -= sizeof(*pupdu);
}
if (count != sizeof(struct update_pdu_header)) {
CPRINTS("FW update: wrong first block, size %d", count);
return 0;
}
/* In the first block the payload (updu.cmd) must be all zeros. */
for (i = 0; i < sizeof(pupdu->cmd); i++)
if (((uint8_t *)&pupdu->cmd)[i])
return 0;
return 1;
}
/*
* When was last time a USB callback was called, in microseconds, free running
* timer.
*/
static uint64_t prev_activity_timestamp;
#define UPDATE_PROTOCOL_VERSION 2
/* Called to deal with data from the host */
static void update_out_handler(struct consumer const *consumer, size_t count)
{
struct update_pdu_header updu;
size_t resp_size;
uint32_t resp_value;
uint64_t delta_time;
/* How much time since the previous USB callback? */
delta_time = get_time().val - prev_activity_timestamp;
prev_activity_timestamp += delta_time;
/* If timeout exceeds 5 seconds - let's start over. */
if ((delta_time > 5000000) && (rx_state_ != rx_idle)) {
if (block_buffer) {
/*
* Previous transfer could have been aborted mid
* block.
*/
shared_mem_release(block_buffer);
block_buffer = NULL;
}
rx_state_ = rx_idle;
CPRINTS("FW update: recovering after timeout");
}
if (rx_state_ == rx_idle) {
/*
* When responding to the very first packet of the update
* sequence, the original implementation was responding with a
* four byte value, just as to any other block of the transfer
* sequence.
*
* It became clear that there is a need to be able to enhance
* the update protocol, while stayng backwards compatible. To
* achieve that we respond to the very first packet with an 8
* byte value, the first 4 bytes the same as before, the
* second 4 bytes - the protocol version number.
*
* This way if on the host side receiving of a four byte value
* in response to the first packet is an indication of the
* 'legacy' protocol, version 0. Receiving of an 8 byte
* response would communicate the protocol version in the
* second 4 bytes.
*/
struct {
uint32_t value;
uint32_t version;
} startup_resp;
if (!valid_transfer_start(consumer, count, &updu))
return;
CPRINTS("FW update: starting...");
fw_update_command_handler(&updu.cmd, count -
offsetof(struct update_pdu_header,
cmd),
&resp_size);
if (resp_size == 4) {
/* Already in network order. */
startup_resp.value = updu.resp;
rx_state_ = rx_outside_block;
} else {
/* This must be a single byte error code. */
startup_resp.value = htobe32(*((uint8_t *)&updu.resp));
}
startup_resp.version = htobe32(UPDATE_PROTOCOL_VERSION);
/* Let the host know what updater had to say. */
QUEUE_ADD_UNITS(&update_to_usb, &startup_resp,
sizeof(startup_resp));
return;
}
if (rx_state_ == rx_awaiting_reset) {
/*
* Any USB data received in this state triggers reset, no
* response required.
*/
CPRINTS("reboot hard");
cflush();
system_reset(SYSTEM_RESET_HARD);
while (1)
;
}
if (rx_state_ == rx_outside_block) {
/*
* Expecting to receive the beginning of the block or the
* reset command if all data blocks have been processed.
*/
if (count == 4) {
uint32_t command;
QUEUE_REMOVE_UNITS(consumer->queue, &command,
sizeof(command));
command = be32toh(command);
if (command == UPDATE_DONE) {
CPRINTS("FW update: done");
resp_value = 0;
QUEUE_ADD_UNITS(&update_to_usb, &resp_value,
sizeof(resp_value));
rx_state_ = rx_awaiting_reset;
return;
} else {
CPRINTS("Unexpected packet command 0x%x",
command);
}
}
/*
* At this point we expect a block start message. It is
* sizeof(updu) bytes in size, but is not the transfer start
* message, which also is of that size AND has the command
* field of all zeros.
*/
if (valid_transfer_start(consumer, count, &updu) ||
(count != sizeof(updu)))
/*
* Instead of a block start message we received either
* a transfer start message or a chunk. We must have
* gotten out of sync with the host.
*/
return;
/* Let's allocate a large enough buffer. */
block_size = be32toh(updu.block_size) -
offsetof(struct update_pdu_header, cmd);
if (shared_mem_acquire(block_size, (char **)&block_buffer)
!= EC_SUCCESS) {
/* TODO:(vbendeb) report out of memory here. */
CPRINTS("FW update: error: failed to alloc %d bytes.",
block_size);
return;
}
/*
* Copy the rest of the message into the block buffer to pass
* to the updater.
*/
block_index = sizeof(updu) -
offsetof(struct update_pdu_header, cmd);
memcpy(block_buffer, &updu.cmd, block_index);
block_size -= block_index;
rx_state_ = rx_inside_block;
return;
}
/* Must be inside block. */
QUEUE_REMOVE_UNITS(consumer->queue, block_buffer + block_index, count);
block_index += count;
block_size -= count;
if (block_size) {
if (count == sizeof(updu)) {
/*
* A block header size instead of chunk size message
* has been received. There must have been some packet
* loss and the host is restarting this block.
*
* Let's copy its contents into the header structure.
*/
memcpy(&updu, block_buffer + block_index - count,
count);
/* And re-allocate a large enough buffer. */
shared_mem_release(block_buffer);
block_size = be32toh(updu.block_size) -
offsetof(struct update_pdu_header, cmd);
if (shared_mem_acquire(block_size,
(char **)&block_buffer)
!= EC_SUCCESS) {
/* TODO:(vbendeb) report out of memory here. */
CPRINTS("FW update: error: failed to alloc "
"%d bytes.", block_size);
return;
}
/*
* Copy the rest of the message into the block buffer
* to pass to the updater.
*/
block_index = sizeof(updu) -
offsetof(struct update_pdu_header, cmd);
memcpy(block_buffer, &updu.cmd, block_index);
block_size -= block_index;
}
return; /* More to come. */
}
/*
* Ok, the entire block has been received and reassembled, pass it to
* the updater for verification and programming.
*/
fw_update_command_handler(block_buffer, block_index, &resp_size);
resp_value = block_buffer[0];
QUEUE_ADD_UNITS(&update_to_usb, &resp_value, sizeof(resp_value));
rx_state_ = rx_outside_block;
shared_mem_release(block_buffer);
block_buffer = NULL;
}
static void update_flush(struct consumer const *consumer)
{
}
struct consumer const update_consumer = {
.queue = &usb_to_update,
.ops = &((struct consumer_ops const) {
.written = update_out_handler,
.flush = update_flush,
}),
};