| // 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 <inttypes.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <threads.h> |
| #include <unistd.h> |
| |
| #include <ddk/binding.h> |
| #include <ddk/debug.h> |
| #include <ddk/driver.h> |
| #include <ddk/protocol/usb.h> |
| #include <ddktl/device.h> |
| #include <ddktl/protocol/serialimpl.h> |
| #include <fbl/alloc_checker.h> |
| #include <fbl/auto_call.h> |
| #include <fbl/auto_lock.h> |
| #include <fuchsia/hardware/serial/c/fidl.h> |
| #include <usb/usb-request.h> |
| #include <usb/usb.h> |
| #include <zircon/hw/usb.h> |
| #include <zircon/listnode.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\n", 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\n"); |
| 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; |
| } |
| |
| req->CopyFrom(&buffer[bytes_copied], to_copy, offset + FTDI_STATUS_SIZE); |
| |
| 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\n", 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() { |
| cancel_thread_ = std::thread([this]() { |
| usb_client_.CancelAll(bulk_in_addr_); |
| usb_client_.CancelAll(bulk_out_addr_); |
| DdkRemove(); |
| }); |
| } |
| |
| void FtdiDevice::DdkRelease() { |
| cancel_thread_.join(); |
| delete this; |
| } |
| |
| zx_status_t FtdiDevice::FidlCreateI2c(void* ctx, const fuchsia_hardware_ftdi_I2cBusLayout* layout, |
| const fuchsia_hardware_ftdi_I2cDevice* device) { |
| FtdiDevice* ftdi = static_cast<FtdiDevice*>(ctx); |
| |
| // Set the chip to run in MPSSE mode. |
| zx_status_t status = ftdi->SetBitMode(0, 0); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "FTDI: setting bitmode 0 failed\n"); |
| return status; |
| } |
| status = ftdi->SetBitMode(0, 2); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "FTDI: setting bitmode 2 failed\n"); |
| return status; |
| } |
| |
| status = ftdi_mpsse::FtdiI2c::Create(ftdi->zxdev(), layout, device); |
| return status; |
| } |
| |
| zx_status_t FtdiDevice::DdkMessage(fidl_msg_t* msg, fidl_txn_t* txn) { |
| static const fuchsia_hardware_ftdi_Device_ops_t ftdi_fidl_ops = { |
| .CreateI2C = FidlCreateI2c, |
| }; |
| return fuchsia_hardware_ftdi_Device_dispatch(this, txn, msg, &ftdi_fidl_ops); |
| } |
| |
| zx_status_t ftdi_bind_fail(zx_status_t status) { |
| zxlogf(ERROR, "ftdi_bind failed: %d\n", 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; |
| } |
| |
| auto usb_interface = usb_interface_list->begin(); |
| |
| uint8_t bulk_in_addr = 0; |
| uint8_t bulk_out_addr = 0; |
| |
| for (auto endp : *usb_interface) { |
| if (usb_ep_direction(&endp.descriptor) == USB_ENDPOINT_OUT) { |
| if (usb_ep_type(&endp.descriptor) == USB_ENDPOINT_BULK) { |
| bulk_out_addr = endp.descriptor.bEndpointAddress; |
| } |
| } else { |
| if (usb_ep_type(&endp.descriptor) == USB_ENDPOINT_BULK) { |
| bulk_in_addr = endp.descriptor.bEndpointAddress; |
| } |
| } |
| } |
| if (!bulk_in_addr || !bulk_out_addr) { |
| zxlogf(ERROR, "FTDI: could not find all endpoints\n"); |
| 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\n", 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\n", status); |
| return FtdiBindFail(status); |
| } |
| free_write_queue_.push(*std::move(req)); |
| } |
| |
| status = Reset(); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "FTDI reset failed %d\n", status); |
| return FtdiBindFail(status); |
| } |
| |
| status = SetBaudrate(115200); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "FTDI: set baudrate failed\n"); |
| 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\n"); |
| 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\n"); |
| 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), |
| 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 |