Cubietruck开发板SPL阶段加载uboot到SDRAM并启动

接着前几篇博客继续分析,在lowlevel_init结束后也就是s_init返回后,跳转到_main,那么就从_main开始吧!

ENTRY(_main)

/*
 * Set up initial C runtime environment and call board_init_f(0).
 */

#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
	ldr	sp, =(CONFIG_SPL_STACK)
#else
	ldr	sp, =(CONFIG_SYS_INIT_SP_ADDR)
#endif
	bic	sp, sp, #7	/* 8-byte alignment for ABI compliance */
	sub	sp, sp, #GD_SIZE	/* allocate one GD above SP */
	bic	sp, sp, #7	/* 8-byte alignment for ABI compliance */
	mov	r9, sp		/* GD is above SP */
	mov	r0, #0
	bl	board_init_f

#if ! defined(CONFIG_SPL_BUILD)

/*
 * Set up intermediate environment (new sp and gd) and call
 * relocate_code(addr_moni). Trick here is that we'll return
 * 'here' but relocated.
 */

	ldr	sp, [r9, #GD_START_ADDR_SP]	/* sp = gd->start_addr_sp */
	bic	sp, sp, #7	/* 8-byte alignment for ABI compliance */
	ldr	r9, [r9, #GD_BD]		/* r9 = gd->bd */
	sub	r9, r9, #GD_SIZE		/* new GD is below bd */

	adr	lr, here
	ldr	r0, [r9, #GD_RELOC_OFF]		/* r0 = gd->reloc_off */
	add	lr, lr, r0
	ldr	r0, [r9, #GD_RELOCADDR]		/* r0 = gd->relocaddr */
	b	relocate_code
here:

/* Set up final (full) environment */

	bl	c_runtime_cpu_setup	/* we still call old routine here */

	ldr	r0, =__bss_start	/* this is auto-relocated! */
	ldr	r1, =__bss_end		/* this is auto-relocated! */

	mov	r2, #0x00000000		/* prepare zero to clear BSS */

clbss_l:cmp	r0, r1			/* while not at end of BSS */
	strlo	r2, [r0]		/* clear 32-bit BSS word */
	addlo	r0, r0, #4		/* move to next */
	blo	clbss_l

	bl coloured_LED_init
	bl red_led_on

	/* call board_init_r(gd_t *id, ulong dest_addr) */
	mov     r0, r9                  /* gd_t */
	ldr	r1, [r9, #GD_RELOCADDR]	/* dest_addr */
	/* call board_init_r */
	ldr	pc, =board_init_r	/* this is auto-relocated! */

	/* we should not return here. */

#endif

ENDPROC(_main)
前面已经分析过了,SPL阶段只会编译到bl board_init_f,而且这个函数是中的

void __weak board_init_f(ulong dummy)
{
	/* Clear the BSS. */
	memset(__bss_start, 0, __bss_end - __bss_start);

	/* Set global data pointer. */
	gd = &gdata;

	board_init_r(NULL, 0);
}
这里的board_init_r是中的

void board_init_r(gd_t *dummy1, ulong dummy2)
{
	u32 boot_device;
	debug(">>spl:board_init_r()\n");

#ifdef CONFIG_SYS_SPL_MALLOC_START
	mem_malloc_init(CONFIG_SYS_SPL_MALLOC_START,
			CONFIG_SYS_SPL_MALLOC_SIZE);
#endif

#ifndef CONFIG_PPC
	/*
	 * timer_init() does not exist on PPC systems. The timer is initialized
	 * and enabled (decrementer) in interrupt_init() here.
	 */
	timer_init();
#endif

#ifdef CONFIG_SPL_BOARD_INIT
	spl_board_init();
#endif

	boot_device = spl_boot_device();
	debug("boot device - %d\n", boot_device);
	switch (boot_device) {
#ifdef CONFIG_SPL_RAM_DEVICE
	case BOOT_DEVICE_RAM:
		spl_ram_load_image();
		break;
#endif
#ifdef CONFIG_SPL_MMC_SUPPORT
	case BOOT_DEVICE_MMC1:
	case BOOT_DEVICE_MMC2:
	case BOOT_DEVICE_MMC2_2:
		spl_mmc_load_image();
		break;
#endif
#ifdef CONFIG_SPL_NAND_SUPPORT
	case BOOT_DEVICE_NAND:
		spl_nand_load_image();
		break;
#endif
#ifdef CONFIG_SPL_ONENAND_SUPPORT
	case BOOT_DEVICE_ONENAND:
		spl_onenand_load_image();
		break;
#endif
#ifdef CONFIG_SPL_NOR_SUPPORT
	case BOOT_DEVICE_NOR:
		spl_nor_load_image();
		break;
#endif
#ifdef CONFIG_SPL_YMODEM_SUPPORT
	case BOOT_DEVICE_UART:
		spl_ymodem_load_image();
		break;
#endif
#ifdef CONFIG_SPL_SPI_SUPPORT
	case BOOT_DEVICE_SPI:
		spl_spi_load_image();
		break;
#endif
#ifdef CONFIG_SPL_ETH_SUPPORT
	case BOOT_DEVICE_CPGMAC:
#ifdef CONFIG_SPL_ETH_DEVICE
		spl_net_load_image(CONFIG_SPL_ETH_DEVICE);
#else
		spl_net_load_image(NULL);
#endif
		break;
#endif
#ifdef CONFIG_SPL_USBETH_SUPPORT
	case BOOT_DEVICE_USBETH:
		spl_net_load_image("usb_ether");
		break;
#endif
#ifdef CONFIG_SPL_USB_SUPPORT
	case BOOT_DEVICE_USB:
		spl_usb_load_image();
		break;
#endif
#ifdef CONFIG_SPL_SATA_SUPPORT
	case BOOT_DEVICE_SATA:
		spl_sata_load_image();
		break;
#endif
	default:
		debug("SPL: Un-supported Boot Device\n");
		hang();
	}

	switch (spl_image.os) {
	case IH_OS_U_BOOT:
		debug("Jumping to U-Boot\n");
		break;
#ifdef CONFIG_SPL_OS_BOOT
	case IH_OS_LINUX:
		debug("Jumping to Linux\n");
		spl_board_prepare_for_linux();
		jump_to_image_linux((void *)CONFIG_SYS_SPL_ARGS_ADDR);
#endif
	default:
		debug("Unsupported OS image.. Jumping nevertheless..\n");
	}
	jump_to_image_no_args(&spl_image);
}
其实这些前几篇大概已经分析过了,这里详细分析一下spl_mmc_load_image();系统从SD卡启动

的就是运行这段代码的,想学习一下SD卡的驱动

void spl_mmc_load_image(void)
{
	struct mmc *mmc;
	int err;
	u32 boot_mode;

	mmc_initialize(gd->bd);
	/* We register only one device. So, the dev id is always 0 */
	mmc = find_mmc_device(0);
	if (!mmc) {
#ifdef CONFIG_SPL_LIBCOMMON_SUPPORT
		puts("spl: mmc device not found!!\n");
#endif
		hang();
	}

	err = mmc_init(mmc);
	if (err) {
#ifdef CONFIG_SPL_LIBCOMMON_SUPPORT
		printf("spl: mmc init failed: err - %d\n", err);
#endif
		hang();
	}

	boot_mode = spl_boot_mode();
	if (boot_mode == MMCSD_MODE_RAW) {
		debug("boot mode - RAW\n");
#ifdef CONFIG_SPL_OS_BOOT
		if (spl_start_uboot() || mmc_load_image_raw_os(mmc))
#endif
		err = mmc_load_image_raw(mmc,
			CONFIG_SYS_MMCSD_RAW_MODE_U_BOOT_SECTOR);
#ifdef CONFIG_SPL_FAT_SUPPORT
	} else if (boot_mode == MMCSD_MODE_FAT) {
		debug("boot mode - FAT\n");
#ifdef CONFIG_SPL_OS_BOOT
		if (spl_start_uboot() || spl_load_image_fat_os(&mmc->block_dev,
								CONFIG_SYS_MMC_SD_FAT_BOOT_PARTITION))
#endif
		err = spl_load_image_fat(&mmc->block_dev,
					CONFIG_SYS_MMC_SD_FAT_BOOT_PARTITION,
					CONFIG_SPL_FAT_LOAD_PAYLOAD_NAME);
#endif
#ifdef CONFIG_SUPPORT_EMMC_BOOT
	} else if (boot_mode == MMCSD_MODE_EMMCBOOT) {
		/*
		 * We need to check what the partition is configured to.
		 * 1 and 2 match up to boot0 / boot1 and 7 is user data
		 * which is the first physical partition (0).
		 */
		int part = (mmc->part_config >> 3) & PART_ACCESS_MASK;

		if (part == 7)
			part = 0;

		if (mmc_switch_part(0, part)) {
#ifdef CONFIG_SPL_LIBCOMMON_SUPPORT
			puts("MMC partition switch failed\n");
#endif
			hang();
		}
#ifdef CONFIG_SPL_OS_BOOT
		if (spl_start_uboot() || mmc_load_image_raw_os(mmc))
#endif
		err = mmc_load_image_raw(mmc,
			CONFIG_SYS_MMCSD_RAW_MODE_U_BOOT_SECTOR);
#endif
	} else {
#ifdef CONFIG_SPL_LIBCOMMON_SUPPORT
		puts("spl: wrong MMC boot mode\n");
#endif
		hang();
	}

	if (err)
		hang();
}

首先mmc_initialize(gd->bd);

int mmc_initialize(bd_t *bis)
{
	INIT_LIST_HEAD (&mmc_devices);
	cur_dev_num = 0;

	if (board_mmc_init(bis) < 0)
		cpu_mmc_init(bis);

#ifndef CONFIG_SPL_BUILD
	print_mmc_devices(',');
#endif

	do_preinit();
	return 0;
}
其中的board_mmc_init(bis)
#ifdef CONFIG_GENERIC_MMC
int board_mmc_init(bd_t *bis)
{
	sunxi_mmc_init(CONFIG_MMC_SUNXI_SLOT);
#if !defined (CONFIG_SPL_BUILD) && defined (CONFIG_MMC_SUNXI_SLOT_EXTRA)
	sunxi_mmc_init(CONFIG_MMC_SUNXI_SLOT_EXTRA);
#endif

	return 0;
}
#endif
很明显实际的工作交给了sunxi_mmc_init(CONFIG_MMC_SUNXI_SLOT);

int sunxi_mmc_init(int sdc_no)
{
	struct mmc *mmc;

	memset(&mmc_dev[sdc_no], 0, sizeof(struct mmc));
	memset(&mmc_host[sdc_no], 0, sizeof(struct sunxi_mmc_host));
	mmc = &mmc_dev[sdc_no];

	sprintf(mmc->name, "SUNXI SD/MMC");
	mmc->priv = &mmc_host[sdc_no];
	mmc->send_cmd = mmc_send_cmd;
	mmc->set_ios = mmc_set_ios;
	mmc->init = mmc_core_init;

	mmc->voltages = MMC_VDD_32_33 | MMC_VDD_33_34;
	mmc->host_caps = MMC_MODE_4BIT;
	mmc->host_caps |= MMC_MODE_HS_52MHz | MMC_MODE_HS;

	mmc->f_min = 400000;
	mmc->f_max = 52000000;

	mmc_resource_init(sdc_no);
	mmc_clk_io_on(sdc_no);

	mmc_register(mmc);

	return 0;
}
上面就是mmc_initialize(gd->bd);的实际工作了,满眼都是结构体,还是从结构体说起吧!

首先抛开linux下SD卡的驱动模型,比如什么kobject、kset、块设备的缓冲、总线等概念,

但是有些概念还是需要的,比如HOST控制器,所以这里主要介绍4个struct

struct mmc {
	struct list_head link;
	char name[32];
	void *priv;
	uint voltages;
	uint version;
	uint has_init;
	uint f_min;
	uint f_max;
	int high_capacity;
	uint bus_width;
	uint clock;
	uint card_caps;
	uint host_caps;
	uint ocr;
	uint dsr;
	uint dsr_imp;
	uint scr[2];
	uint csd[4];
	uint cid[4];
	ushort rca;
	char part_config;
	char part_num;
	uint tran_speed;
	uint read_bl_len;
	uint write_bl_len;
	uint erase_grp_size;
	u64 capacity;
	u64 capacity_user;
	u64 capacity_boot;
	u64 capacity_rpmb;
	u64 capacity_gp[4];
	block_dev_desc_t block_dev;
	int (*send_cmd)(struct mmc *mmc,
			struct mmc_cmd *cmd, struct mmc_data *data);
	void (*set_ios)(struct mmc *mmc);
	int (*init)(struct mmc *mmc);
	int (*getcd)(struct mmc *mmc);
	int (*getwp)(struct mmc *mmc);
	uint b_max;
	char op_cond_pending;	/* 1 if we are waiting on an op_cond command */
	char init_in_progress;	/* 1 if we have done mmc_start_init() */
	char preinit;		/* start init as early as possible */
	uint op_cond_response;	/* the response byte from the last op_cond */
};
不用说了,MMC的主要结构体。其中要注意的是block_dev_desc_t block_dev;void *priv;

说一下个人的理解,其中block_dev_desc_t block_dev;相当于MMC留个上层的接口,相对于

linux驱动中的嵌入到实际驱动的字符设备结构体、块设备结构体 ,毕竟MMC对于上层而言是

块设备,那么这个block_dev_desc_t block_dev;中重要的接口就是block_read、block_write、

block_erase,由于对linux块设备驱动研究的不是很深,好像linux的块设备驱动没有这种直接的

接口,这里的接口更像是无OS的裸机驱动,还有和linux块设备驱动的最大区别莫过于没有缓冲,

或者说没有linux的缓冲结构复杂,这个跟SD卡是否存在文件系统有关系吗??

那么void *priv;是什么呢?在sunxi_mmc_init中对priv 的操作就是mmc->priv = &mmc_host[sdc_no];

所以猜测用于指向mmc控制器,和linux的MMC驱动中的host有点像。

那么小结一下:

mmc驱动的模型:

block_dev_desc_t block_dev;是留给上层的接口,使用方法是

mmc->block_dev.block_read(0, sector, 1, header);读取块设备的一个例子。


void *priv;是具体的MMC控制器,为什么要有这个呢?还拿读取块设备作为例子吧。

mmc->block_dev.block_read(0, sector, 1, header);的实际操作按理说是直接操作MMC控制器的,

但是系统往往不只有一个MMC控制器,所以加入这个void *priv可以指向某个MMC控制器,而不至于

把驱动写死了,也就是说mmc->block_dev.block_read(0, sector, 1, header);的实际执行动作是可以

从几个MMC控制器中选择的。

小结完毕

看看block_dev_desc(也就是block_dev_desc_t)和sunxi_mmc_host的定义吧

typedef struct block_dev_desc {
	int		if_type;	/* type of the interface */
	int		dev;		/* device number */
	unsigned char	part_type;	/* partition type */
	unsigned char	target;		/* target SCSI ID */
	unsigned char	lun;		/* target LUN */
	unsigned char	type;		/* device type */
	unsigned char	removable;	/* removable device */
#ifdef CONFIG_LBA48
	unsigned char	lba48;		/* device can use 48bit addr (ATA/ATAPI v7) */
#endif
	lbaint_t	lba;		/* number of blocks */
	unsigned long	blksz;		/* block size */
	int		log2blksz;	/* for convenience: log2(blksz) */
	char		vendor [40+1];	/* IDE model, SCSI Vendor */
	char		product[20+1];	/* IDE Serial no, SCSI product */
	char		revision[8+1];	/* firmware revision */
	unsigned long	(*block_read)(int dev,
				      lbaint_t start,
				      lbaint_t blkcnt,
				      void *buffer);
	unsigned long	(*block_write)(int dev,
				       lbaint_t start,
				       lbaint_t blkcnt,
				       const void *buffer);
	unsigned long   (*block_erase)(int dev,
				       lbaint_t start,
				       lbaint_t blkcnt);
	void		*priv;		/* driver private struct pointer */
}block_dev_desc_t;
struct sunxi_mmc_host {
	unsigned mmc_no;
	uint32_t *mclkreg;
	unsigned database;
	unsigned fatal_err;
	unsigned mod_clk;
	struct sunxi_mmc *reg;
};
虽然不能理解每个成员的意义,但是主要的成员还是看的懂的。

那么回过头来看看int sunxi_mmc_init(int sdc_no)

memset(&mmc_dev[sdc_no], 0, sizeof(struct mmc));
memset(&mmc_host[sdc_no], 0, sizeof(struct sunxi_mmc_host));

意思很明白,但是问题是为什么会有struct mmc、struct sunxi_mmc_host数组呢?

其实也不难理解,a20有4个MMC控制器吧,搜索参考手册中的SDC,结果存在SDC0~3。

OK继续,mmc->priv = &mmc_host[sdc_no]不用解释了,参考前面对MMC控制器的解释。

mmc->send_cmd = mmc_send_cmd;
mmc->set_ios = mmc_set_ios;
mmc->init = mmc_core_init;

mmc->voltages = MMC_VDD_32_33 | MMC_VDD_33_34;
mmc->host_caps = MMC_MODE_4BIT;
mmc->host_caps |= MMC_MODE_HS_52MHz | MMC_MODE_HS;

mmc->f_min = 400000;
mmc->f_max = 52000000;

这些都不用说了吧,每个意义不能很深的理解,但是大概意思还是懂的,

mmc_resource_init(sdc_no);

static int mmc_resource_init(int sdc_no)
{
	struct sunxi_mmc_host *mmchost = &mmc_host[sdc_no];
	struct sunxi_ccm_reg *ccm = (struct sunxi_ccm_reg *)SUNXI_CCM_BASE;

	debug("init mmc %d resource\n", sdc_no);

	switch (sdc_no) {
	case 0:
		mmchost->reg = (struct sunxi_mmc *)SUNXI_MMC0_BASE;
		mmchost->mclkreg = &ccm->sd0_clk_cfg;
		break;
	case 1:
		mmchost->reg = (struct sunxi_mmc *)SUNXI_MMC1_BASE;
		mmchost->mclkreg = &ccm->sd1_clk_cfg;
		break;
	case 2:
		mmchost->reg = (struct sunxi_mmc *)SUNXI_MMC2_BASE;
		mmchost->mclkreg = &ccm->sd2_clk_cfg;
		break;
	case 3:
		mmchost->reg = (struct sunxi_mmc *)SUNXI_MMC3_BASE;
		mmchost->mclkreg = &ccm->sd3_clk_cfg;
		break;
	default:
		printf("Wrong mmc number %d\n", sdc_no);
		return -1;
	}
	mmchost->database = (unsigned int)mmchost->reg + 0x100;
	mmchost->mmc_no = sdc_no;

	return 0;
}
设置mmhost为具体的控制器。由于在这里是SUNXI_MMC0_BASE

mmc_clk_io_on(sdc_no);代码就不粘了,主要是对MMC要用到的管脚初始化、还有用刚才

设置好的mmhost设置MMC时钟等等。

mmc_register(mmc);不用说了,注册MMC,在这里分析一下

int mmc_register(struct mmc *mmc)
{
	/* Setup dsr related values */
	mmc->dsr_imp = 0;
	mmc->dsr = 0xffffffff;
	/* Setup the universal parts of the block interface just once */
	mmc->block_dev.if_type = IF_TYPE_MMC;
	mmc->block_dev.dev = cur_dev_num++;
	mmc->block_dev.removable = 1;
	mmc->block_dev.block_read = mmc_bread;
	mmc->block_dev.block_write = mmc_bwrite;
	mmc->block_dev.block_erase = mmc_berase;
	if (!mmc->b_max)
		mmc->b_max = CONFIG_SYS_MMC_MAX_BLK_COUNT;

	INIT_LIST_HEAD (&mmc->link);

	list_add_tail (&mmc->link, &mmc_devices);

	return 0;
}
很明显,设置对上层留出的接口。

mmc->block_dev.block_read = mmc_bread;
mmc->block_dev.block_write = mmc_bwrite;
mmc->block_dev.block_erase = mmc_berase;
等赋值语句,其实就是通过给block_dev.block_read、block_dev.block_write、block_dev.block_erase赋值。

从而给出了读、写、擦除块的统一访问方式,要不然必须直接(其实也可以)调用mmc_bread、mmc_bwrite、

mmc_berase。注意这里的 mmc_bread、mmc_bwrite、mmc_berase并不属于struct mmc。

同样

mmc->send_cmd = mmc_send_cmd;
mmc->set_ios = mmc_set_ios;
mmc->init = mmc_core_init;

中的mmc_send_cmd、mmc_set_ios、mmc_core_init;并不属于struct mmc。可以看到struct mmc的作用

只是起到具体的连接几个函数的作用。比如把block_read连接到mmc_bread;而mmc_bread又调用

mmc_read_blocks,而mmc_read_blocks又调用mmc_send_cmd,需要注意的是mmc_send_cmd是操作

具体的MMC控制器的,刚才说了block_read是可以选择具体的MMC控制器的,那么怎么选择呢?这当中就是

通过mmc这个结构体,可以看到整个流程的每个函数都需要参数struct mmc,而这个mmc可以通过设备号

查找得到,从而就选择了某个mmc,由于mmc的mmc_initialize(gd->bd)过程已经把mmc设备号绑定到具体的

MMC控制器上,但是可以重新绑定的,用户需要注意的就是在选在实际SD卡0,的时候有可能程序中是find设备

好为3的mmc(是由mmc_initialize(gd->bd)造成的)。

继续分析INIT_LIST_HEAD (&mmc->link);list_add_tail (&mmc->link, &mmc_devices);

不用说了,系统是通过链表把各个设备的驱动联系起来的,所以要有链表的操作。

这里需要注意的是mmc->block_dev.dev = cur_dev_num++;给了mmc->block_dev.dev 一个设备号。

所以在寻找设备时,可以通过设备号。比如spl_mmc_load_image函数中

/* We register only one device. So, the dev id is always 0 */
mmc = find_mmc_device(0);

意思就很明白了。


告一段落,返回spl_mmc_load_image中,接下来是mmc = find_mmc_device(0);不用说了吧。

err = mmc_init(mmc);刚开始看到的时候好郁闷,怎么又是init?那么分析一下吧

int mmc_init(struct mmc *mmc)
{
	int err = IN_PROGRESS;
	unsigned start = get_timer(0);

	if (mmc->has_init)
		return 0;
	if (!mmc->init_in_progress)
		err = mmc_start_init(mmc);

	if (!err || err == IN_PROGRESS)
		err = mmc_complete_init(mmc);
	debug("%s: %d, time %lu\n", __func__, err, get_timer(start));
	return err;
}
主要的是mmc_start_init(mmc)、mmc_complete_init(mmc)。

int mmc_start_init(struct mmc *mmc)
{
	int err;

	if (mmc_getcd(mmc) == 0) {
		mmc->has_init = 0;
#if !defined(CONFIG_SPL_BUILD) || defined(CONFIG_SPL_LIBCOMMON_SUPPORT)
		printf("MMC: no card present\n");
#endif
		return NO_CARD_ERR;
	}

	if (mmc->has_init)
		return 0;

	err = mmc->init(mmc);

	if (err)
		return err;

	mmc_set_bus_width(mmc, 1);
	mmc_set_clock(mmc, 1);

	/* Reset the Card */
	err = mmc_go_idle(mmc);

	if (err)
		return err;

	/* The internal partition reset to user partition(0) at every CMD0*/
	mmc->part_num = 0;

	/* Test for SD version 2 */
	err = mmc_send_if_cond(mmc);

	/* Now try to get the SD card's operating condition */
	err = sd_send_op_cond(mmc);

	/* If the command timed out, we check for an MMC card */
	if (err == TIMEOUT) {
		err = mmc_send_op_cond(mmc);

		if (err && err != IN_PROGRESS) {
#if !defined(CONFIG_SPL_BUILD) || defined(CONFIG_SPL_LIBCOMMON_SUPPORT)
			printf("Card did not respond to voltage select!\n");
#endif
			return UNUSABLE_ERR;
		}
	}

	if (err == IN_PROGRESS)
		mmc->init_in_progress = 1;

	return err;
}
看到这里大概就明白了,这其实是涉及到SD卡的协议的,回想一下,其实刚才的mmc_initialize(gd->bd)

做的工作其实都只是为访问SD卡而做的数据结构的初始化和a20的mmc控制器、管脚的初始化,这里的

mmc_init(mmc);其实是按照SD协议去初始化SD卡,恍然大迷瞪。

mmc_complete_init()就不说了。

返回到void spl_mmc_load_image(void)中,接下来boot_mode = spl_boot_mode();

前面已经分析过了,会返回boot_mode == MMCSD_MODE_RAW。

所以mmc_load_image_raw(mmc,CONFIG_SYS_MMCSD_RAW_MODE_U_BOOT_SECTOR);

我去,这才是真正的开始加载uboot代码。

下一篇再分析吧,有点累

你可能感兴趣的:(Cubietruck)