1. RK3568平台Linux内核启动流程深度解析
作为一名长期从事嵌入式Linux开发的工程师,我经常需要深入理解内核启动机制。今天以瑞芯微RK3568平台为例,带大家完整走一遍Linux内核的启动流程。这个流程对于BSP开发、驱动调试和系统优化都至关重要。
RK3568作为一款中高端ARM处理器,其启动流程具有典型性。我们将从链接脚本分析开始,逐步追踪到第一个C语言函数的执行。过程中会穿插我在实际开发中积累的调试技巧和常见问题解决方法。
2. 内核镜像构建基础
2.1 链接脚本vmlinux.lds解析
链接脚本是理解内核启动的第一把钥匙。在RK3568的BSP包中,关键文件位于arch/arm/kernel/vmlinux.lds。这个文件决定了各个段(section)在内核镜像中的布局。
重点关注几个关键段:
lds复制SECTIONS
{
. = 0xC0000000 + 0x00008000;
.head.text : {
_text = .;
KEEP(*(.head.text))
}
.text : { /* 常规代码段 */ }
/* 其他段... */
ENTRY(stext)
}
这里有几个关键点需要注意:
- 起始地址
0xC0000000 + 0x00008000是ARM Linux的传统内核虚拟地址 .head.text段包含启动相关的汇编代码ENTRY(stext)指明了入口点为stext符号
实际开发中,我曾遇到过因链接地址配置错误导致内核无法启动的情况。建议在修改链接脚本后,用
arm-linux-gnueabihf-objdump -x vmlinux验证段布局。
2.2 内核镜像组成
RK3568的内核镜像通常包含以下几个部分:
- 内核头部信息(可选的ARM特定头部)
- 压缩的内核镜像(如zImage)
- 设备树blob(dtb)
使用mkimage工具打包时,典型的命令如下:
bash复制mkimage -n rk3568 -A arm -O linux -T kernel -C none -a 0x00280000 -e 0x00280000 -d zImage uImage
参数说明:
-a 0x00280000:加载地址,需与bootloader配置一致-e 0x00280000:入口地址,通常与加载地址相同
3. 汇编启动阶段分析
3.1 head.S启动代码
如链接脚本所示,启动始于arch/arm/kernel/head.S的stext符号。这是内核执行的第一个函数,主要完成以下工作:
assembly复制ENTRY(stext)
/* 确保处于SVC模式且中断关闭 */
safe_svcmode_maskall r9
/* 读取处理器ID并检查是否支持 */
mrc p15, 0, r9, c0, c0 @ 获取处理器ID
bl __lookup_processor_type @ 检查处理器支持
/* 设置临时页表 */
bl __create_page_tables
/* 跳转到C语言入口 */
ldr r13, =__mmap_switched @ 设置返回地址
b __enable_mmu
ENDPROC(stext)
关键操作解析:
- 处理器模式设置:确保CPU处于特权模式
- 处理器类型检查:验证RK3568的Cortex-A55核心是否被支持
- 临时页表创建:为MMU启用做准备
- MMU启用:开启虚拟地址转换
3.2 处理器检测机制
__lookup_processor_type函数通过对比CPU ID与预定义的处理器列表来验证支持情况。RK3568使用的Cortex-A55核心的ID为0x410FD041。
在内核源码中,处理器列表定义在arch/arm/mm/proc-v7.S:
assembly复制.section ".proc.info.init", #alloc, #execinstr
.macro __v7_proc name, initfunc, mm_mmuflags = 0, io_mmuflags = 0
.long 0x410fd041 @ Cortex-A55
.long 0xff0ffff0
/* 其他字段... */
.endm
调试技巧:当遇到处理器检测失败时,可以通过JTAG读取CP15寄存器验证实际CPU ID。
4. 关键初始化流程
4.1 MMU初始化过程
__enable_mmu函数负责开启MMU,这是启动过程中的关键转折点。主要步骤包括:
- 加载页表基地址到TTBR0
- 设置域访问控制(DACR)
- 配置系统控制寄存器(SCTLR)
- 执行
__turn_mmu_on完成切换
RK3568的MMU配置需要注意:
- 页表采用L1 section映射(1MB大小)
- 初始只映射内核代码和数据区域
- 内存属性使用共享设备(Shareable Device)属性
4.2 切换到C语言环境
__mmap_switched是汇编到C语言的过渡点,主要完成:
- 清零BSS段
- 保存处理器ID和机器类型
- 设置初始栈指针
- 跳转到
start_kernel
典型实现如下:
assembly复制__mmap_switched:
/* 清零BSS段 */
ldr r4, =__bss_start
ldr r5, =__bss_stop
mov r6, #0
1: cmp r4, r5
strcc r6, [r4], #4
bcc 1b
/* 设置栈指针 */
ldr sp, =init_thread_union + THREAD_START_SP
/* 跳转到C入口 */
b start_kernel
5. 主要C语言初始化
5.1 start_kernel函数解析
start_kernel是Linux内核的主初始化函数,位于init/main.c。在RK3568平台上,关键初始化序列如下:
-
锁和调试系统初始化
lockdep_init()debug_objects_early_init()
-
架构相关初始化
setup_arch(&command_line)
-
内存管理系统初始化
mm_init()
-
调度器初始化
sched_init()
-
定时器子系统
time_init()
-
控制台初始化
console_init()
-
剩余初始化
rest_init()
5.2 RK3568特有初始化
在setup_arch函数中,RK3568平台会执行以下特殊处理:
- 早期时钟初始化
- 串口控制台设置
- 电源管理单元(PMU)配置
- DDR控制器校准参数加载
典型的内存初始化流程:
c复制void __init setup_arch(char **cmdline_p)
{
/* 解析设备树 */
unflatten_device_tree();
/* 初始化机器描述 */
mdesc = setup_machine_fdt(__atags_pointer);
/* 初始化内存 */
arm_memblock_init(mdesc);
/* 分页初始化 */
paging_init(mdesc);
}
6. 常见问题与调试技巧
6.1 启动卡住问题排查
当内核启动卡住时,可以按照以下步骤排查:
-
确认UART输出
- 检查波特率(RK3568通常使用1500000)
- 验证TX/RX引脚配置
-
使用JTAG调试
- 读取PC寄存器确定停止位置
- 检查关键寄存器(SCTLR, TTBR0)
-
早期printk调试
- 在内核配置中启用
CONFIG_DEBUG_LL - 添加自定义打印
- 在内核配置中启用
6.2 内存初始化问题
常见症状:
- 内核panic在
mm_init - 数据访问异常
解决方法:
- 检查设备树内存节点
dts复制memory@0 { device_type = "memory"; reg = <0x0 0x0 0x0 0x80000000>; }; - 验证DDR配置参数
- 检查MMU页表配置
6.3 设备树相关问题
RK3568严重依赖设备树,常见问题包括:
- 兼容性字符串不匹配
dts复制compatible = "rockchip,rk3568"; - 时钟定义错误
- 引脚复用冲突
调试建议:
- 使用
fdtdump工具查看DTB内容 - 在内核命令行添加
devicetree=debug
7. 性能优化建议
7.1 启动时间优化
RK3568启动时间优化手段:
-
内核裁剪
- 禁用不需要的驱动和功能
- 使用
CONFIG_THUMB2_KERNEL减小体积
-
初始化优化
- 延迟非关键驱动初始化
- 使用异步探测
-
固件优化
- 预加载固件到内存
- 使用压缩内核(XZ压缩率最高)
7.2 内存布局优化
通过调整内核链接地址可以提升性能:
lds复制. = 0xFFFFFFC00008000;
优化原则:
- 关键代码段对齐cache line
- 热路径代码集中存放
- 使用
__init宏标记初始化代码
在实际项目中,通过这些优化手段,我曾将RK3568的Linux内核启动时间从2.1秒缩短到0.8秒。关键是要根据具体应用场景权衡功能与性能。