Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 294a39e7 authored by David Vrabel's avatar David Vrabel Committed by Greg Kroah-Hartman
Browse files

USB: whci-hcd: support urbs with scatter-gather lists



Support urbs with scatter-gather lists by trying to fit sg list elements
into page lists in one or more qTDs.  qTDs must end on a wMaxPacketSize
boundary so if this isn't possible the urb's sg list must be copied into
bounce buffers.

Signed-off-by: default avatarDavid Vrabel <david.vrabel@csr.com>
Signed-off-by: default avatarGreg Kroah-Hartman <gregkh@suse.de>
parent 4c1bd3d7
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -250,6 +250,7 @@ static int whc_probe(struct umc_dev *umc)
	}

	usb_hcd->wireless = 1;
	usb_hcd->self.sg_tablesize = 2048; /* somewhat arbitrary */

	wusbhc = usb_hcd_to_wusbhc(usb_hcd);
	whc = wusbhc_to_whc(wusbhc);
+319 −29
Original line number Diff line number Diff line
@@ -57,8 +57,9 @@ static void qset_fill_qh(struct whc_qset *qset, struct urb *urb)

	is_out = usb_pipeout(urb->pipe);

	epcd = (struct usb_wireless_ep_comp_descriptor *)qset->ep->extra;
	qset->max_packet = le16_to_cpu(urb->ep->desc.wMaxPacketSize);

	epcd = (struct usb_wireless_ep_comp_descriptor *)qset->ep->extra;
	if (epcd) {
		qset->max_seq = epcd->bMaxSequence;
		qset->max_burst = epcd->bMaxBurst;
@@ -72,7 +73,7 @@ static void qset_fill_qh(struct whc_qset *qset, struct urb *urb)
		| (is_out ? QH_INFO1_DIR_OUT : QH_INFO1_DIR_IN)
		| usb_pipe_to_qh_type(urb->pipe)
		| QH_INFO1_DEV_INFO_IDX(wusb_port_no_to_idx(usb_dev->portnum))
		| QH_INFO1_MAX_PKT_LEN(usb_maxpacket(urb->dev, urb->pipe, is_out))
		| QH_INFO1_MAX_PKT_LEN(qset->max_packet)
		);
	qset->qh.info2 = cpu_to_le32(
		QH_INFO2_BURST(qset->max_burst)
@@ -241,6 +242,36 @@ static void qset_remove_qtd(struct whc *whc, struct whc_qset *qset)
	qset->ntds--;
}

static void qset_copy_bounce_to_sg(struct whc *whc, struct whc_std *std)
{
	struct scatterlist *sg;
	void *bounce;
	size_t remaining, offset;

	bounce = std->bounce_buf;
	remaining = std->len;

	sg = std->bounce_sg;
	offset = std->bounce_offset;

	while (remaining) {
		size_t len;

		len = min(sg->length - offset, remaining);
		memcpy(sg_virt(sg) + offset, bounce, len);

		bounce += len;
		remaining -= len;

		offset += len;
		if (offset >= sg->length) {
			sg = sg_next(sg);
			offset = 0;
		}
	}

}

/**
 * qset_free_std - remove an sTD and free it.
 * @whc: the WHCI host controller
@@ -249,13 +280,29 @@ static void qset_remove_qtd(struct whc *whc, struct whc_qset *qset)
void qset_free_std(struct whc *whc, struct whc_std *std)
{
	list_del(&std->list_node);
	if (std->num_pointers) {
	if (std->bounce_buf) {
		bool is_out = usb_pipeout(std->urb->pipe);
		dma_addr_t dma_addr;

		if (std->num_pointers)
			dma_addr = le64_to_cpu(std->pl_virt[0].buf_ptr);
		else
			dma_addr = std->dma_addr;

		dma_unmap_single(whc->wusbhc.dev, dma_addr,
				 std->len, is_out ? DMA_TO_DEVICE : DMA_FROM_DEVICE);
		if (!is_out)
			qset_copy_bounce_to_sg(whc, std);
		kfree(std->bounce_buf);
	}
	if (std->pl_virt) {
		if (std->dma_addr)
			dma_unmap_single(whc->wusbhc.dev, std->dma_addr,
					 std->num_pointers * sizeof(struct whc_page_list_entry),
					 DMA_TO_DEVICE);
		kfree(std->pl_virt);
		std->pl_virt = NULL;
	}

	kfree(std);
}

@@ -293,12 +340,17 @@ static int qset_fill_page_list(struct whc *whc, struct whc_std *std, gfp_t mem_f
{
	dma_addr_t dma_addr = std->dma_addr;
	dma_addr_t sp, ep;
	size_t std_len = std->len;
	size_t pl_len;
	int p;

	sp = ALIGN(dma_addr, WHCI_PAGE_SIZE);
	ep = dma_addr + std_len;
	/* Short buffers don't need a page list. */
	if (std->len <= WHCI_PAGE_SIZE) {
		std->num_pointers = 0;
		return 0;
	}

	sp = dma_addr & ~(WHCI_PAGE_SIZE-1);
	ep = dma_addr + std->len;
	std->num_pointers = DIV_ROUND_UP(ep - sp, WHCI_PAGE_SIZE);

	pl_len = std->num_pointers * sizeof(struct whc_page_list_entry);
@@ -309,7 +361,7 @@ static int qset_fill_page_list(struct whc *whc, struct whc_std *std, gfp_t mem_f

	for (p = 0; p < std->num_pointers; p++) {
		std->pl_virt[p].buf_ptr = cpu_to_le64(dma_addr);
		dma_addr = ALIGN(dma_addr + WHCI_PAGE_SIZE, WHCI_PAGE_SIZE);
		dma_addr = (dma_addr + WHCI_PAGE_SIZE) & ~(WHCI_PAGE_SIZE-1);
	}

	return 0;
@@ -339,6 +391,238 @@ static void urb_dequeue_work(struct work_struct *work)
	spin_unlock_irqrestore(&whc->lock, flags);
}

static struct whc_std *qset_new_std(struct whc *whc, struct whc_qset *qset,
				    struct urb *urb, gfp_t mem_flags)
{
	struct whc_std *std;

	std = kzalloc(sizeof(struct whc_std), mem_flags);
	if (std == NULL)
		return NULL;

	std->urb = urb;
	std->qtd = NULL;

	INIT_LIST_HEAD(&std->list_node);
	list_add_tail(&std->list_node, &qset->stds);

	return std;
}

static int qset_add_urb_sg(struct whc *whc, struct whc_qset *qset, struct urb *urb,
			   gfp_t mem_flags)
{
	size_t remaining;
	struct scatterlist *sg;
	int i;
	int ntds = 0;
	struct whc_std *std = NULL;
	struct whc_page_list_entry *entry;
	dma_addr_t prev_end = 0;
	size_t pl_len;
	int p = 0;

	dev_dbg(&whc->umc->dev, "adding urb w/ sg of length %d\n", urb->transfer_buffer_length);

	remaining = urb->transfer_buffer_length;

	for_each_sg(urb->sg->sg, sg, urb->num_sgs, i) {
		dma_addr_t dma_addr;
		size_t dma_remaining;
		dma_addr_t sp, ep;
		int num_pointers;

		if (remaining == 0) {
			break;
		}

		dma_addr = sg_dma_address(sg);
		dma_remaining = min(sg_dma_len(sg), remaining);

		dev_dbg(&whc->umc->dev, "adding sg[%d] %08x %d\n", i, (unsigned)dma_addr,
			dma_remaining);

		while (dma_remaining) {
			size_t dma_len;

			/*
			 * We can use the previous std (if it exists) provided that:
			 * - the previous one ended on a page boundary.
			 * - the current one begins on a page boundary.
			 * - the previous one isn't full.
			 *
			 * If a new std is needed but the previous one
			 * did not end on a wMaxPacketSize boundary
			 * then this sg list cannot be mapped onto
			 * multiple qTDs.  Return an error and let the
			 * caller sort it out.
			 */
			if (!std
			    || (prev_end & (WHCI_PAGE_SIZE-1))
			    || (dma_addr & (WHCI_PAGE_SIZE-1))
			    || std->len + WHCI_PAGE_SIZE > QTD_MAX_XFER_SIZE) {
				if (prev_end % qset->max_packet != 0)
					return -EINVAL;
				dev_dbg(&whc->umc->dev, "need new std\n");
				std = qset_new_std(whc, qset, urb, mem_flags);
				if (std == NULL) {
					return -ENOMEM;
				}
				ntds++;
				p = 0;
			}

			dma_len = dma_remaining;

			/*
			 * If the remainder in this element doesn't
			 * fit in a single qTD, end the qTD on a
			 * wMaxPacketSize boundary.
			 */
			if (std->len + dma_len > QTD_MAX_XFER_SIZE) {
				dma_len = QTD_MAX_XFER_SIZE - std->len;
				ep = ((dma_addr + dma_len) / qset->max_packet) * qset->max_packet;
				dma_len = ep - dma_addr;
			}

			dev_dbg(&whc->umc->dev, "adding %d\n", dma_len);

			std->len += dma_len;
			std->ntds_remaining = -1; /* filled in later */

			sp = dma_addr & ~(WHCI_PAGE_SIZE-1);
			ep = dma_addr + dma_len;
			num_pointers = DIV_ROUND_UP(ep - sp, WHCI_PAGE_SIZE);
			std->num_pointers += num_pointers;

			dev_dbg(&whc->umc->dev, "need %d more (%d total) page pointers\n",
				num_pointers, std->num_pointers);

			pl_len = std->num_pointers * sizeof(struct whc_page_list_entry);

			std->pl_virt = krealloc(std->pl_virt, pl_len, mem_flags);
			if (std->pl_virt == NULL) {
				return -ENOMEM;
			}

			for (;p < std->num_pointers; p++, entry++) {
				dev_dbg(&whc->umc->dev, "e[%d] %08x\n", p, dma_addr);
				std->pl_virt[p].buf_ptr = cpu_to_le64(dma_addr);
				dma_addr = (dma_addr + WHCI_PAGE_SIZE) & ~(WHCI_PAGE_SIZE-1);
			}

			prev_end = dma_addr = ep;
			dma_remaining -= dma_len;
			remaining -= dma_len;
		}
	}

	dev_dbg(&whc->umc->dev, "used %d tds\n", ntds);

	/* Now the number of stds is know, go back and fill in
	   std->ntds_remaining. */
	list_for_each_entry(std, &qset->stds, list_node) {
		if (std->ntds_remaining == -1) {
			pl_len = std->num_pointers * sizeof(struct whc_page_list_entry);
			std->ntds_remaining = ntds--;
			std->dma_addr = dma_map_single(whc->wusbhc.dev, std->pl_virt,
						       pl_len, DMA_TO_DEVICE);
		}
	}
	return 0;
}

/**
 * qset_add_urb_sg_linearize - add an urb with sg list, copying the data
 *
 * If the URB contains an sg list whose elements cannot be directly
 * mapped to qTDs then the data must be transferred via bounce
 * buffers.
 */
static int qset_add_urb_sg_linearize(struct whc *whc, struct whc_qset *qset,
				     struct urb *urb, gfp_t mem_flags)
{
	bool is_out = usb_pipeout(urb->pipe);
	size_t max_std_len;
	size_t remaining;
	int ntds = 0;
	struct whc_std *std = NULL;
	void *bounce = NULL;
	struct scatterlist *sg;
	int i;

	/* limit maximum bounce buffer to 16 * 3.5 KiB ~= 28 k */
	max_std_len = qset->max_burst * qset->max_packet;

	remaining = urb->transfer_buffer_length;

	for_each_sg(urb->sg->sg, sg, urb->sg->nents, i) {
		size_t len;
		size_t sg_remaining;
		void *orig;

		if (remaining == 0) {
			break;
		}

		sg_remaining = min(remaining, sg->length);
		orig = sg_virt(sg);

		dev_dbg(&whc->umc->dev, "adding sg[%d] %d\n", i, sg_remaining);

		while (sg_remaining) {
			if (!std || std->len == max_std_len) {
				dev_dbg(&whc->umc->dev, "need new std\n");
				std = qset_new_std(whc, qset, urb, mem_flags);
				if (std == NULL)
					return -ENOMEM;
				std->bounce_buf = kmalloc(max_std_len, mem_flags);
				if (std->bounce_buf == NULL)
					return -ENOMEM;
				std->bounce_sg = sg;
				std->bounce_offset = orig - sg_virt(sg);
				bounce = std->bounce_buf;
				ntds++;
			}

			len = min(sg_remaining, max_std_len - std->len);

			dev_dbg(&whc->umc->dev, "added %d from sg[%d] @ offset %d\n",
				len, i, orig - sg_virt(sg));

			if (is_out)
				memcpy(bounce, orig, len);

			std->len += len;
			std->ntds_remaining = -1; /* filled in later */

			bounce += len;
			orig += len;
			sg_remaining -= len;
			remaining -= len;
		}
	}

	/*
	 * For each of the new sTDs, map the bounce buffers, create
	 * page lists (if necessary), and fill in std->ntds_remaining.
	 */
	list_for_each_entry(std, &qset->stds, list_node) {
		if (std->ntds_remaining != -1)
			continue;

		std->dma_addr = dma_map_single(&whc->umc->dev, std->bounce_buf, std->len,
					       is_out ? DMA_TO_DEVICE : DMA_FROM_DEVICE);

		if (qset_fill_page_list(whc, std, mem_flags) < 0)
			return -ENOMEM;

		std->ntds_remaining = ntds--;
	}

	return 0;
}

/**
 * qset_add_urb - add an urb to the qset's queue.
 *
@@ -353,10 +637,7 @@ int qset_add_urb(struct whc *whc, struct whc_qset *qset, struct urb *urb,
	int remaining = urb->transfer_buffer_length;
	u64 transfer_dma = urb->transfer_dma;
	int ntds_remaining;

	ntds_remaining = DIV_ROUND_UP(remaining, QTD_MAX_XFER_SIZE);
	if (ntds_remaining == 0)
		ntds_remaining = 1;
	int ret;

	wurb = kzalloc(sizeof(struct whc_urb), mem_flags);
	if (wurb == NULL)
@@ -366,32 +647,41 @@ int qset_add_urb(struct whc *whc, struct whc_qset *qset, struct urb *urb,
	wurb->urb = urb;
	INIT_WORK(&wurb->dequeue_work, urb_dequeue_work);

	if (urb->sg) {
		ret = qset_add_urb_sg(whc, qset, urb, mem_flags);
		if (ret == -EINVAL) {
			dev_dbg(&whc->umc->dev, "linearizing %d octet urb\n",
				urb->transfer_buffer_length);
			qset_free_stds(qset, urb);
			ret = qset_add_urb_sg_linearize(whc, qset, urb, mem_flags);
		}
		if (ret < 0)
			goto err_no_mem;
		return 0;
	}

	ntds_remaining = DIV_ROUND_UP(remaining, QTD_MAX_XFER_SIZE);
	if (ntds_remaining == 0)
		ntds_remaining = 1;

	while (ntds_remaining) {
		struct whc_std *std;
		size_t std_len;

		std = kmalloc(sizeof(struct whc_std), mem_flags);
		if (std == NULL)
			goto err_no_mem;

		std_len = remaining;
		if (std_len > QTD_MAX_XFER_SIZE)
			std_len = QTD_MAX_XFER_SIZE;

		std->urb = urb;
		std = qset_new_std(whc, qset, urb, mem_flags);
		if (std == NULL)
			goto err_no_mem;

		std->dma_addr = transfer_dma;
		std->len = std_len;
		std->ntds_remaining = ntds_remaining;
		std->qtd = NULL;

		INIT_LIST_HEAD(&std->list_node);
		list_add_tail(&std->list_node, &qset->stds);

		if (std_len > WHCI_PAGE_SIZE) {
		if (qset_fill_page_list(whc, std, mem_flags) < 0)
			goto err_no_mem;
		} else
			std->num_pointers = 0;

		ntds_remaining--;
		remaining -= std_len;
+9 −0
Original line number Diff line number Diff line
@@ -84,6 +84,11 @@ struct whc {
 * @len: the length of data in the associated TD.
 * @ntds_remaining: number of TDs (starting from this one) in this transfer.
 *
 * @bounce_buf: a bounce buffer if the std was from an urb with a sg
 * list that could not be mapped to qTDs directly.
 * @bounce_sg: the first scatterlist element bounce_buf is for.
 * @bounce_offset: the offset into bounce_sg for the start of bounce_buf.
 *
 * Queued URBs may require more TDs than are available in a qset so we
 * use a list of these "software TDs" (sTDs) to hold per-TD data.
 */
@@ -97,6 +102,10 @@ struct whc_std {
	int num_pointers;
	dma_addr_t dma_addr;
	struct whc_page_list_entry *pl_virt;

	void *bounce_buf;
	struct scatterlist *bounce_sg;
	unsigned bounce_offset;
};

/**
+3 −2
Original line number Diff line number Diff line
@@ -267,8 +267,9 @@ struct whc_qset {
	unsigned reset:1;
	struct urb *pause_after_urb;
	struct completion remove_complete;
	int max_burst;
	int max_seq;
	uint16_t max_packet;
	uint8_t max_burst;
	uint8_t max_seq;
};

static inline void whc_qset_set_link_ptr(u64 *ptr, u64 target)