Linux驱动开发:uboot启动流程详解

news2024/12/24 2:39:59

前言:uboot作为Linux驱动开发的 “三巨头” 之一,绝对是一座绕不开的大山。当然,即使不去细致了解uboot启动流程依旧不影响开发者对uboot的简单移植。但秉持着知其然知其所以然的学习态度,作者将给读者朋友细致化的过一遍uboot启动流程(考虑到硬件平台与uboot版本不一致,实际情况可能有些许出入)

实验硬件:imx6ull;uboot版本:2016.03 

想深挖uboot的启动流程就需要从uboot的链接脚本入手(程序的入口:程序执行的第一条指令被称为程序的入口,这个入口通常就是在链接脚本指定的),打开 arch/arm/cpu/armv7/u-boot.lds 这个文件(下载uboot源码后进行编译得到lds链接脚本):

#include <config.h>
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)    //当前程序的入口函数,定义在arch/arm/lib/vectors.S
SECTIONS
{
#if defined(CONFIG_ARMV7_SECURE_BASE) && defined(CONFIG_ARMV7_NONSEC)
	/DISCARD/ : { *(.rel._secure*) }
#endif
	. = 0x00000000;
	. = ALIGN(4);
	.text :
	{
      *(.__image_copy_start)//uboot 拷贝的首地址
      *(.vectors)//vectors 段保存中断向量表
      arch/arm/cpu/armv7/start.o (.text*)//将 arch/arm/cpu/armv7/start.s 编译出来的代码放到        中断向量表后面
      *(.text*)//text 段,其他的代码段就放到这里
	}
 . = ALIGN(4);
 .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
 . = ALIGN(4);
 .data : {
  *(.data*)
 }
 . = ALIGN(4);
 . = .;
 . = ALIGN(4);
 .u_boot_list : {
  KEEP(*(SORT(.u_boot_list*)));
 }
 . = ALIGN(4);
 .image_copy_end :
 {
  *(.__image_copy_end)//uboot 拷贝的结束地址
 }
 .rel_dyn_start :
 {
  *(.__rel_dyn_start)//.rel.dyn 段起始地址
 }
 .rel.dyn : {
  *(.rel*)
 }
 .rel_dyn_end :
 {
  *(.__rel_dyn_end)//.rel.dyn 段结束地址
 }
 .end :
 {
  *(.__end)
 }
 _image_binary_end = .;//镜像结束地址
 . = ALIGN(4096);
 .mmutable : {
  *(.mmutable)
 }
 .bss_start __rel_dyn_start (OVERLAY) : {
  KEEP(*(.__bss_start));//.bss 段起始地址
  __bss_base = .;
 }
 .bss __bss_base (OVERLAY) : {
  *(.bss*)
   . = ALIGN(4);
   __bss_limit = .;
 }
 .bss_end __bss_limit (OVERLAY) : {
  KEEP(*(.__bss_end));//.bss 段结束地址
 }
 .dynsym _image_binary_end : { *(.dynsym) }
 .dynbss : { *(.dynbss) }
 .dynstr : { *(.dynstr*) }
 .dynamic : { *(.dynamic*) }
 .plt : { *(.plt*) }
 .interp : { *(.interp*) }
 .gnu.hash : { *(.gnu.hash) }
 .gnu : { *(.gnu*) }
 .ARM.exidx : { *(.ARM.exidx*) }
 .gnu.linkonce.armexidx : { *(.gnu.linkonce.armexidx.*) }
}

通过查看链接脚本u-boot.lds知道入口点是 arch/arm/lib/vectors.S 文件中的_start函数。

一、uboot总体启动流程

作者将uboot的总体启动流程分为2部分:arch级初始化(架构)板级初始化

二、arch级初始化

如下图所示,uboot 程序的入口函数_start位于arch/arm/lib/vector.S文件中,_start 开始的是中断向量表,随后通过 b reset 函数跳转到 reset 函数, reset 函数在 arch/arm/cpu/armv7/start.S 里面。 reset 函数跳转到了 save_boot_params 函数,save_boot_params 函数又跳转到 save_boot_params_ret 函数,该函数主要分为五个功能:

①、设置CPU处于SVC特权模式,并且关闭FIQ和IRQ两个中断;
②、设置向量表重定位;
③、设置cp15寄存器(禁止Cache,MMU,TLBs);
④、配置关键寄存器和初始化
⑤、跳转_main,进入板级初始化

补充说明:_start 开始是中断向量表,这段话语部分读者可能存在疑惑,因为自己看 uboot 代码流程发现: _start 进入之后就紧跟着 b reset,并未出现定义中断向量表。其实,大部分开发板的中断向量表是固定定义在 0x0000000 开始的地址上的,比如 reset中断 的地址是 0x00000000

2.1 u-boot.lds链接代码

 打开 arch/arm/cpu/armv7/u-boot.lds 这个文件, ENTRY 进入 _start ,代码具体如下:

#include <config.h>
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
#if defined(CONFIG_ARMV7_SECURE_BASE) && defined(CONFIG_ARMV7_NONSEC)
	/DISCARD/ : { *(.rel._secure*) }
#endif
	. = 0x00000000;

	. = ALIGN(4);
	.text :
	{
		*(.__image_copy_start)
		*(.vectors)
		CPUDIR/start.o (.text*)
		*(.text*)
	}

在 arch/arm/lib/vector.S 中寻找到 _start 定义,并且发现跳转到了 reset

#include <config.h>
.globl _start
    .section ".vectors", "ax"
_start:
#ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
    .word   CONFIG_SYS_DV_NOR_BOOT_CFG
#endif
    b   reset
    ldr pc, _undefined_instruction
    ldr pc, _software_interrupt
    ldr pc, _prefetch_abort
    ldr pc, _data_abort
    ldr pc, _not_used
    ldr pc, _irq
    ldr pc, _fiq
,,,,

2.2 reset

reset 函数存在于 arch/arm/cpu/armv7/start.s 文件中,而 reset 函数跳转到 save_boot_params 函数save_boot_params 函数又跳转到 save_boot_params_ret 中,具体如下:

//第一段:reset跳转到save_boot_params	
    .globl	reset
	.globl	save_boot_params_ret

reset:
	/* Allow the board to save important registers */
	b	save_boot_params
save_boot_params_ret:
	/*
	 * disable interrupts (FIQ and IRQ), also set the cpu to SVC32 mode,
	 * except if in HYP mode already
	 */
	mrs	r0, cpsr
	and	r1, r0, #0x1f		@ mask mode bits
	teq	r1, #0x1a		@ test for HYP mode
	bicne	r0, r0, #0x1f		@ clear all mode bits
	orrne	r0, r0, #0x13		@ set SVC mode
	orr	r0, r0, #0xc0		@ disable FIQ and IRQ
	msr	cpsr,r0

//第二段:save_boot_params跳转到save_boot_params_ret
    ENTRY(save_boot_params)
	    b	save_boot_params_ret		@ back to my caller
    ENDPROC(save_boot_params)
	    .weak	save_boot_params

2.3 cpsr和cp15 STCLR

 reset 函数跳转到 save_boot_params_ret 函数后,具体如下:

save_boot_params_ret:
	/*
	 * 关闭 FIQ 和 IRQ 中断,设置 CPU 处于 SVC 特权模式
	 * except if in HYP mode already(除非已经处于HYP特权模式,比SVC低一点的特权)
	 */
	mrs	r0, cpsr
	and	r1, r0, #0x1f		@ mask mode bits
	teq	r1, #0x1a		@ test for HYP mode
	bicne	r0, r0, #0x1f		@ clear all mode bits
	orrne	r0, r0, #0x13		@ set SVC mode
	orr	r0, r0, #0xc0		@ disable FIQ and IRQ
	msr	cpsr,r0

    /*
     *★SCTLR寄存器bit13清零(bit13控制中断向量表地址,0:可以重定位;1:默认为0xFFFF0000)将_start 
     *的数值写入该寄存器,也就是中断向量表的起始地址为0x87800000
     */
#if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD))
	/* Set V=0 in CP15 SCTLR register - for VBAR to point to vector */
	mrc	p15, 0, r0, c1, c0, 0	@ Read CP15 SCTLR Register
	bic	r0, #CR_V		@ V = 0
	mcr	p15, 0, r0, c1, c0, 0	@ Write CP15 SCTLR Register

	/* Set vector address in CP15 VBAR register */
	ldr	r0, =_start
	mcr	p15, 0, r0, c12, c0, 0	@Set VBAR
#endif

	/* the mask ROM code should have PLL and others stable */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
	bl	cpu_init_cp15
	bl	cpu_init_crit
#endif

	bl	_main

从上述代码能看出,代码包含5个部分:cpsr,cp15,cpu_init_cp15,cpu_init_crit_main

2.3.1 cpsr

/*
 * disable interrupts (FIQ and IRQ), also set the cpu to SVC32 mode,
 * except if in HYP mode already
 */
mrs	r0, cpsr
and	r1, r0, #0x1f		@ mask mode bits
teq	r1, #0x1a		@ test for HYP mode
bicne	r0, r0, #0x1f		@ clear all mode bits
orrne	r0, r0, #0x13		@ set SVC mode
orr	r0, r0, #0xc0		@ disable FIQ and IRQ
msr	cpsr,r0

cpsr:给目标寄存器写入对应的数值(可以参考芯片手册),关闭 FIQ 和 IRQ,并且讲CPU设置为 SVC 特权模式。

为什么这样操作的原因:

(1) 禁止 FIQ 和 IRQ:uboot 作为芯片上电的第一道程序是至关重要的,而中断是很危险的,它的等级太高了,可以轻松打断程序的正常运行。所以,为了保证uboot的正常运行,需要关闭部分中断。

(2) 设置 SVC:uboot的运行避免不了对各类寄存器等操作,为了保证操作的正常,需要给CPU设置高特权模式。

2.3.2 cp15

/*
 * Setup vector:
 * (OMAP4 spl TEXT_BASE is not 32 byte aligned.
 * Continue to use ROM code vector only in OMAP4 spl)
 */
#if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD))
	/* Set V=0 in CP15 SCTLR register - for VBAR to point to vector */
	mrc	p15, 0, r0, c1, c0, 0	@ Read CP15 SCTLR Register
	bic	r0, #CR_V		@ V = 0
	mcr	p15, 0, r0, c1, c0, 0	@ Write CP15 SCTLR Register

	/* Set vector address in CP15 VBAR register */
	ldr	r0, =_start
	mcr	p15, 0, r0, c12, c0, 0	@Set VBAR
#endif

cp15 SCTLR:SCTLR寄存器bit13清零(bit13控制中断向量表地址,0:可以重定位;1:默认为0xFFFF0000)将_start的数值写入该寄存器,也就是中断向量表的起始地址为0x87800000

2.4 cpu_init_cp15

cpu_init_cp15 存在于 arch/arm/cpu/armv7/start.s 文件中:

/*************************************************************************
 *
 * cpu_init_cp15
 *
 * Setup CP15 registers (cache, MMU, TLBs). The I-cache is turned on unless
 * CONFIG_SYS_ICACHE_OFF is defined.
 *
 *************************************************************************/
ENTRY(cpu_init_cp15)
	/*
	 * Invalidate L1 I/D
	 */
    mov	r0, #0			@ set up for MCR
	mcr	p15, 0, r0, c8, c7, 0	@ invalidate TLBs  /*禁止从TLB中取地址描述符,也就是禁止虚拟地址到物理地址的转换,因为刚开始操作的都是物理寄存器!*/
	mcr	p15, 0, r0, c7, c5, 0	@ invalidate icache 	/*关闭指令cache*/
	mcr	p15, 0, r0, c7, c5, 6	@ invalidate BP array	/*关闭分支预测*/
	mcr p15, 0, r0, c7, c10, 4	@ DSB	 /*多核cpu之间进行数据同步*/
	mcr p15, 0, r0, c7, c5, 4	@ ISB /*进行指令同步,放弃流水线中已经取到的指令,重新取指令*/


	/*
	 * disable MMU stuff and caches
	 */
	mrc	p15, 0, r0, c1, c0, 0
	bic	r0, r0, #0x00002000	@ clear bits 13 (--V-)
	bic	r0, r0, #0x00000007	@ clear bits 2:0 (-CAM)
	orr	r0, r0, #0x00000002	@ set bit 1 (--A-) Align
	orr	r0, r0, #0x00000800	@ set bit 11 (Z---) BTB
#ifdef CONFIG_SYS_ICACHE_OFF
	bic	r0, r0, #0x00001000	@ clear bit 12 (I) I-cache
#else
	orr	r0, r0, #0x00001000	@ set bit 12 (I) I-cache
#endif
	mcr	p15, 0, r0, c1, c0, 0

#ifdef CONFIG_ARM_ERRATA_716044
	mrc	p15, 0, r0, c1, c0, 0	@ read system control register
	orr	r0, r0, #1 << 11	@ set bit #11
	mcr	p15, 0, r0, c1, c0, 0	@ write system control register
#endif

#if (defined(CONFIG_ARM_ERRATA_742230) || defined(CONFIG_ARM_ERRATA_794072))
	mrc	p15, 0, r0, c15, c0, 1	@ read diagnostic register
	orr	r0, r0, #1 << 4		@ set bit #4
	mcr	p15, 0, r0, c15, c0, 1	@ write diagnostic register
#endif

#ifdef CONFIG_ARM_ERRATA_743622
	mrc	p15, 0, r0, c15, c0, 1	@ read diagnostic register
	orr	r0, r0, #1 << 6		@ set bit #6
	mcr	p15, 0, r0, c15, c0, 1	@ write diagnostic register
#endif

#ifdef CONFIG_ARM_ERRATA_751472
	mrc	p15, 0, r0, c15, c0, 1	@ read diagnostic register
	orr	r0, r0, #1 << 11	@ set bit #11
	mcr	p15, 0, r0, c15, c0, 1	@ write diagnostic register
#endif
#ifdef CONFIG_ARM_ERRATA_761320
	mrc	p15, 0, r0, c15, c0, 1	@ read diagnostic register
	orr	r0, r0, #1 << 21	@ set bit #21
	mcr	p15, 0, r0, c15, c0, 1	@ write diagnostic register
#endif
#ifdef CONFIG_ARM_ERRATA_845369
	mrc	p15, 0, r0, c15, c0, 1	@ read diagnostic register
	orr	r0, r0, #1 << 22	@ set bit #22
	mcr	p15, 0, r0, c15, c0, 1	@ write diagnostic register
#endif

	mov	r5, lr			@ Store my Caller
	mrc	p15, 0, r1, c0, c0, 0	@ r1 has Read Main ID Register (MIDR)
	mov	r3, r1, lsr #20		@ get variant field
	and	r3, r3, #0xf		@ r3 has CPU variant
	and	r4, r1, #0xf		@ r4 has CPU revision
	mov	r2, r3, lsl #4		@ shift variant field for combined value
	orr	r2, r4, r2		@ r2 has combined CPU variant + revision

#ifdef CONFIG_ARM_ERRATA_798870
	cmp	r2, #0x30		@ Applies to lower than R3p0
	bge	skip_errata_798870      @ skip if not affected rev
	cmp	r2, #0x20		@ Applies to including and above R2p0
	blt	skip_errata_798870      @ skip if not affected rev

	mrc	p15, 1, r0, c15, c0, 0  @ read l2 aux ctrl reg
	orr	r0, r0, #1 << 7         @ Enable hazard-detect timeout
	push	{r1-r5}			@ Save the cpu info registers
	bl	v7_arch_cp15_set_l2aux_ctrl
	isb				@ Recommended ISB after l2actlr update
	pop	{r1-r5}			@ Restore the cpu info - fall through
skip_errata_798870:
#endif

#ifdef CONFIG_ARM_ERRATA_801819
	cmp	r2, #0x24		@ Applies to lt including R2p4
	bgt	skip_errata_801819      @ skip if not affected rev
	cmp	r2, #0x20		@ Applies to including and above R2p0
	blt	skip_errata_801819      @ skip if not affected rev
	mrc	p15, 0, r0, c0, c0, 6	@ pick up REVIDR reg
	and	r0, r0, #1 << 3		@ check REVIDR[3]
	cmp	r0, #1 << 3
	beq	skip_errata_801819	@ skip erratum if REVIDR[3] is set

	mrc	p15, 0, r0, c1, c0, 1	@ read auxilary control register
	orr	r0, r0, #3 << 27	@ Disables streaming. All write-allocate
					@ lines allocate in the L1 or L2 cache.
	orr	r0, r0, #3 << 25	@ Disables streaming. All write-allocate
					@ lines allocate in the L1 cache.
	push	{r1-r5}			@ Save the cpu info registers
	bl	v7_arch_cp15_set_acr
	pop	{r1-r5}			@ Restore the cpu info - fall through
skip_errata_801819:
#endif

#ifdef CONFIG_ARM_ERRATA_454179
	cmp	r2, #0x21		@ Only on < r2p1
	bge	skip_errata_454179

	mrc	p15, 0, r0, c1, c0, 1	@ Read ACR
	orr	r0, r0, #(0x3 << 6)	@ Set DBSM(BIT7) and IBE(BIT6) bits
	push	{r1-r5}			@ Save the cpu info registers
	bl	v7_arch_cp15_set_acr
	pop	{r1-r5}			@ Restore the cpu info - fall through

skip_errata_454179:
#endif

#ifdef CONFIG_ARM_ERRATA_430973
	cmp	r2, #0x21		@ Only on < r2p1
	bge	skip_errata_430973

	mrc	p15, 0, r0, c1, c0, 1	@ Read ACR
	orr	r0, r0, #(0x1 << 6)	@ Set IBE bit
	push	{r1-r5}			@ Save the cpu info registers
	bl	v7_arch_cp15_set_acr
	pop	{r1-r5}			@ Restore the cpu info - fall through

skip_errata_430973:
#endif

#ifdef CONFIG_ARM_ERRATA_621766
	cmp	r2, #0x21		@ Only on < r2p1
	bge	skip_errata_621766

	mrc	p15, 0, r0, c1, c0, 1	@ Read ACR
	orr	r0, r0, #(0x1 << 5)	@ Set L1NEON bit
	push	{r1-r5}			@ Save the cpu info registers
	bl	v7_arch_cp15_set_acr
	pop	{r1-r5}			@ Restore the cpu info - fall through

skip_errata_621766:
#endif

	mov	pc, r5			@ back to my caller
ENDPROC(cpu_init_cp15)

cpu_init_cp15:关闭MMU和Cache

为什么这样操作的原因:

(1) 关闭MMU:因为MMU是把虚拟地址转化为物理地址得作用而我们现在是要设置控制寄存器,而控制寄存器本来就是实地址(物理地址),再使能MMU,不就是多此一举了吗?

(2) 关闭cache:cache和MMU是通过CP15管理的,刚上电的时候,CPU还不能管理他们。所以上电的时候MMU必须关闭,指令cache可关闭,可不关闭,但数据cache一定要关闭否则可能导致刚开始的代码里面,去取数据的时候,从cache里面取,而这时候RAM中数据还没有cache过来,导致数据预取异常。

2.5 cpu_init_crit

cpu_init_crit 存在于 arch/arm/cpu/armv7/start.s 文件中,cpu_init_crit 代码比较简单:b    lowlevel_init。所以,直接去研究 lowlevel_init 函数lowlevel_init 函数存在 arch/arm/cpu/armv7/lowlevel_init.S 文件中:

ENTRY(lowlevel_init)
	/*
	 * Setup a temporary stack. Global data is not available yet.
	 */
	ldr	sp, =CONFIG_SYS_INIT_SP_ADDR
	bic	sp, sp, #7 /* 8-byte alignment for ABI compliance */
#ifdef CONFIG_SPL_DM
	mov	r9, #0
#else
	/*
	 * Set up global data for boards that still need it. This will be
	 * removed soon.
	 */
#ifdef CONFIG_SPL_BUILD
	ldr	r9, =gdata
#else
	sub	sp, sp, #GD_SIZE
	bic	sp, sp, #7
	mov	r9, sp
#endif
#endif
	/*
	 * Save the old lr(passed in ip) and the current lr to stack
	 */
	push	{ip, lr}
	/*
	 * Call the very early init function. This should do only the
	 * absolute bare minimum to get started. It should not:
	 *
	 * - set up DRAM
	 * - use global_data
	 * - clear BSS
	 * - try to start a console
	 *
	 * For boards with SPL this should be empty since SPL can do all of
	 * this init in the SPL board_init_f() function which is called
	 * immediately after this.
	 */
	bl	s_init
	pop	{ip, pc}
ENDPROC(lowlevel_init)

考虑到 cpu_init_crit 函数是 arch 级初始化中重要的函数,所以,作者这里详解给大家分析一下。

设置SP指向CONFIG_SYS_INIT_SP_ADDR=0X0091FF00,流程见上图。可结合下图自己分析

设置SP 8字节对齐
设置gd(global data)大小为248B
sp地址(0x0091FE08)保存在r9寄存器中

将ip和lr入栈
跳转s_init //对于IMX6ULL而言,什么都没有做(空函数)
将入栈的ip和lr出栈,并将lr赋给pc

三、板级初始化

_main 函数存在于 arch/arm/lib/crt0.S 中,_main 包含六个部分,简单来说:主要是把ROM中的程序拷贝至DDR(加快uboot的运行),然后载重定向,最后初始化各种外设。_mian 函数具体如下:

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
#if defined(CONFIG_CPU_V7M)	/* v7M forbids using SP as BIC destination */
	mov	r3, sp
	bic	r3, r3, #7
	mov	sp, r3
#else
	bic	sp, sp, #7	/* 8-byte alignment for ABI compliance */
#endif
	mov	r0, sp
	bl	board_init_f_alloc_reserve
	mov	sp, r0
	/* set up gd here, outside any C code */
	mov	r9, r0
	bl	board_init_f_init_reserve

	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 */
#if defined(CONFIG_CPU_V7M)	/* v7M forbids using SP as BIC destination */
	mov	r3, sp
	bic	r3, r3, #7
	mov	sp, r3
#else
	bic	sp, sp, #7	/* 8-byte alignment for ABI compliance */
#endif
	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
#if defined(CONFIG_CPU_V7M)
	orr	lr, #1				/* As required by Thumb-only */
#endif
	ldr	r0, [r9, #GD_RELOCADDR]		/* r0 = gd->relocaddr */
	b	relocate_code
here:
/*
 * now relocate vectors
 */

	bl	relocate_vectors

/* Set up final (full) environment */

	bl	c_runtime_cpu_setup	/* we still call old routine here */
#endif
#if !defined(CONFIG_SPL_BUILD) || defined(CONFIG_SPL_FRAMEWORK)
# ifdef CONFIG_SPL_BUILD
	/* Use a DRAM stack for the rest of SPL, if requested */
	bl	spl_relocate_stack_gd
	cmp	r0, #0
	movne	sp, r0
	movne	r9, r0
# endif
	ldr	r0, =__bss_start	/* this is auto-relocated! */

#ifdef CONFIG_USE_ARCH_MEMSET
	ldr	r3, =__bss_end		/* this is auto-relocated! */
	mov	r1, #0x00000000		/* prepare zero to clear BSS */

	subs	r2, r3, r0		/* r2 = memset len */
	bl	memset
#else
	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 */
#if defined(CONFIG_CPU_V7M)
	itt	lo
#endif
	strlo	r2, [r0]		/* clear 32-bit BSS word */
	addlo	r0, r0, #4		/* move to next */
	blo	clbss_l
#endif

#if ! defined(CONFIG_SPL_BUILD)
	bl coloured_LED_init
	bl red_led_on
#endif
	/* 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 */
#if defined(CONFIG_SYS_THUMB_BUILD)
	ldr	lr, =board_init_r	/* this is auto-relocated! */
	bx	lr
#else
	ldr	pc, =board_init_r	/* this is auto-relocated! */
#endif
	/* we should not return here. */
#endif

ENDPROC(_main)

3.1 board_init_f_alloc_reserve

board_init_f_alloc_reserve 函数位于 common/init/board_init.c 文件中,具体如下:

ulong board_init_f_alloc_reserve(ulong top)
{
	/* Reserve early malloc arena */
#if defined(CONFIG_SYS_MALLOC_F)
	top -= CONFIG_SYS_MALLOC_F_LEN;
#endif
	/* LAST : reserve GD (rounded up to a multiple of 16 bytes) */
	top = rounddown(top-sizeof(struct global_data), 16);

	return top;
}

board_init_f_alloc_reserve 函数目的:留出早期的 malloc 内存区域和 gd 内存区域,其中CONFIG_SYS_MALLOC_F_LEN=0X400( 在文件 include/generated/autoconf.h 中定义 ) ,sizeof(struct global_data)=248(GD_SIZE 值),完成以后的内存分布如图所示(top返回地址为:0x0091FA00):

3.2 board_init_f_init_reserve

board_init_f_init_reserve 函数同样位于common/init/board_init.c 文件中,具体如下:

void board_init_f_init_reserve(ulong base)
{
	struct global_data *gd_ptr;
#ifndef _USE_MEMCPY
	int *ptr;
#endif

	/*
	 * clear GD entirely and set it up.
	 * Use gd_ptr, as gd may not be properly set yet.
	 */

	gd_ptr = (struct global_data *)base;
	/* zero the area */
#ifdef _USE_MEMCPY
	memset(gd_ptr, '\0', sizeof(*gd));
#else
	for (ptr = (int *)gd_ptr; ptr < (int *)(gd_ptr + 1); )
		*ptr++ = 0;
#endif
	/* set GD unless architecture did it already */
#if !defined(CONFIG_ARM)
	arch_setup_gd(gd_ptr);
#endif
	/* next alloc will be higher by one GD plus 16-byte alignment */
	base += roundup(sizeof(struct global_data), 16);

	/*
	 * record early malloc arena start.
	 * Use gd as it is now properly set for all architectures.
	 */

#if defined(CONFIG_SYS_MALLOC_F)
	/* go down one 'early malloc arena' */
	gd->malloc_base = base;
	/* next alloc will be higher by one 'early malloc arena' size */
	base += CONFIG_SYS_MALLOC_F_LEN;
#endif
}

board_init_f_init_reserve 函数目的:初始化gd(全局变量)

3.3 board_init_f

board_init_f 函数定义在文件 common/board_f.c 中,具体代码如下:

void board_init_f(ulong boot_flags)
{
#ifdef CONFIG_SYS_GENERIC_GLOBAL_DATA
	/*
	 * For some archtectures, global data is initialized and used before
	 * calling this function. The data should be preserved. For others,
	 * CONFIG_SYS_GENERIC_GLOBAL_DATA should be defined and use the stack
	 * here to host global data until relocation.
	 */
	gd_t data;

	gd = &data;

	/*
	 * Clear global data before it is accessed at debug print
	 * in initcall_run_list. Otherwise the debug print probably
	 * get the wrong vaule of gd->have_console.
	 */
	zero_global_data();
#endif

	gd->flags = boot_flags;
	gd->have_console = 0;

	if (initcall_run_list(init_sequence_f))
		hang();

#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
		!defined(CONFIG_EFI_APP)
	/* NOTREACHED - jump_to_copy() does not return */
	hang();
#endif

	/* Light up LED1 */
	imx6_light_up_led1();
}
board_init_f 函数主要有两个工作:
1、 初始化一系列外设,比如串口、定时器,或者打印一些消息等。
2、初始化 gd 的各个成员变量,uboot 会将自己重定位到 DRAM 最后面的地址区域,也就
是将自己拷贝到 DRAM 最后面的内存区域中。

board_init_f 函数中的 initcall_run_list 函数主要用于调用一系列函数,值保存在 init_sequence_f 函数中。 

initcall_run_list 函数的具体主要功能如下(可结合上图分析):

1、gd->num_len=_bss_end-_start//uboot image大小,即代码长度,0X878A8E74-0x87800000=0XA8EF4
2、initf_malloc()//gd->malloc=CONFIG_SYS_MALLOC_F_LEN=0X400,内存池大小为0x400
3、arch_cpu_init()//初始化架构相关的内容,CPU级别操作
4、 initf_dm()//驱动模型一些初始化
5、 board_early_init_f()//初始化串口的IO配置(I.MX6ULL)
6、 timer_init()//初始化定时器(Cortex-A7内核)
…
14、 init_baud_rate()//根据环境变量baudrate设置gd->baudrate=115200
…
24、dram_init()//设置gd->ram_size=512MB 0X2000 0000B
…
44、set_up_addr()//设置地址gd->ram_zise=0X2000 0000;gd->ram_top=0XA0000000
(0X80000000+0X2000 0000);gd->relocadder=0XA0000000(重定位后最高地址)…
...
48、reserve_uboot//gd->mon_len=0X8EF4;gd->start_addr_sp=
0X9FF47000;gd->relocadder=0X9FF47000//uboot重定位后的起始地址
49、reserve_malloc//TOTAL_MALLOC_LEN=CONFIG_SYS_MALLOC_LEN(0X10000000B=16MB)+
CONFIG_ENV_SIZE(0X2000=8K)
50、reserve_board()//留出板子bd所占的内存区
52、reserve_global_data()//留出gd所占的内存区
…
55、resreve_stacks//留出栈空间,gd->start_addr_sp-16,然后16字节对齐
…
最终sp=gd->start_addr_sp=0X9EF44E90
…
61、setup_reloc//设置gd其他一些成员变量,供后面定位使用,并且将以前的gd拷贝到gd->new_gd处

最终uboot重定位后偏移为0X18747000(0X9FF47000-0X87800000),新的gd首地址0X9EF44EB8,新的sp首地址0X9EF44E90

3.4 relocate_code

relocate_code 函数主要用于代码拷贝,在 relocate_code 函数之前还有语句 ldr r0,[r9,#GD_RELOCADDR],即r0=gd-> relocaddr= 0X9FF47000, uboot 重定位后的首地址。
relocate_code 函数在 arch/arm/lib/relocate.S 中,下面结合代码分析该函数

ENTRY(relocate_code)    
    ldr r1,=__image_copy_start//r1=0X8780000源地址起始地址
    subs r4, r0, r1//r4=0X9FF47000-0X87800000=0X18747000 偏移
    beq	relocate_done		/* skip relocation */
    ldr r2, =__image_copy_end//r2=0X8785dc6c源地址结束地址

copy_loop: //拷贝,将uboot从源地址0X8780000拷贝至0X9FF47000
	ldmia	r1!, {r10-r11}		/* copy from source address [r1]    */
	stmia	r0!, {r10-r11}		/* copy to   target address [r0]    */
	cmp	r1, r2			/* until source end address [r2]    */
	blo	copy_loop

	/*
	 * fix .rel.dyn relocations
	 */
	ldr	r2, =__rel_dyn_start	/* r2 <- SRC &__rel_dyn_start */
	ldr	r3, =__rel_dyn_end	/* r3 <- SRC &__rel_dyn_end */
fixloop:
	ldmia	r2!, {r0-r1}		/* (r0,r1) <- (SRC location,fixup) */
	and	r1, r1, #0xff
	cmp	r1, #23			/* relative fixup? */
	bne	fixnext

	/* relative fix: increase location by offset */
	add	r0, r0, r4
	ldr	r1, [r0]
	add	r1, r1, r4
	str	r1, [r0]
fixnext:
	cmp	r2, r3
	blo	fixloop

relocate_done:

#ifdef __XSCALE__
	/*
	 * On xscale, icache must be invalidated and write buffers drained,
	 * even with cache disabled - 4.2.7 of xscale core developer's manual
	 */
	mcr	p15, 0, r0, c7, c7, 0	/* invalidate icache */
	mcr	p15, 0, r0, c7, c10, 4	/* drain write buffer */
#endif

	/* ARMv4- don't know bx lr but the assembler fails to see that */

#ifdef __ARM_ARCH_4__
	mov	pc, lr
#else
	bx	lr
#endif

ENDPROC(relocate_code)

注意:直接将uboot从0X87800000拷贝至其他地方后,函数调用、全局变量引用可能会出问题。uboot采用位置无关码来处理该类问题(简单说采用相对地址寻址,而不是采用绝对地址寻址,并且重定位后需要将Label+offset)。在使用 ld 进行链接的时候使用选项”- pie” 生成位置无关的可执行文件。具体为.rel.dyn段。 

核心代码:
	/*
	 * fix .rel.dyn relocations
	 */
	ldr	r2, =__rel_dyn_start	/* r2 <- SRC &__rel_dyn_start */
	ldr	r3, =__rel_dyn_end	/* r3 <- SRC &__rel_dyn_end */
fixloop:
	ldmia	r2!, {r0-r1}		/* (r0,r1) <- (SRC location,fixup) */
	and	r1, r1, #0xff
	cmp	r1, #23			/* relative fixup? */
	bne	fixnext

	/* relative fix: increase location by offset */
	add	r0, r0, r4
	ldr	r1, [r0]
	add	r1, r1, r4
	str	r1, [r0]
fixnext:
	cmp	r2, r3
	blo	fixloop

核心代码程序分析如下:

1、r2=__rel_dyn_start,也就是.rel.dyn 段的起始地址。
2、r3=__rel_dyn_end,也就是.rel.dyn 段的终止地址。
3、从.rel.dyn 段起始地址开始,每次读取两个 4 字节的数据存放到 r0 和 r1 寄存器中, r0 存放低 4 字节的数据,也就是 Label 地址; r1 存放高 4 字节的数据,也就是 Label 标志。
4、r1 中给的值与 0xff 进行与运算,其实就是取 r1 的低 8 位。
5、判断 r1 中的值是否等于 23(0X17)。//0X17就是判断是否是Label
6、如果 r1 不等于 23 的话就说明不是描述 Label 的,执行函数 fixnext ,否则的话继续执行下面的代码。
7、r0 保存着 Label 值, r4 保存着重定位后的地址偏移, r0+r4 就得到了重定位后的Label值。此时 r0 保存着重定位后的 Label 值。
8、读取重定位后 Label 所保存的变量地址,此时这个变量地址还是重定位前的地址,将得到的值放到 r1 寄存器中。
9、 r1+r4 即可得到重定位后的变量地址 。
10、重定位后的变量地址写入到重定位后的 Label 中。
11、比较 r2 和 r3,查看.rel.dyn 段重定位是否完成。
12、如果 r2 和 r3 不相等,说明.rel.dyn 重定位还未完成,因此跳到 fixloop 继续重定位 .rel.dyn 段。

3.5 relocate_vector

relocate_vectors 函数同样位于 arch/arm/lib/relocate.S 中,relocate_vectors 函数的目的:重定位中断向量表(设置VBAR寄存器为重定位后的中断向量表起始地址)

ENTRY(relocate_vectors)

#ifdef CONFIG_CPU_V7M
	/*
	 * On ARMv7-M we only have to write the new vector address
	 * to VTOR register.
	 */
	ldr	r0, [r9, #GD_RELOCADDR]	/* r0 = gd->relocaddr */
	ldr	r1, =V7M_SCB_BASE
	str	r0, [r1, V7M_SCB_VTOR]
#else
#ifdef CONFIG_HAS_VBAR
	/*
	 * If the ARM processor has the security extensions,
	 * use VBAR to relocate the exception vectors.
	 */
	ldr	r0, [r9, #GD_RELOCADDR]	/* r0 = gd->relocaddr */
	mcr     p15, 0, r0, c12, c0, 0  /* Set VBAR */
#else
	/*
	 * Copy the relocated exception vectors to the
	 * correct address
	 * CP15 c1 V bit gives us the location of the vectors:
	 * 0x00000000 or 0xFFFF0000.
	 */
	ldr	r0, [r9, #GD_RELOCADDR]	/* r0 = gd->relocaddr */
	mrc	p15, 0, r2, c1, c0, 0	/* V bit (bit[13]) in CP15 c1 */
	ands	r2, r2, #(1 << 13)
	ldreq	r1, =0x00000000		/* If V=0 */
	ldrne	r1, =0xFFFF0000		/* If V=1 */
	ldmia	r0!, {r2-r8,r10}
	stmia	r1!, {r2-r8,r10}
	ldmia	r0!, {r2-r8,r10}
	stmia	r1!, {r2-r8,r10}
#endif
#endif
	bx	lr

ENDPROC(relocate_vectors)

3.6 board_init_r

board_init_r 函数与 board_init_f 函数类似,用于初始化一系列外设 board_init_r 函数位于 commmon/board_r.c 中,实际上
board_init_f 函数并没有初始化所有的外设,还需要做一些后续工作,这些后续工作就是由函数board_init_r 函数来完成。
board_init_r 函数中含有 init_sequence_r 函数,该函数用于初始化序列。而 init_sequence_r 函数又含有 run_main_loop 函数,用于进入 uboot 命令模式或启动linux内核。run_main_loop 函数中最重要的是 main_loop 函数,该函数位于 common/main.c 中。

board_init_r 函数如下:

void board_init_r(gd_t *new_gd, ulong dest_addr)
{
#ifdef CONFIG_NEEDS_MANUAL_RELOC
	int i;
#endif

#ifdef CONFIG_AVR32
	mmu_init_r(dest_addr);
#endif

#if !defined(CONFIG_X86) && !defined(CONFIG_ARM) && !defined(CONFIG_ARM64)
	gd = new_gd;
#endif

#ifdef CONFIG_NEEDS_MANUAL_RELOC
	for (i = 0; i < ARRAY_SIZE(init_sequence_r); i++)
		init_sequence_r[i] += gd->reloc_off;
#endif

	if (initcall_run_list(init_sequence_r))
		hang();

	/* NOTREACHED - run_main_loop() does not return */
	hang();
}

上段代码中关键的是 initcall_run_list 函数,调用 initcall_run_list 函数来执行初始化序列 init_sequence_rinit_sequence_r 是一个函数集合, init_sequence_r 也定义在文件 common/board_r.c 中,由于 init_sequence_f 的内容比较长,里面有大量的条件编译代码,这里为了缩小篇幅,将条件编译部分删除掉了,去掉条件编译以后的 init_sequence_r 定义如下:

1 init_fnc_t init_sequence_r[] = {
2 initr_trace,
3 initr_reloc,
4 initr_caches,
5 initr_reloc_global_data,
6 initr_barrier,
7 initr_malloc,
8 initr_console_record,
9 bootstage_relocate,
10 initr_bootstage,
11 board_init, /* Setup chipselects */
12 stdio_init_tables,
13 initr_serial,
14 initr_announce,
15 INIT_FUNC_WATCHDOG_RESET
16 INIT_FUNC_WATCHDOG_RESET
17 INIT_FUNC_WATCHDOG_RESET
18 power_init_board,
19 initr_flash,
20 INIT_FUNC_WATCHDOG_RESET
21 initr_nand,
22 initr_mmc,
23 initr_env,
24 INIT_FUNC_WATCHDOG_RESET
25 initr_secondary_cpu,
26 INIT_FUNC_WATCHDOG_RESET
27 stdio_add_devices,
28 initr_jumptable,
29 console_init_r, /* fully init console as a device */
30 INIT_FUNC_WATCHDOG_RESET
31 interrupt_init,
32 initr_enable_interrupts,
33 initr_ethaddr,
34 board_late_init,
35 INIT_FUNC_WATCHDOG_RESET
36 INIT_FUNC_WATCHDOG_RESET
37 INIT_FUNC_WATCHDOG_RESET
38 initr_net,
39 INIT_FUNC_WATCHDOG_RESET
40 run_main_loop,
41 };

 第 2 行, initr_trace 函数,如果定义了宏 CONFIG_TRACE 的话就会调用函数 trace_init,初始化和调试跟踪有关的内容。
第 3 行, initr_reloc 函数用于设置 gd->flags,标记重定位完成。
第 4 行, initr_caches 函数用于初始化 cache,使能 cache。
第 5 行, initr_reloc_global_data 函数,初始化重定位后 gd 的一些成员变量。
第 6 行, initr_barrier 函数, I.MX6ULL 未用到。
第 7 行, initr_malloc 函数,初始化 malloc。
第 8 行, initr_console_record 函数,初始化控制台相关的内容, I.MX6ULL 未用到,空函数。
第 9 行, bootstage_relocate 函数,启动状态重定位。
第 10 行, initr_bootstage 函数,初始化 bootstage 什么的。
第 11 行, board_init 函数,板级初始化,包括 74XX 芯片, I2C、 FEC、 USB 和 QSPI 等。
这里执行的是 mx6ull_alientek_emmc.c 文件中的 board_init 函数。
第 12 行, stdio_init_tables 函数, stdio 相关初始化。
第 13 行, initr_serial 函数,初始化串口。
第 14 行, initr_announce 函数,与调试有关,通知已经在 RAM 中运行。
第 18 行, power_init_board 函数,初始化电源芯片
第 19 行, initr_flash 函数,对于 I.MX6ULL 而言,没有定义宏 CONFIG_SYS_NO_FLASH的话函数 initr_flash 才有效。但是 mx6_common.h 中定义了宏 CONFIG_SYS_NO_FLASH,所以此函数无效。
第 21 行, initr_nand 函数,初始化 NAND,如果使用 NAND 版本核心板的话就会初始化NAND。
第 22 行, initr_mmc 函数,初始化 EMMC,如果使用 EMMC 版本核心板的话就会初始化EMMC
第 23 行, initr_env 函数,初始化环境变量。
第 25 行, initr_secondary_cpu 函数,初始化其他 CPU 核, I.MX6ULL 只有一个核,因此此函数没用。
第 27 行, stdio_add_devices 函数,各种输入输出设备的初始化,如 LCD driver
第 28 行, initr_jumptable 函数,初始化跳转表。
第 29 行 , console_init_r 函 数 , 控 制 台 初 始 化 , 初 始 化 完 成 以 后 此 函 数 会 调 用stdio_print_current_devices 函数来打印出当前的控制台设备
第 31 行, interrupt_init 函数,初始化中断。
第 32 行, initr_enable_interrupts 函数,使能中断。
第 33 行, initr_ethaddr 函数,初始化网络地址,也就是获取 MAC 地址。读取环境变量“ethaddr”的值。
第 34 行, board_late_init 函数,板子后续初始化,如果环境变量存储在 EMMC 或者 SD 卡中的话此函数会调用 board_late_mmc_env_init 函数初始化 EMMC/SD。会切换到正在时候用的 emmc 设备
第 38 行 , initr_net 函 数 , 初 始 化 网 络 设 备
第 40 行, run_main_loop 行,主循环,处理命令。

run_main_loop 函数中最重要的是 main_loop 函数,该函数主要功能如下:

1、打印启动进度
2、设置环境变量
3、cli_init()//初始化hush shell相关变量
4、run_preboot_environment_command()//获取环境变量prebooot的内容,preboot是一些预启动命令,一般不使用该环境变量
5、bootdelay_process()//获取bootdelay的值,然后保存到stored_bootdelay全局变量里面,获取bootcmd环境变量值,并且将其返回
6、autoboot_command(bootcmd)
	---> abortboot(stored_bootdelay)//参数为bootdelay,该函数用于处理倒计时
		---> abortboot_normal(bootdelay)//参数为bootdelay,该函数用于处理倒计时
7、cli_loop()//uboot命令处理函数 common/cli.c
	---> parse_file_outer()//common/cli_hush.c 
		---> rcode = parse_stream_outer(&input, FLAG_PARSE_SEMICOLON);//hush shell 的命令解释器,负责接收命令行输入,然后解析并执行相应的命令
			---> parse_stream()//命令解析
			---> run_list()//运行命令
				---> run_list_real()
					---> run_pipe_real()
						---> cmd_process()//处理命令,即执行命令。Uboot使用 U_BOOT_CMD 来定义一个命令。 CONFIG_CMD_XXX来使能uboot中的某个命令。U_BOOT_CMD最终是定义了一个cmd_tbl_t类型的变量,所有的命令最终都是存放在.u_boot_list段里面。cmd_tbl_t的cmd成员变量就是具体的命令执行函数,命令执行函数都是do_xxx。
							---> find_cmd()//从.u_boot_list段里查找命令,当找到对应的命令以cmd_tlb_t类型返回
							---> cmd_call()//cmdtp->cmd,直接引用cmd成员变量		

以上就是本篇博客对uboot启动流程的详细解读。 

四、uboot启动流程总结

uboot启动流程总结: 

uboot的语言构成:10%的汇编语言;90%的C语言

uboot的启动特性:稳定性;速度

uboot的简化版启动流程:

1、设置状态寄存器 cpsr ,使CPU进入 SVC 特权模式,并且禁止 FIQ 和 IRQ;

2、关闭看门狗、中断、MMU、Cache;

3、初始化部分寄存器和外设(时钟、串口、Flash、内存);

4、自搬移uboot到内存中运行;

5、设置栈空间并初始化global_data;

6、剩余大部分硬件的初始化;

7、搬移Linux内核到内存;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/450613.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何用ChatGPT举办活动,人类与AI的一次深度对谈

刚刚&#xff0c;Mixlab今年首次线下联合举办的活动开启了&#xff0c;活动不仅分享了AIGC对体验设计的新要求、内容产业的发展研判、用于模拟仿真的生成式智能体&#xff0c;还演示了AI如何深度整合到一场活动之中。 1/ 数字人出场介绍Mixlab 是如何实现的呢&#xff1f;无限…

[Net]SSE消息推送简介

文章目录 SSE网络协议客户端服务端事件 SSE示例客户端服务端 SSE&#xff08;Server-Sent Events&#xff09;是一种服务端到客户端&#xff08;浏览器&#xff09;的单向消息推送方式。 SSE网络协议 SSE是基于HTTP协议的&#xff0c;客户端向服务端发起一个请求&#xff0c;建…

Android 9.0 系统设置显示主菜单添加屏幕旋转菜单实现旋转屏幕功能

1.前言 在android9.0的系统rom定制化开发中,在对系统设置进行定制开发中,有产品需求要求增加旋转屏幕功能的菜单,就是在点击旋转屏幕菜单后弹窗显示旋转0度,旋转 90度,旋转180度,旋转270度针对不同分辨率的无重力感应的大屏设备的屏幕旋转功能的实现,接下来就来分析实现…

以太网PLC无线WIFI跨网段通讯和Modbus仪表数据采集

产品介绍 产品型号&#xff1a;NET50-NAT-W4 使用范围&#xff1a;用于以太网PLC的跨网段无线通讯和仪表的数据采集 产品介绍 工业通讯桥接器&#xff08;NET50-NAT-W4&#xff09;用于以太网PLC的通讯扩展&#xff0c;以太网跨网段通讯和Modbus仪表的数据采集&#xff0c;上…

2023年制造业产品经理考NPDP有什么用?

产品经理国际资格认证NPDP是新产品开发方面的认证&#xff0c;集理论、方法与实践为一体的全方位的知识体系&#xff0c;为公司组织层级进行规划、决策、执行提供良好的方法体系支撑。 【认证机构】 产品开发与管理协会&#xff08;PDMA&#xff09;成立于1979年&#xff0c;是…

谈一谈django应用实践

python 的 web 框架非常多,比较出名的有 django, flask, tornado。django 作为一个老牌框架,无论是文档还是代码质量都非常高,另外他自带的 admin 后台和一些有用的 app,如果你的需求是做 cms 之类的 web 应用的话,基本上不用开发多少代码就能出一个成品。不过很多新手可能…

2023-04-23 学习记录--C/C++-函数

合抱之木&#xff0c;生于毫末&#xff1b;九层之台&#xff0c;起于累土&#xff1b;千里之行&#xff0c;始于足下。&#x1f4aa;&#x1f3fb; 【一】、调用有参函数 ⭐️ 有参函数&#xff1a;调用函数时&#xff0c;需要传对应参数。 一、实现步骤 &#x1f338; 实现步骤…

Adobe国际认证证书,深化设计师个人优势!

Adobe国际认证又称为Adobe认证(英文:Adobe Certified Professional)是Adobe公司CEO签发的权威国际认证体系,旨在为用户提供Adobe软件的专业认证。 该体系基于Adobe核心技术及岗位实际应用操作能力的测评体系得到国际ISTE协会的认可&#xff0c;并在全球 148 个国家推广&#xf…

mybatis分页插件的详细理解和使用

mybatis分页插件的基本理解和使用 为什么要使用mybatis分页插件&#xff1f; 分页是一种将所有数据分段展示给用户的技术。用户每次看到的不是全部数据&#xff0c;而是其中一部分&#xff0c;如果在其中没有找到自己想要的内容&#xff0c;用户可以通过制定页码或者是翻页的…

头歌c语言实训项目-综合案例课外练习:学生成绩管理系统

(创作不易&#xff0c;感谢有你&#xff0c;你的支持&#xff0c;就是我前行的最大动力&#xff0c;如果看完对你有帮助&#xff0c;请留下您的足迹&#xff09; 目录 第1关&#xff1a;学生成绩管理系统 题目&#xff1a; 代码思路&#xff1a; 代码表示&#xff1a; 如…

【Git】git使用

vitevue3部署静态文件到github 1. 新建仓库 新建仓库 仓库名称: 必须是 [你的git用户名]或[仓库名称] .github.io&#xff0c;例如你的用户名是YunZhonJun&#xff0c;统一为小写&#xff0c;如↓ 例1 用户名.github.io yunzhonjun.github.io 例2 仓库名称.github.io colors…

【动力节点】Springsecurity14-18章JWT+Spring Security+redis+mysql 实现认证

15 SpringSecurity 集成thymeleaf 此项目是在springsecurity-12-database-authorization-method 的基础上进行 复制springsecurity-12-database-authorization-method 并重命名为springsecurity-13-thymeleaf 15.1 添加thymeleaf依赖 | org.springframework.boot spring-…

根据vue2数组响应式实现原理找到了一个待优化点

最近学习了相关知识 写出了 vue2数据响应式原理(5) 通过重写函数实现数组响应式监听但也在这里 我发现 数组响应式 一个是通过for遍历监听已有属性 还有就是重写会改变数组的几个内置函数来监听 但 如果 元素之前没有 且 我们不适用这些内置函数去修改呢&#xff1f; 我们创建…

报表生成器 FastReport .Net 用户指南(一):基本原理

FastReport .Net是一款全功能的Windows Forms、ASP.NET和MVC报表分析解决方案&#xff0c;使用FastReport .NET可以创建独立于应用程序的.NET报表。 随着FastReport .Net 2023版的正式发布&#xff0c;厂商也发布了最新版的用户手册&#xff0c;从今天起我们将持续更新2023版的…

苹果手机怎么看生产日期?参考方法在这!

案例&#xff1a;怎么查苹果手机买了几年&#xff1f; 【求助&#xff01;我从别人那里买了一部苹果手机&#xff08;非官方&#xff09;&#xff0c;怎么看这个手机用了几年&#xff1f;】 苹果手机作为一款高端手机&#xff0c;备受用户的喜爱。然而&#xff0c;许多用户不知…

utools - 电脑必装软件

目录 一、超级面板二、计算稿纸三、IP地址查询和正则表达式测试四、浏览器书签五、OCR识别六、Markdown编辑器、Emoji表情输入工具七、总结 简介&#xff1a; 今天我要向大家介绍一款非常实用的电脑软件——utools。 官网&#xff1a;utloos &#x1f433;作为一款集成了多种实…

初识c++语法(一)

我们在C语言的基础之上进行c语言的学习。对于我们的c语言来说&#xff0c;c兼容C语言&#xff0c;所以我们以前编写的C语言的程序在c平台上也是可以运行的。唯一不同的就是我们的c对于我们C语言的部分语法做出了优化以及引入了面向对象的概念。所以在刚开始学习c的时候我们可以…

免费馅饼(牛客刷dp)

免费馅饼 比赛主页 我的提交 时间限制&#xff1a;C/C 1秒&#xff0c;其他语言2秒 空间限制&#xff1a;C/C 262144K&#xff0c;其他语言524288K 64bit IO Format: %lld 题目描述 SERKOI最新推出了一种叫做“免费馅饼”的游戏:游戏在一个舞台上进行。舞台的宽度为W格&#…

D. Color with Occurrences(Codeforces Round 811 (Div. 3))

https://codeforces.com/contest/1714/problem/D 题目大意 给定一个字符串 t t t 和一组字符串 s 1 , s 2 , … , s n s_1, s_2, \ldots, s_n s1​,s2​,…,sn​。每次操作可以选择其中一个字符串 s i s_i si​&#xff0c;将 t t t 中所有 s i s_i si​ 的出现位置染…

管理后台项目-04-SPU列表-增删改SPU-获取SKU【续】

目录 1-删除spu 2-添加sku 2.1-获取skuForm页面组件的数据 2.2-收集form表单数据 2.3-保存提交数据 3-查看SKU信息和loading效果 上一篇文章管理后台项目-03-SPU列表-增删改SPU-获取SKU_ycmy2017的博客-CSDN博客内容较多&#xff0c;交互逻辑有点复杂&#xff0c;所以分两…