| // Copyright 2017 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/ethernet.h> |
| #include <ddk/protocol/usb.h> |
| #include <ddk/protocol/usb/composite.h> |
| #include <usb/usb.h> |
| #include <usb/usb-request.h> |
| #include <zircon/hw/usb/cdc.h> |
| #include <lib/sync/completion.h> |
| |
| #include <inttypes.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <threads.h> |
| |
| #define CDC_SUPPORTED_VERSION 0x0110 /* 1.10 */ |
| |
| // The maximum amount of memory we are willing to allocate to transaction buffers |
| #define MAX_TX_BUF_SZ 32768 |
| #define MAX_RX_BUF_SZ 1500 * 2048 |
| |
| #define ETHMAC_MAX_TRANSMIT_DELAY 100 |
| #define ETHMAC_MAX_RECV_DELAY 100 |
| #define ETHMAC_TRANSMIT_DELAY 10 |
| #define ETHMAC_RECV_DELAY 10 |
| #define ETHMAC_INITIAL_TRANSMIT_DELAY 0 |
| #define ETHMAC_INITIAL_RECV_DELAY 0 |
| |
| const char* module_name = "usb-cdc-ecm"; |
| |
| typedef struct { |
| uint8_t addr; |
| uint16_t max_packet_size; |
| } ecm_endpoint_t; |
| |
| typedef struct { |
| zx_device_t* zxdev; |
| zx_device_t* usb_device; |
| usb_protocol_t usb; |
| // Ethmac lock -- must be acquired after tx_mutex |
| // when both locks are held. |
| mtx_t ethmac_mutex; |
| ethmac_ifc_protocol_t ethmac_ifc; |
| |
| // Device attributes |
| uint8_t mac_addr[ETH_MAC_SIZE]; |
| uint16_t mtu; |
| |
| // Connection attributes |
| bool online; |
| uint32_t ds_bps; |
| uint32_t us_bps; |
| |
| // Interrupt handling |
| ecm_endpoint_t int_endpoint; |
| usb_request_t* int_txn_buf; |
| sync_completion_t completion; |
| thrd_t int_thread; |
| |
| // Send context |
| // TX lock -- Must be acquired before ethmac_mutex |
| // when both locks are held. |
| mtx_t tx_mutex; |
| ecm_endpoint_t tx_endpoint; |
| list_node_t tx_txn_bufs; // list of usb_request_t |
| list_node_t tx_pending_infos; // list of txn_info_t |
| bool unbound; // set to true when device is going away. Guarded by tx_mutex |
| uint64_t tx_endpoint_delay; // wait time between 2 transmit requests |
| |
| size_t parent_req_size; |
| |
| // Receive context |
| ecm_endpoint_t rx_endpoint; |
| uint64_t rx_endpoint_delay; // wait time between 2 recv requests |
| } ecm_ctx_t; |
| |
| typedef struct txn_info { |
| ethmac_netbuf_t netbuf; |
| list_node_t node; |
| } txn_info_t; |
| |
| static void usb_write_complete(void* cookie, usb_request_t* request); |
| |
| static void ecm_unbind(void* cookie) { |
| zxlogf(TRACE, "%s: unbinding\n", module_name); |
| ecm_ctx_t* ctx = cookie; |
| |
| mtx_lock(&ctx->tx_mutex); |
| ctx->unbound = true; |
| if (ctx->ethmac_ifc.ops) { |
| txn_info_t* txn; |
| while ((txn = list_remove_head_type(&ctx->tx_pending_infos, txn_info_t, node)) != |
| NULL) { |
| ethmac_ifc_complete_tx(&ctx->ethmac_ifc, &txn->netbuf, ZX_ERR_PEER_CLOSED); |
| } |
| } |
| mtx_unlock(&ctx->tx_mutex); |
| |
| device_remove(ctx->zxdev); |
| } |
| |
| static void ecm_free(ecm_ctx_t* ctx) { |
| zxlogf(TRACE, "%s: deallocating memory\n", module_name); |
| if (ctx->int_thread) { |
| thrd_join(ctx->int_thread, NULL); |
| } |
| usb_request_t* txn; |
| while ((txn = usb_req_list_remove_head(&ctx->tx_txn_bufs, ctx->parent_req_size)) != NULL) { |
| usb_request_release(txn); |
| } |
| if (ctx->int_txn_buf) { |
| usb_request_release(ctx->int_txn_buf); |
| } |
| mtx_destroy(&ctx->ethmac_mutex); |
| mtx_destroy(&ctx->tx_mutex); |
| free(ctx); |
| } |
| |
| static void ecm_release(void* ctx) { |
| ecm_ctx_t* eth = ctx; |
| ecm_free(eth); |
| } |
| |
| static zx_protocol_device_t ecm_device_proto = { |
| .version = DEVICE_OPS_VERSION, |
| .unbind = ecm_unbind, |
| .release = ecm_release, |
| }; |
| |
| static void ecm_update_online_status(ecm_ctx_t* ctx, bool is_online) { |
| mtx_lock(&ctx->ethmac_mutex); |
| if ((is_online && ctx->online) || (!is_online && !ctx->online)) { |
| goto done; |
| } |
| |
| if (is_online) { |
| zxlogf(INFO, "%s: connected to network\n", module_name); |
| ctx->online = true; |
| if (ctx->ethmac_ifc.ops) { |
| ethmac_ifc_status(&ctx->ethmac_ifc, ETHMAC_STATUS_ONLINE); |
| } else { |
| zxlogf(ERROR, "%s: not connected to ethermac interface\n", module_name); |
| } |
| } else { |
| zxlogf(INFO, "%s: no connection to network\n", module_name); |
| ctx->online = false; |
| if (ctx->ethmac_ifc.ops) { |
| ethmac_ifc_status(&ctx->ethmac_ifc, 0); |
| } |
| } |
| |
| done: |
| mtx_unlock(&ctx->ethmac_mutex); |
| } |
| |
| static zx_status_t ecm_ethmac_query(void* ctx, uint32_t options, ethmac_info_t* info) { |
| ecm_ctx_t* eth = ctx; |
| |
| zxlogf(TRACE, "%s: %s called\n", module_name, __FUNCTION__); |
| |
| // No options are supported |
| if (options) { |
| zxlogf(ERROR, "%s: unexpected options (0x%"PRIx32") to ecm_ethmac_query\n", module_name, |
| options); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| memset(info, 0, sizeof(*info)); |
| info->mtu = eth->mtu; |
| memcpy(info->mac, eth->mac_addr, sizeof(eth->mac_addr)); |
| info->netbuf_size = sizeof(txn_info_t); |
| |
| return ZX_OK; |
| } |
| |
| static void ecm_ethmac_stop(void* cookie) { |
| zxlogf(TRACE, "%s: %s called\n", module_name, __FUNCTION__); |
| ecm_ctx_t* ctx = cookie; |
| mtx_lock(&ctx->tx_mutex); |
| mtx_lock(&ctx->ethmac_mutex); |
| ctx->ethmac_ifc.ops = NULL; |
| mtx_unlock(&ctx->ethmac_mutex); |
| mtx_unlock(&ctx->tx_mutex); |
| } |
| |
| static zx_status_t ecm_ethmac_start(void* ctx_cookie, const ethmac_ifc_protocol_t* ifc) { |
| zxlogf(TRACE, "%s: %s called\n", module_name, __FUNCTION__); |
| ecm_ctx_t* ctx = ctx_cookie; |
| zx_status_t status = ZX_OK; |
| |
| mtx_lock(&ctx->ethmac_mutex); |
| if (ctx->ethmac_ifc.ops) { |
| status = ZX_ERR_ALREADY_BOUND; |
| } else { |
| ctx->ethmac_ifc = *ifc; |
| ethmac_ifc_status(&ctx->ethmac_ifc, ctx->online ? ETHMAC_STATUS_ONLINE : 0); |
| } |
| mtx_unlock(&ctx->ethmac_mutex); |
| |
| return status; |
| } |
| |
| static zx_status_t queue_request(ecm_ctx_t* ctx, const uint8_t* data, size_t length, |
| usb_request_t* req) { |
| req->header.length = length; |
| if (!ctx->ethmac_ifc.ops) { |
| return ZX_ERR_BAD_STATE; |
| } |
| ssize_t bytes_copied = usb_request_copy_to(req, data, length, 0); |
| if (bytes_copied < 0) { |
| zxlogf(ERROR, "%s: failed to copy data into send txn (error %zd)\n", module_name, |
| bytes_copied); |
| return ZX_ERR_IO; |
| } |
| usb_request_complete_t complete = { |
| .callback = usb_write_complete, |
| .ctx = ctx, |
| }; |
| usb_request_queue(&ctx->usb, req, &complete); |
| return ZX_OK; |
| } |
| |
| static zx_status_t send_locked(ecm_ctx_t* ctx, ethmac_netbuf_t* netbuf) { |
| const uint8_t* byte_data = netbuf->data_buffer; |
| size_t length = netbuf->data_size; |
| |
| // Make sure that we can get all of the tx buffers we need to use |
| usb_request_t* tx_req = usb_req_list_remove_head(&ctx->tx_txn_bufs, ctx->parent_req_size); |
| if (tx_req == NULL) { |
| return ZX_ERR_SHOULD_WAIT; |
| } |
| |
| zx_nanosleep(zx_deadline_after(ZX_USEC(ctx->tx_endpoint_delay))); |
| zx_status_t status; |
| if ((status = queue_request(ctx, byte_data, length, tx_req)) != ZX_OK) { |
| zx_status_t add_status = usb_req_list_add_tail(&ctx->tx_txn_bufs, tx_req, |
| ctx->parent_req_size); |
| ZX_DEBUG_ASSERT(add_status == ZX_OK); |
| return status; |
| } |
| |
| return ZX_OK; |
| } |
| |
| // Write completion callback. Normally -- this will simply acquire the TX lock, release it, |
| // and re-queue the USB request. |
| // The error case is a bit more complicated. We set the reset bit on the request, and queue |
| // a packet that triggers a reset (asynchronously). We then immediately return to the interrupt |
| // thread with the lock held to allow for interrupt processing to take place. Once the reset |
| // completes, this function is called again with the lock still held, and request processing |
| // continues normally. It is necessary to keep the lock held after returning in the error case |
| // because we do not want other packets to get queued out-of-order while the asynchronous operation |
| // is in progress. |
| static void usb_write_complete(void* cookie, |
| usb_request_t* request) __TA_NO_THREAD_SAFETY_ANALYSIS { |
| ecm_ctx_t* ctx = cookie; |
| |
| if (request->response.status == ZX_ERR_IO_NOT_PRESENT) { |
| usb_request_release(request); |
| return; |
| } |
| // If reset, we still hold the TX mutex. |
| if (!request->reset) { |
| mtx_lock(&ctx->tx_mutex); |
| // Return transmission buffer to pool |
| zx_status_t status = |
| usb_req_list_add_tail(&ctx->tx_txn_bufs, request, ctx->parent_req_size); |
| ZX_DEBUG_ASSERT(status == ZX_OK); |
| if (request->response.status == ZX_ERR_IO_REFUSED) { |
| zxlogf(TRACE, "%s: resetting transmit endpoint\n", module_name); |
| request->reset = true; |
| request->reset_address = ctx->tx_endpoint.addr; |
| usb_request_complete_t complete = { |
| .callback = usb_write_complete, |
| .ctx = ctx, |
| }; |
| usb_request_queue(&ctx->usb, request, &complete); |
| return; |
| } |
| |
| if (request->response.status == ZX_ERR_IO_INVALID) { |
| zxlogf(TRACE, |
| "%s: slowing down the requests by %d usec." |
| "Resetting the transmit endpoint\n", |
| module_name, ETHMAC_TRANSMIT_DELAY); |
| if (ctx->tx_endpoint_delay < ETHMAC_MAX_TRANSMIT_DELAY) { |
| ctx->tx_endpoint_delay += ETHMAC_TRANSMIT_DELAY; |
| } |
| request->reset = true; |
| request->reset_address = ctx->tx_endpoint.addr; |
| usb_request_complete_t complete = { |
| .callback = usb_write_complete, |
| .ctx = ctx, |
| }; |
| usb_request_queue(&ctx->usb, request, &complete); |
| return; |
| } |
| } |
| request->reset = false; |
| |
| bool additional_tx_queued = false; |
| txn_info_t* txn; |
| zx_status_t send_status = ZX_OK; |
| if (!list_is_empty(&ctx->tx_pending_infos)) { |
| txn = list_peek_head_type(&ctx->tx_pending_infos, txn_info_t, node); |
| if ((send_status = send_locked(ctx, &txn->netbuf)) != ZX_ERR_SHOULD_WAIT) { |
| list_remove_head(&ctx->tx_pending_infos); |
| additional_tx_queued = true; |
| } |
| } |
| |
| mtx_unlock(&ctx->tx_mutex); |
| |
| mtx_lock(&ctx->ethmac_mutex); |
| if (additional_tx_queued && ctx->ethmac_ifc.ops) { |
| ethmac_ifc_complete_tx(&ctx->ethmac_ifc, &txn->netbuf, send_status); |
| } |
| mtx_unlock(&ctx->ethmac_mutex); |
| |
| // When the interface is offline, the transaction will complete with status set to |
| // ZX_ERR_IO_NOT_PRESENT. There's not much we can do except ignore it. |
| } |
| |
| // Note: the assumption made here is that no rx transmissions will be processed in parallel, |
| // so we do not maintain an rx mutex. |
| static void usb_recv(ecm_ctx_t* ctx, usb_request_t* request) { |
| size_t len = request->response.actual; |
| |
| uint8_t* read_data; |
| zx_status_t status = usb_request_mmap(request, (void*)&read_data); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s: usb_request_mmap failed with status %d\n", |
| module_name, status); |
| return; |
| } |
| |
| mtx_lock(&ctx->ethmac_mutex); |
| if (ctx->ethmac_ifc.ops) { |
| ethmac_ifc_recv(&ctx->ethmac_ifc, read_data, len, 0); |
| } |
| mtx_unlock(&ctx->ethmac_mutex); |
| } |
| |
| static void usb_read_complete(void* cookie, usb_request_t* request) __TA_NO_THREAD_SAFETY_ANALYSIS { |
| ecm_ctx_t* ctx = cookie; |
| if (request->response.status != ZX_OK) { |
| zxlogf(TRACE, "%s: usb_read_complete called with status %d\n", module_name, |
| (int)request->response.status); |
| } |
| |
| if (request->response.status == ZX_ERR_IO_NOT_PRESENT) { |
| usb_request_release(request); |
| return; |
| } |
| |
| if (request->response.status == ZX_ERR_IO_REFUSED) { |
| zxlogf(TRACE, "%s: resetting receive endpoint\n", module_name); |
| request->reset = true; |
| request->reset_address = ctx->rx_endpoint.addr; |
| usb_request_complete_t complete = { |
| .callback = usb_read_complete, |
| .ctx = ctx, |
| }; |
| usb_request_queue(&ctx->usb, request, &complete); |
| return; |
| } else if (request->response.status == ZX_ERR_IO_INVALID) { |
| if (ctx->rx_endpoint_delay < ETHMAC_MAX_RECV_DELAY) { |
| ctx->rx_endpoint_delay += ETHMAC_RECV_DELAY; |
| } |
| zxlogf(TRACE, |
| "%s: slowing down the requests by %d usec." |
| "Resetting the recv endpoint\n", |
| module_name, ETHMAC_RECV_DELAY); |
| request->reset = true; |
| request->reset_address = ctx->rx_endpoint.addr; |
| usb_request_complete_t complete = { |
| .callback = usb_read_complete, |
| .ctx = ctx, |
| }; |
| usb_request_queue(&ctx->usb, request, &complete); |
| return; |
| } else if (request->response.status == ZX_OK && !request->reset) { |
| usb_recv(ctx, request); |
| } |
| if (ctx->rx_endpoint_delay) { |
| zx_nanosleep(zx_deadline_after(ZX_USEC(ctx->rx_endpoint_delay))); |
| } |
| request->reset = false; |
| usb_request_complete_t complete = { |
| .callback = usb_read_complete, |
| .ctx = ctx, |
| }; |
| usb_request_queue(&ctx->usb, request, &complete); |
| } |
| |
| static zx_status_t ecm_ethmac_queue_tx(void* cookie, uint32_t options, ethmac_netbuf_t* netbuf) { |
| ecm_ctx_t* ctx = cookie; |
| size_t length = netbuf->data_size; |
| zx_status_t status; |
| |
| if (length > ctx->mtu || length == 0) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| zxlogf(SPEW, "%s: sending %zu bytes to endpoint 0x%"PRIx8"\n", |
| module_name, length, ctx->tx_endpoint.addr); |
| |
| mtx_lock(&ctx->tx_mutex); |
| if (ctx->unbound) { |
| status = ZX_ERR_IO_NOT_PRESENT; |
| } else { |
| status = send_locked(ctx, netbuf); |
| if (status == ZX_ERR_SHOULD_WAIT) { |
| // No buffers available, queue it up |
| txn_info_t* txn = containerof(netbuf, txn_info_t, netbuf); |
| list_add_tail(&ctx->tx_pending_infos, &txn->node); |
| } |
| } |
| |
| mtx_unlock(&ctx->tx_mutex); |
| return status; |
| } |
| |
| static zx_status_t ecm_ethmac_set_param(void *cookie, uint32_t param, int32_t value, |
| const void* data, size_t data_size) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| static ethmac_protocol_ops_t ethmac_ops = { |
| .query = ecm_ethmac_query, |
| .stop = ecm_ethmac_stop, |
| .start = ecm_ethmac_start, |
| .queue_tx = ecm_ethmac_queue_tx, |
| .set_param = ecm_ethmac_set_param, |
| }; |
| |
| static void ecm_interrupt_complete(void* cookie, usb_request_t* request) { |
| ecm_ctx_t* ctx = cookie; |
| sync_completion_signal(&ctx->completion); |
| } |
| |
| static void ecm_handle_interrupt(ecm_ctx_t* ctx, usb_request_t* request) { |
| if (request->response.actual < sizeof(usb_cdc_notification_t)) { |
| zxlogf(ERROR, "%s: ignored interrupt (size = %ld)\n", module_name, (long)request->response.actual); |
| return; |
| } |
| |
| usb_cdc_notification_t usb_req; |
| usb_request_copy_from(request, &usb_req, sizeof(usb_cdc_notification_t), 0); |
| if (usb_req.bmRequestType == (USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE) && |
| usb_req.bNotification == USB_CDC_NC_NETWORK_CONNECTION) { |
| ecm_update_online_status(ctx, usb_req.wValue != 0); |
| } else if (usb_req.bmRequestType == (USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE) && |
| usb_req.bNotification == USB_CDC_NC_CONNECTION_SPEED_CHANGE) { |
| // The ethermac driver doesn't care about speed changes, so even though we track this |
| // information, it's currently unused. |
| if (usb_req.wLength != 8) { |
| zxlogf(ERROR, "%s: invalid size (%"PRIu16") for CONNECTION_SPEED_CHANGE notification\n", |
| module_name, usb_req.wLength); |
| return; |
| } |
| // Data immediately follows notification in packet |
| uint32_t new_us_bps, new_ds_bps; |
| usb_request_copy_from(request, &new_us_bps, 4, sizeof(usb_cdc_notification_t)); |
| usb_request_copy_from(request, &new_ds_bps, 4, sizeof(usb_cdc_notification_t) + 4); |
| if (new_us_bps != ctx->us_bps) { |
| zxlogf(ERROR, "%s: connection speed change... upstream bits/s: %"PRIu32"\n", |
| module_name, new_us_bps); |
| ctx->us_bps = new_us_bps; |
| } |
| if (new_ds_bps != ctx->ds_bps) { |
| zxlogf(ERROR, "%s: connection speed change... downstream bits/s: %"PRIu32"\n", |
| module_name, new_ds_bps); |
| ctx->ds_bps = new_ds_bps; |
| } |
| } else { |
| zxlogf(ERROR, "%s: ignored interrupt (type = %"PRIu8", request = %"PRIu8")\n", |
| module_name, usb_req.bmRequestType, usb_req.bNotification); |
| return; |
| } |
| } |
| |
| static int ecm_int_handler_thread(void* cookie) { |
| ecm_ctx_t* ctx = cookie; |
| usb_request_t* txn = ctx->int_txn_buf; |
| |
| usb_request_complete_t complete = { |
| .callback = ecm_interrupt_complete, |
| .ctx = ctx, |
| }; |
| while (true) { |
| sync_completion_reset(&ctx->completion); |
| usb_request_queue(&ctx->usb, txn, &complete); |
| sync_completion_wait(&ctx->completion, ZX_TIME_INFINITE); |
| if (txn->response.status == ZX_OK) { |
| ecm_handle_interrupt(ctx, txn); |
| } else if (txn->response.status == ZX_ERR_PEER_CLOSED || |
| txn->response.status == ZX_ERR_IO_NOT_PRESENT) { |
| zxlogf(TRACE, "%s: terminating interrupt handling thread\n", module_name); |
| return txn->response.status; |
| } else if (txn->response.status == ZX_ERR_IO_REFUSED || |
| txn->response.status == ZX_ERR_IO_INVALID) { |
| zxlogf(TRACE, "%s: resetting interrupt endpoint\n", module_name); |
| usb_reset_endpoint(&ctx->usb, ctx->int_endpoint.addr); |
| } else { |
| zxlogf(ERROR, "%s: error (%ld) waiting for interrupt - ignoring\n", |
| module_name, (long)txn->response.status); |
| } |
| } |
| } |
| |
| static bool parse_cdc_header(usb_cs_header_interface_descriptor_t* header_desc) { |
| // Check for supported CDC version |
| zxlogf(TRACE, "%s: device reports CDC version as 0x%x\n", module_name, header_desc->bcdCDC); |
| return header_desc->bcdCDC >= CDC_SUPPORTED_VERSION; |
| } |
| |
| static bool parse_cdc_ethernet_descriptor(ecm_ctx_t* ctx, |
| usb_cs_ethernet_interface_descriptor_t* desc) { |
| ctx->mtu = desc->wMaxSegmentSize; |
| |
| // MAC address is stored in a string descriptor in UTF-16 format, so we get one byte of |
| // address for each 32 bits of text. |
| const size_t expected_str_size = sizeof(usb_string_descriptor_t) + ETH_MAC_SIZE * 4; |
| char str_desc_buf[expected_str_size]; |
| |
| // Read string descriptor for MAC address (string index is in iMACAddress field) |
| size_t out_length; |
| zx_status_t result = usb_get_descriptor(&ctx->usb, 0, USB_DT_STRING, desc->iMACAddress, |
| str_desc_buf, sizeof(str_desc_buf), ZX_TIME_INFINITE, |
| &out_length); |
| if (result < 0) { |
| zxlogf(ERROR, "%s: error reading MAC address\n", module_name); |
| return false; |
| } |
| if (out_length != expected_str_size) { |
| zxlogf(ERROR, "%s: MAC address string incorrect length (saw %zd, expected %zd)\n", |
| module_name, out_length, expected_str_size); |
| return false; |
| } |
| |
| // Convert MAC address to something more machine-friendly |
| usb_string_descriptor_t* str_desc = (usb_string_descriptor_t*)str_desc_buf; |
| uint8_t* str = str_desc->bString; |
| size_t ndx; |
| for (ndx = 0; ndx < ETH_MAC_SIZE * 4; ndx++) { |
| if (ndx % 2 == 1) { |
| if (str[ndx] != 0) { |
| zxlogf(ERROR, "%s: MAC address contains invalid characters\n", module_name); |
| return false; |
| } |
| continue; |
| } |
| uint8_t value; |
| if (str[ndx] >= '0' && str[ndx] <= '9') { |
| value = str[ndx] - '0'; |
| } else if (str[ndx] >= 'A' && str[ndx] <= 'F') { |
| value = (str[ndx] - 'A') + 0xa; |
| } else { |
| zxlogf(ERROR, "%s: MAC address contains invalid characters\n", module_name); |
| return false; |
| } |
| if (ndx % 4 == 0) { |
| ctx->mac_addr[ndx/4] = value << 4; |
| } else { |
| ctx->mac_addr[ndx/4] |= value; |
| } |
| } |
| |
| zxlogf(ERROR, "%s: MAC address is %02X:%02X:%02X:%02X:%02X:%02X\n", module_name, |
| ctx->mac_addr[0], ctx->mac_addr[1], ctx->mac_addr[2], |
| ctx->mac_addr[3], ctx->mac_addr[4], ctx->mac_addr[5]); |
| return true; |
| } |
| |
| static void copy_endpoint_info(ecm_endpoint_t* ep_info, usb_endpoint_descriptor_t* desc) { |
| ep_info->addr = desc->bEndpointAddress; |
| ep_info->max_packet_size = desc->wMaxPacketSize; |
| } |
| |
| static bool want_interface(usb_interface_descriptor_t* intf, void* arg) { |
| return intf->bInterfaceClass == USB_CLASS_CDC; |
| } |
| |
| static zx_status_t ecm_bind(void* ctx, zx_device_t* device) { |
| zxlogf(TRACE, "%s: starting %s\n", module_name, __FUNCTION__); |
| |
| usb_protocol_t usb; |
| zx_status_t result = device_get_protocol(device, ZX_PROTOCOL_USB, &usb); |
| if (result != ZX_OK) { |
| return result; |
| } |
| usb_composite_protocol_t usb_composite; |
| result = device_get_protocol(device, ZX_PROTOCOL_USB_COMPOSITE, &usb_composite); |
| if (result != ZX_OK) { |
| return result; |
| } |
| |
| // Allocate context |
| ecm_ctx_t* ecm_ctx = calloc(1, sizeof(ecm_ctx_t)); |
| if (!ecm_ctx) { |
| zxlogf(ERROR, "%s: failed to allocate memory for USB CDC ECM driver\n", module_name); |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| result = usb_claim_additional_interfaces(&usb_composite, want_interface, NULL); |
| if (result != ZX_OK) { |
| goto fail; |
| } |
| // Initialize context |
| ecm_ctx->usb_device = device; |
| memcpy(&ecm_ctx->usb, &usb, sizeof(ecm_ctx->usb)); |
| list_initialize(&ecm_ctx->tx_txn_bufs); |
| list_initialize(&ecm_ctx->tx_pending_infos); |
| mtx_init(&ecm_ctx->ethmac_mutex, mtx_plain); |
| mtx_init(&ecm_ctx->tx_mutex, mtx_plain); |
| |
| ecm_ctx->parent_req_size = usb_get_request_size(&ecm_ctx->usb); |
| |
| usb_desc_iter_t iter; |
| result = usb_desc_iter_init(&usb, &iter); |
| if (result != ZX_OK) { |
| goto fail; |
| } |
| result = ZX_ERR_NOT_SUPPORTED; |
| |
| // Find the CDC descriptors and endpoints |
| usb_descriptor_header_t* desc = usb_desc_iter_next(&iter); |
| usb_cs_header_interface_descriptor_t* cdc_header_desc = NULL; |
| usb_cs_ethernet_interface_descriptor_t* cdc_eth_desc = NULL; |
| usb_endpoint_descriptor_t* int_ep = NULL; |
| usb_endpoint_descriptor_t* tx_ep = NULL; |
| usb_endpoint_descriptor_t* rx_ep = NULL; |
| usb_interface_descriptor_t* default_ifc = NULL; |
| usb_interface_descriptor_t* data_ifc = NULL; |
| while (desc) { |
| if (desc->bDescriptorType == USB_DT_INTERFACE) { |
| usb_interface_descriptor_t* ifc_desc = (void*)desc; |
| if (ifc_desc->bInterfaceClass == USB_CLASS_CDC) { |
| if (ifc_desc->bNumEndpoints == 0) { |
| if (default_ifc) { |
| zxlogf(ERROR, "%s: multiple default interfaces found\n", module_name); |
| goto fail; |
| } |
| default_ifc = ifc_desc; |
| } else if (ifc_desc->bNumEndpoints == 2) { |
| if (data_ifc) { |
| zxlogf(ERROR, "%s: multiple data interfaces found\n", module_name); |
| goto fail; |
| } |
| data_ifc = ifc_desc; |
| } |
| } |
| } else if (desc->bDescriptorType == USB_DT_CS_INTERFACE) { |
| usb_cs_interface_descriptor_t* cs_ifc_desc = (void*)desc; |
| if (cs_ifc_desc->bDescriptorSubType == USB_CDC_DST_HEADER) { |
| if (cdc_header_desc != NULL) { |
| zxlogf(ERROR, "%s: multiple CDC headers\n", module_name); |
| goto fail; |
| } |
| cdc_header_desc = (void*)cs_ifc_desc; |
| } else if (cs_ifc_desc->bDescriptorSubType == USB_CDC_DST_ETHERNET) { |
| if (cdc_eth_desc != NULL) { |
| zxlogf(ERROR, "%s: multiple CDC ethernet descriptors\n", module_name); |
| goto fail; |
| } |
| cdc_eth_desc = (void*)cs_ifc_desc; |
| } |
| } else if (desc->bDescriptorType == USB_DT_ENDPOINT) { |
| usb_endpoint_descriptor_t* endpoint_desc = (void*)desc; |
| if (usb_ep_direction(endpoint_desc) == USB_ENDPOINT_IN && |
| usb_ep_type(endpoint_desc) == USB_ENDPOINT_INTERRUPT) { |
| if (int_ep != NULL) { |
| zxlogf(ERROR, "%s: multiple interrupt endpoint descriptors\n", module_name); |
| goto fail; |
| } |
| int_ep = endpoint_desc; |
| } else if (usb_ep_direction(endpoint_desc) == USB_ENDPOINT_OUT && |
| usb_ep_type(endpoint_desc) == USB_ENDPOINT_BULK) { |
| if (tx_ep != NULL) { |
| zxlogf(ERROR, "%s: multiple tx endpoint descriptors\n", module_name); |
| goto fail; |
| } |
| tx_ep = endpoint_desc; |
| } else if (usb_ep_direction(endpoint_desc) == USB_ENDPOINT_IN && |
| usb_ep_type(endpoint_desc) == USB_ENDPOINT_BULK) { |
| if (rx_ep != NULL) { |
| zxlogf(ERROR, "%s: multiple rx endpoint descriptors\n", module_name); |
| goto fail; |
| } |
| rx_ep = endpoint_desc; |
| } else { |
| zxlogf(ERROR, "%s: unrecognized endpoint\n", module_name); |
| goto fail; |
| } |
| } |
| desc = usb_desc_iter_next(&iter); |
| } |
| if (cdc_header_desc == NULL || cdc_eth_desc == NULL) { |
| zxlogf(ERROR, "%s: CDC %s descriptor(s) not found", module_name, |
| cdc_header_desc ? "ethernet" : cdc_eth_desc ? "header" : "ethernet and header"); |
| goto fail; |
| } |
| if (int_ep == NULL || tx_ep == NULL || rx_ep == NULL) { |
| zxlogf(ERROR, "%s: missing one or more required endpoints\n", module_name); |
| goto fail; |
| } |
| if (default_ifc == NULL) { |
| zxlogf(ERROR, "%s: unable to find CDC default interface\n", module_name); |
| goto fail; |
| } |
| if (data_ifc == NULL) { |
| zxlogf(ERROR, "%s: unable to find CDC data interface\n", module_name); |
| goto fail; |
| } |
| |
| // Parse the information in the CDC descriptors |
| if (!parse_cdc_header(cdc_header_desc)) { |
| goto fail; |
| } |
| if (!parse_cdc_ethernet_descriptor(ecm_ctx, cdc_eth_desc)) { |
| goto fail; |
| } |
| |
| // Parse endpoint information |
| copy_endpoint_info(&ecm_ctx->int_endpoint, int_ep); |
| copy_endpoint_info(&ecm_ctx->tx_endpoint, tx_ep); |
| copy_endpoint_info(&ecm_ctx->rx_endpoint, rx_ep); |
| |
| ecm_ctx->rx_endpoint_delay = ETHMAC_INITIAL_RECV_DELAY; |
| ecm_ctx->tx_endpoint_delay = ETHMAC_INITIAL_TRANSMIT_DELAY; |
| // Reset by selecting default interface followed by data interface. We can't start |
| // queueing transactions until this is complete. |
| usb_set_interface(&usb, default_ifc->bInterfaceNumber, default_ifc->bAlternateSetting); |
| usb_set_interface(&usb, data_ifc->bInterfaceNumber, data_ifc->bAlternateSetting); |
| |
| // Allocate interrupt transaction buffer |
| usb_request_t* int_buf; |
| uint64_t req_size = ecm_ctx->parent_req_size + sizeof(usb_req_internal_t); |
| zx_status_t alloc_result = usb_request_alloc(&int_buf, |
| ecm_ctx->int_endpoint.max_packet_size, |
| ecm_ctx->int_endpoint.addr, req_size); |
| if (alloc_result != ZX_OK) { |
| result = alloc_result; |
| goto fail; |
| } |
| |
| ecm_ctx->int_txn_buf = int_buf; |
| |
| // Allocate tx transaction buffers |
| uint16_t tx_buf_sz = ecm_ctx->mtu; |
| #if MAX_TX_BUF_SZ < UINT16_MAX |
| if (tx_buf_sz > MAX_TX_BUF_SZ) { |
| zxlogf(ERROR, "%s: insufficient space for even a single tx buffer\n", module_name); |
| goto fail; |
| } |
| #endif |
| size_t tx_buf_remain = MAX_TX_BUF_SZ; |
| while (tx_buf_remain >= tx_buf_sz) { |
| usb_request_t* tx_buf; |
| zx_status_t alloc_result = |
| usb_request_alloc(&tx_buf, tx_buf_sz, ecm_ctx->tx_endpoint.addr, req_size); |
| tx_buf->direct = true; |
| if (alloc_result != ZX_OK) { |
| result = alloc_result; |
| goto fail; |
| } |
| |
| // As per the CDC-ECM spec, we need to send a zero-length packet to signify the end of |
| // transmission when the endpoint max packet size is a factor of the total transmission size |
| tx_buf->header.send_zlp = true; |
| |
| zx_status_t add_result = usb_req_list_add_head(&ecm_ctx->tx_txn_bufs, tx_buf, |
| ecm_ctx->parent_req_size); |
| ZX_DEBUG_ASSERT(add_result == ZX_OK); |
| |
| tx_buf_remain -= tx_buf_sz; |
| } |
| |
| // Allocate rx transaction buffers |
| uint32_t rx_buf_sz = ecm_ctx->mtu; |
| #if MAX_TX_BUF_SZ < UINT16_MAX |
| if (rx_buf_sz > MAX_RX_BUF_SZ) { |
| zxlogf(ERROR, "%s: insufficient space for even a single rx buffer\n", module_name); |
| goto fail; |
| } |
| #endif |
| |
| usb_request_complete_t complete = { |
| .callback = usb_read_complete, |
| .ctx = ecm_ctx, |
| }; |
| size_t rx_buf_remain = MAX_RX_BUF_SZ; |
| while (rx_buf_remain >= rx_buf_sz) { |
| usb_request_t* rx_buf; |
| zx_status_t alloc_result = usb_request_alloc(&rx_buf, rx_buf_sz, |
| ecm_ctx->rx_endpoint.addr, |
| req_size); |
| if (alloc_result != ZX_OK) { |
| result = alloc_result; |
| goto fail; |
| } |
| rx_buf->direct = true; |
| usb_request_queue(&ecm_ctx->usb, rx_buf, &complete); |
| rx_buf_remain -= rx_buf_sz; |
| } |
| |
| // Kick off the handler thread |
| int thread_result = thrd_create_with_name(&ecm_ctx->int_thread, ecm_int_handler_thread, |
| ecm_ctx, "ecm_int_handler_thread"); |
| if (thread_result != thrd_success) { |
| zxlogf(ERROR, "%s: failed to create interrupt handler thread (%d)\n", module_name, thread_result); |
| goto fail; |
| } |
| |
| // Add the device |
| device_add_args_t args = { |
| .version = DEVICE_ADD_ARGS_VERSION, |
| .name = "usb-cdc-ecm", |
| .ctx = ecm_ctx, |
| .ops = &ecm_device_proto, |
| .proto_id = ZX_PROTOCOL_ETHMAC, |
| .proto_ops = ðmac_ops, |
| }; |
| result = device_add(ecm_ctx->usb_device, &args, &ecm_ctx->zxdev); |
| if (result < 0) { |
| zxlogf(ERROR, "%s: failed to add device: %d\n", module_name, (int)result); |
| goto fail; |
| } |
| |
| usb_desc_iter_release(&iter); |
| return ZX_OK; |
| |
| fail: |
| usb_desc_iter_release(&iter); |
| ecm_free(ecm_ctx); |
| zxlogf(ERROR, "%s: failed to bind\n", module_name); |
| return result; |
| } |
| |
| static zx_driver_ops_t ecm_driver_ops = { |
| .version = DRIVER_OPS_VERSION, |
| .bind = ecm_bind, |
| }; |
| |
| ZIRCON_DRIVER_BEGIN(ethernet_usb_cdc_ecm, ecm_driver_ops, "zircon", "0.1", 4) |
| BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_USB), |
| BI_ABORT_IF(NE, BIND_USB_CLASS, USB_CLASS_COMM), |
| BI_ABORT_IF(NE, BIND_USB_SUBCLASS, USB_CDC_SUBCLASS_ETHERNET), |
| BI_MATCH_IF(EQ, BIND_USB_PROTOCOL, 0), |
| ZIRCON_DRIVER_END(ethernet_usb_cdc_ecm) |