blob: 24377c627d2f371ecf938f3c2930503718548c6c [file] [log] [blame]
/*
* block.c - simple block layer
*
* Copyright (c) 2011 Sascha Hauer <s.hauer@pengutronix.de>, Pengutronix
*
* See file CREDITS for list of people who contributed to this
* project.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include <common.h>
#include <block.h>
#include <linux/err.h>
#define BLOCKSIZE(blk) (1 << blk->blockbits)
#define WRBUFFER_LAST(blk) (blk->wrblock + blk->wrbufblocks - 1)
#ifdef CONFIG_BLOCK_WRITE
static int writebuffer_flush(struct block_device *blk)
{
if (!blk->wrbufblocks)
return 0;
blk->ops->write(blk, blk->wrbuf, blk->wrblock,
blk->wrbufblocks);
blk->wrbufblocks = 0;
return 0;
}
static int block_put(struct block_device *blk, const void *buf, int block)
{
if (block >= blk->num_blocks)
return -EIO;
if (block < blk->wrblock || block > blk->wrblock + blk->wrbufblocks) {
writebuffer_flush(blk);
}
if (blk->wrbufblocks == 0) {
blk->wrblock = block;
blk->wrbufblocks = 1;
}
memcpy(blk->wrbuf + (block - blk->wrblock) * BLOCKSIZE(blk),
buf, BLOCKSIZE(blk));
if (block > WRBUFFER_LAST(blk))
blk->wrbufblocks++;
if (blk->wrbufblocks == blk->wrbufsize)
writebuffer_flush(blk);
return 0;
}
#else
static int writebuffer_flush(struct block_device *blk)
{
return 0;
}
#endif
static void *block_get(struct block_device *blk, int block)
{
int ret;
int num_blocks;
if (block >= blk->num_blocks)
return ERR_PTR(-EIO);
/* first look into write buffer */
if (block >= blk->wrblock && block <= WRBUFFER_LAST(blk))
return blk->wrbuf + (block - blk->wrblock) * BLOCKSIZE(blk);
/* then look into read buffer */
if (block >= blk->rdblock && block <= blk->rdblockend)
return blk->rdbuf + (block - blk->rdblock) * BLOCKSIZE(blk);
/*
* If none of the buffers above match read the block from
* the device
*/
num_blocks = min(blk->rdbufsize, blk->num_blocks - block);
ret = blk->ops->read(blk, blk->rdbuf, block, num_blocks);
if (ret)
return ERR_PTR(ret);
blk->rdblock = block;
blk->rdblockend = block + num_blocks - 1;
return blk->rdbuf;
}
static ssize_t block_read(struct cdev *cdev, void *buf, size_t count,
unsigned long offset, unsigned long flags)
{
struct block_device *blk = cdev->priv;
unsigned long mask = BLOCKSIZE(blk) - 1;
unsigned long block = offset >> blk->blockbits;
size_t icount = count;
int blocks;
if (offset & mask) {
size_t now = BLOCKSIZE(blk) - (offset & mask);
void *iobuf = block_get(blk, block);
now = min(count, now);
if (IS_ERR(iobuf))
return PTR_ERR(iobuf);
memcpy(buf, iobuf + (offset & mask), now);
buf += now;
count -= now;
block++;
}
blocks = count >> blk->blockbits;
while (blocks) {
void *iobuf = block_get(blk, block);
if (IS_ERR(iobuf))
return PTR_ERR(iobuf);
memcpy(buf, iobuf, BLOCKSIZE(blk));
buf += BLOCKSIZE(blk);
blocks--;
block++;
count -= BLOCKSIZE(blk);
}
if (count) {
void *iobuf = block_get(blk, block);
if (IS_ERR(iobuf))
return PTR_ERR(iobuf);
memcpy(buf, iobuf, count);
}
return icount;
}
#ifdef CONFIG_BLOCK_WRITE
static ssize_t block_write(struct cdev *cdev, const void *buf, size_t count,
unsigned long offset, ulong flags)
{
struct block_device *blk = cdev->priv;
unsigned long mask = BLOCKSIZE(blk) - 1;
unsigned long block = offset >> blk->blockbits;
size_t icount = count;
int blocks;
if (offset & mask) {
size_t now = BLOCKSIZE(blk) - (offset & mask);
void *iobuf = block_get(blk, block);
now = min(count, now);
if (IS_ERR(iobuf))
return PTR_ERR(iobuf);
memcpy(iobuf + (offset & mask), buf, now);
block_put(blk, iobuf, block);
buf += now;
count -= now;
block++;
}
blocks = count >> blk->blockbits;
while (blocks) {
block_put(blk, buf, block);
buf += BLOCKSIZE(blk);
blocks--;
block++;
count -= BLOCKSIZE(blk);
}
if (count) {
void *iobuf = block_get(blk, block);
if (IS_ERR(iobuf))
return PTR_ERR(iobuf);
memcpy(iobuf, buf, count);
block_put(blk, iobuf, block);
}
return icount;
}
#endif
static int block_close(struct cdev *cdev)
{
struct block_device *blk = cdev->priv;
return writebuffer_flush(blk);
}
static int block_flush(struct cdev *cdev)
{
struct block_device *blk = cdev->priv;
return writebuffer_flush(blk);
}
struct file_operations block_ops = {
.read = block_read,
#ifdef CONFIG_BLOCK_WRITE
.write = block_write,
#endif
.close = block_close,
.flush = block_flush,
.lseek = dev_lseek_default,
};
int blockdevice_register(struct block_device *blk)
{
size_t size = blk->num_blocks * BLOCKSIZE(blk);
int ret;
blk->cdev.size = size;
blk->cdev.dev = blk->dev;
blk->cdev.ops = &block_ops;
blk->cdev.priv = blk;
blk->rdbufsize = PAGE_SIZE >> blk->blockbits;
blk->rdbuf = xmalloc(PAGE_SIZE);
blk->rdblock = 1;
blk->rdblockend = 0;
blk->wrbufsize = PAGE_SIZE >> blk->blockbits;
blk->wrbuf = xmalloc(PAGE_SIZE);
blk->wrblock = 0;
blk->wrbufblocks = 0;
ret = devfs_create(&blk->cdev);
if (ret)
return ret;
return 0;
}
int blockdevice_unregister(struct block_device *blk)
{
return 0;
}