From: Mona Anonuevo <manonu...@micron.com> This patch adds support for a generic spinand framework(spinand_mtd.c). This frameowrk can be used for other spi based flash devices also. The idea is to have a common model under drivers/mtd, as also present for other no spi devices(there is a generic framework and device part simply attaches itself to it.)
The generic frework will be used later by me for a SPI based spansion S25FL256 device. The patch also contains a micron driver attaching itself to generic framework. Signed-off-by: Mona Anonuevo <manonu...@micron.com> Signed-off-by: Tuan Nguyen <tqngu...@micron.com> Signed-off-by: Sourav Poddar <sourav.pod...@ti.com> ---- [I picked this as a standalone patch, can split it into generic and device part based on community feedback.] drivers/mtd/Kconfig | 2 + drivers/mtd/Makefile | 2 + drivers/mtd/spinand/Kconfig | 24 ++ drivers/mtd/spinand/Makefile | 10 + drivers/mtd/spinand/spinand_lld.c | 776 +++++++++++++++++++++++++++++++++++++ drivers/mtd/spinand/spinand_mtd.c | 690 +++++++++++++++++++++++++++++++++ include/linux/mtd/spinand.h | 155 ++++++++ 7 files changed, 1659 insertions(+), 0 deletions(-) create mode 100644 drivers/mtd/spinand/Kconfig create mode 100644 drivers/mtd/spinand/Makefile create mode 100644 drivers/mtd/spinand/spinand_lld.c create mode 100644 drivers/mtd/spinand/spinand_mtd.c create mode 100644 include/linux/mtd/spinand.h diff --git a/drivers/mtd/Kconfig b/drivers/mtd/Kconfig index 5fab4e6..c9e6c60 100644 --- a/drivers/mtd/Kconfig +++ b/drivers/mtd/Kconfig @@ -318,6 +318,8 @@ source "drivers/mtd/nand/Kconfig" source "drivers/mtd/onenand/Kconfig" +source "drivers/mtd/spinand/Kconfig" + source "drivers/mtd/lpddr/Kconfig" source "drivers/mtd/ubi/Kconfig" diff --git a/drivers/mtd/Makefile b/drivers/mtd/Makefile index 4cfb31e..cce68db 100644 --- a/drivers/mtd/Makefile +++ b/drivers/mtd/Makefile @@ -32,4 +32,6 @@ inftl-objs := inftlcore.o inftlmount.o obj-y += chips/ lpddr/ maps/ devices/ nand/ onenand/ tests/ +obj-y += spinand/ + obj-$(CONFIG_MTD_UBI) += ubi/ diff --git a/drivers/mtd/spinand/Kconfig b/drivers/mtd/spinand/Kconfig new file mode 100644 index 0000000..38c739f --- /dev/null +++ b/drivers/mtd/spinand/Kconfig @@ -0,0 +1,24 @@ +# +# linux/drivers/mtd/spinand/Kconfig +# + +menuconfig MTD_SPINAND + tristate "SPINAND Device Support" + depends on MTD + help + This enables support for accessing Micron SPI NAND flash + devices. + +if MTD_SPINAND + +config MTD_SPINAND_ONDIEECC + bool "Use SPINAND internal ECC" + help + Internel ECC + +config MTD_SPINAND_SWECC + bool "Use software ECC" + depends on MTD_NAND + help + software ECC +endif diff --git a/drivers/mtd/spinand/Makefile b/drivers/mtd/spinand/Makefile new file mode 100644 index 0000000..355e726 --- /dev/null +++ b/drivers/mtd/spinand/Makefile @@ -0,0 +1,10 @@ +# +# Makefile for the SPI NAND MTD +# + +# Core functionality. +obj-$(CONFIG_MTD_SPINAND) += spinand.o + +spinand-objs := spinand_mtd.o spinand_lld.o + + diff --git a/drivers/mtd/spinand/spinand_lld.c b/drivers/mtd/spinand/spinand_lld.c new file mode 100644 index 0000000..9f53737 --- /dev/null +++ b/drivers/mtd/spinand/spinand_lld.c @@ -0,0 +1,776 @@ +/* +spinand_lld.c + +Copyright (c) 2009-2010 Micron Technology, Inc. + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +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. + +*/ + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/device.h> +#include <linux/interrupt.h> +#include <linux/mutex.h> +#include <linux/math64.h> + +#include <linux/mtd/mtd.h> +#include <linux/mtd/partitions.h> +#include <linux/mtd/spinand.h> + +#include <linux/spi/spi.h> +#include <linux/spi/flash.h> + +#define mu_spi_nand_driver_version "Beagle-MTD_01.00_Linux2.6.33_20100507" +#define SPI_NAND_MICRON_DRIVER_KEY 0x1233567 + +/****************************************************************************/ + +/** + OOB area specification layout: Total 32 available free bytes. +*/ +static struct nand_ecclayout spinand_oob_64 = { + .eccbytes = 24, + .eccpos = { + 1, 2, 3, 4, 5, 6, + 17, 18, 19, 20, 21, 22, + 33, 34, 35, 36, 37, 38, + 49, 50, 51, 52, 53, 54, }, + .oobavail = 32, + .oobfree = { + {.offset = 8, + .length = 8}, + {.offset = 24, + .length = 8}, + {.offset = 40, + .length = 8}, + {.offset = 56, + .length = 8}, } +}; +/** + * spinand_cmd - to process a command to send to the SPI Nand + * + * Description: + * Set up the command buffer to send to the SPI controller. + * The command buffer has to initized to 0 + */ +int spinand_cmd(struct spi_device *spi, struct spinand_cmd *cmd) +{ + int ret; + struct spi_message message; + struct spi_transfer x[4]; + u8 dummy = 0xff; + + spi_message_init(&message); + memset(x, 0, sizeof(x)); + + x[0].len = 1; + x[0].tx_buf = &cmd->cmd; + spi_message_add_tail(&x[0], &message); + + if (cmd->n_addr) { + x[1].len = cmd->n_addr; + x[1].tx_buf = cmd->addr; + spi_message_add_tail(&x[1], &message); + } + + if (cmd->n_dummy) { + x[2].len = cmd->n_dummy; + x[2].tx_buf = &dummy; + spi_message_add_tail(&x[2], &message); + } + + if (cmd->n_tx) { + x[3].len = cmd->n_tx; + x[3].tx_buf = cmd->tx_buf; + spi_message_add_tail(&x[3], &message); + } + + if (cmd->n_rx) { + x[3].len = cmd->n_rx; + x[3].rx_buf = cmd->rx_buf; + spi_message_add_tail(&x[3], &message); + } + + ret = spi_sync(spi, &message); + + return ret; +} + +/** + * spinand_reset- send reset command "0xff" to the Nand device + * + * Description: + * Reset the SPI Nand with the reset command 0xff +*/ +static int spinand_reset(struct spi_device *spi_nand) +{ + struct spinand_cmd cmd = {0}; + + cmd.cmd = CMD_RESET; + + return spinand_cmd(spi_nand, &cmd); +} + +/** + * spinand_read_id- Read SPI Nand ID + * + * Description: + * Read ID: read two ID bytes from the SPI Nand device +*/ +static int spinand_read_id(struct spi_device *spi_nand, u8 *id) +{ + struct spinand_cmd cmd = {0}; + ssize_t retval; + + cmd.cmd = CMD_READ_ID; + cmd.n_dummy = 1; + cmd.n_rx = 2; + cmd.rx_buf = id; + + retval = spinand_cmd(spi_nand, &cmd); + + if (retval != 0) { + dev_err(&spi_nand->dev, "error %d reading id\n", + (int) retval); + return retval; + } + + return 0; +} + +/** + * spinand_lock_block- send write register 0x1f command to the Nand device + * + * Description: + * After power up, all the Nand blocks are locked. This function allows + * one to unlock the blocks, and so it can be wriiten or erased. +*/ +static int spinand_lock_block(struct spi_device *spi_nand, + struct spinand_info *info, u8 lock) +{ + struct spinand_cmd cmd = {0}; + ssize_t retval; + + cmd.cmd = CMD_WRITE_REG; + cmd.n_addr = 1; + cmd.addr[0] = REG_BLOCK_LOCK; + cmd.n_tx = 1; + cmd.tx_buf = &lock; + + retval = spinand_cmd(spi_nand, &cmd); + + if (retval != 0) { + dev_err(&spi_nand->dev, "error %d lock block\n", + (int) retval); + return retval; + } + + return 0; +} + +/** + * spinand_read_status- send command 0xf to the SPI Nand status register + * + * Description: + * After read, write, or erase, the Nand device is expected to + set the busy status. + * This function is to allow reading the status of the command: + read, write, and erase. + * Once the status turns to be ready, the other status bits also + are valid status bits. +*/ +static int spinand_read_status(struct spi_device *spi_nand, + struct spinand_info *info, u8 *status) +{ + struct spinand_cmd cmd = {0}; + ssize_t retval; + + cmd.cmd = CMD_READ_REG; + cmd.n_addr = 1; + cmd.addr[0] = REG_STATUS; + cmd.n_rx = 1; + cmd.rx_buf = status; + + retval = spinand_cmd(spi_nand, &cmd); + + if (retval != 0) { + dev_err(&spi_nand->dev, "error %d reading status register\n", + (int) retval); + return retval; + } + + return 0; +} + +/** + * spinand_get_otp- send command 0xf to read the SPI Nand OTP register + * + * Description: + * There is one bit( bit 0x10 ) to set or to clear the internal ECC. + * Enable chip internal ECC, set the bit to 1 + * Disable chip internal ECC, clear the bit to 0 + */ +static int spinand_get_otp(struct spi_device *spi_nand, + struct spinand_info *info, u8 *otp) +{ + struct spinand_cmd cmd = {0}; + ssize_t retval; + + cmd.cmd = CMD_READ_REG; + cmd.n_addr = 1; + cmd.addr[0] = REG_OTP; + cmd.n_rx = 1; + cmd.rx_buf = otp; + + retval = spinand_cmd(spi_nand, &cmd); + + if (retval != 0) { + dev_err(&spi_nand->dev, "error %d get otp\n", + (int) retval); + return retval; + } + + return 0; +} + +/** + * spinand_set_otp- send command 0x1f to write the SPI Nand OTP register + * + * Description: + * There is one bit( bit 0x10 ) to set or to clear the internal ECC. + * Enable chip internal ECC, set the bit to 1 + * Disable chip internal ECC, clear the bit to 0 +*/ +static int spinand_set_otp(struct spi_device *spi_nand, + struct spinand_info *info, u8 *otp) +{ + struct spinand_cmd cmd = {0}; + ssize_t retval; + + cmd.cmd = CMD_WRITE_REG; + cmd.n_addr = 1; + cmd.addr[0] = REG_OTP; + cmd.n_tx = 1; + cmd.tx_buf = otp; + + retval = spinand_cmd(spi_nand, &cmd); + + if (retval != 0) { + dev_err(&spi_nand->dev, "error %d set otp\n", + (int) retval); + return retval; + } + + return 0; +} + +/** + * sspinand_enable_ecc- send command 0x1f to write the SPI Nand OTP register + * + * Description: + * There is one bit( bit 0x10 ) to set or to clear the internal ECC. + * Enable chip internal ECC, set the bit to 1 + * Disable chip internal ECC, clear the bit to 0 +*/ +#ifdef CONFIG_MTD_SPINAND_ONDIEECC +static int spinand_enable_ecc(struct spi_device *spi_nand, + struct spinand_info *info) +{ + ssize_t retval; + u8 otp = 0; + + retval = spinand_get_otp(spi_nand, info, &otp); + + if ((otp & OTP_ECC_MASK) == OTP_ECC_MASK) { + return 0; + } else { + otp |= OTP_ECC_MASK; + retval = spinand_set_otp(spi_nand, info, &otp); + retval = spinand_get_otp(spi_nand, info, &otp); + return retval; + } +} +#else +static int spinand_disable_ecc(struct spi_device *spi_nand, + struct spinand_info *info) +{ + ssize_t retval; + u8 otp = 0; + + retval = spinand_get_otp(spi_nand, info, &otp); + + if ((otp & OTP_ECC_MASK) == OTP_ECC_MASK) { + otp &= ~OTP_ECC_MASK; + retval = spinand_set_otp(spi_nand, info, &otp); + retval = spinand_get_otp(spi_nand, info, &otp); + return retval; + } else { + return 0; + } +} +#endif + +/** + * sspinand_write_enable- send command 0x06 to enable write or erase the Nand cells + * + * Description: + * Before write and erase the Nand cells, the write enable has to be set. + * After the write or erase, the write enable bit is automatically + cleared( status register bit 2 ) + * Set the bit 2 of the status register has the same effect +*/ +static int spinand_write_enable(struct spi_device *spi_nand, + struct spinand_info *info) +{ + struct spinand_cmd cmd = {0}; + + cmd.cmd = CMD_WR_ENABLE; + + return spinand_cmd(spi_nand, &cmd); +} + +static int spinand_read_page_to_cache(struct spi_device *spi_nand, + struct spinand_info *info, u16 page_id) +{ + struct spinand_cmd cmd = {0}; + u16 row; + + row = page_id; + + cmd.cmd = CMD_READ; + cmd.n_addr = 3; + cmd.addr[1] = (u8)((row & 0xff00) >> 8); + cmd.addr[2] = (u8)(row & 0x00ff); + + return spinand_cmd(spi_nand, &cmd); +} + +/** + * spinand_read_from_cache- send command 0x03 to read out the data from the + cache register( 2112 bytes max ) + * + * Description: + * The read can specify 1 to 2112 bytes of data read at the + coresponded locations. + * No tRd delay. +*/ +static int spinand_read_from_cache(struct spi_device *spi_nand, + struct spinand_info *info, u16 byte_id, u16 len, u8 *rbuf) +{ + struct spinand_cmd cmd = {0}; + u16 column; + + column = byte_id; + + cmd.cmd = CMD_READ_RDM; + cmd.n_addr = 2; + cmd.addr[0] = (u8)((column&0xff00)>>8); + cmd.addr[1] = (u8)(column&0x00ff); + cmd.n_dummy = 1; + cmd.n_rx = len; + cmd.rx_buf = rbuf; + + return spinand_cmd(spi_nand, &cmd); +} + +/** + * spinand_read_page-to read a page with: + * @page_id: the physical page number + * @offset: the location from 0 to 2111 + * @len: number of bytes to read + * @rbuf: read buffer to hold @len bytes + * + * Description: + * The read icludes two commands to the Nand: 0x13 and 0x03 commands + * Poll to read status to wait for tRD time. + */ +static int spinand_read_page(struct spi_device *spi_nand, + struct spinand_info *info, u16 page_id, u16 offset, + u16 len, u8 *rbuf) +{ + ssize_t retval; + u8 status = 0; + + retval = spinand_read_page_to_cache(spi_nand, info, page_id); + + while (1) { + retval = spinand_read_status(spi_nand, info, &status); + if (retval < 0) { + dev_err(&spi_nand->dev, "error %d reading status register\n", + (int) retval); + return retval; + } + + if ((status & STATUS_OIP_MASK) == STATUS_READY) { + if ((status & STATUS_ECC_MASK) == STATUS_ECC_ERROR) { + dev_err(&spi_nand->dev, + "ecc error, page=%d\n", page_id); + } + break; + } + } + + retval = spinand_read_from_cache(spi_nand, info, offset, len, rbuf); + return 0; +} + +/** + * spinand_program_data_to_cache--to write a page to cache with: + * @byte_id: the location to write to the cache + * @len: number of bytes to write + * @rbuf: read buffer to hold @len bytes + * + * Description: + * The write command used here is 0x84--indicating that the cache + is not cleared first. + * Since it is writing the data to cache, there is no tPROG time. + */ +static int spinand_program_data_to_cache(struct spi_device *spi_nand, + struct spinand_info *info, u16 byte_id, u16 len, u8 *wbuf) +{ + struct spinand_cmd cmd = {0}; + u16 column; + + column = byte_id; + + cmd.cmd = CMD_PROG_PAGE_CLRCACHE; + cmd.n_addr = 2; + cmd.addr[0] = (u8)((column & 0xff00) >> 8); + cmd.addr[1] = (u8)(column & 0x00ff); + cmd.n_tx = len; + cmd.tx_buf = wbuf; + + return spinand_cmd(spi_nand, &cmd); +} + +/** + * spinand_program_execute--to write a page from cache to the Nand array with: + * @page_id: the physical page location to write the page. + * + * Description: + * The write command used here is 0x10--indicating the cache is + writing to the Nand array. + * Need to wait for tPROG time to finish the transaction. + */ +static int spinand_program_execute(struct spi_device *spi_nand, + struct spinand_info *info, u16 page_id) +{ + struct spinand_cmd cmd = {0}; + u16 row; + + row = page_id; + + cmd.cmd = CMD_PROG_PAGE_EXC; + cmd.n_addr = 3; + cmd.addr[1] = (u8)((row & 0xff00) >> 8); + cmd.addr[2] = (u8)(row & 0x00ff); + + return spinand_cmd(spi_nand, &cmd); +} + +/** + * spinand_program_page--to write a page with: + * @page_id: the physical page location to write the page. + * @offset: the location from the cache starting from 0 to 2111 + * @len: the number of bytes to write + * @wbuf: the buffer to hold the number of bytes + * + * Description: + * The commands used here are 0x06, 0x84, and 0x10--indicating that + the write enable is first + * sent, the write cache command, and the write execute command + * Poll to wait for the tPROG time to finish the transaction. + */ +static int spinand_program_page(struct spi_device *spi_nand, + struct spinand_info *info, u16 page_id, u16 offset, + u16 len, u8 *wbuf) +{ + ssize_t retval; + u8 status = 0; + + retval = spinand_write_enable(spi_nand, info); + + retval = spinand_program_data_to_cache(spi_nand, info, offset, + len, wbuf); + + retval = spinand_program_execute(spi_nand, info, page_id); + + while (1) { + retval = spinand_read_status(spi_nand, info, &status); + if (retval < 0) { + dev_err(&spi_nand->dev, + "error %d reading status register\n", + (int) retval); + return retval; + } + + if ((status & STATUS_OIP_MASK) == STATUS_READY) { + if ((status & STATUS_P_FAIL_MASK) == STATUS_P_FAIL) { + dev_err(&spi_nand->dev, + "program error, page=%d\n", page_id); + return -1; + } + } else { + break; + } + } + return 0; +} + +/** + * spinand_erase_block_erase--to erase a page with: + * @block_id: the physical block location to erase. + * + * Description: + * The command used here is 0xd8--indicating an erase +command to erase one block--64 pages + * Need to wait for tERS. + */ +static int spinand_erase_block_erase(struct spi_device *spi_nand, + struct spinand_info *info, u16 block_id) +{ + struct spinand_cmd cmd = {0}; + u16 row; + + row = block_id << 6; + cmd.cmd = CMD_ERASE_BLK; + cmd.n_addr = 3; + cmd.addr[1] = (u8)((row & 0xff00) >> 8); + cmd.addr[2] = (u8)(row & 0x00ff); + + return spinand_cmd(spi_nand, &cmd); +} + +/** + * spinand_erase_block--to erase a page with: + * @block_id: the physical block location to erase. + * + * Description: + * The commands used here are 0x06 and 0xd8--indicating an erase + command to erase one block--64 pages + * It will first to enable the write enable bit ( 0x06 command ), + and then send the 0xd8 erase command + * Poll to wait for the tERS time to complete the tranaction. + */ +static int spinand_erase_block(struct spi_device *spi_nand, + struct spinand_info *info, u16 block_id) +{ + ssize_t retval; + u8 status = 0; + + retval = spinand_write_enable(spi_nand, info); + + retval = spinand_erase_block_erase(spi_nand, info, block_id); + + while (1) { + retval = spinand_read_status(spi_nand, info, &status); + if (retval < 0) { + dev_err(&spi_nand->dev, + "error %d reading status register\n", + (int) retval); + return retval; + } + + if ((status & STATUS_OIP_MASK) == STATUS_READY) { + if ((status & STATUS_E_FAIL_MASK) == STATUS_E_FAIL) { + dev_err(&spi_nand->dev, + "erase error, block=%d\n", block_id); + return -1; + } else { + break; + } + } + } + + return 0; +} + +/* + * spinand_get_info: get NAND info, from read id or const value + * Description: + * To set up the device parameters. + */ +static int spinand_get_info(struct spi_device *spi_nand, + struct spinand_info *info, u8 *id) +{ + if (id[0] == 0x2C && (id[1] == 0x11 || + id[1] == 0x12 || id[1] == 0x13)) { + info->mid = id[0]; + info->did = id[1]; + info->name = "MT29F1G01ZAC"; + info->nand_size = (1024 * 64 * 2112); + info->usable_size = (1024 * 64 * 2048); + info->block_size = (2112*64); + info->block_main_size = (2048*64); + info->block_num_per_chip = 1024; + info->page_size = 2112; + info->page_main_size = 2048; + info->page_spare_size = 64; + info->page_num_per_block = 64; + + info->block_shift = 17; + info->block_mask = 0x1ffff; + + info->page_shift = 11; + info->page_mask = 0x7ff; + + info->ecclayout = &spinand_oob_64; + } + return 0; +} + +/** + * spinand_probe - [spinand Interface] + * @spi_nand: registered device driver. + * + * Description: + * To set up the device driver parameters to make the device available. +*/ +static int spinand_probe(struct spi_device *spi_nand) +{ + ssize_t retval; + struct mtd_info *mtd; + struct spinand_chip *chip; + struct spinand_info *info; + struct flash_platform_data *data; + struct mtd_part_parser_data ppdata; + u8 id[2] = {0}; + + retval = spinand_reset(spi_nand); + retval = spinand_reset(spi_nand); + retval = spinand_read_id(spi_nand, (u8 *)&id); + if (id[0] == 0 && id[1] == 0) { + pr_info(KERN_INFO "SPINAND: read id error! 0x%02x, 0x%02x!\n", + id[0], id[1]); + return 0; + } + + data = spi_nand->dev.platform_data; + info = kzalloc(sizeof(struct spinand_info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + retval = spinand_get_info(spi_nand, info, (u8 *)&id); + pr_info(KERN_INFO "SPINAND: 0x%02x, 0x%02x, %s\n", + id[0], id[1], info->name); + pr_info(KERN_INFO "%s\n", mu_spi_nand_driver_version); + retval = spinand_lock_block(spi_nand, info, BL_ALL_UNLOCKED); + +#ifdef CONFIG_MTD_SPINAND_ONDIEECC + retval = spinand_enable_ecc(spi_nand, info); +#else + retval = spinand_disable_ecc(spi_nand, info); +#endif + + ppdata.of_node = spi_nand->dev.of_node; + + chip = kzalloc(sizeof(struct spinand_chip), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + chip->spi_nand = spi_nand; + chip->info = info; + chip->reset = spinand_reset; + chip->read_id = spinand_read_id; + chip->read_page = spinand_read_page; + chip->program_page = spinand_program_page; + chip->erase_block = spinand_erase_block; + + chip->buf = kzalloc(info->page_size, GFP_KERNEL); + if (!chip->buf) + return -ENOMEM; + + chip->oobbuf = kzalloc(info->ecclayout->oobavail, GFP_KERNEL); + if (!chip->oobbuf) + return -ENOMEM; + + mtd = kzalloc(sizeof(struct mtd_info), GFP_KERNEL); + if (!mtd) + return -ENOMEM; + + dev_set_drvdata(&spi_nand->dev, mtd); + + mtd->priv = chip; + + retval = spinand_mtd(mtd); + + return mtd_device_parse_register(mtd, NULL, &ppdata, + data ? data->parts : NULL, + data ? data->nr_parts : 0); +} + +/** + * __devexit spinand_remove--Remove the device driver + * @spi: the spi device. + * + * Description: + * To remove the device driver parameters and free up allocated memories. + */ +static int spinand_remove(struct spi_device *spi) +{ + struct mtd_info *mtd; + struct spinand_chip *chip; + + pr_debug("%s: remove\n", dev_name(&spi->dev)); + + mtd = dev_get_drvdata(&spi->dev); + + mtd_device_unregister(mtd); + + chip = mtd->priv; + + kfree(chip->info); + kfree(chip->buf); + kfree(chip->oobbuf); + kfree(chip); + kfree(mtd); + + return 0; +} + +/** + * Device name structure description +*/ +static struct spi_driver spinand_driver = { + .driver = { + .name = "spi_nand", + .bus = &spi_bus_type, + .owner = THIS_MODULE, + }, + + .probe = spinand_probe, + .remove = spinand_remove, +}; + +/** + * Device driver registration +*/ +static int __init spinand_init(void) +{ + return spi_register_driver(&spinand_driver); +} + +/** + * unregister Device driver. +*/ +static void __exit spinand_exit(void) +{ + spi_unregister_driver(&spinand_driver); +} + +module_init(spinand_init); +module_exit(spinand_exit); + + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Henry Pan<hs...@micron.com>"); +MODULE_DESCRIPTION("SPI NAND driver code"); diff --git a/drivers/mtd/spinand/spinand_mtd.c b/drivers/mtd/spinand/spinand_mtd.c new file mode 100644 index 0000000..8bfff86 --- /dev/null +++ b/drivers/mtd/spinand/spinand_mtd.c @@ -0,0 +1,690 @@ +/* +spinand_mtd.c + +Copyright (c) 2009-2010 Micron Technology, Inc. + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +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. +*/ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/sched.h> +#include <linux/delay.h> +#include <linux/interrupt.h> +#include <linux/jiffies.h> +#include <linux/mtd/mtd.h> +#include <linux/mtd/partitions.h> +#include <linux/mtd/spinand.h> +#include <linux/mtd/nand_ecc.h> + +/** + * spinand_get_device - [GENERIC] Get chip for selected access + * @param mtd MTD device structure + * @param new_state the state which is requested + * + * Get the device and lock it for exclusive access + */ +#define mu_spi_nand_driver_version "Beagle-MTD_01.00_Linux2.6.33_20100507" + +static int spinand_get_device(struct mtd_info *mtd, int new_state) +{ + struct spinand_chip *this = mtd->priv; + DECLARE_WAITQUEUE(wait, current); + + /* + * Grab the lock and see if the device is available + */ + while (1) { + spin_lock(&this->chip_lock); + if (this->state == FL_READY) { + this->state = new_state; + spin_unlock(&this->chip_lock); + break; + } + if (new_state == FL_PM_SUSPENDED) { + spin_unlock(&this->chip_lock); + return (this->state == FL_PM_SUSPENDED) ? 0 : -EAGAIN; + } + set_current_state(TASK_UNINTERRUPTIBLE); + add_wait_queue(&this->wq, &wait); + spin_unlock(&this->chip_lock); + schedule(); + remove_wait_queue(&this->wq, &wait); + } + return 0; +} + +/** + * spinand_release_device - [GENERIC] release chip + * @param mtd MTD device structure + * + * Deselect, release chip lock and wake up anyone waiting on the device + */ +static void spinand_release_device(struct mtd_info *mtd) +{ + struct spinand_chip *this = mtd->priv; + + /* Release the chip */ + spin_lock(&this->chip_lock); + this->state = FL_READY; + wake_up(&this->wq); + spin_unlock(&this->chip_lock); +} + +#ifdef CONFIG_MTD_SPINAND_SWECC +static void spinand_calculate_ecc(struct mtd_info *mtd) +{ + int i; + int eccsize = 512; + int eccbytes = 3; + int eccsteps = 4; + int ecctotal = 12; + struct spinand_chip *chip = mtd->priv; + struct spinand_info *info = chip->info; + unsigned char *p = chip->buf; + + for (i = 0; eccsteps; eccsteps--, i += eccbytes, p += eccsize) + __nand_calculate_ecc(p, eccsize, &chip->ecc_calc[i]); + + for (i = 0; i < ecctotal; i++) + chip->buf[info->page_main_size + + info->ecclayout->eccpos[i]] = chip->ecc_calc[i]; +} + +static int spinand_correct_data(struct mtd_info *mtd) +{ + int i; + int eccsize = 512; + int eccbytes = 3; + int eccsteps = 4; + int ecctotal = 12; + struct spinand_chip *chip = mtd->priv; + struct spinand_info *info = chip->info; + unsigned char *p = chip->buf; + int errcode = 0; + + for (i = 0; eccsteps; eccsteps--, i += eccbytes, p += eccsize) + __nand_calculate_ecc(p, eccsize, &chip->ecc_calc[i]); + + for (i = 0; i < ecctotal; i++) + chip->ecc_code[i] = chip->buf[info->page_main_size + + info->ecclayout->eccpos[i]]; + + for (i = 0; eccsteps; eccsteps--, i += eccbytes, p += eccsize) { + int stat; + + stat = __nand_correct_data(p, &chip->ecc_code[i], + &chip->ecc_calc[i], eccsize); + if (stat < 0) + errcode = -1; + else if (stat == 1) + errcode = 1; + } + return errcode; +} +#endif + +static int spinand_read_ops(struct mtd_info *mtd, loff_t from, + struct mtd_oob_ops *ops) +{ + struct spinand_chip *chip = mtd->priv; + struct spi_device *spi_nand = chip->spi_nand; + struct spinand_info *info = chip->info; + int page_id, page_offset, page_num, oob_num; + + int count; + int main_ok, main_left, main_offset; + int oob_ok, oob_left; + + signed int retval; + signed int errcode = 0; + + if (!chip->buf) + return -1; + + page_id = from >> info->page_shift; + + /* for main data */ + page_offset = from & info->page_mask; + page_num = (page_offset + ops->len + + info->page_main_size - 1) / info->page_main_size; + + /* for oob */ + if (info->ecclayout->oobavail) + oob_num = (ops->ooblen + + info->ecclayout->oobavail - 1) / info->ecclayout->oobavail; + else + oob_num = 0; + + count = 0; + + main_left = ops->len; + main_ok = 0; + main_offset = page_offset; + + oob_left = ops->ooblen; + oob_ok = 0; + + while (1) { + if (count < page_num || count < oob_num) { + memset(chip->buf, 0, info->page_size); + retval = chip->read_page(spi_nand, info, + page_id + count, 0, info->page_size, + chip->buf); + if (retval != 0) { + errcode = -1; + pr_info(KERN_INFO + "spinand_read_ops: fail, page=%d!\n", + page_id); + return errcode; + } + } else { + break; + } + if (count < page_num && ops->datbuf) { + int size; + +#ifdef CONFIG_MTD_SPINAND_SWECC + retval = spinand_correct_data(mtd); + if (retval == -1) + pr_info(KERN_INFO + "SWECC uncorrectable error! page=%x\n", + page_id+count); + else if (retval == 1) + pr_info(KERN_INFO + "SWECC 1 bit error, corrected! page=%x\n", + page_id+count); +#endif + + if ((main_offset + main_left) < info->page_main_size) + size = main_left; + else + size = info->page_main_size - main_offset; + + memcpy(ops->datbuf + main_ok, chip->buf, size); + + main_ok += size; + main_left -= size; + main_offset = 0; + ops->retlen = main_ok; + } + + if (count < oob_num && ops->oobbuf && chip->oobbuf) { + int size; + int offset, len, temp; + + /* repack spare to oob */ + memset(chip->oobbuf, 0, info->ecclayout->oobavail); + + temp = 0; + offset = info->ecclayout->oobfree[0].offset; + len = info->ecclayout->oobfree[0].length; + memcpy(chip->oobbuf + temp, + chip->buf + info->page_main_size + offset, len); + + temp += len; + offset = info->ecclayout->oobfree[1].offset; + len = info->ecclayout->oobfree[1].length; + memcpy(chip->oobbuf + temp, + chip->buf + info->page_main_size + offset, len); + + temp += len; + offset = info->ecclayout->oobfree[2].offset; + len = info->ecclayout->oobfree[2].length; + memcpy(chip->oobbuf + temp, + chip->buf + info->page_main_size + offset, len); + + temp += len; + offset = info->ecclayout->oobfree[3].offset; + len = info->ecclayout->oobfree[3].length; + memcpy(chip->oobbuf + temp, + chip->buf + info->page_main_size + offset, len); + + /* copy oobbuf to ops oobbuf */ + if (oob_left < info->ecclayout->oobavail) + size = oob_left; + else + size = info->ecclayout->oobavail; + + memcpy(ops->oobbuf + oob_ok, chip->oobbuf, size); + + oob_ok += size; + oob_left -= size; + + ops->oobretlen = oob_ok; + } + count++; + } + return errcode; +} + +static int spinand_write_ops(struct mtd_info *mtd, loff_t to, + struct mtd_oob_ops *ops) +{ + struct spinand_chip *chip = mtd->priv; + struct spi_device *spi_nand = chip->spi_nand; + struct spinand_info *info = chip->info; + int page_id, page_offset, page_num, oob_num; + + int count; + + int main_ok, main_left, main_offset; + int oob_ok, oob_left; + + signed int retval; + signed int errcode = 0; + + if (!chip->buf) + return -1; + + page_id = to >> info->page_shift; + + /* for main data */ + page_offset = to & info->page_mask; + page_num = (page_offset + ops->len + + info->page_main_size - 1) / info->page_main_size; + + /* for oob */ + if (info->ecclayout->oobavail) + oob_num = (ops->ooblen + + info->ecclayout->oobavail - 1) / info->ecclayout->oobavail; + else + oob_num = 0; + + count = 0; + + main_left = ops->len; + main_ok = 0; + main_offset = page_offset; + + oob_left = ops->ooblen; + oob_ok = 0; + + while (1) { + if (count < page_num || count < oob_num) + memset(chip->buf, 0xFF, info->page_size); + else + break; + + if (count < page_num && ops->datbuf) { + int size; + + if ((main_offset + main_left) < info->page_main_size) + size = main_left; + else + size = info->page_main_size - main_offset; + + memcpy(chip->buf, ops->datbuf + main_ok, size); + + main_ok += size; + main_left -= size; + main_offset = 0; + +#ifdef CONFIG_MTD_SPINAND_SWECC + spinand_calculate_ecc(mtd); +#endif + } + + if (count < oob_num && ops->oobbuf && chip->oobbuf) { + int size; + int offset, len, temp; + + memset(chip->oobbuf, 0xFF, info->ecclayout->oobavail); + + if (oob_left < info->ecclayout->oobavail) + size = oob_left; + else + size = info->ecclayout->oobavail; + + memcpy(chip->oobbuf, ops->oobbuf + oob_ok, size); + + oob_ok += size; + oob_left -= size; + + /* repack oob to spare */ + temp = 0; + offset = info->ecclayout->oobfree[0].offset; + len = info->ecclayout->oobfree[0].length; + memcpy(chip->buf + info->page_main_size + offset, + chip->oobbuf + temp, len); + + temp += len; + offset = info->ecclayout->oobfree[1].offset; + len = info->ecclayout->oobfree[1].length; + memcpy(chip->buf + info->page_main_size + offset, + chip->oobbuf + temp, len); + + temp += len; + offset = info->ecclayout->oobfree[2].offset; + len = info->ecclayout->oobfree[2].length; + memcpy(chip->buf + info->page_main_size + offset, + chip->oobbuf + temp, len); + + temp += len; + offset = info->ecclayout->oobfree[3].offset; + len = info->ecclayout->oobfree[3].length; + memcpy(chip->buf + info->page_main_size + offset, + chip->oobbuf + temp, len); + } + + if (count < page_num || count < oob_num) { + retval = chip->program_page(spi_nand, info, + page_id + count, 0, info->page_size, chip->buf); + if (retval != 0) { + errcode = -1; + pr_err(KERN_INFO "spinand_write_ops: fail, page=%d!\n", page_id); + + return errcode; + } + } + + if (count < page_num && ops->datbuf) + ops->retlen = main_ok; + + if (count < oob_num && ops->oobbuf && chip->oobbuf) + ops->oobretlen = oob_ok; + + count++; + } + return errcode; +} + +static int spinand_read(struct mtd_info *mtd, loff_t from, size_t len, + size_t *retlen, u_char *buf) +{ + struct mtd_oob_ops ops = {0}; + int ret; + + /* Do not allow reads past end of device */ + if ((from + len) > mtd->size) + return -EINVAL; + + if (!len) + return 0; + + spinand_get_device(mtd, FL_READING); + + ops.len = len; + ops.datbuf = buf; + + ret = spinand_read_ops(mtd, from, &ops); + + *retlen = ops.retlen; + + spinand_release_device(mtd); + + return ret; +} + +static int spinand_write(struct mtd_info *mtd, loff_t to, size_t len, + size_t *retlen, const u_char *buf) +{ + struct mtd_oob_ops ops = {0}; + int ret; + + /* Do not allow reads past end of device */ + if ((to + len) > mtd->size) + return -EINVAL; + if (!len) + return 0; + + spinand_get_device(mtd, FL_WRITING); + + ops.len = len; + ops.datbuf = (uint8_t *)buf; + + ret = spinand_write_ops(mtd, to, &ops); + + *retlen = ops.retlen; + + spinand_release_device(mtd); + + return ret; +} + +static int spinand_read_oob(struct mtd_info *mtd, loff_t from, + struct mtd_oob_ops *ops) +{ + int ret; + + spinand_get_device(mtd, FL_READING); + + ret = spinand_read_ops(mtd, from, ops); + + spinand_release_device(mtd); + return ret; +} + +static int spinand_write_oob(struct mtd_info *mtd, loff_t to, + struct mtd_oob_ops *ops) +{ + int ret; + + spinand_get_device(mtd, FL_WRITING); + + ret = spinand_write_ops(mtd, to, ops); + + spinand_release_device(mtd); + return ret; +} + +/** + * spinand_erase - [MTD Interface] erase block(s) + * @param mtd MTD device structure + * @param instr erase instruction + * + * Erase one ore more blocks + */ +static int spinand_erase(struct mtd_info *mtd, struct erase_info *instr) +{ + struct spinand_chip *chip = mtd->priv; + struct spi_device *spi_nand = chip->spi_nand; + struct spinand_info *info = chip->info; + u16 block_id, block_num, count; + signed int retval = 0; + signed int errcode = 0; + + pr_info("spinand_erase: start = 0x%012llx, len = %llu\n", + (unsigned long long)instr->addr, (unsigned long long)instr->len); + + /* check address align on block boundary */ + if (instr->addr & (info->block_main_size - 1)) { + pr_err("spinand_erase: Unaligned address\n"); + return -EINVAL; + } + + if (instr->len & (info->block_main_size - 1)) { + pr_err("spinand_erase: ""Length not block aligned\n"); + return -EINVAL; + } + + /* Do not allow erase past end of device */ + if ((instr->len + instr->addr) > info->usable_size) { + pr_err("spinand_erase: ""Erase past end of device\n"); + return -EINVAL; + } + + instr->fail_addr = MTD_FAIL_ADDR_UNKNOWN; + + /* Grab the lock and see if the device is available */ + spinand_get_device(mtd, FL_ERASING); + + block_id = instr->addr >> info->block_shift; + block_num = instr->len >> info->block_shift; + count = 0; + + while (count < block_num) { + retval = chip->erase_block(spi_nand, info, block_id + count); + + if (retval != 0) { + retval = chip->erase_block(spi_nand, info, + block_id + count); + if (retval != 0) { + pr_info(KERN_INFO "spinand_erase: fail, block=%d!\n", + block_id + count); + errcode = -1; + } + } + count++; + } + + if (errcode == 0) + instr->state = MTD_ERASE_DONE; + + /* Deselect and wake up anyone waiting on the device */ + spinand_release_device(mtd); + + /* Do call back function */ + if (instr->callback) + instr->callback(instr); + + return errcode; +} + +/** + * spinand_sync - [MTD Interface] sync + * @param mtd MTD device structure + * + * Sync is actually a wait for chip ready function + */ +static void spinand_sync(struct mtd_info *mtd) +{ + pr_debug("spinand_sync: called\n"); + + /* Grab the lock and see if the device is available */ + spinand_get_device(mtd, FL_SYNCING); + + /* Release it and go back */ + spinand_release_device(mtd); +} + +static int spinand_block_isbad(struct mtd_info *mtd, loff_t ofs) +{ + struct spinand_chip *chip = mtd->priv; + struct spi_device *spi_nand = chip->spi_nand; + struct spinand_info *info = chip->info; + u16 block_id; + u8 is_bad = 0x00; + u8 ret = 0; + + spinand_get_device(mtd, FL_READING); + + block_id = ofs >> info->block_shift; + + chip->read_page(spi_nand, info, block_id*info->page_num_per_block, + info->page_main_size, 1, &is_bad); + + if (is_bad != 0xFF) + ret = 1; + + spinand_release_device(mtd); + + return ret; +} + +/** + * spinand_block_markbad - [MTD Interface] Mark bad block + * @param mtd MTD device structure + * @param ofs Bad block number + */ +static int spinand_block_markbad(struct mtd_info *mtd, loff_t ofs) +{ + struct spinand_chip *chip = mtd->priv; + struct spi_device *spi_nand = chip->spi_nand; + struct spinand_info *info = chip->info; + u16 block_id; + u8 is_bad = 0x00; + u8 ret = 0; + + spinand_get_device(mtd, FL_WRITING); + + block_id = ofs >> info->block_shift; + + chip->program_page(spi_nand, info, block_id*info->page_num_per_block, + info->page_main_size, 1, &is_bad); + + spinand_release_device(mtd); + + return ret; +} + + +/** + * spinand_suspend - [MTD Interface] Suspend the spinand flash + * @param mtd MTD device structure + */ +static int spinand_suspend(struct mtd_info *mtd) +{ + return spinand_get_device(mtd, FL_PM_SUSPENDED); +} + +/** + * spinand_resume - [MTD Interface] Resume the spinand flash + * @param mtd MTD device structure + */ +static void spinand_resume(struct mtd_info *mtd) +{ + struct spinand_chip *this = mtd->priv; + + if (this->state == FL_PM_SUSPENDED) + spinand_release_device(mtd); + else + pr_err(KERN_ERR "resume() called for the chip which is not" "in suspended state\n"); +} + +/** + * spinand_mtd - add MTD device with parameters + * @param mtd MTD device structure + * + * Add MTD device with parameters. + */ +int spinand_mtd(struct mtd_info *mtd) +{ + struct spinand_chip *chip = mtd->priv; + struct spinand_info *info = chip->info; + + chip->state = FL_READY; + init_waitqueue_head(&chip->wq); + spin_lock_init(&chip->chip_lock); + + mtd->name = info->name; + mtd->size = info->usable_size; + mtd->erasesize = info->block_main_size; + mtd->writesize = info->page_main_size; + mtd->oobsize = info->page_spare_size; + mtd->owner = THIS_MODULE; + mtd->type = MTD_NANDFLASH; + mtd->flags = MTD_CAP_NANDFLASH; + + mtd->ecclayout = info->ecclayout; + + mtd->_erase = spinand_erase; + mtd->_point = NULL; + mtd->_unpoint = NULL; + mtd->_read = spinand_read; + mtd->_write = spinand_write; + mtd->_read_oob = spinand_read_oob; + mtd->_write_oob = spinand_write_oob; + mtd->_sync = spinand_sync; + mtd->_lock = NULL; + mtd->_unlock = NULL; + mtd->_suspend = spinand_suspend; + mtd->_resume = spinand_resume; + mtd->_block_isbad = spinand_block_isbad; + mtd->_block_markbad = spinand_block_markbad; + + return 0; +} +EXPORT_SYMBOL_GPL(spinand_mtd); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Henry Pan<hs...@micron.com>"); diff --git a/include/linux/mtd/spinand.h b/include/linux/mtd/spinand.h new file mode 100644 index 0000000..3b8802a --- /dev/null +++ b/include/linux/mtd/spinand.h @@ -0,0 +1,155 @@ +/* + * linux/include/linux/mtd/spinand.h + * Copyright (c) 2009-2010 Micron Technology, Inc. + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + + * 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. +/bin/bash: 4: command not found + * + * based on nand.h + */ +#ifndef __LINUX_MTD_SPI_NAND_H +#define __LINUX_MTD_SPI_NAND_H + +#include <linux/wait.h> +#include <linux/spinlock.h> +#include <linux/mtd/mtd.h> + +/* cmd */ +#define CMD_READ 0x13 +#define CMD_READ_RDM 0x03 +#define CMD_PROG_PAGE_CLRCACHE 0x02 +#define CMD_PROG_PAGE 0x84 +#define CMD_PROG_PAGE_EXC 0x10 +#define CMD_ERASE_BLK 0xd8 +#define CMD_WR_ENABLE 0x06 +#define CMD_WR_DISABLE 0x04 +#define CMD_READ_ID 0x9f +#define CMD_RESET 0xff +#define CMD_READ_REG 0x0f +#define CMD_WRITE_REG 0x1f + +/* feature/ status reg */ +#define REG_BLOCK_LOCK 0xa0 +#define REG_OTP 0xb0 +#define REG_STATUS 0xc0/* timing */ + +/* status */ +#define STATUS_OIP_MASK 0x01 +#define STATUS_READY (0 << 0) +#define STATUS_BUSY (1 << 0) + +#define STATUS_E_FAIL_MASK 0x04 +#define STATUS_E_FAIL (1 << 2) + +#define STATUS_P_FAIL_MASK 0x08 +#define STATUS_P_FAIL (1 << 3) + +#define STATUS_ECC_MASK 0x30 +#define STATUS_ECC_1BIT_CORRECTED (1 << 4) +#define STATUS_ECC_ERROR (2 << 4) +#define STATUS_ECC_RESERVED (3 << 4) + + +/*ECC enable defines*/ +#define OTP_ECC_MASK 0x10 +#define OTP_ECC_OFF 0 +#define OTP_ECC_ON 1 + +#define ECC_DISABLED +#define ECC_IN_NAND +#define ECC_SOFT + +/* block lock */ +#define BL_ALL_LOCKED 0x38 +#define BL_1_2_LOCKED 0x30 +#define BL_1_4_LOCKED 0x28 +#define BL_1_8_LOCKED 0x20 +#define BL_1_16_LOCKED 0x18 +#define BL_1_32_LOCKED 0x10 +#define BL_1_64_LOCKED 0x08 +#define BL_ALL_UNLOCKED 0 + +struct spinand_info { + u8 mid; + u8 did; + char *name; + u64 nand_size; + u64 usable_size; + + u32 block_size; + u32 block_main_size; + /*u32 block_spare_size; */ + u16 block_num_per_chip; + u16 page_size; + u16 page_main_size; + u16 page_spare_size; + u16 page_num_per_block; + u8 block_shift; + u32 block_mask; + u8 page_shift; + u16 page_mask; + + struct nand_ecclayout *ecclayout; +}; + +typedef enum { + FL_READY, + FL_READING, + FL_WRITING, + FL_ERASING, + FL_SYNCING, + FL_LOCKING, + FL_RESETING, + FL_OTPING, + FL_PM_SUSPENDED, +} spinand_state_t; + +struct spinand_chip { /* used for multi chip */ + spinlock_t chip_lock; + wait_queue_head_t wq; + spinand_state_t state; + struct spi_device *spi_nand; + struct spinand_info *info; + /*struct mtd_info *mtd; */ + + int (*reset) (struct spi_device *spi_nand); + int (*read_id) (struct spi_device *spi_nand, u8 *id); + int (*read_page) (struct spi_device *spi_nand, + struct spinand_info *info, u16 page_id, u16 offset, + u16 len, u8 *rbuf); + int (*program_page) (struct spi_device *spi_nand, + struct spinand_info *info, u16 page_id, u16 offset, + u16 len, u8 *wbuf); + int (*erase_block) (struct spi_device *spi_nand, + struct spinand_info *info, u16 block_id); + + u8 *buf; + u8 *oobbuf; /* temp buffer */ + +#ifdef CONFIG_MTD_SPINAND_SWECC + u8 ecc_calc[12]; + u8 ecc_code[12]; +#endif +}; + +struct spinand_cmd { + u8 cmd; + unsigned n_addr; + u8 addr[3]; + unsigned n_dummy; + unsigned n_tx; + u8 *tx_buf; + unsigned n_rx; + u8 *rx_buf; +}; + +extern int spinand_mtd(struct mtd_info *mtd); +extern void spinand_mtd_release(struct mtd_info *mtd); + +#endif -- 1.7.1 -- To unsubscribe from this list: send the line "unsubscribe linux-kernel" in the body of a message to majord...@vger.kernel.org More majordomo info at http://vger.kernel.org/majordomo-info.html Please read the FAQ at http://www.tux.org/lkml/