| /* |
| * Copyright (c) 2008 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 <command.h> |
| #include <fs.h> |
| #include <linux/stat.h> |
| #include <errno.h> |
| #include <malloc.h> |
| #include <getopt.h> |
| #include <xfuncs.h> |
| #include <init.h> |
| #include <ioctl.h> |
| #include <nand.h> |
| #include <linux/mtd/mtd-abi.h> |
| #include <fcntl.h> |
| #include <libgen.h> |
| #include <linux/list.h> |
| |
| struct nand_bb { |
| char cdevname[MAX_DRIVER_NAME]; |
| struct cdev *cdev_parent; |
| char *name; |
| int open; |
| int needs_write; |
| |
| struct mtd_info_user info; |
| |
| size_t raw_size; |
| size_t size; |
| off_t offset; |
| unsigned long flags; |
| void *writebuf; |
| |
| struct cdev cdev; |
| |
| struct list_head list; |
| }; |
| |
| static ssize_t nand_bb_read(struct cdev *cdev, void *buf, size_t count, |
| unsigned long offset, ulong flags) |
| { |
| struct nand_bb *bb = cdev->priv; |
| struct cdev *parent = bb->cdev_parent; |
| int ret, bytes = 0, now; |
| |
| debug("%s %d %d\n", __func__, offset, count); |
| |
| while(count) { |
| ret = cdev_ioctl(parent, MEMGETBADBLOCK, (void *)bb->offset); |
| if (ret < 0) |
| return ret; |
| |
| if (ret) { |
| printf("skipping bad block at 0x%08lx\n", bb->offset); |
| bb->offset += bb->info.erasesize; |
| continue; |
| } |
| |
| now = min(count, (size_t)(bb->info.erasesize - |
| (bb->offset % bb->info.erasesize))); |
| ret = cdev_read(parent, buf, now, bb->offset, 0); |
| if (ret < 0) |
| return ret; |
| buf += now; |
| count -= now; |
| bb->offset += now; |
| bytes += now; |
| }; |
| |
| return bytes; |
| } |
| |
| /* Must be a multiple of the largest NAND page size */ |
| #define BB_WRITEBUF_SIZE 4096 |
| |
| #ifdef CONFIG_NAND_WRITE |
| static int nand_bb_write_buf(struct nand_bb *bb, size_t count) |
| { |
| int ret, now; |
| struct cdev *parent = bb->cdev_parent; |
| void *buf = bb->writebuf; |
| int cur_ofs = bb->offset & ~(BB_WRITEBUF_SIZE - 1); |
| |
| while (count) { |
| ret = cdev_ioctl(parent, MEMGETBADBLOCK, (void *)cur_ofs); |
| if (ret < 0) |
| return ret; |
| |
| if (ret) { |
| debug("skipping bad block at 0x%08x\n", cur_ofs); |
| bb->offset += bb->info.erasesize; |
| cur_ofs += bb->info.erasesize; |
| continue; |
| } |
| |
| now = min(count, (size_t)(bb->info.erasesize)); |
| ret = cdev_write(parent, buf, now, cur_ofs, 0); |
| if (ret < 0) |
| return ret; |
| buf += now; |
| count -= now; |
| cur_ofs += now; |
| }; |
| |
| return 0; |
| } |
| |
| static ssize_t nand_bb_write(struct cdev *cdev, const void *buf, size_t count, |
| unsigned long offset, ulong flags) |
| { |
| struct nand_bb *bb = cdev->priv; |
| int bytes = count, now, wroffs, ret; |
| |
| debug("%s offset: 0x%08x count: 0x%08x\n", __func__, offset, count); |
| |
| while (count) { |
| wroffs = bb->offset % BB_WRITEBUF_SIZE; |
| now = min((int)count, BB_WRITEBUF_SIZE - wroffs); |
| memcpy(bb->writebuf + wroffs, buf, now); |
| |
| if (wroffs + now == BB_WRITEBUF_SIZE) { |
| bb->needs_write = 0; |
| ret = nand_bb_write_buf(bb, BB_WRITEBUF_SIZE); |
| if (ret) |
| return ret; |
| } else { |
| bb->needs_write = 1; |
| } |
| |
| bb->offset += now; |
| count -= now; |
| buf += now; |
| } |
| |
| return bytes; |
| } |
| |
| static int nand_bb_erase(struct cdev *cdev, size_t count, unsigned long offset) |
| { |
| struct nand_bb *bb = cdev->priv; |
| |
| if (offset != 0) { |
| printf("can only erase from beginning of device\n"); |
| return -EINVAL; |
| } |
| |
| return cdev_erase(bb->cdev_parent, bb->raw_size, 0); |
| } |
| #endif |
| |
| static int nand_bb_open(struct cdev *cdev, unsigned long flags) |
| { |
| struct nand_bb *bb = cdev->priv; |
| |
| if (bb->open) |
| return -EBUSY; |
| |
| bb->flags = flags; |
| bb->open = 1; |
| bb->offset = 0; |
| bb->needs_write = 0; |
| bb->writebuf = xmalloc(BB_WRITEBUF_SIZE); |
| |
| return 0; |
| } |
| |
| static int nand_bb_close(struct cdev *cdev) |
| { |
| struct nand_bb *bb = cdev->priv; |
| |
| #ifdef CONFIG_NAND_WRITE |
| if (bb->needs_write) |
| nand_bb_write_buf(bb, bb->offset % BB_WRITEBUF_SIZE); |
| #endif |
| bb->open = 0; |
| free(bb->writebuf); |
| |
| return 0; |
| } |
| |
| static int nand_bb_calc_size(struct nand_bb *bb) |
| { |
| ulong pos = 0; |
| int ret; |
| |
| while (pos < bb->raw_size) { |
| ret = cdev_ioctl(bb->cdev_parent, MEMGETBADBLOCK, (void *)pos); |
| if (ret < 0) |
| return ret; |
| if (!ret) |
| bb->cdev.size += bb->info.erasesize; |
| |
| pos += bb->info.erasesize; |
| } |
| |
| return 0; |
| } |
| |
| static off_t nand_bb_lseek(struct cdev *cdev, off_t __offset) |
| { |
| struct nand_bb *bb = cdev->priv; |
| unsigned long raw_pos = 0; |
| uint32_t offset = __offset; |
| int ret; |
| |
| /* lseek only in readonly mode */ |
| if (bb->flags & O_ACCMODE) |
| return -ENOSYS; |
| while (raw_pos < bb->raw_size) { |
| off_t now = min(offset, bb->info.erasesize); |
| |
| ret = cdev_ioctl(bb->cdev_parent, MEMGETBADBLOCK, (void *)raw_pos); |
| if (ret < 0) |
| return ret; |
| if (!ret) { |
| offset -= now; |
| raw_pos += now; |
| } else { |
| raw_pos += bb->info.erasesize; |
| } |
| |
| if (!offset) { |
| bb->offset = raw_pos; |
| return __offset; |
| } |
| } |
| |
| return -EINVAL; |
| } |
| |
| static struct file_operations nand_bb_ops = { |
| .open = nand_bb_open, |
| .close = nand_bb_close, |
| .read = nand_bb_read, |
| .lseek = nand_bb_lseek, |
| #ifdef CONFIG_NAND_WRITE |
| .write = nand_bb_write, |
| .erase = nand_bb_erase, |
| #endif |
| }; |
| |
| static LIST_HEAD(bb_list); |
| |
| /** |
| * Add a bad block aware device ontop of another (NAND) device |
| * @param[in] dev The device to add a partition on |
| * @param[in] name Partition name (can be obtained with devinfo command) |
| * @return The device representing the new partition. |
| */ |
| int dev_add_bb_dev(char *path, const char *name) |
| { |
| struct nand_bb *bb; |
| int ret = -ENOMEM; |
| |
| bb = xzalloc(sizeof(*bb)); |
| |
| bb->cdev_parent = cdev_open(path, O_RDWR); |
| if (!bb->cdev_parent) |
| goto out1; |
| |
| if (name) { |
| strcpy(bb->cdevname, name); |
| } else { |
| strcpy(bb->cdevname, path); |
| strcat(bb->cdevname, ".bb"); |
| } |
| |
| bb->cdev.name = bb->cdevname; |
| |
| bb->raw_size = bb->cdev_parent->size; |
| |
| ret = cdev_ioctl(bb->cdev_parent, MEMGETINFO, &bb->info); |
| if (ret) |
| goto out4; |
| |
| nand_bb_calc_size(bb); |
| bb->cdev.ops = &nand_bb_ops; |
| bb->cdev.priv = bb; |
| |
| ret = devfs_create(&bb->cdev); |
| if (ret) |
| goto out4; |
| |
| list_add_tail(&bb->list, &bb_list); |
| |
| return 0; |
| |
| out4: |
| cdev_close(bb->cdev_parent); |
| out1: |
| free(bb); |
| return ret; |
| } |
| |
| int dev_remove_bb_dev(const char *name) |
| { |
| struct nand_bb *bb; |
| |
| list_for_each_entry(bb, &bb_list, list) { |
| if (!strcmp(bb->cdev.name, name)) { |
| devfs_remove(&bb->cdev); |
| cdev_close(bb->cdev_parent); |
| free(bb); |
| return 0; |
| } |
| } |
| |
| return -ENODEV; |
| } |