ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MTK平台LK启动流程详解:从Preloader到内核加载的底层开发与调试

MTK平台LK启动流程详解:从Preloader到内核加载的底层开发与调试 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段跳转到kmainkmain是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初始化流程包括配置GPIO复用功能把对应的引脚设为UART模式使能UART时钟设置波特率通常是115200配置数据位、停止位、校验位8N1使能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初始化流程包括配置eMMC控制器的时钟和电源发送CMD0复位卡发送CMD1获取卡的状态发送CMD2获取CID发送CMD3设置RCA发送CMD9获取CSD选择卡CMD7设置总线宽度CMD6或ACMD6设置高速模式这些步骤在LK的mmc.c或sdhci.c中有实现。MTK的eMMC控制器通常是SDHCI兼容的但有一些MTK特有的寄存器需要配置。分区表方面MTK平台通常使用GPT分区表。LK会读取GPT找到boot、recovery、system等分区的位置。如果你在LK里读不到分区可能是GPT解析出了问题或者存储控制器的初始化不完整。注意MTK平台的分区布局在不同Android版本和芯片平台上会有差异。Android 10以后很多平台采用了动态分区Dynamic PartitionLK需要支持super分区的解析。这部分逻辑在LK的partition.c中调试时需要特别关注。3.4 显示初始化让用户看到第一眼LK阶段通常要显示开机Logo。MTK平台的显示子系统包括Display ControllerDISP、MIPI DSI控制器、以及可能的GPU。LK一般只初始化DISP和DSI不涉及GPU。显示初始化的流程大致是配置显示相关的GPIO如背光、复位初始化MIPI DSI控制器配置Display Controller的时序参数加载Logo图片到FrameBuffer使能显示输出MTK的显示初始化代码通常在platform/mtXXXX/disp/目录下。不同屏幕的时序参数不同需要根据屏幕规格书配置。如果Logo显示异常花屏、偏移、颜色不对通常是时序参数或FrameBuffer格式配置有误。我遇到过因为DSI的Lane数配置错误导致Logo只显示一半的情况。排查时可以用示波器量MIPI信号或者对比正常板子的寄存器配置。3.5 按键与PMIC初始化响应用户输入LK需要初始化按键和PMICPower Management IC以便检测用户是否按下了特定组合键如音量上电源键进入Recovery音量下电源键进入Fastboot。MTK平台的按键通常通过PMIC的GPIO或专门的Keypad控制器读取。PMIC初始化包括通过I2C或SPI与PMIC通信配置PMIC的寄存器设置各路电源的输出电压配置按键检测相关的寄存器这里有一个常见问题如果PMIC通信失败LK可能无法正确读取按键状态导致无法进入Recovery或Fastboot。排查时可以先确认I2C总线是否正常PMIC的地址是否正确。4. 从LK到内核加载与跳转的完整过程4.1 内核镜像的读取与解压LK完成硬件初始化后下一步是从boot分区读取内核镜像。MTK平台的boot分区通常包含Kernel Image通常是Image.gz或Image.lz4Ramdiskramdisk.imgDevice Tree Blobdtb可能的签名信息LK需要解析boot分区的头部boot_img_hdr找到各个组件的位置和大小然后把它们加载到内存中的指定地址。内核镜像通常是压缩的gzip或lz4LK需要解压。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会执行最后的跳转。跳转前需要关闭MMU和Cache或者保持内核期望的状态设置CPU寄存器x0或r0为DTB地址x1或r1为机器类型ARM32x2或r2为ATAG地址如果有跳转到内核入口地址在ARM64平台上跳转时x0存放DTB的物理地址x1到x3保留为0。内核入口通常是KERNEL_LOAD_ADDR加上一个固定偏移。跳转后LK的使命就结束了。内核会接管一切继续完成剩余的初始化。4.4 内核启动后的验证内核启动后可以通过串口log确认是否正常。关键信息包括Booting Linux on physical CPU 0x0内核开始执行Machine model: MTXXXXDTB被正确解析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支持格式化输出但要注意不要在中断上下文中调用否则可能导致不可预期的问题。
返回列表