| // 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 |