/** Mark this block as bad block */ URET uffs_FlashMarkBadBlock(uffs_Device *dev, int block) { int ret; uffs_BlockInfo *bc; uffs_Perror(UFFS_MSG_NORMAL, "Mark bad block: %d", block); bc = uffs_BlockInfoGet(dev, block); if (bc) { uffs_BlockInfoExpire(dev, bc, UFFS_ALL_PAGES); // expire this block, just in case it's been cached before uffs_BlockInfoPut(dev, bc); } if (dev->ops->MarkBadBlock) return dev->ops->MarkBadBlock(dev, block) == 0 ? U_SUCC : U_FAIL; #ifdef CONFIG_ERASE_BLOCK_BEFORE_MARK_BAD ret = dev->ops->EraseBlock(dev, block); if (ret != UFFS_FLASH_IO_ERR) { // note: even EraseBlock return UFFS_FLASH_BAD_BLK, // we still process it ... not recommended for most NAND flash. #endif if (dev->ops->WritePageWithLayout) ret = dev->ops->WritePageWithLayout(dev, block, 0, NULL, 0, NULL, NULL); else ret = dev->ops->WritePage(dev, block, 0, NULL, 0, NULL, 0); #ifdef CONFIG_ERASE_BLOCK_BEFORE_MARK_BAD } #endif return ret == UFFS_FLASH_NO_ERR ? U_SUCC : U_FAIL; }
/** Erase flash block */ URET uffs_FlashEraseBlock(uffs_Device *dev, int block) { int ret; uffs_BlockInfo *bc; ret = dev->ops->EraseBlock(dev, block); if (UFFS_FLASH_IS_BAD_BLOCK(ret)) uffs_BadBlockAdd(dev, block); bc = uffs_BlockInfoGet(dev, block); if (bc) { uffs_BlockInfoExpire(dev, bc, UFFS_ALL_PAGES); uffs_BlockInfoPut(dev, bc); } return UFFS_FLASH_HAVE_ERR(ret) ? U_FAIL : U_SUCC; }
/** truncate obj without lock device */ static URET do_TruncateObject(uffs_Object *obj, u32 remain, RunOptionE run_opt) { uffs_Device *dev = obj->dev; TreeNode *fnode = obj->node; u16 fdn; u32 flen; u32 block_start; TreeNode *node; uffs_BlockInfo *bc; uffs_Buf *buf; u16 page; int pos; pos = obj->pos; // save current file position if (obj->dev == NULL || obj->open_succ == U_FALSE || fnode == NULL) { obj->err = UEBADF; goto ext; } /* can't truncate a dir */ /* TODO: delete files under dir ? */ if (obj->type == UFFS_TYPE_DIR) { obj->err = UEEXIST; goto ext; } if (remain >= fnode->u.file.len) { goto ext; //!< nothing to do ... } flen = fnode->u.file.len; if (flen < remain) { // file is shorter than 'reamin', fill the gap with '\0' if (run_opt == eREAL_RUN) { obj->pos = flen; // move file pointer to the end if (do_WriteObject(obj, NULL, remain - flen) > 0) { // fill '\0' ... uffs_Perror(UFFS_MSG_SERIOUS, "Write object not finished. expect %d but only %d wrote.", remain - flen, fnode->u.file.len - flen); obj->err = UEIOERR; // likely be an I/O error. } flen = obj->node->u.file.len; } } else { while (flen > remain) { fdn = GetFdnByOfs(obj, flen - 1); //uffs_BufFlushGroup(dev, obj->serial, fdn); //!< flush the buffer block_start = GetStartOfDataBlock(obj, fdn); if (remain <= block_start && fdn > 0) { node = uffs_TreeFindDataNode(dev, obj->serial, fdn); if (node == NULL) { uffs_Perror(UFFS_MSG_SERIOUS, "can't find data node when trancate obj."); obj->err = UEIOERR; goto ext; } bc = uffs_BlockInfoGet(dev, node->u.data.block); if (bc == NULL) { uffs_Perror(UFFS_MSG_SERIOUS, "can't get block info when trancate obj."); obj->err = UEIOERR; goto ext; } for (page = 0; page < dev->attr->pages_per_block; page++) { buf = uffs_BufFind(dev, fnode->u.file.serial, fdn, page); if (buf) { //!< ok, the buffer was loaded before ... if (uffs_BufIsFree(buf) == U_FALSE) { uffs_BlockInfoPut(dev, bc); goto ext; //!< and someone is still holding the buffer, // can't truncate it !!! } else if (run_opt == eREAL_RUN) uffs_BufMarkEmpty(dev, buf); //!< discard the buffer } } if (run_opt == eREAL_RUN) { uffs_BreakFromEntry(dev, UFFS_TYPE_DATA, node); uffs_FlashEraseBlock(dev, bc->block); node->u.list.block = bc->block; if (HAVE_BADBLOCK(dev)) uffs_BadBlockProcess(dev, node); else uffs_TreeInsertToErasedListTail(dev, node); fnode->u.file.len = block_start; } flen = block_start; uffs_BlockInfoPut(dev, bc); } else { if (do_TruncateInternalWithBlockRecover(obj, fdn, remain, run_opt) == U_SUCC) { if (run_opt == eREAL_RUN) fnode->u.file.len = remain; flen = remain; } } } } if (HAVE_BADBLOCK(dev)) uffs_BadBlockRecover(dev); ext: obj->pos = pos; // keep file pointer offset not changed. uffs_Assert(fnode == obj->node, "obj->node change!\n"); return (obj->err == UENOERR ? U_SUCC : U_FAIL); }
// // To trancate the file, this is the last block to be trancated. // We need to discard one or more pages within this block, hence requires 'block recover'. // static URET do_TruncateInternalWithBlockRecover(uffs_Object *obj, u16 fdn, u32 remain, RunOptionE run_opt) { uffs_Device *dev = obj->dev; TreeNode *fnode = obj->node; u16 page_id, max_page_id; TreeNode *node; uffs_Buf *buf = NULL; u8 type; u32 block_start, block_offset; u16 parent, serial; int slot; uffs_BlockInfo *bc = NULL; int block = -1; if (fdn == 0) { node = fnode; type = UFFS_TYPE_FILE; max_page_id = obj->head_pages; block_start = 0; parent = node->u.file.parent; serial = node->u.file.serial; block = node->u.file.block; } else { node = uffs_TreeFindDataNode(dev, fnode->u.file.serial, fdn); if (node == NULL) { obj->err = UEIOERR; uffs_Perror(UFFS_MSG_SERIOUS, "can't find data node when truncate obj"); goto ext; } block = node->u.data.block; type = UFFS_TYPE_DATA; max_page_id = dev->attr->pages_per_block - 1; block_start = obj->head_pages * dev->com.pg_data_size + (fdn - 1) * dev->com.pg_data_size * dev->attr->pages_per_block; parent = node->u.data.parent; serial = node->u.data.serial; } if (run_opt == eDRY_RUN) { // checking the buffer. this is the main reason why we need the 'dry run' mode. for (page_id = 0; page_id <= max_page_id; page_id++) { buf = uffs_BufFind(dev, parent, serial, page_id); if (buf) { //!< ok, the buffer was loaded before ... if (uffs_BufIsFree(buf) == U_FALSE) { obj->err = UEEXIST; break; //!< and someone is still holding the buffer, // can't truncate it !!! } } } buf = NULL; goto ext; } // find the last page *after* truncate block_offset = remain - block_start; page_id = block_offset / dev->com.pg_data_size; if (fdn == 0) page_id++; if (!uffs_Assert(page_id <= max_page_id, "fdn = %d, block_start = %d, remain = %d\n", fdn, block_start, remain)) { obj->err = UEUNKNOWN_ERR; goto ext; } // flush buffer before performing block recovery uffs_BufFlushGroup(dev, parent, serial); // load the last page buf = uffs_BufGetEx(dev, type, node, page_id, obj->oflag); if (buf == NULL) { obj->err = UENOMEM; uffs_Perror(UFFS_MSG_SERIOUS, "Can't get buf"); goto ext; } uffs_BufWrite(dev, buf, NULL, 0, 0); // just make this buf dirty // lock the group slot = uffs_BufFindGroupSlot(dev, parent, serial); uffs_BufLockGroup(dev, slot); if (remain == 0) // remain == 0: means discard all data in this block. buf->data_len = 0; else { remain = (remain % dev->com.pg_data_size); // remain == 0: means that we need to keep all data in this page. buf->data_len = (remain == 0 ? dev->com.pg_data_size : remain); } /* mark this buf as UFFS_BUF_EXT_MARK_TRUNC_TAIL, when flushing dirty buffers, UFFS will not do page recover for pages after this buf page id (because this file is ended at this page) */ buf->ext_mark |= UFFS_BUF_EXT_MARK_TRUNC_TAIL; uffs_BufPut(dev, buf); // invalidate the rest page buf page_id++; for (; page_id <= max_page_id; page_id++) { buf = uffs_BufFind(dev, parent, serial, page_id); if (buf) uffs_BufMarkEmpty(dev, buf); } // flush dirty buffer immediately, forcing block recovery. uffs_BufFlushGroupEx(dev, parent, serial, U_TRUE); // unlock the group uffs_BufUnLockGroup(dev, slot); // Invalidate block info cache for the 'old' block bc = uffs_BlockInfoGet(dev, block); if (bc) { uffs_BlockInfoExpire(dev, bc, UFFS_ALL_PAGES); uffs_BlockInfoPut(dev, bc); } ext: return (obj->err == UENOERR ? U_SUCC : U_FAIL); }
/** truncate obj without lock device */ static URET do_TruncateObject(uffs_Object *obj, u32 remain, UBOOL dry_run) { uffs_Device *dev = obj->dev; TreeNode *fnode = obj->node; u16 fdn; u32 flen; u32 block_start; TreeNode *node; uffs_BlockInfo *bc; uffs_Buf *buf; u16 page; if (obj->dev == NULL || obj->open_succ == U_FALSE || fnode == NULL) { obj->err = UEBADF; goto ext; } /* can't truncate a dir */ /* TODO: delete files under dir ? */ if (obj->type == UFFS_TYPE_DIR) { obj->err = UEEXIST; goto ext; } if (remain >= fnode->u.file.len) { goto ext; //!< nothing to do ... } flen = fnode->u.file.len; while (flen > remain) { fdn = GetFdnByOfs(obj, flen - 1); //uffs_BufFlushGroup(dev, obj->serial, fdn); //!< flush the buffer block_start = GetStartOfDataBlock(obj, fdn); if (remain <= block_start && fdn > 0) { node = uffs_TreeFindDataNode(dev, obj->serial, fdn); if (node == NULL) { uffs_Perror(UFFS_ERR_SERIOUS, "can't find data node when trancate obj."); obj->err = UEIOERR; goto ext; } bc = uffs_BlockInfoGet(dev, node->u.data.block); if (bc == NULL) { uffs_Perror(UFFS_ERR_SERIOUS, "can't get block info when trancate obj."); obj->err = UEIOERR; goto ext; } for (page = 0; page < dev->attr->pages_per_block; page++) { buf = uffs_BufFind(dev, fnode->u.file.serial, fdn, page); if (buf) { //!< ok, the buffer was loaded before ... if (uffs_BufIsFree(buf) == U_FALSE) { uffs_BlockInfoPut(dev, bc); goto ext; //!< and someone is still holding the buffer, can't truncate it !!! } else if (dry_run == U_FALSE) uffs_BufMarkEmpty(dev, buf); //!< discard the buffer } } if (dry_run == U_FALSE) { uffs_BlockInfoExpire(dev, bc, UFFS_ALL_PAGES); uffs_BreakFromEntry(dev, UFFS_TYPE_DATA, node); uffs_FlashEraseBlock(dev, bc->block); node->u.list.block = bc->block; if (HAVE_BADBLOCK(dev)) uffs_BadBlockProcess(dev, node); else uffs_TreeInsertToErasedListTail(dev, node); uffs_BlockInfoPut(dev, bc); fnode->u.file.len = block_start; } else { uffs_BlockInfoPut(dev, bc); } flen = block_start; } else { if (do_TruncateInternalWithBlockRecover(obj, fdn, remain, dry_run) == U_SUCC) { if (dry_run == U_FALSE) fnode->u.file.len = remain; flen = remain; } } } if (HAVE_BADBLOCK(dev)) uffs_BadBlockRecover(dev); ext: return (obj->err == UENOERR ? U_SUCC : U_FAIL); }
static URET _BuildTreeStepOne(uffs_Device *dev) { int block_lt; uffs_BlockInfo *bc = NULL; TreeNode *node; struct uffs_TreeSt *tree; uffs_Pool *pool; struct uffs_MiniHeaderSt header; URET ret = U_SUCC; struct BlockTypeStatSt st = {0, 0, 0}; tree = &(dev->tree); pool = TPOOL(dev); tree->bad = NULL; tree->bad_count = 0; tree->erased = NULL; tree->erased_tail = NULL; tree->erased_count = 0; uffs_Perror(UFFS_MSG_NOISY, "build tree step one"); // printf("s:%d e:%d\n", dev->par.start, dev->par.end); for (block_lt = dev->par.start; block_lt <= dev->par.end; block_lt++) { bc = uffs_BlockInfoGet(dev, block_lt); if (bc == NULL) { uffs_Perror(UFFS_MSG_SERIOUS, "step one:fail to get block info"); ret = U_FAIL; break; } node = (TreeNode *)uffs_PoolGet(pool); if (node == NULL) { uffs_Perror(UFFS_MSG_SERIOUS, "insufficient tree node!"); ret = U_FAIL; break; } // Need to check bad block at first ! if (uffs_FlashIsBadBlock(dev, block_lt) == U_TRUE) { node->u.list.block = block_lt; uffs_TreeInsertToBadBlockList(dev, node); uffs_Perror(UFFS_MSG_NORMAL, "found bad block %d", block_lt); } else if (uffs_IsPageErased(dev, bc, 0) == U_TRUE) { //@ read one spare: 0 // page 0 tag shows it's an erased block, we need to check the mini header status to make sure it is clean. if (uffs_LoadMiniHeader(dev, block_lt, 0, &header) == U_FAIL) { uffs_Perror(UFFS_MSG_SERIOUS, "I/O error when reading mini header !" "block %d page %d", block_lt, 0); ret = U_FAIL; break; } if (header.status != 0xFF) { // page 0 tag is clean but page data is dirty ??? // this block should be erased immediately ! uffs_FlashEraseBlock(dev, block_lt); } node->u.list.block = block_lt; if (HAVE_BADBLOCK(dev)) { uffs_Perror(UFFS_MSG_NORMAL, "New bad block (%d) discovered.", block_lt); uffs_BadBlockProcess(dev, node); } else { // page 0 is clean does not means all pages in this block are clean, // need to check this block later before use it. uffs_TreeInsertToErasedListTailEx(dev, node, 1); } } else { // this block have valid data page(s). ret = _ScanAndFixUnCleanPage(dev, bc); if (ret == U_FAIL) break; ret = _BuildValidTreeNode(dev, node, bc, &st); if (ret == U_FAIL) break; } uffs_BlockInfoPut(dev, bc); } //end of for if(ret == U_FAIL) uffs_BlockInfoPut(dev, bc); uffs_Perror(UFFS_MSG_NORMAL, "DIR %d, FILE %d, DATA %d", st.dir, st.file, st.data); return ret; }
static URET _BuildValidTreeNode(uffs_Device *dev, TreeNode *node, //!< empty node uffs_BlockInfo *bc, struct BlockTypeStatSt *st) { uffs_Tags *tag; TreeNode *node_alt; u16 block, parent, serial, block_alt, block_save; uffs_BlockInfo *bc_alt; u8 type; int page; UBOOL needToInsertToTree = U_FALSE; uffs_Buf *buf = NULL; uffs_FileInfo *info; u16 data_sum = 0; // check the first page on the block ... uffs_BlockInfoLoad(dev, bc, 0); tag = GET_TAG(bc, 0); //get first page's tag if (!TAG_IS_DIRTY(tag)) { // should never go here ... unless mark dirty page failed ? uffs_Perror(UFFS_MSG_NORMAL, "First page is clean in a non-erased block ?"); return U_FAIL; } if (!TAG_IS_VALID(tag)) { //first page is invalid ? should be erased now! uffs_Perror(UFFS_MSG_NORMAL, "first page in block %d is invalid, will be erased now!", bc->block); goto process_invalid_block; } block = bc->block; parent = TAG_PARENT(tag); serial = TAG_SERIAL(tag); type = TAG_TYPE(tag); // check if there is an 'alternative block' // (node which has the same serial number) in tree ? node_alt = uffs_FindFromTree(dev, type, parent, serial); if (node_alt != NULL) { //find a alternate node ! need to check the timestamp ! block_alt = _GetBlockFromNode(type, node_alt); uffs_Perror(UFFS_MSG_NORMAL, "Process unclean block (%d vs %d)", block, block_alt); if (block_alt == INVALID_UFFS_SERIAL) { uffs_Perror(UFFS_MSG_SERIOUS, "invalid block ?"); return U_FAIL; } bc_alt = uffs_BlockInfoGet(dev, block_alt); if (bc_alt == NULL) { uffs_Perror(UFFS_MSG_SERIOUS, "can't get block info "); return U_FAIL; } uffs_BlockInfoLoad(dev, bc_alt, 0); if (uffs_IsSrcNewerThanObj ( TAG_BLOCK_TS(tag), TAG_BLOCK_TS(GET_TAG(bc_alt, 0))) == U_TRUE) { //the node is newer than node_alt, so keep node_alt, and erase node uffs_FlashEraseBlock(dev, block); node->u.list.block = block; if (HAVE_BADBLOCK(dev)) uffs_BadBlockProcess(dev, node); else uffs_TreeInsertToErasedListTail(dev, node); uffs_BlockInfoPut(dev, bc_alt); //put back bc_alt before we return. return U_SUCC; } else { //the node is older than node_alt, so keep node, and erase node_alt //we use node as erased node to insert to erased list block_save = _GetBlockFromNode(type, node_alt); uffs_FlashEraseBlock(dev, block_save); node->u.list.block = block_save; if (HAVE_BADBLOCK(dev)) uffs_BadBlockProcess(dev, node); else uffs_TreeInsertToErasedListTail(dev, node); //put back bc_alt because we don't need it anymore. uffs_BlockInfoPut(dev, bc_alt); //use node_alt to store new informations in following node = node_alt; needToInsertToTree = U_FALSE; } } else { needToInsertToTree = U_TRUE; } if (type == UFFS_TYPE_DIR || type == UFFS_TYPE_FILE) { buf = uffs_BufClone(dev, NULL); if (buf == NULL) return U_FAIL; uffs_BlockInfoLoad(dev, bc, UFFS_ALL_PAGES); page = uffs_FindPageInBlockWithPageId(dev, bc, 0); if (page == UFFS_INVALID_PAGE) { uffs_BufFreeClone(dev, buf); uffs_Perror(UFFS_MSG_SERIOUS, "Can't find any valid page for page_id=0 ? invalid block !" "this might be caused by the tag layout change.\n"); goto process_invalid_block; } page = uffs_FindBestPageInBlock(dev, bc, page); uffs_FlashReadPage(dev, block, page, buf, U_FALSE); info = (uffs_FileInfo *) (buf->data); data_sum = uffs_MakeSum16(info->name, info->name_len); uffs_BufFreeClone(dev, buf); } switch (type) { case UFFS_TYPE_DIR: node->u.dir.block = bc->block; node->u.dir.checksum = data_sum; node->u.dir.parent = TAG_PARENT(tag); node->u.dir.serial = TAG_SERIAL(tag); st->dir++; break; case UFFS_TYPE_FILE: node->u.file.block = bc->block; node->u.file.checksum = data_sum; node->u.file.parent = TAG_PARENT(tag); node->u.file.serial = TAG_SERIAL(tag); node->u.file.len = uffs_GetBlockFileDataLength(dev, bc, UFFS_TYPE_FILE); st->file++; break; case UFFS_TYPE_DATA: node->u.data.block = bc->block; node->u.data.parent = TAG_PARENT(tag); node->u.data.serial = TAG_SERIAL(tag); node->u.data.len = uffs_GetBlockFileDataLength(dev, bc, UFFS_TYPE_DATA); st->data++; break; } if (needToInsertToTree == U_TRUE) { uffs_InsertNodeToTree(dev, type, node); } return U_SUCC; process_invalid_block: /* erase the invalid block */ uffs_FlashEraseBlock(dev, bc->block); node->u.list.block = bc->block; if (HAVE_BADBLOCK(dev)) uffs_BadBlockProcess(dev, node); else uffs_TreeInsertToErasedListTail(dev, node); return U_SUCC; }
/** * \brief recover bad block * \param[in] dev uffs device */ void uffs_BadBlockRecover(uffs_Device *dev) { TreeNode *good, *bad; uffs_Buf *buf; u16 i; u16 page; uffs_BlockInfo *bc = NULL; uffs_Tags *tag; uffs_Tags newTag; UBOOL succRecov; UBOOL goodBlockIsDirty = U_FALSE; int ret; int region; u8 type; if (dev->bad.block == UFFS_INVALID_BLOCK) return; // pick up an erased good block good = uffs_TreeGetErasedNode(dev); if (good == NULL) { uffs_Perror(UFFS_ERR_SERIOUS, "no free block to replace bad block!"); return; } //recover block bc = uffs_BlockInfoGet(dev, dev->bad.block); if (bc == NULL) { uffs_Perror(UFFS_ERR_SERIOUS, "can't get bad block info"); return; } succRecov = U_TRUE; for (i = 0; i < dev->attr->pages_per_block; i++) { page = uffs_FindPageInBlockWithPageId(dev, bc, i); if(page == UFFS_INVALID_PAGE) { break; //end of last valid page, normal break } page = uffs_FindBestPageInBlock(dev, bc, page); tag = GET_TAG(bc, page); buf = uffs_BufClone(dev, NULL); if (buf == NULL) { uffs_Perror(UFFS_ERR_SERIOUS, "Can't clone a new buf!"); succRecov = U_FALSE; break; } //NOTE: since this is a bad block, we can't guarantee the data is ECC ok, so just load data even ECC is not OK. ret = uffs_LoadPhyDataToBufEccUnCare(dev, buf, bc->block, page); if (ret == U_FAIL) { uffs_Perror(UFFS_ERR_SERIOUS, "I/O error ?"); uffs_BufFreeClone(dev, buf); succRecov = U_FALSE; break; } buf->data_len = TAG_DATA_LEN(tag); if (buf->data_len > dev->com.pg_data_size) { uffs_Perror(UFFS_ERR_NOISY, "data length over flow!!!"); buf->data_len = dev->com.pg_data_size; } buf->parent = TAG_PARENT(tag); buf->serial = TAG_SERIAL(tag); buf->type = TAG_TYPE(tag); buf->page_id = TAG_PAGE_ID(tag); newTag = *tag; TAG_BLOCK_TS(&newTag) = uffs_GetNextBlockTimeStamp(TAG_BLOCK_TS(tag)); ret = uffs_FlashWritePageCombine(dev, good->u.list.block, i, buf, &newTag); goodBlockIsDirty = U_TRUE; uffs_BufFreeClone(dev, buf); if (ret == UFFS_FLASH_IO_ERR) { uffs_Perror(UFFS_ERR_NORMAL, "I/O error ?"); succRecov = U_FALSE; break; } } if (succRecov == U_TRUE) { //successful recover bad block, so need to mark bad block, and replace with good one region = SEARCH_REGION_DIR|SEARCH_REGION_FILE|SEARCH_REGION_DATA; bad = uffs_TreeFindNodeByBlock(dev, dev->bad.block, ®ion); if (bad != NULL) { switch (region) { case SEARCH_REGION_DIR: bad->u.dir.block = good->u.list.block; type = UFFS_TYPE_DIR; break; case SEARCH_REGION_FILE: bad->u.file.block = good->u.list.block; type = UFFS_TYPE_FILE; break; case SEARCH_REGION_DATA: bad->u.data.block = good->u.list.block; type = UFFS_TYPE_DATA; } //from now, the 'bad' is actually good block :))) uffs_Perror(UFFS_ERR_NOISY, "new bad block %d found, and replaced by %d!", dev->bad.block, good->u.list.block); uffs_BlockInfoExpire(dev, bc, UFFS_ALL_PAGES); //we reuse the 'good' node as bad block node, and process the bad block. good->u.list.block = dev->bad.block; uffs_BadBlockProcess(dev, good); } else { uffs_Perror(UFFS_ERR_SERIOUS, "can't find the reported bad block(%d) in the tree???", dev->bad.block); if (goodBlockIsDirty == U_TRUE) dev->ops->EraseBlock(dev, good->u.list.block); uffs_TreeInsertToErasedListTail(dev, good); } } else { if (goodBlockIsDirty == U_TRUE) dev->ops->EraseBlock(dev, good->u.list.block); uffs_TreeInsertToErasedListTail(dev, good); //put back to erased list } uffs_BlockInfoPut(dev, bc); }