Patchwork [02/13] mtd: SST25L (non JEDEC) SPI Flash driver

login
register
mail settings
Submitter Andrew Morton
Date Sept. 18, 2009, 7:51 p.m.
Message ID <200909181951.n8IJpeq1023467@imap1.linux-foundation.org>
Download mbox | patch
Permalink /patch/33879/
State New
Headers show

Comments

Andrew Morton - Sept. 18, 2009, 7:51 p.m.
From: Ryan Mallon <ryan@bluewatersys.com>

Add support for the non JEDEC SST25L SPI Flash devices.

Signed-off-by: Andre Renaud <andre@bluewatersys.com>
Signed-off-by: Ryan Mallon <ryan@bluewatersys.com>
Acked-by: Linus Walleij <linus.walleij@stericsson.com>
Cc: David Woodhouse <dwmw2@infradead.org>
Cc: Anton Vorontsov <avorontsov@ru.mvista.com>
Cc: "H Hartley Sweeten" <hartleys@visionengravers.com>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
---

 drivers/mtd/devices/Kconfig  |   10 
 drivers/mtd/devices/Makefile |    1 
 drivers/mtd/devices/sst25l.c |  510 +++++++++++++++++++++++++++++++++
 3 files changed, 521 insertions(+)
David Woodhouse - Sept. 19, 2009, 5:22 p.m.
On Fri, 2009-09-18 at 12:51 -0700, akpm@linux-foundation.org wrote:
> 
> +static int sst25l_wait_till_ready(struct sst25l_flash *flash)
> +{
> +       unsigned long deadline;
> +       int status, err;
> +
> +       deadline = jiffies + MAX_READY_WAIT_JIFFIES;
> +       do {
> +               err = sst25l_status(flash, &status);
> +               if (err)
> +                       return err;
> +               if (!(status & SST25L_STATUS_BUSY))
> +                       return 0;
> +
> +               cond_resched();
> +       } while (!time_after_eq(jiffies, deadline));
> +
> +       return -ETIMEDOUT;
> +}

If your system is busy and you end up relinquishing the CPU for a long
period of time during that cond_resched(), you could hit the timeout
condition even though the hardware _is_ actually reporting 'ready'
status by the time you get back on the CPU.

It's unlikely, admittedly, but it's good practice to make sure it can't
happen like that. Something like

 while (busy) {
    if (timed_out) return -ETIMEDOUT;
    cond_resched();
 }
Ryan Mallon - Sept. 21, 2009, 3:24 a.m.
David Woodhouse wrote:
> On Fri, 2009-09-18 at 12:51 -0700, akpm@linux-foundation.org wrote:
>> +static int sst25l_wait_till_ready(struct sst25l_flash *flash)
>> +{
>> +       unsigned long deadline;
>> +       int status, err;
>> +
>> +       deadline = jiffies + MAX_READY_WAIT_JIFFIES;
>> +       do {
>> +               err = sst25l_status(flash, &status);
>> +               if (err)
>> +                       return err;
>> +               if (!(status & SST25L_STATUS_BUSY))
>> +                       return 0;
>> +
>> +               cond_resched();
>> +       } while (!time_after_eq(jiffies, deadline));
>> +
>> +       return -ETIMEDOUT;
>> +}
> 
> If your system is busy and you end up relinquishing the CPU for a long
> period of time during that cond_resched(), you could hit the timeout
> condition even though the hardware _is_ actually reporting 'ready'
> status by the time you get back on the CPU.
> 
> It's unlikely, admittedly, but it's good practice to make sure it can't
> happen like that. Something like
> 
>  while (busy) {
>     if (timed_out) return -ETIMEDOUT;
>     cond_resched();
>  }
> 

Okay, makes sense. I'm a bit busy at the moment, but I'll try and post a
fix in the next couple of days. Is the patch still going to be merged as
is, or are you waiting on a fix for this?

The function above is based on the one of the same name from
drivers/mtd/devices/m25p80.c, so that should also be changed? There are
possibly other mtd drivers which also have this construct.

~Ryan
Ryan Mallon - Sept. 21, 2009, 4:47 a.m.
David Woodhouse wrote:
> On Fri, 2009-09-18 at 12:51 -0700, akpm@linux-foundation.org wrote:
>> +static int sst25l_wait_till_ready(struct sst25l_flash *flash)
>> +{
>> +       unsigned long deadline;
>> +       int status, err;
>> +
>> +       deadline = jiffies + MAX_READY_WAIT_JIFFIES;
>> +       do {
>> +               err = sst25l_status(flash, &status);
>> +               if (err)
>> +                       return err;
>> +               if (!(status & SST25L_STATUS_BUSY))
>> +                       return 0;
>> +
>> +               cond_resched();
>> +       } while (!time_after_eq(jiffies, deadline));
>> +
>> +       return -ETIMEDOUT;
>> +}
> 
> If your system is busy and you end up relinquishing the CPU for a long
> period of time during that cond_resched(), you could hit the timeout
> condition even though the hardware _is_ actually reporting 'ready'
> status by the time you get back on the CPU.
> 
> It's unlikely, admittedly, but it's good practice to make sure it can't
> happen like that. Something like
> 
>  while (busy) {
>     if (timed_out) return -ETIMEDOUT;
>     cond_resched();
>  }
> 

Just thinking about this a bit more. We don't want to call cond_resched
if the device is ready immediately, and we want to do the check _after_
cond_resched each time through the loop. There are probably enough
places that this sort of thing gets used that it may be worth having a
generic function like this (untested):

int cond_resched_wait_timeout(unsigned long timeout_jiffies,
			      int (*cond_check)(void *cookie),
			      void *data)
{
	unsigned long deadline;
	int ret;

	ret = cond_check(data);
	if (ret == 1)
		return 0;
	if (ret < 0)
		return ret;
	deadline = jiffies + timeout_jiffies;
	while (1) {
		cond_resched();
		ret = cond_check(data);
		if (ret == 1)
			return 0;
		if (ret < 0)
			return ret;

		if (time_after_eq(jiffies, deadline))
			return -ETIMEDOUT;
	}

	/* Not reached */
	return 0;
}

Not sure if the name is entirely appropriate, or where it should go as a
generic function. Thoughts?

~Ryan
David Woodhouse - Sept. 21, 2009, 6:08 a.m.
On Mon, 2009-09-21 at 15:24 +1200, Ryan Mallon wrote:
> David Woodhouse wrote:
> >
> >  while (busy) {
> >     if (timed_out) return -ETIMEDOUT;
> >     cond_resched();
> >  }
> > 
> 
> Okay, makes sense. I'm a bit busy at the moment, but I'll try and post a
> fix in the next couple of days. Is the patch still going to be merged as
> is, or are you waiting on a fix for this?

I've merged it already; a few days for a fix is fine. Thanks.

> The function above is based on the one of the same name from
> drivers/mtd/devices/m25p80.c, so that should also be changed? There are
> possibly other mtd drivers which also have this construct.

Hm, yes. I try to look out for this stuff but sometimes I miss it.
David Woodhouse - Sept. 21, 2009, 6:14 a.m.
On Mon, 2009-09-21 at 16:47 +1200, Ryan Mallon wrote:
> >  while (busy) {
> >     if (timed_out) return -ETIMEDOUT;
> >     cond_resched();
> >  }
> > 
> 
> Just thinking about this a bit more. We don't want to call cond_resched
> if the device is ready immediately, and we want to do the check _after_
> cond_resched each time through the loop.

Right, absolutely.

And that's exactly what the loop above was doing, isn't it? If you
unroll the loop, it goes...

 if (busy) {
  if (timed_out) return -ETIMEDOUT;
  cond_resched();
  if (busy) {
    if (timed_out) return -ETIMEDOUT;
    cond_resched();
    if (busy) {
      if (timed_out) return -ETIMEDOUT;
      cond_resched();
      if (busy) {
        ...

It _isn't_ calling cond_resched() if the device is ready immediately,
and it _is_ doing the check _after_ cond_resched() each time. Yes?
  
> There are probably enough places that this sort of thing gets used
> that it may be worth having a generic function like this (untested):
> 
> int cond_resched_wait_timeout(unsigned long timeout_jiffies,
>                               int (*cond_check)(void *cookie),
>                               void *data)

Yes, it's probably a good idea. Maybe better off as a macro, rather than
having a function call to evaluate the condition. Much like wait_event?

Patch

diff -puN drivers/mtd/devices/Kconfig~mtd-sst25l-non-jedec-spi-flash-driver drivers/mtd/devices/Kconfig
--- a/drivers/mtd/devices/Kconfig~mtd-sst25l-non-jedec-spi-flash-driver
+++ a/drivers/mtd/devices/Kconfig
@@ -104,6 +104,16 @@  config M25PXX_USE_FAST_READ
 	help
 	  This option enables FAST_READ access supported by ST M25Pxx.
 
+config MTD_SST25L
+	tristate "Support SST25L (non JEDEC) SPI Flash chips"
+	depends on SPI_MASTER
+	help
+	  This enables access to the non JEDEC SST25L SPI flash chips, used
+	  for program and data storage.
+
+	  Set up your spi devices with the right board-specific platform data,
+	  if you want to specify device partitioning.
+
 config MTD_SLRAM
 	tristate "Uncached system RAM"
 	help
diff -puN drivers/mtd/devices/Makefile~mtd-sst25l-non-jedec-spi-flash-driver drivers/mtd/devices/Makefile
--- a/drivers/mtd/devices/Makefile~mtd-sst25l-non-jedec-spi-flash-driver
+++ a/drivers/mtd/devices/Makefile
@@ -16,3 +16,4 @@  obj-$(CONFIG_MTD_LART)		+= lart.o
 obj-$(CONFIG_MTD_BLOCK2MTD)	+= block2mtd.o
 obj-$(CONFIG_MTD_DATAFLASH)	+= mtd_dataflash.o
 obj-$(CONFIG_MTD_M25P80)	+= m25p80.o
+obj-$(CONFIG_MTD_SST25L)	+= sst25l.o
diff -puN /dev/null drivers/mtd/devices/sst25l.c
--- /dev/null
+++ a/drivers/mtd/devices/sst25l.c
@@ -0,0 +1,510 @@ 
+/*
+ * drivers/mtd/sst25l.c
+ *
+ * Driver for SST25L SPI Flash chips
+ *
+ * Copyright (C) 2009 Bluewater Systems Ltd
+ * Author: Andre Renaud <andre@bluewatersys.com>
+ * Author: Ryan Mallon <ryan@bluewatersys.com>
+ *
+ * Based on m25p80.c
+ *
+ * This code 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.
+ *
+ */
+
+#include <linux/init.h>
+#include <linux/module.h>
+#include <linux/device.h>
+#include <linux/mutex.h>
+#include <linux/interrupt.h>
+
+#include <linux/mtd/mtd.h>
+#include <linux/mtd/partitions.h>
+
+#include <linux/spi/spi.h>
+#include <linux/spi/flash.h>
+
+/* Erases can take up to 3 seconds! */
+#define MAX_READY_WAIT_JIFFIES	msecs_to_jiffies(3000)
+
+#define SST25L_CMD_WRSR		0x01	/* Write status register */
+#define SST25L_CMD_WRDI		0x04	/* Write disable */
+#define SST25L_CMD_RDSR		0x05	/* Read status register */
+#define SST25L_CMD_WREN		0x06	/* Write enable */
+#define SST25L_CMD_READ		0x03	/* High speed read */
+
+#define SST25L_CMD_EWSR		0x50	/* Enable write status register */
+#define SST25L_CMD_SECTOR_ERASE	0x20	/* Erase sector */
+#define SST25L_CMD_READ_ID	0x90	/* Read device ID */
+#define SST25L_CMD_AAI_PROGRAM	0xaf	/* Auto address increment */
+
+#define SST25L_STATUS_BUSY	(1 << 0)	/* Chip is busy */
+#define SST25L_STATUS_WREN	(1 << 1)	/* Write enabled */
+#define SST25L_STATUS_BP0	(1 << 2)	/* Block protection 0 */
+#define SST25L_STATUS_BP1	(1 << 3)	/* Block protection 1 */
+
+struct sst25l_flash {
+	struct spi_device	*spi;
+	struct mutex		lock;
+	struct mtd_info		mtd;
+
+	int 			partitioned;
+};
+
+struct flash_info {
+	const char		*name;
+	u16			device_id;
+	unsigned		page_size;
+	unsigned		nr_pages;
+	unsigned		erase_size;
+};
+
+#define to_sst25l_flash(x) container_of(x, struct sst25l_flash, mtd)
+
+static struct flash_info __initdata sst25l_flash_info[] = {
+	{"sst25lf020a", 0xbf43, 256, 1024, 4096},
+	{"sst25lf040a",	0xbf44,	256, 2048, 4096},
+};
+
+static int sst25l_status(struct sst25l_flash *flash, int *status)
+{
+	u8 command, response;
+	int err;
+
+	command = SST25L_CMD_RDSR;
+	err = spi_write_then_read(flash->spi, &command, 1, &response, 1);
+	if (err < 0)
+		return err;
+
+	*status = response;
+	return 0;
+}
+
+static int sst25l_write_enable(struct sst25l_flash *flash, int enable)
+{
+	u8 command[2];
+	int status, err;
+
+	command[0] = enable ? SST25L_CMD_WREN : SST25L_CMD_WRDI;
+	err = spi_write(flash->spi, command, 1);
+	if (err)
+		return err;
+
+	command[0] = SST25L_CMD_EWSR;
+	err = spi_write(flash->spi, command, 1);
+	if (err)
+		return err;
+
+	command[0] = SST25L_CMD_WRSR;
+	command[1] = enable ? 0 : SST25L_STATUS_BP0 | SST25L_STATUS_BP1;
+	err = spi_write(flash->spi, command, 2);
+	if (err)
+		return err;
+
+	if (enable) {
+		err = sst25l_status(flash, &status);
+		if (err)
+			return err;
+		if (!(status & SST25L_STATUS_WREN))
+			return -EROFS;
+	}
+
+	return 0;
+}
+
+static int sst25l_wait_till_ready(struct sst25l_flash *flash)
+{
+	unsigned long deadline;
+	int status, err;
+
+	deadline = jiffies + MAX_READY_WAIT_JIFFIES;
+	do {
+		err = sst25l_status(flash, &status);
+		if (err)
+			return err;
+		if (!(status & SST25L_STATUS_BUSY))
+			return 0;
+
+		cond_resched();
+	} while (!time_after_eq(jiffies, deadline));
+
+	return -ETIMEDOUT;
+}
+
+static int sst25l_erase_sector(struct sst25l_flash *flash, u32 offset)
+{
+	u8 command[4];
+	int err;
+
+	err = sst25l_write_enable(flash, 1);
+	if (err)
+		return err;
+
+	command[0] = SST25L_CMD_SECTOR_ERASE;
+	command[1] = offset >> 16;
+	command[2] = offset >> 8;
+	command[3] = offset;
+	err = spi_write(flash->spi, command, 4);
+	if (err)
+		return err;
+
+	err = sst25l_wait_till_ready(flash);
+	if (err)
+		return err;
+
+	return sst25l_write_enable(flash, 0);
+}
+
+static int sst25l_erase(struct mtd_info *mtd, struct erase_info *instr)
+{
+	struct sst25l_flash *flash = to_sst25l_flash(mtd);
+	u32 addr, end;
+	int err;
+
+	/* Sanity checks */
+	if (instr->addr + instr->len > flash->mtd.size)
+		return -EINVAL;
+
+	if ((u32)instr->len % mtd->erasesize)
+		return -EINVAL;
+
+	if ((u32)instr->addr % mtd->erasesize)
+		return -EINVAL;
+
+	addr = instr->addr;
+	end = addr + instr->len;
+
+	mutex_lock(&flash->lock);
+
+	err = sst25l_wait_till_ready(flash);
+	if (err)
+		return err;
+
+	while (addr < end) {
+		err = sst25l_erase_sector(flash, addr);
+		if (err) {
+			mutex_unlock(&flash->lock);
+			instr->state = MTD_ERASE_FAILED;
+			dev_err(&flash->spi->dev, "Erase failed\n");
+			return err;
+		}
+
+		addr += mtd->erasesize;
+	}
+
+	mutex_unlock(&flash->lock);
+
+	instr->state = MTD_ERASE_DONE;
+	mtd_erase_callback(instr);
+	return 0;
+}
+
+static int sst25l_read(struct mtd_info *mtd, loff_t from, size_t len,
+		       size_t *retlen, u_char *buf)
+{
+	struct sst25l_flash *flash = to_sst25l_flash(mtd);
+	struct spi_transfer transfer[2];
+	struct spi_message message;
+	u8 command[4];
+	int ret;
+
+	/* Sanity checking */
+	if (len == 0)
+		return 0;
+
+	if (from + len > flash->mtd.size)
+		return -EINVAL;
+
+	if (retlen)
+		*retlen = 0;
+
+	spi_message_init(&message);
+	memset(&transfer, 0, sizeof(transfer));
+
+	command[0] = SST25L_CMD_READ;
+	command[1] = from >> 16;
+	command[2] = from >> 8;
+	command[3] = from;
+
+	transfer[0].tx_buf = command;
+	transfer[0].len = sizeof(command);
+	spi_message_add_tail(&transfer[0], &message);
+
+	transfer[1].rx_buf = buf;
+	transfer[1].len = len;
+	spi_message_add_tail(&transfer[1], &message);
+
+	mutex_lock(&flash->lock);
+
+	/* Wait for previous write/erase to complete */
+	ret = sst25l_wait_till_ready(flash);
+	if (ret) {
+		mutex_unlock(&flash->lock);
+		return ret;
+	}
+
+	spi_sync(flash->spi, &message);
+
+	if (retlen && message.actual_length > sizeof(command))
+		*retlen += message.actual_length - sizeof(command);
+
+	mutex_unlock(&flash->lock);
+	return 0;
+}
+
+static int sst25l_write(struct mtd_info *mtd, loff_t to, size_t len,
+			size_t *retlen, const u_char *buf)
+{
+	struct sst25l_flash *flash = to_sst25l_flash(mtd);
+	int i, j, ret, bytes, copied = 0;
+	u8 command[5];
+
+	/* Sanity checks */
+	if (!len)
+		return 0;
+
+	if (to + len > flash->mtd.size)
+		return -EINVAL;
+
+	if ((u32)to % mtd->writesize)
+		return -EINVAL;
+
+	mutex_lock(&flash->lock);
+
+	ret = sst25l_write_enable(flash, 1);
+	if (ret)
+		goto out;
+
+	for (i = 0; i < len; i += mtd->writesize) {
+		ret = sst25l_wait_till_ready(flash);
+		if (ret)
+			goto out;
+
+		/* Write the first byte of the page */
+		command[0] = SST25L_CMD_AAI_PROGRAM;
+		command[1] = (to + i) >> 16;
+		command[2] = (to + i) >> 8;
+		command[3] = (to + i);
+		command[4] = buf[i];
+		ret = spi_write(flash->spi, command, 5);
+		if (ret < 0)
+			goto out;
+		copied++;
+
+		/*
+		 * Write the remaining bytes using auto address
+		 * increment mode
+		 */
+		bytes = min_t(u32, mtd->writesize, len - i);
+		for (j = 1; j < bytes; j++, copied++) {
+			ret = sst25l_wait_till_ready(flash);
+			if (ret)
+				goto out;
+
+			command[1] = buf[i + j];
+			ret = spi_write(flash->spi, command, 2);
+			if (ret)
+				goto out;
+		}
+	}
+
+out:
+	ret = sst25l_write_enable(flash, 0);
+
+	if (retlen)
+		*retlen = copied;
+
+	mutex_unlock(&flash->lock);
+	return ret;
+}
+
+static struct flash_info *__init sst25l_match_device(struct spi_device *spi)
+{
+	struct flash_info *flash_info = NULL;
+	u8 command[4], response;
+	int i, err;
+	u16 id;
+
+	command[0] = SST25L_CMD_READ_ID;
+	command[1] = 0;
+	command[2] = 0;
+	command[3] = 0;
+	err = spi_write_then_read(spi, command, sizeof(command), &response, 1);
+	if (err < 0) {
+		dev_err(&spi->dev, "error reading device id msb\n");
+		return NULL;
+	}
+
+	id = response << 8;
+
+	command[0] = SST25L_CMD_READ_ID;
+	command[1] = 0;
+	command[2] = 0;
+	command[3] = 1;
+	err = spi_write_then_read(spi, command, sizeof(command), &response, 1);
+	if (err < 0) {
+		dev_err(&spi->dev, "error reading device id lsb\n");
+		return NULL;
+	}
+
+	id |= response;
+
+	for (i = 0; i < ARRAY_SIZE(sst25l_flash_info); i++)
+		if (sst25l_flash_info[i].device_id == id)
+			flash_info = &sst25l_flash_info[i];
+
+	if (!flash_info)
+		dev_err(&spi->dev, "unknown id %.4x\n", id);
+
+	return flash_info;
+}
+
+static int __init sst25l_probe(struct spi_device *spi)
+{
+	struct flash_info *flash_info;
+	struct sst25l_flash *flash;
+	struct flash_platform_data *data;
+	int ret, i;
+
+	flash_info = sst25l_match_device(spi);
+	if (!flash_info)
+		return -ENODEV;
+
+	flash = kzalloc(sizeof(struct sst25l_flash), GFP_KERNEL);
+	if (!flash)
+		return -ENOMEM;
+
+	flash->spi = spi;
+	mutex_init(&flash->lock);
+	dev_set_drvdata(&spi->dev, flash);
+
+	data = spi->dev.platform_data;
+	if (data && data->name)
+		flash->mtd.name = data->name;
+	else
+		flash->mtd.name = dev_name(&spi->dev);
+
+	flash->mtd.type		= MTD_NORFLASH;
+	flash->mtd.flags	= MTD_CAP_NORFLASH;
+	flash->mtd.erasesize	= flash_info->erase_size;
+	flash->mtd.writesize	= flash_info->page_size;
+	flash->mtd.size		= flash_info->page_size * flash_info->nr_pages;
+	flash->mtd.erase	= sst25l_erase;
+	flash->mtd.read		= sst25l_read;
+	flash->mtd.write 	= sst25l_write;
+
+	dev_info(&spi->dev, "%s (%lld Kbytes)\n", flash_info->name,
+		 (long long)flash->mtd.size >> 10);
+
+	DEBUG(MTD_DEBUG_LEVEL2,
+	      "mtd .name = %s, .size = 0x%llx (%lldMiB) "
+	      ".erasesize = 0x%.8x (%uKiB) .numeraseregions = %d\n",
+	      flash->mtd.name,
+	      (long long)flash->mtd.size, (long long)(flash->mtd.size >> 20),
+	      flash->mtd.erasesize, flash->mtd.erasesize / 1024,
+	      flash->mtd.numeraseregions);
+
+	if (flash->mtd.numeraseregions)
+		for (i = 0; i < flash->mtd.numeraseregions; i++)
+			DEBUG(MTD_DEBUG_LEVEL2,
+			      "mtd.eraseregions[%d] = { .offset = 0x%llx, "
+			      ".erasesize = 0x%.8x (%uKiB), "
+			      ".numblocks = %d }\n",
+			      i, (long long)flash->mtd.eraseregions[i].offset,
+			      flash->mtd.eraseregions[i].erasesize,
+			      flash->mtd.eraseregions[i].erasesize / 1024,
+			      flash->mtd.eraseregions[i].numblocks);
+
+	if (mtd_has_partitions()) {
+		struct mtd_partition *parts = NULL;
+		int nr_parts = 0;
+
+		if (mtd_has_cmdlinepart()) {
+			static const char *part_probes[] =
+				{"cmdlinepart", NULL};
+
+			nr_parts = parse_mtd_partitions(&flash->mtd,
+							part_probes,
+							&parts, 0);
+		}
+
+		if (nr_parts <= 0 && data && data->parts) {
+			parts = data->parts;
+			nr_parts = data->nr_parts;
+		}
+
+		if (nr_parts > 0) {
+			for (i = 0; i < nr_parts; i++) {
+				DEBUG(MTD_DEBUG_LEVEL2, "partitions[%d] = "
+				      "{.name = %s, .offset = 0x%llx, "
+				      ".size = 0x%llx (%lldKiB) }\n",
+				      i, parts[i].name,
+				      (long long)parts[i].offset,
+				      (long long)parts[i].size,
+				      (long long)(parts[i].size >> 10));
+			}
+
+			flash->partitioned = 1;
+			return add_mtd_partitions(&flash->mtd,
+						  parts, nr_parts);
+		}
+
+	} else if (data->nr_parts) {
+		dev_warn(&spi->dev, "ignoring %d default partitions on %s\n",
+			 data->nr_parts, data->name);
+	}
+
+	ret = add_mtd_device(&flash->mtd);
+	if (ret == 1) {
+		kfree(flash);
+		dev_set_drvdata(&spi->dev, NULL);
+		return -ENODEV;
+	}
+
+	return 0;
+}
+
+static int __exit sst25l_remove(struct spi_device *spi)
+{
+	struct sst25l_flash *flash = dev_get_drvdata(&spi->dev);
+	int ret;
+
+	if (mtd_has_partitions() && flash->partitioned)
+		ret = del_mtd_partitions(&flash->mtd);
+	else
+		ret = del_mtd_device(&flash->mtd);
+	if (ret == 0)
+		kfree(flash);
+	return ret;
+}
+
+static struct spi_driver sst25l_driver = {
+	.driver = {
+		.name	= "sst25l",
+		.bus	= &spi_bus_type,
+		.owner	= THIS_MODULE,
+	},
+	.probe		= sst25l_probe,
+	.remove		= __exit_p(sst25l_remove),
+};
+
+static int __init sst25l_init(void)
+{
+	return spi_register_driver(&sst25l_driver);
+}
+
+static void __exit sst25l_exit(void)
+{
+	spi_unregister_driver(&sst25l_driver);
+}
+
+module_init(sst25l_init);
+module_exit(sst25l_exit);
+
+MODULE_DESCRIPTION("MTD SPI driver for SST25L Flash chips");
+MODULE_AUTHOR("Andre Renaud <andre@bluewatersys.com>, "
+	      "Ryan Mallon <ryan@bluewatersys.com>");
+MODULE_LICENSE("GPL");