blob: 3f56b7275d7f80cd401298fe3a935306c4e5965c [file] [log] [blame]
// Copyright 2018 The Fuchsia 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 "ftdi.h"
#include <fcntl.h>
#include <fuchsia/hardware/serial/c/fidl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <threads.h>
#include <unistd.h>
#include <zircon/hw/usb.h>
#include <zircon/listnode.h>
#include <ddk/binding.h>
#include <ddk/debug.h>
#include <ddk/driver.h>
#include <ddk/protocol/usb.h>
#include <ddktl/device.h>
#include <ddktl/fidl.h>
#include <ddktl/protocol/serialimpl.h>
#include <fbl/alloc_checker.h>
#include <fbl/auto_call.h>
#include <fbl/auto_lock.h>
#include <usb/usb-request.h>
#include <usb/usb.h>
#include "ftdi-i2c.h"
#define FTDI_STATUS_SIZE 2
#define FTDI_RX_HEADER_SIZE 4
#define READ_REQ_COUNT 8
#define WRITE_REQ_COUNT 4
#define INTR_REQ_COUNT 4
#define USB_BUF_SIZE 2048
#define INTR_REQ_SIZE 4
#define FIFOSIZE 256
#define FIFOMASK (FIFOSIZE - 1)
namespace {
static zx_status_t FtdiBindFail(zx_status_t status) {
zxlogf(ERROR, "ftdi_bind failed: %d", status);
return status;
}
} // namespace
namespace ftdi_serial {
void FtdiDevice::NotifyCallback() {
if (need_to_notify_cb_ == true) {
need_to_notify_cb_ = false;
if (notify_cb_.callback) {
notify_cb_.callback(notify_cb_.ctx, state_);
}
}
}
void FtdiDevice::CheckStateLocked() {
uint32_t state = 0;
state |= free_write_queue_.is_empty() ? 0 : SERIAL_STATE_WRITABLE;
state |= completed_reads_queue_.is_empty() ? 0 : SERIAL_STATE_READABLE;
if (state != state_) {
state_ = state;
need_to_notify_cb_ = true;
}
}
void FtdiDevice::ReadComplete(usb_request_t* request) {
usb::Request<> req(request, parent_req_size_);
if (req.request()->response.status == ZX_ERR_IO_NOT_PRESENT) {
zxlogf(INFO, "FTDI: remote closed");
return;
}
fbl::AutoLock lock(&mutex_);
if ((req.request()->response.status == ZX_OK) && (req.request()->response.actual > 2)) {
completed_reads_queue_.push(std::move(req));
CheckStateLocked();
} else {
usb_request_complete_t complete = {
.callback =
[](void* ctx, usb_request_t* request) {
static_cast<FtdiDevice*>(ctx)->ReadComplete(request);
},
.ctx = this,
};
usb_client_.RequestQueue(req.take(), &complete);
}
lock.release();
NotifyCallback();
}
void FtdiDevice::WriteComplete(usb_request_t* request) {
usb::Request<> req(request, parent_req_size_);
if (req.request()->response.status == ZX_ERR_IO_NOT_PRESENT) {
return;
}
fbl::AutoLock lock(&mutex_);
free_write_queue_.push(std::move(req));
CheckStateLocked();
lock.release();
NotifyCallback();
}
zx_status_t FtdiDevice::CalcDividers(uint32_t* baudrate, uint32_t clock, uint32_t divisor,
uint16_t* integer_div, uint16_t* fraction_div) {
static constexpr uint8_t kFractionLookup[8] = {0, 3, 2, 4, 1, 5, 6, 7};
uint32_t base_clock = clock / divisor;
// Integer dividers of 1 and 0 are special cases.
// 0 = base_clock and 1 = 2/3 of base clock.
if (*baudrate >= base_clock) {
// Return with max baud rate achievable.
*fraction_div = 0;
*integer_div = 0;
*baudrate = base_clock;
} else if (*baudrate >= (base_clock * 2) / 3) {
*integer_div = 1;
*fraction_div = 0;
*baudrate = (base_clock * 2) / 3;
} else {
// Create a 28.4 fractional integer.
uint32_t ratio = (base_clock * 16) / *baudrate;
// Round up if needed.
ratio++;
ratio = ratio & 0xfffffffe;
*baudrate = (base_clock << 4) / ratio;
*integer_div = static_cast<uint16_t>(ratio >> 4);
*fraction_div = kFractionLookup[(ratio >> 1) & 0x07];
}
return ZX_OK;
}
zx_status_t FtdiDevice::SerialImplWrite(const void* buf, size_t length, size_t* actual) {
return DdkWrite(buf, length, 0, actual);
}
zx_status_t FtdiDevice::DdkWrite(const void* buf, size_t length, zx_off_t off, size_t* actual) {
zx_status_t status = ZX_OK;
fbl::AutoLock lock(&mutex_);
std::optional<usb::Request<>> req = free_write_queue_.pop();
if (!req) {
status = ZX_ERR_SHOULD_WAIT;
*actual = 0;
return status;
}
*actual = req->CopyTo(buf, length, 0);
req->request()->header.length = length;
usb_request_complete_t complete = {
.callback =
[](void* ctx, usb_request_t* request) {
static_cast<FtdiDevice*>(ctx)->WriteComplete(request);
},
.ctx = this,
};
usb_client_.RequestQueue(req->take(), &complete);
CheckStateLocked();
lock.release();
NotifyCallback();
return status;
}
zx_status_t FtdiDevice::SerialImplRead(void* data, size_t len, size_t* actual) {
zx_status_t status = DdkRead(data, len, 0, actual);
if (status == ZX_OK && (actual == 0)) {
return ZX_ERR_SHOULD_WAIT;
}
return status;
}
zx_status_t FtdiDevice::DdkRead(void* data, size_t len, zx_off_t off, size_t* actual) {
size_t bytes_copied = 0;
size_t offset = read_offset_;
uint8_t* buffer = static_cast<uint8_t*>(data);
usb_request_complete_t complete = {
.callback =
[](void* ctx, usb_request_t* request) {
static_cast<FtdiDevice*>(ctx)->ReadComplete(request);
},
.ctx = this,
};
fbl::AutoLock lock(&mutex_);
while (bytes_copied < len) {
std::optional<usb::Request<>> req = completed_reads_queue_.pop();
if (!req) {
break;
}
size_t to_copy = req->request()->response.actual - offset - FTDI_STATUS_SIZE;
if ((to_copy + bytes_copied) > len) {
to_copy = len - bytes_copied;
}
size_t result = req->CopyFrom(&buffer[bytes_copied], to_copy, offset + FTDI_STATUS_SIZE);
ZX_ASSERT(result == to_copy);
bytes_copied = bytes_copied + to_copy;
// If we aren't reading the whole request then put it in the front of the queue
// and return.
if ((to_copy + offset + FTDI_STATUS_SIZE) < req->request()->response.actual) {
offset = offset + to_copy;
completed_reads_queue_.push_next(*std::move(req));
break;
}
// Requeue the read request.
usb_client_.RequestQueue(req->take(), &complete);
offset = 0;
}
CheckStateLocked();
read_offset_ = offset;
*actual = bytes_copied;
lock.release();
NotifyCallback();
return ZX_OK;
}
zx_status_t FtdiDevice::SetBaudrate(uint32_t baudrate) {
uint16_t whole, fraction, value, index;
zx_status_t status;
switch (ftditype_) {
case kFtdiTypeR:
case kFtdiType2232c:
case kFtdiTypeBm:
CalcDividers(&baudrate, kFtdiCClk, 16, &whole, &fraction);
baudrate_ = baudrate;
break;
default:
return ZX_ERR_INVALID_ARGS;
}
value = static_cast<uint16_t>((whole & 0x3fff) | (fraction << 14));
index = static_cast<uint16_t>(fraction >> 2);
status = usb_client_.ControlOut(USB_DIR_OUT | USB_TYPE_VENDOR | USB_RECIP_DEVICE,
kFtdiSioSetBaudrate, value, index, ZX_TIME_INFINITE, NULL, 0);
if (status == ZX_OK) {
baudrate_ = baudrate;
}
return status;
}
zx_status_t FtdiDevice::Reset() {
if (!usb_client_.is_valid()) {
return ZX_ERR_INVALID_ARGS;
}
return usb_client_.ControlOut(USB_DIR_OUT | USB_TYPE_VENDOR | USB_RECIP_DEVICE,
kFtdiSioResetRequest, kFtdiSioReset, 0, ZX_TIME_INFINITE, NULL, 0);
}
zx_status_t FtdiDevice::SetBitMode(uint8_t line_mask, uint8_t mode) {
uint16_t val = static_cast<uint16_t>(line_mask | (mode << 8));
zx_status_t status =
usb_client_.ControlOut(USB_DIR_OUT | USB_TYPE_VENDOR | USB_RECIP_DEVICE, kFtdiSioSetBitmode,
val, 0, ZX_TIME_INFINITE, NULL, 0);
if (status != ZX_OK) {
zxlogf(ERROR, "FTDI set bitmode failed with %d", status);
return status;
}
return status;
}
zx_status_t FtdiDevice::SerialImplConfig(uint32_t baudrate, uint32_t flags) {
if (baudrate != baudrate_) {
return SetBaudrate(baudrate);
}
return ZX_OK;
}
zx_status_t FtdiDevice::SerialImplGetInfo(serial_port_info_t* info) {
memcpy(info, &serial_port_info_, sizeof(*info));
return ZX_OK;
}
zx_status_t FtdiDevice::SerialImplEnable(bool enable) {
enabled_ = enable;
return ZX_OK;
}
zx_status_t FtdiDevice::SerialImplSetNotifyCallback(const serial_notify_t* cb) {
if (enabled_) {
return ZX_ERR_BAD_STATE;
}
notify_cb_ = *cb;
fbl::AutoLock lock(&mutex_);
CheckStateLocked();
lock.release();
NotifyCallback();
return ZX_OK;
}
FtdiDevice::~FtdiDevice() {}
void FtdiDevice::DdkUnbind(ddk::UnbindTxn txn) {
cancel_thread_ = std::thread([this, unbind_txn = std::move(txn)]() mutable {
usb_client_.CancelAll(bulk_in_addr_);
usb_client_.CancelAll(bulk_out_addr_);
unbind_txn.Reply();
});
}
void FtdiDevice::DdkRelease() {
cancel_thread_.join();
delete this;
}
void FtdiDevice::CreateI2C(::llcpp::fuchsia::hardware::ftdi::I2cBusLayout layout,
::llcpp::fuchsia::hardware::ftdi::I2cDevice device,
CreateI2CCompleter::Sync& completer) {
// Set the chip to run in MPSSE mode.
zx_status_t status = this->SetBitMode(0, 0);
if (status != ZX_OK) {
zxlogf(ERROR, "FTDI: setting bitmode 0 failed");
return;
}
status = this->SetBitMode(0, 2);
if (status != ZX_OK) {
zxlogf(ERROR, "FTDI: setting bitmode 2 failed");
return;
}
ftdi_mpsse::FtdiI2c::Create(this->zxdev(), &layout, &device);
}
zx_status_t FtdiDevice::DdkMessage(fidl_incoming_msg_t* msg, fidl_txn_t* txn) {
DdkTransaction transaction(txn);
::llcpp::fuchsia::hardware::ftdi::Device::Dispatch(this, msg, &transaction);
return transaction.Status();
}
zx_status_t ftdi_bind_fail(zx_status_t status) {
zxlogf(ERROR, "ftdi_bind failed: %d", status);
return status;
}
zx_status_t FtdiDevice::Bind() {
zx_status_t status = ZX_OK;
if (!usb_client_.is_valid()) {
return ZX_ERR_NOT_SUPPORTED;
}
fbl::AutoLock lock(&mutex_);
// Find our endpoints.
std::optional<usb::InterfaceList> usb_interface_list;
status = usb::InterfaceList::Create(usb_client_, true, &usb_interface_list);
if (status != ZX_OK) {
return status;
}
uint8_t bulk_in_addr = 0;
uint8_t bulk_out_addr = 0;
for (auto& interface : *usb_interface_list) {
for (auto ep_itr : interface.GetEndpointList()) {
if (usb_ep_direction(&ep_itr.descriptor) == USB_ENDPOINT_OUT) {
if (usb_ep_type(&ep_itr.descriptor) == USB_ENDPOINT_BULK) {
bulk_out_addr = ep_itr.descriptor.bEndpointAddress;
}
} else {
if (usb_ep_type(&ep_itr.descriptor) == USB_ENDPOINT_BULK) {
bulk_in_addr = ep_itr.descriptor.bEndpointAddress;
}
}
}
}
if (!bulk_in_addr || !bulk_out_addr) {
zxlogf(ERROR, "FTDI: could not find all endpoints");
return ZX_ERR_NOT_SUPPORTED;
}
ftditype_ = kFtdiTypeR;
parent_req_size_ = usb_client_.GetRequestSize();
for (int i = 0; i < READ_REQ_COUNT; i++) {
std::optional<usb::Request<>> req;
status = usb::Request<>::Alloc(&req, USB_BUF_SIZE, bulk_in_addr, parent_req_size_);
if (status != ZX_OK) {
zxlogf(ERROR, "FTDI allocating reads failed %d", status);
return FtdiBindFail(status);
}
free_read_queue_.push(*std::move(req));
}
for (int i = 0; i < WRITE_REQ_COUNT; i++) {
std::optional<usb::Request<>> req;
status = usb::Request<>::Alloc(&req, USB_BUF_SIZE, bulk_out_addr, parent_req_size_);
if (status != ZX_OK) {
zxlogf(ERROR, "FTDI allocating writes failed %d", status);
return FtdiBindFail(status);
}
free_write_queue_.push(*std::move(req));
}
status = Reset();
if (status != ZX_OK) {
zxlogf(ERROR, "FTDI reset failed %d", status);
return FtdiBindFail(status);
}
status = SetBaudrate(115200);
if (status != ZX_OK) {
zxlogf(ERROR, "FTDI: set baudrate failed");
return FtdiBindFail(status);
}
serial_port_info_.serial_class = fuchsia_hardware_serial_Class_GENERIC;
status = DdkAdd("ftdi-uart");
if (status != ZX_OK) {
zxlogf(ERROR, "ftdi_uart: device_add failed");
return FtdiBindFail(status);
}
usb_request_complete_t complete = {
.callback =
[](void* ctx, usb_request_t* request) {
static_cast<FtdiDevice*>(ctx)->ReadComplete(request);
},
.ctx = this,
};
// Queue the read requests.
std::optional<usb::Request<>> req;
while ((req = free_read_queue_.pop())) {
usb_client_.RequestQueue(req->take(), &complete);
}
bulk_in_addr_ = bulk_in_addr;
bulk_out_addr_ = bulk_out_addr;
zxlogf(INFO, "ftdi bind successful");
return status;
}
} // namespace ftdi_serial
namespace {
zx_status_t ftdi_bind(void* ctx, zx_device_t* device) {
fbl::AllocChecker ac;
auto dev = fbl::make_unique_checked<ftdi_serial::FtdiDevice>(&ac, device);
if (!ac.check()) {
return ZX_ERR_NO_MEMORY;
}
auto status = dev->Bind();
if (status == ZX_OK) {
// Devmgr is now in charge of the memory for dev.
__UNUSED auto ptr = dev.release();
}
return status;
}
static constexpr zx_driver_ops_t ftdi_driver_ops = []() {
zx_driver_ops_t ops = {};
ops.version = DRIVER_OPS_VERSION;
ops.bind = ftdi_bind;
return ops;
}();
} // namespace
// clang-format off
ZIRCON_DRIVER_BEGIN(ftdi, ftdi_driver_ops, "zircon", "0.1", 4)
BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_USB_INTERFACE),
BI_MATCH_IF(EQ, BIND_USB_VID, ftdi_serial::kFtdiUsbVid),
BI_MATCH_IF(EQ, BIND_USB_PID, ftdi_serial::kFtdiUsb232rPid),
BI_MATCH_IF(EQ, BIND_USB_PID, ftdi_serial::kFtdiUsb232hPid),
ZIRCON_DRIVER_END(ftdi)
// clang-format on