BootLoader介绍

2023-11-11

一.BootLoader的引入

首先我们知道对于pc机,他的启动过程是:
BIOS(启动)—>Windows内核(挂在C/D盘)—>系统盘/应用盘(启动)—>应用程序

而对于嵌入式系统(比如Android手机,工控设备等)他的启动过程是:
BootLoader(启动)—>Linux kernel(挂载)—>根文件系统(启动)—>APP

系统上电之后需要一段程序来进行初始化,比如关闭看门狗,改变系统时钟,初始化存储控制器,将更多的代码复制到内存中等等,这些主要是将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核准备好环境,而这一小段程序就是BootLoader。当然为了方便开发,也可以增加一些功能,比如增加网络功能,这样来增强BootLoader的功能。

二.BootLoader的启动方式

大多数的BootLoader都有两种不同的操作模式:“启动加载模式”和“下载模式”。但是注意BootLoader的最终目的是启动内核,因此其实这两种当时并没有所谓的差别。

1.启动加载(boot loading)模式
上电后,BootLoader从板子的某个固态存储设备上将操作系统加载到RAM中运行,整个过程没有用户介入。这种模式是BootLoader的正常工作模式,产品发布时候,BootLoader就工作在这种模式下。

2.下载(downloading)模式
这种模式下,开发人员使用各种命令,通过串口连接或者网络连接从主机上下载文件,将他们直接放在内存运行或者烧入flash类固态存储设备中。以后的系统更新也会使用 Boot Loader 的这种工作模式。工作于这种模式下的 Boot Loader 通常都会向它的终端用户提供一个简单的命令行接口。

对于串口通信的方式:使用xmodem/ymodem/zmodem 协议
对于网络通信的方式:使用tftp,nfs服务等

三.BootLoader的结构和启动过程

1.概述
嵌入式Linux系统的4个层次
(1)引导加载程序:BootLoader
(2)Linux内核:特定于嵌入式板子的定制内核和内核的启动参数。内核的启动参数可以是内核默认的,也可以是BootLoader传递给他的。
(3)文件系统:包括根文件系统和建立于flash内存设备之上的文件系统。里面包含了Linux系统能够运行所必须的应用程序、库等,比如可以给用户提供操作Linux界面的shell程序、动态链接的程序运行时需要的glibc等。
(4)用户应用程序:特定用户的应用程序。

嵌入式Linux系统的典型分区结构:
在这里插入图片描述
boot parameters:分区中存放一些可以设置的参数,如:IP地址,串口波特率、要传递给内核的命令行参数等。正常启动过程中,BootLoader先运行,然后他将内核复制到内存中,并且在内核某个固定位置设置好要传递给内核的参数,最后运行内核,内核启动之后,他会挂在根文件系统(root filesystem),启动文件系统中的应用程序。

2.BootLoader的两个阶段
BootLoader的启动过程可以分为单阶段、多阶段两种,多阶段的BootLoader能提供更加复杂的功能以及更好的移植性。

从固态存储设备上启动的BootLoader大多都是两阶段的启动过程。第一阶段用汇编实现,完成一些依赖于CPU体系结构的初始化,并调用第二阶段的代码;第二阶段则是通常使用C语言来实现,这样可以实现更加复杂的功能,而且代码会有更好的可读性和可移植性。

第一阶段
1、硬件设备初始化:关闭看门狗、关中断、设置CPU的速度和时钟频率、RAM初始化等。
2、为加载BootLoader的第二阶段准备RAM空间
3、复制BootLoader的第二阶段代码到RAM空间中
4、设置好栈
5、跳转到第二阶段代码的C入口处

第二阶段
1、初始化本阶段要使用的硬件设备
2、检查系统内存映射(memory map):就是确定板子使用了多少内存,他们的地址空间是什么。
3、将内核映象和根文件映象从flash上读到RAM空间中
4、为内核映象设置启动参数
5、调用内核

为了开发的方便,至少要初始化一个串口以便程序员与BootLoader交互。

四.自己写一个BootLoader

BootLoader是裸板程序组成,因此可以参考uboot来写。
写一个简单的BootLoader,他的功能就是能够启动内核。因此,BootLoader所要实现的功能就是:
(1)关闭看门狗:开发板默认是打开的,如果不关闭,那么在开发板起来之后一段时间之后会复位
(2)初始化时钟:设置分频系数,为了让系统能够跑的更快
(3)初始化sdram
(4)重定位代码:如果BootLoader太大,那么就需要重定位,也就是将BootLoader本身的代码从flash复制到他的链接地址去
(5)执行main函数,就是执行启动的第二阶段

1.BootLoader第一阶段

首先写出一个汇编文件start.s

1、关闭看门狗

/* 1、关闭看门狗 
 * 对于s3c2440来说看门狗默认是关闭的,如果不关闭,那么在开发板起来之后一段时间后会复位
 * 对于2440来说,看门狗的地址是0x53000000,只要将它置为0即可
 */

	/* 这是一条伪汇编指令 
	 * 编译器在编译的时候发现指令比较复杂,会把他拆分成两条指令,先把他放到某个地址,然后再去这个地址读出来
	 */
	ldr r0, =0x53000000
	ldr r1, =0	 /* 值比较简单的话就直接用mov指令 */
	str r1, [r0]	/* 将r1的值存放到r0所在的地址 */

2、设置时钟
设置时钟的目的就是为了让系统能够跑的更快,因此在这里将FCLK设置为400MHz,将HCLK设置为100MHz,将PCLK设置为50MHz。
在这里插入图片描述

也就是FCLK:HCLK:PCLK = 1:4:8

怎么编程控制MPLL、HDIV、PDIV,使FCLK=400MHz,HCLK=100MHz,PLCK=50MHz?

因此需要设置`MPLLCON`的FCLK=400MHz,设置`CLKDIVN`的HCLK=FCLK/4,PCLK=FCLK/8。

看芯片手册得知:使用CLKDIVN寄存器来设置这些CLK
请添加图片描述
请添加图片描述
那么就可以写出一下代码:
根据PDIVN的第0位和HDVIN的第2位为1,即101,可以计算出为0x5

/* 设置分频比为FCLK:HCLK:PCLK=1:4:8 HDIVN=2,PDIVN=1*/
	ldr r0, =0x4C000014
	ldr r1, =0x5
	str r1, [r0]

同时看到芯片手册notes:
请添加图片描述
这里我们设置的HDIVN=2,并不为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		/* 写入控制寄存器 */

r:寄存器
c:协处理器
mrc:从协处理器读取某个值放入寄存器中
mcr:从寄存器中读取某个值到协处理器中

既然我们让HCLK = 400MHz,我们就要去设置对应的寄存器MPLLCON
请添加图片描述
当晶振为12MHz的时候,MDIV = 92,PDIV = 1,SDIV = 1
在这里插入图片描述
因此可以通过计算得到
m = 100;
p = 3;
s = 1;
Fin为12M
MPLL = (210012)/(3*2) = 400MHz

再来看到MPLLCON寄存器:
请添加图片描述
因此需要设置为:MPLLCON = ((92<<12) | (1<<4) | (1<<0));

代码实现为:

/* 对应的开发板为2440,设置MPLLCON = S3C2440_MPLL_200MHZ */
	ldr r0, =0x4c000004
	ldr r1, =S3C2440_MPLL_400MHZ
	str r1, [r0]
	/* 一旦设置PLL, 就会锁定lock time直到PLL输出稳定
	 * 然后CPU工作于新的频率FCLK
	 */

对于S3C2440_MPLL_400MHZ的定义我们放在开头:

#define S3C2440_MPLL_400MHZ     ((0x5c<<12)|(0x01<<4)|(0x01))

3、初始化sdram
2440一共有8个BANK,关于各个BANK的性质,在这里不再叙述,需要按照数据手册,将BWSCON,BANKCON0~7,REFRESH,BANKSIZE,MRSRB6,MRSRB7这几个寄存器的值一一算出来,然后依次存到寄存器中

sdram_config是程序的一个标号,用来存放算出来的这些值,然后用上面的方法一一存进去

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

汇编代码如下:

/*3、初始化SRAM */
	ldr r0, =MEM_CTL_BASE
	adr r1, sdram_config     /* sdram_config的当前地址 */
	add r3, r0, #(13*4)
1:
	ldr r2, [r1], #4 /*让r1地址的值读到r2,让后r1加4,也就是指向下一个地址*/
	str r2, [r0], #4 /*让r2的值写入到r0地址的寄存器,r0加上4,指向下一个地址*/
	cmp r0, r3 /*不断的循环把sdram_config里面的值写入到BWSCON开始的寄存器里面*/
	bne 1b /*b的含义代表调到这行代码前面的1,如果是1f就代表下面的1*/

对于MEM_CTL_BASE 的定义为:#define MEM_CTL_BASE 0x48000000

4、重定位
注意用C语言写的函数这些要先设置好栈,那么为什么汇编语言调用C函数要设置栈?
(1)保存现场:也就是寄存器的值,防止被破坏。因此,在函数调用之前,应该将这些寄存器等现场暂时保存(入栈push),等调用函数执行完毕后出栈(pop)再恢复现场。这样CPU就可以正确的继续执行了。
(2)传递参数:C语言函数调用时,会传给被调用函数一些参数,对于这些C语言级别参数,被编译器翻译成汇编语言时,要找个地方存放下来,并且让被调用函数能访问,否则没法传递。找个地方存放下来分2种情况。一是,本身传递的参数不多于4个,可以通过寄存器传送。因为在前面的保存现场动作中,已经保存好对应的寄存器的值,此时这些寄存器是空闲的,可以供我们使用存放参数。二是,参数多于4个,寄存器不够用,就得用栈。
(3)保存临时变量:这些临时变量包括函数的非静态局部变量以及编译器自动生成的其他临时变量。

对于重定位,我们为什么要进行重定位?
我们板子中有nor flash、SDRAM和nand flash,还有一个4k的片内内存SRAM。

CPU能直接访问的地方有:nor flash、SDRAM、SRAM和各种控制器(包括NAND flash控制器)。所以当我们的程序烧写到SDRAM或者NOR flash的时候,程序能直接运行。但是如果烧写到NAND flash,芯片会把程序的头4K先拷贝到SRAM中执行,如果NAND flash中的程序小于4K的话,程序还能正常运行,如果大于4K,那大于4K的这部分就运行不了。所以我们就引入了重定位,NAND flash的代码中的前4K的代码中需要把整个代码拷贝到SDRAM去执行。
另外,对于NOR FLASH来说,虽然能在上面执行代码,但是我们却无法写NOR FLASH,所以一旦程序中有需要写的变量,比如全局变量和静态变量,我们在无法在NOR FLASH上直接修改它们的值。因此,我们还是需要将代码重定位到SDRAM中去执行。

这里重定位涉及到了链接脚本,编写出一个boot.lds如下:

/* 链接脚本 */
SECTIONS{
	/* 使用位置无关码,只用一个链接地址 也就是代码运行时地址的起始地址 */
	. = 0x33f80000;	/* 刚好跟最高地址相差512k,足够用的 */
	.text : { *(.text) }	/* 所有文件的代码段 */
	
	. = ALIGN(4);	/* 对齐 */
	.rodata : { *(.rodata) }	/* 所有文件的只读数据段 */
	
	. = ALIGN(4);
	.data : { *(.data) }	/* 所有文件的数据段 */
		
	. = ALIGN(4);
	__bss_start = .;	/* 等于当前地址 */
	.bss : { *(.bss)  *(COMMON) }	/* 所有文件的bss段,程序运行之前先把这段内存清0 */
	__bss_end = .;
}

重定位是为了将BootLoader本身的代码从flash复制到链接地址去。查看芯片手册得知:
在这里插入图片描述
我们的内存为64M,基地址是0x30000000,所以在这里我们让sp指向最高地址就好了,栈是向下增长,因此0x30000000再加上64M就位0x34000000

代码如下:

ldr sp, =0x34000000 //设置栈
bl nand_init //不管是nor还是nand启动,都需要初始化nand flash,因为内核存在nand flash上

后续代码如下:

	/* r0为第一个参数,r1为第二个参数,就是链接地址*/
	mov r0, #0	/* 源是0,就是从0地址开始读东西 */
	ldr r1, =_start /* 第一条标号的地址 */
	ldr r2, =__bss_start	/* bss起始地址 */
	sub r2, r2, r1	/* 除去bss段之后的二进制文件的大小 */
    bl copy_code_to_sdram  
    /* 对于bss段(没有初始化或者初始化为0的全局变量),不会存放在最后生成的二进制文件中,因此会把他清0 */
    bl clear_bss

注意到copy_code_to_sdram 这个函数需要三个参数,所以需要在上面写出对应的r0,r1,r2;

1、把地址0作为第一个参数(如果是NAND启动,就是NAND FLASH零地址的位置,如果是NOR启动,就是NOR FLASH 零地址的位置),为拷贝代码的源地址;

2、_start(为代码一开始的地址,也就是链接脚本中定义的0x33f80000)作为第二个参数,为代码拷贝的目的地址;

3、而要拷贝的代码有多长呢?这里就要用到有关ELF文件中BSS段的知识。我们都知道编译出来的bin文件是不包含BSS段的,BSS段存放的是未初始化的全局变量和静态变量,所以我们可以把它想象为初始化为0值,如果bin文件中存放一堆0值的变量是很浪费空间的。这里我们就知道,拷贝的代码长度为BSS段开始的地址减去代码的起始地址,也就是__bss_start - _start 。

对于copy_code_to_sdram函数如下:

/*知识背景:
  *对于nand flash: 开机启动的时候,从0地址开始的前4k内容会被拷贝到
  *芯片的片内0地址开始的RAM里面,并在RAM的0地址开始执行,所以
  *我们可以读写0地址开始的内容。
  *而对于nor flash : 是能直接在nor flash读的,但是不能写,开机启动
  *是在nor flash的0地址处开始执行,所以我们能读但是写不了。
  */
int isBootFromNorFlash(void)
{
	volatile int *p = (volatile int *)0;
	int val;
 
	val = *p;
	*p = 0x12345678;
	if(*p == 0x12345678)
	{
		/*nand flash启动*/
		*p = val;
		return 0;
	}
	else
	{
		/*nor flash启动*/
		return 1;
	}
}
 
void copy_code_to_sdram(unsigned char *src, unsigned char *dest, unsigned int len)
{
	int i = 0;
	
	/*如果是NOR启动*/
	if(isBootFromNorFlash())
	{
		while(i < len)
		{
			dest[i] = src[i];
			i++;
		}
	}
	else
	{
		nand_read((unsigned int)src, dest, len);
	}
}

对于nand_read()函数我们后面进行分析

对于clear_bss函数如下:

void clean_bss(void)
{
	extern int __bss_start, __bss_end;
	int *p = &__bss_start;
 
	for (; p<&__bss_end; p++)
		*p = 0;
}

5、执行main函数

	ldr lr, =halt
	ldr pc, =main
	
/* main函数有返回就会跳到这里,避免单板跑飞 */
halt:
	b halt

到此,BootLoader第一阶段汇编部分就是实现了。

2.BootLoader第二阶段

首先注意一点:BootLoader不依赖于任何其他的代码,因此所有的函数都要自己实现。

就像前面说的,为了开发的方便,至少要初始化一个串口以便程序员与BootLoader交互。

1.初始化串口
对于串口的初始化如下:

/* 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 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
}

2.从nand flash里面把内核读入内存

	/* 1. 从NAND FLASH里把内核读入内存 */
	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");

这里注意: 对于nand flash有一个缺陷,就是位反转,因此在nand flash的结构图中会有一个OOB(out of bank)用来解决位反转
的时候除了写一页nand flash之外,还要生成校验码,也就是ECC码,这个ECC码写到OOB中。
的时候除了读一页数据,还要把ECC码从OOB中读出来,读出来的数据要重新生成一个校验码,两个进行比较,如果相等,则读数据没错。有错误就会找出某一位,修正他的错误,
因此访问2048其实是在第二页,对于OBB,可以描述为OOB里的第几个字节

因此下面要实现nand_read()函数

/* 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))

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_wait_ready(void)
{
	while (!(NFSTAT & 1));
}

unsigned char nand_data(void)
{
	return NFDATA;
}

void nand_read(unsigned int addr, unsigned char *buf, unsigned int len)
{
	int col = addr % 2048;
	int i = 0;
		
	/* 1. 选中 */
	nand_select();

	while (i < len)
	{
		/* 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 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);
		
	}	
}

3.设置参数

怎么为内核设置启动参数?
BootLoader和内核的交互是单向的,方法就是BootLoader将参数放在某个约定的地方,再启动内核,内核启动后就去这个地址获得参数。
除了约定好参数的地址,还要规定参数的结构,是以标记列表TAG的形式来启动内核,标记就是一种数据结构,标记以标记 ATAG CORE开始,以标记 ATAG NONE结束。

标记的数据结构是tag,他是由一个tag_handler和一个联合体union组成
tag_hander结构体的两个成员分别表示类型和长度,比如表示的是内存还是命令行参数
对于不同类型的标记使用不同的联合体,比如内存使用tag_mem32,命令行参数使用tag_cmdline

参数的开始:setup_start_tag
参数的结束:setup_end_tag

/* 2. 设置参数 */
	puts("Set boot params\n\r");
	setup_start_tag();
	setup_memory_tags();
	setup_commandline_tag("noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0");
	setup_end_tag();

上述代码中我们用到了4个函数,来源于这里:
在这里插入图片描述
那我们可以模仿这四个函数
1.setup_start_tag(bd)函数:

static void setup_start_tag (bd_t *bd)
{
	/* static struct tag *params; */	
	/* gd->bd->bi_boot_params = 0x30000100; */
	params = (struct tag *) bd->bi_boot_params;

	params->hdr.tag = ATAG_CORE;	/* 表示一个参数的开始 */

	
	/*#define tag_size(type)	((sizeof(struct tag_header) + sizeof(struct type)) >> 2) */
	/*                                       4+4                        3*4             */
	params->hdr.size = tag_size (tag_core);	/* 20 >> 2 = 20/4 = 5 以4字节为单位 */
	/* 一共5个,tag和size占据两个,还有三个用来存储core里面的三个熟悉 */

	/* 都设置为0,说明没有用到这些 */
	params->u.core.flags = 0;
	params->u.core.pagesize = 0;
	params->u.core.rootdev = 0;

	params = tag_next (params); /* 下一个参数的位置 = 当前位置 + 头部的size;指针加5,相当于加上5*4*/
}

对于tag,定义如下:

struct tag {
	struct tag_header hdr;	/* tag的头部 */
	union {
		struct tag_core core;
		struct tag_mem_range mem_range;
		struct tag_cmdline cmdline;
		struct tag_clock clock;
		struct tag_ethernet ethernet;
	} u;
};

他有一个头部tag_hander,定义如下:

struct tag_header {
	u32 size;
	u32 tag;
};

2.setup_memory_tags (bd)函数:

static void setup_memory_tags (bd_t *bd)	/* tags说明可以设置多个memory */
{
	int i;

	for (i = 0; i < CONFIG_NR_DRAM_BANKS; i++) {
		params->hdr.tag = ATAG_MEM;
		params->hdr.size = tag_size (tag_mem32);

		params->u.mem.start = bd->bi_dram[i].start;	/* 起始地址 */
		params->u.mem.size = bd->bi_dram[i].size;	/* 大小 */

		params = tag_next (params);
	}
}

3.setup_commandline_tag函数:

static void setup_commandline_tag (bd_t *bd, char *commandline)	/* 设置命令行参数 */
{
	char *p;

	if (!commandline)
		return;

	/* eat leading white space */
	for (p = commandline; *p == ' '; p++);

	/* skip non-existent command lines so the kernel will still
	 * use its default command line.
	 */
	if (*p == '\0')
		return;

	params->hdr.tag = ATAG_CMDLINE;
	params->hdr.size =
		(sizeof (struct tag_header) + strlen (p) + 1 + 4) >> 2;	/* 向4取整 */

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

	params = tag_next (params);
}

4.setup_end_tag 函数:

static void setup_end_tag (bd_t *bd)
{
	params->hdr.tag = ATAG_NONE;
	params->hdr.size = 0;
}

上面给出的是源码里面的实现方法,下面我们自己实现:

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;

	params = tag_next (params);
}

void setup_memory_tags(void)
{
	params->hdr.tag = ATAG_MEM;
	params->hdr.size = tag_size (tag_mem32);
	
	params->u.mem.start = 0x30000000;
	params->u.mem.size  = 64*1024*1024;
	
	params = tag_next (params);
}

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);

	params = tag_next (params);
}

void setup_end_tag(void)
{
	params->hdr.tag = ATAG_NONE;
	params->hdr.size = 0;
}

对于上面4个函数,在内存中的分布如下:
请添加图片描述

以上我们就设置好了参数,下面就是要跳转执行

4.跳转执行kernel
这一步就更加简单了,我们都知道函数的名字其实就是一个地址值而已,我们前面已经把内核拷贝到内存0x3000800的地方,只要将这个地址值赋给函数指针变量thekernel,再执行就可以了,并传递相应的参数,比如tag的起始地址。

	theKernel = (void (*)(int, int, unsigned int))0x30008000;
	theKernel(0, 362, 0x30000100);  
	/* 
	 *  mov r0, #0
	 *  ldr r1, =362
	 *  ldr r2, =0x30000100
	 *  mov pc, #0x30008000 
	 */

void (*theKernel)(int zero, int arch, uint params);
第一个参数:0 相当于mov r0, #0
第二个参数:机器ID,单板属于那个ID 相当于mov r1, #362
第三个参数:参数的位置

为什么要为内核传递参数?
首先在第一阶段里面是进行一些初始化的工作,这是为了使用开发板,但是内核并不是适配所有的开发板,
内核对于开发板的环境一无所知,因此想要启动内核,还需要给他传递一些参数,告诉内核当前所处的环境

怎么传递参数?
前面讲过三个参数,先将机器ID通过R1传递给内核,内核运行的时候会去从R1中取出机器ID进行分析
是否支持当前机器,这个机器ID实质上就是开发板CPU的ID。再传递参数的位置,也就是这块内存的基地址
这块内存中存放的就是Uboot给linux内核的其他参数,有起始地址、内存大小等,这个参数需要按照指定的
格式,并且还要规定参数的结构,也就是TAG

5.改进
启动内核花了7s,时间很长。主要是花在nand_read里面,这个可以改进
1、提高CPU频率,200MHz—>400MHz
2、启动ICACHE
读协处理器,然后写协处理器 ---->最终不到2ms就读出来了

2440里有CPU,CPU里面有cache,cache里面有指令ICACHE,数据DCACHE。我们的程序在SDRAM中。

如果不使用指令cache,CPU取完指令回来执行,每执行一条指令都回去访问SDRAM,代码不断执行,不断取
但是当有了ICACEH之后,CPU去取指令的时候会把那一块指令全部放到ICACHE中,CPU下次取指令的时候会先去ICACHE中找有没有指令,有的话直接取出来执行
访问外部SDRAM还要发出各种命令来读内存,非常耗时

cache就是高速内存
DCACHE能够使用的前提是MMU要启动

以上我们就是实现了一个简单的bootloader
代码如下:
boot.lds:

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

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)


void nand_read(unsigned int addr, unsigned char *buf, unsigned int len);


int isBootFromNorFlash(void)
{
	volatile int *p = (volatile int *)0;
	int val;

	val = *p;
	*p = 0x12345678;
	if (*p == 0x12345678)
	{
		/* 写成功, 是nand启动 */
		*p = val;
		return 0;
	}
	else
	{
		/* NOR不能像内存一样写 */
		return 1;
	}
}

void copy_code_to_sdram(unsigned char *src, unsigned char *dest, unsigned int len)
{	
	int i = 0;
	
	/* 如果是NOR启动 */
	if (isBootFromNorFlash())
	{
		while (i < len)
		{
			dest[i] = src[i];
			i++;
		}
	}
	else
	{
		//nand_init();
		nand_read((unsigned int)src, dest, len);
	}
}

void clear_bss(void)
{
	extern int __bss_start, __bss_end;
	int *p = &__bss_start;
	
	for (; p < &__bss_end; p++)
		*p = 0;
}

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_wait_ready(void)
{
	while (!(NFSTAT & 1));
}

unsigned char nand_data(void)
{
	return NFDATA;
}

void nand_read(unsigned int addr, unsigned char *buf, unsigned int len)
{
	int col = addr % 2048;
	int i = 0;
		
	/* 1. 选中 */
	nand_select();

	while (i < len)
	{
		/* 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();
}

#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);
		
	}
	
}

boot.c:

#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;

	params = tag_next (params);
}

void setup_memory_tags(void)
{
	params->hdr.tag = ATAG_MEM;
	params->hdr.size = tag_size (tag_mem32);
	
	params->u.mem.start = 0x30000000;
	params->u.mem.size  = 64*1024*1024;
	
	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);

	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里把内核读入内存 */
	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");
	setup_end_tag();

	/* 3. 跳转执行 */
	puts("Boot kernel\n\r");
	theKernel = (void (*)(int, int, unsigned int))0x30008000;
	theKernel(0, 362, 0x30000100);  
	/* 
	 *  mov r0, #0
	 *  ldr r1, =362
	 *  ldr r2, =0x30000100
	 *  mov pc, #0x30008000 
	 */

	puts("Error!\n\r");
	/* 如果一切正常, 不会执行到这里 */

	return -1;
}

start.s:


#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)
1:
	ldr r2, [r1], #4
	str r2, [r0], #4
	cmp r0, r3
	bne 1b

/* 4. 重定位 : 把bootloader本身的代码从flash复制到它的链接地址去 */
	ldr sp, =0x34000000

	bl nand_init

	mov r0, #0
	ldr r1, =_start
	ldr r2, =__bss_start
	sub r2, r2, r1
	
	bl copy_code_to_sdram
	bl clear_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

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
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

BootLoader介绍 的相关文章

  • 如何将 bin 文件(512 字节)写入软盘的第一个扇区(扇区 0)?

    如何将 bin 文件写入软盘 虚拟软盘 软盘映像的第一个扇区 我正在尝试启动一个简单的 512 字节引导加载程序 到处都显示 512 字节 的大小 所以我应该已经很好了 附加信息 引导加载程序仅显示一个字符串 我正在学习simple集会 有
  • 有没有一种方法可以在不读取 Linux 上的 proc/sys 文件的情况下获取电池信息(状态、插入等)?

    我想在linux上用C获取有关电池的信息 我don t想要阅读或解析any文件 是否有任何与 acpi 内核或任何其他模块的低级接口来获取我想要的信息 我已经在网上搜索过 但每个问题都会得到答案 parse proc foo bar 我真的
  • 如何测量 Linux 中的真实 CPU 使用率?

    我知道有类似的工具top and ps用于测量 CPU 使用率 但他们测量 CPU 使用率的方法是测量空闲任务未运行的时间 因此 例如 即使 CPU 由于缓存未命中而出现停顿 这些工具仍然会认为 CPU 被占用 然而 我想要的是分析工具在停
  • 如何指定使用 bitbake/yocto 构建哪个内核

    我正在努力使用 yocto daisy 生成新的 BSP 当我构建图像时 我收到以下警告 NOTE Resolving any missing task queue dependencies NOTE multiple providers
  • Linux 内核驱动程序的探测函数何时被调用?

    我正在尝试更新Android的内核驱动程序 我添加了一些printk来调试它 调用了 init函数 但没有调用probe函数 我缺少什么 何时 如何调用探测函数 该代码可在以下位置获取 https github com lamegopint
  • /arm64/Image 到 zImage 或 boot.img

    大家好 我一直在试图弄清楚如何使我的 android 内核成为 zImage 或 boot img 我试图弄清楚但没有运气 有人告诉我 zImage 不适用于我的设备 因为它是 arm64 内核 但我想我会再问一次 如果是这种情况 我会尝试
  • 使用BIOS int 13h访问不同磁头的扇区

    我的磁盘每磁道有 63 个扇区 根据我的观察 我假设 我想使用 int 13h 读取 16 位引导加载程序上的扇区 例如 如果我想读取扇区号 63 我将执行以下操作 mov dl 0x80 Drive number mov dh 0 Thi
  • 定义新的套接字选项以在 TCP 内核代码中使用

    我正在尝试向 TCP 内核代码添加一些功能 在tcp input c 我希望我实现的代码仅在某些情况下运行 我想添加一个控制标志 可以从用户空间应用程序设置它 我 认为我 需要添加一个新的套接字选项 以便我可以完成以下操作setsockop
  • 将 CPU 频率指定为 Linux 启动时的内核 CMD_LINE 参数?

    我将笔记本电脑的i5 CPU更换为i7 CPU 这样它可以运行得更快 但由于i7的功率更大 温度也比以前更高 所以我的笔记本经常死机 所以 我使用cpupower来指定CPU的最大频率 它起作用了 现在 我的问题是 有没有办法在启动时将CP
  • 破坏用户空间是什么意思? [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 这可能是一个简单的问题 但是 我听说在内核上工作的唯一规则是不要破坏 用户空间 所以我想知道这意味着什么 打破用户空间这是怎么发生的 Ed
  • 如何设置 intel_idle.max_cstate=0 来禁用 c 状态?

    我想在我的计算机上禁用 c 状态 我在 BIOS 上禁用了 c state 但没有获得任何结果 不过 我找到了一个解释 大多数较新的 Linux 发行版 在配备 Intel 处理器的系统上 使用 intel idle 驱动程序 可能编译到内
  • 如何在 Windows 中拦截 DNS 查询

    我正在研究如何在 Windows 中拦截 DNS 查询 以一种不需要将 DLL 注入到每个进程中的方式 并且理想情况下能够根据发出查询的进程做出决策 因此简单的 DNS 代理服务器是不够的 从表面上看 DNS 查询所采用的路径如下所示 某些
  • 在执行期间访问.eh_frame数据

    我正在尝试访问以下内容 eh frame正在运行的程序的一部分 具体来说 该程序是 Linux 内核 2 6 34 8 这 eh frame包含用于异常处理的有用数据 我想在内核代码内部使用它 该部分已经由以下人员编写gcc readelf
  • 转储 $mft 文件的内容

    对于一些商业的我正在做的项目我需要能够读取 mft 文件中存储的实际数据 我找到了一个gpl lib http www codeproject com KB files NTFSParseLib aspx artkw ntfs这可能会有所帮
  • 每个进程是否都存在内核堆栈?

    每个用户空间进程是否都存在一个内核堆栈和一个用户空间堆栈 如果两个堆栈都存在 那么每个用户空间进程应该有 2 个堆栈指针 对吗 在 Linux 中 每个任务 用户空间或内核线程 都有一个 8kb 或 4kb 的内核堆栈 具体取决于内核配置
  • Windows 内存映射文件

    我正在尝试研究 Windows 内核在内存映射文件 虚拟内存方面的行为 具体来说 我感兴趣的是确定内存映射文件的内容 由 Windows 刷新到磁盘的频率以及 Windows 使用什么标准来决定是时候这样做 我在网上做了一些研究 除了 MS
  • 没有设备的设备驱动程序?

    我正在创建一个需要使用一些内核级模块的应用程序 为此我将应用程序分为 2 个 一个用户级程序和一个内核级程序 在阅读了有关设备驱动程序并浏览一些教程后 我有点困惑 是否可以存在没有任何特定设备与之关联的设备驱动程序 除了设备驱动程序 内核代
  • 内核与系统中的 Windows 进程

    我有一些与内核和用户模式下的 Windows 进程相关的问题 如果我有一个 hello world 应用程序和一个公开新系统调用 foo 的 hello world 驱动程序 我很好奇一旦处于内核模式 我能做什么和不能做什么 对于初学者来说
  • 了解 U-Boot 内存占用

    我不明白加载 U Boot 时 RAM 中发生了什么 我正在开发 Xilinx Zynq ZC702 评估套件 并尝试使用 U Boot 在其上加载 Linux 内核 于是我使用Xilinx工具Vivado和SDK生成了一个BOOT bin
  • 每个进程每个线程的时间量

    我有一个关于 Windows 和 Linux 中进程和线程的时间量子的问题 我知道操作系统通常为每个线程提供固定的时间量 我知道时间量根据前台或后台线程而变化 也可能根据进程的优先级而变化 每个进程有固定的时间量吗 例如 如果操作系统为每个

随机推荐

  • 成都亚恒丰创科技USB-CAN和CAN分析仪接口方式

    USB CAN和CAN分析仪接口方式 导语 随着现代汽车电子技术的飞速发展 CAN总线成为了汽车领域中最常用的通信协议 而在进行CAN总线的调试和分析时 CAN分析仪是一种必不可少的工具 本文将介绍USB CAN接口和CAN分析仪的基本原理
  • Spring参数校验--List<E>类型参数校验

    1 遇到的问题 今天开发接口 遇到请求参数固定为List
  • 2023年第三届能源、电力与电气工程国际会议 (CoEEPE 2023)

    会议简介 Brief Introduction 2023年第三届能源 电力与电气工程国际会议 CoEEPE 2023 会议时间 2023年11月22日 24日 召开地点 澳大利亚 墨尔本 大会官网 www coeepe org 2023年第
  • Javascript:谈谈JS的全局变量跟局部变量

    今天公司一个实习小妹子问我两段JS代码的区别 我想 好简单
  • 干货分享:六个知名的Go语言web框架

    框架一直是敏捷开发中的利器 能让开发者很快的上手并做出应用 甚至有的时候 脱离了框架 一些开发者都不会写程序了 成长总不会一蹴而就 从写出程序获取成就感 再到精通框架 快速构造应用 当这些方面都得心应手的时候 可以尝试改造一些框架 或是自己
  • 【华为OD机试】 阿里巴巴找黄金宝箱(I)【2023 B卷

    华为OD机试 真题 点这里 华为OD机试 真题考点分类 点这里 题目描述 一贫如洗的樵夫阿里巴巴在去砍柴的路上 无意中发现了强盗集团的藏宝地 藏宝地有编号从0 N的箱子 每个箱子上面贴有一个数字 箱子中可能有一个黄金宝箱 黄金宝箱满足排在它
  • BurpSuite实战教程01-web渗透安全测试(靶场搭建及常见漏洞攻防)

    渗透测试 渗透测试 Penetration test 即安全工程师模拟黑客 在合法授权范围内 通过信息搜集 漏洞挖掘 权限提升等行为 对目标对象进行安全测试 或攻击 最终找出安全风险并输出测试报告 Web渗透测试分为白盒测试和黑盒测试 白盒
  • 在浏览器输入URL,按下回车之后的流程?

    1 在浏览器中输入一个URL 2 查找本地配置文件 如果之前有访问过 浏览器会进行缓存 如果没有的话会在本机域名解析文件hosts文件中寻找是否存在该URL的域名映射 如Windows的配置文件 C Windows System32 dri
  • NIO初级例子

    NIO初级例子 前言 一 代码撸上 前言 使用window系统环境 window 环境测试 测试使用telnet ip 端口 win R cmd 输入telnet id port Ctrl send 发送信息 缺点 无阻塞 但是cpu空转
  • UE4_代理示例_时钟

    时钟 TimeOfDayHandler 注册代理 执行代理 Fill out your copyright notice in the Description page of Project Settings pragma once inc
  • 陇原战“疫“2021网络安全大赛 Web EasyJaba

    陇原战 疫 2021网络安全大赛 Web EasyJaba 文章目录 陇原战 疫 2021网络安全大赛 Web EasyJaba 不出网 参考链接 查看源码 禁用了一些类 这里说一下反编译工具的情况 之前我一直用的jd gui 但是本题的附
  • SQLi-LABS(21~25a关详解)

    SQLi LABS Less 21 查看题目环境 登陆给我回显的数据是I LOVE YOU COOKIES 这题看了网上的wp才知道原来是将我们的uname和passwd都进行base64编码 表示不知道怎么看出来的 Cookie unam
  • StandardScaler函数用法

    StandardScaler 是来自 sklearn preprocessing 模块的一个类 其作用是进行特征缩放 使得所有特征的均值为 0 标准差为 1 这种处理方式也被称为数据的标准化 Standardization 或者 Z Sco
  • webView打开的页面和手机浏览器打开的不一样

    同一个url 用webView打开的和直接打开的不一样 webView打开的自动就有个商品在里面了 而且按钮也点击无效 大神们帮帮忙吧
  • java socket聊天室 swing做界面 Tcp为通讯协议 支持私聊 群聊 发文件

    Java的的的的聊天室 源代码下载 首先我们来看看程序界面 丑到爆 勉强能用就行啦 第一个 登录界面 第二个 用户界面 第三个 服务器界面 好了上面三个界面是程序的主界面 下面我们先讲讲如何使用源代码 使用条件 一数据库 我这里用的MyS
  • Linux系统点亮LED

    目录 应用层操控硬件的两种方式 sysfs 文件系统 sysfs 与 sys 总结 标准接口与非标准接口 LED 硬件控制方式 编写LED 应用程序 在开发板上测试 对于一款学习型开发板来说 永远都绕不开LED 这个小小的设备 基本上每块板
  • webstorm配置sass

    最近用webstorm 做项目 使用create react app创建项目 安装node sass chokidar 使用命令行来将sass转换为css 不尽人意的是 在vscode 可以正常使用 到了webstorm TM 一直不会自动
  • 视频点播服务器的配置如何选择,需要多大的带宽

    对于普通的企业网站 服务器带宽只需5M 10M 每天面对1w用户是没有问题的 图片网站 10M带宽可能只支持100 1k人 天访问 然后 如果是一个视频点播网站 服务器的带宽将增加几十倍 特别是对于视频点播服务器 瓶颈是带宽 视频点播服务器
  • 知识星球-伙伴匹配系统笔记2

    朋友伙伴匹配系统笔记2 1 前端整合路由 下载vue router 由于我们使用的是vue3 所以对应的是4版本的路由 npm install vue router 4 或者 yarn add vue router 4 如下图 前端整合路由
  • BootLoader介绍

    文章目录 一 BootLoader的引入 二 BootLoader的启动方式 三 BootLoader的结构和启动过程 四 自己写一个BootLoader 1 BootLoader第一阶段 2 BootLoader第二阶段 一 BootLo