]> git.dujemihanovic.xyz Git - u-boot.git/commitdiff
mtd: spi-nor: Add parallel and stacked memories support
authorVenkatesh Yadav Abbarapu <venkatesh.abbarapu@amd.com>
Thu, 26 Sep 2024 04:55:02 +0000 (10:25 +0530)
committerTom Rini <trini@konsulko.com>
Wed, 9 Oct 2024 15:01:54 +0000 (09:01 -0600)
In parallel mode, the current implementation assumes that a maximum of
two flashes are connected. The QSPI controller splits the data evenly
between both the flashes so, both the flashes that are connected in
parallel mode should be identical.
During each operation SPI-NOR sets 0th bit for CS0 & 1st bit for CS1 in
nor->flags.

In stacked mode the current implementation assumes that a maximum of two
flashes are connected and both the flashes are of same make but can
differ in sizes. So, except the sizes all other flash parameters of both
the flashes are identical

Spi-nor will pass on the appropriate flash select flag to low level
driver, and it will select pass all the data to that particular flash.

Write operation in parallel mode are performed in page size * 2 chunks as
each write operation results in writing both the flashes. For doubling
the address space each operation is performed at addr/2 flash offset,
where addr is the address specified by the user.

Similarly for read and erase operations it will read from both flashes,
so size and offset are divided by 2 and send to flash.

Adding the config option SPI_ADVANCE for non SPL code.

Signed-off-by: Ashok Reddy Soma <ashok.reddy.soma@amd.com>
Signed-off-by: Venkatesh Yadav Abbarapu <venkatesh.abbarapu@amd.com>
drivers/mtd/spi/spi-nor-core.c
drivers/spi/Kconfig
include/linux/mtd/spi-nor.h
include/spi.h

index 8f7a77e716933e783f5f59d377abaad1a1ea9e71..8ba3a9bd60e238b7d5ffa759feb4b3587ebe64c9 100644 (file)
@@ -668,12 +668,17 @@ static u8 spi_nor_convert_3to4_erase(u8 opcode)
 static void spi_nor_set_4byte_opcodes(struct spi_nor *nor,
                                      const struct flash_info *info)
 {
+       bool shift = 0;
+
+       if (nor->flags & SNOR_F_HAS_PARALLEL)
+               shift = 1;
+
        /* Do some manufacturer fixups first */
        switch (JEDEC_MFR(info)) {
        case SNOR_MFR_SPANSION:
                /* No small sector erase for 4-byte command set */
                nor->erase_opcode = SPINOR_OP_SE;
-               nor->mtd.erasesize = info->sector_size;
+               nor->mtd.erasesize = info->sector_size << shift;
                break;
 
        default:
@@ -1008,8 +1013,8 @@ static int spi_nor_erase_sector(struct spi_nor *nor, u32 addr)
 static int spi_nor_erase(struct mtd_info *mtd, struct erase_info *instr)
 {
        struct spi_nor *nor = mtd_to_spi_nor(mtd);
+       u32 addr, len, rem, offset, max_size;
        bool addr_known = false;
-       u32 addr, len, rem, max_size;
        int ret, err;
 
        dev_dbg(nor->dev, "at 0x%llx, len %lld\n", (long long)instr->addr,
@@ -1035,6 +1040,18 @@ static int spi_nor_erase(struct mtd_info *mtd, struct erase_info *instr)
                        ret = -EINTR;
                        goto erase_err;
                }
+               offset = addr;
+               if (nor->flags & SNOR_F_HAS_PARALLEL)
+                       offset /= 2;
+
+               if (nor->flags & SNOR_F_HAS_STACKED) {
+                       if (offset >= (mtd->size / 2)) {
+                               offset = offset - (mtd->size / 2);
+                               nor->spi->flags |= SPI_XFER_U_PAGE;
+                       } else {
+                               nor->spi->flags &= ~SPI_XFER_U_PAGE;
+                       }
+               }
 #ifdef CONFIG_SPI_FLASH_BAR
                ret = write_bar(nor, addr);
                if (ret < 0)
@@ -1446,6 +1463,9 @@ static const struct flash_info *spi_nor_read_id(struct spi_nor *nor)
        u8                      id[SPI_NOR_MAX_ID_LEN];
        const struct flash_info *info;
 
+       if (nor->flags & SNOR_F_HAS_PARALLEL)
+               nor->spi->flags |= SPI_XFER_LOWER;
+
        tmp = nor->read_reg(nor, SPINOR_OP_RDID, id, SPI_NOR_MAX_ID_LEN);
        if (tmp < 0) {
                dev_dbg(nor->dev, "error %d reading JEDEC ID\n", tmp);
@@ -1470,28 +1490,67 @@ static int spi_nor_read(struct mtd_info *mtd, loff_t from, size_t len,
 {
        struct spi_nor *nor = mtd_to_spi_nor(mtd);
        int ret;
+       loff_t offset = from;
+       u32 read_len = 0;
+       u32 rem_bank_len = 0;
+       u8 bank;
+       bool is_ofst_odd = false;
 
        dev_dbg(nor->dev, "from 0x%08x, len %zd\n", (u32)from, len);
 
+       if ((nor->flags & SNOR_F_HAS_PARALLEL) && (offset & 1)) {
+           /* We can hit this case when we use file system like ubifs */
+               from--;
+               len++;
+               is_ofst_odd = true;
+       }
+
        while (len) {
-               loff_t addr = from;
-               size_t read_len = len;
+               if (nor->addr_width == 3) {
+                       if (nor->flags & SNOR_F_HAS_PARALLEL) {
+                               bank = (u32)from / (SZ_16M << 0x01);
+                               rem_bank_len = ((SZ_16M << 0x01) *
+                                       (bank + 1)) - from;
+                       } else {
+                               bank = (u32)from / SZ_16M;
+                               rem_bank_len = (SZ_16M * (bank + 1)) - from;
+                       }
+               }
+               offset = from;
 
-#ifdef CONFIG_SPI_FLASH_BAR
-               u32 remain_len;
+               if (nor->flags & SNOR_F_HAS_STACKED) {
+                       if (offset >= (mtd->size / 2)) {
+                               offset = offset - (mtd->size / 2);
+                               nor->spi->flags |= SPI_XFER_U_PAGE;
+                       } else {
+                               nor->spi->flags &= ~SPI_XFER_U_PAGE;
+                       }
+               }
 
-               ret = write_bar(nor, addr);
-               if (ret < 0)
-                       return log_ret(ret);
-               remain_len = (SZ_16M * (nor->bank_curr + 1)) - addr;
+               if (nor->flags & SNOR_F_HAS_PARALLEL)
+                       offset /= 2;
 
-               if (len < remain_len)
+               if (nor->addr_width == 3) {
+#ifdef CONFIG_SPI_FLASH_BAR
+                       ret = write_bar(nor, offset);
+                       if (ret < 0)
+                               return log_ret(ret);
+#endif
+               }
+
+               if (len < rem_bank_len)
                        read_len = len;
                else
-                       read_len = remain_len;
-#endif
+                       read_len = rem_bank_len;
+
+               if (read_len == 0)
+                       return -EIO;
+
+               ret = spi_nor_wait_till_ready(nor);
+               if (ret)
+                       goto read_err;
 
-               ret = nor->read(nor, addr, read_len, buf);
+               ret = nor->read(nor, offset, read_len, buf);
                if (ret == 0) {
                        /* We shouldn't see 0-length reads */
                        ret = -EIO;
@@ -1500,8 +1559,15 @@ static int spi_nor_read(struct mtd_info *mtd, loff_t from, size_t len,
                if (ret < 0)
                        goto read_err;
 
-               *retlen += ret;
-               buf += ret;
+               if (is_ofst_odd == true) {
+                       memmove(buf, (buf + 1), (len - 1));
+                       *retlen += (ret - 1);
+                       buf += ret - 1;
+                       is_ofst_odd = false;
+               } else {
+                       *retlen += ret;
+                       buf += ret;
+               }
                from += ret;
                len -= ret;
        }
@@ -1796,6 +1862,7 @@ static int spi_nor_write(struct mtd_info *mtd, loff_t to, size_t len,
        struct spi_nor *nor = mtd_to_spi_nor(mtd);
        size_t page_offset, page_remain, i;
        ssize_t ret;
+       u32 offset;
 
 #ifdef CONFIG_SPI_FLASH_SST
        /* sst nor chips use AAI word program */
@@ -1805,6 +1872,27 @@ static int spi_nor_write(struct mtd_info *mtd, loff_t to, size_t len,
 
        dev_dbg(nor->dev, "to 0x%08x, len %zd\n", (u32)to, len);
 
+       if (!len)
+               return 0;
+
+       /*
+        * Cannot write to odd offset in parallel mode,
+        * so write 2 bytes first
+        */
+       if ((nor->flags & SNOR_F_HAS_PARALLEL) && (to & 1)) {
+               u8 two[2] = {0xff, buf[0]};
+               size_t local_retlen;
+
+               ret = spi_nor_write(mtd, to & ~1, 2, &local_retlen, two);
+               if (ret < 0)
+                       return ret;
+
+               *retlen += 1; /* We've written only one actual byte */
+               ++buf;
+               --len;
+               ++to;
+       }
+
        for (i = 0; i < len; ) {
                ssize_t written;
                loff_t addr = to + i;
@@ -1822,18 +1910,35 @@ static int spi_nor_write(struct mtd_info *mtd, loff_t to, size_t len,
 
                        page_offset = do_div(aux, nor->page_size);
                }
+               offset = (to + i);
+               if (nor->flags & SNOR_F_HAS_PARALLEL)
+                       offset /= 2;
+
+               if (nor->flags & SNOR_F_HAS_STACKED) {
+                       if (offset >= (mtd->size / 2)) {
+                               offset = offset - (mtd->size / 2);
+                               nor->spi->flags |= SPI_XFER_U_PAGE;
+                       } else {
+                               nor->spi->flags &= ~SPI_XFER_U_PAGE;
+                       }
+               }
+
+               if (nor->addr_width == 3) {
+#ifdef CONFIG_SPI_FLASH_BAR
+                       ret = write_bar(nor, offset);
+                       if (ret < 0)
+                               return ret;
+#endif
+               }
                /* the size of data remaining on the first page */
                page_remain = min_t(size_t,
                                    nor->page_size - page_offset, len - i);
 
-#ifdef CONFIG_SPI_FLASH_BAR
-               ret = write_bar(nor, addr);
-               if (ret < 0)
-                       return ret;
-#endif
+               ret = spi_nor_wait_till_ready(nor);
+               if (ret)
+                       goto write_err;
 
                write_enable(nor);
-
                /*
                 * On DTR capable flashes like Micron Xcella the writes cannot
                 * start or end at an odd address in DTR mode. So we need to
@@ -1841,7 +1946,7 @@ static int spi_nor_write(struct mtd_info *mtd, loff_t to, size_t len,
                 * address and end address are even.
                 */
                if (spi_nor_protocol_is_dtr(nor->write_proto) &&
-                   ((addr | page_remain) & 1)) {
+                   ((offset | page_remain) & 1)) {
                        u_char *tmp;
                        size_t extra_bytes = 0;
 
@@ -1852,10 +1957,10 @@ static int spi_nor_write(struct mtd_info *mtd, loff_t to, size_t len,
                        }
 
                        /* Prepend a 0xff byte if the start address is odd. */
-                       if (addr & 1) {
+                       if (offset & 1) {
                                tmp[0] = 0xff;
                                memcpy(tmp + 1, buf + i, page_remain);
-                               addr--;
+                               offset--;
                                page_remain++;
                                extra_bytes++;
                        } else {
@@ -1863,13 +1968,13 @@ static int spi_nor_write(struct mtd_info *mtd, loff_t to, size_t len,
                        }
 
                        /* Append a 0xff byte if the end address is odd. */
-                       if ((addr + page_remain) & 1) {
+                       if ((offset + page_remain) & 1) {
                                tmp[page_remain + extra_bytes] = 0xff;
                                extra_bytes++;
                                page_remain++;
                        }
 
-                       ret = nor->write(nor, addr, page_remain, tmp);
+                       ret = nor->write(nor, offset, page_remain, tmp);
 
                        kfree(tmp);
 
@@ -1882,7 +1987,7 @@ static int spi_nor_write(struct mtd_info *mtd, loff_t to, size_t len,
                         */
                        written = ret - extra_bytes;
                } else {
-                       ret = nor->write(nor, addr, page_remain, buf + i);
+                       ret = nor->write(nor, offset, page_remain, buf + i);
                        if (ret < 0)
                                goto write_err;
                        written = ret;
@@ -1891,6 +1996,11 @@ static int spi_nor_write(struct mtd_info *mtd, loff_t to, size_t len,
                ret = spi_nor_wait_till_ready(nor);
                if (ret)
                        goto write_err;
+
+               ret = write_disable(nor);
+               if (ret)
+                       goto write_err;
+
                *retlen += written;
                i += written;
        }
@@ -1931,6 +2041,10 @@ static int macronix_quad_enable(struct spi_nor *nor)
        if (ret)
                return ret;
 
+       ret = write_disable(nor);
+       if (ret)
+               return ret;
+
        ret = read_sr(nor);
        if (!(ret > 0 && (ret & SR_QUAD_EN_MX))) {
                dev_err(nor->dev, "Macronix Quad bit not set\n");
@@ -1992,7 +2106,7 @@ static int spansion_quad_enable_volatile(struct spi_nor *nor, u32 addr_base,
                return -EINVAL;
        }
 
-       return 0;
+       return write_disable(nor);
 }
 #endif
 
@@ -2168,6 +2282,10 @@ static int spi_nor_read_sfdp(struct spi_nor *nor, u32 addr,
        nor->read_dummy = 8;
 
        while (len) {
+               /* Both chips are identical, so should be the SFDP data */
+               if (nor->flags & SNOR_F_HAS_PARALLEL)
+                       nor->spi->flags |= SPI_XFER_LOWER;
+
                ret = nor->read(nor, addr, len, (u8 *)buf);
                if (!ret || ret > len) {
                        ret = -EIO;
@@ -2862,6 +2980,13 @@ static int spi_nor_init_params(struct spi_nor *nor,
                               const struct flash_info *info,
                               struct spi_nor_flash_parameter *params)
 {
+#if CONFIG_IS_ENABLED(DM_SPI) && CONFIG_IS_ENABLED(SPI_ADVANCE)
+       struct udevice *dev = nor->spi->dev;
+       u64 flash_size[SNOR_FLASH_CNT_MAX] = {0};
+       u32 idx = 0, i = 0;
+       int rc;
+#endif
+
        /* Set legacy flash parameters as default. */
        memset(params, 0, sizeof(*params));
 
@@ -2979,7 +3104,62 @@ static int spi_nor_init_params(struct spi_nor *nor,
                        memcpy(params, &sfdp_params, sizeof(*params));
                }
        }
+#if CONFIG_IS_ENABLED(DM_SPI) && CONFIG_IS_ENABLED(SPI_ADVANCE)
+       /*
+        * The flashes that are connected in stacked mode should be of same make.
+        * Except the flash size all other properties are identical for all the
+        * flashes connected in stacked mode.
+        * The flashes that are connected in parallel mode should be identical.
+        */
+       while (i < SNOR_FLASH_CNT_MAX) {
+               rc = ofnode_read_u64_index(dev_ofnode(dev), "stacked-memories",
+                                          idx, &flash_size[i]);
+               if (rc == -EINVAL) {
+                       break;
+               } else if (rc == -EOVERFLOW) {
+                       idx++;
+               } else {
+                       idx++;
+                       i++;
+                       if (!(nor->flags & SNOR_F_HAS_STACKED))
+                               nor->flags |= SNOR_F_HAS_STACKED;
+                       if (!(nor->spi->flags & SPI_XFER_STACKED))
+                               nor->spi->flags |= SPI_XFER_STACKED;
+               }
+       }
+
+       i = 0;
+       idx = 0;
+       while (i < SNOR_FLASH_CNT_MAX) {
+               rc = ofnode_read_u64_index(dev_ofnode(dev), "parallel-memories",
+                                          idx, &flash_size[i]);
+               if (rc == -EINVAL) {
+                       break;
+               } else if (rc == -EOVERFLOW) {
+                       idx++;
+               } else {
+                       idx++;
+                       i++;
+                       if (!(nor->flags & SNOR_F_HAS_PARALLEL))
+                               nor->flags |= SNOR_F_HAS_PARALLEL;
+               }
+       }
 
+       if (nor->flags & (SNOR_F_HAS_STACKED | SNOR_F_HAS_PARALLEL)) {
+               params->size = 0;
+               for (idx = 0; idx < SNOR_FLASH_CNT_MAX; idx++)
+                       params->size += flash_size[idx];
+       }
+       /*
+        * In parallel-memories the erase operation is
+        * performed on both the flashes simultaneously
+        * so, double the erasesize.
+        */
+       if (nor->flags & SNOR_F_HAS_PARALLEL) {
+               nor->mtd.erasesize <<= 1;
+               params->page_size <<= 1;
+       }
+#endif
        spi_nor_post_sfdp_fixups(nor, params);
 
        return 0;
@@ -3294,16 +3474,54 @@ static int spi_nor_select_erase(struct spi_nor *nor,
        /* prefer "small sector" erase if possible */
        if (info->flags & SECT_4K) {
                nor->erase_opcode = SPINOR_OP_BE_4K;
-               mtd->erasesize = 4096;
+               /*
+                * In parallel-memories the erase operation is
+                * performed on both the flashes simultaneously
+                * so, double the erasesize.
+                */
+               if (nor->flags & SNOR_F_HAS_PARALLEL)
+                       mtd->erasesize = 4096 * 2;
+               else
+                       mtd->erasesize = 4096;
        } else if (info->flags & SECT_4K_PMC) {
                nor->erase_opcode = SPINOR_OP_BE_4K_PMC;
-               mtd->erasesize = 4096;
+               /*
+                * In parallel-memories the erase operation is
+                * performed on both the flashes simultaneously
+                * so, double the erasesize.
+                */
+               if (nor->flags & SNOR_F_HAS_PARALLEL)
+                       mtd->erasesize = 4096 * 2;
+               else
+                       mtd->erasesize = 4096;
        } else
 #endif
        {
                nor->erase_opcode = SPINOR_OP_SE;
-               mtd->erasesize = info->sector_size;
+               /*
+                * In parallel-memories the erase operation is
+                * performed on both the flashes simultaneously
+                * so, double the erasesize.
+                */
+               if (nor->flags & SNOR_F_HAS_PARALLEL)
+                       mtd->erasesize = info->sector_size * 2;
+               else
+                       mtd->erasesize = info->sector_size;
+       }
+
+       if ((JEDEC_MFR(info) == SNOR_MFR_SST) && info->flags & SECT_4K) {
+               nor->erase_opcode = SPINOR_OP_BE_4K;
+               /*
+                * In parallel-memories the erase operation is
+                * performed on both the flashes simultaneously
+                * so, double the erasesize.
+                */
+               if (nor->flags & SNOR_F_HAS_PARALLEL)
+                       mtd->erasesize = 4096 * 2;
+               else
+                       mtd->erasesize = 4096;
        }
+
        return 0;
 }
 
@@ -3925,6 +4143,9 @@ static int spi_nor_init(struct spi_nor *nor)
 {
        int err;
 
+       if (nor->flags & SNOR_F_HAS_PARALLEL)
+               nor->spi->flags |= SPI_NOR_ENABLE_MULTI_CS;
+
        err = spi_nor_octal_dtr_enable(nor);
        if (err) {
                dev_dbg(nor->dev, "Octal DTR mode not supported\n");
@@ -4091,6 +4312,7 @@ int spi_nor_scan(struct spi_nor *nor)
        struct spi_slave *spi = nor->spi;
        int ret;
        int cfi_mtd_nb = 0;
+       bool shift = 0;
 
 #ifdef CONFIG_FLASH_CFI_MTD
        cfi_mtd_nb = CFI_FLASH_BANKS;
@@ -4228,7 +4450,9 @@ int spi_nor_scan(struct spi_nor *nor)
                nor->addr_width = 3;
        }
 
-       if (nor->addr_width == 3 && mtd->size > SZ_16M) {
+       if (nor->flags & (SNOR_F_HAS_PARALLEL | SNOR_F_HAS_STACKED))
+               shift = 1;
+       if (nor->addr_width == 3 && (mtd->size >> shift) > SZ_16M) {
 #ifndef CONFIG_SPI_FLASH_BAR
                /* enable 4-byte addressing if the device exceeds 16MiB */
                nor->addr_width = 4;
@@ -4238,6 +4462,7 @@ int spi_nor_scan(struct spi_nor *nor)
 #else
        /* Configure the BAR - discover bank cmds and read current bank */
        nor->addr_width = 3;
+       set_4byte(nor, info, 0);
        ret = read_bar(nor, info);
        if (ret < 0)
                return ret;
@@ -4255,6 +4480,14 @@ int spi_nor_scan(struct spi_nor *nor)
        if (ret)
                return ret;
 
+       if (nor->flags & SNOR_F_HAS_STACKED) {
+               nor->spi->flags |= SPI_XFER_U_PAGE;
+               ret = spi_nor_init(nor);
+               if (ret)
+                       return ret;
+               nor->spi->flags &= ~SPI_XFER_U_PAGE;
+       }
+
        nor->rdsr_dummy = params.rdsr_dummy;
        nor->rdsr_addr_nbytes = params.rdsr_addr_nbytes;
        nor->name = info->name;
index cd785aefd56e8625dd21c76c59efc5edc9e6ba8a..18dfd1796fc51d1b25928ce3cd2569eb920f1446 100644 (file)
@@ -20,6 +20,12 @@ menuconfig SPI
 
 if SPI
 
+config SPI_ADVANCE
+       bool "Enable the advance feature"
+       help
+        Enable the SPI advance feature support. By default this is disabled.
+        If you intend to use the advance feature support you should enable.
+
 config DM_SPI
        bool "Enable Driver Model for SPI drivers"
        depends on DM
index d1dbf3eadbf7cc49212b42876f33dc21879f047a..d5f4faf0a68d089a666c87a876016bd9bb222d97 100644 (file)
@@ -13,6 +13,9 @@
 #include <linux/mtd/mtd.h>
 #include <spi-mem.h>
 
+/* In parallel configuration enable multiple CS */
+#define SPI_NOR_ENABLE_MULTI_CS        (BIT(0) | BIT(1))
+
 /*
  * Manufacturer IDs
  *
 /* Status Register 2 bits. */
 #define SR2_QUAD_EN_BIT7       BIT(7)
 
+/*
+ * Maximum number of flashes that can be connected
+ * in stacked/parallel configuration
+ */
+#define SNOR_FLASH_CNT_MAX     2
+
 /* For Cypress flash. */
 #define SPINOR_OP_RD_ANY_REG                   0x65    /* Read any register */
 #define SPINOR_OP_WR_ANY_REG                   0x71    /* Write any register */
@@ -294,6 +303,13 @@ enum spi_nor_option_flags {
        SNOR_F_BROKEN_RESET     = BIT(6),
        SNOR_F_SOFT_RESET       = BIT(7),
        SNOR_F_IO_MODE_EN_VOLATILE = BIT(8),
+#if defined(CONFIG_SPI_ADVANCE)
+       SNOR_F_HAS_STACKED      = BIT(9),
+       SNOR_F_HAS_PARALLEL     = BIT(10),
+#else
+       SNOR_F_HAS_STACKED      = 0,
+       SNOR_F_HAS_PARALLEL     = 0,
+#endif
 };
 
 struct spi_nor;
@@ -551,6 +567,7 @@ struct spi_nor {
        u8                      bank_read_cmd;
        u8                      bank_write_cmd;
        u8                      bank_curr;
+       u8                      upage_prev;
 #endif
        enum spi_nor_protocol   read_proto;
        enum spi_nor_protocol   write_proto;
index 9e9851284c864a8659dc119a9fb0dce0cf251cda..6e8e0cce7f25e481b89cf158ce2fea52fdcd35f3 100644 (file)
 
 #define SPI_DEFAULT_WORDLEN    8
 
+/* SPI transfer flags */
+#define SPI_XFER_STRIPE        (1 << 6)
+#define SPI_XFER_MASK  (3 << 8)
+#define SPI_XFER_LOWER (1 << 8)
+#define SPI_XFER_UPPER (2 << 8)
+
+/* Max no. of CS supported per spi device */
+#define SPI_CS_CNT_MAX 2
+
 /**
  * struct dm_spi_bus - SPI bus info
  *
@@ -155,6 +164,8 @@ struct spi_slave {
 #define SPI_XFER_BEGIN         BIT(0)  /* Assert CS before transfer */
 #define SPI_XFER_END           BIT(1)  /* Deassert CS after transfer */
 #define SPI_XFER_ONCE          (SPI_XFER_BEGIN | SPI_XFER_END)
+#define SPI_XFER_U_PAGE                BIT(4)
+#define SPI_XFER_STACKED       BIT(5)
 };
 
 /**