| // 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 <usb/usb-request.h> |
| |
| #include <ddk/protocol/usb.h> |
| #include <ddk/debug.h> |
| |
| #include <zircon/process.h> |
| #include <zircon/syscalls.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| |
| #define MIN(a, b) ((a) < (b) ? (a) : (b)) |
| |
| static inline size_t req_buffer_size(usb_request_t* req, size_t offset) { |
| size_t remaining = req->size - req->offset - offset; |
| // May overflow. |
| if (remaining > req->size) { |
| remaining = 0; |
| } |
| return remaining; |
| } |
| |
| static inline void* req_buffer_virt(usb_request_t* req) { |
| return (void*)(((uintptr_t)req->virt) + req->offset); |
| } |
| |
| __EXPORT zx_status_t usb_request_alloc(usb_request_t** out, uint64_t data_size, |
| uint8_t ep_address, size_t req_size) { |
| if (req_size < sizeof(usb_request_t)) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| usb_request_t* req = calloc(1, req_size); |
| if (!req) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| zx_status_t status = ZX_OK; |
| if (data_size > 0) { |
| status = zx_vmo_create(data_size, 0, &req->vmo_handle); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "usb_request_alloc: Failed to create vmo: %d\n", status); |
| free(req); |
| return status; |
| } |
| |
| zx_vaddr_t mapped_addr; |
| status = zx_vmar_map(zx_vmar_root_self(), ZX_VM_PERM_READ | ZX_VM_PERM_WRITE, |
| 0, req->vmo_handle, 0, data_size, &mapped_addr); |
| |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "usb_request_alloc: Failed to map the vmo: %d\n", status); |
| free(req); |
| return status; |
| } |
| |
| req->virt = mapped_addr; |
| req->offset = 0; |
| req->size = data_size; |
| } |
| req->alloc_size = req_size; |
| req->header.ep_address = ep_address; |
| req->header.length = data_size; |
| req->release_frees = true; |
| *out = req; |
| return ZX_OK; |
| } |
| |
| // usb_request_alloc_vmo() creates a new usb request with the given VMO. |
| __EXPORT zx_status_t usb_request_alloc_vmo(usb_request_t** out, zx_handle_t vmo_handle, |
| uint64_t vmo_offset, uint64_t length, |
| uint8_t ep_address, size_t req_size) { |
| usb_request_t* req = calloc(1, req_size); |
| if (!req) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| zx_handle_t dup_handle; |
| zx_status_t status = zx_handle_duplicate(vmo_handle, ZX_RIGHT_SAME_RIGHTS, &dup_handle); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "usb_request_alloc_vmo: Failed to duplicate handle: %d\n", status); |
| free(req); |
| return status; |
| } |
| |
| uint64_t size; |
| status = zx_vmo_get_size(dup_handle, &size); |
| if (status != ZX_OK) { |
| zx_handle_close(dup_handle); |
| free(req); |
| return status; |
| } |
| |
| zx_vaddr_t mapped_addr; |
| status = zx_vmar_map(zx_vmar_root_self(), ZX_VM_PERM_READ | ZX_VM_PERM_WRITE, |
| 0, dup_handle, 0, size, &mapped_addr); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "usb_request_alloc_vmo: zx_vmar_map failed %d size: %zu\n", status, size); |
| zx_handle_close(dup_handle); |
| free(req); |
| return status; |
| } |
| |
| req->alloc_size = req_size; |
| req->vmo_handle = dup_handle; |
| req->virt = mapped_addr; |
| req->offset = vmo_offset; |
| req->size = size; |
| |
| req->pmt = ZX_HANDLE_INVALID; |
| |
| req->header.ep_address = ep_address; |
| req->header.length = length; |
| req->release_frees = true; |
| *out = req; |
| return ZX_OK; |
| } |
| |
| // usb_request_init() initializes the statically allocated usb request with the given VMO. |
| // This will free any resources allocated by the usb request but not the usb request itself. |
| __EXPORT zx_status_t usb_request_init(usb_request_t* req, zx_handle_t vmo_handle, |
| uint64_t vmo_offset, uint64_t length, uint8_t ep_address) { |
| memset(req, 0, req->alloc_size); |
| |
| zx_handle_t dup_handle; |
| zx_status_t status = zx_handle_duplicate(vmo_handle, ZX_RIGHT_SAME_RIGHTS, &dup_handle); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "usb_request_init: Failed to duplicate handle: %d\n", status); |
| return status; |
| } |
| |
| uint64_t size; |
| status = zx_vmo_get_size(dup_handle, &size); |
| if (status != ZX_OK) { |
| zx_handle_close(dup_handle); |
| return status; |
| } |
| |
| //TODO(ravoorir): Do not map the entire vmo. Map only what is needed. |
| zx_vaddr_t mapped_addr; |
| status = zx_vmar_map(zx_vmar_root_self(), ZX_VM_PERM_READ | ZX_VM_PERM_WRITE, |
| 0, dup_handle, 0, size, &mapped_addr); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "usb_request_init: zx_vmar_map failed %d size: %zu\n", status, size); |
| zx_handle_close(dup_handle); |
| return status; |
| } |
| |
| req->vmo_handle = dup_handle; |
| req->virt = mapped_addr; |
| req->offset = vmo_offset; |
| req->size = size; |
| |
| req->pmt = ZX_HANDLE_INVALID; |
| |
| req->header.ep_address = ep_address; |
| req->header.length = length; |
| req->release_frees = false; |
| return ZX_OK; |
| } |
| |
| __EXPORT zx_status_t usb_request_set_sg_list(usb_request_t* req, |
| const phys_iter_sg_entry_t* sg_list, size_t sg_count) { |
| if (req->sg_list) { |
| free(req->sg_list); |
| req->sg_list = NULL; |
| req->sg_count = 0; |
| } |
| size_t total_length = 0; |
| // TODO(jocelyndang): disallow overlapping entries? |
| for (size_t i = 0; i < sg_count; ++i) { |
| const phys_iter_sg_entry_t* entry = &sg_list[i]; |
| if (entry->length == 0 || (req_buffer_size(req, entry->offset) < entry->length)) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| total_length += entry->length; |
| } |
| size_t num_bytes = sg_count * sizeof(phys_iter_sg_entry_t); |
| req->sg_list = malloc(num_bytes); |
| if (req->sg_list == NULL) { |
| zxlogf(ERROR, "usb_request_set_sg_list: out of memory\n"); |
| return ZX_ERR_NO_MEMORY; |
| } |
| memcpy(req->sg_list, sg_list, num_bytes); |
| req->sg_count = sg_count; |
| req->header.length = total_length; |
| return ZX_OK; |
| } |
| |
| __EXPORT ssize_t usb_request_copy_from(usb_request_t* req, void* data, size_t length, size_t offset) { |
| length = MIN(req_buffer_size(req, offset), length); |
| memcpy(data, req_buffer_virt(req) + offset, length); |
| return length; |
| } |
| |
| __EXPORT ssize_t usb_request_copy_to(usb_request_t* req, const void* data, size_t length, size_t offset) { |
| length = MIN(req_buffer_size(req, offset), length); |
| memcpy(req_buffer_virt(req) + offset, data, length); |
| return length; |
| } |
| |
| __EXPORT zx_status_t usb_request_mmap(usb_request_t* req, void** data) { |
| *data = req_buffer_virt(req); |
| // TODO(jocelyndang): modify this once we start passing usb requests across process boundaries. |
| return ZX_OK; |
| } |
| |
| __EXPORT zx_status_t usb_request_cacheop(usb_request_t* req, uint32_t op, size_t offset, size_t length) { |
| if (length > 0) { |
| return zx_vmo_op_range(req->vmo_handle, op, req->offset + offset, length, NULL, 0); |
| } else { |
| return ZX_OK; |
| } |
| } |
| |
| __EXPORT zx_status_t usb_request_cache_flush(usb_request_t* req, zx_off_t offset, size_t length) { |
| if (offset + length < offset || offset + length > req->size) { |
| return ZX_ERR_OUT_OF_RANGE; |
| } |
| return zx_cache_flush(req_buffer_virt(req) + offset, length, ZX_CACHE_FLUSH_DATA); |
| } |
| |
| __EXPORT zx_status_t usb_request_cache_flush_invalidate(usb_request_t* req, zx_off_t offset, size_t length) { |
| if (offset + length < offset || offset + length > req->size) { |
| return ZX_ERR_OUT_OF_RANGE; |
| } |
| return zx_cache_flush(req_buffer_virt(req) + offset, length, |
| ZX_CACHE_FLUSH_DATA | ZX_CACHE_FLUSH_INVALIDATE); |
| } |
| |
| zx_status_t usb_request_physmap(usb_request_t* req, zx_handle_t bti_handle) { |
| if (req->phys_count > 0) { |
| return ZX_OK; |
| } |
| // zx_bti_pin returns whole pages, so take into account unaligned vmo |
| // offset and length when calculating the amount of pages returned |
| uint64_t page_offset = ROUNDDOWN(req->offset, PAGE_SIZE); |
| // The buffer size is the vmo size from offset 0. |
| uint64_t page_length = req->size - page_offset; |
| uint64_t pages = ROUNDUP(page_length, PAGE_SIZE) / PAGE_SIZE; |
| |
| zx_paddr_t* paddrs = malloc(pages * sizeof(zx_paddr_t)); |
| if (paddrs == NULL) { |
| zxlogf(ERROR, "usb_request_physmap: out of memory\n"); |
| return ZX_ERR_NO_MEMORY; |
| } |
| const size_t sub_offset = page_offset & (PAGE_SIZE - 1); |
| const size_t pin_offset = page_offset - sub_offset; |
| const size_t pin_length = ROUNDUP(page_length + sub_offset, PAGE_SIZE); |
| |
| if (pin_length / PAGE_SIZE != pages) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| zx_handle_t pmt; |
| uint32_t options = ZX_BTI_PERM_READ | ZX_BTI_PERM_WRITE; |
| zx_status_t status = zx_bti_pin(bti_handle, options, req->vmo_handle, |
| pin_offset, pin_length, paddrs, pages, &pmt); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "usb_request_physmap: zx_bti_pin failed:%d\n", status); |
| free(paddrs); |
| return status; |
| } |
| // Account for the initial misalignment if any |
| paddrs[0] += sub_offset; |
| req->phys_list = paddrs; |
| req->phys_count = pages; |
| req->pmt = pmt; |
| |
| return ZX_OK; |
| } |
| |
| __EXPORT void usb_request_release(usb_request_t* req) { |
| if (req->vmo_handle != ZX_HANDLE_INVALID) { |
| if (req->pmt != ZX_HANDLE_INVALID) { |
| zx_status_t status = zx_pmt_unpin(req->pmt); |
| ZX_DEBUG_ASSERT(status == ZX_OK); |
| req->pmt = ZX_HANDLE_INVALID; |
| } |
| |
| zx_vmar_unmap(zx_vmar_root_self(), (uintptr_t)req->virt, req->size); |
| zx_handle_close(req->vmo_handle); |
| req->vmo_handle = ZX_HANDLE_INVALID; |
| } |
| if (req->phys_list && req->pmt != ZX_HANDLE_INVALID) { |
| zx_status_t status = zx_pmt_unpin(req->pmt); |
| ZX_DEBUG_ASSERT(status == ZX_OK); |
| req->pmt = ZX_HANDLE_INVALID; |
| } |
| free(req->phys_list); |
| req->phys_list = NULL; |
| req->phys_count = 0; |
| free(req->sg_list); |
| req->sg_list = NULL; |
| req->sg_count = 0; |
| if (req->release_frees) { |
| free(req); |
| } |
| } |
| |
| __EXPORT void usb_request_complete(usb_request_t* req, zx_status_t status, zx_off_t actual, |
| const usb_request_complete_t* complete_cb) { |
| req->response.status = status; |
| req->response.actual = actual; |
| |
| if (complete_cb) { |
| complete_cb->callback(complete_cb->ctx, req); |
| } |
| } |
| |
| __EXPORT void usb_request_phys_iter_init(phys_iter_t* iter, usb_request_t* req, size_t max_length) { |
| phys_iter_buffer_t buf = { |
| .length = req->header.length, |
| .vmo_offset = req->offset, |
| .phys = req->phys_list, |
| .phys_count = req->phys_count, |
| .sg_list = req->sg_list, |
| .sg_count = req->sg_count |
| }; |
| phys_iter_init(iter, &buf, max_length); |
| } |
| |
| __EXPORT size_t usb_request_phys_iter_next(phys_iter_t* iter, zx_paddr_t* out_paddr) { |
| return phys_iter_next(iter, out_paddr); |
| } |
| |
| __EXPORT void usb_request_pool_init(usb_request_pool_t* pool, uint64_t node_offset) { |
| mtx_init(&pool->lock, mtx_plain); |
| list_initialize(&pool->free_reqs); |
| pool->node_offset = node_offset; |
| } |
| |
| __EXPORT zx_status_t usb_request_pool_add(usb_request_pool_t* pool, usb_request_t* req) { |
| mtx_lock(&pool->lock); |
| if (req->alloc_size < (pool->node_offset + sizeof(list_node_t))) { |
| mtx_unlock(&pool->lock); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| list_add_tail(&pool->free_reqs, (list_node_t*)((uintptr_t)req + pool->node_offset)); |
| mtx_unlock(&pool->lock); |
| return ZX_OK; |
| } |
| |
| __EXPORT usb_request_t* usb_request_pool_get(usb_request_pool_t* pool, size_t length) { |
| usb_request_t* req = NULL; |
| bool found = false; |
| |
| mtx_lock(&pool->lock); |
| list_node_t* node; |
| list_for_every(&pool->free_reqs, node) { |
| req = (usb_request_t*)((uintptr_t)node - pool->node_offset); |
| if (req->size == length) { |
| found = true; |
| break; |
| } |
| } |
| if (found) { |
| list_delete(node); |
| } |
| mtx_unlock(&pool->lock); |
| |
| return found ? req : NULL; |
| } |
| |
| __EXPORT void usb_request_pool_release(usb_request_pool_t* pool) { |
| mtx_lock(&pool->lock); |
| |
| usb_request_t* req; |
| list_node_t* node; |
| while ((node = list_remove_tail(&pool->free_reqs)) != NULL) { |
| req = (usb_request_t*)((uintptr_t)node - pool->node_offset); |
| usb_request_release(req); |
| } |
| |
| mtx_unlock(&pool->lock); |
| } |
| |
| __EXPORT zx_status_t usb_req_list_add_head(list_node_t* list, usb_request_t* req, |
| size_t parent_req_size) { |
| if (req->alloc_size < parent_req_size + sizeof(list_node_t)) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| usb_req_internal_t* req_int = USB_REQ_TO_REQ_INTERNAL(req, parent_req_size); |
| list_add_head(list, &req_int->node); |
| return ZX_OK; |
| } |
| |
| __EXPORT zx_status_t usb_req_list_add_tail(list_node_t* list, usb_request_t* req, |
| size_t parent_req_size) { |
| if (req->alloc_size < parent_req_size + sizeof(list_node_t)) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| usb_req_internal_t* req_int = USB_REQ_TO_REQ_INTERNAL(req, parent_req_size); |
| list_add_tail(list, &req_int->node); |
| return ZX_OK; |
| } |
| |
| __EXPORT usb_request_t* usb_req_list_remove_head(list_node_t* list, size_t parent_req_size) { |
| usb_req_internal_t* req_int = list_remove_head_type(list, usb_req_internal_t, node); |
| if (req_int) { |
| return REQ_INTERNAL_TO_USB_REQ(req_int, parent_req_size); |
| } |
| return NULL; |
| } |