276 lines
6.3 KiB
C
276 lines
6.3 KiB
C
|
// SPDX-License-Identifier: GPL-2.0
|
||
|
/*
|
||
|
* Copyright 2018-2020 Broadcom.
|
||
|
*/
|
||
|
#include <linux/dma-mapping.h>
|
||
|
#include <linux/mm.h>
|
||
|
#include <linux/pagemap.h>
|
||
|
#include <linux/pgtable.h>
|
||
|
#include <linux/vmalloc.h>
|
||
|
|
||
|
#include <asm/page.h>
|
||
|
#include <asm/unaligned.h>
|
||
|
|
||
|
#include <uapi/linux/misc/bcm_vk.h>
|
||
|
|
||
|
#include "bcm_vk.h"
|
||
|
#include "bcm_vk_msg.h"
|
||
|
#include "bcm_vk_sg.h"
|
||
|
|
||
|
/*
|
||
|
* Valkyrie has a hardware limitation of 16M transfer size.
|
||
|
* So limit the SGL chunks to 16M.
|
||
|
*/
|
||
|
#define BCM_VK_MAX_SGL_CHUNK SZ_16M
|
||
|
|
||
|
static int bcm_vk_dma_alloc(struct device *dev,
|
||
|
struct bcm_vk_dma *dma,
|
||
|
int dir,
|
||
|
struct _vk_data *vkdata);
|
||
|
static int bcm_vk_dma_free(struct device *dev, struct bcm_vk_dma *dma);
|
||
|
|
||
|
/* Uncomment to dump SGLIST */
|
||
|
/* #define BCM_VK_DUMP_SGLIST */
|
||
|
|
||
|
static int bcm_vk_dma_alloc(struct device *dev,
|
||
|
struct bcm_vk_dma *dma,
|
||
|
int direction,
|
||
|
struct _vk_data *vkdata)
|
||
|
{
|
||
|
dma_addr_t addr, sg_addr;
|
||
|
int err;
|
||
|
int i;
|
||
|
int offset;
|
||
|
u32 size;
|
||
|
u32 remaining_size;
|
||
|
u32 transfer_size;
|
||
|
u64 data;
|
||
|
unsigned long first, last;
|
||
|
struct _vk_data *sgdata;
|
||
|
|
||
|
/* Get 64-bit user address */
|
||
|
data = get_unaligned(&vkdata->address);
|
||
|
|
||
|
/* offset into first page */
|
||
|
offset = offset_in_page(data);
|
||
|
|
||
|
/* Calculate number of pages */
|
||
|
first = (data & PAGE_MASK) >> PAGE_SHIFT;
|
||
|
last = ((data + vkdata->size - 1) & PAGE_MASK) >> PAGE_SHIFT;
|
||
|
dma->nr_pages = last - first + 1;
|
||
|
|
||
|
/* Allocate DMA pages */
|
||
|
dma->pages = kmalloc_array(dma->nr_pages,
|
||
|
sizeof(struct page *),
|
||
|
GFP_KERNEL);
|
||
|
if (!dma->pages)
|
||
|
return -ENOMEM;
|
||
|
|
||
|
dev_dbg(dev, "Alloc DMA Pages [0x%llx+0x%x => %d pages]\n",
|
||
|
data, vkdata->size, dma->nr_pages);
|
||
|
|
||
|
dma->direction = direction;
|
||
|
|
||
|
/* Get user pages into memory */
|
||
|
err = get_user_pages_fast(data & PAGE_MASK,
|
||
|
dma->nr_pages,
|
||
|
direction == DMA_FROM_DEVICE,
|
||
|
dma->pages);
|
||
|
if (err != dma->nr_pages) {
|
||
|
dma->nr_pages = (err >= 0) ? err : 0;
|
||
|
dev_err(dev, "get_user_pages_fast, err=%d [%d]\n",
|
||
|
err, dma->nr_pages);
|
||
|
return err < 0 ? err : -EINVAL;
|
||
|
}
|
||
|
|
||
|
/* Max size of sg list is 1 per mapped page + fields at start */
|
||
|
dma->sglen = (dma->nr_pages * sizeof(*sgdata)) +
|
||
|
(sizeof(u32) * SGLIST_VKDATA_START);
|
||
|
|
||
|
/* Allocate sglist */
|
||
|
dma->sglist = dma_alloc_coherent(dev,
|
||
|
dma->sglen,
|
||
|
&dma->handle,
|
||
|
GFP_KERNEL);
|
||
|
if (!dma->sglist)
|
||
|
return -ENOMEM;
|
||
|
|
||
|
dma->sglist[SGLIST_NUM_SG] = 0;
|
||
|
dma->sglist[SGLIST_TOTALSIZE] = vkdata->size;
|
||
|
remaining_size = vkdata->size;
|
||
|
sgdata = (struct _vk_data *)&dma->sglist[SGLIST_VKDATA_START];
|
||
|
|
||
|
/* Map all pages into DMA */
|
||
|
size = min_t(size_t, PAGE_SIZE - offset, remaining_size);
|
||
|
remaining_size -= size;
|
||
|
sg_addr = dma_map_page(dev,
|
||
|
dma->pages[0],
|
||
|
offset,
|
||
|
size,
|
||
|
dma->direction);
|
||
|
transfer_size = size;
|
||
|
if (unlikely(dma_mapping_error(dev, sg_addr))) {
|
||
|
__free_page(dma->pages[0]);
|
||
|
return -EIO;
|
||
|
}
|
||
|
|
||
|
for (i = 1; i < dma->nr_pages; i++) {
|
||
|
size = min_t(size_t, PAGE_SIZE, remaining_size);
|
||
|
remaining_size -= size;
|
||
|
addr = dma_map_page(dev,
|
||
|
dma->pages[i],
|
||
|
0,
|
||
|
size,
|
||
|
dma->direction);
|
||
|
if (unlikely(dma_mapping_error(dev, addr))) {
|
||
|
__free_page(dma->pages[i]);
|
||
|
return -EIO;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Compress SG list entry when pages are contiguous
|
||
|
* and transfer size less or equal to BCM_VK_MAX_SGL_CHUNK
|
||
|
*/
|
||
|
if ((addr == (sg_addr + transfer_size)) &&
|
||
|
((transfer_size + size) <= BCM_VK_MAX_SGL_CHUNK)) {
|
||
|
/* pages are contiguous, add to same sg entry */
|
||
|
transfer_size += size;
|
||
|
} else {
|
||
|
/* pages are not contiguous, write sg entry */
|
||
|
sgdata->size = transfer_size;
|
||
|
put_unaligned(sg_addr, (u64 *)&sgdata->address);
|
||
|
dma->sglist[SGLIST_NUM_SG]++;
|
||
|
|
||
|
/* start new sg entry */
|
||
|
sgdata++;
|
||
|
sg_addr = addr;
|
||
|
transfer_size = size;
|
||
|
}
|
||
|
}
|
||
|
/* Write last sg list entry */
|
||
|
sgdata->size = transfer_size;
|
||
|
put_unaligned(sg_addr, (u64 *)&sgdata->address);
|
||
|
dma->sglist[SGLIST_NUM_SG]++;
|
||
|
|
||
|
/* Update pointers and size field to point to sglist */
|
||
|
put_unaligned((u64)dma->handle, &vkdata->address);
|
||
|
vkdata->size = (dma->sglist[SGLIST_NUM_SG] * sizeof(*sgdata)) +
|
||
|
(sizeof(u32) * SGLIST_VKDATA_START);
|
||
|
|
||
|
#ifdef BCM_VK_DUMP_SGLIST
|
||
|
dev_dbg(dev,
|
||
|
"sgl 0x%llx handle 0x%llx, sglen: 0x%x sgsize: 0x%x\n",
|
||
|
(u64)dma->sglist,
|
||
|
dma->handle,
|
||
|
dma->sglen,
|
||
|
vkdata->size);
|
||
|
for (i = 0; i < vkdata->size / sizeof(u32); i++)
|
||
|
dev_dbg(dev, "i:0x%x 0x%x\n", i, dma->sglist[i]);
|
||
|
#endif
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
int bcm_vk_sg_alloc(struct device *dev,
|
||
|
struct bcm_vk_dma *dma,
|
||
|
int dir,
|
||
|
struct _vk_data *vkdata,
|
||
|
int num)
|
||
|
{
|
||
|
int i;
|
||
|
int rc = -EINVAL;
|
||
|
|
||
|
/* Convert user addresses to DMA SG List */
|
||
|
for (i = 0; i < num; i++) {
|
||
|
if (vkdata[i].size && vkdata[i].address) {
|
||
|
/*
|
||
|
* If both size and address are non-zero
|
||
|
* then DMA alloc.
|
||
|
*/
|
||
|
rc = bcm_vk_dma_alloc(dev,
|
||
|
&dma[i],
|
||
|
dir,
|
||
|
&vkdata[i]);
|
||
|
} else if (vkdata[i].size ||
|
||
|
vkdata[i].address) {
|
||
|
/*
|
||
|
* If one of size and address are zero
|
||
|
* there is a problem.
|
||
|
*/
|
||
|
dev_err(dev,
|
||
|
"Invalid vkdata %x 0x%x 0x%llx\n",
|
||
|
i, vkdata[i].size, vkdata[i].address);
|
||
|
rc = -EINVAL;
|
||
|
} else {
|
||
|
/*
|
||
|
* If size and address are both zero
|
||
|
* don't convert, but return success.
|
||
|
*/
|
||
|
rc = 0;
|
||
|
}
|
||
|
|
||
|
if (rc)
|
||
|
goto fail_alloc;
|
||
|
}
|
||
|
return rc;
|
||
|
|
||
|
fail_alloc:
|
||
|
while (i > 0) {
|
||
|
i--;
|
||
|
if (dma[i].sglist)
|
||
|
bcm_vk_dma_free(dev, &dma[i]);
|
||
|
}
|
||
|
return rc;
|
||
|
}
|
||
|
|
||
|
static int bcm_vk_dma_free(struct device *dev, struct bcm_vk_dma *dma)
|
||
|
{
|
||
|
dma_addr_t addr;
|
||
|
int i;
|
||
|
int num_sg;
|
||
|
u32 size;
|
||
|
struct _vk_data *vkdata;
|
||
|
|
||
|
dev_dbg(dev, "free sglist=%p sglen=0x%x\n", dma->sglist, dma->sglen);
|
||
|
|
||
|
/* Unmap all pages in the sglist */
|
||
|
num_sg = dma->sglist[SGLIST_NUM_SG];
|
||
|
vkdata = (struct _vk_data *)&dma->sglist[SGLIST_VKDATA_START];
|
||
|
for (i = 0; i < num_sg; i++) {
|
||
|
size = vkdata[i].size;
|
||
|
addr = get_unaligned(&vkdata[i].address);
|
||
|
|
||
|
dma_unmap_page(dev, addr, size, dma->direction);
|
||
|
}
|
||
|
|
||
|
/* Free allocated sglist */
|
||
|
dma_free_coherent(dev, dma->sglen, dma->sglist, dma->handle);
|
||
|
|
||
|
/* Release lock on all pages */
|
||
|
for (i = 0; i < dma->nr_pages; i++)
|
||
|
put_page(dma->pages[i]);
|
||
|
|
||
|
/* Free allocated dma pages */
|
||
|
kfree(dma->pages);
|
||
|
dma->sglist = NULL;
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
int bcm_vk_sg_free(struct device *dev, struct bcm_vk_dma *dma, int num,
|
||
|
int *proc_cnt)
|
||
|
{
|
||
|
int i;
|
||
|
|
||
|
*proc_cnt = 0;
|
||
|
/* Unmap and free all pages and sglists */
|
||
|
for (i = 0; i < num; i++) {
|
||
|
if (dma[i].sglist) {
|
||
|
bcm_vk_dma_free(dev, &dma[i]);
|
||
|
*proc_cnt += 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|