学习uboot也有一段时间了,今天就对uboot做一个总结
简而言之,uboot是用来引导系统启动的。所以,uboot的功能首先是启动系统。但是,我们使用的
uboot功能除了启动系统,还可以进行很多操作:操作存储设备、烧写文件系统、烧写操作系统、操
作板子上的硬件设备等等。所以学习uboot的过程中就要按照uboot的功能分布进行。
1、首先是启动系统,那么,我们启动的Linux的要求是什么?
对于ARM答案是:>R0 =0
>R1= machine type number
>R2= physical address of tagged list in system RAM
>CPU must be in SVC mode
>All forms of interrupts must be disabled (IRQs and FIQs)
>The MMU must be off.
>Instruction cache may be on or off.
>Data cache must be off.
>The boot loader is expected to call the kernel image by jumping
directly to the first instruction of the kernel image.
那么我们只要是CPU满足上述这些条件不就行了么。
要满足上述三个条件,最难恐怕是启动参数了吧,启动参数为了是将板子的核心资源及其分布传
递给内核,我们就要了解这个启动参数包含哪些参数了。
我们先从启动过程来分析板子的初始化和这些启动条件的实现
uboot的启动过程分为stage1和stage2。前者使用汇编写的,短小精悍,实现CPU核心资源的初始化
并把自己拷贝到,RAM中,跳到第二阶段。后者使用C语言写的,实现开发板上资源的初始化。
stage1的入口点是start.s。为什么是这个文件是有原因的,代码经过汇编处理下一步就是链接,
链接后得到文件就是可执行文件。那么,只要链接时候把一段代码放到最前面,就可以使得这段代码
在机器上首先执行。uboot.lds文件,就是定义了uboot的代码分布。其中start.s放在首位,done
那么start.s做了写什么?
设置状态寄存器设置当前的CPU模式svc
关闭看门狗、中断、MMU、catche
初始化时钟
自拷贝到RAM
建立堆栈
清除bss段
跳转到start_armboot
start_armboot函数是用C语言写的,它是stage2的开始。
start_armboot实现了哪些功能?
分配全局数据区gd和板子数据区bd,其中gd和bd是两个结构体类型,第二阶段的任务主要是初始化gd
然后根据gd设置启动参数并引导内核启动。先来看看这两个结构体
typedef struct global_data {
bd_t *bd;
unsigned long flags;
unsigned long baudrate;
unsigned long have_console; /* serial_init() was called */
unsigned long reloc_off; /* Relocation Offset */
unsigned long env_addr; /* Address of Environment struct */
unsigned long env_valid; /* Checksum of Environment valid? */
unsigned long fb_base; /* base address of frame buffer */
#ifdef CONFIG_VFD
unsigned char vfd_type; /* display type */
#endif
#if 0
unsigned long cpu_clk; /* CPU clock in Hz! */
unsigned long bus_clk;
unsigned long ram_size; /* RAM size */
unsigned long reset_status; /* reset status register at boot */
#endif
void **jt; /* jump table */
} gd_t;
typedef struct bd_info {
int bi_baudrate; /* serial console baudrate */
unsigned long bi_ip_addr; /* IP Address */
unsigned char bi_enetaddr[6]; /* Ethernet adress */
struct environment_s *bi_env;
ulong bi_arch_number; /* unique id for this board */
ulong bi_boot_params; /* where this board expects params */
struct /* RAM configuration */
{
ulong start;
ulong size;
} bi_dram[CONFIG_NR_DRAM_BANKS];
} bd_t;
该结构定义了串口的波特率、IP地址、Ethnernet地址、环境结构体、arch number、启动参数、
RAM结构体数组(RAM的起始地址和大小)
那么可以看到DECLARE_GLOBAL_DATA_PTR; 就是声明了一个指针变量,并且它就在r8这个寄存器
里它所指向的地址就是存储开发板的一些参数设置的信息。
跟着代码跑吧:
1、一些列函数的执行,都是硬件初始化
cpu_init, /* basic cpu dependent setup */
board_init, /* basic board dependent setup */
interrupt_init, /* set up exceptions */
env_init, /* initialize environment */
init_baudrate, /* initialze baudrate settings */
serial_init, /* serial communications setup */
console_init_f, /* stage 1 init of console */
display_banner, /* say that we are here */
dram_init, /* configure available RAM banks */
display_dram_config,
2、flash初始化
size = flash_init ();
:
;
:
:
main_loop
这个函数主要是实现
2、存储设备的操作
存储常见的有flash、ram、rom、eeprom它们在Linux嵌入式设备中的的常用功能
>flash:分为nor和nand,主要存储bootloader、kernel、启动参数、文件系统
>ram:就是内存
>rom:
>eeprom:
1.1 首先,学习一下nand flash,我个人觉得nand flash的学习是学习uboot的重中之重,nand
flash作为存储设备广泛用于嵌入式设备,uboot也支持对它的读写等操作,此外,我们要使用
的操作系统大多也烧写在nand中,由于uboot本身就支持对一些操作系统的烧写,如yaffs/yaff2
jffs2等。其重要性不言而喻。
/---------------------------------/
<此处介绍一下nand,待补充>
/---------------------------------/
对于nand的学习,uboot中的突破口有两个,一个是nand_init函数,此在分析uboot的启动过程中
会遇到,它主要是实现
/---------------------------------/
<此处介绍一下nand——init,待补充>
/---------------------------------/
另外一个是cmd_nand.c文件,此文件是实现uboot中关于nand操作的一些函数的,还是好好学习一下吧
我来介绍一下这个文件中函数的调用流程,首先要有一个层次感,这些层次本不存在,只是为了便于理解
从上到下第一层 do_nand 实现命令层,主要实现命令的解析,根据解析结果调用下一层
第二层 nand_rw 根据传递来的命令参数判断调用具体的读或者写
第三层 nand_write_ecc 具体的读写函数,多页读写,调用单独页的读写函数
第四层 nand_write_page 具体的读写函数,单页读写,根据规则调用一些宏命令来实现
第五层 NandCommand、NandAddr 用来实现发送命令或者地址
如果有第六层那就是对于不同的nand的具体的操作命令了,它们对上层的接口一定要统一,下面来
看看这个文件的具体调用
do_nand ----调用--> nand_rw|----调用--> nand_read_ecc ----调用-->
|----调用--> nand_write_ecc ----调用-->nand_write_page
do_nand的主要功能:实现nand命令
主要实现:根据命令参数argc,通过一个switch语句分别判断nand的具体命令,特别注意的
是读写命令调用了nand_rw。
nand_rw的主要功能:根据命令参数读写flash
主要实现:1、坏块检测,如果出现坏块,根据命令做出相应处理
a>如果NANDRW_READ | NANDRW_JFFS2:向buf中写入start到块结束或len长度的0xff
b>如果NANDRW_READ | NANDRW_JFFS2 | NANDRW_JFFS2_SKIP
或NANDRW_WRITE | NANDRW_JFFS2 ,跳过一个块的大小
2、根据命令判读,以最大一个块的大小读写nand,这里调用的读写函数nand_read_ecc
和nand_write_ecc。
nand_read_ecc的主要功能:从flash中读出数据,支持ECC,如果定义了MTD就支持ECC,否则就是一般读取
主要实现:MTD的读:
1、将数据读到databuf
2、将databuf拷贝到datacatche
3、从databuf中获得ecc
4、计算ecc判断,如果valid则直接跳到readdata,否则纠正databuf中的数据
5、将纠正后的数据考到datacatche
6、read data
普通读就是在读数据时不读取ECC,不进行有效性验证,注释很详细
注意:由于读取的操作的最大单位是block,在读取时是同while(retlen<len)循环来分块读取的
nand_write_ecc的主要功能:写flash函数,同时写入了ECC,调用了nand_write_page
主要实现:就是在循环里调用nand_write_page
nand_write_page的主要功能:这个函数是把nand这个参数的nand->data_buf中从col到last的数据写入flash中
static int nand_write_page (struct nand_chip *nand,
int page, int col, int last, u_char * ecc_code)
其中,col是nand->data_buf中要写入flash的开始地址,last是nand->data_buf
中要写入flash的结束地址
主要实现: 1、清空obb
2、如果定义了MTD_NAND_ECC,就进行ECC校验并把ECC值写入相应地址,eccvalid_pos对应地址的值0x0f
3、向databuf 中0~col和last~oobblock写入0xff并进行program
4、如果定义了读确认,读出数据并与原数据比较,如果不同则打印信息
5、如果定义了MTD_NAND_ECC,读出ECC并与原数据比较
备注:eccvalid_pos应该是表示在oob区分配一个字节用来表示ECC是否valid,如果没有这个验证功能
eccvalid_pos=-1.应该是这样的。
1.2 nor flash
nor flash也是嵌入式设备中常用的存储设备,nor 与 nand 相比,我觉得最大的区别是访问方式。nor 有独立的数据线和地址线,
而nand使用通用的输入输出接口线。此外,nor可以片上执行,这就使得nor更适合存放bootloader最开始的代码了。所以,nor
flash也是要好好分析的。
<----------------------对norflash做介绍----------------------->
在uboot中涉及到nor flash的地方有两个,一个是启动流程中的flash_init ,另一处是cmd_flash.c文件
我们首先对flash_init实现的功能做简要介绍:
说白了这个init函数主要实现对 flash_info_t flash_info[CFG_MAX_FLASH_BANKS],这个数组的初始化,flash_info_t的定义如下
typedef struct {
ulong size; /* total bank size in bytes */
ushort sector_count; /* number of erase units */
ulong flash_id; /* combined device & manufacturer code */
ulong start[CFG_MAX_FLASH_SECT]; /* physical sector start addresses */ 每个物理块的开始地址
uchar protect[CFG_MAX_FLASH_SECT]; /* sector protection status */ 每个物理块保护状态
#ifdef CFG_FLASH_CFI
uchar portwidth; /* the width of the port */
uchar chipwidth; /* the width of the chip */
ushort buffer_size; /* # of bytes in write buffer */
ulong erase_blk_tout; /* maximum block erase timeout */
ulong write_tout; /* maximum write timeout */
ulong buffer_write_tout; /* maximum buffer write timeout */
ushort vendor; /* the primary vendor id */
ushort cmd_reset; /* Vendor specific reset command */
ushort interface; /* used for x8/x16 adjustments */
#endif
} flash_info_t
确实,在这个函数中依次初始化了flash_id、size、sector_count、start、并对uboot和ENV写保护,上面几个元素都对上了。
flash_init里面调用了flash_protect函数里面有两个算法用得挺妙的,我也做个记录.
<----------------------对flash_pritect做介绍----------------------->
除了初始化函数顺便把对nor flash操作的实现也一并做个了结:
nor擦除函数
int flash_erase (flash_info_t * info, int s_first, int s_last)
1、首先是检查要擦除flash的信息检验一下ID,没有ID说明初始化的时候就失败了何谈擦除,判断擦除区域是否超出范围
2、判断是否有sector被保护,只要擦除区域中有一个sector被保护,该擦除不得进行
3、关闭中断和catche,这个我还没弄懂,虽然上面有注释。
4、连续几个周期向特定的地址发送特定的命令。flash的命令就是分几个周期向特定地址发送不同的命令
5、下面判断结束状态,打开关闭的中断和catche
nor写操作
volatile static int write_hword (flash_info_t * info, ulong dest, ushort data)
1、关中断和catche
2、检查flash有没有被擦除过,没有擦除的flash是不可以写数据的。
3、写入命令和数据并设置计时器等待结束,计时器是为了防止超时
当然,nor的读操作是非常非常简单的,就跟内存一样,不然怎能片上执行。
这里还要介绍一下nor与CPU的链接问题
nor flash的数据访问有byte、half word、word三种方式。
1、byte的方式很简单不用介绍
2、half word的接线方式是,arm的A1接flash的A0,这是因为ARM是以byte编址的,而flash是以 half word
编址的,所以地址的对应关系
ARM flash
0x0 0x0
0x1 0x0
0x2 0x1
0x3 0x1
这个关系是addr(ARM)>> 1 == addr(flash),地址线的错位正好解决了这一点。
#define SysAddr16(sysbase, offset) ((volatile U16*)(sysbase)+(offset))
这样定义了访问flash的宏,注意offset的偏移量是16位的。所以offset就是flsah中的地址了
但是从ARM的角度看偏移量就是 2*offset
3、word的访问方式和half word 的相同,就是错两位。
///write最大只能写一个页一次 ,ECC对于每次写入数据时,先计算出ECC的值,并写入对应区域,
然后在eccvalid_pos处写入0x0f,读出数据时,要读出ECC,并检查eccvalid_pos处的值是否为0x0f
如果不是,根据读出ECC和计算得到的ECC对数据进行纠正
简而言之,uboot是用来引导系统启动的。所以,uboot的功能首先是启动系统。但是,我们使用的
uboot功能除了启动系统,还可以进行很多操作:操作存储设备、烧写文件系统、烧写操作系统、操
作板子上的硬件设备等等。所以学习uboot的过程中就要按照uboot的功能分布进行。
1、首先是启动系统,那么,我们启动的Linux的要求是什么?
对于ARM答案是:>R0 =0
>R1= machine type number
>R2= physical address of tagged list in system RAM
>CPU must be in SVC mode
>All forms of interrupts must be disabled (IRQs and FIQs)
>The MMU must be off.
>Instruction cache may be on or off.
>Data cache must be off.
>The boot loader is expected to call the kernel image by jumping
directly to the first instruction of the kernel image.
那么我们只要是CPU满足上述这些条件不就行了么。
要满足上述三个条件,最难恐怕是启动参数了吧,启动参数为了是将板子的核心资源及其分布传
递给内核,我们就要了解这个启动参数包含哪些参数了。
我们先从启动过程来分析板子的初始化和这些启动条件的实现
uboot的启动过程分为stage1和stage2。前者使用汇编写的,短小精悍,实现CPU核心资源的初始化
并把自己拷贝到,RAM中,跳到第二阶段。后者使用C语言写的,实现开发板上资源的初始化。
stage1的入口点是start.s。为什么是这个文件是有原因的,代码经过汇编处理下一步就是链接,
链接后得到文件就是可执行文件。那么,只要链接时候把一段代码放到最前面,就可以使得这段代码
在机器上首先执行。uboot.lds文件,就是定义了uboot的代码分布。其中start.s放在首位,done
那么start.s做了写什么?
设置状态寄存器设置当前的CPU模式svc
关闭看门狗、中断、MMU、catche
初始化时钟
自拷贝到RAM
建立堆栈
清除bss段
跳转到start_armboot
start_armboot函数是用C语言写的,它是stage2的开始。
start_armboot实现了哪些功能?
分配全局数据区gd和板子数据区bd,其中gd和bd是两个结构体类型,第二阶段的任务主要是初始化gd
然后根据gd设置启动参数并引导内核启动。先来看看这两个结构体
typedef struct global_data {
bd_t *bd;
unsigned long flags;
unsigned long baudrate;
unsigned long have_console; /* serial_init() was called */
unsigned long reloc_off; /* Relocation Offset */
unsigned long env_addr; /* Address of Environment struct */
unsigned long env_valid; /* Checksum of Environment valid? */
unsigned long fb_base; /* base address of frame buffer */
#ifdef CONFIG_VFD
unsigned char vfd_type; /* display type */
#endif
#if 0
unsigned long cpu_clk; /* CPU clock in Hz! */
unsigned long bus_clk;
unsigned long ram_size; /* RAM size */
unsigned long reset_status; /* reset status register at boot */
#endif
void **jt; /* jump table */
} gd_t;
typedef struct bd_info {
int bi_baudrate; /* serial console baudrate */
unsigned long bi_ip_addr; /* IP Address */
unsigned char bi_enetaddr[6]; /* Ethernet adress */
struct environment_s *bi_env;
ulong bi_arch_number; /* unique id for this board */
ulong bi_boot_params; /* where this board expects params */
struct /* RAM configuration */
{
ulong start;
ulong size;
} bi_dram[CONFIG_NR_DRAM_BANKS];
} bd_t;
该结构定义了串口的波特率、IP地址、Ethnernet地址、环境结构体、arch number、启动参数、
RAM结构体数组(RAM的起始地址和大小)
那么可以看到DECLARE_GLOBAL_DATA_PTR; 就是声明了一个指针变量,并且它就在r8这个寄存器
里它所指向的地址就是存储开发板的一些参数设置的信息。
跟着代码跑吧:
1、一些列函数的执行,都是硬件初始化
cpu_init, /* basic cpu dependent setup */
board_init, /* basic board dependent setup */
interrupt_init, /* set up exceptions */
env_init, /* initialize environment */
init_baudrate, /* initialze baudrate settings */
serial_init, /* serial communications setup */
console_init_f, /* stage 1 init of console */
display_banner, /* say that we are here */
dram_init, /* configure available RAM banks */
display_dram_config,
2、flash初始化
size = flash_init ();
:
;
:
:
main_loop
这个函数主要是实现
2、存储设备的操作
存储常见的有flash、ram、rom、eeprom它们在Linux嵌入式设备中的的常用功能
>flash:分为nor和nand,主要存储bootloader、kernel、启动参数、文件系统
>ram:就是内存
>rom:
>eeprom:
1.1 首先,学习一下nand flash,我个人觉得nand flash的学习是学习uboot的重中之重,nand
flash作为存储设备广泛用于嵌入式设备,uboot也支持对它的读写等操作,此外,我们要使用
的操作系统大多也烧写在nand中,由于uboot本身就支持对一些操作系统的烧写,如yaffs/yaff2
jffs2等。其重要性不言而喻。
/---------------------------------/
<此处介绍一下nand,待补充>
/---------------------------------/
对于nand的学习,uboot中的突破口有两个,一个是nand_init函数,此在分析uboot的启动过程中
会遇到,它主要是实现
/---------------------------------/
<此处介绍一下nand——init,待补充>
/---------------------------------/
另外一个是cmd_nand.c文件,此文件是实现uboot中关于nand操作的一些函数的,还是好好学习一下吧
我来介绍一下这个文件中函数的调用流程,首先要有一个层次感,这些层次本不存在,只是为了便于理解
从上到下第一层 do_nand 实现命令层,主要实现命令的解析,根据解析结果调用下一层
第二层 nand_rw 根据传递来的命令参数判断调用具体的读或者写
第三层 nand_write_ecc 具体的读写函数,多页读写,调用单独页的读写函数
第四层 nand_write_page 具体的读写函数,单页读写,根据规则调用一些宏命令来实现
第五层 NandCommand、NandAddr 用来实现发送命令或者地址
如果有第六层那就是对于不同的nand的具体的操作命令了,它们对上层的接口一定要统一,下面来
看看这个文件的具体调用
do_nand ----调用--> nand_rw|----调用--> nand_read_ecc ----调用-->
|----调用--> nand_write_ecc ----调用-->nand_write_page
do_nand的主要功能:实现nand命令
主要实现:根据命令参数argc,通过一个switch语句分别判断nand的具体命令,特别注意的
是读写命令调用了nand_rw。
nand_rw的主要功能:根据命令参数读写flash
主要实现:1、坏块检测,如果出现坏块,根据命令做出相应处理
a>如果NANDRW_READ | NANDRW_JFFS2:向buf中写入start到块结束或len长度的0xff
b>如果NANDRW_READ | NANDRW_JFFS2 | NANDRW_JFFS2_SKIP
或NANDRW_WRITE | NANDRW_JFFS2 ,跳过一个块的大小
2、根据命令判读,以最大一个块的大小读写nand,这里调用的读写函数nand_read_ecc
和nand_write_ecc。
nand_read_ecc的主要功能:从flash中读出数据,支持ECC,如果定义了MTD就支持ECC,否则就是一般读取
主要实现:MTD的读:
1、将数据读到databuf
2、将databuf拷贝到datacatche
3、从databuf中获得ecc
4、计算ecc判断,如果valid则直接跳到readdata,否则纠正databuf中的数据
5、将纠正后的数据考到datacatche
6、read data
普通读就是在读数据时不读取ECC,不进行有效性验证,注释很详细
注意:由于读取的操作的最大单位是block,在读取时是同while(retlen<len)循环来分块读取的
nand_write_ecc的主要功能:写flash函数,同时写入了ECC,调用了nand_write_page
主要实现:就是在循环里调用nand_write_page
nand_write_page的主要功能:这个函数是把nand这个参数的nand->data_buf中从col到last的数据写入flash中
static int nand_write_page (struct nand_chip *nand,
int page, int col, int last, u_char * ecc_code)
其中,col是nand->data_buf中要写入flash的开始地址,last是nand->data_buf
中要写入flash的结束地址
主要实现: 1、清空obb
2、如果定义了MTD_NAND_ECC,就进行ECC校验并把ECC值写入相应地址,eccvalid_pos对应地址的值0x0f
3、向databuf 中0~col和last~oobblock写入0xff并进行program
4、如果定义了读确认,读出数据并与原数据比较,如果不同则打印信息
5、如果定义了MTD_NAND_ECC,读出ECC并与原数据比较
备注:eccvalid_pos应该是表示在oob区分配一个字节用来表示ECC是否valid,如果没有这个验证功能
eccvalid_pos=-1.应该是这样的。
1.2 nor flash
nor flash也是嵌入式设备中常用的存储设备,nor 与 nand 相比,我觉得最大的区别是访问方式。nor 有独立的数据线和地址线,
而nand使用通用的输入输出接口线。此外,nor可以片上执行,这就使得nor更适合存放bootloader最开始的代码了。所以,nor
flash也是要好好分析的。
<----------------------对norflash做介绍----------------------->
在uboot中涉及到nor flash的地方有两个,一个是启动流程中的flash_init ,另一处是cmd_flash.c文件
我们首先对flash_init实现的功能做简要介绍:
说白了这个init函数主要实现对 flash_info_t flash_info[CFG_MAX_FLASH_BANKS],这个数组的初始化,flash_info_t的定义如下
typedef struct {
ulong size; /* total bank size in bytes */
ushort sector_count; /* number of erase units */
ulong flash_id; /* combined device & manufacturer code */
ulong start[CFG_MAX_FLASH_SECT]; /* physical sector start addresses */ 每个物理块的开始地址
uchar protect[CFG_MAX_FLASH_SECT]; /* sector protection status */ 每个物理块保护状态
#ifdef CFG_FLASH_CFI
uchar portwidth; /* the width of the port */
uchar chipwidth; /* the width of the chip */
ushort buffer_size; /* # of bytes in write buffer */
ulong erase_blk_tout; /* maximum block erase timeout */
ulong write_tout; /* maximum write timeout */
ulong buffer_write_tout; /* maximum buffer write timeout */
ushort vendor; /* the primary vendor id */
ushort cmd_reset; /* Vendor specific reset command */
ushort interface; /* used for x8/x16 adjustments */
#endif
} flash_info_t
确实,在这个函数中依次初始化了flash_id、size、sector_count、start、并对uboot和ENV写保护,上面几个元素都对上了。
flash_init里面调用了flash_protect函数里面有两个算法用得挺妙的,我也做个记录.
<----------------------对flash_pritect做介绍----------------------->
除了初始化函数顺便把对nor flash操作的实现也一并做个了结:
nor擦除函数
int flash_erase (flash_info_t * info, int s_first, int s_last)
1、首先是检查要擦除flash的信息检验一下ID,没有ID说明初始化的时候就失败了何谈擦除,判断擦除区域是否超出范围
2、判断是否有sector被保护,只要擦除区域中有一个sector被保护,该擦除不得进行
3、关闭中断和catche,这个我还没弄懂,虽然上面有注释。
4、连续几个周期向特定的地址发送特定的命令。flash的命令就是分几个周期向特定地址发送不同的命令
5、下面判断结束状态,打开关闭的中断和catche
nor写操作
volatile static int write_hword (flash_info_t * info, ulong dest, ushort data)
1、关中断和catche
2、检查flash有没有被擦除过,没有擦除的flash是不可以写数据的。
3、写入命令和数据并设置计时器等待结束,计时器是为了防止超时
当然,nor的读操作是非常非常简单的,就跟内存一样,不然怎能片上执行。
这里还要介绍一下nor与CPU的链接问题
nor flash的数据访问有byte、half word、word三种方式。
1、byte的方式很简单不用介绍
2、half word的接线方式是,arm的A1接flash的A0,这是因为ARM是以byte编址的,而flash是以 half word
编址的,所以地址的对应关系
ARM flash
0x0 0x0
0x1 0x0
0x2 0x1
0x3 0x1
这个关系是addr(ARM)>> 1 == addr(flash),地址线的错位正好解决了这一点。
#define SysAddr16(sysbase, offset) ((volatile U16*)(sysbase)+(offset))
这样定义了访问flash的宏,注意offset的偏移量是16位的。所以offset就是flsah中的地址了
但是从ARM的角度看偏移量就是 2*offset
3、word的访问方式和half word 的相同,就是错两位。
///write最大只能写一个页一次 ,ECC对于每次写入数据时,先计算出ECC的值,并写入对应区域,
然后在eccvalid_pos处写入0x0f,读出数据时,要读出ECC,并检查eccvalid_pos处的值是否为0x0f
如果不是,根据读出ECC和计算得到的ECC对数据进行纠正