Bootloader详解,理解Bootloader看这篇就够了

一、Bootloader作用(目的)

Bootloader=Boot + loader

Boot的目的:
最终目的:跳到C语言中;为了C语言运行程序会进行一系列的初始化,系统一上电后如何通过一系列的设置让软件程序员进入C语言/更高级语言环境的开发,这个过程就是boot的主要目的。

Loader的目的:
主要目的是开始执行应用逻辑,比如点灯:需要灯的接口开发;串口输入输出:需要串口编程;加载linux的内核:flash的编程、网卡的编程、内核启动前的初始化部分。根据不同的应用会有不同的变化。

一个开发板要想执行loader,要先看boot做了什么事。

二、完成Boot最终目的的前提条件

围绕boot的最终目的,对执行最终目的的前提条件进行说明:
前提条件:
(1)让SP指向可读可写的设备空间中

(2)满足递减栈的规则—SP想办法放在内存的高段地址

(3)配置SDRAM的控制器。首选空间为SDRAM,SDRAM不是系统一上电就好,所以在做(1)、(2)之前要配置SDRAM的控制器,使SDRAM可以正常工作。

(4)配置系统工作时钟,通过代码对相应寄存器进行相关配置即可实现。

(5)关闭看门狗、中断、MMU、CACHE,通过汇编语言对相应的寄存器进行置位即可实现。

开发时前提条件的执行顺序:(5)、(4)、(3)、(1)、(2)

三、对前提条件的详细说明

3.1、对前提条件(1)的说明:

先看代码:

int main()
{
	abc();//main函数中调用abc函数
}

int abc()
{
	int  a = 10;//假设用r0寄存器保存了立即数10,接下来跳到fun函数中
	fun();//abc函数中调用fun函数
	if(a xx)//函数fun返回后再判断a的情况,发现a不再是10
}

int fun()
{
	int  b = 20;/*假设同样用寄存器r0保存20,不用内存保存是因为寄存器的访问速度比内存访问速度快,
				函数abc中的变量a在整个代码执行区中不需要再往回传给main函数了,a是一个临时的值,
				寄存器r0只有一个,也只能保存一个,所以a只是出现一次,然后就不用了。当abc函数调用fun函数后,
				寄存器r0中的值10会被20替换*/
}

在上述代码运行过程中,一旦函数abc又调用了函数fun,寄存器r0中的值会被替换,从10变成了20,函数fun在返回后就会出现问题:这时用if语句判断a的情况,会发现a已经不再等于10了。

如果按照上述逻辑去设计汇编的话问题会有很多。于此问题,C语言给我们提供了栈的概念—stack

栈的逻辑:先进后出、后进先出,压栈出栈是通过代码对指向栈区指针的值进行更新、赋值,进而对指针指向的内存空间进行读和写。由此有了栈指针概念—该指针必须指向一个具备可以进行读写的硬件空间。在Arm体系架构中,栈指针用寄存器SP来描述。何为可读可写——与应用程序思路不同,硬件上的可读可写如代码:

int  a = 10;//这句话执行完后让以a为首地址的那段空间/设备把10赋进去
*(&a)==10;//进行解引用后将值取出来和10是相等的相当于将10写进去了

如果a的地址有问题,比如它指向的是一个不存在的空间。何为不存在的空间——0x2000 0000是内存机地址,但硬件公司只买了一个16M的内存,现在软件工程师想把a放在地址0x2100 0000,这个地址发出去后显然没有落到16M的范围内,那么对a的赋值就无法进行了。这时C语言无法帮我们进行压栈入栈的操作。

小结:栈指针一定要指向一个合法的设备——可读可写的地址空间。

3.2、对前提条件(2)的说明:

系统刚上电时SP指向的是0地址,即ROM,显然写不进去。所以在跳到C语言之前一定要让SP指向一个可读可写的设备区间中,SP首选指向地址是SDRAM中。如果用汇编写Bootloader:
代码为:

MOV  SP, #0x2000  0000//跳之前将sp赋值
BL  MAIN//跳到MAIN中

这样会出现问题,我们在学Arm基础时知道栈指针在C语言中有一个规范,称之为ATPCS,它规定C语言默认将SP作为栈指针,且SP指向的是一个向下压栈的过程,所以把栈指针指向内存的低地址不合适,SP指针一定要往上指,所以要满足递减栈的规则。
代码应为:

MOV  SP, #0x2100  0000//跳之前将sp赋值
BL  MAIN//跳到MAIN中

问题没完。既然指向地址0x2100 0000,而这个地址是内存,内存是一上电就能干活?不行,内存涉及到一个动态的充电过程。内存的访问:发出地址——直接把数据发给它——将数据写过去。
假设内存大小是16M,地址总线数计算:
在这里插入图片描述
存储总量=存储单元个数×存储字长,
然后:
存 储 单 元 个 数 = 2 地 址 总 线 位 数 存储单元个数 = 2^{地址总线位数} =2线

另:
1 M B = 1024 K B = 1024 B ∗ 1024 = 1048576 B = = 2 20 B 1MB=1024KB=1024B*1024=1048576B== 2^{20}B 1MB=1024KB=1024B1024=1048576B==220B

存储字长是什么:
存储字长:存储单元中的二进制代码(存储字)位数,存储字长可以是8位、16位、32位等。
早期计算机的存储字长一般和机器的指令字长与数据字长相等,故访问一次主存便可取一条指令或一个数据。随着计算机的应用范围的不断扩大,解题精度的不断提高,往往要求指令字长是可变的,数据字长也要求可变。为了适应指令和数据字长的可变性,其长度不由存储字长来确定,而由字节的个数来表示。1个字节(Byte)被定义为由8位(Bit)二进制代码组成。
存储字是指存放在一个存储单元中的二进制代码组合。一个存储字可代表一个二进制数,也可代表一串字符,如存储字为0011011001111101,既可表示为由十六进制字符组成的367DH(ASCII码),又可代表16位的二进制数,此值对应十进制数为13 949,还可代表两个ASCII码:“6”和“}”。一个存储字还可代表一条指令。 字长就是在同一时间中处理二进制数的位数叫字长。通常称处理字长为8位数据的CPU叫8位CPU,32位CPU就是在同一时间内处理字长为32位的二进制数据。
存储字长是在内存中存储字的长度,可以是16bit或8bit

故:
16 M = 2 地 址 总 线 位 数 × 存 储 字 长 16M=2^{地址总线位数}×存储字长 16M=2线×

即:
( 2 4 ∗ 2 20 ) B = 2 地 址 总 线 位 数 × 存 储 字 长 (2^4*2^{20})B= 2^{地址总线位数}×存储字长 (24220)B=2线×

存储字长为1,所以地址总线数为24。

所以这个内存有24根地址总线,用这24根地址总线就可以对16兆进行逐一编址。现在CPU只要发出对应的地址,就会进行数据的读写。

如果一个芯片的内存比较大,为了减小芯片的体积,需要减少地址总线的数目。如何用较少的地址线来将较大的内存全访问到?利用C语言中一维数组、二维数组的概念。
如代码:

int  a[16];//在这里定义了16个字节(2^4),用4根线就能找到它(用4bit可以表示)

即用这4根线就能表示这16种状态。
现在想办法减少这4根线的数目,对于同样的空间我还可以怎样访问?如代码:

int  b[4][4];//将16个字节分为4行4列

用这种方式访问时需要知道行数和列数,范围都是0~3,4个字节大小,现在用2bit就能表示(2根线访问)。先发出行坐标,再发出列坐标。
通过改成2维数组使得外部接口上的位数变少了。数组维数越大,在表示相同大小的内存时,需要的位数越小。

芯片内存的内部结构可以理解为一个多维数组,它由多个面构成,要想访问其中的一块空间,要先知道它在哪一个面上,然后知道它在这个面上的行、列坐标(地址)。内部结构大致如下图所示:
在这里插入图片描述
引出问题:现在发出地址0x2100 0000,要将这个地址转换为在这个芯片中的哪一个面上、哪一行上、那一列上。这个转换过程可以由软件程序员来自行分解,也可以由硬件做。软件做成本高、意义也不大,故而让硬件来帮我们确定。要让硬件做会引出另外一个概念:在芯片的datasheet中的DRAM CONTROLLER中规定了这一过程:见对前提条件(3)的说明中的内容。

细节补充说明:SP指针要往上指,但是不是随便指。在ARM体系中ARM工作分了7个工作空间。第一个:SVC模式,在系统复位后默认会用的一个模式,工作在Linux的kernel空间;第二个:IRQ模式,中断一旦发生后的运行模式;第三个:FIQ模式;第四个:USR模式,它对应的实际上就是Linux下的app,就是应用程序空间。
…等七个工作空间。
其中USR模式和SVC模式工作的空间都有自己的sp指针,这样用户空间操作的代码不会影响内核。这样会出现一个问题,内存假如只有一块,两个sp指针在遵循指向高地址的原则时可能会重复指向一个地址,所以在对sp赋值时要考虑用哪一个模式时要将sp分开,如何分开:对应每一个工作模式划分相应的可供使用的内存空间,如下图所示,但这时可能会出现另一个问题,如果USR的内存空间没有设计好,一旦发生死循环递归,栈会慢慢的往下延伸,会发生栈的溢出,溢出后会将SVC的空间内容修改了,那么在执行SVC时结果就错了,程序就跑得乱七八糟。
在这里插入图片描述
所以在执行前提条件(1)(2)的时候要考虑几个问题,首先要考虑用哪些空间/模式,就要初始化哪些模式下的sp,如何初始化:有一套专门用于模式切换的指令。其次在对sp赋值时有原则:每个模式的值不能重复、不能覆盖其他模式。

3.3、对前提条件(3)的说明:

在这里插入图片描述
CPU直接发出地址后现在要访问SDRAM,让CPU把想做的事情告诉给控制器,控制器根据我的配置(行地址数、列地址数、多少块、周期性充电问题、计算多久让它充一次电)

关于计算多久让它充一次电:CPU是一个脉冲波形,这里要引入时序的概念。
在这里插入图片描述
时序是芯片的心脏,每一个上升沿触发CPU干一件事,多久充一次电?
控制器凭什么知道经过多久可以充?所以要先配置控制器,在配置中又有一个问题,CPU和控制器都有自己的工作频率,它们共用同一个频率吗?即它们共用同一个时钟吗?因为外围控制器的成本/芯片工艺过程决定了控制器处理数据的能力较弱,导致不能同步接收到发送给它的数据,所以总时钟总线上的频率不会直接给控制器用,而是通过一个分频器,将频率降低后再给它用。所以还要先配置系统工作时钟。只有配置好了工作时钟才能计算出多久充一次电。

3.4、对前提条件(4)的说明:

在嵌入式开发过程中这一步是最耗时的,因为后期Linux内核最终还是运行在RAM,包括很多程序都是在RAM上运行,而RAM中,如果我们给它的频率不符合这款芯片本身的物理特性,那么很容易出现程序跑飞,在PC中就可能出现程序在运行时突然蓝屏,蓝屏的原因有很多,但是大部分都是由于内存的问题,有些时候买的内存的芯片的物理结构/质量不好,响应时间会很慢。举例:现在CPU要发出序列1101,现在芯片要想办法以一定的速度将这4bit发到数据总线上,或者说给到这片SDRAM上,假设CPU工作在1GHZ(这频率很快了),基本上1GHZ分之一再乘以4,这个时间就可以把4个比特发完了。SDRAM价格便宜,根本就不能同步响应1GHZ的CPU给的信息,那么内存工作就有问题,出现蓝屏。

配置SDRAM的难度就在于不仅要看懂SDRAM的芯片手册,同时还要懂得一定的硬件原理,包括一些调试经验,然后去动态的算出针对某一块开发板的SDRAM的具体工作参数。

3.5、对前提条件(5)的说明:

配置系统时钟之前还有一个问题,不是配了就马上工作正常,还要经过一段时间,在这期间还要引入看门狗。看门狗就是一个定时器,在一段时间之内,如果不对定时器进行重新的喂狗,一旦减到0就会触发一个中断/复位,CPU就相当于重启了。所以要保证有足够的长的时间来执行前提条件(1)、(2)、(3)、(4),所以在配置时钟之前为了安全起见还要把看门狗给关了。同时中断也要关了,因为我们还没进入到C或者说还没进到系统处理,即使中断来了,我们还不知道该怎么处理,因为我们的函数还没注册进去,这时最好把中断也关了,在调用main函数时再将中断打开。除此之外还可以关闭MMU、CACHE,MMU一般指的是有操作系统情况下才用,裸机程序上一般情况下为了简单一点可以不要管它,对于CACHE,指令CACHE无所谓可开可关,但是数据CACHE一般情况下在这时最好关了,虽然说速度可以提高,但是由于涉及到数据完整性的问题,有可能CACHE打开后数据的完整性就会有影响,这样给后期的调试带来很大麻烦,所以先关了它。

3.6、Boot阶段基本流程简单总结:

Boot最终目的是跳到C语言,在跳到C语言之前要考虑栈指针的初始化,因为C语言编译器要自动的利用SP指针来作为我们的栈空间,这样我们就必须要把SP指向一个可读可写的设备,具体赋什么值要根据规则来决定,然后再放到SDRAM上,紧接着SDRAM要想工作,在擦之前还要配置SDRAM,这个工作量比较大,包括:行地址数、列地址数、多少块、周期性充电问题,在配置SDRAM之前还要计算多久充一次电,所以还要配置系统时钟,而系统时钟工作还要一些前导,即关闭看门狗、中断、MMU、CACHE。

所以具体的来说,不管什么系统,要想设计一款Bootloader中的Boot阶段基本是要完成上述前提工作。最后根据每个系统的条件不同,还涉及到一个概念就是代码搬移。

为什么要代码搬移?一个芯片,系统启动时它会规定只能处理多少量的代码,如果代码量超过了规定的可处理的量,多余的代码就处理不了了,这时就要程序员自己把代码搬到一个更大的空间上去运行,要程序员自己对控制器进行控制,所以在main函数执行前也还要对相应存储器的控制器进行初始化,在代码搬移前还要初始化对应控制器,这样做软件工程师的工作量大一些,因为首先要看硬件公司用哪一个存储器启动,比如SD卡就要写SD卡的驱动,如果是NAND启动,就要写NAND的驱动,然后考虑怎么把代码快读搬到内存上,跳到内存上执行,一般来说代码搬移到RAM上,所以都要在SDRAM工作正常的基础上。其次是执行速度问题,如果代码本身之前工作在flash上,flash的执行速度比RAM慢得多,所以要把程序从存储器(nor-flash)搬移到快速的内存上运行。这就是我们说的代码搬移。

以上就是完成Bootloader中的Boot工作所要进行的一些事。

四、loader是干啥的

当MAIN函数跳过来之后(进入到C语言后)进行Loader的目的,即开始执行应用逻辑,这个要根据具体的工程代码来决定。

<think>好的,用户遇到了旁路由设置的问题,导致丢包严重无法上网。我需要仔细分析可能的原因和解决方法。首先,用户的主机IP是192.168.47.13,旁路由的地址是192.168.47.9,设置旁路由作为网关后出现丢包。常见的可能原因包括防火墙设置、路由配置错误、网关或DNS问题,以及硬件问题。 首先,我应该考虑旁路由的防火墙是否阻止了流量。用户可能需要检查OpenWrt的防火墙区域设置,确保LAN区域的入站、出站和转发都允许。另外,NAT规则是否正确,比如是否需要启用Fullcone NAT或者关闭某些过滤选项。 接下来是路由配置的问题。主路由和旁路由之间的通信是否正常?需要确认主路由是否正确地转发流量到旁路由,同时旁路由的路由表是否正确指向主路由作为默认网关。此外,用户的主机是否正确设置了网关和DNS,可能还需要检查是否有IP冲突。 然后是DNS配置,如果DNS解析有问题,即使路由正确,也可能导致无法上网。建议用户将DNS设置为旁路由的IP或者公共DNS如114.114.114.114。 硬件或网络性能问题也不能忽视。网线、接口松动或者旁路由设备性能不足可能导致丢包。可以建议用户通过ping测试检查延迟和丢包情况,比如在旁路由上ping主路由和外网地址,以及在主机上ping旁路由和主路由,查看哪里出现丢包。 另外,用户是否开启了某些可能导致问题的功能,如Turbo ACC中的流量分载或者SFE快速路径,这些有时会导致冲突,需要暂时关闭测试。 还有可能主路由的DHCP设置没有正确分配旁路由作为网关,用户可能手动设置了主机的网关,但其他设备如果有问题可能需要检查DHCP配置。另外,检查主路由的防火墙规则是否阻止了旁路由的流量。 最后,如果以上方法都不行,可能需要更详细的日志分析,比如在旁路由上使用tcpdump抓包,或者查看系统日志中的错误信息。如果用户对网络配置不熟悉,可能需要简化设置,比如先确保旁路由作为纯网关,关闭不必要的插件,逐步排查问题。根据</think>你的描述,设置旁路由后出现丢包和无法上网的问题,可能是由于路由规则、防火墙设置或网络流向配置不当导致的。以下是逐步排查和修改建议: --- ### **一、基础检查** 1. **确认网关指向正确** - 检查设备(192.168.47.13)的网关是否已设置为旁路由 IP `192.168.47.9` - 查看命令(Windows):`ipconfig /all` - Linux/OpenWrt:`ip route show default` 2. **旁路由与主路由连通性** - 在旁路由上执行 `ping 192.168.47.1`(假设主路由 IP 是 192.168.47.1,需确认) - 如果无法 ping 通,检查旁路由的网线、VLAN 配置或主路由的 ARP 表。 --- ### **二、旁路由配置修改** #### 1. **关闭 OpenWrt 防火墙(临时测试)** - 在「常规设置」中 **关闭「SYN-flood 防御」** - 在「区域设置」中确保 **LAN → 入站/出站/转发均设为「接受」** - 重启防火墙:`/etc/init.d/firewall restart` - **注意**:测试后若生效,需重新配置防火墙规则而非长期关闭。 #### 2. **配置 NAT 规则(关键步骤)** - 如果旁路由需要处理 NAT(例如主路由未做 NAT): 在 OpenWrt 的 **网络 → 防火墙 → 自定义规则** 添加: ```bash iptables -t nat -I POSTROUTING -o eth0 -j MASQUERADE ``` (`eth0` 需替换为旁路由连接主路由的实际接口名,如 `br-lan`) #### 3. **修改网关和 DNS** - 进入 OpenWrt 管理界面:**网络 → 接口 → LAN → 编辑** - **IPv4 网关**:填写主路由 IP(如 `192.168.47.1`) - **DNS 服务器**:填写主路由 IP 或公共 DNS(如 `114.114.114.114`) - 保存并应用后,重启网络:`/etc/init.d/network restart` --- ### **三、主路由配置** 1. **关闭主路由的 DHCP 冲突** - 如果主路由和旁路由均开启 DHCP,需关闭其中一个(建议保留主路由 DHCP,旁路由关闭)。 2. **主路由静态路由(可选)** - 如果网络有跨网段需求,在主路由添加静态路由: 目标网络:`0.0.0.0/0`,网关:旁路由 IP `192.168.47.9`。 --- ### **四、诊断工具** 1. **检查丢包路径** - 在主机上执行: ```bash ping 192.168.47.9 # 测试到旁路由的连通性 ping 192.168.47.1 # 测试到主路由的连通性 ping 8.8.8.8 # 测试外网连通性 traceroute 8.8.8.8 # 查看路径卡在哪一跳 ``` 2. **OpenWrt 系统日志** - 通过 `logread` 命令或管理界面查看是否有防火墙丢包记录: ```bash logread | grep DROP ``` --- ### **五、其他可能性** 1. **MTU 不匹配** - 尝试在 OpenWrt 的 LAN 接口设置中修改 MTU 为 `1472` 或 `1480`(需与主路由一致)。 2. **IPv6 冲突** - 在 OpenWrt 的 LAN 接口中禁用 IPv6(除非明确需要)。 3. **硬件性能问题** - 旁路由设备(如 R2S、树莓派)性能不足可能导致丢包,可尝试关闭流量监控类插件(如 SQM、AdGuard)。 --- ### **六、最终建议** 1. 按以下流程简化配置: - 旁路由仅作为网关(关闭 DHCP、DNS 等服务)。 - 主路由负责 DHCP,并强制分配网关为旁路由 IP。 - 旁路由防火墙保留默认规则,仅添加 NAT 伪装规则。 2. 若问题依旧,尝试重置 OpenWrt 至初始配置,逐步添加规则排查。 请根据实际网络结构调整配置,若仍有问题可提供更多细节(如主路由型号、OpenWrt 版本、网络拓扑)。
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值