| /* |
| * DMA region bookkeeping routines |
| * |
| * Copyright (C) 2002 Maas Digital LLC |
| * |
| * This code is licensed under the GPL. See the file COPYING in the root |
| * directory of the kernel sources for details. |
| */ |
| |
| #include <linux/mm.h> |
| #include <linux/module.h> |
| #include <linux/pci.h> |
| #include <linux/slab.h> |
| #include <linux/vmalloc.h> |
| #include <linux/scatterlist.h> |
| |
| #include "dma.h" |
| |
| /* dma_prog_region */ |
| |
| void dma_prog_region_init(struct dma_prog_region *prog) |
| { |
| prog->kvirt = NULL; |
| prog->dev = NULL; |
| prog->n_pages = 0; |
| prog->bus_addr = 0; |
| } |
| |
| int dma_prog_region_alloc(struct dma_prog_region *prog, unsigned long n_bytes, |
| struct pci_dev *dev) |
| { |
| /* round up to page size */ |
| n_bytes = PAGE_ALIGN(n_bytes); |
| |
| prog->n_pages = n_bytes >> PAGE_SHIFT; |
| |
| prog->kvirt = pci_alloc_consistent(dev, n_bytes, &prog->bus_addr); |
| if (!prog->kvirt) { |
| printk(KERN_ERR |
| "dma_prog_region_alloc: pci_alloc_consistent() failed\n"); |
| dma_prog_region_free(prog); |
| return -ENOMEM; |
| } |
| |
| prog->dev = dev; |
| |
| return 0; |
| } |
| |
| void dma_prog_region_free(struct dma_prog_region *prog) |
| { |
| if (prog->kvirt) { |
| pci_free_consistent(prog->dev, prog->n_pages << PAGE_SHIFT, |
| prog->kvirt, prog->bus_addr); |
| } |
| |
| prog->kvirt = NULL; |
| prog->dev = NULL; |
| prog->n_pages = 0; |
| prog->bus_addr = 0; |
| } |
| |
| /* dma_region */ |
| |
| /** |
| * dma_region_init - clear out all fields but do not allocate anything |
| */ |
| void dma_region_init(struct dma_region *dma) |
| { |
| dma->kvirt = NULL; |
| dma->dev = NULL; |
| dma->n_pages = 0; |
| dma->n_dma_pages = 0; |
| dma->sglist = NULL; |
| } |
| |
| /** |
| * dma_region_alloc - allocate the buffer and map it to the IOMMU |
| */ |
| int dma_region_alloc(struct dma_region *dma, unsigned long n_bytes, |
| struct pci_dev *dev, int direction) |
| { |
| unsigned int i; |
| |
| /* round up to page size */ |
| n_bytes = PAGE_ALIGN(n_bytes); |
| |
| dma->n_pages = n_bytes >> PAGE_SHIFT; |
| |
| dma->kvirt = vmalloc_32(n_bytes); |
| if (!dma->kvirt) { |
| printk(KERN_ERR "dma_region_alloc: vmalloc_32() failed\n"); |
| goto err; |
| } |
| |
| /* Clear the ram out, no junk to the user */ |
| memset(dma->kvirt, 0, n_bytes); |
| |
| /* allocate scatter/gather list */ |
| dma->sglist = vmalloc(dma->n_pages * sizeof(*dma->sglist)); |
| if (!dma->sglist) { |
| printk(KERN_ERR "dma_region_alloc: vmalloc(sglist) failed\n"); |
| goto err; |
| } |
| |
| /* just to be safe - this will become unnecessary once sglist->address goes away */ |
| memset(dma->sglist, 0, dma->n_pages * sizeof(*dma->sglist)); |
| |
| /* fill scatter/gather list with pages */ |
| for (i = 0; i < dma->n_pages; i++) { |
| unsigned long va = |
| (unsigned long)dma->kvirt + (i << PAGE_SHIFT); |
| |
| sg_set_page(&dma->sglist[i], vmalloc_to_page((void *)va), |
| PAGE_SIZE, 0); |
| } |
| |
| /* map sglist to the IOMMU */ |
| dma->n_dma_pages = |
| pci_map_sg(dev, dma->sglist, dma->n_pages, direction); |
| |
| if (dma->n_dma_pages == 0) { |
| printk(KERN_ERR "dma_region_alloc: pci_map_sg() failed\n"); |
| goto err; |
| } |
| |
| dma->dev = dev; |
| dma->direction = direction; |
| |
| return 0; |
| |
| err: |
| dma_region_free(dma); |
| return -ENOMEM; |
| } |
| |
| /** |
| * dma_region_free - unmap and free the buffer |
| */ |
| void dma_region_free(struct dma_region *dma) |
| { |
| if (dma->n_dma_pages) { |
| pci_unmap_sg(dma->dev, dma->sglist, dma->n_pages, |
| dma->direction); |
| dma->n_dma_pages = 0; |
| dma->dev = NULL; |
| } |
| |
| vfree(dma->sglist); |
| dma->sglist = NULL; |
| |
| vfree(dma->kvirt); |
| dma->kvirt = NULL; |
| dma->n_pages = 0; |
| } |
| |
| /* find the scatterlist index and remaining offset corresponding to a |
| given offset from the beginning of the buffer */ |
| static inline int dma_region_find(struct dma_region *dma, unsigned long offset, |
| unsigned int start, unsigned long *rem) |
| { |
| int i; |
| unsigned long off = offset; |
| |
| for (i = start; i < dma->n_dma_pages; i++) { |
| if (off < sg_dma_len(&dma->sglist[i])) { |
| *rem = off; |
| break; |
| } |
| |
| off -= sg_dma_len(&dma->sglist[i]); |
| } |
| |
| BUG_ON(i >= dma->n_dma_pages); |
| |
| return i; |
| } |
| |
| /** |
| * dma_region_offset_to_bus - get bus address of an offset within a DMA region |
| * |
| * Returns the DMA bus address of the byte with the given @offset relative to |
| * the beginning of the @dma. |
| */ |
| dma_addr_t dma_region_offset_to_bus(struct dma_region * dma, |
| unsigned long offset) |
| { |
| unsigned long rem = 0; |
| |
| struct scatterlist *sg = |
| &dma->sglist[dma_region_find(dma, offset, 0, &rem)]; |
| return sg_dma_address(sg) + rem; |
| } |
| |
| /** |
| * dma_region_sync_for_cpu - sync the CPU's view of the buffer |
| */ |
| void dma_region_sync_for_cpu(struct dma_region *dma, unsigned long offset, |
| unsigned long len) |
| { |
| int first, last; |
| unsigned long rem = 0; |
| |
| if (!len) |
| len = 1; |
| |
| first = dma_region_find(dma, offset, 0, &rem); |
| last = dma_region_find(dma, rem + len - 1, first, &rem); |
| |
| pci_dma_sync_sg_for_cpu(dma->dev, &dma->sglist[first], last - first + 1, |
| dma->direction); |
| } |
| |
| /** |
| * dma_region_sync_for_device - sync the IO bus' view of the buffer |
| */ |
| void dma_region_sync_for_device(struct dma_region *dma, unsigned long offset, |
| unsigned long len) |
| { |
| int first, last; |
| unsigned long rem = 0; |
| |
| if (!len) |
| len = 1; |
| |
| first = dma_region_find(dma, offset, 0, &rem); |
| last = dma_region_find(dma, rem + len - 1, first, &rem); |
| |
| pci_dma_sync_sg_for_device(dma->dev, &dma->sglist[first], |
| last - first + 1, dma->direction); |
| } |
| |
| #ifdef CONFIG_MMU |
| |
| /* nopage() handler for mmap access */ |
| |
| static struct page *dma_region_pagefault(struct vm_area_struct *area, |
| unsigned long address, int *type) |
| { |
| unsigned long offset; |
| unsigned long kernel_virt_addr; |
| struct page *ret = NOPAGE_SIGBUS; |
| |
| struct dma_region *dma = (struct dma_region *)area->vm_private_data; |
| |
| if (!dma->kvirt) |
| goto out; |
| |
| if ((address < (unsigned long)area->vm_start) || |
| (address > |
| (unsigned long)area->vm_start + (dma->n_pages << PAGE_SHIFT))) |
| goto out; |
| |
| if (type) |
| *type = VM_FAULT_MINOR; |
| offset = address - area->vm_start; |
| kernel_virt_addr = (unsigned long)dma->kvirt + offset; |
| ret = vmalloc_to_page((void *)kernel_virt_addr); |
| get_page(ret); |
| out: |
| return ret; |
| } |
| |
| static struct vm_operations_struct dma_region_vm_ops = { |
| .nopage = dma_region_pagefault, |
| }; |
| |
| /** |
| * dma_region_mmap - map the buffer into a user space process |
| */ |
| int dma_region_mmap(struct dma_region *dma, struct file *file, |
| struct vm_area_struct *vma) |
| { |
| unsigned long size; |
| |
| if (!dma->kvirt) |
| return -EINVAL; |
| |
| /* must be page-aligned */ |
| if (vma->vm_pgoff != 0) |
| return -EINVAL; |
| |
| /* check the length */ |
| size = vma->vm_end - vma->vm_start; |
| if (size > (dma->n_pages << PAGE_SHIFT)) |
| return -EINVAL; |
| |
| vma->vm_ops = &dma_region_vm_ops; |
| vma->vm_private_data = dma; |
| vma->vm_file = file; |
| vma->vm_flags |= VM_RESERVED; |
| |
| return 0; |
| } |
| |
| #else /* CONFIG_MMU */ |
| |
| int dma_region_mmap(struct dma_region *dma, struct file *file, |
| struct vm_area_struct *vma) |
| { |
| return -EINVAL; |
| } |
| |
| #endif /* CONFIG_MMU */ |