目录
解决DM368 UBL读取UBoot无法启动的问题
0. 问题描述
最近遇到了一个DM368突然无法启动的问题。
问题现象:开机后无法加载Uboot,卡死在UBL处,现象大概为在0x19处找到MagicNum后未正常进入系统,跳到0x1a找到第二个MagicNumber后卡死
1. 卡死原因分析
初步分析启动流程,卡死原因是UBL启动时加载了错误的UBoot。
1.1 UBL启动流程
UBL启动的流程如下(主要函数在Common\ubl\src\nandboot.c里的NANDBOOT_copy()中):
-
启动后,从UBoot分区读取每块的第一页,并检测第一页的前4个字节是否为Magic Number:0xA1ACED**
NAND_startAgain: if (blockNum > DEVICE_NAND_UBL_SEARCH_END_BLOCK) { return E_FAIL; // NAND boot failed and return fail to main } for (count = blockNum;count <= DEVICE_NAND_UBL_SEARCH_END_BLOCK;count++) { if (NAND_readPage(hNandInfo, count, 0,rxBuf) != E_PASS) { DEBUG_printString("NAND_readPage Fail at #"); DEBUG_printHexInt(count); DEBUG_printString(".\r\n"); continue; } magicNum = ((Uint32 *)rxBuf)[0]; /* Valid magic number found */ if ((magicNum & 0xFFFFFF00) == MAGIC_NUMBER_VALID) { blockNum = count; DEBUG_printString("Valid magicnum, "); DEBUG_printHexInt(magicNum); DEBUG_printString(", found in block "); DEBUG_printHexInt(blockNum); DEBUG_printString(".\r\n"); break; } } // Never found valid header in any page 0 of any of searched blocks if (count > DEVICE_NAND_UBL_SEARCH_END_BLOCK) { DEBUG_printString("No valid boot image found!\r\n"); return E_FAIL; }
-
读取到了Magic Number之后,会将后续的几个启动系统的数据进行保存,用于加载U-Boot:
gNandBoot.entryPoint = *(((Uint32 *)(&rxBuf[4])));/* 1.UBoot程序入口点 */ gNandBoot.numPage = *(((Uint32 *)(&rxBuf[8]))); /* 2.UBoot总共占用的页数 */ gNandBoot.block = *(((Uint32 *)(&rxBuf[12]))); /* 3.这份UBoot存在系统的哪一个Block */ gNandBoot.page = *(((Uint32 *)(&rxBuf[16]))); /* 4.这份UBoot在本块的起始页 */ gNandBoot.ldAddress = *(((Uint32 *)(&rxBuf[20]))); /* 5.Uboot在内存中加载到的地方 */
UBoot中使用nand dump命令,可以查到nand中这些数据的存储如下(以存放在第一分区的UBoot为例):
U-boot > nand dump 320000 //0x320000 = 0x19(Block) * 64 (Pages/Block) * 2048(Bytes/Page) Page 00320000 dump: 66 ed ac a1 00 00 08 81 9e 00 00 00 19 00 00 00 01 00 00 00 00 00 08 81 ff ff ff ff ff ff ff ff 这个CPU是Little-Endian的,所以需要字节倒序: Magic number = 66 ed ac a1 = 0xa1aced66,指示系统类型 entryPoint = 0x81080000,U-Boot会从这里启动 numPage = 0x0000009e,指示UBoot占用0x9e页 block = 0x00000019,说明这份UBoot从第0x19个Block开始读取 page = 0x00000001,即UBoot在这个Block的第一页 ldAddress = 0x81080000,UBoot将会被加载到这个地址
-
设置内存区域。所有调用UTIL_allocMem函数的内存分配都必须在这些语句执行之前完成。
if ((magicNum == UBL_MAGIC_BIN_IMG) || (magicNum == UBL_MAGIC_DMA)) //内存的分配操作需要在这一条语句之前结束…… { // Set the copy location to final run location rxBuf = (Uint8 *)gNandBoot.ldAddress; // Free temp memory rxBuf used to point to UTIL_setCurrMemPtr((void *)((Uint32) UTIL_getCurrMemPtr() - (APP_IMAGE_SIZE >> 1))); }
-
读取UBoot的数据复制到启动地址,如果读取出错则回到第一步查找下一个分区(Magic Number)
NAND_retry: /* initialize block and page number to be used for read */ block = gNandBoot.block; page = gNandBoot.page; // Perform the actual copying of the application from NAND to RAM for (i = 0; i < gNandBoot.numPage; i++) { // if page goes beyond max number of pages increment block number and reset page number if (page >= hNandInfo->pagesPerBlock) { page = 0; block++; } readError = NAND_readPage(hNandInfo, block, page++, (&rxBuf[i * (hNandInfo->dataBytesPerPage)])); /* Copy the data */ // We attempt to read the app data twice. If we fail twice then we go look for a new // application header in the NAND flash at the next block. if (readError != E_PASS) { if (failedOnceAlready) { blockNum++; DEBUG_printString("Start Again From Block:"); DEBUG_printHexInt(blockNum); DEBUG_printString("\r\n"); goto NAND_startAgain; //重试失败,查找下一个Magic Number } else { failedOnceAlready = TRUE; //每次读取失败,重试读取一次 DEBUG_printString("failed At Block:"); DEBUG_printHexInt(blockNum); DEBUG_printString(".Retry.\r\n"); goto NAND_retry; } } } // Application was read correctly, so set entrypoint gEntryPoint = gNandBoot.entryPoint; return E_PASS;
-
回到启动流程启动UBoot。
1.2. 启动异常点分析
从0中的问题描述可以看出,在读取0x19 Block里的UBoot时,检测到发生了ECC错误,导致无法读取本块UBoot,跳到下一块UBoot(0x1C)进行读取。
进入UBoot,读取Nand中位于0x1C的数据(地址为0x1C(Block) * 64(Page/Block) * 2048(Byte/Page) = 0x380000),发现第一页是正常的Magic Number,第二页本应该是UBoot的内容,却被写入了垃圾数据。
初步分析,问题应该是读取第一块UBoot失败后,读取了第二块区域的垃圾数据导致启动失败。
而由于检测数据有效性的手段仅有ECC,因此程序本身无法判断第二块区域是否为正常的UBoot数据,导致启动失败。
从上述分析中我们能够得出以下2个初步结论:
- 启动失败原因:读取第一块UBoot失败后,读取了第二块区域的垃圾数据。
- 垃圾数据的来源应该是烧写时烧写工具通过正常写入流程写入的,因此这些垃圾数据也有正常的ECC。
结合上述分析,可以初步判断368的烧写工具存在Bug,虽然做了多UBoot的备份逻辑,但是实际上只写入了1份UBoot。
2. 修改烧录工具SFT,正确写入多份UBoot
2.1 修改点
烧录工具分为SFH/SFT两部分。前者是电脑上运行的程序,后者是通过串口传输到手机上手机运行的程序。
烧录软件时,SFH会先将SFT通过串口烧录到板子上,板子接收到串口传输的SFT后启动烧录系统,再接收UBL和UBoot进行烧写。
结合上述分析流程可知,问题出在SFT写入的流程中。
分析sft代码(路径:flash-utils\Common\sft\src\uartboot.c),发现问题出在函数LOCAL_NANDWriteHeaderAndData()上。
该函数的流程是,根据UBoot的大小,将UBoot拷贝N份并存储在Flash中。然而,由于烧写工具存在Bug,重复写入时Source指针未复位,导致写入失败。
只需要加入下面两行代码,记录下原始数据srcBuf的首地址,在循环写入时恢复指针指向即可
2.2 测试
测试很简单,使用UBoot的nand命令破坏第一块UBoot数据之后,再重启即可。
进入UBoot,使用命令:
nand erase 0x320000 1
清除掉首地址为0x320000的Block
-
nand erase命令使用方法:
nand erase [addr] [len]
addr是擦除的首地址,由于只能按块擦除,因此首地址只能是每个block的首地址。即该地址需要按照0x20000(1Block*64(Pages/Block)*2048(Bytes/Page) = 0x20000Byte)对齐,否则会报错
len是擦除长度,一样按照0x20000向上对齐。例如len为1,也按照0x20000擦除整个Block
部分清除掉第一块UBoot后,系统正常从第二块UBoot启动,问题初步解决。
3. 循环自恢复
上面的方法只是创建了多个备份,能够在第一块UBoot数据出错后使用第二块UBoot。但是错误的UBoot数据不会自行恢复,因此应该在启动时加入对UBoot数据的恢复。
3.1 自恢复的逻辑
自恢复的逻辑如下:
3.2 自恢复功能的实现
3.2.1 初始化缓冲区
由于在程序中检测到MagicNumber之后,分配内存函数会无法正常使用:
if ((magicNum == UBL_MAGIC_BIN_IMG) || (magicNum == UBL_MAGIC_DMA)) //内存的分配操作需要在这一条语句之前结束……
{
rxBuf = (Uint8 *)gNandBoot.ldAddress;
UTIL_setCurrMemPtr((void *)((Uint32)UTIL_getCurrMemPtr() - (APP_IMAGE_SIZE >> 1)));
}
因此需要在这之前给我们的操作分配2Page的缓冲区。
Uint8 *pagebuf1 = NULL,*pagebuf2 = NULL;
Uint32 InitMyBuffer(NAND_InfoHandle hNandInfo)
{
pagebuf1 = (Uint8*)UTIL_allocMem(hNandInfo->dataBytesPerPage);
pagebuf2 = (Uint8*)UTIL_allocMem(hNandInfo->dataBytesPerPage);
if(pagebuf1 == NULL || pagebuf2 == NULL)
return E_FAIL;
return E_PASS;
}
3.2.2 页对页复制的实现
在恢复过程中,需要进行页对页的复制。逻辑很简单,就是从source读取并写入目的位置。
实现相关函数如下:
Uint32 NANDCopyPage2Page(NAND_InfoHandle hNandInfo, Uint32 srcblock, Uint32 srcpage,
Uint32 dstblock, Uint32 dstpage,
Uint32 copyBlockCnt, Uint32 copyPageCnt)
//传入参数:Nand句柄,源Block&page,目的Block&page,要复制的Block和页数
{
if (srcblock == dstblock && srcpage == dstpage) //参数错误
{
return 0x0BADADD0;
}
if (copyBlockCnt == 0 && copyPageCnt == 0)
{
return 0x0BADADD1;
}
Uint32 totalPage = copyBlockCnt *
(hNandInfo->pagesPerBlock) + copyPageCnt; //根据传入的Block和Page计算总共要复制的页数
Uint8 *pageBuf;
pageBuf = pagebuf2;
int i;
for (i = 0; i < totalPage; i++)
{
if (NAND_readPage(hNandInfo, srcblock, srcpage,
pageBuf) != E_PASS)
{
DEBUG_printString("Error Read Page At Blk ");
DEBUG_printHexInt(srcblock);
DEBUG_printString(" Page ");
DEBUG_printHexInt(srcpage);
DEBUG_printString("\r\n");
return E_FAIL;
}
if (NAND_writePage(hNandInfo, dstblock, dstpage,
pageBuf) != E_PASS)
{
DEBUG_printString("Error Write Page At Blk ");
DEBUG_printHexInt(dstblock);
DEBUG_printString(" Page ");
DEBUG_printHexInt(dstpage);
DEBUG_printString("Mark it as BadBlock.");
DEBUG_printString("\r\n");
NAND_badBlockMark(hNandInfo, dstblock);
return E_FAIL;
}
srcpage++;
if (srcpage >= hNandInfo->pagesPerBlock)
{
srcpage = 0;
srcblock++;
}
dstpage++;
if (dstpage >= hNandInfo->pagesPerBlock)
{
dstpage = 0;
dstblock++;
}
}
return E_PASS;
}
3.2.3 定位最后一块Block的位置和检测的实现
定位的方法即从当前Block开始往后找,找到最后一个MagicNumber所在的Block:
DEBUG_printString("Locate Last Block U-boot:\r\n");
Uint32 checkcnt, lastcnt, mymgc;
Uint32 rdpage, rdblk;
Uint8 needrecovery = 0;
for (checkcnt = blockNum;
checkcnt <= DEVICE_NAND_UBL_SEARCH_END_BLOCK;
checkcnt++)
{
if (NAND_readPage(hNandInfo, checkcnt, 0, pagebuf1) != E_PASS)
{
DEBUG_printString("NAND_readPage Fail at #");
DEBUG_printHexInt(checkcnt);
DEBUG_printString(".\r\n");
continue;
}
mymgc = ((Uint32 *)pagebuf1)[0];
if ((mymgc & 0xFFFFFF00) == MAGIC_NUMBER_VALID)
{
lastcnt = checkcnt;
}
}
DEBUG_printString("Last U-Boot is at Blk #");
DEBUG_printHexInt(lastcnt);
DEBUG_printString(".\r\n");
检测数据有效性的方式只能通过ECC进行检测,因此只需要读取所有数据并检测是否有ECC错误就好:
rdpage = 0; rdblk = lastcnt;
for (i = 0; i < gNandBoot.numPage; i++)
{
// if page goes beyond max number of pages increment block number and reset page number
if (rdpage >= hNandInfo->pagesPerBlock)
{
rdpage = 0;
rdblk++;
}
readError = NAND_readPage(hNandInfo, rdblk, rdpage++, pagebuf1); /* 读取数据 */
if (readError != E_PASS)
{
needrecovery = 1; //读取数据出错,说明ECC校验不通过
break;
}
}
3.2.4 检测非最后一块的UBoot的实现
在原有读取逻辑中增加变量lastFailedMagicNumBlock,记下最后一次失败的MagicNumber的块数:
NAND_retry:
/* initialize block and page number to be used for read */
block = gNandBoot.block;
page = gNandBoot.page;
// Perform the actual copying of the application from NAND to RAM
for (i = 0; i < gNandBoot.numPage; i++)
{
// if page goes beyond max number of pages increment block number and reset page number
if (page >= hNandInfo->pagesPerBlock)
{
page = 0;
block++;
}
readError = NAND_readPage(hNandInfo, block,
page++, (&rxBuf[i * (hNandInfo->dataBytesPerPage)])); /* Copy the data */
if (readError != E_PASS)
{
if (failedOnceAlready)
{
lastFailedMagicNumBlock = blockNum; //---记录下最后一次有MagicNumber的Block---
blockNum++;
DEBUG_printString("Start Again From Block:");
DEBUG_printHexInt(blockNum);
DEBUG_printString("\r\n");
goto NAND_startAgain;
}
else
{
failedOnceAlready = TRUE;
DEBUG_printString("failed At Block:");
DEBUG_printHexInt(blockNum);
DEBUG_printString(".Retry.\r\n");
goto NAND_retry;
}
}
}
之后判断该块数和正常启动的块数之间的差异是否为3Block大小(这里一个UBoot占3Block)
if(lastFailedMagicNumBlock !=0 && lastFailedMagicNumBlock - blockNum >=3 && isMyBufValid == TRUE)
{
//恢复数据...
}
3.2.5 恢复功能的实现逻辑
恢复数据需要分4步走:
- 备份第一页的Magic Number。
- 按块擦除Nand上的数据。
- 回写MagicNumber到第一页
- 从有效区域将UBoot内容回写到有问题的部分
以恢复最后一块UBoot为例:
if (needrecovery == 1)
{
//need recovery
DEBUG_printString("Last U-Boot Error\r\n");
Uint8 *MagicHeaderPage = pagebuf1; //只能按块擦除,因此需要备份第一个page的MagicHeader
Uint32 TotalBlock = 3;
NAND_readPage(hNandInfo, lastcnt, 0, MagicHeaderPage);
if (NAND_eraseBlocks(hNandInfo, lastcnt, TotalBlock) != E_PASS) //擦除有问题的3个Block
{
DEBUG_printString("Erase Error.\r\n");
}
DEBUG_printHexInt(TotalBlock);
DEBUG_printString(" Erase OK.Now Recovery Magic Num.\r\n");
if (NAND_writePage(hNandInfo, lastcnt, 0, MagicHeaderPage) != E_PASS) //恢复MagicNumber
{
DEBUG_printString("Write Magic Error.\r\n");
}
DEBUG_printString("Write Magic OK.\r\n");
NANDCopyPage2Page(hNandInfo, blockNum, 1, lastcnt, 1, 0, gNandBoot.numPage); //按页复制Uboot,恢复上个分区的数据
DEBUG_printString("Recovery Success.\r\n");
}
4. 总结
- malloc之类的代码一定要查看return value是否有效
- uboot下的nand命令对nand设备的读写擦除、mm/md/mw等内存读写命令等的使用要熟悉