☰
MTK平台LK启动流程详解:从Preloader到内核加载的底层开发与调试
2026/9/29 3:02:47 网站建设 项目流程

1. 从一个"卡在开机Logo"的板子说起

如果你在MTK平台上做过底层开发,大概率遇到过这样的场景:板子通电,串口终端刷出一行行log,突然停在某个位置不动了,屏幕上的开机Logo亮着,但系统就是起不来。这时候你心里清楚,问题出在LK这一层。

LK是Little Kernel的缩写,在MTK平台的Android系统中,它承担着从Preloader交接过来的接力棒,完成硬件初始化、显示Logo、进入Fastboot或Recovery模式、最终把Linux内核加载起来这一整套动作。很多人把它简单理解成"Bootloader",但LK在MTK平台上的角色比通用Bootloader要复杂得多——它要处理MTK特有的硬件模块、要跟Preloader约定好内存布局、要适配不同芯片平台的显示和存储控制器。

这篇文章面向的是做MTK平台底层开发、系统移植、或者遇到开机启动问题的工程师。我会从LK的启动入口开始,一路讲到内核加载完成,把每个阶段做了什么、为什么这么做、容易在哪里出问题,都拆开来讲清楚。中间会穿插一些我在实际项目中踩过的坑和调试技巧,希望能帮你少走弯路。

提示:本文讨论的内容基于MTK平台常见的LK实现,不同芯片型号(如MT6765、MT6785、MT6833等)在细节上会有差异,但整体框架是一致的。具体寄存器地址和配置请以你手上的芯片Datasheet和BSP代码为准。

2. LK在MTK启动链路中的位置与交接机制

2.1 从Preloader到LK:一次精心设计的接力

MTK平台的启动链路跟高通平台有本质区别。高通平台通常走PBL→SBL→ABL→Kernel的路径,而MTK平台是BootROM→Preloader→LK→Kernel。这个差异不是随便设计的,背后有MTK对芯片成本和启动速度的考量。

BootROM是固化在芯片内部的一小段代码,芯片上电后最先执行。它的任务极其有限:初始化最基本的时钟和SRAM,然后根据启动引脚(通常是EINT或GPIO的组合)判断从哪个介质启动——eMMC、UFS、NAND还是SD卡。BootROM会把Preloader从存储介质的前几个扇区读出来,放到内部SRAM中执行。

Preloader是MTK平台特有的一个阶段,它的核心任务是初始化DRAM控制器。为什么这一步不能放在LK里做?因为LK本身需要被加载到DRAM中运行,而DRAM没初始化之前根本用不了。所以Preloader必须先在SRAM这个"小房子"里把DRAM这个"大仓库"建好,然后把LK搬进去。

Preloader完成DRAM初始化后,会从存储介质中读取LK的镜像(通常是lk.bin),加载到DRAM的指定地址,然后跳转过去执行。这个地址在MTK的Memory Layout中有明确定义,不同平台可能不同,但通常在DRAM起始地址偏移一段固定位置。

这里有一个关键点:Preloader和LK之间通过一个叫做boot_tag或boot argument的结构体传递信息。这个结构体里包含了DRAM大小、启动模式(正常启动、Recovery、Fastboot)、串口配置等。如果你在LK里发现某些硬件信息读不到,很可能就是Preloader没有正确填充这个结构体。

2.2 LK的入口:kmain之前发生了什么

LK的入口函数是kmain,但在到达kmain之前,还有一段汇编代码要做最基础的准备。这段代码通常在arch/arm/crt0.S或类似文件中,做的事情包括:

  • 设置CPU异常向量表
  • 初始化栈指针
  • 关闭MMU和Cache(或者根据需要开启)
  • 清零BSS段
  • 跳转到kmain

kmain是LK真正意义上的主函数,它位于kernel/main.c中。这个函数的执行流程大致如下:

void kmain(void) { // 1. 早期平台初始化 lk_early_init(); // 2. 初始化堆内存 heap_init(); // 3. 初始化线程系统 thread_init(); // 4. 创建主线程 thread_resume(thread_create("bootstrap", &bootstrap, NULL, DEFAULT_PRIORITY, DEFAULT_STACK_SIZE)); // 5. 进入调度循环 thread_become_idle(); }

bootstrap线程才是真正干活的地方。它会依次调用platform_early_init、platform_init、target_init等函数,完成平台相关的硬件初始化。这些函数的实现就在MTK的target目录下,比如target/msmXXXX/或platform/mtXXXX/。

2.3 为什么MTK要把启动分成这么多阶段

有人可能会问:为什么不把Preloader和LK合并成一个阶段?这样不是更快吗?

原因有几个。第一,SRAM容量有限,Preloader必须足够小才能塞进去,而LK功能复杂,代码量大,必须放到DRAM中运行。第二,MTK的芯片出货量巨大,不同客户可能需要不同的LK定制,而Preloader保持相对稳定,这样有利于生产和维护。第三,分阶段设计可以让每个阶段的调试相对独立,出问题时更容易定位是Preloader的问题还是LK的问题。

在实际项目中,我遇到过Preloader传给LK的DRAM大小参数不对,导致LK只识别到一半内存的情况。这种问题在串口log里通常表现为LK打印的内存大小跟实际不符,或者内核启动后dmesg里显示的内存总量偏小。排查方法是在LK里打印boot_tag结构体的内容,跟Preloader的配置对比。

3. 硬件初始化:LK到底在初始化什么

3.1 串口初始化:调试的第一扇窗

LK启动后最早做的事情之一就是初始化串口。没有串口,后面的调试基本无从谈起。MTK平台的串口通常是8250兼容的UART,初始化流程包括:

  1. 配置GPIO复用功能,把对应的引脚设为UART模式
  2. 使能UART时钟
  3. 设置波特率(通常是115200)
  4. 配置数据位、停止位、校验位(8N1)
  5. 使能FIFO

在MTK的代码中,串口初始化通常在platform_early_init中完成。不同平台的UART基地址不同,比如MT6765的UART0基地址是0x11002000,而MT6785可能是0x11002000(具体以Datasheet为准)。

这里有一个容易踩的坑:如果你在platform_early_init之前就调用了dprintf,串口还没初始化,log是打不出来的。所以调试早期启动问题时,要确认串口初始化确实执行到了。

另一个坑是GPIO复用配置。MTK的GPIO控制器(GPIO Mode Controller)需要正确设置才能让引脚工作在UART模式。如果GPIO配置不对,串口就是没输出。我遇到过因为GPIO默认模式跟UART冲突,导致串口完全无输出的情况,最后查了半天才发现是GPIO的dws配置问题。

3.2 时钟与电源管理:让芯片跑起来

LK需要初始化主要的时钟源和电源域。MTK平台的时钟系统比较复杂,有PLL、分频器、门控时钟等多层结构。LK通常只初始化必要的时钟,比如:

  • 主PLL:提供CPU和总线时钟
  • 存储控制器时钟:eMMC/UFS需要
  • 显示控制器时钟:如果要显示Logo
  • 串口时钟:调试用

电源管理方面,LK需要确保各个电源域(Power Domain)处于正确状态。MTK的芯片通常有多个电源域,比如MCU域、GPU域、显示域等。LK阶段一般只打开必要的域,其他域留给内核管理。

这里有一个经验:如果你在LK阶段发现某个外设访问不了,先检查它的时钟和电源域是否使能。MTK的很多外设如果时钟没开,寄存器读写会直接挂死或者返回全0。

3.3 存储控制器初始化:找到你的系统

存储控制器初始化是LK的关键任务之一。MTK平台支持eMMC、UFS、NAND等多种存储介质。LK需要根据Preloader传递的信息,初始化对应的控制器,然后读取分区表。

对于eMMC,初始化流程包括:

  1. 配置eMMC控制器的时钟和电源
  2. 发送CMD0复位卡
  3. 发送CMD1获取卡的状态
  4. 发送CMD2获取CID
  5. 发送CMD3设置RCA
  6. 发送CMD9获取CSD
  7. 选择卡(CMD7)
  8. 设置总线宽度(CMD6或ACMD6)
  9. 设置高速模式

这些步骤在LK的mmc.c或sdhci.c中有实现。MTK的eMMC控制器通常是SDHCI兼容的,但有一些MTK特有的寄存器需要配置。

分区表方面,MTK平台通常使用GPT分区表。LK会读取GPT,找到boot、recovery、system等分区的位置。如果你在LK里读不到分区,可能是GPT解析出了问题,或者存储控制器的初始化不完整。

注意:MTK平台的分区布局在不同Android版本和芯片平台上会有差异。Android 10以后,很多平台采用了动态分区(Dynamic Partition),LK需要支持super分区的解析。这部分逻辑在LK的partition.c中,调试时需要特别关注。

3.4 显示初始化:让用户看到第一眼

LK阶段通常要显示开机Logo。MTK平台的显示子系统包括Display Controller(DISP)、MIPI DSI控制器、以及可能的GPU。LK一般只初始化DISP和DSI,不涉及GPU。

显示初始化的流程大致是:

  1. 配置显示相关的GPIO(如背光、复位)
  2. 初始化MIPI DSI控制器
  3. 配置Display Controller的时序参数
  4. 加载Logo图片到FrameBuffer
  5. 使能显示输出

MTK的显示初始化代码通常在platform/mtXXXX/disp/目录下。不同屏幕的时序参数不同,需要根据屏幕规格书配置。如果Logo显示异常(花屏、偏移、颜色不对),通常是时序参数或FrameBuffer格式配置有误。

我遇到过因为DSI的Lane数配置错误,导致Logo只显示一半的情况。排查时可以用示波器量MIPI信号,或者对比正常板子的寄存器配置。

3.5 按键与PMIC初始化:响应用户输入

LK需要初始化按键和PMIC(Power Management IC),以便检测用户是否按下了特定组合键(如音量上+电源键进入Recovery,音量下+电源键进入Fastboot)。

MTK平台的按键通常通过PMIC的GPIO或专门的Keypad控制器读取。PMIC初始化包括:

  1. 通过I2C或SPI与PMIC通信
  2. 配置PMIC的寄存器,设置各路电源的输出电压
  3. 配置按键检测相关的寄存器

这里有一个常见问题:如果PMIC通信失败,LK可能无法正确读取按键状态,导致无法进入Recovery或Fastboot。排查时可以先确认I2C总线是否正常,PMIC的地址是否正确。

4. 从LK到内核:加载与跳转的完整过程

4.1 内核镜像的读取与解压

LK完成硬件初始化后,下一步是从boot分区读取内核镜像。MTK平台的boot分区通常包含:

  • Kernel Image(通常是Image.gz或Image.lz4)
  • Ramdisk(ramdisk.img)
  • Device Tree Blob(dtb)
  • 可能的签名信息

LK需要解析boot分区的头部(boot_img_hdr),找到各个组件的位置和大小,然后把它们加载到内存中的指定地址。

内核镜像通常是压缩的(gzip或lz4),LK需要解压。MTK的LK中通常集成了libz或liblz4,解压后的内核放到KERNEL_LOAD_ADDR。

这里有一个关键点:内核加载地址必须跟内核编译时的配置一致。如果地址不对,内核启动后会立即崩溃。MTK平台的内核加载地址通常在platform/mtXXXX/rules.mk或target/.../rules.mk中定义。

4.2 Device Tree的传递

Android系统使用Device Tree来描述硬件。LK需要把DTB加载到内存,并在跳转内核时把DTB的地址传递给内核。

MTK平台的DTB通常跟内核一起打包在boot分区中,或者单独放在dtbo分区。LK需要根据当前硬件版本(通过ADC读取或GPIO判断)选择正确的DTB。

如果DTB选择错误,内核可能无法识别某些硬件,导致驱动加载失败。我遇到过因为DTB不匹配,导致触摸屏完全没反应的情况。排查时可以在内核log中搜索of_相关的错误信息。

4.3 跳转到内核:最后的交接

一切准备就绪后,LK会执行最后的跳转。跳转前需要:

  1. 关闭MMU和Cache(或者保持内核期望的状态)
  2. 设置CPU寄存器:x0或r0为DTB地址,x1或r1为机器类型(ARM32),x2或r2为ATAG地址(如果有)
  3. 跳转到内核入口地址

在ARM64平台上,跳转时x0存放DTB的物理地址,x1到x3保留为0。内核入口通常是KERNEL_LOAD_ADDR加上一个固定偏移。

跳转后,LK的使命就结束了。内核会接管一切,继续完成剩余的初始化。

4.4 内核启动后的验证

内核启动后,可以通过串口log确认是否正常。关键信息包括:

  • Booting Linux on physical CPU 0x0:内核开始执行
  • Machine model: MTXXXX:DTB被正确解析
  • Memory: XXXXXXK/XXXXXXK available:内存大小正确
  • Kernel command line: ...:命令行参数正确

如果内核卡在某个位置,可以根据log定位问题。常见的问题包括:

现象可能原因排查方法
内核完全无输出加载地址错误、DTB地址错误检查LK跳转前的寄存器值
内核输出乱码串口波特率不匹配确认内核命令行中的console参数
内存大小不对boot_tag中DRAM信息错误对比Preloader和LK的内存配置
驱动加载失败DTB不匹配或时钟未使能检查内核log中的of_和clk_错误

5. 调试实战:那些年我踩过的LK坑

5.1 串口无输出:从GPIO查起

有一次拿到一块新板子,上电后串口完全没输出。按照常规思路,先确认串口线、波特率、串口终端配置都没问题。然后量UART TX引脚,发现没有波形。

这时候就要怀疑GPIO配置了。MTK平台的GPIO默认模式可能不是UART,需要在Preloader或LK中配置。我查了dws文件,发现UART TX引脚的默认模式被设成了GPIO输入,而不是UART输出。修改dws配置后,串口正常输出。

这个问题的教训是:MTK平台的GPIO配置非常关键,尤其是dws文件中的默认模式。如果硬件设计跟参考设计不同,一定要仔细检查每个引脚的复用配置。

5.2 Logo显示花屏:时序参数的锅

另一块板子,LK阶段Logo显示花屏,但内核启动后显示正常。这说明LK的显示初始化有问题。

对比内核的显示驱动和LK的显示初始化代码,发现LK中MIPI DSI的时序参数跟内核中的不一致。具体是hsync和vsync的极性配置反了。修改后Logo显示正常。

这个问题的启示是:LK的显示初始化代码往往是从内核驱动移植过来的,但可能没有同步更新。如果遇到显示问题,可以对比内核中的时序参数。

5.3 无法进入Fastboot:按键检测的坑

有客户反馈,板子无法通过按键进入Fastboot模式。检查LK的按键检测代码,发现按键的GPIO配置跟实际硬件不符。硬件上按键接在PMIC的GPIO上,但LK代码中配置的是SoC的GPIO。

修改按键检测代码,改为通过PMIC读取按键状态后,问题解决。这个案例说明,MTK平台的按键检测可能涉及PMIC和SoC两套GPIO系统,需要根据硬件设计正确配置。

5.4 内核加载失败:地址对齐问题

有一次修改了内核加载地址,结果内核启动后立即崩溃。检查发现新的地址没有按2MB对齐。ARM64内核要求加载地址按2MB对齐,否则会触发对齐异常。

修改地址为2MB对齐后,内核正常启动。这个坑提醒我们,修改内核加载地址时一定要注意对齐要求。

6. 进阶话题:LK的定制与优化

6.1 加快LK启动速度

LK阶段的启动时间直接影响整机开机时间。优化LK启动速度可以从几个方面入手:

  • 减少不必要的硬件初始化:比如如果不需要在LK阶段显示Logo,可以跳过显示初始化
  • 延迟初始化:把一些不紧急的初始化放到内核阶段
  • 优化存储读取:使用更快的读取模式(如eMMC的HS400模式)
  • 减少log输出:串口log输出会占用时间,量产版本可以关闭详细log

在实际项目中,我通过关闭LK的详细log和延迟显示初始化,把LK阶段的时间从800ms降到了400ms左右。

6.2 Fastboot功能的定制

LK中的Fastboot功能可以定制,比如添加自定义的fastboot命令、修改fastboot的USB VID/PID等。MTK的LK中,Fastboot的实现通常在app/fastboot/目录下。

如果你需要添加自定义命令,可以在fastboot.c中注册回调函数。比如添加一个读取GPIO状态的命令:

static void cmd_gpio_read(const char *arg, void *data, unsigned sz) { // 解析参数,读取GPIO,返回结果 fastboot_info("GPIO value: %d", gpio_get_value(gpio_num)); fastboot_okay(""); } // 注册命令 fastboot_register("oem gpio-read", cmd_gpio_read);

6.3 与OTA的配合

LK在OTA升级中扮演重要角色。OTA升级时,系统会写入新的boot分区,然后重启进入Recovery或直接重启。LK需要能够正确识别新的boot分区内容,并加载新的内核。

在A/B分区系统中,LK需要根据当前slot选择正确的boot分区。MTK的LK中通常有slot_select相关的逻辑,通过读取misc分区中的信息来决定从哪个slot启动。

如果OTA后无法启动,可能是LK没有正确切换slot,或者新的boot分区内容有问题。排查时可以检查misc分区的内容,以及LK中slot选择的log。

7. 一些实用的调试技巧和工具

7.1 串口log的抓取与分析

串口log是调试LK最重要的工具。建议使用minicom、picocom或screen等工具,波特率设为115200。抓取log时最好保存到文件,方便后续分析。

分析log时,可以关注几个关键点:

  • LK的版本和编译时间
  • Preloader传递的boot_tag信息
  • 各个硬件初始化的结果
  • 内核加载的地址和大小
  • 跳转前的寄存器状态

7.2 使用JTAG调试

如果串口log无法定位问题,可以使用JTAG调试器(如Lauterbach、J-Link)连接芯片,单步调试LK代码。JTAG可以看到CPU寄存器、内存内容,对于分析跳转失败、死循环等问题非常有用。

MTK平台通常需要专用的JTAG调试器和配置文件。使用前需要确认芯片的JTAG引脚已经正确引出。

7.3 分析LK的镜像

LK的镜像(lk.bin)可以通过objdump或readelf分析。比如查看LK的入口地址:

arm-none-eabi-objdump -f lk.bin

或者反汇编某个函数:

arm-none-eabi-objdump -d lk.elf | grep -A 50 "<kmain>"

这些工具可以帮助你理解LK的内部结构,定位问题时更有针对性。

7.4 常见问题速查表

问题可能原因解决方法
LK完全无输出串口未初始化、GPIO配置错误检查串口初始化和GPIO复用
LK卡在某个初始化硬件未就绪、时钟未使能检查对应硬件的时钟和电源
无法读取分区存储控制器初始化失败检查eMMC/UFS的初始化和GPT解析
Logo显示异常显示时序参数错误对比内核驱动的时序参数
内核加载失败加载地址错误、镜像损坏检查加载地址和对齐,验证镜像完整性
跳转内核后无输出DTB地址错误、寄存器设置错误检查跳转前的寄存器值

8. 写在最后

LK这一层看起来只是启动流程中的一个环节,但实际做起来,涉及的知识面非常广:从CPU架构、硬件初始化、存储协议,到内核加载、设备树、调试技巧。每一个环节出问题,都可能导致板子起不来。

我在实际项目中最大的体会是:串口log一定要抓全,而且要会看。很多问题的答案就在log里,只是需要你知道该关注哪些关键字。另外,对比法是排查问题的利器——拿一块正常的板子和一块有问题的板子,对比log、对比寄存器、对比配置,差异点往往就是问题所在。

还有一点:MTK平台的文档相对分散,很多细节需要看代码才能确认。建议在做移植或调试时,把相关的LK代码通读一遍,理解每个函数的意图,这样遇到问题时才能快速定位。

最后分享一个小技巧:如果你在LK中需要打印调试信息,但又不想影响正常log的阅读,可以用不同的前缀,比如[DEBUG],这样在log中搜索起来很方便。另外,LK的dprintf支持格式化输出,但要注意不要在中断上下文中调用,否则可能导致不可预期的问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询