| // 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 <ddk/binding.h> |
| #include <ddk/debug.h> |
| #include <ddk/device.h> |
| #include <ddk/driver.h> |
| #include <ddk/protocol/serialimpl.h> |
| #include <ddk/protocol/usb.h> |
| #include <ddk/usb/usb.h> |
| #include <usb/usb-request.h> |
| #include <zircon/device/serial.h> |
| #include <zircon/listnode.h> |
| #include <zircon/hw/usb.h> |
| |
| #include <inttypes.h> |
| #include <fcntl.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <threads.h> |
| #include <unistd.h> |
| |
| #include "ftdi.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) |
| |
| typedef struct { |
| zx_device_t* usb_device; |
| zx_device_t* zxdev; |
| usb_protocol_t usb; |
| |
| uint16_t ftditype; |
| uint32_t baudrate; |
| |
| serial_port_info_t serial_port_info; |
| serial_impl_protocol_t serial; |
| |
| serial_notify_t notify_cb; |
| bool enabled; |
| uint32_t state; |
| // pool of free USB requests |
| list_node_t free_read_reqs; |
| list_node_t free_write_reqs; |
| // list of received packets not yet read by upper layer |
| list_node_t completed_reads; |
| size_t read_offset; |
| |
| size_t parent_req_size; |
| |
| mtx_t mutex; |
| } ftdi_t; |
| |
| static uint32_t ftdi_check_state(ftdi_t* ftdi) { |
| uint32_t state = 0; |
| |
| state |= list_is_empty(&ftdi->free_write_reqs) ? 0 : SERIAL_STATE_WRITABLE; |
| |
| state |= list_is_empty(&ftdi->completed_reads) ? 0 : SERIAL_STATE_READABLE; |
| |
| if (state != ftdi->state) { |
| ftdi->state = state; |
| if (ftdi->notify_cb.callback) { |
| ftdi->notify_cb.callback(ftdi->notify_cb.ctx, state); |
| } |
| } |
| return state; |
| } |
| |
| static void ftdi_read_complete(usb_request_t* request, void* cookie) { |
| ftdi_t* ftdi = (ftdi_t*)cookie; |
| |
| if (request->response.status == ZX_ERR_IO_NOT_PRESENT) { |
| zxlogf(INFO,"FTDI: remote closed\n"); |
| usb_request_release(request); |
| return; |
| } |
| |
| mtx_lock(&ftdi->mutex); |
| if ((request->response.status == ZX_OK) && (request->response.actual > 2)) { |
| zx_status_t status = usb_req_list_add_tail(&ftdi->completed_reads, request, |
| ftdi->parent_req_size); |
| ZX_DEBUG_ASSERT(status == ZX_OK); |
| ftdi_check_state(ftdi); |
| } else { |
| usb_request_queue(&ftdi->usb, request); |
| } |
| mtx_unlock(&ftdi->mutex); |
| } |
| |
| static void ftdi_write_complete(usb_request_t* request, void* cookie) { |
| ftdi_t* ftdi = (ftdi_t*)cookie; |
| |
| if (request->response.status == ZX_ERR_IO_NOT_PRESENT) { |
| usb_request_release(request); |
| return; |
| } |
| mtx_lock(&ftdi->mutex); |
| zx_status_t status = usb_req_list_add_tail(&ftdi->free_write_reqs, request, |
| ftdi->parent_req_size); |
| ZX_DEBUG_ASSERT(status == ZX_OK); |
| ftdi_check_state(ftdi); |
| mtx_unlock(&ftdi->mutex); |
| } |
| |
| static zx_status_t ftdi_calc_dividers(uint32_t* baudrate, uint32_t clock, uint32_t divisor, |
| uint16_t* integer_div, uint16_t* fraction_div) { |
| |
| static const uint8_t frac_lookup[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; |
| ratio++; //round up if needed |
| ratio = ratio & 0xfffffffe; |
| |
| *baudrate = (base_clock << 4) / ratio; |
| *integer_div = ratio >> 4; |
| *fraction_div = frac_lookup[ (ratio >> 1) & 0x07 ]; |
| } |
| return ZX_OK; |
| } |
| |
| static zx_status_t ftdi_write(void *ctx, const void* buf, size_t length, size_t* actual) { |
| ftdi_t* ftdi = ctx; |
| usb_request_t* req = NULL; |
| zx_status_t status = ZX_OK; |
| |
| mtx_lock(&ftdi->mutex); |
| |
| req = usb_req_list_remove_head(&ftdi->free_write_reqs, ftdi->parent_req_size); |
| if (!req) { |
| status = ZX_ERR_SHOULD_WAIT; |
| *actual = 0; |
| goto out; |
| } |
| |
| *actual = usb_request_copy_to(req, buf, length, 0); |
| req->header.length = length; |
| |
| usb_request_queue(&ftdi->usb,req); |
| ftdi_check_state(ftdi); |
| |
| out: |
| mtx_unlock(&ftdi->mutex); |
| return status; |
| } |
| |
| |
| static zx_status_t ftdi_read(void* ctx, void* data, size_t len, size_t* actual) { |
| ftdi_t* ftdi = ctx; |
| size_t bytes_copied = 0; |
| size_t offset = ftdi->read_offset; |
| uint8_t* buffer = (uint8_t*)data; |
| |
| mtx_lock(&ftdi->mutex); |
| |
| usb_request_t* req; |
| usb_req_internal_t* req_int = list_peek_head_type(&ftdi->completed_reads, |
| usb_req_internal_t, node); |
| req = REQ_INTERNAL_TO_USB_REQ(req_int, ftdi->parent_req_size); |
| while ((req) && (bytes_copied < len)) { |
| |
| size_t to_copy = req->response.actual - offset - FTDI_STATUS_SIZE; |
| |
| if ( (to_copy + bytes_copied) > len) { |
| to_copy = len - bytes_copied; |
| } |
| |
| usb_request_copy_from(req, &buffer[bytes_copied], to_copy, |
| offset + FTDI_STATUS_SIZE); |
| |
| bytes_copied = bytes_copied + to_copy; |
| |
| if ((to_copy + offset + FTDI_STATUS_SIZE) < req->response.actual) { |
| offset = offset + to_copy; |
| goto out; |
| } else { |
| list_remove_head(&ftdi->completed_reads); |
| // requeue the read request |
| usb_request_queue(&ftdi->usb, req); |
| offset = 0; |
| } |
| |
| req_int = list_peek_head_type(&ftdi->completed_reads, usb_req_internal_t, node); |
| req = REQ_INTERNAL_TO_USB_REQ(req_int, ftdi->parent_req_size); |
| } |
| ftdi_check_state(ftdi); |
| |
| out: |
| ftdi->read_offset = offset; |
| mtx_unlock(&ftdi->mutex); |
| *actual = bytes_copied; |
| return *actual? ZX_OK : ZX_ERR_SHOULD_WAIT; |
| } |
| |
| |
| static zx_status_t ftdi_set_baudrate(ftdi_t* ftdi, uint32_t baudrate){ |
| uint16_t whole,fraction,value,index; |
| zx_status_t status; |
| |
| if (ftdi == NULL) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| switch(ftdi->ftditype) { |
| case FTDI_TYPE_R: |
| case FTDI_TYPE_2232C: |
| case FTDI_TYPE_BM: |
| ftdi_calc_dividers(&baudrate,FTDI_C_CLK,16,&whole,&fraction); |
| ftdi->baudrate = baudrate; |
| break; |
| default: |
| return ZX_ERR_INVALID_ARGS; |
| } |
| value = (whole & 0x3fff) | (fraction << 14); |
| index = fraction >> 2; |
| status = usb_control(&ftdi->usb, USB_DIR_OUT | USB_TYPE_VENDOR | USB_RECIP_DEVICE, |
| FTDI_SIO_SET_BAUDRATE, value, index, NULL, 0, |
| ZX_TIME_INFINITE,NULL); |
| if (status == ZX_OK) { |
| ftdi->baudrate = baudrate; |
| } |
| return status; |
| } |
| |
| static zx_status_t ftdi_reset(ftdi_t* ftdi) { |
| |
| if (ftdi == NULL || ftdi->usb_device == NULL) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| return usb_control( |
| &ftdi->usb, |
| USB_DIR_OUT | USB_TYPE_VENDOR | USB_RECIP_DEVICE, |
| FTDI_SIO_RESET_REQUEST, |
| FTDI_SIO_RESET, //value |
| 0, //index |
| NULL, 0, //data,length |
| ZX_TIME_INFINITE, |
| NULL); |
| } |
| |
| |
| static zx_status_t ftdi_serial_config(void* ctx, uint32_t baud_rate, uint32_t flags) { |
| ftdi_t* ftdi = ctx; |
| |
| if (baud_rate != ftdi->baudrate) { |
| return ftdi_set_baudrate(ftdi, baud_rate); |
| } |
| |
| return ZX_OK; |
| } |
| |
| |
| static zx_status_t ftdi_serial_get_info(void* ctx, serial_port_info_t* info) { |
| ftdi_t* ftdi = ctx; |
| memcpy(info, &ftdi->serial_port_info, sizeof(*info)); |
| return ZX_OK; |
| } |
| |
| static zx_status_t ftdi_serial_enable(void* ctx, bool enable) { |
| ftdi_t* ftdi = ctx; |
| ftdi->enabled = enable; |
| return ZX_OK; |
| } |
| |
| static zx_status_t ftdi_set_notify_callback(void* ctx, const serial_notify_t* cb) { |
| ftdi_t* ftdi = ctx; |
| |
| if (ftdi->enabled) { |
| return ZX_ERR_BAD_STATE; |
| } |
| |
| ftdi->notify_cb = *cb; |
| |
| mtx_lock(&ftdi->mutex); |
| ftdi_check_state(ftdi); |
| mtx_unlock(&ftdi->mutex); |
| |
| return ZX_OK; |
| } |
| |
| static serial_impl_protocol_ops_t ftdi_serial_ops = { |
| .get_info = ftdi_serial_get_info, |
| .config = ftdi_serial_config, |
| .enable = ftdi_serial_enable, |
| |
| .read = ftdi_read, |
| .write = ftdi_write, |
| .set_notify_callback = ftdi_set_notify_callback, |
| }; |
| |
| |
| static void ftdi_free(ftdi_t* ftdi) { |
| usb_request_t* req; |
| while ((req = usb_req_list_remove_head(&ftdi->free_read_reqs, ftdi->parent_req_size)) != NULL) { |
| usb_request_release(req); |
| } |
| while ((req = usb_req_list_remove_head(&ftdi->free_write_reqs, |
| ftdi->parent_req_size)) != NULL) { |
| usb_request_release(req); |
| } |
| while ((req = usb_req_list_remove_head(&ftdi->completed_reads, |
| ftdi->parent_req_size)) != NULL) { |
| usb_request_release(req); |
| } |
| |
| free(ftdi); |
| } |
| |
| static void ftdi_uart_release(void* ctx) { |
| zxlogf(INFO,"releasing ftdi uart driver\n"); |
| ftdi_t* ftdi = ctx; |
| ftdi_free(ftdi); |
| } |
| |
| static void ftdi_unbind(void* ctx) { |
| ftdi_t* ftdi = ctx; |
| device_remove(ftdi->usb_device); |
| } |
| |
| static zx_protocol_device_t ftdi_device_proto = { |
| .version = DEVICE_OPS_VERSION, |
| .unbind = ftdi_unbind, |
| .release = ftdi_uart_release, |
| }; |
| |
| static zx_status_t ftdi_bind(void* ctx, zx_device_t* device) { |
| |
| usb_protocol_t usb; |
| zx_status_t status = device_get_protocol(device, ZX_PROTOCOL_USB, &usb); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| // find our endpoints |
| usb_desc_iter_t iter; |
| status = usb_desc_iter_init(&usb, &iter); |
| |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| usb_desc_iter_next_interface(&iter, true); |
| |
| uint8_t bulk_in_addr = 0; |
| uint8_t bulk_out_addr = 0; |
| |
| usb_endpoint_descriptor_t* endp = usb_desc_iter_next_endpoint(&iter); |
| //int idx = 0; |
| while (endp) { |
| if (usb_ep_direction(endp) == USB_ENDPOINT_OUT) { |
| if (usb_ep_type(endp) == USB_ENDPOINT_BULK) { |
| bulk_out_addr = endp->bEndpointAddress; |
| } |
| } else { |
| if (usb_ep_type(endp) == USB_ENDPOINT_BULK) { |
| bulk_in_addr = endp->bEndpointAddress; |
| } |
| } |
| endp = usb_desc_iter_next_endpoint(&iter); |
| } |
| |
| usb_desc_iter_release(&iter); |
| |
| if (!bulk_in_addr || !bulk_out_addr ) { |
| zxlogf(ERROR,"FTDI: could not find all endpoints\n"); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| ftdi_t* ftdi = calloc(1, sizeof(ftdi_t)); |
| if (!ftdi) { |
| zxlogf(ERROR,"FTDI: Not enough memory\n"); |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| ftdi->ftditype = FTDI_TYPE_R; |
| |
| list_initialize(&ftdi->free_read_reqs); |
| list_initialize(&ftdi->free_write_reqs); |
| list_initialize(&ftdi->completed_reads); |
| |
| ftdi->usb_device = device; |
| |
| memcpy(&ftdi->usb, &usb, sizeof(ftdi->usb)); |
| |
| mtx_init(&ftdi->mutex, mtx_plain); |
| |
| ftdi->parent_req_size = usb_get_request_size(&ftdi->usb); |
| uint64_t req_size = ftdi->parent_req_size + sizeof(usb_req_internal_t); |
| |
| for (int i = 0; i < READ_REQ_COUNT; i++) { |
| usb_request_t* req; |
| status = usb_request_alloc(&req, USB_BUF_SIZE, bulk_in_addr, req_size); |
| if (status != ZX_OK) { |
| goto fail; |
| } |
| req->complete_cb = ftdi_read_complete; |
| req->cookie = ftdi; |
| status = usb_req_list_add_head(&ftdi->free_read_reqs, req, ftdi->parent_req_size); |
| ZX_DEBUG_ASSERT(status == ZX_OK); |
| } |
| for (int i = 0; i < WRITE_REQ_COUNT; i++) { |
| usb_request_t* req; |
| status = usb_request_alloc(&req, USB_BUF_SIZE, bulk_out_addr, req_size); |
| if (status != ZX_OK) { |
| goto fail; |
| } |
| req->complete_cb = ftdi_write_complete; |
| req->cookie = ftdi; |
| status = usb_req_list_add_head(&ftdi->free_write_reqs, req, ftdi->parent_req_size); |
| ZX_DEBUG_ASSERT(status == ZX_OK); |
| } |
| |
| if (ftdi_reset(ftdi) < 0) { |
| zxlogf(ERROR,"FTDI reset failed\n"); |
| goto fail; |
| } |
| |
| status = ftdi_set_baudrate(ftdi, 115200); |
| if (status != ZX_OK) { |
| zxlogf(ERROR,"FTDI: set baudrate failed\n"); |
| goto fail; |
| } |
| |
| ftdi->serial_port_info.serial_class = SERIAL_CLASS_GENERIC; |
| |
| device_add_args_t args = { |
| .version = DEVICE_ADD_ARGS_VERSION, |
| .name = "ftdi-uart", |
| .ctx = ftdi, |
| .ops = &ftdi_device_proto, |
| .proto_id = ZX_PROTOCOL_SERIAL_IMPL, |
| .proto_ops = &ftdi_serial_ops, |
| }; |
| |
| status = device_add(ftdi->usb_device, &args, &ftdi->zxdev); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "ftdi_uart: device_add failed\n"); |
| goto fail; |
| } |
| |
| //Queue the read requests |
| usb_req_internal_t* req_int; |
| usb_req_internal_t* prev; |
| usb_request_t* req; |
| list_for_every_entry_safe(&ftdi->free_read_reqs, req_int, prev, |
| usb_req_internal_t, node) { |
| list_delete(&req_int->node); |
| req = REQ_INTERNAL_TO_USB_REQ(req_int, ftdi->parent_req_size); |
| usb_request_queue(&ftdi->usb, req); |
| } |
| |
| zxlogf(INFO,"ftdi bind successful\n"); |
| return status; |
| |
| fail: |
| zxlogf(ERROR,"ftdi_bind failed: %d\n", status); |
| ftdi_free(ftdi); |
| return status; |
| } |
| |
| static zx_driver_ops_t _ftdi_driver_ops = { |
| .version = DRIVER_OPS_VERSION, |
| .bind = ftdi_bind, |
| }; |
| |
| ZIRCON_DRIVER_BEGIN(ftdi, _ftdi_driver_ops, "zircon", "0.1", 3) |
| BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_USB), |
| BI_MATCH_IF(EQ, BIND_USB_VID, FTDI_VID), |
| BI_MATCH_IF(EQ, BIND_USB_PID, FTDI_232R_PID), |
| ZIRCON_DRIVER_END(ftdi) |