第一、二期銜接——6.1 從0寫bootloader

u-boot分析與使用—u-boot編譯體驗


一、前言

在嵌入式Linux開發板中,u-boot的目的是爲了啓動內核,大致流程如下圖:
在這裏插入圖片描述

  1. 開發板一上電啓動bootloader
  2. bootloader首先從Nand Flash上把內核加載到內存中,
  3. bootloader隨後初始化內存、時鐘等
  4. bootloader之後進行參數的設置,如啓動參數、內存參數、命令參數、結束參數
  5. bootloader最後跳轉啓動內核

下面就來從0開始編寫bootloader,實現上述功能。

二、編寫bootloader

  • 【5.1 u-boot分析與使用—u-boot編譯體驗】有提到過,bootloader可以類比成一個複雜的裸機程序。對於一個裸機程序,根據第一期的學習經驗,可以知道程序首先執行的是.S文件中的指令,所以第一個需要編寫的文件是start.S
  • 整個程序的流程:先執行start.S中的程序,下面的第五步,進入到main()內進行串口的初始化、傳遞給內核的參數的一些設置、把內核讀到內存、以及跳轉去執行內核部分。

1、編寫start.S文件

實現的功能如下:

  1. 關看門狗
  2. 設置時鐘
  3. 初始化SDRAM
  4. 重定位 : 把bootloader本身的代碼從flash複製到它的鏈接地址去
  5. 執行main()

1.1 關看門狗

查s3c2440可以往0x53000000地址寫0可以關閉看門狗

	ldr r0, =0x53000000
	mov r1, #0
	str r1, [r0]

1.2 設置時鐘

  • 這裏的時鐘設置爲:FCLK:HCLK:PCLK=1:4:8,主要是一個優化措施:提高時鐘頻率
  • 對於CPU總線模式設置爲asynchronous bus mode,這裏是手冊上規定的
  • 啓動ICACHE:主要是一個優化措施:提高從Flash上讀出內核的速度
	ldr r0, =0x4c000014
	//	mov r1, #0x03;			// FCLK:HCLK:PCLK=1:2:4, HDIVN=1,PDIVN=1
	mov r1, #0x05;			  	// FCLK:HCLK:PCLK=1:4:8
	str r1, [r0]
	
	/* 如果HDIVN非0,CPU的總線模式應該從“fast bus mode”變爲“asynchronous bus mode” */
	mrc	p15, 0, r1, c1, c0, 0		//讀出控制寄存器
	orr	r1, r1, #0xc0000000			//設置爲“asynchronous bus mode
	mcr	p15, 0, r1, c1, c0, 0		//寫入控制寄存器
	
	/* MPLLCON = S3C2440_MPLL_200MHZ */
	ldr r0, =0x4c000004
	ldr r1, =S3C2440_MPLL_400MHZ
	str r1, [r0]
	
	/* 啓動ICACHE */
	mrc p15, 0, r0, c1, c0, 0	@ read control reg
	orr r0, r0, #(1<<12)
	mcr	p15, 0, r0, c1, c0, 0   @ write it back

1.3 初始化SDRAM

	ldr r0, =MEM_CTL_BASE
	adr r1, sdram_config    //sdram_config的當前地址
	add r3, r0, #(13*4)		//r3等於r0 + 52,即sdram_config的末地址
1:
	ldr r2, [r1], #4		//r2從r1的地址讀到一個值,後r1+4
	str r2, [r0], #4		//把r2的值存到r0所指向的地方,後r0+4
	cmp r0, r3				//比較r0與r3,不等則跳回1
	bne 1b

sdram_config:
	.long 0x22011110	 //BWSCON
	.long 0x00000700	 //BANKCON0
	.long 0x00000700	 //BANKCON1
	.long 0x00000700	 //BANKCON2
	.long 0x00000700	 //BANKCON3  
	.long 0x00000700	 //BANKCON4
	.long 0x00000700	 //BANKCON5
	.long 0x00018005	 //BANKCON6
	.long 0x00018005	 //BANKCON7
	.long 0x008C04F4	 //REFRESH
	.long 0x000000B1	 //BANKSIZE
	.long 0x00000030	 //MRSRB6
	.long 0x00000030	 //MRSRB7

1.4 重定位 : 把bootloader本身的代碼從flash複製到它的鏈接地址去

對於copy_code_to_sdram()clean bss()是通過C語言實現的。
爲什麼需要重定位呢?(具體可以看這篇博客【009 數據段重定位】
問題:

  • 首先:CPU能直接訪問的地方有:NOR FLASH、SDRAM、SRAM和各種控制器(包括NAND flash控制器)。所以當我們的程序燒寫到SDRAM或者NOR FALSH的時候,程序能直接運行
  • 如果燒寫到NAND FLASH,芯片會把程序的頭4K先拷貝到SRAM中執行,如果NAND flash中的程序小於4K的話,程序還能正常運行,如果大於4K,那大於4K的這部分就運行不了

解決:

  • 所以我們就引入了重定位,NAND FLASH的代碼中的前4K的代碼需要把整個代碼拷貝到SDRAM去執行。

  • 另外,對於NOR FLASH來說,我們無法簡單的去寫NOR FLASH,所以一旦程序中有需要寫的變量,比如全局變量和靜態變量,我們在無法在NOR FLASH上直接修改它們的值。因此,我們還是需要將NOR FLASH代碼重定位到SDRAM中去執行。

	ldr sp, =0x34000000		//設置棧

	bl nand_init			//因爲無論是何種方式啓動,內核都是在nand flash上,需要讀出來

	mov r0, #0
	ldr r1, =_start			//鏈接地址 
	ldr r2, =__bss_start	//鏈接腳本的地址
	sub r2, r2, r1			//得到程序的大小 
	
	bl copy_code_to_sdram	//執行重定位代碼
	bl clear_bss			//清楚bss段

1.5 執行main()

對於main()是通過C語言實現

	ldr lr, =halt			//返回地址死循環
	ldr pc, =main
halt:
	b halt

1.6 完整的程序


#define S3C2440_MPLL_200MHZ     ((0x5c<<12)|(0x01<<4)|(0x02))
#define S3C2440_MPLL_400MHZ     ((0x5c<<12)|(0x01<<4)|(0x01))
#define MEM_CTL_BASE    0x48000000

.text
.global _start
_start:

/* 1. 關看門狗 */

	ldr r0, =0x53000000
	mov r1, #0
	str r1, [r0]

/* 2. 設置時鐘 */

	ldr r0, =0x4c000014
	//	mov r1, #0x03;			// FCLK:HCLK:PCLK=1:2:4, HDIVN=1,PDIVN=1
	mov r1, #0x05;			  	// FCLK:HCLK:PCLK=1:4:8
	str r1, [r0]

	/* 如果HDIVN非0,CPU的總線模式應該從“fast bus mode”變爲“asynchronous bus mode” */
	mrc	p15, 0, r1, c1, c0, 0		//讀出控制寄存器
	orr	r1, r1, #0xc0000000			//設置爲“asynchronous bus mode
	mcr	p15, 0, r1, c1, c0, 0		//寫入控制寄存器

	/* MPLLCON = S3C2440_MPLL_200MHZ */
	ldr r0, =0x4c000004
	ldr r1, =S3C2440_MPLL_400MHZ
	str r1, [r0]

	/* 啓動ICACHE */
	mrc p15, 0, r0, c1, c0, 0	@ read control reg
	orr r0, r0, #(1<<12)
	mcr	p15, 0, r0, c1, c0, 0   @ write it back


/* 3. 初始化SDRAM */
	
	ldr r0, =MEM_CTL_BASE
	adr r1, sdram_config    //sdram_config的當前地址
	add r3, r0, #(13*4)		//r3等於r0 + 52,即sdram_config的末地址
1:
	ldr r2, [r1], #4		//r2從r1的地址讀到一個值,後r1+4
	str r2, [r0], #4		//把r2的值存到r0所指向的地方,後r0+4
	cmp r0, r3				//比較r0與r3,不等則跳回1
	bne 1b

/* 4. 重定位 : 把bootloader本身的代碼從flash複製到它的鏈接地址去 */
	
	ldr sp, =0x34000000		//設置棧

	bl nand_init			//因爲無論是何種方式啓動,內核都是在nand flash上,需要讀出來

	mov r0, #0
	ldr r1, =_start			//鏈接地址 
	ldr r2, =__bss_start	//鏈接腳本的地址
	sub r2, r2, r1			//得到程序的大小 
	
	bl copy_code_to_sdram	//執行重定位代碼
	bl clear_bss			//清楚bss段
	
/* 5. 執行main */
	
	ldr lr, =halt			//返回地址死循環
	ldr pc, =main
halt:
	b halt

sdram_config:
	.long 0x22011110	 //BWSCON
	.long 0x00000700	 //BANKCON0
	.long 0x00000700	 //BANKCON1
	.long 0x00000700	 //BANKCON2
	.long 0x00000700	 //BANKCON3  
	.long 0x00000700	 //BANKCON4
	.long 0x00000700	 //BANKCON5
	.long 0x00018005	 //BANKCON6
	.long 0x00018005	 //BANKCON7
	.long 0x008C04F4	 //REFRESH
	.long 0x000000B1	 //BANKSIZE
	.long 0x00000030	 //MRSRB6
	.long 0x00000030	 //MRSRB7

2、編寫鏈接腳本boot.lds

SECTIONS {
    . = 0x33f80000;
    .text : { *(.text) }
    
    . = ALIGN(4);
    .rodata : {*(.rodata*)} 
    
    . = ALIGN(4);
    .data : { *(.data) }
    
    . = ALIGN(4);
    __bss_start = .;
    .bss : { *(.bss)  *(COMMON) }
    __bss_end = .;
}

3、編寫boot.c主函數文件

實現的功能如下:

  1. 幫內核設置串口: 內核啓動的開始部分會從串口打印一些信息
  2. NAND FLASH裏把內核讀入內存
  3. 設置參數
  4. 跳轉執行

3.1 幫內核設置串口

uart0_init()是在init.c文件中實現的,這裏先把代碼貼出來

/*
 * 初始化UART0
 * 115200,8N1,無流控
 */
void uart0_init(void)
{
    GPHCON  |= 0xa0;    // GPH2,GPH3用作TXD0,RXD0
    GPHUP   = 0x0c;     // GPH2,GPH3內部上拉

    ULCON0  = 0x03;     // 8N1(8個數據位,無較驗,1個停止位)
    UCON0   = 0x05;     // 查詢方式,UART時鐘源爲PCLK
    UFCON0  = 0x00;     // 不使用FIFO
    UMCON0  = 0x00;     // 不使用流控
    UBRDIV0 = UART_BRD; // 波特率爲115200
}

3.2 從NAND FLASH裏把內核讀入內存

這裏的nand_read()、puthex()、puts()也是在init.c文件中實現

	/* 1、從NAND FLASH裏把內核讀入內存 
	 * uImage = 64 + zImage,通過uboot執行mtd可以找到zImage燒寫地址
  	 */
	puts("Copy kernel from nand\n\r");
	nand_read(0x60000+64, (unsigned char *)0x30008000, 0x200000);
	/* 調試代碼 */
	puthex(0x1234ABCD);
	puts("\n\r");
	puthex(*p);  
	puts("\n\r");

3.3 設置參數

	/* 2、設置參數 */
	puts("Set boot params\n\r");
	setup_start_tag();		//啓動參數
	setup_memory_tags();	//內存參數
	setup_commandline_tag("noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0,115200");	//命令行參數
	setup_end_tag();		//結束參數

對於4個參數的設置,我畫了下面的圖以供理解:

  1. 根據start_tag的參數tag:0x30000100,在地址爲0x30000100的內存中進行入棧

  2. 每設置完一個參數後params指針移動到下一個tag的位置,進行下一個參數的入棧
    在這裏插入圖片描述

3.4 跳轉執行

最後則會根據傳入的參數,跳去執行theKernel()函數

	/* 3、跳轉執行 */
	puts("Boot kernel\n\r");
	theKernel = (void (*)(int, int, unsigned int))0x30008000;
	/* 
	 *  mov r0, #0
	 *  ldr r1, =362 —> 機器ID
	 *  ldr r2, =0x30000100
	 *  mov pc, #0x30008000 
	 */
	theKernel(0, 362, 0x30000100);  

	/* 如果一切正常, 不會執行到這裏 */
	puts("Error!\n\r");

3.5 完整的文件

#include "setup.h"
 
extern void uart0_init(void);
extern void nand_read(unsigned int addr, unsigned char *buf, unsigned int len);
extern void puts(char *str);
extern void puthex(unsigned int val);

static struct tag *params;

/* 設置啓動參數 */
void setup_start_tag(void)
{
	params = (struct tag *)0x30000100;

	params->hdr.tag = ATAG_CORE;
	params->hdr.size = tag_size (tag_core);

	/* 未用到 */
	params->u.core.flags = 0;
	params->u.core.pagesize = 0;
	params->u.core.rootdev = 0;

	/* 移動指針指向下一個tag */
	params = tag_next (params);
}

/* 設置內存參數 */
void setup_memory_tags(void)
{
	params->hdr.tag = ATAG_MEM;

	/* ((sizeof(struct tag_header) + sizeof(struct type)) >> 2)
	 * 		struct tag_header {       	 struct tag_mem32 {  
	 *			__u32 size;					__u32	size;
	 *			__u32 tag;					__u32	start;
	 *		};							};
	 *     字節: (8 + 8) >> 2 = 4
	 */
	params->hdr.size = tag_size (tag_mem32);	
	
	params->u.mem.start = 0x30000000;		//內存起始地址
	params->u.mem.size  = 64*1024*1024;		//內存大小64M	

	/* 移動指針指向下一個tag */
	params = tag_next (params);
}

int strlen(char *str)
{
	int i = 0;
	while (str[i])
		i++;
	
	return i;
}

void strcpy(char *dest, char *src)
{
	while ((*dest++ = *src++) != '\0');
}

/* 設置命令行參數 */
void setup_commandline_tag(char *cmdline)
{
	int len = strlen(cmdline) + 1;
	
	params->hdr.tag  = ATAG_CMDLINE;
	params->hdr.size = (sizeof (struct tag_header) + len + 3) >> 2;	//向四取整  

	strcpy (params->u.cmdline.cmdline, cmdline);

	/* 移動指針指向下一個tag */
	params = tag_next (params);
}

/* 設置結束函數 */
void setup_end_tag(void)
{
	params->hdr.tag = ATAG_NONE;
	params->hdr.size = 0;
}

int main(void)
{
	void (*theKernel)(int zero, int arch, unsigned int params);
	volatile unsigned int *p = (volatile unsigned int *)0x30008000;

	/* 0、幫內核設置串口: 內核啓動的開始部分會從串口打印一些信息,但是內核一開始沒有初始化串口 */
	uart0_init();
	
	/* 1、從NAND FLASH裏把內核讀入內存 
	 * uImage = 64 + zImage,通過uboot執行mtd可以找到zImage燒寫地址
  	 */
	puts("Copy kernel from nand\n\r");
	nand_read(0x60000+64, (unsigned char *)0x30008000, 0x200000);
	puthex(0x1234ABCD);
	puts("\n\r");
	puthex(*p);  
	puts("\n\r");

	/* 2、設置參數 */
	puts("Set boot params\n\r");
	setup_start_tag();		//啓動參數
	setup_memory_tags();	//內存參數
	setup_commandline_tag("noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0,115200");	//命令行參數
	setup_end_tag();		//結束參數

	/* 3、跳轉執行 */
	puts("Boot kernel\n\r");
	theKernel = (void (*)(int, int, unsigned int))0x30008000;
	/* 
	 *  mov r0, #0
	 *  ldr r1, =362 —> 機器ID
	 *  ldr r2, =0x30000100
	 *  mov pc, #0x30008000 
	 */
	theKernel(0, 362, 0x30000100);  

	/* 如果一切正常, 不會執行到這裏 */
	puts("Error!\n\r");

	return -1;
}

4、編寫相關初始化init.c文件


 /* NAND FLASH控制器 */
#define NFCONF 			(*((volatile unsigned long *)0x4E000000))
#define NFCONT 			(*((volatile unsigned long *)0x4E000004))
#define NFCMMD 			(*((volatile unsigned char *)0x4E000008))
#define NFADDR 			(*((volatile unsigned char *)0x4E00000C))
#define NFDATA 			(*((volatile unsigned char *)0x4E000010))
#define NFSTAT 			(*((volatile unsigned char *)0x4E000020))
 
 /* GPIO */
#define GPHCON          (*(volatile unsigned long *)0x56000070)
#define GPHUP           (*(volatile unsigned long *)0x56000078)
 
 /* UART registers*/
#define ULCON0          (*(volatile unsigned long *)0x50000000)
#define UCON0           (*(volatile unsigned long *)0x50000004)
#define UFCON0          (*(volatile unsigned long *)0x50000008)
#define UMCON0          (*(volatile unsigned long *)0x5000000c)
#define UTRSTAT0        (*(volatile unsigned long *)0x50000010)
#define UTXH0           (*(volatile unsigned char *)0x50000020)
#define URXH0           (*(volatile unsigned char *)0x50000024)
#define UBRDIV0         (*(volatile unsigned long *)0x50000028)
 
#define TXD0READY   	(1<<2)
 
#define PCLK            50000000    // init.c中的clock_init函數設置PCLK爲50MHz
#define UART_CLK        PCLK        //  UART0的時鐘源設爲PCLK
#define UART_BAUD_RATE  115200      // 波特率
#define UART_BRD        ((UART_CLK  / (UART_BAUD_RATE * 16)) - 1)

/*
 * 初始化UART0
 * 115200,8N1,無流控
 */
void uart0_init(void)
{
    GPHCON  |= 0xa0;    // GPH2,GPH3用作TXD0,RXD0
    GPHUP   = 0x0c;     // GPH2,GPH3內部上拉

    ULCON0  = 0x03;     // 8N1(8個數據位,無較驗,1個停止位)
    UCON0   = 0x05;     // 查詢方式,UART時鐘源爲PCLK
    UFCON0  = 0x00;     // 不使用FIFO
    UMCON0  = 0x00;     // 不使用流控
    UBRDIV0 = UART_BRD; // 波特率爲115200
}

/* 發送一個字符 */
void putc(unsigned char c)
{
    /* 等待,直到發送緩衝區中的數據已經全部發送出去 */
    while (!(UTRSTAT0 & TXD0READY));
    
    /* 向UTXH0寄存器中寫入數據,UART即自動將它發送出去 */
    UTXH0 = c;
}

void puts(char *str)
{
	int i = 0;
	while (str[i])
	{
		putc(str[i]);
		i++;
	}
}

void puthex(unsigned int val)
{
	/* 0x1234abcd */
	int i;
	int j;
	
	puts("0x");

	for (i = 0; i < 8; i++) {
		j = (val >> ((7-i)*4)) & 0xf;
		if ((j >= 0) && (j <= 9))
			putc('0' + j);
		else
			putc('A' + j - 0xa);
		
	}
}

/* nor flash啓動,0地址是nor flash的0地址,可以像內存一樣讀,但是不能像內存一樣寫
 * nand flash啓動,0地址是片內內存,內存可以寫
 */ 
int isBootFromNorFlash(void)
{
	volatile int *p = (volatile int *)0;	//舊值
	int val;

	val = *p;		//緩衝舊值
	*p = 0x12345;	//往0地址寫入新值

	/* 判斷新值是否寫成功 */
	if (*p == 0x12345) {	
		/* 寫成功,nand啓動       */
		*p = val;	
		return 0;
	} else {
		/* 寫失敗,nor啓動 */
		return 1;
	}
}

 void nand_init(void)
{
#define TACLS   0
#define TWRPH0  1
#define TWRPH1  0

	/* 設置時序 */
	NFCONF = (TACLS<<12)|(TWRPH0<<8)|(TWRPH1<<4);
	/* 使能NAND Flash控制器, 初始化ECC, 禁止片選 */
	NFCONT = (1<<4)|(1<<1)|(1<<0);	
}
 
void nand_select(void)
{
	NFCONT &= ~(1<<1);	
}

void nand_deselect(void)
{
	NFCONT |= (1<<1);	
}

void nand_cmd(unsigned char cmd)
{
	volatile int i;
	NFCMMD = cmd;
	for (i = 0; i < 10; i++);
}

void nand_addr(unsigned int addr)
{
	unsigned int col  = addr % 2048;
	unsigned int page = addr / 2048;
	volatile int i;

	NFADDR = col & 0xff;
	for (i = 0; i < 10; i++);
	NFADDR = (col >> 8) & 0xff;
	for (i = 0; i < 10; i++);
	
	NFADDR  = page & 0xff;
	for (i = 0; i < 10; i++);
	NFADDR  = (page >> 8) & 0xff;
	for (i = 0; i < 10; i++);
	NFADDR  = (page >> 16) & 0xff;
	for (i = 0; i < 10; i++);	
}

void nand_page(unsigned int page)
{
	volatile int i;
	
	NFADDR  = page & 0xff;
	for (i = 0; i < 10; i++);
	NFADDR  = (page >> 8) & 0xff;
	for (i = 0; i < 10; i++);
	NFADDR  = (page >> 16) & 0xff;
	for (i = 0; i < 10; i++);	
}

void nand_col(unsigned int col)
{
	volatile int i;

	NFADDR = col & 0xff;
	for (i = 0; i < 10; i++);
	NFADDR = (col >> 8) & 0xff;
	for (i = 0; i < 10; i++);
}

void nand_wait_ready(void)
{
	while (!(NFSTAT & 1));
}

unsigned char nand_data(void)
{
	return NFDATA;
}

int nand_bad(unsigned int addr)
{
	unsigned int col  = 2048;
	unsigned int page = addr / 2048;
	unsigned char val;

	/* 1. 選中 */
	nand_select();
	
	/* 2. 發出讀命令00h */
	nand_cmd(0x00);
	
	/* 3. 發出地址(分5步發出) */
	nand_col(col);
	nand_page(page);
	
	/* 4. 發出讀命令30h */
	nand_cmd(0x30);
	
	/* 5. 判斷狀態 */
	nand_wait_ready();

	/* 6. 讀數據 */
	val = nand_data();
	
	/* 7. 取消選中 */		
	nand_deselect();

	if (val != 0xff)
		return 1;  /* bad blcok */
	else
		return 0;
}

void nand_read(unsigned int addr, unsigned char *buf, unsigned int len)
{
	int col = addr % 2048;
	int i = 0;
		
	while (i < len) {
	 	/* 一個block只判斷一次 */
		if (!(addr & 0x1FFFF) && nand_bad(addr)) {
			addr += (128*1024);  /* 跳過當前block */
			continue;
		}

		/* 1. 選中 */
		nand_select();	
		
		/* 2. 發出讀命令00h */
		nand_cmd(0x00);

		/* 3. 發出地址(分5步發出) */
		nand_addr(addr);

		/* 4. 發出讀命令30h */
		nand_cmd(0x30);

		/* 5. 判斷狀態 */
		nand_wait_ready();

		/* 6. 讀數據 */
		for (; (col < 2048) && (i < len); col++) {
			buf[i] = nand_data();
			i++;
			addr++;
		}
		
		col = 0;

		/* 7. 取消選中 */		
		nand_deselect();	
	}
}

void copy_code_to_sdram(unsigned char *src, unsigned char *dest,
						unsigned int length)
{
	int i = 0;

	/* 如果是NOR	啓動 */
	if (isBootFromNorFlash()) {
		while (i < length) {
			dest[i] = src[i];
			i++;
		}
	} else {
		//nand_init();
		nand_read((unsigned int)src, dest, length);
	}
}

void clear_bss(void)
{
	/* 引用鏈接腳本的變量方法:定義外部變量,取地址 */
	extern int __bss_start, __bss_end;
	int *p = &__bss_start;

	for (; p < &__bss_end; p++)
		*p = 0;
}

5、編寫Makefile文件

CC      = arm-linux-gcc
LD      = arm-linux-ld
AR      = arm-linux-ar
OBJCOPY = arm-linux-objcopy
OBJDUMP = arm-linux-objdump

CFLAGS 		:= -Wall -O2
CPPFLAGS   	:= -nostdinc -nostdlib -fno-builtin

objs := start.o init.o boot.o

boot.bin: $(objs)
	${LD} -Tboot.lds -o boot.elf $^
	${OBJCOPY} -O binary -S boot.elf $@
	${OBJDUMP} -D -m arm boot.elf > boot.dis
	
%.o:%.c
	${CC} $(CPPFLAGS) $(CFLAGS) -c -o $@ $<

%.o:%.S
	${CC} $(CPPFLAGS) $(CFLAGS) -c -o $@ $<

clean:
	rm -f *.o *.bin *.elf *.dis

三、編譯與測試

1、編譯

執行make之後會生成boot.bin文件
在這裏插入圖片描述

2、測試

boot.bin文件通過OpenJtag分別燒寫到Nor FlashNand Flash中,可以看到開發板成功啓動bootloader

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章