| // 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 "qmi-usb-transport.h" |
| #include <ddk/binding.h> |
| #include <ddk/device.h> |
| #include <ddk/driver.h> |
| #include <ddk/protocol/ethernet.h> |
| #include <ddk/protocol/usb.h> |
| #include <ddk/usb/usb.h> |
| #include <lib/sync/completion.h> |
| #include <zircon/device/qmi-transport.h> |
| #include <zircon/hw/usb-cdc.h> |
| #include <zircon/status.h> |
| #include <zircon/syscalls/port.h> |
| #include <zircon/types.h> |
| |
| #include <assert.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #define _ALL_SOURCE |
| #include <threads.h> |
| #include <usb/usb-request.h> |
| |
| // The maximum amount of memory we are willing to allocate to transaction |
| // buffers |
| #define MAX_TX_BUF_SZ 32768 |
| #define MAX_RX_BUF_SZ 32768 |
| |
| #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 |
| |
| #define ETHERNET_FRAME_OFFSET 14 |
| |
| typedef struct txn_info { |
| ethmac_netbuf_t netbuf; |
| list_node_t node; |
| } txn_info_t; |
| |
| // qmi usb transport device |
| typedef struct qmi_ctx { |
| // Interrupt handling |
| usb_request_t* int_txn_buf; |
| sync_completion_t completion; |
| thrd_t int_thread; |
| |
| uint16_t max_packet_size; |
| |
| // Port to watch for QMI messages on |
| zx_handle_t channel_port; |
| zx_handle_t channel; |
| |
| usb_protocol_t usb; |
| zx_device_t* usb_device; |
| zx_device_t* zxdev; |
| size_t parent_req_size; |
| |
| // Ethernet |
| zx_device_t* eth_zxdev; |
| |
| mtx_t ethmac_mutex; |
| ethmac_ifc_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; |
| |
| // Send context |
| mtx_t tx_mutex; |
| uint8_t tx_endpoint_addr; |
| uint16_t endpoint_size; |
| 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 |
| |
| // Receive context |
| uint8_t rx_endpoint_addr; |
| uint64_t rx_endpoint_delay; // wait time between 2 recv requests |
| } qmi_ctx_t; |
| |
| static zx_status_t get_channel(void* ctx, zx_handle_t* out_channel) { |
| ZX_DEBUG_ASSERT(ctx); |
| zxlogf(INFO, "qmi-usb-transport: getting channel from transport\n"); |
| qmi_ctx_t* qmi_ctx = ctx; |
| zx_status_t result = ZX_OK; |
| |
| zx_handle_t* in_channel = &qmi_ctx->channel; |
| |
| if (*in_channel != ZX_HANDLE_INVALID) { |
| zxlogf(ERROR, "qmi-usb-transport: already bound, failing\n"); |
| result = ZX_ERR_ALREADY_BOUND; |
| goto done; |
| } |
| |
| result = zx_channel_create(0, in_channel, out_channel); |
| if (result < 0) { |
| zxlogf(ERROR, "qmi-usb-transport: Failed to create channel: %s\n", |
| zx_status_get_string(result)); |
| goto done; |
| } |
| |
| done: |
| return result; |
| } |
| |
| static zx_status_t queue_request(qmi_ctx_t* ctx, const uint8_t* data, size_t length, |
| usb_request_t* req) { |
| req->header.length = length; |
| if (length < 41) { |
| zxlogf(ERROR, "qmi-usb-transport: length is too short (length: %zx) \n", length); |
| return ZX_ERR_IO; |
| } |
| |
| if (length > 1024) { |
| zxlogf(ERROR, |
| "qmi-usb-transport: length is greater than buffer size. (length: %zx) \n", length); |
| return ZX_ERR_IO; |
| } |
| |
| // Check if this in an arp frame, short-circuit return a synthetic one if so |
| const uint8_t* eth = &data[ETHERNET_FRAME_OFFSET]; |
| if ((eth[0] == 0x00) && // hardware type |
| (eth[1] == 0x01) && (eth[2] == 0x08) && // protocol |
| (eth[3] == 0x00) && (eth[4] == 0x06) && // hardware len |
| (eth[5] == 0x04) && // protocol len |
| (eth[6] == 0x00) && // arp request op |
| (eth[7] == 0x01)) { |
| uint8_t read_data[] = { |
| // ethernet frame |
| 0x62, 0x77, 0x62, 0x62, 0x77, 0x62, // destination mac addr (bwb) |
| 0x79, 0x61, 0x6B, 0x79, 0x61, 0x6B, // source mac addr (yak) |
| 0x08, 0x06, // arp ethertype |
| // data payload |
| 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x02, // ARP header |
| 0x79, 0x61, 0x6B, 0x79, 0x61, 0x6B, // yak mac addr |
| eth[24], eth[25], eth[26], eth[27], // swapped IP addr |
| 0x62, 0x77, 0x62, 0x62, 0x77, 0x62, // bwb mac addr |
| eth[14], eth[15], eth[16], eth[17], // swapped IP addr |
| }; |
| |
| zx_nanosleep(zx_deadline_after(ZX_USEC(ctx->tx_endpoint_delay))); |
| mtx_lock(&ctx->ethmac_mutex); |
| if (ctx->ethmac_ifc.ops) { |
| ethmac_ifc_recv(&ctx->ethmac_ifc, read_data, sizeof(read_data), 0); |
| } |
| mtx_unlock(&ctx->ethmac_mutex); |
| return ZX_OK; |
| } |
| |
| ssize_t ip_length = ((eth[2] << 8) + eth[3]); |
| if (ip_length > (ssize_t)length) { |
| zxlogf(ERROR, |
| "qmi-usb-transport: length of IP packet is more than the ethernet frame! %zx/%zd \n", |
| ip_length, length); |
| return ZX_ERR_IO; |
| } |
| |
| ssize_t bytes_copied = usb_request_copy_to(req, eth, ip_length, 0); |
| if (bytes_copied < 0) { |
| zxlogf(ERROR, |
| "qmi-usb-transport: failed to copy data into send txn (error %zd)\n", |
| bytes_copied); |
| return ZX_ERR_IO; |
| } |
| |
| usb_request_queue(&ctx->usb, req); |
| return ZX_OK; |
| } |
| |
| static zx_status_t send_locked(qmi_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 = |
| list_remove_head_type(&ctx->tx_txn_bufs, usb_request_t, node); |
| 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) { |
| list_add_tail(&ctx->tx_txn_bufs, &tx_req->node); |
| return status; |
| } |
| |
| return ZX_OK; |
| } |
| |
| static void qmi_update_online_status(qmi_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, "qmi-usb-transport: connected to network\n"); |
| ctx->online = true; |
| if (ctx->ethmac_ifc.ops) { |
| ethmac_ifc_status(&ctx->ethmac_ifc, ctx->online ? ETHMAC_STATUS_ONLINE : 0); |
| } else { |
| zxlogf(ERROR, "qmi-usb-transport: not connected to ethermac interface\n"); |
| } |
| } else { |
| zxlogf(INFO, "qmi-usb-transport: no connection to network\n"); |
| ctx->online = false; |
| if (ctx->ethmac_ifc.ops) { |
| ethmac_ifc_status(&ctx->ethmac_ifc, 0); |
| } |
| } |
| |
| done: |
| mtx_unlock(&ctx->ethmac_mutex); |
| } |
| |
| static inline zx_status_t set_async_wait(qmi_ctx_t* ctx) { |
| zx_status_t status = zx_object_wait_async( |
| ctx->channel, ctx->channel_port, CHANNEL_MSG, |
| ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED, ZX_WAIT_ASYNC_ONCE); |
| return status; |
| } |
| |
| static zx_status_t qmi_ioctl(void* ctx, uint32_t op, const void* in_buf, |
| size_t in_len, void* out_buf, size_t out_len, |
| size_t* out_actual) { |
| qmi_ctx_t* qmi_ctx = ctx; |
| zx_status_t status = ZX_OK; |
| if ((op != IOCTL_QMI_GET_CHANNEL) && (op != IOCTL_QMI_SET_NETWORK)) { |
| status = ZX_ERR_NOT_SUPPORTED; |
| goto done; |
| } |
| |
| if (op == IOCTL_QMI_SET_NETWORK) { |
| bool state = (bool)in_buf; |
| qmi_update_online_status(qmi_ctx, state); |
| return status; |
| } |
| |
| if (out_buf == NULL || out_len != sizeof(zx_handle_t)) { |
| status = ZX_ERR_INVALID_ARGS; |
| goto done; |
| } |
| |
| zx_handle_t* out_channel = (zx_handle_t*)out_buf; |
| status = get_channel(ctx, out_channel); |
| if (status != ZX_OK) { |
| goto done; |
| } |
| *out_actual = sizeof(zx_handle_t); |
| |
| status = set_async_wait(qmi_ctx); |
| if (status != ZX_OK) { |
| zx_handle_close(*out_channel); |
| zx_handle_close(qmi_ctx->channel); |
| } |
| |
| done: |
| return status; |
| } |
| |
| static void qmi_release(void* ctx) { |
| zxlogf(INFO, "qmi-usb-transport: releasing device\n"); |
| qmi_ctx_t* qmi_ctx = ctx; |
| free(qmi_ctx); |
| } |
| |
| static void qmi_unbind(void* ctx) { |
| qmi_ctx_t* qmi_ctx = ctx; |
| zx_status_t result = ZX_OK; |
| result = device_remove(qmi_ctx->zxdev); |
| if (result != ZX_OK) { |
| zxlogf( |
| ERROR, |
| "Failed to unbind qmi-usb-transport driver. Cannot remove device: %u\n", |
| result); |
| } |
| } |
| |
| static zx_status_t qmi_ethmac_query(void* ctx, uint32_t options, ethmac_info_t* info) { |
| qmi_ctx_t* eth = ctx; |
| |
| zxlogf(INFO, "qmi-usb-transport: %s called\n", __FUNCTION__); |
| |
| // No options are supported |
| if (options) { |
| zxlogf(ERROR, "qmi-usb-transport: unexpected options to ethmac_query\n"); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| memset(info, 0, sizeof(*info)); |
| info->mtu = 1024; |
| memcpy(info->mac, eth->mac_addr, sizeof(eth->mac_addr)); |
| info->netbuf_size = sizeof(txn_info_t); |
| |
| return ZX_OK; |
| } |
| |
| static zx_status_t qmi_ethmac_start(void* ctx_cookie, const ethmac_ifc_t* ifc) { |
| zxlogf(INFO, "qmi-usb-transport: %s called\n", __FUNCTION__); |
| qmi_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 qmi_ethmac_set_param(void *cookie, uint32_t param, int32_t value, |
| const void* data, size_t data_size) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| static void qmi_ethmac_stop(void* cookie) { |
| zxlogf(INFO, "qmi-usb-transport: %s called\n", __FUNCTION__); |
| qmi_ctx_t* ctx = cookie; |
| mtx_lock(&ctx->ethmac_mutex); |
| ctx->ethmac_ifc.ops = NULL; |
| mtx_unlock(&ctx->ethmac_mutex); |
| } |
| |
| static zx_status_t qmi_ethmac_queue_tx(void* cookie, uint32_t options, |
| ethmac_netbuf_t* netbuf) { |
| qmi_ctx_t* ctx = cookie; |
| size_t length = netbuf->data_size; |
| zx_status_t status; |
| |
| // TODO mtu better |
| if (length > 1024 || length == 0) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| 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 ethmac_protocol_ops_t ethmac_ops = { |
| .query = qmi_ethmac_query, |
| .stop = qmi_ethmac_stop, |
| .start = qmi_ethmac_start, |
| .queue_tx = qmi_ethmac_queue_tx, |
| .set_param = qmi_ethmac_set_param, |
| }; |
| |
| static zx_protocol_device_t qmi_ops = { |
| .version = DEVICE_OPS_VERSION, |
| .ioctl = qmi_ioctl, |
| .release = qmi_release, |
| .unbind = qmi_unbind, |
| }; |
| |
| static zx_protocol_device_t eth_qmi_ops = { |
| .version = DEVICE_OPS_VERSION, |
| }; |
| |
| static void qmi_handle_interrupt(qmi_ctx_t* qmi_ctx, usb_request_t* request) { |
| if (request->response.actual < sizeof(usb_cdc_notification_t)) { |
| zxlogf(ERROR, "qmi-usb-transport: ignored interrupt (size = %ld)\n", |
| (long)request->response.actual); |
| return; |
| } |
| |
| usb_cdc_notification_t usb_req; |
| usb_request_copy_from(request, &usb_req, sizeof(usb_cdc_notification_t), 0); |
| |
| uint16_t packet_size = qmi_ctx->max_packet_size; |
| if (packet_size > 2048) { |
| zxlogf(ERROR, "qmi-usb-transport: packet too big: %d\n", packet_size); |
| return; |
| } |
| uint8_t buffer[packet_size]; |
| zx_status_t status; |
| |
| switch (usb_req.bNotification) { |
| case USB_CDC_NC_RESPONSE_AVAILABLE: |
| status = usb_control( |
| &qmi_ctx->usb, USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE, |
| USB_CDC_GET_ENCAPSULATED_RESPONSE, 0, QMI_INTERFACE_NUM, buffer, |
| packet_size, ZX_TIME_INFINITE, NULL); |
| if (!qmi_ctx->channel) { |
| zxlogf( |
| WARN, |
| "qmi-usb-transport: recieving USB CDC frames without a channel\n"); |
| return; |
| } |
| status = zx_channel_write(qmi_ctx->channel, 0, buffer, sizeof(buffer), |
| NULL, 0); |
| if (status < 0) { |
| zxlogf(ERROR, |
| "qmi-usb-transport: failed to write message to channel: %s\n", |
| zx_status_get_string(status)); |
| } |
| return; |
| default: |
| zxlogf(ERROR, "qmi-usb-transport: Unknown Notification Type for QMI: %d\n", |
| usb_req.bNotification); |
| } |
| } |
| |
| static void qmi_interrupt_cb(usb_request_t* req, void* cookie) { |
| qmi_ctx_t* qmi_ctx = (qmi_ctx_t*)cookie; |
| |
| zx_port_packet_t packet = {}; |
| packet.key = INTERRUPT_MSG; |
| zx_port_queue(qmi_ctx->channel_port, &packet); |
| } |
| |
| static int qmi_transport_thread(void* cookie) { |
| qmi_ctx_t* ctx = cookie; |
| usb_request_t* txn = ctx->int_txn_buf; |
| |
| usb_request_queue(&ctx->usb, txn); |
| if (ctx->max_packet_size > 2048) { |
| zxlogf(ERROR, "qmi-usb-transport: packet too big: %d\n", |
| ctx->max_packet_size); |
| return ZX_ERR_IO_REFUSED; |
| } |
| uint8_t buffer[ctx->max_packet_size]; |
| uint32_t length = sizeof(buffer); |
| zx_port_packet_t packet; |
| while (true) { |
| zx_status_t status = |
| zx_port_wait(ctx->channel_port, ZX_TIME_INFINITE, &packet); |
| if (status == ZX_ERR_TIMED_OUT) { |
| zxlogf(ERROR, "qmi-usb-transport: timed out: %s\n", |
| zx_status_get_string(status)); |
| } else { |
| if (packet.key == CHANNEL_MSG) { |
| status = zx_channel_read(ctx->channel, 0, buffer, NULL, sizeof(buffer), |
| 0, &length, NULL); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "qmi-usb-transport: failed to read channel: %s\n", |
| zx_status_get_string(status)); |
| return status; |
| } |
| status = usb_control(&ctx->usb, |
| USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE, |
| USB_CDC_SEND_ENCAPSULATED_COMMAND, 0, 8, buffer, |
| length, ZX_TIME_INFINITE, NULL); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, |
| "qmi-usb-transport: got an bad status from usb_control: %s\n", |
| zx_status_get_string(status)); |
| return status; |
| } |
| status = set_async_wait(ctx); |
| if (status != ZX_OK) { |
| return status; |
| } |
| } else if (packet.key == INTERRUPT_MSG) { |
| if (txn->response.status == ZX_OK) { |
| qmi_handle_interrupt(ctx, txn); |
| usb_request_queue(&ctx->usb, txn); |
| } else if (txn->response.status == ZX_ERR_PEER_CLOSED || |
| txn->response.status == ZX_ERR_IO_NOT_PRESENT) { |
| zxlogf(INFO, |
| "qmi-usb-transport: terminating interrupt handling thread\n"); |
| return txn->response.status; |
| } |
| } |
| } |
| } |
| } |
| |
| // 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(qmi_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, "qmi-usb-transport: usb_request_mmap failed with status %d\n", |
| status); |
| return; |
| } |
| |
| if (len > 2048) { |
| zxlogf(ERROR, "qmi-usb-transport: recieved usb packet is too large: %zd\n", len); |
| return; |
| } |
| |
| uint8_t send_data[len + ETHERNET_FRAME_OFFSET]; // woo! VLA! |
| send_data[0] = 0x62; // destination mac addr |
| send_data[1] = 0x77; |
| send_data[2] = 0x62; |
| send_data[3] = 0x62; |
| send_data[4] = 0x77; |
| send_data[5] = 0x62; |
| |
| send_data[6] = 0x79; // source mac addr |
| send_data[7] = 0x61; |
| send_data[8] = 0x6B; |
| send_data[9] = 0x79; |
| send_data[10] = 0x61; |
| send_data[11] = 0x6B; |
| |
| send_data[12] = 0x08; |
| send_data[13] = 0x00; |
| |
| memcpy(&send_data[ETHERNET_FRAME_OFFSET], read_data, len); |
| mtx_lock(&ctx->ethmac_mutex); |
| if (ctx->ethmac_ifc.ops) { |
| ethmac_ifc_recv(&ctx->ethmac_ifc, send_data, len + ETHERNET_FRAME_OFFSET, 0); |
| } |
| mtx_unlock(&ctx->ethmac_mutex); |
| } |
| |
| static void usb_read_complete(usb_request_t* request, void* cookie) { |
| qmi_ctx_t* ctx = cookie; |
| |
| if (request->response.status != ZX_OK) { |
| zxlogf(ERROR, |
| "qmi-usb-transport: usb_read_complete called with status %d\n", |
| (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(ERROR, "qmi-usb-transport: resetting receive endpoint\n"); |
| usb_reset_endpoint(&ctx->usb, ctx->rx_endpoint_addr); |
| } 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(ERROR, |
| "qmi-usb-transport: slowing down the requests by %d usec." |
| "Resetting the recv endpoint\n", |
| ETHMAC_RECV_DELAY); |
| usb_reset_endpoint(&ctx->usb, ctx->rx_endpoint_addr); |
| } else if (request->response.status == ZX_OK) { |
| usb_recv(ctx, request); |
| } |
| |
| zx_nanosleep(zx_deadline_after(ZX_USEC(ctx->rx_endpoint_delay))); |
| usb_request_queue(&ctx->usb, request); |
| } |
| |
| static void usb_write_complete(usb_request_t* request, void* cookie) { |
| qmi_ctx_t* ctx = cookie; |
| |
| if (request->response.status == ZX_ERR_IO_NOT_PRESENT) { |
| usb_request_release(request); |
| return; |
| } |
| |
| mtx_lock(&ctx->tx_mutex); |
| |
| // Return transmission buffer to pool |
| list_add_tail(&ctx->tx_txn_bufs, &request->node); |
| |
| if (request->response.status == ZX_ERR_IO_REFUSED) { |
| zxlogf(ERROR, "qmi-usb-transport: resetting transmit endpoint\n"); |
| usb_reset_endpoint(&ctx->usb, ctx->tx_endpoint_addr); |
| } |
| |
| if (request->response.status == ZX_ERR_IO_INVALID) { |
| zxlogf(ERROR, |
| "qmi-usb-transport: slowing down the requests by %d usec." |
| "Resetting the transmit endpoint\n", |
| ETHMAC_TRANSMIT_DELAY); |
| if (ctx->tx_endpoint_delay < ETHMAC_MAX_TRANSMIT_DELAY) { |
| ctx->tx_endpoint_delay += ETHMAC_TRANSMIT_DELAY; |
| } |
| usb_reset_endpoint(&ctx->usb, ctx->tx_endpoint_addr); |
| } |
| |
| 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. |
| } |
| |
| static zx_status_t qmi_bind(void* ctx, zx_device_t* device) { |
| zx_status_t status; |
| qmi_ctx_t* qmi_ctx; |
| usb_request_t* int_buf = NULL; |
| if ((qmi_ctx = calloc(1, sizeof(qmi_ctx_t))) == NULL) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| // Set up USB stuff |
| usb_protocol_t usb; |
| status = device_get_protocol(device, ZX_PROTOCOL_USB, &usb); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "qmi-usb-transport: get protocol failed: %s\n", |
| zx_status_get_string(status)); |
| goto fail; |
| } |
| |
| // Initialize context |
| qmi_ctx->usb_device = device; |
| memcpy(&qmi_ctx->usb, &usb, sizeof(qmi_ctx->usb)); |
| list_initialize(&qmi_ctx->tx_txn_bufs); |
| list_initialize(&qmi_ctx->tx_pending_infos); |
| mtx_init(&qmi_ctx->ethmac_mutex, mtx_plain); |
| mtx_init(&qmi_ctx->tx_mutex, mtx_plain); |
| |
| qmi_ctx->parent_req_size = usb_get_request_size(&qmi_ctx->usb); |
| ZX_DEBUG_ASSERT(qmi_ctx->parent_req_size != 0); |
| |
| // find our endpoints |
| usb_desc_iter_t iter; |
| zx_status_t result = usb_desc_iter_init(&usb, &iter); |
| if (result < 0) { |
| goto fail; |
| } |
| |
| // QMI needs to bind to interface QMI_INTERFACE_NUM on current hardware. |
| // Ignore the others for now. |
| // TODO generic way of describing usb interfaces |
| usb_interface_descriptor_t* intf = usb_desc_iter_next_interface(&iter, true); |
| if (!intf || intf->bInterfaceNumber != QMI_INTERFACE_NUM) { |
| usb_desc_iter_release(&iter); |
| status = ZX_ERR_NOT_SUPPORTED; |
| goto failnoerr; // this is not a big deal, just don't bind |
| } |
| |
| if (intf->bNumEndpoints != 3) { |
| zxlogf(ERROR, |
| "qmi-usb-transport: interface does not have the required 3 " |
| "endpoints\n"); |
| usb_desc_iter_release(&iter); |
| status = ZX_ERR_NOT_SUPPORTED; |
| goto fail; |
| } |
| |
| uint8_t bulk_in_addr = 0; |
| uint8_t bulk_out_addr = 0; |
| uint8_t intr_addr = 0; |
| uint16_t intr_max_packet = 0; |
| uint16_t bulk_max_packet = 0; |
| |
| usb_descriptor_header_t* desc = usb_desc_iter_next(&iter); |
| while (desc) { |
| if (desc->bDescriptorType == USB_DT_ENDPOINT) { |
| usb_endpoint_descriptor_t* endp = (void*)desc; |
| if (usb_ep_direction(endp) == USB_ENDPOINT_OUT) { |
| if (usb_ep_type(endp) == USB_ENDPOINT_BULK) { |
| bulk_out_addr = endp->bEndpointAddress; |
| bulk_max_packet = usb_ep_max_packet(endp); |
| } |
| } else { |
| if (usb_ep_type(endp) == USB_ENDPOINT_BULK) { |
| bulk_in_addr = endp->bEndpointAddress; |
| } else if (usb_ep_type(endp) == USB_ENDPOINT_INTERRUPT) { |
| intr_addr = endp->bEndpointAddress; |
| intr_max_packet = usb_ep_max_packet(endp); |
| } |
| } |
| } |
| desc = usb_desc_iter_next(&iter); |
| } |
| usb_desc_iter_release(&iter); |
| |
| if (bulk_in_addr == 0 || bulk_out_addr == 0 || intr_addr == 0) { |
| zxlogf(ERROR, "qmi-usb-transport: failed to find one of the usb endpoints"); |
| goto fail; |
| } |
| |
| if (intr_max_packet < 1 || bulk_max_packet < 1) { |
| zxlogf(ERROR, |
| "qmi-usb-transport: failed to find reasonable max packet sizes"); |
| goto fail; |
| } |
| |
| qmi_ctx->rx_endpoint_delay = ETHMAC_INITIAL_RECV_DELAY; |
| qmi_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, 8, 0); |
| |
| // set up interrupt |
| status = usb_request_alloc(&int_buf, intr_max_packet, intr_addr, qmi_ctx->parent_req_size); |
| qmi_ctx->max_packet_size = bulk_max_packet; |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "qmi-usb-transport: failed to allocate for usb request: %s\n", |
| zx_status_get_string(status)); |
| goto fail; |
| } |
| int_buf->complete_cb = qmi_interrupt_cb; |
| int_buf->cookie = qmi_ctx; |
| qmi_ctx->int_txn_buf = int_buf; |
| |
| // create port to watch for interrupts and channel messages |
| status = zx_port_create(0, &qmi_ctx->channel_port); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "qmi-usb-transport: failed to create a port: %s\n", |
| zx_status_get_string(status)); |
| goto fail; |
| } |
| qmi_ctx->tx_endpoint_addr = bulk_out_addr; |
| qmi_ctx->rx_endpoint_addr = bulk_in_addr; |
| qmi_ctx->endpoint_size = bulk_max_packet; |
| |
| // Allocate tx transaction buffers |
| // TODO uint16_t tx_buf_sz = qmi_ctx->mtu; |
| uint16_t tx_buf_sz = 1024; |
| 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, qmi_ctx->tx_endpoint_addr, qmi_ctx->parent_req_size); |
| 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; |
| |
| tx_buf->complete_cb = usb_write_complete; |
| tx_buf->cookie = qmi_ctx; |
| list_add_head(&qmi_ctx->tx_txn_bufs, &tx_buf->node); |
| tx_buf_remain -= tx_buf_sz; |
| } |
| |
| // Allocate rx transaction buffers |
| // TODO(bwb) get correct buffer sizes from usb |
| uint16_t rx_buf_sz = 1024; |
| 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, qmi_ctx->rx_endpoint_addr, qmi_ctx->parent_req_size); |
| if (alloc_result != ZX_OK) { |
| result = alloc_result; |
| goto fail; |
| } |
| |
| rx_buf->complete_cb = usb_read_complete; |
| rx_buf->cookie = qmi_ctx; |
| usb_request_queue(&qmi_ctx->usb, rx_buf); |
| rx_buf_remain -= rx_buf_sz; |
| } |
| |
| // Set MAC addr |
| qmi_ctx->mac_addr[0] = 0x62; |
| qmi_ctx->mac_addr[1] = 0x77; |
| qmi_ctx->mac_addr[2] = 0x62; |
| qmi_ctx->mac_addr[3] = 0x62; |
| qmi_ctx->mac_addr[4] = 0x77; |
| qmi_ctx->mac_addr[5] = 0x62; |
| |
| // Kick off the handler thread |
| int thread_result = |
| thrd_create_with_name(&qmi_ctx->int_thread, qmi_transport_thread, qmi_ctx, |
| "qmi_transport_thread"); |
| if (thread_result != thrd_success) { |
| zxlogf(ERROR, "qmi-usb-transport: failed to create transport thread (%d)\n", |
| thread_result); |
| goto fail; |
| } |
| |
| device_add_args_t eth_args = { |
| .version = DEVICE_ADD_ARGS_VERSION, |
| .name = "qmi-cdc-ethernet", |
| .ctx = qmi_ctx, |
| // sibling of qmi transport. Cleanup happens when qmi device unbinds |
| .ops = ð_qmi_ops, |
| .proto_id = ZX_PROTOCOL_ETHMAC, |
| .proto_ops = ðmac_ops, |
| }; |
| result = device_add(device, ð_args, &qmi_ctx->eth_zxdev); |
| if (result < 0) { |
| zxlogf(ERROR, "qmi-usb-transport: failed to add ethernet device: %d\n", |
| (int)result); |
| goto fail; |
| } |
| |
| // Add the devices |
| device_add_args_t args = { |
| .version = DEVICE_ADD_ARGS_VERSION, |
| .name = "qmi-usb-transport", |
| .ctx = qmi_ctx, |
| .ops = &qmi_ops, |
| .proto_id = ZX_PROTOCOL_QMI_TRANSPORT, |
| }; |
| |
| if ((status = device_add(device, &args, &qmi_ctx->zxdev)) < 0) { |
| goto fail; |
| } |
| |
| return ZX_OK; |
| |
| fail: |
| zxlogf(ERROR, "qmi-usb-transport: bind failed: %s\n", |
| zx_status_get_string(status)); |
| failnoerr: |
| if (int_buf) { |
| usb_request_release(int_buf); |
| } |
| free(qmi_ctx); |
| return status; |
| } |
| |
| static zx_driver_ops_t qmi_driver_ops = { |
| .version = DRIVER_OPS_VERSION, |
| .bind = qmi_bind, |
| }; |
| |
| // clang-format off |
| ZIRCON_DRIVER_BEGIN(qmi_usb, qmi_driver_ops, "zircon", "0.1", 3) |
| BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_USB), |
| BI_ABORT_IF(NE, BIND_USB_VID, SIERRA_VID), |
| BI_MATCH_IF(EQ, BIND_USB_PID, EM7565_PID), |
| ZIRCON_DRIVER_END(qmi_usb) |