7e74a5076e
The changelog on the driver is superflous given this is being kept under revision control. Remove the other cruft in the header and update the copyright and the supported device list. Signed-off-by: Ben Dooks <ben-linux@fluff.org> Signed-off-by: David Woodhouse <dwmw2@infradead.org>
957 lines
23 KiB
C
957 lines
23 KiB
C
/* linux/drivers/mtd/nand/s3c2410.c
|
|
*
|
|
* Copyright © 2004-2008 Simtec Electronics
|
|
* http://armlinux.simtec.co.uk/
|
|
* Ben Dooks <ben@simtec.co.uk>
|
|
*
|
|
* Samsung S3C2410/S3C2440/S3C2412 NAND driver
|
|
*
|
|
* 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.
|
|
*
|
|
* 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
|
|
*/
|
|
|
|
#ifdef CONFIG_MTD_NAND_S3C2410_DEBUG
|
|
#define DEBUG
|
|
#endif
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/types.h>
|
|
#include <linux/init.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/string.h>
|
|
#include <linux/ioport.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/err.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/clk.h>
|
|
|
|
#include <linux/mtd/mtd.h>
|
|
#include <linux/mtd/nand.h>
|
|
#include <linux/mtd/nand_ecc.h>
|
|
#include <linux/mtd/partitions.h>
|
|
|
|
#include <asm/io.h>
|
|
|
|
#include <asm/plat-s3c/regs-nand.h>
|
|
#include <asm/plat-s3c/nand.h>
|
|
|
|
#ifdef CONFIG_MTD_NAND_S3C2410_HWECC
|
|
static int hardware_ecc = 1;
|
|
#else
|
|
static int hardware_ecc = 0;
|
|
#endif
|
|
|
|
#ifdef CONFIG_MTD_NAND_S3C2410_CLKSTOP
|
|
static int clock_stop = 1;
|
|
#else
|
|
static const int clock_stop = 0;
|
|
#endif
|
|
|
|
|
|
/* new oob placement block for use with hardware ecc generation
|
|
*/
|
|
|
|
static struct nand_ecclayout nand_hw_eccoob = {
|
|
.eccbytes = 3,
|
|
.eccpos = {0, 1, 2},
|
|
.oobfree = {{8, 8}}
|
|
};
|
|
|
|
/* controller and mtd information */
|
|
|
|
struct s3c2410_nand_info;
|
|
|
|
struct s3c2410_nand_mtd {
|
|
struct mtd_info mtd;
|
|
struct nand_chip chip;
|
|
struct s3c2410_nand_set *set;
|
|
struct s3c2410_nand_info *info;
|
|
int scan_res;
|
|
};
|
|
|
|
enum s3c_cpu_type {
|
|
TYPE_S3C2410,
|
|
TYPE_S3C2412,
|
|
TYPE_S3C2440,
|
|
};
|
|
|
|
/* overview of the s3c2410 nand state */
|
|
|
|
struct s3c2410_nand_info {
|
|
/* mtd info */
|
|
struct nand_hw_control controller;
|
|
struct s3c2410_nand_mtd *mtds;
|
|
struct s3c2410_platform_nand *platform;
|
|
|
|
/* device info */
|
|
struct device *device;
|
|
struct resource *area;
|
|
struct clk *clk;
|
|
void __iomem *regs;
|
|
void __iomem *sel_reg;
|
|
int sel_bit;
|
|
int mtd_count;
|
|
unsigned long save_sel;
|
|
|
|
enum s3c_cpu_type cpu_type;
|
|
};
|
|
|
|
/* conversion functions */
|
|
|
|
static struct s3c2410_nand_mtd *s3c2410_nand_mtd_toours(struct mtd_info *mtd)
|
|
{
|
|
return container_of(mtd, struct s3c2410_nand_mtd, mtd);
|
|
}
|
|
|
|
static struct s3c2410_nand_info *s3c2410_nand_mtd_toinfo(struct mtd_info *mtd)
|
|
{
|
|
return s3c2410_nand_mtd_toours(mtd)->info;
|
|
}
|
|
|
|
static struct s3c2410_nand_info *to_nand_info(struct platform_device *dev)
|
|
{
|
|
return platform_get_drvdata(dev);
|
|
}
|
|
|
|
static struct s3c2410_platform_nand *to_nand_plat(struct platform_device *dev)
|
|
{
|
|
return dev->dev.platform_data;
|
|
}
|
|
|
|
static inline int allow_clk_stop(struct s3c2410_nand_info *info)
|
|
{
|
|
return clock_stop;
|
|
}
|
|
|
|
/* timing calculations */
|
|
|
|
#define NS_IN_KHZ 1000000
|
|
|
|
static int s3c_nand_calc_rate(int wanted, unsigned long clk, int max)
|
|
{
|
|
int result;
|
|
|
|
result = (wanted * clk) / NS_IN_KHZ;
|
|
result++;
|
|
|
|
pr_debug("result %d from %ld, %d\n", result, clk, wanted);
|
|
|
|
if (result > max) {
|
|
printk("%d ns is too big for current clock rate %ld\n", wanted, clk);
|
|
return -1;
|
|
}
|
|
|
|
if (result < 1)
|
|
result = 1;
|
|
|
|
return result;
|
|
}
|
|
|
|
#define to_ns(ticks,clk) (((ticks) * NS_IN_KHZ) / (unsigned int)(clk))
|
|
|
|
/* controller setup */
|
|
|
|
static int s3c2410_nand_inithw(struct s3c2410_nand_info *info,
|
|
struct platform_device *pdev)
|
|
{
|
|
struct s3c2410_platform_nand *plat = to_nand_plat(pdev);
|
|
unsigned long clkrate = clk_get_rate(info->clk);
|
|
int tacls_max = (info->cpu_type == TYPE_S3C2412) ? 8 : 4;
|
|
int tacls, twrph0, twrph1;
|
|
unsigned long cfg = 0;
|
|
|
|
/* calculate the timing information for the controller */
|
|
|
|
clkrate /= 1000; /* turn clock into kHz for ease of use */
|
|
|
|
if (plat != NULL) {
|
|
tacls = s3c_nand_calc_rate(plat->tacls, clkrate, tacls_max);
|
|
twrph0 = s3c_nand_calc_rate(plat->twrph0, clkrate, 8);
|
|
twrph1 = s3c_nand_calc_rate(plat->twrph1, clkrate, 8);
|
|
} else {
|
|
/* default timings */
|
|
tacls = tacls_max;
|
|
twrph0 = 8;
|
|
twrph1 = 8;
|
|
}
|
|
|
|
if (tacls < 0 || twrph0 < 0 || twrph1 < 0) {
|
|
dev_err(info->device, "cannot get suitable timings\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
dev_info(info->device, "Tacls=%d, %dns Twrph0=%d %dns, Twrph1=%d %dns\n",
|
|
tacls, to_ns(tacls, clkrate), twrph0, to_ns(twrph0, clkrate), twrph1, to_ns(twrph1, clkrate));
|
|
|
|
switch (info->cpu_type) {
|
|
case TYPE_S3C2410:
|
|
cfg = S3C2410_NFCONF_EN;
|
|
cfg |= S3C2410_NFCONF_TACLS(tacls - 1);
|
|
cfg |= S3C2410_NFCONF_TWRPH0(twrph0 - 1);
|
|
cfg |= S3C2410_NFCONF_TWRPH1(twrph1 - 1);
|
|
break;
|
|
|
|
case TYPE_S3C2440:
|
|
case TYPE_S3C2412:
|
|
cfg = S3C2440_NFCONF_TACLS(tacls - 1);
|
|
cfg |= S3C2440_NFCONF_TWRPH0(twrph0 - 1);
|
|
cfg |= S3C2440_NFCONF_TWRPH1(twrph1 - 1);
|
|
|
|
/* enable the controller and de-assert nFCE */
|
|
|
|
writel(S3C2440_NFCONT_ENABLE, info->regs + S3C2440_NFCONT);
|
|
}
|
|
|
|
dev_dbg(info->device, "NF_CONF is 0x%lx\n", cfg);
|
|
|
|
writel(cfg, info->regs + S3C2410_NFCONF);
|
|
return 0;
|
|
}
|
|
|
|
/* select chip */
|
|
|
|
static void s3c2410_nand_select_chip(struct mtd_info *mtd, int chip)
|
|
{
|
|
struct s3c2410_nand_info *info;
|
|
struct s3c2410_nand_mtd *nmtd;
|
|
struct nand_chip *this = mtd->priv;
|
|
unsigned long cur;
|
|
|
|
nmtd = this->priv;
|
|
info = nmtd->info;
|
|
|
|
if (chip != -1 && allow_clk_stop(info))
|
|
clk_enable(info->clk);
|
|
|
|
cur = readl(info->sel_reg);
|
|
|
|
if (chip == -1) {
|
|
cur |= info->sel_bit;
|
|
} else {
|
|
if (nmtd->set != NULL && chip > nmtd->set->nr_chips) {
|
|
dev_err(info->device, "invalid chip %d\n", chip);
|
|
return;
|
|
}
|
|
|
|
if (info->platform != NULL) {
|
|
if (info->platform->select_chip != NULL)
|
|
(info->platform->select_chip) (nmtd->set, chip);
|
|
}
|
|
|
|
cur &= ~info->sel_bit;
|
|
}
|
|
|
|
writel(cur, info->sel_reg);
|
|
|
|
if (chip == -1 && allow_clk_stop(info))
|
|
clk_disable(info->clk);
|
|
}
|
|
|
|
/* s3c2410_nand_hwcontrol
|
|
*
|
|
* Issue command and address cycles to the chip
|
|
*/
|
|
|
|
static void s3c2410_nand_hwcontrol(struct mtd_info *mtd, int cmd,
|
|
unsigned int ctrl)
|
|
{
|
|
struct s3c2410_nand_info *info = s3c2410_nand_mtd_toinfo(mtd);
|
|
|
|
if (cmd == NAND_CMD_NONE)
|
|
return;
|
|
|
|
if (ctrl & NAND_CLE)
|
|
writeb(cmd, info->regs + S3C2410_NFCMD);
|
|
else
|
|
writeb(cmd, info->regs + S3C2410_NFADDR);
|
|
}
|
|
|
|
/* command and control functions */
|
|
|
|
static void s3c2440_nand_hwcontrol(struct mtd_info *mtd, int cmd,
|
|
unsigned int ctrl)
|
|
{
|
|
struct s3c2410_nand_info *info = s3c2410_nand_mtd_toinfo(mtd);
|
|
|
|
if (cmd == NAND_CMD_NONE)
|
|
return;
|
|
|
|
if (ctrl & NAND_CLE)
|
|
writeb(cmd, info->regs + S3C2440_NFCMD);
|
|
else
|
|
writeb(cmd, info->regs + S3C2440_NFADDR);
|
|
}
|
|
|
|
/* s3c2410_nand_devready()
|
|
*
|
|
* returns 0 if the nand is busy, 1 if it is ready
|
|
*/
|
|
|
|
static int s3c2410_nand_devready(struct mtd_info *mtd)
|
|
{
|
|
struct s3c2410_nand_info *info = s3c2410_nand_mtd_toinfo(mtd);
|
|
return readb(info->regs + S3C2410_NFSTAT) & S3C2410_NFSTAT_BUSY;
|
|
}
|
|
|
|
static int s3c2440_nand_devready(struct mtd_info *mtd)
|
|
{
|
|
struct s3c2410_nand_info *info = s3c2410_nand_mtd_toinfo(mtd);
|
|
return readb(info->regs + S3C2440_NFSTAT) & S3C2440_NFSTAT_READY;
|
|
}
|
|
|
|
static int s3c2412_nand_devready(struct mtd_info *mtd)
|
|
{
|
|
struct s3c2410_nand_info *info = s3c2410_nand_mtd_toinfo(mtd);
|
|
return readb(info->regs + S3C2412_NFSTAT) & S3C2412_NFSTAT_READY;
|
|
}
|
|
|
|
/* ECC handling functions */
|
|
|
|
static int s3c2410_nand_correct_data(struct mtd_info *mtd, u_char *dat,
|
|
u_char *read_ecc, u_char *calc_ecc)
|
|
{
|
|
struct s3c2410_nand_info *info = s3c2410_nand_mtd_toinfo(mtd);
|
|
unsigned int diff0, diff1, diff2;
|
|
unsigned int bit, byte;
|
|
|
|
pr_debug("%s(%p,%p,%p,%p)\n", __func__, mtd, dat, read_ecc, calc_ecc);
|
|
|
|
diff0 = read_ecc[0] ^ calc_ecc[0];
|
|
diff1 = read_ecc[1] ^ calc_ecc[1];
|
|
diff2 = read_ecc[2] ^ calc_ecc[2];
|
|
|
|
pr_debug("%s: rd %02x%02x%02x calc %02x%02x%02x diff %02x%02x%02x\n",
|
|
__func__,
|
|
read_ecc[0], read_ecc[1], read_ecc[2],
|
|
calc_ecc[0], calc_ecc[1], calc_ecc[2],
|
|
diff0, diff1, diff2);
|
|
|
|
if (diff0 == 0 && diff1 == 0 && diff2 == 0)
|
|
return 0; /* ECC is ok */
|
|
|
|
/* sometimes people do not think about using the ECC, so check
|
|
* to see if we have an 0xff,0xff,0xff read ECC and then ignore
|
|
* the error, on the assumption that this is an un-eccd page.
|
|
*/
|
|
if (read_ecc[0] == 0xff && read_ecc[1] == 0xff && read_ecc[2] == 0xff
|
|
&& info->platform->ignore_unset_ecc)
|
|
return 0;
|
|
|
|
/* Can we correct this ECC (ie, one row and column change).
|
|
* Note, this is similar to the 256 error code on smartmedia */
|
|
|
|
if (((diff0 ^ (diff0 >> 1)) & 0x55) == 0x55 &&
|
|
((diff1 ^ (diff1 >> 1)) & 0x55) == 0x55 &&
|
|
((diff2 ^ (diff2 >> 1)) & 0x55) == 0x55) {
|
|
/* calculate the bit position of the error */
|
|
|
|
bit = ((diff2 >> 3) & 1) |
|
|
((diff2 >> 4) & 2) |
|
|
((diff2 >> 5) & 4);
|
|
|
|
/* calculate the byte position of the error */
|
|
|
|
byte = ((diff2 << 7) & 0x100) |
|
|
((diff1 << 0) & 0x80) |
|
|
((diff1 << 1) & 0x40) |
|
|
((diff1 << 2) & 0x20) |
|
|
((diff1 << 3) & 0x10) |
|
|
((diff0 >> 4) & 0x08) |
|
|
((diff0 >> 3) & 0x04) |
|
|
((diff0 >> 2) & 0x02) |
|
|
((diff0 >> 1) & 0x01);
|
|
|
|
dev_dbg(info->device, "correcting error bit %d, byte %d\n",
|
|
bit, byte);
|
|
|
|
dat[byte] ^= (1 << bit);
|
|
return 1;
|
|
}
|
|
|
|
/* if there is only one bit difference in the ECC, then
|
|
* one of only a row or column parity has changed, which
|
|
* means the error is most probably in the ECC itself */
|
|
|
|
diff0 |= (diff1 << 8);
|
|
diff0 |= (diff2 << 16);
|
|
|
|
if ((diff0 & ~(1<<fls(diff0))) == 0)
|
|
return 1;
|
|
|
|
return -1;
|
|
}
|
|
|
|
/* ECC functions
|
|
*
|
|
* These allow the s3c2410 and s3c2440 to use the controller's ECC
|
|
* generator block to ECC the data as it passes through]
|
|
*/
|
|
|
|
static void s3c2410_nand_enable_hwecc(struct mtd_info *mtd, int mode)
|
|
{
|
|
struct s3c2410_nand_info *info = s3c2410_nand_mtd_toinfo(mtd);
|
|
unsigned long ctrl;
|
|
|
|
ctrl = readl(info->regs + S3C2410_NFCONF);
|
|
ctrl |= S3C2410_NFCONF_INITECC;
|
|
writel(ctrl, info->regs + S3C2410_NFCONF);
|
|
}
|
|
|
|
static void s3c2412_nand_enable_hwecc(struct mtd_info *mtd, int mode)
|
|
{
|
|
struct s3c2410_nand_info *info = s3c2410_nand_mtd_toinfo(mtd);
|
|
unsigned long ctrl;
|
|
|
|
ctrl = readl(info->regs + S3C2440_NFCONT);
|
|
writel(ctrl | S3C2412_NFCONT_INIT_MAIN_ECC, info->regs + S3C2440_NFCONT);
|
|
}
|
|
|
|
static void s3c2440_nand_enable_hwecc(struct mtd_info *mtd, int mode)
|
|
{
|
|
struct s3c2410_nand_info *info = s3c2410_nand_mtd_toinfo(mtd);
|
|
unsigned long ctrl;
|
|
|
|
ctrl = readl(info->regs + S3C2440_NFCONT);
|
|
writel(ctrl | S3C2440_NFCONT_INITECC, info->regs + S3C2440_NFCONT);
|
|
}
|
|
|
|
static int s3c2410_nand_calculate_ecc(struct mtd_info *mtd, const u_char *dat, u_char *ecc_code)
|
|
{
|
|
struct s3c2410_nand_info *info = s3c2410_nand_mtd_toinfo(mtd);
|
|
|
|
ecc_code[0] = readb(info->regs + S3C2410_NFECC + 0);
|
|
ecc_code[1] = readb(info->regs + S3C2410_NFECC + 1);
|
|
ecc_code[2] = readb(info->regs + S3C2410_NFECC + 2);
|
|
|
|
pr_debug("%s: returning ecc %02x%02x%02x\n", __func__,
|
|
ecc_code[0], ecc_code[1], ecc_code[2]);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int s3c2412_nand_calculate_ecc(struct mtd_info *mtd, const u_char *dat, u_char *ecc_code)
|
|
{
|
|
struct s3c2410_nand_info *info = s3c2410_nand_mtd_toinfo(mtd);
|
|
unsigned long ecc = readl(info->regs + S3C2412_NFMECC0);
|
|
|
|
ecc_code[0] = ecc;
|
|
ecc_code[1] = ecc >> 8;
|
|
ecc_code[2] = ecc >> 16;
|
|
|
|
pr_debug("calculate_ecc: returning ecc %02x,%02x,%02x\n", ecc_code[0], ecc_code[1], ecc_code[2]);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int s3c2440_nand_calculate_ecc(struct mtd_info *mtd, const u_char *dat, u_char *ecc_code)
|
|
{
|
|
struct s3c2410_nand_info *info = s3c2410_nand_mtd_toinfo(mtd);
|
|
unsigned long ecc = readl(info->regs + S3C2440_NFMECC0);
|
|
|
|
ecc_code[0] = ecc;
|
|
ecc_code[1] = ecc >> 8;
|
|
ecc_code[2] = ecc >> 16;
|
|
|
|
pr_debug("%s: returning ecc %06lx\n", __func__, ecc & 0xffffff);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* over-ride the standard functions for a little more speed. We can
|
|
* use read/write block to move the data buffers to/from the controller
|
|
*/
|
|
|
|
static void s3c2410_nand_read_buf(struct mtd_info *mtd, u_char *buf, int len)
|
|
{
|
|
struct nand_chip *this = mtd->priv;
|
|
readsb(this->IO_ADDR_R, buf, len);
|
|
}
|
|
|
|
static void s3c2440_nand_read_buf(struct mtd_info *mtd, u_char *buf, int len)
|
|
{
|
|
struct s3c2410_nand_info *info = s3c2410_nand_mtd_toinfo(mtd);
|
|
readsl(info->regs + S3C2440_NFDATA, buf, len / 4);
|
|
}
|
|
|
|
static void s3c2410_nand_write_buf(struct mtd_info *mtd, const u_char *buf, int len)
|
|
{
|
|
struct nand_chip *this = mtd->priv;
|
|
writesb(this->IO_ADDR_W, buf, len);
|
|
}
|
|
|
|
static void s3c2440_nand_write_buf(struct mtd_info *mtd, const u_char *buf, int len)
|
|
{
|
|
struct s3c2410_nand_info *info = s3c2410_nand_mtd_toinfo(mtd);
|
|
writesl(info->regs + S3C2440_NFDATA, buf, len / 4);
|
|
}
|
|
|
|
/* device management functions */
|
|
|
|
static int s3c2410_nand_remove(struct platform_device *pdev)
|
|
{
|
|
struct s3c2410_nand_info *info = to_nand_info(pdev);
|
|
|
|
platform_set_drvdata(pdev, NULL);
|
|
|
|
if (info == NULL)
|
|
return 0;
|
|
|
|
/* first thing we need to do is release all our mtds
|
|
* and their partitions, then go through freeing the
|
|
* resources used
|
|
*/
|
|
|
|
if (info->mtds != NULL) {
|
|
struct s3c2410_nand_mtd *ptr = info->mtds;
|
|
int mtdno;
|
|
|
|
for (mtdno = 0; mtdno < info->mtd_count; mtdno++, ptr++) {
|
|
pr_debug("releasing mtd %d (%p)\n", mtdno, ptr);
|
|
nand_release(&ptr->mtd);
|
|
}
|
|
|
|
kfree(info->mtds);
|
|
}
|
|
|
|
/* free the common resources */
|
|
|
|
if (info->clk != NULL && !IS_ERR(info->clk)) {
|
|
if (!allow_clk_stop(info))
|
|
clk_disable(info->clk);
|
|
clk_put(info->clk);
|
|
}
|
|
|
|
if (info->regs != NULL) {
|
|
iounmap(info->regs);
|
|
info->regs = NULL;
|
|
}
|
|
|
|
if (info->area != NULL) {
|
|
release_resource(info->area);
|
|
kfree(info->area);
|
|
info->area = NULL;
|
|
}
|
|
|
|
kfree(info);
|
|
|
|
return 0;
|
|
}
|
|
|
|
#ifdef CONFIG_MTD_PARTITIONS
|
|
static int s3c2410_nand_add_partition(struct s3c2410_nand_info *info,
|
|
struct s3c2410_nand_mtd *mtd,
|
|
struct s3c2410_nand_set *set)
|
|
{
|
|
if (set == NULL)
|
|
return add_mtd_device(&mtd->mtd);
|
|
|
|
if (set->nr_partitions > 0 && set->partitions != NULL) {
|
|
return add_mtd_partitions(&mtd->mtd, set->partitions, set->nr_partitions);
|
|
}
|
|
|
|
return add_mtd_device(&mtd->mtd);
|
|
}
|
|
#else
|
|
static int s3c2410_nand_add_partition(struct s3c2410_nand_info *info,
|
|
struct s3c2410_nand_mtd *mtd,
|
|
struct s3c2410_nand_set *set)
|
|
{
|
|
return add_mtd_device(&mtd->mtd);
|
|
}
|
|
#endif
|
|
|
|
/* s3c2410_nand_init_chip
|
|
*
|
|
* init a single instance of an chip
|
|
*/
|
|
|
|
static void s3c2410_nand_init_chip(struct s3c2410_nand_info *info,
|
|
struct s3c2410_nand_mtd *nmtd,
|
|
struct s3c2410_nand_set *set)
|
|
{
|
|
struct nand_chip *chip = &nmtd->chip;
|
|
void __iomem *regs = info->regs;
|
|
|
|
chip->write_buf = s3c2410_nand_write_buf;
|
|
chip->read_buf = s3c2410_nand_read_buf;
|
|
chip->select_chip = s3c2410_nand_select_chip;
|
|
chip->chip_delay = 50;
|
|
chip->priv = nmtd;
|
|
chip->options = 0;
|
|
chip->controller = &info->controller;
|
|
|
|
switch (info->cpu_type) {
|
|
case TYPE_S3C2410:
|
|
chip->IO_ADDR_W = regs + S3C2410_NFDATA;
|
|
info->sel_reg = regs + S3C2410_NFCONF;
|
|
info->sel_bit = S3C2410_NFCONF_nFCE;
|
|
chip->cmd_ctrl = s3c2410_nand_hwcontrol;
|
|
chip->dev_ready = s3c2410_nand_devready;
|
|
break;
|
|
|
|
case TYPE_S3C2440:
|
|
chip->IO_ADDR_W = regs + S3C2440_NFDATA;
|
|
info->sel_reg = regs + S3C2440_NFCONT;
|
|
info->sel_bit = S3C2440_NFCONT_nFCE;
|
|
chip->cmd_ctrl = s3c2440_nand_hwcontrol;
|
|
chip->dev_ready = s3c2440_nand_devready;
|
|
chip->read_buf = s3c2440_nand_read_buf;
|
|
chip->write_buf = s3c2440_nand_write_buf;
|
|
break;
|
|
|
|
case TYPE_S3C2412:
|
|
chip->IO_ADDR_W = regs + S3C2440_NFDATA;
|
|
info->sel_reg = regs + S3C2440_NFCONT;
|
|
info->sel_bit = S3C2412_NFCONT_nFCE0;
|
|
chip->cmd_ctrl = s3c2440_nand_hwcontrol;
|
|
chip->dev_ready = s3c2412_nand_devready;
|
|
|
|
if (readl(regs + S3C2410_NFCONF) & S3C2412_NFCONF_NANDBOOT)
|
|
dev_info(info->device, "System booted from NAND\n");
|
|
|
|
break;
|
|
}
|
|
|
|
chip->IO_ADDR_R = chip->IO_ADDR_W;
|
|
|
|
nmtd->info = info;
|
|
nmtd->mtd.priv = chip;
|
|
nmtd->mtd.owner = THIS_MODULE;
|
|
nmtd->set = set;
|
|
|
|
if (hardware_ecc) {
|
|
chip->ecc.calculate = s3c2410_nand_calculate_ecc;
|
|
chip->ecc.correct = s3c2410_nand_correct_data;
|
|
chip->ecc.mode = NAND_ECC_HW;
|
|
|
|
switch (info->cpu_type) {
|
|
case TYPE_S3C2410:
|
|
chip->ecc.hwctl = s3c2410_nand_enable_hwecc;
|
|
chip->ecc.calculate = s3c2410_nand_calculate_ecc;
|
|
break;
|
|
|
|
case TYPE_S3C2412:
|
|
chip->ecc.hwctl = s3c2412_nand_enable_hwecc;
|
|
chip->ecc.calculate = s3c2412_nand_calculate_ecc;
|
|
break;
|
|
|
|
case TYPE_S3C2440:
|
|
chip->ecc.hwctl = s3c2440_nand_enable_hwecc;
|
|
chip->ecc.calculate = s3c2440_nand_calculate_ecc;
|
|
break;
|
|
|
|
}
|
|
} else {
|
|
chip->ecc.mode = NAND_ECC_SOFT;
|
|
}
|
|
|
|
if (set->ecc_layout != NULL)
|
|
chip->ecc.layout = set->ecc_layout;
|
|
|
|
if (set->disable_ecc)
|
|
chip->ecc.mode = NAND_ECC_NONE;
|
|
}
|
|
|
|
/* s3c2410_nand_update_chip
|
|
*
|
|
* post-probe chip update, to change any items, such as the
|
|
* layout for large page nand
|
|
*/
|
|
|
|
static void s3c2410_nand_update_chip(struct s3c2410_nand_info *info,
|
|
struct s3c2410_nand_mtd *nmtd)
|
|
{
|
|
struct nand_chip *chip = &nmtd->chip;
|
|
|
|
dev_dbg(info->device, "chip %p => page shift %d\n",
|
|
chip, chip->page_shift);
|
|
|
|
if (hardware_ecc) {
|
|
/* change the behaviour depending on wether we are using
|
|
* the large or small page nand device */
|
|
|
|
if (chip->page_shift > 10) {
|
|
chip->ecc.size = 256;
|
|
chip->ecc.bytes = 3;
|
|
} else {
|
|
chip->ecc.size = 512;
|
|
chip->ecc.bytes = 3;
|
|
chip->ecc.layout = &nand_hw_eccoob;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* s3c2410_nand_probe
|
|
*
|
|
* called by device layer when it finds a device matching
|
|
* one our driver can handled. This code checks to see if
|
|
* it can allocate all necessary resources then calls the
|
|
* nand layer to look for devices
|
|
*/
|
|
|
|
static int s3c24xx_nand_probe(struct platform_device *pdev,
|
|
enum s3c_cpu_type cpu_type)
|
|
{
|
|
struct s3c2410_platform_nand *plat = to_nand_plat(pdev);
|
|
struct s3c2410_nand_info *info;
|
|
struct s3c2410_nand_mtd *nmtd;
|
|
struct s3c2410_nand_set *sets;
|
|
struct resource *res;
|
|
int err = 0;
|
|
int size;
|
|
int nr_sets;
|
|
int setno;
|
|
|
|
pr_debug("s3c2410_nand_probe(%p)\n", pdev);
|
|
|
|
info = kmalloc(sizeof(*info), GFP_KERNEL);
|
|
if (info == NULL) {
|
|
dev_err(&pdev->dev, "no memory for flash info\n");
|
|
err = -ENOMEM;
|
|
goto exit_error;
|
|
}
|
|
|
|
memzero(info, sizeof(*info));
|
|
platform_set_drvdata(pdev, info);
|
|
|
|
spin_lock_init(&info->controller.lock);
|
|
init_waitqueue_head(&info->controller.wq);
|
|
|
|
/* get the clock source and enable it */
|
|
|
|
info->clk = clk_get(&pdev->dev, "nand");
|
|
if (IS_ERR(info->clk)) {
|
|
dev_err(&pdev->dev, "failed to get clock\n");
|
|
err = -ENOENT;
|
|
goto exit_error;
|
|
}
|
|
|
|
clk_enable(info->clk);
|
|
|
|
/* allocate and map the resource */
|
|
|
|
/* currently we assume we have the one resource */
|
|
res = pdev->resource;
|
|
size = res->end - res->start + 1;
|
|
|
|
info->area = request_mem_region(res->start, size, pdev->name);
|
|
|
|
if (info->area == NULL) {
|
|
dev_err(&pdev->dev, "cannot reserve register region\n");
|
|
err = -ENOENT;
|
|
goto exit_error;
|
|
}
|
|
|
|
info->device = &pdev->dev;
|
|
info->platform = plat;
|
|
info->regs = ioremap(res->start, size);
|
|
info->cpu_type = cpu_type;
|
|
|
|
if (info->regs == NULL) {
|
|
dev_err(&pdev->dev, "cannot reserve register region\n");
|
|
err = -EIO;
|
|
goto exit_error;
|
|
}
|
|
|
|
dev_dbg(&pdev->dev, "mapped registers at %p\n", info->regs);
|
|
|
|
/* initialise the hardware */
|
|
|
|
err = s3c2410_nand_inithw(info, pdev);
|
|
if (err != 0)
|
|
goto exit_error;
|
|
|
|
sets = (plat != NULL) ? plat->sets : NULL;
|
|
nr_sets = (plat != NULL) ? plat->nr_sets : 1;
|
|
|
|
info->mtd_count = nr_sets;
|
|
|
|
/* allocate our information */
|
|
|
|
size = nr_sets * sizeof(*info->mtds);
|
|
info->mtds = kmalloc(size, GFP_KERNEL);
|
|
if (info->mtds == NULL) {
|
|
dev_err(&pdev->dev, "failed to allocate mtd storage\n");
|
|
err = -ENOMEM;
|
|
goto exit_error;
|
|
}
|
|
|
|
memzero(info->mtds, size);
|
|
|
|
/* initialise all possible chips */
|
|
|
|
nmtd = info->mtds;
|
|
|
|
for (setno = 0; setno < nr_sets; setno++, nmtd++) {
|
|
pr_debug("initialising set %d (%p, info %p)\n", setno, nmtd, info);
|
|
|
|
s3c2410_nand_init_chip(info, nmtd, sets);
|
|
|
|
nmtd->scan_res = nand_scan_ident(&nmtd->mtd,
|
|
(sets) ? sets->nr_chips : 1);
|
|
|
|
if (nmtd->scan_res == 0) {
|
|
s3c2410_nand_update_chip(info, nmtd);
|
|
nand_scan_tail(&nmtd->mtd);
|
|
s3c2410_nand_add_partition(info, nmtd, sets);
|
|
}
|
|
|
|
if (sets != NULL)
|
|
sets++;
|
|
}
|
|
|
|
if (allow_clk_stop(info)) {
|
|
dev_info(&pdev->dev, "clock idle support enabled\n");
|
|
clk_disable(info->clk);
|
|
}
|
|
|
|
pr_debug("initialised ok\n");
|
|
return 0;
|
|
|
|
exit_error:
|
|
s3c2410_nand_remove(pdev);
|
|
|
|
if (err == 0)
|
|
err = -EINVAL;
|
|
return err;
|
|
}
|
|
|
|
/* PM Support */
|
|
#ifdef CONFIG_PM
|
|
|
|
static int s3c24xx_nand_suspend(struct platform_device *dev, pm_message_t pm)
|
|
{
|
|
struct s3c2410_nand_info *info = platform_get_drvdata(dev);
|
|
|
|
if (info) {
|
|
info->save_sel = readl(info->sel_reg);
|
|
|
|
/* For the moment, we must ensure nFCE is high during
|
|
* the time we are suspended. This really should be
|
|
* handled by suspending the MTDs we are using, but
|
|
* that is currently not the case. */
|
|
|
|
writel(info->save_sel | info->sel_bit, info->sel_reg);
|
|
|
|
if (!allow_clk_stop(info))
|
|
clk_disable(info->clk);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int s3c24xx_nand_resume(struct platform_device *dev)
|
|
{
|
|
struct s3c2410_nand_info *info = platform_get_drvdata(dev);
|
|
unsigned long sel;
|
|
|
|
if (info) {
|
|
clk_enable(info->clk);
|
|
s3c2410_nand_inithw(info, dev);
|
|
|
|
/* Restore the state of the nFCE line. */
|
|
|
|
sel = readl(info->sel_reg);
|
|
sel &= ~info->sel_bit;
|
|
sel |= info->save_sel & info->sel_bit;
|
|
writel(sel, info->sel_reg);
|
|
|
|
if (allow_clk_stop(info))
|
|
clk_disable(info->clk);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
#else
|
|
#define s3c24xx_nand_suspend NULL
|
|
#define s3c24xx_nand_resume NULL
|
|
#endif
|
|
|
|
/* driver device registration */
|
|
|
|
static int s3c2410_nand_probe(struct platform_device *dev)
|
|
{
|
|
return s3c24xx_nand_probe(dev, TYPE_S3C2410);
|
|
}
|
|
|
|
static int s3c2440_nand_probe(struct platform_device *dev)
|
|
{
|
|
return s3c24xx_nand_probe(dev, TYPE_S3C2440);
|
|
}
|
|
|
|
static int s3c2412_nand_probe(struct platform_device *dev)
|
|
{
|
|
return s3c24xx_nand_probe(dev, TYPE_S3C2412);
|
|
}
|
|
|
|
static struct platform_driver s3c2410_nand_driver = {
|
|
.probe = s3c2410_nand_probe,
|
|
.remove = s3c2410_nand_remove,
|
|
.suspend = s3c24xx_nand_suspend,
|
|
.resume = s3c24xx_nand_resume,
|
|
.driver = {
|
|
.name = "s3c2410-nand",
|
|
.owner = THIS_MODULE,
|
|
},
|
|
};
|
|
|
|
static struct platform_driver s3c2440_nand_driver = {
|
|
.probe = s3c2440_nand_probe,
|
|
.remove = s3c2410_nand_remove,
|
|
.suspend = s3c24xx_nand_suspend,
|
|
.resume = s3c24xx_nand_resume,
|
|
.driver = {
|
|
.name = "s3c2440-nand",
|
|
.owner = THIS_MODULE,
|
|
},
|
|
};
|
|
|
|
static struct platform_driver s3c2412_nand_driver = {
|
|
.probe = s3c2412_nand_probe,
|
|
.remove = s3c2410_nand_remove,
|
|
.suspend = s3c24xx_nand_suspend,
|
|
.resume = s3c24xx_nand_resume,
|
|
.driver = {
|
|
.name = "s3c2412-nand",
|
|
.owner = THIS_MODULE,
|
|
},
|
|
};
|
|
|
|
static int __init s3c2410_nand_init(void)
|
|
{
|
|
printk("S3C24XX NAND Driver, (c) 2004 Simtec Electronics\n");
|
|
|
|
platform_driver_register(&s3c2412_nand_driver);
|
|
platform_driver_register(&s3c2440_nand_driver);
|
|
return platform_driver_register(&s3c2410_nand_driver);
|
|
}
|
|
|
|
static void __exit s3c2410_nand_exit(void)
|
|
{
|
|
platform_driver_unregister(&s3c2412_nand_driver);
|
|
platform_driver_unregister(&s3c2440_nand_driver);
|
|
platform_driver_unregister(&s3c2410_nand_driver);
|
|
}
|
|
|
|
module_init(s3c2410_nand_init);
|
|
module_exit(s3c2410_nand_exit);
|
|
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_AUTHOR("Ben Dooks <ben@simtec.co.uk>");
|
|
MODULE_DESCRIPTION("S3C24XX MTD NAND driver");
|
|
MODULE_ALIAS("platform:s3c2410-nand");
|
|
MODULE_ALIAS("platform:s3c2412-nand");
|
|
MODULE_ALIAS("platform:s3c2440-nand");
|