spl: nand: sunxi: add support for NAND config auto-detection
authorBoris Brezillon <boris.brezillon@free-electrons.com>
Mon, 6 Jun 2016 08:17:02 +0000 (10:17 +0200)
committerScott Wood <oss@buserror.net>
Mon, 20 Jun 2016 00:12:02 +0000 (19:12 -0500)
NAND chips are supposed to expose their capabilities through advanced
mechanisms like READID, ONFI or JEDEC parameter tables. While those
methods are appropriate for the bootloader itself, it's way to
complicated and takes too much space to fit in the SPL.

Replace those mechanisms by a dumb 'trial and error' mechanism.

With this new approach we can get rid of the fixed config list that was
used in the sunxi NAND SPL driver.

Signed-off-by: Boris Brezillon <boris.brezillon@free-electrons.com>
Acked-by: Hans de Goede <hdegoede@redhat.com>
drivers/mtd/nand/sunxi_nand_spl.c

index b43f2afc22fea557bbe3ea0f77dedbe4131a7179..1ef7366d4c42d0cd4fdcb77af7acd29bc5251d96 100644 (file)
@@ -103,6 +103,7 @@ struct nfc_config {
        int addr_cycles;
        int nseeds;
        bool randomize;
+       bool valid;
 };
 
 /* minimal "boot0" style NAND support for Allwinner A20 */
@@ -219,6 +220,26 @@ static int nand_load_page(const struct nfc_config *conf, u32 offs)
        return 0;
 }
 
+static int nand_reset_column(void)
+{
+       writel((NFC_CMD_RNDOUTSTART << NFC_RANDOM_READ_CMD1_OFFSET) |
+              (NFC_CMD_RNDOUT << NFC_RANDOM_READ_CMD0_OFFSET) |
+              (NFC_CMD_RNDOUTSTART << NFC_READ_CMD_OFFSET),
+              SUNXI_NFC_BASE + NFC_RCMD_SET);
+       writel(0, SUNXI_NFC_BASE + NFC_ADDR_LOW);
+       writel(NFC_SEND_CMD1 | NFC_SEND_CMD2 | NFC_RAW_CMD |
+              (1 << NFC_ADDR_NUM_OFFSET) | NFC_SEND_ADR | NFC_CMD_RNDOUT,
+              SUNXI_NFC_BASE + NFC_CMD);
+
+       if (!check_value(SUNXI_NFC_BASE + NFC_ST, NFC_ST_CMD_INT_FLAG,
+                        DEFAULT_TIMEOUT_US)) {
+               printf("Error while initializing dma interrupt\n");
+               return -1;
+       }
+
+       return 0;
+}
+
 static int nand_read_page(const struct nfc_config *conf, u32 offs,
                          void *dest, int len)
 {
@@ -303,88 +324,213 @@ static int nand_read_page(const struct nfc_config *conf, u32 offs,
        return (val & 0x10000) ? 1 : 0;
 }
 
-static int nand_read_ecc(int page_size, int ecc_strength, int ecc_page_size,
-       int addr_cycles, uint32_t offs, uint32_t size, void *dest)
+static int nand_max_ecc_strength(struct nfc_config *conf)
 {
-       void *end = dest + size;
-       static const u8 strengths[] = { 16, 24, 28, 32, 40, 48, 56, 60, 64 };
-       struct nfc_config conf = {
-               .page_size = page_size,
-               .ecc_size = ecc_page_size,
-               .addr_cycles = addr_cycles,
-               .nseeds = ARRAY_SIZE(random_seed),
-               .randomize = true,
-       };
+       static const int ecc_bytes[] = { 32, 46, 54, 60, 74, 88, 102, 110, 116 };
+       int max_oobsize, max_ecc_bytes;
+       int nsectors = conf->page_size / conf->ecc_size;
        int i;
 
-       for (i = 0; i < ARRAY_SIZE(strengths); i++) {
-               if (ecc_strength == strengths[i]) {
-                       conf.ecc_strength = i;
+       /*
+        * ECC strength is limited by the size of the OOB area which is
+        * correlated with the page size.
+        */
+       switch (conf->page_size) {
+       case 2048:
+               max_oobsize = 64;
+               break;
+       case 4096:
+               max_oobsize = 256;
+               break;
+       case 8192:
+               max_oobsize = 640;
+               break;
+       case 16384:
+               max_oobsize = 1664;
+               break;
+       default:
+               return -EINVAL;
+       }
+
+       max_ecc_bytes = max_oobsize / nsectors;
+
+       for (i = 0; i < ARRAY_SIZE(ecc_bytes); i++) {
+               if (ecc_bytes[i] > max_ecc_bytes)
                        break;
-               }
        }
 
+       if (!i)
+               return -EINVAL;
 
-       nand_apply_config(&conf);
+       return i - 1;
+}
 
-       for ( ;dest < end; dest += ecc_page_size, offs += page_size) {
-               if (nand_load_page(&conf, offs))
-                       return -1;
+static int nand_detect_ecc_config(struct nfc_config *conf, u32 offs,
+                                 void *dest)
+{
+       /* NAND with pages > 4k will likely require 1k sector size. */
+       int min_ecc_size = conf->page_size > 4096 ? 1024 : 512;
+       int page = offs / conf->page_size;
+       int ret;
 
-               if (nand_read_page(&conf, offs, dest, page_size))
-                       return -1;
+       /*
+        * In most cases, 1k sectors are preferred over 512b ones, start
+        * testing this config first.
+        */
+       for (conf->ecc_size = 1024; conf->ecc_size >= min_ecc_size;
+            conf->ecc_size >>= 1) {
+               int max_ecc_strength = nand_max_ecc_strength(conf);
+
+               nand_apply_config(conf);
+
+               /*
+                * We are starting from the maximum ECC strength because
+                * most of the time NAND vendors provide an OOB area that
+                * barely meets the ECC requirements.
+                */
+               for (conf->ecc_strength = max_ecc_strength;
+                    conf->ecc_strength >= 0;
+                    conf->ecc_strength--) {
+                       conf->randomize = false;
+                       if (nand_reset_column())
+                               return -EIO;
+
+                       /*
+                        * Only read the first sector to speedup detection.
+                        */
+                       ret = nand_read_page(conf, offs, dest, conf->ecc_size);
+                       if (!ret) {
+                               return 0;
+                       } else if (ret > 0) {
+                               /*
+                                * If page is empty we can't deduce anything
+                                * about the ECC config => stop the detection.
+                                */
+                               return -EINVAL;
+                       }
+
+                       conf->randomize = true;
+                       conf->nseeds = ARRAY_SIZE(random_seed);
+                       do {
+                               if (nand_reset_column())
+                                       return -EIO;
+
+                               if (!nand_read_page(conf, offs, dest,
+                                                   conf->ecc_size))
+                                       return 0;
+
+                               /*
+                                * Find the next ->nseeds value that would
+                                * change the randomizer seed for the page
+                                * we're trying to read.
+                                */
+                               while (conf->nseeds >= 16) {
+                                       int seed = page % conf->nseeds;
+
+                                       conf->nseeds >>= 1;
+                                       if (seed != page % conf->nseeds)
+                                               break;
+                               }
+                       } while (conf->nseeds >= 16);
+               }
        }
 
-       return 0;
+       return -EINVAL;
 }
 
-static int nand_read_buffer(uint32_t offs, unsigned int size, void *dest)
+static int nand_detect_config(struct nfc_config *conf, u32 offs, void *dest)
 {
-       const struct {
-               int page_size;
-               int ecc_strength;
-               int ecc_page_size;
-               int addr_cycles;
-       } nand_configs[] = {
-               {  8192, 40, 1024, 5 },
-               { 16384, 56, 1024, 5 },
-               {  8192, 24, 1024, 5 },
-               {  4096, 24, 1024, 5 },
-       };
-       static int nand_config = -1;
-       int i;
+       if (conf->valid)
+               return 0;
 
-       if (nand_config == -1) {
-               for (i = 0; i < ARRAY_SIZE(nand_configs); i++) {
-                       debug("nand: trying page %d ecc %d / %d addr %d: ",
-                             nand_configs[i].page_size,
-                             nand_configs[i].ecc_strength,
-                             nand_configs[i].ecc_page_size,
-                             nand_configs[i].addr_cycles);
-                       if (nand_read_ecc(nand_configs[i].page_size,
-                                         nand_configs[i].ecc_strength,
-                                         nand_configs[i].ecc_page_size,
-                                         nand_configs[i].addr_cycles,
-                                         offs, size, dest) == 0) {
-                               debug("success\n");
-                               nand_config = i;
+       /*
+        * Modern NANDs are more likely than legacy ones, so we start testing
+        * with 5 address cycles.
+        */
+       for (conf->addr_cycles = 5;
+            conf->addr_cycles >= 4;
+            conf->addr_cycles--) {
+               int max_page_size = conf->addr_cycles == 4 ? 2048 : 16384;
+
+               /*
+                * Ignoring 1k pages cause I'm not even sure this case exist
+                * in the real world.
+                */
+               for (conf->page_size = 2048; conf->page_size <= max_page_size;
+                    conf->page_size <<= 1) {
+                       if (nand_load_page(conf, offs))
+                               return -1;
+
+                       if (!nand_detect_ecc_config(conf, offs, dest)) {
+                               conf->valid = true;
                                return 0;
                        }
-                       debug("failed\n");
                }
-               return -1;
        }
 
-       return nand_read_ecc(nand_configs[nand_config].page_size,
-                            nand_configs[nand_config].ecc_strength,
-                            nand_configs[nand_config].ecc_page_size,
-                            nand_configs[nand_config].addr_cycles,
-                            offs, size, dest);
+       return -EINVAL;
+}
+
+static int nand_read_buffer(struct nfc_config *conf, uint32_t offs,
+                           unsigned int size, void *dest)
+{
+       int first_seed, page, ret;
+
+       size = ALIGN(size, conf->page_size);
+       page = offs / conf->page_size;
+       first_seed = page % conf->nseeds;
+
+       for (; size; size -= conf->page_size) {
+               if (nand_load_page(conf, offs))
+                       return -1;
+
+               ret = nand_read_page(conf, offs, dest, conf->page_size);
+               /*
+                * The ->nseeds value should be equal to the number of pages
+                * in an eraseblock. Since we don't know this information in
+                * advance we might have picked a wrong value.
+                */
+               if (ret < 0 && conf->randomize) {
+                       int cur_seed = page % conf->nseeds;
+
+                       /*
+                        * We already tried all the seed values => we are
+                        * facing a real corruption.
+                        */
+                       if (cur_seed < first_seed)
+                               return -EIO;
+
+                       /* Try to adjust ->nseeds and read the page again... */
+                       conf->nseeds = cur_seed;
+
+                       if (nand_reset_column())
+                               return -EIO;
+
+                       /* ... it still fails => it's a real corruption. */
+                       if (nand_read_page(conf, offs, dest, conf->page_size))
+                               return -EIO;
+               } else if (ret && conf->randomize) {
+                       memset(dest, 0xff, conf->page_size);
+               }
+
+               page++;
+               offs += conf->page_size;
+               dest += conf->page_size;
+       }
+
+       return 0;
 }
 
 int nand_spl_load_image(uint32_t offs, unsigned int size, void *dest)
 {
-       return nand_read_buffer(offs, size, dest);
+       static struct nfc_config conf = { };
+       int ret;
+
+       ret = nand_detect_config(&conf, offs, dest);
+       if (ret)
+               return ret;
+
+       return nand_read_buffer(&conf, offs, size, dest);
 }
 
 void nand_deselect(void)