blob: 8e2df4c07d5996ecfff4bcb61e19975dd79fe4b1 [file] [log] [blame]
// Copyright 2021 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.
/*
* MCP2221A device interface layer.
*/
#include "hps/hal/mcp.h"
#include <memory>
#include <utility>
#include <libusb-1.0/libusb.h>
#include <stdlib.h>
#include <base/check.h>
#include <base/strings/stringprintf.h>
#include <base/threading/thread.h>
#include <base/time/time.h>
#define BIT(x) (1ULL << (x))
namespace {
// USB parameters.
static const uint16_t kUsbVendorIdMCP2221 = 0x04d8;
static const uint16_t kUsbDeviceIdMCP2221 = 0x00dd;
static const int kUsbInterfaceNumber = 2;
static const uint8_t kWriteEndpoint = 0x03; // host to device
static const uint8_t kReadEndpoint = 0x83; // device to host
static const int kTimeout = 1000; // Timeout in milliseconds.
static const int kRetries = 50; // Max retries.
static const int kDelay = 10; // Milliseconds delay between retries.
/*
* Calculate write block size.
* The I2C header is 4 bytes.
* 1 byte is reserved for the I2C cmd byte, 4 for the 32 bit address.
* The value should be a multiple of 8 since the flash writing
* is generally done in 64 bit blocks.
*/
static constexpr size_t kBlockSize =
((hps::kMcpTransferSize - 4 - sizeof(uint32_t) - 1) / 8) * 8;
// Command byte to send to MCP2221
enum : uint8_t {
kCmdStatus = 0x10,
kCmdReadData = 0x40,
kCmdGetGpio = 0x51,
kCmdSetSram = 0x60,
kCmdWriteData = 0x90,
kCmdReadRepeatStart = 0x93,
kCmdWriteNoStop = 0x94,
};
/*
* Convert status to error string.
*/
static inline const char* errString(int status) {
return libusb_strerror(static_cast<libusb_error>(status));
}
/*
* Clock divider uses 12MHz clock as base, divided by target
* bus speed in Hz, offset by 2.
*/
static inline uint8_t ClockDivider(uint32_t speedKHz) {
return (12 * 1000) / speedKHz - 2;
}
} // namespace
namespace hps {
Mcp::~Mcp() {
Close();
}
/*
* Open and initialise the MCP2221A device.
* Scan the available USB devices until the correct VID/PID is found,
* and then open that device.
*/
bool Mcp::Init(uint32_t speedKHz) {
if (speedKHz > 1000 || speedKHz < 50) {
LOG(ERROR) << "I2C bus peed must be > 50KHz and < 1000KHz";
return false;
}
this->div_ = ClockDivider(speedKHz);
int status = libusb_init(&this->context_);
if (status != 0) {
this->context_ = nullptr;
LOG(ERROR) << "libusb_init: " << errString(status);
return false;
}
// Get list of devices.
libusb_device** list;
ssize_t count = libusb_get_device_list(this->context_, &list);
bool ret = false;
for (int i = 0; i < count; i++) {
libusb_device_descriptor desc;
status = libusb_get_device_descriptor(list[i], &desc);
if (status != 0) {
LOG(ERROR) << "get_device_descriptor: " << errString(status);
break;
}
if (desc.idVendor == kUsbVendorIdMCP2221 &&
desc.idProduct == kUsbDeviceIdMCP2221) {
// Found the correct device.
status = libusb_open(list[i], &this->handle_);
if (status != 0) {
LOG(ERROR) << "libusb_open: " << errString(status);
break;
}
status = libusb_claim_interface(this->handle_, kUsbInterfaceNumber);
if (status != 0) {
LOG(ERROR) << "claim interface: " << errString(status);
// Close the handle here, since Close() assumes that the
// interface has been claimed.
libusb_close(this->handle_);
this->handle_ = nullptr;
break;
}
libusb_device* dev = libusb_get_device(this->handle_);
/*
* Read the device status to ensure it is reachable, and to
* retrieve the hardware and firmware revision.
*/
this->Clear();
this->out_[0] = kCmdStatus;
if (!this->Cmd()) {
break;
}
LOG(INFO) << base::StringPrintf(
"MCP2221 at bus %d port %d H/W rev %c.%c f/w %c.%c",
libusb_get_bus_number(dev), libusb_get_port_number(dev),
this->in_[46], this->in_[47], this->in_[48], this->in_[49]);
/* Set GPIO0 high to enable the level converter */
this->Clear();
this->out_[0] = kCmdSetSram;
/* alter GP settings */
/* The old datasheet says this must be 1, 2020 datasheet says (1<<7). */
this->out_[7] = BIT(7);
/* GP0 settings: GPIO mode, output, set high */
this->out_[8] = BIT(4);
if (!this->Cmd() || this->in_[1] != 0) {
LOG(INFO) << base::StringPrintf("MCP2221A SetSram failure 0x%x 0x%x",
this->in_[0], this->in_[1]);
break;
}
/* Check the GPIO0 setting */
this->Clear();
this->out_[0] = kCmdGetGpio;
if (!this->Cmd() || this->in_[1] != 0 || this->in_[2] != 1 ||
this->in_[3] != 0) {
LOG(INFO) << base::StringPrintf("MCP2221A Bad GPIO 0x%x 0x%x 0x%x",
this->in_[0], this->in_[2],
this->in_[3]);
break;
}
ret = true;
break;
}
}
libusb_free_device_list(list, 1);
if (!ret) {
LOG(ERROR) << "Failed to open MCP2221";
this->Close();
}
return ret;
}
void Mcp::Close() {
// If handle set, interface is claimed.
if (this->handle_ != nullptr) {
libusb_release_interface(this->handle_, kUsbInterfaceNumber);
libusb_close(this->handle_);
this->handle_ = nullptr;
}
if (this->context_ != nullptr) {
libusb_exit(this->context_);
this->context_ = nullptr;
}
}
bool Mcp::Read(uint8_t cmd, uint8_t* data, size_t len) {
// 3 bytes at the start of the buffer are reserved for the response header,
// so do not allow transfers that are larger than the remaining space.
if (len > (kMcpTransferSize - 3)) {
LOG(ERROR) << base::StringPrintf("Read req too long (%zu)", len);
return false;
}
if (!this->PrepareBus()) {
return false;
}
// Send a I2C Write Data No STOP with the cmd as data.
this->Clear();
this->out_[0] = kCmdWriteNoStop;
this->out_[1] = 1; // LSB transfer length
this->out_[2] = 0; // MSB transfer length
this->out_[3] = this->address_;
this->out_[4] = cmd;
if (!this->Cmd()) {
LOG(ERROR) << "Read (cmd phase) failed";
return false;
}
if (this->in_[1] != 0) {
// Should retry here.
LOG(ERROR) << "Read (busy) failed";
return false;
}
this->Clear();
this->out_[0] = kCmdReadRepeatStart;
this->out_[1] = len; // LSB transfer length
this->out_[2] = 0; // MSB transfer length
this->out_[3] = this->address_ | 1; // For read.
if (!this->Cmd()) {
LOG(ERROR) << "Read (read phase) failed";
return false;
}
if (this->in_[1] != 0) {
// Should retry here.
LOG(ERROR) << "Read (read busy) failed";
return false;
}
this->Clear();
for (int i = 0; i < kRetries; i++) {
this->out_[0] = kCmdReadData;
if (!this->Cmd()) {
LOG(ERROR) << "Read (read data) failed";
return false;
}
if (this->in_[1] == 0) {
if (this->in_[3] == 127) {
LOG(ERROR) << "Read (count error) failed";
return false;
}
size_t sz = this->in_[3];
if (sz > len) {
LOG(ERROR) << base::StringPrintf("Read size error (%zu)", sz);
return false;
}
memcpy(data, &this->in_[4], sz);
return true;
}
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(kDelay));
}
LOG(ERROR) << "Read (retries exceeded) failed";
return false;
}
bool Mcp::Write(uint8_t cmd, const uint8_t* data, size_t len) {
if (len > (kMcpTransferSize - 5)) {
LOG(ERROR) << base::StringPrintf("Write req too long (%zu)", len);
return false;
}
if (!this->PrepareBus()) {
return false;
}
// Send I2C Write Data
this->Clear();
this->out_[0] = kCmdWriteData;
this->out_[1] = len + 1; // LSB transfer length
this->out_[2] = 0; // MSB transfer length
this->out_[3] = this->address_;
this->out_[4] = cmd;
memcpy(&this->out_[5], data, len);
if (!this->Cmd()) {
LOG(ERROR) << "Write (cmd phase) failed";
return false;
}
// Wait for write to complete.
for (int i = 0; i < kRetries; i++) {
this->Clear();
this->out_[0] = kCmdStatus;
if (!this->Cmd()) {
LOG(ERROR) << "Write (wait for complete) failed";
return false;
}
if (this->in_[8] == 0) { // bus is idle.
return true;
}
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(kDelay));
}
LOG(ERROR) << "Write (retries exceeded) failed";
return false;
}
/*
* Return max block size for download.
*/
size_t Mcp::BlockSizeBytes() {
return kBlockSize;
}
// Ensure the I2C bus is ready. Returns true if bus is clear and ready.
bool Mcp::PrepareBus() {
this->Clear();
for (int i = 0; i < kRetries; i++) {
// Send a status/setup command.
this->out_[0] = kCmdStatus;
// If the bus is not ready, send a cancel transaction subcommand
// on the first retry. This will clear any pending transaction
// and reset the I2C controller.
this->out_[2] = (i == 1) ? 0x10 : 0; // No cancel on first try.
this->out_[3] = 0x20; // Set clock divider.
this->out_[4] = this->div_;
if (!this->Cmd()) {
LOG(ERROR) << "PrepareBus CMD failed";
return false;
}
if (this->in_[1] != 0x00) {
LOG(ERROR) << base::StringPrintf("Cmd failed, status: 0x%x",
this->in_[1]);
return false;
}
if (this->in_[3] == 0x20) { // Set speed succeeded.
return true;
}
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(kDelay));
}
LOG(ERROR) << "PrepareBus retries exceeded";
return false;
}
// Send the USB output data block and read the response.
bool Mcp::Cmd() {
CHECK(this->handle_);
int transferred = 0;
int status =
libusb_interrupt_transfer(this->handle_, kWriteEndpoint, this->out_,
kMcpTransferSize, &transferred, kTimeout);
if (status != 0) {
LOG(ERROR) << "Send (TX): " << errString(status);
return false;
}
if (transferred != kMcpTransferSize) {
LOG(WARNING) << "Short TX: " << transferred;
}
// Now read the response.
transferred = 0;
status = libusb_interrupt_transfer(this->handle_, kReadEndpoint, this->in_,
kMcpTransferSize, &transferred, kTimeout);
if (status != 0) {
LOG(ERROR) << "Send (RX): " << errString(status);
return false;
}
if (transferred != kMcpTransferSize) {
LOG(WARNING) << "Short RX: " << transferred;
}
VLOG(1) << base::StringPrintf(
"Out: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x", this->out_[0],
this->out_[1], this->out_[2], this->out_[3], this->out_[4], this->out_[5],
this->out_[6], this->out_[7], this->out_[8], this->out_[9]);
VLOG(1) << base::StringPrintf(
" In: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x", this->in_[0],
this->in_[1], this->in_[2], this->in_[3], this->in_[4], this->in_[5],
this->in_[6], this->in_[7], this->in_[8], this->in_[9]);
return true;
}
// Clear the input and output buffers.
void Mcp::Clear() {
memset(this->in_, 0, sizeof(this->in_));
memset(this->out_, 0, sizeof(this->out_));
}
// Static factory method.
std::unique_ptr<DevInterface> Mcp::Create(uint8_t address, uint32_t speedKHz) {
// Use new so that private constructor can be accessed.
auto dev = std::unique_ptr<Mcp>(new Mcp(address));
CHECK(dev->Init(speedKHz));
return std::unique_ptr<DevInterface>(std::move(dev));
}
} // namespace hps