分享下之前移植ThreadX+FileX+LevelX移植到stm32f407+nandflash的经验
板子的话,用的是我之前自己布板焊接的。
我这里使用的是标准库移植的。
先放上ThreadX官方的文档介绍,都是中文的很好懂。
链接: Azure RTOS ThreadX 文档.
通过上面那个链接可以进入到ThreadX全家桶的所有文档里

移植前的准备工作

首先要先移植ThreadX,移植完后,移植levelX,通过LevelX对接到FileX里。

ThreadX移植

这里移植ThreadX参考硬汉哥的移植流程,下面放个链接
链接: ThreadX移植教程.

LevelX移植

LevelX实际上是一个nftl层,是我们的存储介质到文件系统中的中间层,用来实现nandflash的磨损均衡,掉电保护,坏块管理的功能。

源码下载

首先,我们要到这个链接下: azure-rtos/levelx.下载到LevelX的源码。解压到我们要移植的工程里面。

stm32 移植 freemodbus modbustcp stm32 移植 FileX_i++


由于分享的是以前的移植过程,使用的是以前的版本,实际上官网上已经更新到了6.18。

进入到filex的文件目录,比较重要的是common和samples两个文件夹,cmomon里有src和inc两个文件夹,其中src里是.c文件,inc里是.h文件。

samples是微软提供的一些例程文件。

stm32 移植 freemodbus modbustcp stm32 移植 FileX_sed_02

源码添加到工程

这里由于我们使用的nandflash,所以只需要添加nandflash的API到我们的工程里。如下图所示

stm32 移植 freemodbus modbustcp stm32 移植 FileX_i++_03

补充接口文件

参考LevelX官方说明文档,得知我们需要补充结构体LX_NAND_FLASH的以下几个函数和参数

我们可以新建一个文件,在这个文件里实现以下部分的功能

stm32 移植 freemodbus modbustcp stm32 移植 FileX_数据_04


其中

这三个是参数

lx_nand_flash_total_blocks是nandflash的总块数

lx_nand_flash_pages_per_block是每块包含的页数

lx_nand_flash_bytes_per_page是每页有多少字节

以下10个是函数指针,

lx_nand_flash_driver_read是扇区读函数

lx_nand_flash_driver_write是扇区写函数

lx_nand_flash_driver_block_erase是块擦除函数

lx_nand_flash_driver_block_erased_verify是块擦除验证函数

lx_nand_flash_driver_page_erased_verify是页擦除验证函数

lx_nand_flash_driver_block_status_get块状态获取函数

lx_nand_flash_driver_block_status_set块状态设置函数

lx_nand_flash_driver_extra_bytes_get额外字节获取函数

lx_nand_flash_driver_extra_bytes_set额外字节设置函数

lx_nand_flash_driver_system_error出错时会调用的函数,相当于错误通知

下面这个是存放LevelX扇区缓冲区的指针

lx_nand_flash_page_buffer

首先,根据我用的Nandflash来补充以下参数

#define NAND_PAGE_SIZE             ((uint16_t)0x0800) /* 定义一个页有多少个字节,这里是2048字节 */

#define TOTAL_BLOCKS                        4096 /*定义nandflash的总块数,我用的是有4096块,两个plane*/
#define PHYSICAL_PAGES_PER_BLOCK            64 /* 每个块包含的页的数量,我这里一块有64个页                                              */
#define BYTES_PER_PHYSICAL_PAGE             2048        /* 定义每页包含的字节数量                                         */
#define WORDS_PER_PHYSICAL_PAGE             2048/4      /* 每页包含的字节数量,按words算就是2048/4                                               */
#define SPARE_BYTES_PER_PAGE                64          /* 每个页备用区的数量,2048字节的页大小对应的备用区一般都是64字节                                    */
                                                        /* For 2048 byte block spare area:                              */
//下边这几个是LevelX定义的备用区的保存位置,这个我们不用改,后面补充接口函数时直接用就行
#define BAD_BLOCK_POSITION                  0           /*      0 is the bad block byte postion                         */
#define EXTRA_BYTE_POSITION                 2           /*      2 is the extra bytes starting byte postion              */
#define ECC_BYTE_POSITION                   40          /*      40 is the ECC starting byte position   */
补充扇区读函数

它的函数原型是:

UINT (*lx_nand_flash_driver_read)(ULONG block, ULONG page, ULONG *destination, ULONG words);

这个函数负责读取nandflash中的特定界面,里面要实现ECC错误检查和错误校正。其中输入参数
block:要读第几块
page:要读第几页
destination:读出的数据的存放地址
words:读的个数(这里是以word为单位的,也就是读words*4个字节)
下面放一下我的具体实现

UINT  nand_driver_read_page(ULONG block, ULONG page, ULONG *destination, ULONG words)
{
	u32 PageNum=0,i=0,n=0;
	u8 nand_err=0;
	u8 ecc_num=0;
	UINT lx_err=0;
	
	PageNum=(block<<6)|(page);
//	printf("Read: block=%d,page=%d,pagenum=%d,words=%d,err=%d\r\n",block,page,PageNum,words,nand_err);
	for(i=0;i<TIME_OUT;i++)
	{
		nand_err=NAND_ReadPage(PageNum,0,(u8*)destination,words*4);
		if(nand_err==0)
		{
			break;
		}
		printf("read page error,%d\r\n",nand_err);
		tx_thread_sleep(50);
	}
//	for(u16 i=0;i<words;i++)
//	{
//		printf("%08X+%d,",destination[i],i);
//	}
//	printf("\r\n");
	
	if(nand_err!=0)
	{
		return (LX_ERROR);
	}
	/******ECC校验*******/
	if(page==0)
	{
		for(i=0;i<words;i++)
		{
			if(destination[i]==0xFFFFFFFF)
			{
				n++;
			}else
			{
				break;
			}
		}
	}
	
	if(n==words||page==0)//全为0xFFFFFFFF不需要进行ECC校验
	{
		return LX_SUCCESS;
	}
	ecc_num=(words*4)/256;
	memset(&nand_dev.ecc_buf[0],0xFF,3*ecc_num);
	nand_err=NAND_ReadSpare(PageNum,ECC_BYTE_POSITION,&nand_dev.ecc_buf[0],3*ecc_num);
	if(nand_err!=0)
	{
		printf("read ecc error,%d\r\n",nand_err);
		return (LX_ERROR);
	}
	for(i=0;i<ecc_num;i++)
	{
		lx_err=_lx_nand_flash_256byte_ecc_check((u8*)destination,&nand_dev.ecc_buf[i*3]);
		if(lx_err!=0&&lx_err!=6)
		{
			printf("ECC验证出错,%d\r\n",lx_err);
			return (LX_ERROR);
		}
	}
	return LX_SUCCESS;
}

上边TIME_OUT是我自己定义的一个超时设定,主要是为了防止读数据时偶发性的超时,做一个二次读确认。
说下ECC的具体实现思路,这里直接调用了LevelX自带的ECC校验函数,以256字节为单位,每256字节生成3个ECC校验值。每次写数据时,计算写入数据的ECC值,并将他存储到每页中spare的ECC_BYTE_POSITION的偏移地址处,2048字节的话就是要存储24字节的ECC值。这里可以看出LevelX预留的是刚好够的64-40刚好等于24字节。

而读数据时,先将数据读出来,再将我们先前存入的ECC值读出来,对读出的数据进行ECC校验,并将结果与读出的ECC值做对比,如果不一致的话,尝试修复一下数据,LevelX提供的ECC函数每256字节是可以修正1b的错误的。
以上就是ECC的大体实现思路。
再说下,移植过程中发现的小问题,算是LevelX的一个小Bug吧,影响不是很大。在LevelX的API函数lx_nand_flash_open.c的286和296行会对nandflash每个块的第0页在不擦除的情况下进行两次读写。由于每次写入数据均大于256字节,所以要对这两次都进行ECC校验,但是Nandflash的特性是每次写入之前都要先擦除,也就是Nandflash的写入操作只能从1->0。这样就导致我们第二次校验的ECC值大概率是错误的。我当时为了这个问题临时是使用了把第0页的ECC校验屏蔽了。
这个问题之前移植时已经向官方反馈了,前几天他们工作人员发邮件给我,说最近会解决这个问题。期待一下。

补充扇区写函数

函数原型:

UINT (*lx_nand_flash_driver_write)(ULONG block, ULONG page, ULONG *source, ULONG words);

参数与读函数一致。这个函数负责将数据写入Nandflash里。同样也需要实现ECC的相关功能
直接放具体的实现把:

UINT  nand_driver_write_page(ULONG block, ULONG page, ULONG *source, ULONG words)
{
	u32 PageNum=0;
	u8 nand_err=0;
	u8 ecc_num=0,i=0;
//	u8 ecc_buf[24]={0xFF};
	
	PageNum=(block<<6)|(page);
//	printf("Write: block=%d,page=%d,pagenum=%d,words=%d\r\n",block,page,PageNum,words);
	for(i=0;i<TIME_OUT;i++)
	{
		nand_err=NAND_WritePage(PageNum,0,(u8*)source,words*4);
		if(nand_err==0)
		{
			break;
		}
		printf("wirte page error,%d\r\n",nand_err);
		tx_thread_sleep(50);
	}
//	for(u16 i=0;i<words;i++)
//	{
//		printf("%08X+%d,",source[i],i);
//	}
//	printf("\r\n");

	
	if(nand_err!=0)
	{
		return (LX_ERROR);
	}
//	if((page==0)&&(source[0]&LX_BLOCK_ERASED)==LX_BLOCK_ERASED)
//	{
//		return LX_SUCCESS;
//	
//	}
	ecc_num=(words*4)/256;
	memset(&nand_dev.ecc_buf[0],0xFF,3*ecc_num);
	for(i=0;i<ecc_num;i++)
	{
		_lx_nand_flash_256byte_ecc_compute((u8*)source,&nand_dev.ecc_buf[i*3]);
	}
	nand_err=NAND_WriteSpare(PageNum,ECC_BYTE_POSITION,&nand_dev.ecc_buf[0],3*ecc_num);
	if(nand_err!=0)
	{
		printf("wirte ecc error,%d\r\n",nand_err);
		return (LX_ERROR);
	}
//	memset(ecc_buf,0xFF,3*ecc_num);
//	nand_err=NAND_ReadSpare(PageNum,ECC_BYTE_POSITION,ecc_buf,3*ecc_num);
	
	return LX_SUCCESS;
}
补充块擦除函数

函数原型:

UINT (*lx_nand_flash_driver_block_erase)(ULONG block, ULONG erase_count);

erase_count是擦除的数量,这里我直接调用了之前已经整理好的块擦除函数。

UINT  nand_driver_block_erase(ULONG block, ULONG erase_count)
{
	u8 nand_err=0;
	
	LX_PARAMETER_NOT_USED(erase_count);
	printf("erase block:%d,num=%d\r\n",block,erase_count);
	nand_err=NAND_EraseBlock(block);
	if(nand_err!=0)
	{
		printf("block_erase error,%d\r\n",nand_err);
		return (LX_ERROR);
	}
	return(LX_SUCCESS);
}
补充块擦除验证函数

函数原型:

UINT (*lx_nand_flash_driver_block_erased_verify)(ULONG block);

这个函数是每次擦除块之后调用的,因为Nandflash擦除是0->1,理论上一个块擦除后数据应该全是0xFF,但是Nandflash使用的过程中可能会出现坏块,就导致擦除完之后不是0xFF,LevelX通过这个函数来确定坏块的产生。这个函数要对比块里的数据是不是全为0xFF 包括Page的数据区和备用区。
具体实现:

UINT  nand_driver_block_erased_verify(ULONG block)
{
	u16 i=0,j=0;
	u32 PageNum=0;
	
	for(i=0;i<PHYSICAL_PAGES_PER_BLOCK;i++)
	{
		PageNum=0;
		PageNum=(block<<6)|(i);
		memset(verifybuf,0,sizeof(verifybuf));
		NAND_ReadPage(PageNum,0,(u8*)verifybuf,NAND_PAGE_SIZE);
		for(j=0;j<NAND_PAGE_SIZE/4;j++)
		{
			if(verifybuf[j]!=0xFFFFFFFF)
			{
				printf("block[%d]_erased_verify error\r\n",block);
				return(LX_ERROR);
			}
		}
		NAND_ReadSpare(PageNum,0,SpareArea,64);
		for(j=0;j<64;j++)
		{
			if(SpareArea[j]!=0xFF)
			{
				printf("block[%d]_erased_verify error\r\n",block);
				return(LX_ERROR);
			}
		}
	}
	return(LX_SUCCESS); 
}
补充页擦除验证函数

函数原型:

UINT (*lx_nand_flash_driver_page_erased_verify)(ULONG block, ULONG page);

与上个函数类似,这个函数是对页进行校验。
具体实现:

UINT  nand_driver_page_erased_verify(ULONG block, ULONG page)
{
	u16 i=0;
	u32 PageNum=0;
	
	PageNum=(block<<6)|(page);
	memset(verifybuf,0,sizeof(verifybuf));
	NAND_ReadPage(PageNum,0,(u8*)verifybuf,NAND_PAGE_SIZE);
	
	for(i=0;i<NAND_PAGE_SIZE/4;i++)
	{
		if(verifybuf[i]!=0xFFFFFFFF)
		{
			printf("page_erased_verify error\r\n");
			return(LX_ERROR);
		}
	}
	NAND_ReadSpare(PageNum,0,SpareArea,64);
	for(i=0;i<64;i++)
	{
		if(SpareArea[i]!=0xFF)
		{
			printf("page_erased_verify error\r\n");
			return(LX_ERROR);
		}
	}
	return(LX_SUCCESS); 
}
补充块状态获取函数

函数原型:

UINT (*lx_nand_flash_driver_block_status_get)(ULONG block, UCHAR *bad_block_flag);

LevelX每个块的状态被放在第0page的spare区中,位置,LevelX已经给我们设置好了,是在BAD_BLOCK_POSITION处。我们要实现的就是从第Block的0page的Spare的BAD_BLOCK_POSITION处读出1字节的数据到bad_block_flag处就行了。
具体实现:

UINT  nand_driver_block_status_get(ULONG block, UCHAR *bad_block_byte)
{
	u32 PageNum=0,i=0;
	u8 nand_err=0;
	
	PageNum=(block<<6);
	for(i=0;i<TIME_OUT;i++)
	{
		nand_err=NAND_ReadSpare(PageNum,BAD_BLOCK_POSITION,bad_block_byte,1);
		if(nand_err==0)
		{
			break;
		}
		printf("status_get error!,%d\r\n",nand_err);
		tx_thread_sleep(50);
	}
	
	if(nand_err!=0)
	{
		
		return(LX_ERROR);
	}
//	printf("block:%d,status_get:%d\r\n",block,*bad_block_byte);
	return(LX_SUCCESS);
}
补充块状态设置函数

函数原型:

UINT (*lx_nand_flash_driver_block_status_set)(ULONG block, UCHAR bad_block_flag);

与上函数类似,这个函数用来标记nandflash的块状态。
具体实现:

UINT  nand_driver_block_status_set(ULONG block, UCHAR bad_block_byte)
{
	u32 PageNum=0,i=0;
	u8 nand_err=0;
	
	PageNum=(block<<6);
	printf("block:%d,status_set:%d\r\n",block,bad_block_byte);
	for(i=0;i<TIME_OUT;i++)
	{
		nand_err=NAND_WriteSpare(PageNum,BAD_BLOCK_POSITION,&bad_block_byte,1);
		if(nand_err==0)
		{
			break;
		}
		printf("status_set write error!,%d\r\n",nand_err);
		tx_thread_sleep(50);
	}
	
	if(nand_err!=0)
	{
		
		return(LX_ERROR);
	}
	return(LX_SUCCESS);
}
补充块额外字节获取与设置函数

函数原型:

UINT (*lx_nand_flash_driver_extra_bytes_get)(ULONG block, ULONG page, UCHAR *destination, UINT size);

根据LevelX的官方文档解释,可知,这个额外字节对于64字节的spare,是有4字节大小的。这4个字节是LevelX定义的一个扇区映射存放地点,下面是这个32位字大小每一位的含义

stm32 移植 freemodbus modbustcp stm32 移植 FileX_stm32_05


具体代码实现:

UINT  nand_driver_block_extra_bytes_get(ULONG block, ULONG page, UCHAR *destination, UINT size)
{
	u32 PageNum=0,i=0;
	u8 nand_err=0;
	
	PageNum=(block<<6)|page;
	for(i=0;i<TIME_OUT;i++)
	{
		nand_err=NAND_ReadSpare(PageNum,EXTRA_BYTE_POSITION,destination,size);
		if(nand_err==0)
		{
			break;
		}
		printf("extra_get error!,%d,%d\r\n",nand_err,size);
		tx_thread_sleep(50);
	}
	
//	destination+=size;
//	printf("extra_get: block=%d,page=%d,size=%d,err=%d\r\n",block,page,size,nand_err);
	if(nand_err!=0)
	{
		
		return(LX_ERROR);
	}
//	printf("\r\n");
	return(LX_SUCCESS);
}
UINT  nand_driver_block_extra_bytes_set(ULONG block, ULONG page, UCHAR *source, UINT size)
{
	u32 PageNum=0,i=0;
	u8 nand_err=0;
	
	PageNum=(block<<6)|page;
//	printf("\r\n");
	for(i=0;i<TIME_OUT;i++)
	{
		nand_err=NAND_WriteSpare(PageNum,EXTRA_BYTE_POSITION,source,size);
		if(nand_err==0)
		{
			break;
		}
		printf("extra_set write error!,%d\r\n",nand_err);
		tx_thread_sleep(50);
	}
	
//	source+=size;
	if(nand_err!=0)
	{
		
		return(LX_ERROR);
	}
	return(LX_SUCCESS);
}
补充系统错误处理函数

原型:

UINT  (*lx_nand_flash_driver_system_error)(UINT error_code, ULONG block, ULONG page);

这个函数在LevelX遇到错误时自动调用,相当于一个错误通知函数,函数参数error_code是错误代码,另外两个分别是出错的地方所在的块和页。
我这里写的就比较简单啦,就是单纯的打印一下在哪出的错,以及错误代码。

UINT  nand_driver_system_error(UINT error_code, ULONG block, ULONG page)
{
    LX_PARAMETER_NOT_USED(error_code);
    LX_PARAMETER_NOT_USED(block);
    LX_PARAMETER_NOT_USED(page);
		printf("err:%d,block:%d,page:%d\r\n",error_code,block,page);
    /* Custom processing goes here...  all errors except for LX_NAND_ERROR_CORRECTED are fatal.  */
    return(LX_ERROR);
}

在LevelX中操作函数

以上已经补充完了LevelX需要的所有底层API函数,接下来我们需要将这些函数注册到LevelX里面,以便LevelX能够调用到我们写的函数。相当于一个初始化函数,要在LevelX运行之前执行。

UINT  nand_flash_initialize(LX_NAND_FLASH *nand_flash)
{
		memset((char*)nand_flash_simulator_buffer,0xFF,BYTES_PER_PHYSICAL_PAGE);
		memset((char*)verifybuf,0xFF,BYTES_PER_PHYSICAL_PAGE);
    /* Setup geometry of the NAND flash.  */
    nand_flash -> lx_nand_flash_total_blocks =                  TOTAL_BLOCKS;
    nand_flash -> lx_nand_flash_pages_per_block =               PHYSICAL_PAGES_PER_BLOCK;
    nand_flash -> lx_nand_flash_bytes_per_page =                BYTES_PER_PHYSICAL_PAGE;

    /* Setup function pointers for the NAND flash services.  */
    nand_flash -> lx_nand_flash_driver_read =                   nand_driver_read_page;
    nand_flash -> lx_nand_flash_driver_write =                  nand_driver_write_page;
    nand_flash -> lx_nand_flash_driver_block_erase =            nand_driver_block_erase;
    nand_flash -> lx_nand_flash_driver_block_erased_verify =    nand_driver_block_erased_verify;
    nand_flash -> lx_nand_flash_driver_page_erased_verify =     nand_driver_page_erased_verify;
    nand_flash -> lx_nand_flash_driver_block_status_get =       nand_driver_block_status_get;
    nand_flash -> lx_nand_flash_driver_block_status_set =       nand_driver_block_status_set;
    nand_flash -> lx_nand_flash_driver_extra_bytes_get =        nand_driver_block_extra_bytes_get;
    nand_flash -> lx_nand_flash_driver_extra_bytes_set =        nand_driver_block_extra_bytes_set;
    nand_flash -> lx_nand_flash_driver_system_error =           nand_driver_system_error;

    /* Setup local buffer for NAND flash operation. This buffer must be the page size of the NAND flash memory.  */
    nand_flash -> lx_nand_flash_page_buffer =  &nand_flash_simulator_buffer[0];

    /* Return success.  */
    return(LX_SUCCESS);
}

上述函数的参数的nand_flash可以看作一个对象(其实看他们开源的代码,其实很多C源码都是以面向对象的思想写的,参考这种思想,后来也对我自己的项目以面向对象的思想进行了重构,多读读源码还是挺有意义的),其中有各种参数和方法可以调用。我们就是对这个对象中的某些方法进行补充。最后调用这个对象来实现我们需要的操作。