首页 > 系统相关 >[架构之路-25]:目标系统 - 系统软件 - bootloader uboot内存映射与启动流程

[架构之路-25]:目标系统 - 系统软件 - bootloader uboot内存映射与启动流程

时间:2023-10-06 16:55:11浏览次数:35  
标签:25 uboot 函数 tag init gd 内核 bootloader

原文:https://blog.csdn.net/HiWangWenBing/article/details/127062057

目录

第1章 uboot概述

1.1 概述

1.2 内存映射(案例)

1.3 uboot在嵌入式系统启动中的位置

第2章 uboot启动流程(源码分析)

2.1 入口函数:_start

2.3 执行流程(文字描述)

2.4 初始化过程

第3章 uboot如何加载内核

3.1 vmlinuz/vmlinux、Image、zImage与uImage的区别

3.2 uboot启动内核的大致步骤

3.3 内核文件的三种状态

3.4 内核的加载与重定位

3.5 启动内核的相关命令

3.6 uboot如何给内核传递参数?bootargs

3.6 内核是如何拿到这些参数的?

第1章 uboot概述
1.1 概述
uboot是bootloader的一种,是Linux内核的引导启动程序。

它会初始化嵌入式平台上的一些外设(比如:ddr等),把Linux内核镜像从flash中加载到内存,在完成一些初始化工作后,最后启动Linux内核,类似于windows的BIOS程序。

uboot相当于是一段功能复杂的裸机程序,单片机的裸机程序没有本质区别。

下面将是对uboot启动流程的源码分析,此处使用的嵌入式平台芯片是NXP的 i.mx6ull 芯片(Cortex-A7内核,arm v7架构),uboot源码是NXP官方提供的4.1.15版本uboot。

下载地址:http://git.freescale.com/git/cgit.cgi/imx/uboot-imx.git/tag/?h=imx_v2016.03_4.1.15_2.0.0_ga&id=rel_imx_4.1.15_2.1.0_ga

1.2 内存映射(案例)


(1)有两个地方存放uboot的镜像:

ROM/FLASH/SD卡
SDRAM
(2)用户栈区(栈)

用于存放函数调用的上下文
局部变量
(3)IRQ堆栈区

用于执行中断服务程序的上下文(中断服务程序的嵌套)
(4)全局变量数据区

初始为0的全局数据区
(5)动态内存区(堆)

malloc内存区
free内存区
BSS段通常是指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前BSS段会自动清0。

1.3 uboot在嵌入式系统启动中的位置


(1)bootstrap =》 在Soc芯片内部,由soc芯片厂家提供

(2)bootloader =》 uboot

(3)OS内核 =》 Linux内核

(4)rootf根文件系统 =》存放各种工具、库、脚本、应用程序、数据等等。

(5)根文件夹系统中的应用程序 =》特定的应用程序

第2章 uboot启动流程(源码分析)
2.1 入口函数:_start
2.2 两阶段流程(流程图描述)

 

 

2.3 执行流程(文字描述)
(1)设置CPU为管理模式

(2)关看门狗

(3)关中断

(4)设置时钟频率

(5)关mmu,初始化各个bank

(6)进入board_init_f () 函数 =》能够访问内存RAM和串口相关的外设

初始化DDR
定时器
初始化波特率串口
打印前面暂存在缓冲区的数据
此时堆栈指针sp和gd指向DDR上了,而不是内部的RAM。
(7)重定位

uboot会将自己重定位到 DRAM最后面的地址区域,也就是将自己拷贝到 DRAM最后面的内存区域中。这么做的目的是给Linux腾出空间,防止 Linux kernel覆盖掉 uboot,将 DRAM前面(低位地址空间)的区域完整的空出来。

在拷贝之前肯定要给uboot各部分分配好内存位置和大小,比如 gd应该存放到哪个位置,malloc内存池应该存放到哪个位置等等。这些信息都保存在gd的成员变量中,因此要对 gd的这些成员变量做初始化。

最终形成 一个完整的内存“分配图”,在后面重定位uboot的时候就会用到这个内存“分配图”。

(8)copy

relocate_code代码重定位函数,负责将uboot从ROM拷贝到RAM新的地方,完成代码拷贝。

(9)清bss

(10)跳转到board_init_r() 函数 =》 能否访问Flash和网络相关外设

前面board_init_f函数里面会调用一系列的函数来初始化部分关键性的外设和gd的成员变量。

但是board_init_f并没有初始化所有的外设,还需要做一些后续工作,这些后续工作就是由函数
board_init_r (这里的外设更多包括EMMC,NANDFLASH)来完成的, 存放于common/board_r.c。

(11)启动流程结束

最后运行的是run_main_loop,主循环,处理命令输入。

2.4 初始化过程


2.5 代码树(代码描述)

Uboot启动流程:
_start <arch/arm/lib/vectors.S>
|--->reset <arch/arm/cpu/armv7/start.S>
| |--->save_boot_params <arch/arm/cpu/armv7/start.S>
| |--->save_boot_params_ret <arch/arm/cpu/armv7/start.S>
| | // 禁止中断,设置cpu的模式为SVC
| | // 清楚SCTLR的bit13,允许向量重定位,同时
| // 重定位向量表,把CP15的c12(VBAR)寄存器设置为
| // 0x87800000(uboot的起始地址,也是向量表的起始地址)
|
|--->cpu_init_cp15 <arch/arm/cpu/armv7/start.S>
| // 设置cp15相关内容,比如关闭mmu,cache
|
|--->cpu_init_crit <arch/arm/cpu/armv7/start.S>
| |
| |--->lowlevel_init <arch/arm/cpu/armv7/lowlevel_init.S>
| | // 设置栈指针sp = 0x0091FF00,属于MX6ULL
| | // 的内部ram,同时(sp - GD_SIZE(248))-->sp
| | // 留出global_data数据结构的位置sp = 0x0091FE08
| | // 设置sp-->r9, sp==r9
| |
| |--->s_init <arch/arm/cpu/armv7/mx6/soc.c>
| | // 空函数,直接返回
|
|--->_main <arch/arm/lib/crt0.S>
| // 设置sp为0x0091ff00,调用函数
| // board_init_f_alloc_reserve(arg:0x0091FF00)后,把sp设为
| // 此函数的返回值:0x0091FA00, r9(gd)设为0x0091FA00;
| // 调用board_init_f_init_reserve(arg:0x0091FA00)后
| // 把gb的成员malloc_base设为0x0091FB00(early_malloc的起始地址)
| // 调用board_init_f函数:会初始化gd,返回之后重新设置环境(sp和gd)
| // 把gd的成员start_addr(0x9EF44E90)赋值给sp, 此时sp == 0x9EF44E90
| // 是外部DDR的地址,gd->bd赋给r9(gd),新的gd结构在bd结构下面,
| // 重新设置gd = r9 - sizeof(*gd); lr = here. gd指向新的区域(DDR内)时
| // lr = here + 68,这是为什么?uboot的拷贝目的地址:0x9FF47000
|--->board_init_f_alloc_reserve(arg:0x0091FF) <common/init/board_init.c>
| // 在包含此函数的文件中有:DECLARE_GLOBAL_DATA_PTR;
| // 是个宏定义:#define DECLARE_GLOBAL_DATA_PTR \
| // register volatile gd_t *gd asm("r9")
| // 此函数设置留出早期malloc和global_data内存区域,
| // 返回值:0x0091FA00
|
|--->board_init_f_init_reserve(arg:0x0091FA00) <common/init/board_init.c>
| // 此函数用于初始化gd所指向的结构(清零处理)
| // 设置gd的成员malloc_base为0x91FB00
| // 就是early_malloc的起始地
|
|--->board_init_f <common/board_f.c>
| // 主要做两个工作:初始化一系列外设(串口、定时器等)
| // 初始化gd的各个成员变量(此时gd还保存在内部ocram中)。上面的工作都是通过在函数内运行
| // initcall_sequence_f函数表中的一些函数来实现的,此函数表与
| // board_init_f函数定义在相同的文件,是static属性的静态表
| // 表中的函数执行完后会把gd->mon_len设为0xA8E74(__bss_end-_start),
| // 也就是代码长度。gd->malloc_init设为0x400(malloc内存池的大小)
| // gd->ram_size:0x20000000 gd->ram_top:0xA0000000 gd->relocaddr:0x9FF47000
| // gd->arch.tlb_size:0x4000 gd->arch.tlb_addr:0x9FFF0000
|
|--->relocate_code(arg:0x9FF47000) <arch/arm/lib/relocate.S>
| // 代码拷贝。0x9FF47000是uboot拷贝目标首地址,offset=0x9FF47000-0x8780000,offset:0x18747000
| // 拷贝源地址:__image_copy_start=0x87800000,结束地址:__image_copy_end =0x8785dd54
| // 裸机程序运行需要链接地址与运行地址相同,uboot解决拷贝后的重定位问题是采用ld链接器
| // 链接时使用选项'-pie'生成位置无关的可执行文件,使用此选项时会生成一个.rel.dyn段,
| // uboot就是靠这个.rel.dyn来解决重定位问题的(.rel.dyn 段是存放.text 段中需要重定位地址的集合)
| // 修改.rel.dyn中的label来重定位
|
|--->relocate_vectors <arch/arm/lib/relocate.S>
| // 重定位向量表,将CP15的VBAR寄存器的值设为0x9FF47000,uboot拷贝后的目标首地址
|
|--->c_runtime_cpu_setup <arch/arm/cpu/armv7/start.S>
|
|--->board_init_r <common/board_r.c>
| | // 初始化一些在board_init_f函数中未初始化的一些外设,做些后续工作。
| | // 是通过运行init_sequence_r函数集合中的函数来实现的,init_sequence_r与board_init_f函数
| | // 在同一个文件。在函数集合中initr_reloc_global_data函数初始化重定位后的gd的一些成员变量
| | // 集合中的其他函数:初始化了malloc、串口、电源芯片、emmc、环境变量、LCD、初始化跳转表、中断,使能中断
| | // 初始化网络地址(获取MAC地址,通过读取环境变量ethaddr的值,环境变量保存在emmc中)、
| | // 初始化网络设备,最后执行run_main_loop函数,主循环(处理命令)
| |
| |--->run_main_loop <common/board_r.c>
| // uboot启动以后会进入3秒倒计时,如果在3秒倒计时结束之前按下按下回车键,那么就
| // 会进入uboot的命令模式,如果倒计时结束以后都没有按下回车键,那么就会自动启动Linux内
| // 核,这个功能就是由run_main_loop函数来完成的.
|
|--->main_loop(void) <common/main.c>
| | // 如果如果倒计时结束之前按下按键,那么就会执行cli_loop函数,这个就是
| | // 命令处理函数,负责接收好处理输入的命令。
| |
| |--->bootstage_mark_name
| | // 打印处启动进度
|
|--->autoboot_command
| // 此函数就是检查倒计时是否结束?倒计时结束之前有没有被打断?
|
|--->cli_loop <common/cli.c>
| | // cli_loop函数是uboot的命令行处理函数,我们在uboot中输入
| | // 各种命令,进行各种操作就是由cli_loop来处理的
| |
| |--->parse_file_outer
|
|--->setup_file_in_str
|
|--->parse_stream_outer
| // 这个函数就是 hush shell 的命令解释器,
| // 负责接收命令行输入,然后解析并执行相应的命令

第3章 uboot如何加载内核
3.1 vmlinuz/vmlinux、Image、zImage与uImage的区别
内核镜像和其他的镜像并没有本质上的区别,都是用同一套交叉编译工具链来生成的。

为了满足各种启动方式,编译后的内核提供多种不同类型的镜像。本质上和其他镜像都是一样的,只是在此基础上做了修改。

 

生成镜像的过程:

(1)编译生成vmlinuz/vmlinux

就是普通的elf可执行文件,嵌入式设备一般部署时不会用这种格式的镜像,因为体积太大,并且elf格式也不能直接烧录使用;//elf格式的文件可以在操作系统下执行,但不能在裸机上运行。

(2)将elf格式的vmlinuz/vmlinux变成bin格式的可烧录文件

用交叉编译工具链里的objcopy,将elf格式的vmlinuz/vmlinux变成bin格式的可烧录文件,名字为Image。bin文件是可以直接在裸机上运行的程序。

objcopy把几十M大的vmlinuz/vmlinux精简成了几M大小的Image,因此这个制作烧录镜像主要目的就是缩减大小,节省磁盘;

(3)zip压缩Image文件=》zImage

实际上Image已经可以直接烧录到flash中进行执行,但是人们觉得内核还是太大了,于是对Image进行压缩,再在压缩得到的文件前端加一段解压缩代码,这样就得到了zImage;(zImage= 解压缩代码 + Image压缩得到的文件)

(4)uboot专用的uImage (uboot Image)

uImage是用uboot中的mkimage工具根据zImage制作而来。

uImage是专门给uboot使用的,在zImage头部添加64个字节的头,说明这个内核的版本、加载位置、生成时间、大小等信息,其0x40之后与zImage没区别。

其中最重要的信息就是加载的内存地址信息,即把Linux kernel加载到内存什么地方。

uint32_t ih_load; /* Data Load Address */
uint32_t ih_ep; /* Entry Point Address */

// 如下是uImage的头部信息
typedef struct image_header {
uint32_t ih_magic; /* Image Header Magic Number */
uint32_t ih_hcrc; /* Image Header CRC Checksum */
uint32_t ih_time; /* Image Creation Timestamp */
uint32_t ih_size; /* Image Data Size */
uint32_t ih_load; /* Data Load Address */
uint32_t ih_ep; /* Entry Point Address */
uint32_t ih_dcrc; /* Image Data CRC Checksum */
uint8_t ih_os; /* Operating System */
uint8_t ih_arch; /* CPU architecture */
uint8_t ih_type; /* Image Type */
uint8_t ih_comp; /* Compression Type */
uint8_t ih_name[IH_NMLEN]; /* Image Name */
} image_header_t;

备注:

uboot都支持uImage,不一定支持zImage;
现在uImage的方式被逐渐设备树dtb的方式替代;
3.2 uboot启动内核的大致步骤
(1)编译、制作内核镜像

(2)把内核加载或存放到指定的位置(具体位置与启动方式相关)

(3)uboot加载内核镜像

uboot要通过如下的几种方式将特定位置的内核加载到内存的链接地址处;

读取SD卡上的内核镜像
读取flash上的内核镜像
通过tftp下载内核镜像
通过nfs下载内核镜像
(4)uboot 解析内核镜像,得到image_header_t的内容。

uboot区分出当前启动方式是zImage、uImage还是设备树方式。

然后,构建出描述该内核的image_header_t结构体;

(5)启动内核

将上一步得到的image_header_t结构体和内核所在地址,传入do_bootm_linux函数,启动Linux内核;
(6)内核接管CPU的控制权,uboot结束

boot->bootm->do_bootm_linux->内核启动,uboot结束。

3.3 内核文件的三种状态
(1)静态文件:以文件的方式存在于ROM中。

(2)静态内核:以可执行代码的方式存在于RAM中。

(3)动态执行:以正在运行状态存在与RAM中。

3.4 内核的加载与重定位
内核从外存加载到DDR RAM中,但还没有执行之前的这个过程就叫做重定位。

必须加载到DDR的特定地址(即编译链接时指定的物理地址),因为启动的时候就是去链接地址启动。

内核的重定位是uboot完成的,根据启动方式的不同,uboot可能从flash等外存去读取内核,也可能通过tftp、nfs等网络下载方式读取内核,但不管何种读取内核的方式,最终内核都是被加载到链接地址。

链接地址在编译脚本、环境变量bootcmd、配置文件的CONFIG_BOOTCOMMAND宏定义可以查到。

3.5 启动内核的相关命令
(1)boot

该命令会先将内核重定位,把内核下载到指定的内存空间中。

然后调用bootm命令从内存空间中启动内核;

(2)bootm

这是直接启动内核的命令,只能启动已经加载到DDR的内核,在调用时传入内核在DDR中的地址(一般是内核的链接地址)即可启动内核。

在bootm命令的实现代码里,其实主要完成的是启动方式的判断,判断出启动的操作系统类型后,完成初始化就会去调用相关操作系统的启动函数。

(3)do_bootm_linux函数:

这是启动linux系统的函数,功能包括:准备给内核的传参、找到内核程序入口、启动内核。

3.6 uboot如何给内核传递参数?bootargs
uboot 往内核携带的参数就是bootargs携带的,bootargs并不是一个固定长度、固定结构的数据结构,而是可变长度,可变内容的结构,称为“tag”。

bootargs=earlyprintk console=ttyS0,115200 rootwait nprofile_irq_duration=on root=/dev/ram0 rootfstype=ramfs rdinit=/linuxrc
bootcmd=run tftploadk
bootdelay=3

setenv bootargs 'mem=512M console=ttyAMA0,115200 clk_ignore_unused rw rootwait root=/dev/mmcblk0p5 rootfstype=ext4
blkdevparts=mmcblk0:1M(u-boot.bin),5M(kernel),512K(logo.bin),512K(logo.jpg),1000M(rootfs.ext4)'

(1)mem:设置操作系统内存大小。

以上设置mem=512M,表示分配给操作系统内存为512M。

(2)console:设置控制台设备。

格式为console=ttyAMA0,115200表示控制台为串口0,波特率115200。

(3)root:设置根文件系统rootfs挂载设备, 用来指定rootfs的位置

格式为root=/dev/mmcblk0p5表示从Flash第5个分区挂载(Flash分区编号从0开始)。

常见的rootfs的位置有:

root=/dev/ram rw

root=/dev/mtdx rw

root=/dev/mtdblockx rw

root=/dev/mtdblock/x rw

root=31:0x

root=/dev/nfs # rootfs在网络上。

(4)rootfstype:设置挂载文件系统类型

此处用的是ext4文件系统格式,或是ramfs。

(5)Linux内核初始化后的初始线程的位置

init指定的是内核启起来后,进入系统中运行的第一个脚本.

一般init=/linuxrc, 或者init=/etc/preinit,

preinit的内容一般是创建console,null设备节点。

运行init程序,挂载一些文件系统等等操作。

很多初学者以为,init=/linuxrc是固定写法,其实不然,/linuxrc指的是/目录下面的linuxrc脚本,一般是一个连接罢了。

通过该脚本,Linux内核可以自动创建应用程序环境和启动应用程序。

3.6 内核是如何拿到这些参数的?
由于uboot和内核其实是两个独立的程序,并不能通过函数调用传递参数。

另外,uboot传递给内核的参数的内容并不是固定的结构体。那么内核是如何获取从uboot获取到可变个数的参数呢?

Linux内核发明了一种称为“Tag”的参数传递的方式:

(1)struct tag。tag是一个数据结构,在uboot和linux kernel中都有定义tag数据机构,而且定义是一样的。

(2)如setup_serial_tag -> tag,在uboot\include\asm-arm里面

(3)tag_header和tag_xxx。tag_header中有这个tag的size和类型编码,kernel拿到耦合tag后先分析tag_header得到tag的类型和大小,然后将tag中剩下部分当做一个tag_xxx来处理。类似TLV编码的数据。

(4)tag_start与tag_end。

kernel接收到的传参是若干个tag构成的,这些tag有tag_start起始,到tag_end结束。

所有要传递的tag类型的参数,都会被封装在【tag_start】和【tag_end】之间。

(5)tag传参的方式是由linux kernel发明的,kernel定义了这种向我传参的方式,uboot只是遵循了这种传参的方式给kernel传参罢了。

tag 是一个数据结构:stract tag 这种数据格式在uboot和kernel是一样的,也就是说uboot在启动的时候将需要传给kernel的参数放在了DDR的某个地址处,而存放格式就是tag格式,然后kernel就会到这个地址去读取这些参数,读取方式也是按tag格式去读取的。

简单来说tag就像是一个数组一样,是一块连续的内存,里面存放了uboot需要传递给kernel的参数信息,我们通过定义一个指针params(static struct tag *params;),先使其指向tag的存放地址(30000100,uboot也就是说在30000100的地方存放了一条信息,告诉kernel去0x54410001这个地方去读取tag(也就是uboot传递给kernel的参数))

uboot最终通过调用theKernel (0, machid, bd->bi_boot_params); 函数来执行linux内核的,uboot调用这个函数(其实就是linux内核)时传递了三个参数。

theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep);


这三个参数就是uboot直接传递给linux内核,这3个参数是通过寄存器来实现的传参的:

第一个参数0就放在r0中,
第二个参数(机器码)放在r1中,
第三个参数放在r2中(第三个参数传递的就是tag的首地址这里是30000100)
最后,把内核的地址传递给pc指针,就跳转到内核代码执行程序了。
————————————————
版权声明:本文为CSDN博主「文火冰糖的硅基工坊」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/HiWangWenBing/article/details/127062057

标签:25,uboot,函数,tag,init,gd,内核,bootloader
From: https://www.cnblogs.com/bruce1992/p/17744715.html

相关文章

  • 9.25
    上午的工程实训很有意思,自己动手做一个电子元件,虽然一开始啥都不知道看着挺难,但真正上手之后才发现有意思的部分居多,最重要的是自己做的电子元件还可以自己带回去,当个纪念品很有成就感,下午建民老师的java指导就很折磨了,讲的部分大多都能听懂,最后自己动手写问题的时候是一点头绪都......
  • [ABC257F] Teleporter Setting 题解
    1.题目洛谷传送门2.思路我们可以把不确定的点当成真实存在的\(0\)号点,建边的时候就正常连即可。然后我们来看一个样例:1-2-03-4-5当我们把\(0\)号点看成\(3\)号点时,答案就是\(1\)号点到\(0\)号点的距离加上\(3\)号点到\(5\)号点的距离。然后我们再......
  • 72ed 2023/8/25 点分治学习笔记
    起因&介绍8月22号的T3是道黑,但思路却不算太难,就去打了这是第一次接触点分治,其实之前也有过一道点分治题,叫阴阳,但当时没去改,就一拖拖了半年才学点分治类似于树形DP,但在一些地方上处理有不同就比如在跑过根结点(1),进入处理它的子树时,会将其他的一部分视作没有(emmm大概这个意思,子树......
  • P1025 [NOIP2001 提高组] 数的划分 题解
    题目传送门本题共有两种方法,分别是递归深搜和动态规划方法一:递归深搜Solution从小到大一一枚举每一个划分的数,。只要找到一种方案就记录,具体细节代码中有注释。Code#include<bits/stdc++.h>usingnamespacestd;intn,k,ans;voiddfs(intstart,intstep,intsum){......
  • 跟着思兼学习Klipper(25)提高 Klipper 进程优先级减少报错
    前言原创文章,转载引用请务必注明链接,水平有限,如有疏漏,欢迎指正交流。文章如有更新请访问DFRobot社区或者cnblogs博客园。欢迎对Klipper固件,以及对改版CNC加工的Voron三叉戟、v0、v2.4感兴趣的朋友加群交流(QQGroup:490111638)由于Klipper主要采用滚动更新机制(小......
  • 水星 Mercury MIPC251C-4 网络摄像头 ONVIF 与 PTZ 云台控制
    概况最近在什么值得买上发现一款水星的网络摄像头,除了支持云台/夜视功能之外,还标明支持onvif协议.所以想着买来接入到HomeAssistat作为监控使用.可到手之后发现事情并没有那么简单,记录如下.接入HomeAssistant按照HA的文档 ONVIFCamera 接入无非就是配置文......
  • k8s1.25安装
    环境初始化yuminstallbash-completionvimntpdateiptableslrzszepel-release-y&&execbashsystemctlstopfirewalldsystemctldisabledfirewalldsetenforce0sed-i's/=enforcing/=disabled/g'/etc/selinux/configdocker#step1:安......
  • 使用J4125主机搭建个人微型服务器
    使用J4125主机搭建个人微型服务器对于个人开发者而言,一个稳定可靠的服务器通常是不可或缺的。然而,云服务器的价格却让许多人望而却步。我曾通过白嫖阿里云服务提供给学生的六个月(?)免费公网服务器搭建WEB服务,在其已然过期许久的今天,我选择了一个经济且足够运行虚拟化的解决方案—......
  • 【笔记】P2542 [AHOI2005] 航线规划 答辩做法
    洛谷上是可以过掉的。NFLSOJ上加强数据,还卡常,所以90pts。首先倒着做很好想。对于最终的图,我们可以tarjan缩点然后建树,边权为\(1\),表示一条割边。然后每次连两个点的时候就把树上这一段路径赋值为\(0\)。查询就是树上路径和。这些操作都可以点赋边权然后树剖来做。所以你就得......
  • S32Kxxx bootloader 之 LIN UDS bootloader
    了解更多关于bootloader的C语言实现,请加我Q扣:1273623966(验证信息请填bootloader),欢迎咨询或定制bootloader(在线升级程序)。LIN总线是汽车ECU使用比较多的一种总线,车灯,车门,汽车空调控制面板等等ECU都有在使用.而这些ECU离线升级时,就需要使用到LINbootloader,O......