1163 lines
30 KiB
C
1163 lines
30 KiB
C
// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
|
|
/*
|
|
* Copyright (c) 2019 Amlogic, Inc. All rights reserved.
|
|
*/
|
|
|
|
#include <common.h>
|
|
#include <malloc.h>
|
|
#include <linux/errno.h>
|
|
#include <linux/io.h>
|
|
#include <linux/mtd/mtd.h>
|
|
#include <amlogic/aml_rsv.h>
|
|
#include <amlogic/aml_mtd.h>
|
|
#include <amlogic/partition_table.h>
|
|
#include <asm/amlogic/arch/cpu_config.h>
|
|
#include <amlogic/storage.h>
|
|
#include <fdt_support.h>
|
|
|
|
extern int info_disprotect;
|
|
static struct meson_rsv_handler_t *rsv_handler;
|
|
|
|
int rsvname2index(const char *rsv_name)
|
|
{
|
|
struct meson_rsv_info_t *rsv_info = rsv_handler->rsv_info;
|
|
int i, len = get_mtd_rsv_partition_count();
|
|
|
|
for (i = 0; i < len; i++)
|
|
if (!strncmp(rsv_name, rsv_info[i].name, 4) ||
|
|
!strncmp(rsv_name, &rsv_info[i].name[1], 3))
|
|
return i;
|
|
return -1;
|
|
}
|
|
|
|
u32 meson_rsv_part_get_bl2_part_size(struct mtd_info *mtd)
|
|
{
|
|
static u32 bl2_part_size = 0;
|
|
|
|
if (bl2_part_size)
|
|
return bl2_part_size;
|
|
|
|
if (BOARD_CONFIG_BL2_LAYOUT_TYPE == BL2_LAYOUT_1024)
|
|
bl2_part_size = 1024 * mtd->writesize;
|
|
else if (BOARD_CONFIG_BL2_LAYOUT_TYPE == BL2_LAYOUT_512)
|
|
bl2_part_size = 512 * mtd->writesize;
|
|
else {
|
|
bl2_part_size = round_up(BL2_SIZE, mtd->erasesize);
|
|
bl2_part_size *= CONFIG_BL2_COPY_NUM;
|
|
}
|
|
|
|
return bl2_part_size;
|
|
}
|
|
|
|
u32 meson_rsv_part_get_bl2_copy_number(struct mtd_info *mtd)
|
|
{
|
|
static u32 bl2_copy_number;
|
|
u32 bl2_part_size = meson_rsv_part_get_bl2_part_size(mtd);
|
|
u32 bl2_align_size = round_up(BL2_SIZE, mtd->erasesize);
|
|
|
|
if (bl2_copy_number)
|
|
return bl2_copy_number;
|
|
|
|
bl2_copy_number = bl2_part_size / bl2_align_size;
|
|
|
|
/*
|
|
* To BL2_LAYOUT_1024 and BL2_LAYOUT_512, if boot_info.layout_method
|
|
* is set to LAYOUT_VER0, the copy number must be even number; because ROM
|
|
* can only try the 0/128/256/512/768 page address.
|
|
*/
|
|
if (BOARD_CONFIG_BL2_LAYOUT_TYPE ==
|
|
BL2_LAYOUT_1024 || BOARD_CONFIG_BL2_LAYOUT_TYPE == BL2_LAYOUT_512)
|
|
bl2_copy_number = (bl2_copy_number & 0xE);
|
|
|
|
return bl2_copy_number;
|
|
}
|
|
|
|
u32 meson_rsv_part_get_bl2_copy_size(struct mtd_info *mtd)
|
|
{
|
|
u32 bl2_part_size, bl2_copy_number;
|
|
|
|
bl2_part_size = meson_rsv_part_get_bl2_part_size(mtd);
|
|
bl2_copy_number = meson_rsv_part_get_bl2_copy_number(mtd);
|
|
|
|
return bl2_part_size / bl2_copy_number;
|
|
}
|
|
|
|
u32 meson_rsv_part_get_start_block(struct mtd_info *mtd)
|
|
{
|
|
u32 bl2_part_size;
|
|
static u32 rev_start_block = 0;
|
|
|
|
if (rev_start_block)
|
|
return rev_start_block;
|
|
|
|
bl2_part_size = meson_rsv_part_get_bl2_part_size(mtd);
|
|
rev_start_block = bl2_part_size / mtd->erasesize;
|
|
return rev_start_block;
|
|
}
|
|
|
|
static u32 meson_rsv_part_get_total_blocks(struct rsv_part *rsv_part)
|
|
{
|
|
int array_size, i;
|
|
static u32 blocks = 0;
|
|
|
|
if (blocks)
|
|
return blocks;
|
|
|
|
array_size = get_mtd_rsv_partition_count();
|
|
for (i = 0; i < array_size; i++)
|
|
blocks += rsv_part[i].blocks;
|
|
|
|
if (blocks > MTD_RSV_BLOCK_CNT)
|
|
printf("mismatch between calculation(%d) and defination(%d)\n",
|
|
blocks, MTD_RSV_BLOCK_CNT);
|
|
|
|
return blocks;
|
|
}
|
|
|
|
u64 meson_rsv_part_get_tpl_start(struct mtd_info *mtd)
|
|
{
|
|
u64 reserved_start_block, tpl_start, reserved_total_blocks;
|
|
|
|
reserved_start_block = meson_rsv_part_get_start_block(mtd);
|
|
reserved_total_blocks = meson_rsv_part_get_total_blocks(rsv_handler->rsv_part);
|
|
tpl_start = (u64)(reserved_start_block + reserved_total_blocks) * mtd->erasesize;
|
|
|
|
return tpl_start;
|
|
}
|
|
|
|
u64 meson_rsv_part_get_tpl_size(struct mtd_info *mtd)
|
|
{
|
|
return CONFIG_TPL_SIZE_PER_COPY * CONFIG_NAND_TPL_COPY_NUM;
|
|
}
|
|
|
|
static struct rsv_part *meson_rsv_info_calculate_start_block(struct mtd_info *mtd)
|
|
{
|
|
struct rsv_part *rsv_part, *rsv_partitions;
|
|
int i = 0, array_size;
|
|
u32 bbt_start = BBT_START_BLOCK;
|
|
u32 bbt_end = BBT_START_BLOCK + BBT_TOTAL_BLOCKS;
|
|
u32 cur_part_end;
|
|
|
|
array_size = get_mtd_rsv_partition_count();
|
|
if (array_size > MAX_MESON_RSV_INFO_NUM)
|
|
return NULL;
|
|
|
|
rsv_part = kzalloc(sizeof(*rsv_part) * array_size, GFP_KERNEL);
|
|
if (!rsv_part)
|
|
return NULL;
|
|
|
|
/* insert bbt and always put it 1st */
|
|
rsv_part[0].start_block = BBT_START_BLOCK;
|
|
rsv_part[0].blocks = BBT_TOTAL_BLOCKS;
|
|
memcpy(rsv_part[0].name, BBT_NAND_MAGIC, 4);
|
|
|
|
rsv_partitions = get_mtd_rsv_partition();
|
|
rsv_partitions[0].start_block = meson_rsv_part_get_start_block(mtd);
|
|
for (i = 0; i < array_size - 1; i++) {
|
|
cur_part_end = rsv_partitions[i].start_block +
|
|
rsv_partitions[i].blocks;
|
|
|
|
if (cur_part_end >= bbt_start &&
|
|
bbt_end > rsv_partitions[i].start_block)
|
|
cur_part_end += BBT_TOTAL_BLOCKS;
|
|
|
|
rsv_partitions[i + 1].start_block = cur_part_end;
|
|
}
|
|
memcpy(&rsv_part[1], rsv_partitions,
|
|
(array_size - 1) * sizeof(struct rsv_part));
|
|
|
|
return rsv_part;
|
|
}
|
|
|
|
static struct free_node_t *get_free_node(struct meson_rsv_info_t *rsv_info)
|
|
{
|
|
struct meson_rsv_handler_t *handler = rsv_info->handler;
|
|
u32 index;
|
|
|
|
index = find_first_zero_bit((void *)&handler->fn_bitmask,
|
|
MTD_RSV_BLOCK_CNT);
|
|
if (index >= MTD_RSV_BLOCK_CNT) {
|
|
pr_info("%s %d index :%d is greater than max rsv block num\n",
|
|
__func__, __LINE__, index);
|
|
return NULL;
|
|
}
|
|
WARN_ON(test_and_set_bit(index, (void *)&handler->fn_bitmask));
|
|
|
|
return handler->free_node[index];
|
|
}
|
|
|
|
static void release_free_node(struct meson_rsv_info_t *rsv_info,
|
|
struct free_node_t *free_node)
|
|
{
|
|
struct meson_rsv_handler_t *handler = rsv_info->handler;
|
|
u32 index = free_node->index;
|
|
|
|
pr_info("%s %d: bitmask = 0x%llx\n",
|
|
__func__, __LINE__, handler->fn_bitmask);
|
|
if (index >= MTD_RSV_BLOCK_CNT) {
|
|
pr_info("%s %d index :%d is greater than max rsv block num\n",
|
|
__func__, __LINE__, index);
|
|
return;
|
|
}
|
|
WARN_ON(!test_and_clear_bit(index, (void *)&handler->fn_bitmask));
|
|
memset(free_node, 0, sizeof(struct free_node_t));
|
|
free_node->index = index;
|
|
pr_info("%s %d: bitmask = 0x%llx\n",
|
|
__func__, __LINE__, handler->fn_bitmask);
|
|
}
|
|
|
|
static inline void menson_rsv_disprotect(void)
|
|
{
|
|
/*disprotect*/
|
|
info_disprotect |= DISPROTECT_KEY;
|
|
info_disprotect |= DISPROTECT_FBBT;
|
|
}
|
|
static inline void menson_rsv_protect(void)
|
|
{
|
|
/*protect*/
|
|
info_disprotect &= ~DISPROTECT_KEY;
|
|
info_disprotect &= ~DISPROTECT_FBBT;
|
|
|
|
}
|
|
|
|
static inline u32 is_rsv_block(struct meson_rsv_info_t *rsv_info, u32 block)
|
|
{
|
|
struct free_node_t *temp_node = rsv_info->nfree;
|
|
|
|
if (block == rsv_info->nvalid->blk_addr)
|
|
return 1;
|
|
|
|
while (temp_node) {
|
|
if (block == temp_node->blk_addr)
|
|
return 1;
|
|
temp_node = temp_node->next;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
int meson_rsv_erase_protect(struct meson_rsv_handler_t *handler,
|
|
u32 block_addr)
|
|
{
|
|
struct meson_rsv_info_t *rsv_info;
|
|
static u32 rsv_end;
|
|
int index = rsvname2index(KEY_NAND_MAGIC);
|
|
|
|
if (index < 0)
|
|
return -1;
|
|
|
|
if (!rsv_end) {
|
|
rsv_end = meson_rsv_part_get_start_block(handler->mtd) +
|
|
meson_rsv_part_get_total_blocks(handler->rsv_part);
|
|
}
|
|
|
|
if (block_addr > rsv_end)
|
|
return 0;
|
|
|
|
rsv_info = &handler->rsv_info[index];
|
|
if (rsv_info->valid) {
|
|
if (!(info_disprotect & DISPROTECT_KEY) &&
|
|
is_rsv_block(rsv_info, block_addr))
|
|
return -1;
|
|
}
|
|
|
|
index = rsvname2index(BBT_NAND_MAGIC);
|
|
if (index < 0)
|
|
return -1;
|
|
|
|
rsv_info = &handler->rsv_info[index];
|
|
if (rsv_info->valid) {
|
|
if (!(info_disprotect & DISPROTECT_FBBT) &&
|
|
is_rsv_block(rsv_info, block_addr))
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int meson_rsv_free(struct meson_rsv_info_t *rsv_info)
|
|
{
|
|
struct mtd_info *mtd;
|
|
struct free_node_t *tmp_node, *next_node = NULL;
|
|
int error = 0;
|
|
loff_t addr = 0;
|
|
struct erase_info erase_info;
|
|
|
|
if (!rsv_info) {
|
|
pr_info("%s %d rsv info has not inited yet!\n",
|
|
__func__, __LINE__);
|
|
return 1;
|
|
}
|
|
|
|
mtd = rsv_info->mtd;
|
|
pr_info("free %s\n", rsv_info->name);
|
|
|
|
if (rsv_info->valid) {
|
|
addr = rsv_info->nvalid->blk_addr;
|
|
addr *= mtd->erasesize;
|
|
memset(&erase_info, 0, sizeof(struct erase_info));
|
|
erase_info.mtd = mtd;
|
|
erase_info.addr = addr;
|
|
erase_info.len = mtd->erasesize;
|
|
menson_rsv_disprotect();
|
|
error = mtd_erase(mtd, &erase_info);
|
|
menson_rsv_protect();
|
|
pr_info("erasing valid info block: %llx\n", addr);
|
|
rsv_info->nvalid->blk_addr = -1;
|
|
rsv_info->nvalid->ec = -1;
|
|
rsv_info->nvalid->page_addr = 0;
|
|
rsv_info->nvalid->timestamp = 0;
|
|
rsv_info->nvalid->status = 0;
|
|
rsv_info->valid = 0;
|
|
}
|
|
tmp_node = rsv_info->nfree;
|
|
while (tmp_node) {
|
|
next_node = tmp_node->next;
|
|
release_free_node(rsv_info, tmp_node);
|
|
tmp_node = next_node;
|
|
}
|
|
rsv_info->nfree = NULL;
|
|
|
|
return error;
|
|
}
|
|
|
|
int meson_rsv_save(struct meson_rsv_info_t *rsv_info, u_char *buf)
|
|
{
|
|
struct mtd_info *mtd = rsv_info->mtd;
|
|
struct free_node_t *free_node, *temp_node;
|
|
struct erase_info erase_info;
|
|
int ret = 0, i = 1, pages_per_blk;
|
|
loff_t offset = 0;
|
|
|
|
pages_per_blk = 1 << (mtd->erasesize_shift - mtd->writesize_shift);
|
|
if ((rsv_info->nvalid->status & POWER_ABNORMAL_FLAG) ||
|
|
(rsv_info->nvalid->status & ECC_ABNORMAL_FLAG))
|
|
rsv_info->nvalid->page_addr = pages_per_blk;
|
|
if (mtd->writesize < rsv_info->size)
|
|
i = (rsv_info->size + mtd->writesize - 1) / mtd->writesize;
|
|
RE_SEARCH:
|
|
if (rsv_info->valid) {
|
|
rsv_info->nvalid->page_addr += i;
|
|
if ((rsv_info->nvalid->page_addr + i) > pages_per_blk) {
|
|
if ((rsv_info->nvalid->page_addr - i) ==
|
|
pages_per_blk) {
|
|
offset = rsv_info->nvalid->blk_addr;
|
|
offset *= mtd->erasesize;
|
|
erase_info.mtd = mtd;
|
|
erase_info.addr = offset;
|
|
erase_info.len = mtd->erasesize;
|
|
menson_rsv_disprotect();
|
|
mtd_erase(mtd, &erase_info);
|
|
menson_rsv_protect();
|
|
rsv_info->nvalid->ec++;
|
|
pr_info("%s %d: erasing bad info block:0x%llx\n",
|
|
__func__, __LINE__, offset);
|
|
}
|
|
free_node = get_free_node(rsv_info);
|
|
if (!free_node)
|
|
return -ENOMEM;
|
|
/* set current valid node to free list */
|
|
free_node->blk_addr = rsv_info->nvalid->blk_addr;
|
|
free_node->ec = rsv_info->nvalid->ec;
|
|
temp_node = rsv_info->nfree;
|
|
while (temp_node->next)
|
|
temp_node = temp_node->next;
|
|
temp_node->next = free_node;
|
|
/* get one node from free list and set to current */
|
|
temp_node = rsv_info->nfree;
|
|
rsv_info->nvalid->blk_addr = temp_node->blk_addr;
|
|
rsv_info->nvalid->page_addr = 0;
|
|
rsv_info->nvalid->ec = temp_node->ec;
|
|
rsv_info->nvalid->timestamp++;
|
|
rsv_info->nfree = temp_node->next;
|
|
release_free_node(rsv_info, temp_node);
|
|
}
|
|
} else {
|
|
temp_node = rsv_info->nfree;
|
|
rsv_info->nvalid->blk_addr = temp_node->blk_addr;
|
|
rsv_info->nvalid->page_addr = 0;
|
|
rsv_info->nvalid->ec = temp_node->ec;
|
|
rsv_info->nvalid->timestamp++;
|
|
rsv_info->nfree = temp_node->next;
|
|
release_free_node(rsv_info, temp_node);
|
|
}
|
|
offset = rsv_info->nvalid->blk_addr;
|
|
offset *= mtd->erasesize;
|
|
offset += ((u64)rsv_info->nvalid->page_addr) * mtd->writesize;
|
|
if (rsv_info->nvalid->page_addr == 0) {
|
|
ret = mtd_block_isbad(mtd, offset);
|
|
if (ret) {
|
|
/**
|
|
* cause our rsv list includes bad block,
|
|
* so we need check it here and for fear
|
|
* of data lost.
|
|
*/
|
|
printf("%s %d: %s bad block here 0x%llx\n",
|
|
__func__, __LINE__, rsv_info->name, offset);
|
|
rsv_info->nvalid->page_addr = pages_per_blk - i;
|
|
goto RE_SEARCH;
|
|
}
|
|
memset(&erase_info, 0, sizeof(struct erase_info));
|
|
erase_info.mtd = mtd;
|
|
erase_info.addr = offset;
|
|
erase_info.len = mtd->erasesize;
|
|
menson_rsv_disprotect();
|
|
ret = mtd_erase(mtd, &erase_info);
|
|
menson_rsv_protect();
|
|
if (ret) {
|
|
printf("%s %d %s erase failed at 0x%llx ,mark it bad\n",
|
|
__func__, __LINE__, rsv_info->name, offset);
|
|
mtd_block_markbad(mtd, offset);
|
|
//return ret;
|
|
rsv_info->nvalid->page_addr = pages_per_blk;
|
|
goto RE_SEARCH;
|
|
}
|
|
rsv_info->nvalid->ec++;
|
|
}
|
|
ret = meson_rsv_write(rsv_info, buf);
|
|
if (ret) {
|
|
pr_info("%s %d rsv info: %s save failed!\n",
|
|
__func__, __LINE__, rsv_info->name);
|
|
return ret;
|
|
}
|
|
rsv_info->valid = 1;
|
|
rsv_info->nvalid->status = 0;
|
|
return ret;
|
|
}
|
|
|
|
int meson_rsv_write(struct meson_rsv_info_t *rsv_info, u_char *buf)
|
|
{
|
|
struct mtd_info *mtd;
|
|
struct oobinfo_t oobinfo;
|
|
struct mtd_oob_ops oob_ops;
|
|
size_t length = 0;
|
|
loff_t offset;
|
|
int ret = 0;
|
|
|
|
if (!rsv_info) {
|
|
pr_info("%s %d rsv info has not inited yet!\n",
|
|
__func__, __LINE__);
|
|
return 1;
|
|
}
|
|
|
|
mtd = rsv_info->mtd;
|
|
offset = rsv_info->nvalid->blk_addr;
|
|
offset *= mtd->erasesize;
|
|
offset += ((u64)rsv_info->nvalid->page_addr) * mtd->writesize;
|
|
pr_info("%s %d write %s to 0x%llx\n",
|
|
__func__, __LINE__, rsv_info->name, offset);
|
|
memcpy(oobinfo.name, rsv_info->name, 4);
|
|
oobinfo.ec = rsv_info->nvalid->ec;
|
|
/* TODO: prevent the unrolling situation here */
|
|
oobinfo.timestamp = rsv_info->nvalid->timestamp;
|
|
while (length < rsv_info->size) {
|
|
oob_ops.mode = MTD_OPS_AUTO_OOB;
|
|
oob_ops.len = min_t(u32, mtd->writesize,
|
|
(rsv_info->size - length));
|
|
oob_ops.ooblen = sizeof(struct oobinfo_t);
|
|
oob_ops.ooboffs = 0;
|
|
oob_ops.datbuf = buf + length;
|
|
oob_ops.oobbuf = (u8 *)&oobinfo;
|
|
ret = mtd_write_oob(mtd, offset, &oob_ops);
|
|
if (ret) {
|
|
pr_info("fail to write %s to 0x%llx ret:%d\n",
|
|
rsv_info->name, offset, ret);
|
|
return -EIO;
|
|
}
|
|
offset += mtd->writesize;
|
|
length += oob_ops.len;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
int meson_rsv_read(struct meson_rsv_info_t *rsv_info, u_char *buf)
|
|
{
|
|
struct mtd_info *mtd;
|
|
struct oobinfo_t oobinfo;
|
|
struct mtd_oob_ops oob_ops;
|
|
size_t length = 0;
|
|
loff_t offset;
|
|
int ret = 0;
|
|
|
|
if (!rsv_info) {
|
|
pr_info("%s %d rsv info has not inited yet!\n",
|
|
__func__, __LINE__);
|
|
return 1;
|
|
}
|
|
|
|
mtd = rsv_info->mtd;
|
|
READ_RSV_AGAIN:
|
|
offset = rsv_info->nvalid->blk_addr;
|
|
offset *= mtd->erasesize;
|
|
offset += ((u64)rsv_info->nvalid->page_addr) * mtd->writesize;
|
|
pr_info("%s %d read %s from 0x%llx\n",
|
|
__func__, __LINE__, rsv_info->name, offset);
|
|
memset(buf, 0, rsv_info->size);
|
|
while (length < rsv_info->size) {
|
|
oob_ops.mode = MTD_OPS_AUTO_OOB;
|
|
oob_ops.len = min_t(u32, mtd->writesize,
|
|
(rsv_info->size - length));
|
|
oob_ops.ooblen = sizeof(struct oobinfo_t);
|
|
oob_ops.ooboffs = 0;
|
|
oob_ops.datbuf = buf + length;
|
|
oob_ops.oobbuf = (u8 *)&oobinfo;
|
|
|
|
memset((u8 *)&oobinfo, 0, oob_ops.ooblen);
|
|
ret = mtd_read_oob(mtd, offset, &oob_ops);
|
|
if (ret && (ret != -EUCLEAN)) {
|
|
pr_info("blk good but read failed: %llx, %d\n",
|
|
(u64)offset, ret);
|
|
ret = meson_rsv_scan(rsv_info);
|
|
if (ret)
|
|
return -EIO;
|
|
goto READ_RSV_AGAIN;
|
|
}
|
|
/* Do not use strlen ,Use ARRAY_SIZE to make the length 4 */
|
|
if (memcmp(oobinfo.name, rsv_info->name,
|
|
4))
|
|
pr_info("invalid %s info in %llx:%s\n",
|
|
rsv_info->name, offset, oobinfo.name);
|
|
offset += mtd->writesize;
|
|
length += oob_ops.len;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
int meson_rsv_erase(struct meson_rsv_info_t *rsv_info)
|
|
{
|
|
struct mtd_info *mtd;
|
|
struct free_node_t *free_node, *temp_node = NULL;
|
|
int ret = 0;
|
|
struct erase_info erase_info;
|
|
|
|
if (!rsv_info) {
|
|
pr_info("%s %d rsv info has not inited yet!\n",
|
|
__func__, __LINE__);
|
|
return 1;
|
|
}
|
|
|
|
mtd = rsv_info->mtd;
|
|
pr_info("%s %d rsv erasing %s\n",
|
|
__func__, __LINE__, rsv_info->name);
|
|
|
|
if (rsv_info->valid) {
|
|
rsv_info->nvalid->ec++;
|
|
rsv_info->nvalid->page_addr = -1;
|
|
rsv_info->nvalid->timestamp = 1;
|
|
rsv_info->valid = 0;
|
|
|
|
free_node = get_free_node(rsv_info);
|
|
if (!free_node)
|
|
return -ENOMEM;
|
|
/* set current valid node to free list */
|
|
free_node->blk_addr = rsv_info->nvalid->blk_addr;
|
|
free_node->ec = rsv_info->nvalid->ec;
|
|
temp_node = rsv_info->nfree;
|
|
while (temp_node->next)
|
|
temp_node = temp_node->next;
|
|
temp_node->next = free_node;
|
|
}
|
|
|
|
temp_node = rsv_info->nfree;
|
|
while (temp_node) {
|
|
memset(&erase_info, 0, sizeof(struct erase_info));
|
|
erase_info.mtd = mtd;
|
|
erase_info.addr = temp_node->blk_addr* mtd->erasesize;
|
|
erase_info.len = mtd->erasesize;
|
|
menson_rsv_disprotect();
|
|
ret = mtd_erase(mtd, &erase_info);
|
|
menson_rsv_protect();
|
|
printk("erasing valid info block: %llx \n", erase_info.addr);
|
|
rsv_info->nvalid->ec = -1;
|
|
temp_node->dirty_flag = 0;
|
|
temp_node = temp_node->next;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static u32 skip_bbt_blocks(struct meson_rsv_info_t *rsv_info, u32 start)
|
|
{
|
|
if (memcmp(BBT_NAND_MAGIC, rsv_info->name, 4))
|
|
if (start >= BBT_START_BLOCK &&
|
|
start < BBT_START_BLOCK + BBT_TOTAL_BLOCKS)
|
|
return start + BBT_TOTAL_BLOCKS;
|
|
return start;
|
|
}
|
|
|
|
int meson_rsv_scan(struct meson_rsv_info_t *rsv_info)
|
|
{
|
|
struct mtd_info *mtd = rsv_info->mtd;
|
|
struct mtd_oob_ops oob_ops;
|
|
struct oobinfo_t oobinfo;
|
|
struct free_node_t *free_node, *temp_node;
|
|
loff_t offset;
|
|
u32 start, end;
|
|
int ret = 0, error, rsv_status, i, k;
|
|
u8 scan_status;
|
|
u8 good_addr[256] = {0};
|
|
u32 page_num, pages_per_blk;
|
|
|
|
RE_RSV_INFO_EXT:
|
|
start = rsv_info->start;
|
|
end = rsv_info->end;
|
|
do {
|
|
offset = skip_bbt_blocks(rsv_info, start);
|
|
offset *= mtd->erasesize;
|
|
scan_status = 0;
|
|
RE_RSV_INFO:
|
|
oob_ops.mode = MTD_OPS_AUTO_OOB;
|
|
oob_ops.len = 0;
|
|
oob_ops.ooblen = sizeof(struct oobinfo_t);
|
|
oob_ops.ooboffs = 0;
|
|
oob_ops.datbuf = NULL;
|
|
oob_ops.oobbuf = (u8 *)&oobinfo;
|
|
memset((u8 *)&oobinfo, 0, sizeof(struct oobinfo_t));
|
|
error = mtd_read_oob(mtd, offset, &oob_ops);
|
|
if (error && (error != -EUCLEAN)) {
|
|
pr_info("%s %d blk check good but read failed: %llx, %d\n",
|
|
__func__, __LINE__, (u64)offset, error);
|
|
offset += rsv_info->size;
|
|
if ((scan_status++ > 6) ||
|
|
(!(offset % mtd->erasesize))) {
|
|
pr_info("ECC error, scan ONE block exit\n");
|
|
scan_status = 0;
|
|
continue;
|
|
}
|
|
goto RE_RSV_INFO;
|
|
}
|
|
rsv_info->init = 1;
|
|
rsv_info->nvalid->status = 0;
|
|
/* Do not use strlen ,Use ARRAY_SIZE to make the length 4 */
|
|
if (!memcmp(oobinfo.name, rsv_info->name, 4)) {
|
|
rsv_info->valid = 1;
|
|
if (rsv_info->nvalid->blk_addr >= 0) {
|
|
free_node = get_free_node(rsv_info);
|
|
if (!free_node)
|
|
return -ENOMEM;
|
|
free_node->dirty_flag = 1;
|
|
if (oobinfo.timestamp >
|
|
rsv_info->nvalid->timestamp) {
|
|
free_node->blk_addr =
|
|
rsv_info->nvalid->blk_addr;
|
|
free_node->ec = rsv_info->nvalid->ec;
|
|
rsv_info->nvalid->blk_addr =
|
|
skip_bbt_blocks(rsv_info, start);
|
|
rsv_info->nvalid->page_addr = 0;
|
|
rsv_info->nvalid->ec = oobinfo.ec;
|
|
rsv_info->nvalid->timestamp =
|
|
oobinfo.timestamp;
|
|
} else {
|
|
free_node->blk_addr = skip_bbt_blocks(rsv_info, start);
|
|
free_node->ec = oobinfo.ec;
|
|
}
|
|
if (!rsv_info->nfree) {
|
|
rsv_info->nfree = free_node;
|
|
} else {
|
|
temp_node = rsv_info->nfree;
|
|
while (temp_node->next)
|
|
temp_node = temp_node->next;
|
|
temp_node->next = free_node;
|
|
}
|
|
} else {
|
|
rsv_info->nvalid->blk_addr = skip_bbt_blocks(rsv_info, start);
|
|
rsv_info->nvalid->page_addr = 0;
|
|
rsv_info->nvalid->ec = oobinfo.ec;
|
|
rsv_info->nvalid->timestamp = oobinfo.timestamp;
|
|
}
|
|
} else {
|
|
free_node = get_free_node(rsv_info);
|
|
if (!free_node)
|
|
return -ENOMEM;
|
|
free_node->blk_addr = skip_bbt_blocks(rsv_info, start);
|
|
free_node->ec = oobinfo.ec;
|
|
if (!rsv_info->nfree) {
|
|
rsv_info->nfree = free_node;
|
|
} else {
|
|
temp_node = rsv_info->nfree;
|
|
while (temp_node->next)
|
|
temp_node = temp_node->next;
|
|
temp_node->next = free_node;
|
|
}
|
|
}
|
|
} while ((++start) < end);
|
|
|
|
printf("%s blk = %d, ec = %d, page = %d, timestamp = %d\n",
|
|
rsv_info->name, rsv_info->nvalid->blk_addr, rsv_info->nvalid->ec,
|
|
rsv_info->nvalid->page_addr, rsv_info->nvalid->timestamp);
|
|
printf("%s free list:\n", rsv_info->name);
|
|
temp_node = rsv_info->nfree;
|
|
while (temp_node) {
|
|
printf("block num = %d, ec = %d, dirty_flag = %d\n",
|
|
temp_node->blk_addr,
|
|
temp_node->ec,
|
|
temp_node->dirty_flag);
|
|
temp_node = temp_node->next;
|
|
}
|
|
/**
|
|
* step 2, find the newest in the block
|
|
* watch out here, cause erase size and write size must be
|
|
* power of 2, and write size must equal page size.
|
|
*/
|
|
pages_per_blk = 1 << (mtd->erasesize_shift - mtd->writesize_shift);
|
|
page_num = rsv_info->size >> mtd->writesize_shift;
|
|
if (!page_num)
|
|
page_num++;
|
|
if (rsv_info->valid == 1) {
|
|
pr_info("%s %d selecting in block: %d\n",
|
|
__func__, __LINE__, rsv_info->nvalid->blk_addr);
|
|
oob_ops.mode = MTD_OPS_AUTO_OOB;
|
|
oob_ops.len = 0;
|
|
oob_ops.ooblen = sizeof(struct oobinfo_t);
|
|
oob_ops.ooboffs = 0;
|
|
oob_ops.datbuf = NULL;
|
|
oob_ops.oobbuf = (u8 *)&oobinfo;
|
|
for (i = 0; i < pages_per_blk; i++) {
|
|
memset((u8 *)&oobinfo, 0, oob_ops.ooblen);
|
|
offset = rsv_info->nvalid->blk_addr;
|
|
offset *= mtd->erasesize;
|
|
offset += ((u64)mtd->writesize) * i;
|
|
error = mtd_read_oob(mtd, offset, &oob_ops);
|
|
if (error && error != -EUCLEAN) {
|
|
pr_info("%s %d blk good but read failed:%llx, %d\n",
|
|
__func__, __LINE__, (u64)offset, error);
|
|
rsv_info->nvalid->status |= ECC_ABNORMAL_FLAG;
|
|
continue;
|
|
}
|
|
/* Do not use strlen ,Use ARRAY_SIZE to make the length 4 */
|
|
if (!memcmp(oobinfo.name, rsv_info->name,
|
|
4)) {
|
|
good_addr[i] = 1;
|
|
rsv_info->nvalid->page_addr = i;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (mtd->writesize < rsv_info->size &&
|
|
rsv_info->valid == 1) {
|
|
i = rsv_info->nvalid->page_addr;
|
|
if (((i + 1) % page_num) != 0) {
|
|
ret = -1;
|
|
rsv_info->nvalid->status |= POWER_ABNORMAL_FLAG;
|
|
pr_info("find %s incomplete\n", rsv_info->name);
|
|
}
|
|
pr_info("%s %d page_num %d\n", __func__, __LINE__, page_num);
|
|
if (ret == -1) {
|
|
for (i = 0; i < (pages_per_blk / page_num); i++) {
|
|
rsv_status = 0;
|
|
for (k = 0; k < page_num; k++) {
|
|
if (!good_addr[k + i * page_num]) {
|
|
rsv_status = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (!rsv_status) {
|
|
pr_info("find %d page ok\n",
|
|
i * page_num);
|
|
rsv_info->nvalid->page_addr =
|
|
k + i * page_num - 1;
|
|
ret = 0;
|
|
}
|
|
}
|
|
}
|
|
if (ret == -1) {
|
|
rsv_info->nvalid->status = 0;
|
|
meson_rsv_free(rsv_info);
|
|
goto RE_RSV_INFO_EXT;
|
|
}
|
|
i = (rsv_info->size + mtd->writesize - 1) / mtd->writesize;
|
|
rsv_info->nvalid->page_addr -= (i - 1);
|
|
}
|
|
if (rsv_info->valid != 1)
|
|
ret = -1;
|
|
offset = rsv_info->nvalid->blk_addr;
|
|
offset *= mtd->erasesize;
|
|
offset += ((u64)rsv_info->nvalid->page_addr) * mtd->writesize;
|
|
pr_info("%s valid address 0x%llx\n", rsv_info->name, offset);
|
|
return ret;
|
|
}
|
|
|
|
int meson_rsv_check(struct meson_rsv_info_t *rsv_info)
|
|
{
|
|
int ret = 0;
|
|
|
|
ret = meson_rsv_scan(rsv_info);
|
|
if (ret)
|
|
pr_info("%s %d %s info check failed ret %d\n",
|
|
__func__, __LINE__, rsv_info->name, ret);
|
|
if (!rsv_info->valid) {
|
|
pr_info("%s %d no %s info exist\n",
|
|
__func__, __LINE__, rsv_info->name);
|
|
ret = 1;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void meson_rsv_check_all_except_bbt(void)
|
|
{
|
|
struct meson_rsv_info_t *rsv_info;
|
|
int i;
|
|
|
|
for (i = 1; i < rsv_handler->entries; i++) {
|
|
rsv_info = &rsv_handler->rsv_info[i];
|
|
meson_rsv_check(rsv_info);
|
|
}
|
|
}
|
|
|
|
int meson_rsv_check_bbt(void)
|
|
{
|
|
struct meson_rsv_info_t *rsv_info;
|
|
int index = rsvname2index(BBT_NAND_MAGIC);
|
|
|
|
if (index < 0)
|
|
return -1;
|
|
|
|
rsv_info = &rsv_handler->rsv_info[index];
|
|
return meson_rsv_check(rsv_info);
|
|
}
|
|
|
|
int meson_rsv_save_bbt(u8 *bbt)
|
|
{
|
|
struct meson_rsv_info_t *rsv_info;
|
|
int index = rsvname2index(BBT_NAND_MAGIC);
|
|
|
|
if (index < 0)
|
|
return -1;
|
|
|
|
rsv_info = &rsv_handler->rsv_info[index];
|
|
return meson_rsv_save(rsv_info, bbt);
|
|
}
|
|
|
|
int meson_rsv_read_bbt(u8 *bbt)
|
|
{
|
|
struct meson_rsv_info_t *rsv_info;
|
|
int index = rsvname2index(BBT_NAND_MAGIC);
|
|
|
|
if (index < 0)
|
|
return -1;
|
|
|
|
rsv_info = &rsv_handler->rsv_info[index];
|
|
return meson_rsv_read(rsv_info, bbt);
|
|
}
|
|
|
|
static int aml_nand_rsv_info_alloc_init(struct mtd_info *mtd,
|
|
struct meson_rsv_handler_t *handler,
|
|
struct rsv_part *rsv_part)
|
|
{
|
|
struct meson_rsv_info_t *rsv_info = &handler->rsv_info[handler->entries++];
|
|
|
|
rsv_info->nvalid =
|
|
kzalloc(sizeof(struct valid_node_t), GFP_KERNEL);
|
|
if (!rsv_info->nvalid) {
|
|
handler->entries--;
|
|
return -ENOMEM;
|
|
}
|
|
|
|
rsv_info->mtd = mtd;
|
|
rsv_info->start = rsv_part->start_block;
|
|
rsv_info->end = rsv_part->start_block + rsv_part->blocks;
|
|
rsv_info->nvalid->blk_addr = -1;
|
|
rsv_info->handler = handler;
|
|
if (!memcmp(rsv_part->name, BBT_NAND_MAGIC, 4))
|
|
rsv_info->size = mtd->size >> mtd->erasesize_shift;
|
|
else
|
|
rsv_info->size = rsv_part->size;
|
|
|
|
memcpy(rsv_info->name, rsv_part->name, 4);
|
|
rsv_info->name[4] = '\0';
|
|
|
|
printf("%s init : start(end) = %d(%d) %p\n",
|
|
rsv_info->name, rsv_info->start, rsv_info->end, rsv_info);
|
|
return 0;
|
|
}
|
|
|
|
int meson_rsv_init(struct mtd_info *mtd,
|
|
struct meson_rsv_handler_t *handler)
|
|
{
|
|
struct rsv_part *rsv_part;
|
|
struct free_node_t *ptr;
|
|
int i, ret = 0, array_size;
|
|
u32 rsv_blocks;
|
|
|
|
rsv_part = meson_rsv_info_calculate_start_block(mtd);
|
|
if (!rsv_part)
|
|
return -ENOMEM;
|
|
|
|
rsv_handler = handler;
|
|
rsv_handler->rsv_part = rsv_part;
|
|
handler->entries = 0;
|
|
|
|
rsv_blocks = meson_rsv_part_get_total_blocks(rsv_part);
|
|
handler->free_node = kzalloc(rsv_blocks * sizeof(struct free_node_t *), GFP_KERNEL);
|
|
if (!handler->free_node)
|
|
return -ENOMEM;
|
|
|
|
ptr = kzalloc(sizeof(struct free_node_t) * rsv_blocks, GFP_KERNEL);
|
|
if (!ptr) {
|
|
kfree(handler->free_node);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
handler->fn_bitmask = 0;
|
|
for (i = 0; i < rsv_blocks; i++, ptr++) {
|
|
handler->free_node[i] = ptr;
|
|
handler->free_node[i]->index = i;
|
|
}
|
|
|
|
array_size = get_mtd_rsv_partition_count();
|
|
for (i = 0; i < array_size; i++, rsv_part++) {
|
|
if (!rsv_part->blocks)
|
|
continue;
|
|
ret = aml_nand_rsv_info_alloc_init(mtd, handler, rsv_part);
|
|
if (ret)
|
|
break;
|
|
}
|
|
|
|
for (; ret && handler->entries > 0; --handler->entries)
|
|
kfree(handler->rsv_info[handler->entries - 1].nvalid);
|
|
|
|
if (!handler->entries) {
|
|
kfree(ptr);
|
|
kfree(rsv_handler->free_node);
|
|
kfree(rsv_handler->rsv_part);
|
|
ret = -1;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
int meson_modify_page_info_and_save(struct mtd_info *mtd)
|
|
{
|
|
u64 bl2_mem, bl2_size = BL2_SIZE;
|
|
int ret, i;
|
|
u_char *bl2_buf;
|
|
char str[128];
|
|
|
|
bl2_buf = malloc(bl2_size);
|
|
if (!bl2_buf)
|
|
return -ENOMEM;
|
|
|
|
bl2_mem = (u64)bl2_buf;
|
|
for (i = 0; i < 4; i++) {
|
|
sprintf(str, "store boot_read bl2 0x%llx %d 0x%llx", bl2_mem, i, bl2_size);
|
|
printf("command: %s\n", str);
|
|
ret = run_command(str, 0);
|
|
if (ret)
|
|
goto _err_modify_page_info;
|
|
|
|
sprintf(str, "store boot_erase bl2 %d", i);
|
|
printf("command: %s\n", str);
|
|
ret = run_command(str, 0);
|
|
if (ret)
|
|
goto _err_modify_page_info;
|
|
|
|
sprintf(str, "store boot_write bl2 0x%llx %d 0x%llx", bl2_mem, i, bl2_size);
|
|
printf("command: %s\n", str);
|
|
ret = run_command(str, 0);
|
|
if (ret)
|
|
goto _err_modify_page_info;
|
|
}
|
|
|
|
_err_modify_page_info:
|
|
free(bl2_buf);
|
|
return ret;
|
|
}
|
|
|
|
int meson_ext_rsv_info_read(u_char *dest, size_t size, int index)
|
|
{
|
|
struct meson_rsv_info_t *rsv_info;
|
|
u_char *temp;
|
|
int ret;
|
|
|
|
rsv_info = &rsv_handler->rsv_info[index];
|
|
if (!rsv_info->size || !rsv_info->valid) {
|
|
pr_info("%s %d: not inited yet!\n", __func__, __LINE__);
|
|
return RSV_INVALID;
|
|
}
|
|
|
|
temp = kzalloc(rsv_info->size, GFP_KERNEL);
|
|
if (!temp)
|
|
return -ENOMEM;
|
|
|
|
ret = meson_rsv_read(rsv_info, temp);
|
|
memcpy(dest, temp, rsv_info->size > size ? size : rsv_info->size);
|
|
|
|
kfree(temp);
|
|
return ret;
|
|
}
|
|
|
|
int meson_ext_rsv_info_write(u_char *source, size_t size, int index)
|
|
{
|
|
struct meson_rsv_info_t *rsv_info;
|
|
u_char *temp;
|
|
int ret;
|
|
|
|
rsv_info = &rsv_handler->rsv_info[index];
|
|
if (!rsv_info->size) {
|
|
pr_info("%s %d: not inited yet!\n", __func__, __LINE__);
|
|
return RSV_INVALID;
|
|
}
|
|
|
|
temp = kzalloc(rsv_info->size, GFP_KERNEL);
|
|
if (!temp)
|
|
return -ENOMEM;
|
|
|
|
memset(temp, 0, rsv_info->size);
|
|
memcpy(temp, source, rsv_info->size > size ? size : rsv_info->size);
|
|
ret = meson_rsv_save(rsv_info, temp);
|
|
kfree(temp);
|
|
|
|
if (IS_ENABLED(CONFIG_DDR_PARAMETER_SUPPORT) && IS_ENABLED(CONFIG_MTD_SPI_NAND))
|
|
ret = meson_modify_page_info_and_save(rsv_info->mtd);
|
|
|
|
return ret;
|
|
}
|
|
|
|
u32 meson_ext_rsv_info_size(int index)
|
|
{
|
|
return rsv_handler->rsv_info[index].size;
|
|
}
|
|
|
|
int meson_ext_rsv_info_erase(int index)
|
|
{
|
|
struct meson_rsv_info_t *rsv_info;
|
|
|
|
rsv_info = &rsv_handler->rsv_info[index];
|
|
if (rsv_info->valid)
|
|
return meson_rsv_erase(rsv_info);
|
|
return 0;
|
|
}
|
|
|
|
static int meson_rsv_fdt_setprop(void *fdt, int node_offset,
|
|
const char *name, const void *val, bool string)
|
|
{
|
|
int err = 0;
|
|
|
|
add:
|
|
if (string) {
|
|
err = fdt_setprop_string(fdt, node_offset, name, val);
|
|
} else {
|
|
u32 *tmp = (u32 *)val;
|
|
|
|
err = fdt_setprop_cell(fdt, node_offset, name, *tmp);
|
|
}
|
|
if (err == -FDT_ERR_NOSPACE) {
|
|
err = fdt_increase_size(fdt, 512);
|
|
if (!err)
|
|
goto add;
|
|
else
|
|
goto err_size;
|
|
}
|
|
|
|
return err;
|
|
|
|
err_size:
|
|
pr_err("Can't increase blob size: %s\n", fdt_strerror(err));
|
|
return err;
|
|
}
|
|
|
|
static int meson_rsv_add_node(void *blob, int parent_offset, int index,
|
|
const char *name, u32 block_start, u32 block_cnt, u32 size)
|
|
{
|
|
int err = 0, node_offset = 0;
|
|
char buf[64];
|
|
|
|
pr_info("%s : add node%d %s block start %u block cnt %u size %u\n",
|
|
__func__, index, name, block_start, block_cnt, size);
|
|
add_node:
|
|
memset(buf, 0, sizeof(buf));
|
|
sprintf(buf, "node%d", index);
|
|
node_offset = fdt_add_subnode(blob, parent_offset, buf);
|
|
if (node_offset == -FDT_ERR_NOSPACE) {
|
|
err = fdt_increase_size(blob, 512);
|
|
if (!err)
|
|
goto add_node;
|
|
else
|
|
goto err_size;
|
|
} else if (node_offset == -FDT_ERR_EXISTS) {
|
|
node_offset = fdt_subnode_offset(blob, parent_offset, buf);
|
|
} else if (node_offset < 0) {
|
|
pr_err("Can't add mtdrsvpart node: %s\n",
|
|
fdt_strerror(node_offset));
|
|
return node_offset;
|
|
}
|
|
|
|
err = meson_rsv_fdt_setprop(blob, node_offset, "size", &size, 0);
|
|
if (err)
|
|
goto err_prop;
|
|
|
|
err = meson_rsv_fdt_setprop(blob, node_offset, "block_cnt",
|
|
&block_cnt, 0);
|
|
if (err)
|
|
goto err_prop;
|
|
|
|
err = meson_rsv_fdt_setprop(blob, node_offset, "block_start",
|
|
&block_start, 0);
|
|
if (err)
|
|
goto err_prop;
|
|
|
|
err = meson_rsv_fdt_setprop(blob, node_offset, "label", name, 1);
|
|
if (err)
|
|
goto err_prop;
|
|
|
|
return 0;
|
|
|
|
err_size:
|
|
pr_err("Can't increase blob size: %s\n", fdt_strerror(err));
|
|
|
|
err_prop:
|
|
pr_err("Can't add property: %s\n", fdt_strerror(err));
|
|
return err;
|
|
}
|
|
|
|
int meson_rsv_add_dtb(void *blob, int parent_offset)
|
|
{
|
|
struct meson_rsv_info_t *rsv_info;
|
|
int rsvparts_offset, ret = 0, idx = 0, array_size;
|
|
char buf[64];
|
|
|
|
sprintf(buf, "rsv_partition");
|
|
|
|
add_rsvparts:
|
|
rsvparts_offset = fdt_add_subnode(blob, parent_offset, buf);
|
|
if (rsvparts_offset == -FDT_ERR_NOSPACE) {
|
|
ret = fdt_increase_size(blob, 512);
|
|
if (!ret) {
|
|
goto add_rsvparts;
|
|
} else {
|
|
pr_err("Can't increase blob size: %s\n", fdt_strerror(ret));
|
|
return ret;
|
|
}
|
|
} else if (rsvparts_offset == -FDT_ERR_EXISTS) {
|
|
rsvparts_offset = fdt_subnode_offset(blob, parent_offset, buf);
|
|
fdt_del_node(blob, rsvparts_offset);
|
|
pr_err("mtdrsvpart node already exists, overwrite it!\n");
|
|
goto add_rsvparts;
|
|
} else if (rsvparts_offset < 0) {
|
|
pr_err("Can't add mtdrsvpart node: %s\n",
|
|
fdt_strerror(rsvparts_offset));
|
|
return rsvparts_offset;
|
|
}
|
|
|
|
ret = meson_rsv_add_node(blob, rsvparts_offset, idx, "nrsv",
|
|
meson_rsv_part_get_start_block(rsv_handler->mtd),
|
|
meson_rsv_part_get_total_blocks(rsv_handler->rsv_part),
|
|
0);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
array_size = get_mtd_rsv_partition_count();
|
|
for (rsv_info = rsv_handler->rsv_info;
|
|
idx < array_size; rsv_info++, idx++) {
|
|
ret = meson_rsv_add_node(blob,
|
|
rsvparts_offset,
|
|
idx + 1,
|
|
rsv_info->name,
|
|
rsv_info->start,
|
|
rsv_info->end - rsv_info->start,
|
|
rsv_info->size);
|
|
if (ret < 0)
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|