693 lines
16 KiB
C
Executable File
693 lines
16 KiB
C
Executable File
/*
|
|
* mtd_isp.c- Sigmastar
|
|
*
|
|
* Copyright (C) 2018 Sigmastar Technology Corp.
|
|
*
|
|
* Author: karl.xiao <karl.xiao@sigmastar.com.tw>
|
|
*
|
|
* 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.
|
|
*
|
|
*/
|
|
|
|
#include <common.h>
|
|
#include <flash.h>
|
|
#include <malloc.h>
|
|
|
|
#include <asm/errno.h>
|
|
#include <linux/mtd/mtd.h>
|
|
#include <linux/mtd/concat.h>
|
|
//#include <mtd/cfi_flash.h>
|
|
|
|
#include "MsTypes.h"
|
|
#include "drvDeviceInfo.h"
|
|
#include "drvSERFLASH.h"
|
|
extern MS_BOOL bDetect;
|
|
extern hal_SERFLASH_t _hal_SERFLASH;
|
|
|
|
|
|
#include "asm/arch/mach/platform.h"
|
|
|
|
#ifdef CONFIG_MS_MTD_ISP_FLASH
|
|
//#define CONFIG_MTD_PARTITIONS
|
|
|
|
|
|
/* Define max times to check status register before we give up. */
|
|
#define MAX_READY_WAIT_COUNT 100000
|
|
#define CMD_SIZE 4
|
|
|
|
|
|
|
|
#ifdef CONFIG_MTD_PARTITIONS
|
|
#define mtd_has_partitions() (1)
|
|
#else
|
|
#define mtd_has_partitions() (0)
|
|
#endif
|
|
|
|
#define mutex_init(...)
|
|
#define mutex_lock(...)
|
|
#define mutex_unlock(...)
|
|
//struct mutex
|
|
//{
|
|
//};
|
|
|
|
//#define DEBUG(x...) printk(x...)
|
|
|
|
struct serflash
|
|
{
|
|
struct mutex lock;
|
|
struct mtd_info mtd;
|
|
unsigned partitioned:1;
|
|
u8 erase_opcode;
|
|
};
|
|
|
|
|
|
static inline struct serflash *mtd_to_serflash(struct mtd_info *mtd)
|
|
{
|
|
return container_of(mtd, struct serflash, mtd);
|
|
}
|
|
|
|
/* Erase flash fully or part of it */
|
|
static int serflash_erase(struct mtd_info *mtd, struct erase_info *instr)
|
|
{
|
|
|
|
struct serflash *flash = mtd_to_serflash(mtd);
|
|
flash=flash;
|
|
//ulong addr,len,start_sec,end_sec;
|
|
ulong addr,len;
|
|
// uint64_t addr_bak;
|
|
|
|
printk(KERN_WARNING"%s: addr 0x%08x, len %ld\n",
|
|
__func__, (u32)instr->addr, (long int)instr->len);
|
|
|
|
/* range and alignment check */
|
|
if (instr->addr + instr->len > mtd->size)
|
|
return -EINVAL;
|
|
|
|
// addr_bak = instr->addr;
|
|
// if ((do_div(instr->addr , mtd->erasesize)) != 0 ||(do_div(instr->len, mtd->erasesize) != 0))
|
|
// {
|
|
// return -EINVAL;
|
|
// }
|
|
// instr->addr = addr_bak;
|
|
|
|
addr = instr->addr;
|
|
len = instr->len;
|
|
|
|
mutex_lock(&flash->lock);
|
|
|
|
|
|
/*write protect false before erase*/
|
|
if (!MDrv_SERFLASH_WriteProtect(0))
|
|
{
|
|
mutex_unlock(&flash->lock);
|
|
return -EIO;
|
|
}
|
|
/* erase the whole chip */
|
|
if (len == mtd->size && !MDrv_SERFLASH_EraseChip())
|
|
{
|
|
instr->state = MTD_ERASE_FAILED;
|
|
|
|
MDrv_SERFLASH_WriteProtect(1);
|
|
mutex_unlock(&flash->lock);
|
|
return -EIO;
|
|
}
|
|
else
|
|
{
|
|
if (len)
|
|
{
|
|
if (!MDrv_SERFLASH_AddressErase(addr, len, 1))
|
|
{
|
|
instr->state = MTD_ERASE_FAILED;
|
|
|
|
MDrv_SERFLASH_WriteProtect(1);
|
|
mutex_unlock(&flash->lock);
|
|
return -EIO;
|
|
}
|
|
}
|
|
}
|
|
|
|
MDrv_SERFLASH_WriteProtect(1);
|
|
mutex_unlock(&flash->lock);
|
|
|
|
instr->state = MTD_ERASE_DONE;
|
|
|
|
mtd_erase_callback(instr);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Read an address range from the flash chip. The address range
|
|
* may be any size provided it is within the physical boundaries.
|
|
*/
|
|
static int serflash_read(struct mtd_info *mtd, loff_t from, size_t len,
|
|
size_t *retlen, u_char *buf)
|
|
{
|
|
struct serflash *flash = mtd_to_serflash(mtd);
|
|
|
|
printk(KERN_WARNING "%s %s 0x%08x, len %zd\n",
|
|
__func__, "from", (u32)from, len);
|
|
|
|
/* sanity checks */
|
|
if (!len)
|
|
return 0;
|
|
|
|
if (from + len > flash->mtd.size)
|
|
return -EINVAL;
|
|
|
|
mutex_lock(&flash->lock);
|
|
|
|
#if 0
|
|
/* Wait till previous write/erase is done. */
|
|
if (wait_till_ready(flash))
|
|
{
|
|
/* REVISIT status return?? */
|
|
mutex_unlock(&flash->lock);
|
|
return 1;
|
|
}
|
|
#endif
|
|
|
|
if (MDrv_SERFLASH_Read(from, len, (unsigned char *)buf))
|
|
{
|
|
*retlen = len;
|
|
}
|
|
else
|
|
{
|
|
*retlen = 0;
|
|
mutex_unlock(&flash->lock);
|
|
return -EIO;
|
|
}
|
|
|
|
mutex_unlock(&flash->lock);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Write an address range to the flash chip. Data must be written in
|
|
* FLASH_PAGESIZE chunks. The address range may be any size provided
|
|
* it is within the physical boundaries.
|
|
*/
|
|
static int serflash_write(struct mtd_info *mtd, loff_t to, size_t len,
|
|
size_t *retlen, const u_char *buf)
|
|
{
|
|
struct serflash *flash = mtd_to_serflash(mtd);
|
|
// u32 erase_start, erase_end;
|
|
|
|
printk(KERN_WARNING "%s %s 0x%08x, len %zd\n",
|
|
__func__, "to", (u32)to, len);
|
|
|
|
if (retlen)
|
|
*retlen = 0;
|
|
|
|
/* sanity checks */
|
|
if (!len)
|
|
return(0);
|
|
|
|
if (to + len > flash->mtd.size)
|
|
return -EINVAL;
|
|
|
|
// calculate erase sectors
|
|
// erase_start = (u32)to / mtd->erasesize;
|
|
// erase_end = (u32)(to+len) / mtd->erasesize - 1;
|
|
|
|
mutex_lock(&flash->lock);
|
|
|
|
#if 0
|
|
/* Wait until finished previous write command. */
|
|
if (wait_till_ready(flash))
|
|
{
|
|
mutex_unlock(&flash->lock);
|
|
return 1;
|
|
}
|
|
#endif
|
|
|
|
if (!MDrv_SERFLASH_WriteProtect(0))
|
|
{
|
|
mutex_unlock(&flash->lock);
|
|
return -EIO;
|
|
}
|
|
//modified by daniel.lee 2010/0514
|
|
/*
|
|
if (!MDrv_SERFLASH_BlockErase(erase_start, erase_end, TRUE))
|
|
{
|
|
mutex_unlock(&flash->lock);
|
|
return -EIO;
|
|
}
|
|
*/
|
|
if (MDrv_SERFLASH_Write(to, len, (unsigned char *)buf))
|
|
{
|
|
if (retlen)
|
|
*retlen = len;
|
|
}
|
|
else
|
|
{
|
|
if (retlen)
|
|
*retlen = 0;
|
|
|
|
MDrv_SERFLASH_WriteProtect(1);
|
|
mutex_unlock(&flash->lock);
|
|
return -EIO;
|
|
}
|
|
|
|
MDrv_SERFLASH_WriteProtect(1);
|
|
mutex_unlock(&flash->lock);
|
|
|
|
return 0;
|
|
}
|
|
|
|
#endif //CONFIG_MS_MTD_ISP_FLASH
|
|
|
|
|
|
/*
|
|
|
|
#define MSTAR_SERFLASH_SIZE (8 * 1024 * 1024)
|
|
|
|
|
|
|
|
#define SERFLASH_PART_PARTITION_0_OFFSET 0
|
|
#define SERFLASH_PART_PARTITION_0_SIZE (32+512+32) * 1024
|
|
|
|
#define SERFLASH_PART_PARTITION_1_OFFSET (SERFLASH_PART_PARTITION_0_OFFSET + SERFLASH_PART_PARTITION_0_SIZE)
|
|
#define SERFLASH_PART_PARTITION_1_SIZE 512 * 1024
|
|
|
|
#define SERFLASH_PART_PARTITION_2_OFFSET (SERFLASH_PART_PARTITION_1_OFFSET + SERFLASH_PART_PARTITION_1_SIZE)
|
|
#define SERFLASH_PART_PARTITION_2_SIZE 512 * 1024
|
|
|
|
#define SERFLASH_PART_PARTITION_3_OFFSET (SERFLASH_PART_PARTITION_2_OFFSET + SERFLASH_PART_PARTITION_2_SIZE)
|
|
#define SERFLASH_PART_PARTITION_3_SIZE MSTAR_SERFLASH_SIZE - SERFLASH_PART_PARTITION_3_OFFSET
|
|
|
|
//
|
|
//#if ( (NAND_PART_APPLICATION_OFFSET) >= MSTAR_SERFLASH_SIZE)
|
|
// #error "Error: NAND partition is not correct!!!"
|
|
//#endif
|
|
|
|
|
|
static const struct mtd_partition serflash_partition_info[] =
|
|
{
|
|
{
|
|
.name = "boot",
|
|
.offset = SERFLASH_PART_PARTITION_0_OFFSET,
|
|
.size = SERFLASH_PART_PARTITION_0_SIZE
|
|
},
|
|
{
|
|
.name = "kernel",
|
|
.offset = SERFLASH_PART_PARTITION_1_OFFSET,
|
|
.size = SERFLASH_PART_PARTITION_1_SIZE
|
|
},
|
|
{
|
|
.name = "rootfs",
|
|
.offset = SERFLASH_PART_PARTITION_2_OFFSET,
|
|
.size = SERFLASH_PART_PARTITION_2_SIZE
|
|
},
|
|
{
|
|
.name = "user",
|
|
.offset = SERFLASH_PART_PARTITION_3_OFFSET,
|
|
.size = SERFLASH_PART_PARTITION_3_SIZE
|
|
}
|
|
|
|
};
|
|
|
|
#define SERFLASH_NUM_PARTITIONS ARRAY_SIZE(serflash_partition_info)
|
|
*/
|
|
|
|
|
|
#ifdef CONFIG_MS_MTD_ISP_FLASH
|
|
unsigned int g_size=0;
|
|
/*
|
|
* board specific setup should have ensured the SPI clock used here
|
|
* matches what the READ command supports, at least until this driver
|
|
* understands FAST_READ (for clocks over 25 MHz).
|
|
*/
|
|
|
|
int serflash_probe(void)
|
|
{
|
|
struct serflash *flash;
|
|
struct flash_info *info;
|
|
unsigned i;
|
|
|
|
// jedec_probe() will read id, so initialize hardware first
|
|
MDrv_SERFLASH_Init();
|
|
|
|
|
|
if (bDetect == FALSE)
|
|
{
|
|
printk(KERN_WARNING"no flash_info!!");
|
|
return -ENODEV;
|
|
}
|
|
|
|
flash = kzalloc(sizeof *flash, GFP_KERNEL);
|
|
if (!flash)
|
|
return -ENOMEM;
|
|
|
|
mutex_init(&flash->lock);
|
|
|
|
flash->mtd.priv = flash;
|
|
|
|
#if 0
|
|
/*
|
|
* Atmel serial flash tend to power up
|
|
* with the software protection bits set
|
|
*/
|
|
|
|
if (info->jedec_id >> 16 == 0x1f)
|
|
{
|
|
write_enable(flash);
|
|
write_sr(flash, 0);
|
|
}
|
|
#endif
|
|
|
|
flash->mtd.name ="nor0";
|
|
flash->mtd.type = MTD_NORFLASH;
|
|
flash->mtd.writesize = 1;
|
|
flash->mtd.flags = MTD_CAP_NORFLASH;
|
|
MDrv_SERFLASH_DetectSize((MS_U32*)&flash->mtd.size);
|
|
flash->mtd._erase = serflash_erase;
|
|
flash->mtd._read = serflash_read;
|
|
flash->mtd._write = serflash_write;
|
|
|
|
g_size = flash->mtd.size;
|
|
flash->mtd.erasesize = (_hal_SERFLASH.u32SecSize);
|
|
|
|
//dev_info(&spi->dev, "%s (%d Kbytes)\n", info->name,
|
|
//flash->mtd.size / 1024);
|
|
|
|
printk(
|
|
"mtd .name = %s, .size = 0x%.8x (%uMiB) "
|
|
".erasesize = 0x%.8x .numeraseregions = %d\n",
|
|
flash->mtd.name,
|
|
(unsigned int)flash->mtd.size, (unsigned int)(flash->mtd.size >>20),
|
|
(unsigned int)flash->mtd.erasesize,
|
|
flash->mtd.numeraseregions);
|
|
|
|
if (flash->mtd.numeraseregions)
|
|
for (i = 0; i < flash->mtd.numeraseregions; i++)
|
|
printk(
|
|
"mtd.eraseregions[%d] = { .offset = 0x%.8x, "
|
|
".erasesize = 0x%.8x (%uKiB), "
|
|
".numblocks = %d }\n",
|
|
i, (u32)flash->mtd.eraseregions[i].offset,
|
|
(unsigned int)flash->mtd.eraseregions[i].erasesize,
|
|
(flash->mtd.eraseregions[i].erasesize >>10 ),
|
|
(unsigned int)flash->mtd.eraseregions[i].numblocks);
|
|
|
|
|
|
/* partitions should match sector boundaries; and it may be good to
|
|
* use readonly partitions for writeprotected sectors (BP2..BP0).
|
|
*/
|
|
|
|
/*
|
|
if (mtd_has_partitions())
|
|
{
|
|
struct mtd_partition *parts = NULL;
|
|
int nr_parts = 0;
|
|
|
|
|
|
if (nr_parts > 0)
|
|
{
|
|
for (i = 0; i < nr_parts; i++)
|
|
{
|
|
printk(KERN_WARNING "partitions[%d] = "
|
|
"{.name = %s, .offset = 0x%.8x, "
|
|
".size = 0x%.8x (%uKiB) }\n",
|
|
i, parts[i].name,
|
|
(unsigned int)(parts[i].offset),
|
|
(unsigned int)(parts[i].size),
|
|
(unsigned int)(parts[i].size / 1024));
|
|
}
|
|
flash->partitioned = 1;
|
|
|
|
return add_mtd_partitions(&flash->mtd, parts, nr_parts);
|
|
|
|
}
|
|
else
|
|
return add_mtd_partitions(&flash->mtd, serflash_partition_info, SERFLASH_NUM_PARTITIONS);
|
|
|
|
}
|
|
*/
|
|
|
|
return add_mtd_device(&flash->mtd) == 1 ? -ENODEV : 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
unsigned long flash_init (void)
|
|
{
|
|
serflash_probe();
|
|
|
|
if (g_size>0)
|
|
return (g_size);
|
|
|
|
return 0;
|
|
}
|
|
|
|
#endif // CONFIG_MS_MTD_ISP_FLASH
|
|
|
|
#ifdef CONFIG_CMD_SF
|
|
#include <spi_flash.h>
|
|
int _spi_flash_read(struct spi_flash *flash, u32 offset,
|
|
size_t len, void *buf)
|
|
{
|
|
//printk(KERN_WARNING "%s from 0x%x, len 0x%x, to 0x%x ", __func__, (u32)offset, len, (unsigned int)buf);
|
|
u32 ret = -EIO;
|
|
u32 readsize = 0;
|
|
//u32 starttime = get_timer(0);
|
|
|
|
/* sanity checks */
|
|
if (!len)
|
|
return -EINVAL;
|
|
|
|
if (offset + len > flash->size)
|
|
return -EINVAL;
|
|
#if 0
|
|
if (MDrv_SERFLASH_Read(offset, len, (unsigned char *)buf))
|
|
{
|
|
ret = 0;
|
|
}
|
|
#else
|
|
if (offset < 0x1000000)
|
|
{
|
|
MDrv_SERFLASH_WriteExtAddrRegister(0);
|
|
readsize = len > (0x1000000 - offset) ? (0x1000000 - offset) : len;
|
|
memcpy(buf, (const void *)(MS_SPI_ADDR|offset), readsize);
|
|
len = len - readsize;
|
|
}
|
|
if (len)
|
|
{
|
|
u32 regEAR = 0;
|
|
offset += readsize;
|
|
regEAR = offset >> 24;
|
|
if (regEAR)
|
|
MDrv_SERFLASH_WriteExtAddrRegister(regEAR);
|
|
memcpy(buf+readsize, (const void *)(MS_SPI_ADDR|(offset&0xFFFFFF)), len);
|
|
if (regEAR)
|
|
MDrv_SERFLASH_WriteExtAddrRegister(0);
|
|
}
|
|
ret = 0;
|
|
#endif
|
|
|
|
//printk(KERN_WARNING "(cost %ld ms)\n", get_timer(0) - starttime);
|
|
return ret;
|
|
}
|
|
|
|
int _spi_flash_write(struct spi_flash *flash, u32 offset,
|
|
size_t len, const void *buf)
|
|
{
|
|
printk(KERN_WARNING "%s to 0x%x, len 0x%x from 0x%x ", __func__,(u32)offset, len, (unsigned int)buf);
|
|
u32 ret = -EIO;
|
|
u32 starttime = get_timer(0);
|
|
|
|
/* sanity checks */
|
|
if (!len)
|
|
return (-EINVAL);
|
|
if (offset + len > flash->size)
|
|
return (-EINVAL);
|
|
|
|
if (!MDrv_SERFLASH_WriteProtect(0))
|
|
{
|
|
return (-EIO);
|
|
}
|
|
|
|
if (MDrv_SERFLASH_Write(offset, len, (unsigned char *)buf))
|
|
{
|
|
ret = 0;
|
|
}
|
|
|
|
MDrv_SERFLASH_WriteProtect(1);
|
|
|
|
printk(KERN_WARNING "(cost %ld ms)\n", get_timer(0) - starttime);
|
|
return ret;
|
|
}
|
|
#define FLASH_BLOCK_SIZE 65536
|
|
|
|
int _spi_flash_erase(struct spi_flash *flash, u32 offset,
|
|
size_t len)
|
|
{
|
|
|
|
printk(KERN_WARNING"%s: addr 0x%x, len 0x%x ", __func__, (u32)offset, len);
|
|
u32 ret = -EIO;
|
|
u32 u32ErasedSize;
|
|
u32 starttime = get_timer(0);
|
|
|
|
/* range and alignment check */
|
|
if (!len)
|
|
return (-EINVAL);
|
|
if (offset + len > flash->size)
|
|
return (-EINVAL);
|
|
|
|
/*write protect false before erase*/
|
|
if (!MDrv_SERFLASH_WriteProtect(0))
|
|
{
|
|
return -EIO;
|
|
}
|
|
|
|
/* erase the whole chip */
|
|
//if (len == flash->size)
|
|
//{
|
|
// ret = MDrv_SERFLASH_EraseChip();
|
|
// goto Done;
|
|
//}
|
|
|
|
if(offset % FLASH_BLOCK_SIZE)
|
|
{
|
|
u32ErasedSize = FLASH_BLOCK_SIZE - offset%FLASH_BLOCK_SIZE;
|
|
if (len < u32ErasedSize)
|
|
{
|
|
u32ErasedSize = len;
|
|
}
|
|
if(MDrv_SERFLASH_SectorErase(offset, offset+u32ErasedSize-1))
|
|
{
|
|
offset += u32ErasedSize;
|
|
len -= u32ErasedSize;
|
|
ret = 0;
|
|
}
|
|
else
|
|
{
|
|
ret = (-EINVAL);
|
|
goto Done;
|
|
}
|
|
}
|
|
if(len >= FLASH_BLOCK_SIZE)
|
|
{
|
|
u32ErasedSize = len - len%FLASH_BLOCK_SIZE;
|
|
|
|
if( MDrv_SERFLASH_AddressErase(offset, u32ErasedSize, 1))
|
|
{
|
|
offset += u32ErasedSize;
|
|
len -= u32ErasedSize;
|
|
ret = 0;
|
|
}
|
|
else
|
|
{
|
|
ret = (-EINVAL);
|
|
goto Done;
|
|
}
|
|
}
|
|
|
|
if(len)
|
|
{
|
|
u32ErasedSize = len;
|
|
if(MDrv_SERFLASH_SectorErase(offset, offset+u32ErasedSize-1))
|
|
{
|
|
offset += u32ErasedSize;
|
|
len -= u32ErasedSize;
|
|
ret = 0;
|
|
}
|
|
else
|
|
{
|
|
ret = (-EINVAL);
|
|
goto Done;
|
|
}
|
|
}
|
|
|
|
|
|
Done:
|
|
MDrv_SERFLASH_WriteExtAddrRegister(0);
|
|
MDrv_SERFLASH_WriteProtect(1);
|
|
printk(KERN_WARNING "(cost %ld ms)\n", get_timer(0) - starttime);
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
struct spi_flash *spi_flash_probe(unsigned int bus, unsigned int cs,
|
|
unsigned int max_hz, unsigned int spi_mode)
|
|
{
|
|
// struct spi_slave *spi;
|
|
struct spi_flash *flash = NULL;
|
|
//const SERFLASH_Info *Info = NULL;
|
|
|
|
/*
|
|
spi = spi_setup_slave(bus, cs, max_hz, spi_mode);
|
|
if (!spi) {
|
|
printf("SF: Failed to set up slave\n");
|
|
return err_manufacturer_probe;
|
|
}
|
|
*/
|
|
|
|
// jedec_probe() will read id, so initialize hardware first
|
|
MDrv_SERFLASH_Init();
|
|
|
|
|
|
//bDetect is the global variable from halserflash.c
|
|
if (bDetect == FALSE)
|
|
{
|
|
printk(KERN_WARNING"no flash_info!!");
|
|
goto err_manufacturer_probe;
|
|
}
|
|
|
|
flash = malloc(sizeof(struct spi_flash));
|
|
if (!flash) {
|
|
debug("SF: Failed to allocate memory\n");
|
|
goto err_mem;
|
|
}
|
|
|
|
flash->spi = NULL;
|
|
flash->name = "nor0";
|
|
|
|
flash->write = _spi_flash_write;
|
|
flash->erase = _spi_flash_erase;
|
|
flash->read = _spi_flash_read;
|
|
//Info = MDrv_SERFLASH_GetInfo();
|
|
flash->page_size = (_hal_SERFLASH.u16PageSize);
|
|
flash->sector_size = (_hal_SERFLASH.u32SecSize);
|
|
MDrv_SERFLASH_DetectSize((MS_U32*)&flash->size);
|
|
|
|
|
|
if (!flash) {
|
|
printf("SF: Unsupported manufacturer\n");
|
|
goto err_manufacturer_probe;
|
|
}
|
|
|
|
printf("SF: Detected %s with total size ", flash->name);
|
|
print_size(flash->size, "\n");
|
|
|
|
|
|
return flash;
|
|
|
|
err_manufacturer_probe:
|
|
//err_read_id:
|
|
//err_claim_bus:
|
|
err_mem:
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void spi_flash_free(struct spi_flash *flash)
|
|
{
|
|
free(flash);
|
|
}
|
|
|
|
|
|
#endif
|
|
|
|
|