嵌入式学习笔记

CPPFLAGS 是C预处理器的选项;-nostdlib 不使用标准库 -nostdinc 不使用标准头文件

CFLAGS 是C编译器的选项;

ARM 有7个基本工作模式:

User : 非特权模式,大部分任务执行在这种模式

FIQ : 当一个高优先级(fast) 中断产生时将会进入这种模式

IRQ : 当一个低优先级(normal) 中断产生时将会进入这种模式

Supervisor :当复位或软中断指令执行时将会进入这种模式

Abort : 当存取异常时将会进入这种模式

Undef : 当执行未定义指令时会进入这种模式

System : 使用和User模式相同寄存器集的特权模式

除User(用户模式)是Normal(普通模式)外,其他6种都是Privilege(特权模式)。

Privilege中除System模式外,其余5种为异常模式。

各种模式的切换,可以是程序员通过代码主动切换(通过写CPSR寄存器);也可以是CPU在某些情况下自动切换。

各种模式下权限和可以访问的寄存器不同。

ARM共有37个寄存器,都是32位长度

37个寄存器中30个为“通用”型,1个固定用作PC,一个固定用作CPSR,5个固定用作5种异常模式下的SPSR。

异常和中断的区别和联系

(1)针对SoC来说,发生复位、软中断、中断、快速中断、取指令异常、数据异常等,我们都统一叫异常。所以说:中断其实是异常的一种。

(2)异常的定义就是突发事件,打断了CPU的正常常规业务,CPU不得不跳转到异常向量表中去执行异常处理程序;中断是异常的一种,一般特指SoC内的内部外设产生的打断SoC常规业务,或者外部中断(SoC的GPIO引脚传回来的中断)。

当异常产生时, ARM core:

拷贝 CPSR 到 SPSR_

设置适当的 CPSR 位:

改变处理器状态进入 ARM 态

改变处理器模式进入相应的异常模式

设置中断禁止位禁止相应中断 (如果需要)

保存PC到 LR_

设置 PC 为相应的异常向量

返回时, 异常处理需要:

从 SPSR_恢复CPSR

从LR_恢复PC

Note:这些操作只能在 ARM 态执行.

(1)中断处理要注意保护现场(中断从SVC模式来,则保存SVC模式下的必要寄存器的值)和恢复现场(中断处理完成后,准备返回SVC模式前,要将保存的SVC模式下的必要寄存器的值恢复回去,不然到了SVC模式后寄存器的值乱了,SVC模式下原来正在进行的常规任务就被破坏了)

(2)保存现场包括:第一:设置IRQ栈;第二,保存LR;第三,保存R0~R12

(3)为什么要保存LR寄存器?要考虑中断返回的问题。中断ISR执行完后如何返回SVC模式下去接着执行原来的代码。中断返回其实取决于我们进入中断时如何保存现场。中断返回时关键的2个寄存器就是PC和CPSR。所以我们在进入IRQ模式时,应该将SVC模式下的下一句指令的地址(中断返回地址)和CPSR保存起来,将来恢复时才可以将中断返回地址给PC,将保存的CPSR给CPSR。

(4)中断返回地址就保存在LR中,而CPSR(自动)保存在(IRQ模式下的)SPSR中

汇编保存现场和恢复现场

(1)保护现场关键是保存:中断处理程序的返回地址,r0-r12(cpsr是自动保存的)

(2)恢复现场主要是恢复:r0-r12,pc,cpsr

IRQ_handle:

// 设置IRQ模式下的栈

ldr sp, =IRQ_STACK

// 保存LR

// 因为ARM有流水线,所以PC的值会比真正执行的代码+8,

sub lr, lr, #4

// 保存r0-r12和lr到irq模式下的栈上面

stmfd sp!, {r0-r12, lr}

// 在此调用真正的isr来处理中断

bl irq_handler

// 处理完成开始恢复现场,其实就是做中断返回,关键是将r0-r12,pc,cpsr一起回复

ldmfd sp!, {r0-r12, pc}^

uboot

编译之前首先要配置

x210_sd_config :    unconfig

    @$(MKCONFIG) $(@:_config=) arm s5pc11x x210 samsung s5pc110

    @echo "TEXT_BASE = 0xc3e00000" > $(obj)board/samsung/x210/config.mk

实质上就是运行了mkconfig这个脚本

再添加了TEXT_BASE = 0xC3E00000到board/samsung/smdkc110/config.mk(同时创建了这个文件)中

mkconfig

@$(MKCONFIG) $(@:_config=) arm s5pc11x x210 samsung s5pc110

运行这个脚本时传递了6个参数,其中$(@:_config=) 这个属于变量替换,可以理解为要用@这个变量,用之前先把其中的_config变成空,即x210_sd_config变成了x210_sd

配置的核心就是创建一些符号链接(文件夹),是为了实现可移植性,在源文件中包含头文件时,会用一个统一的文件夹名字来包含,这个名字实质上就是现在创建的符号链接的名字,脚本在这里根据所传递进来的参数为所需要的具体文件夹创建了符号链接。

创建的符号链接(路径在include文件夹中,为原地编译的情况下):

1.asm 指向 asm-arm

2.regs.h 指向 s5pc110.h

3.asm-arm/arch 指向 asm-arm/arch-s5pc11x

4.asm-arm/proc 指向 asm-arm/proc-armv

通过 ln -s 创建链接文件,如果原始文件路径是相对路径,其相对路径的基准路径是链接文件的路径。

这句话有点拗口,简单来说就是通过链接文件找源文件,是以链接文件的路径为当前路径的。

ln -s arch-$3 asm-$2/arch 正是由于上面所说的原因,这句话才能在当前位置为include时,实现步骤3和4

创建include/config.mk,里面是说明arch,cpu,board,vendor,soc(根据所传的6个参数)

创建include/config.h,里面只有一句:#include ,configs/x210_sd.h这个文件里全是宏控,属于移植uboot时要修改的重要文件,这里的宏会被用来生成autoconfig.mk,这个makefile文件会影响整个编译过程中的很多条件编译的选择。

配置完成后可以编译

默认的就是原地编译。如果需要指定具体的输出目录编译则有2种方式来指定输出目录。(具体参考Makefile 56-76行注释内容)

第一种:make O=输出目录

第二种:export BUILD_DIR=输出目录 然后再make

如果两个都指定了(既有BUILD_DIR环境变量存在,又有O=xx),则O=xx具有更高优先级

MKCONFIG    := $(SRCTREE)/mkconfig

这个mkconfig在uboot根目录下,实际上是个shell脚本,其中进行了配置uboot的主要工作

$(obj)include/config.mk 这个文件是配置时生成的

include $(TOPDIR)/config.mk

1.这是在代码中的一个makefile的配置文件(本质也是个makefile)

2.主要是配置了编译工具链

3.其中有个sinclude $(OBJTREE)/include/autoconf.mk

这个文件也是配置时生成的,内容是非常多的配置宏,用于控制uboot的编译

这个文件是根据include/configs/xxx.h头文件产生(九鼎开发板对应的头文件是include/configs/x210_sd.h)

4.指定了链接脚本(LDSCRIPT = u-boot.lds, 根据autoconf.mk中的宏决定的)

5.确定了TEXT_BASE(makefile中需要用到,链接基地址,实际上是在配置时确定的,写在主makefile的配置过程中,在这里被包含进来,并放进CPPFLAGS中,配置中为0xC3E00000,这是一个虚拟地址,对应的实际地址是多少,取决于uboot中的虚拟地址映射关系)

6.也指定了自动推导规则

编译完成后链接

u-boot.lds

ENTRY(_start) 指明整个程序的开头位置是_start函数

TEXT_BASE会覆盖.=0x00000000作为基地址

在uboot中整个程序的入口取决于链接脚本中ENTRY声明的地方。ENTRY(_start)因此_start符号所在的文件就是整个程序的起始文件,_start所在处的代码就是整个程序的起始代码。

start.s

首先包含config.h = include/configs/x210_sd.h

    .word 0x2000

    .word 0x0

    .word 0x0

    .word 0x0

这里是为了填充16个字节的占位,.word是汇编伪指令,相当于int类型

启动代码的16字节头部

(1)裸机中讲过,在SD卡启动/Nand启动等整个镜像开头需要16字节的校验头。(mkv210image.c中就是为了计算这个校验头)。我们以前做裸机程序时根本没考虑这16字节校验头,因为:1、如果我们是usb启动直接下载的方式启动的则不需要16字节校验头(irom application note);2、如果是SD卡启动mkv210image.c中会给原镜像前加16字节的校验头。

(2)uboot这里start.S中在开头位置放了16字节的填充占位,这个占位的16字节只是保证正式的image的头部确实有16字节,但是这16字节的内容是不对的,还是需要后面去计算校验和然后重新填充的。

    ldr pc, _undefined_instruction

    ldr pc, _software_interrupt

    ldr pc, _prefetch_abort

    ldr pc, _data_abort

    ldr pc, _not_used

    ldr pc, _irq

    ldr pc, _fiq.

_undefined_instruction:

    .word undefined_instruction

_software_interrupt:

    .word software_interrupt

_prefetch_abort:

    .word prefetch_abort

_data_abort:

    .word data_abort

_not_used:

    .word not_used

_irq:

    .word irq

_fiq:

    .word fiq

_pad:

    .word 0x12345678 /* now 16*4=64 */

这里是为了设置异常中断向量表

.global _end_vect

_end_vect:

    .balignl 16,0xdeadbeef

这里是为了设置内存对齐,以16字节对齐,没对齐的用0xdeadbeef填充

源代码中和配置Makefile中很多变量是可以互相运送的。简单来说有些符号的值可以从Makefile中传递到源代码中。

比如TEXT_BASE,它就是makefile中的值

CFG_PHY_UBOOT_BASE 33e00000 uboot在DDR中的物理地址

reset:

    /*

     * set the cpu to SVC32 mode and IRQ & FIQ disable

     */

    @;mrs   r0,cpsr

    @;bic   r0,r0,#0x1f

    @;orr   r0,r0,#0xd3

    @;msr   cpsr,r0

    msr cpsr_c, #0xd3       @ I & F disable, Mode: 0x13 - SVC

设置CPU为SVC模式,arm状态,禁止irq,fiq

整个uboot工作时,CPU一直处于SVC模式

ARM CPU在复位时默认就会进入SVC模式

/* Read booting information */

        ldr r0, =PRO_ID_BASE

        ldr r1, [r0,#OMR_OFFSET]

        bic r2, r1, #0xffffffc1

这里是为了从一个寄存器(地址是0xe0000004)获取OM引脚的接法,也就是启动介质是什么

    /*

     * Go setup Memory and board specific bits prior to relocation.

     */

    ldr sp, =0xd0036000 /* end of sram dedicated to u-boot */

    sub sp, sp, #12 /* set stack */

    mov fp, #0

    调用函数之前需要先设置栈,给sp赋值就是在设置栈,主要原因是在被调用的函数内还有再次调用函数,而BL只会将返回地址存储到LR中,但是我们只有一个LR,所以在第二层调用函数前要先将LR入栈,否则函数返回时第一层的返回地址就丢了。这次设置栈是在SRAM中设置的,因为当前整个代码还在SRAM中运行,此时DDR还未被初始化还不能用。栈地址0xd0036000是自己指定的,指定的原则就是这块空间只给栈用,不会被别人占用。

    .globl lowlevel_init

lowlevel_init:

    push    {lr}

    /* check reset status  */

    

    ldr r0, =(ELFIN_CLOCK_POWER_BASE+RST_STAT_OFFSET)

    ldr r1, [r0]

    bic r1, r1, #0xfff6ffff

    cmp r1, #0x10000

    beq wakeup_reset_pre

    cmp r1, #0x80000

    beq wakeup_reset_from_didle

判断哪种复位的意义在于:冷上电时DDR是需要初始化才能用的;而热启动或者低功耗状态下的复位则不需要再次初始化DDR。

    /* Disable Watchdog */

    ldr r0, =ELFIN_WATCHDOG_BASE    /* 0xE2700000 */

    mov r1, #0

    str r1, [r0]

关看门狗

    /* PS_HOLD pin(GPH0_0) set to high */

    ldr r0, =(ELFIN_CLOCK_POWER_BASE + PS_HOLD_CONTROL_OFFSET)

    ldr r1, [r0]

    orr r1, r1, #0x300  

    orr r1, r1, #0x1    

    str r1, [r0]

开发板的供电锁存,由于开发板的供电电路需要按按键才能开机

    /* when we already run in ram, we don't need to relocate U-Boot.

     * and actually, memory controller must be configured before U-Boot

     * is running in ram.

     */

    ldr r0, =0xff000fff

    bic r1, pc, r0      /* r0 

    ldr r2, _TEXT_BASE      /* r1 

    bic r2, r2, r0      /* r0 

    cmp     r1, r2                  /* compare r0, r1                  */

    beq     1f          /* r0 == r1 then skip sdram init   */

    /* init system clock */

    bl system_clock_init

    /* Memory initialize */

    bl mem_ctrl_asm_init

    

1:

    /* for UART */

    bl uart_asm_init

    bl tzpc_init

判断代码的执行位置,在sram或ddr中。

为什么要做这个判定?

原因:我们判定当前运行代码的地址是有用的,可以指导后面代码的运行。譬如在lowlevel_init.S中判定当前代码的运行地址,就是为了确定要不要执行时钟初始化和初始化DDR的代码。如果当前代码是在SRAM中,说明冷启动,那么时钟和DDR都需要初始化;如果当前代码是在DDR中,那么说明是热启动则时钟和DDR都不用再次初始化。

判断方法:BL1(uboot的前一部分)在SRAM中有一份,在DDR中也有一份,因此如果是冷启动那么当前代码应该是在SRAM中运行的BL1,如果是低功耗状态的复位这时候应该就是在DDR中运行的。

这一段代码是通过读取当前运行地址和链接地址,然后处理两个地址后对比是否相等,来判定当前运行是在SRAM中(不相等)还是DDR中(相等)。从而决定是否跳过下面的时钟和DDR初始化。

/*

 * system_clock_init: Initialize core clock and bus clock.

 * void system_clock_init(void)

 */

system_clock_init:

初始化时钟,代码的设置值都被宏定义在x210_sd.h中了。在x210_sd.h中300行到428行,都是和时钟相关的配置值。因此,如果移植时需要更改CPU的时钟设置,根本不需要动代码,只需要在x210_sd.h中更改配置值即可。

    .globl mem_ctrl_asm_init

mem_ctrl_asm_init:

ddr初始化,在uboot中,可用的物理地址范围为:0x30000000-0x4FFFFFFF。一共512MB,其中30000000-3FFFFFFF为DMC0,40000000-4FFFFFFF为DMC1。 内存配置值在x210_sd.h的438行到468行之间。分析的时候要注意条件编译的条件,配置头文件中考虑了不同时钟配置下的内存配置值,这个的主要目的是让不同时钟需求的客户都能找到合适自己的内存配置值。

/*

 * uart_asm_init: Initialize UART in asm mode, 115200bps fixed.

 * void uart_asm_init(void)

 */

uart_asm_init:

初始化串口,初始化完了后通过串口发送了一个'O'

    /* Print 'K' */

    ldr r0, =ELFIN_UART_CONSOLE_BASE

    ldr r1, =0x4b4b4b4b

    str r1, [r0, #UTXH_OFFSET]

    pop {pc}

打印‘K’,然后lowlevel_init函数返回。

lowlevel_init.S中总共做了以下事情:

检查复位状态、IO恢复、关看门狗、开发板供电锁存、时钟初始化、DDR初始化、串口初始化并打印'O'、tzpc初始化、打印'K'。

    /* get ready to call C functions */

    ldr sp, _TEXT_PHY_BASE  /* setup temp stack pointer */

    sub sp, sp, #12

    mov fp, #0          /* no previous frame, so fp=0 */

再次设置栈,之前在调用lowlevel_init程序前设置过1次栈(start.S 284-287行),那时候因为DDR尚未初始化,因此程序执行都是在SRAM中,所以在SRAM中分配了一部分内存作为栈。本次因为DDR已经被初始化了,因此要把栈挪移到DDR中,所以要重新设置栈,这是第二次;这里实际设置的栈的地址是33E00000,刚好在uboot的代码段的下面紧挨着。

    /* when we already run in ram, we don't need to relocate U-Boot.

     * and actually, memory controller must be configured before U-Boot

     * is running in ram.

     */

    ldr r0, =0xff000fff

    bic r1, pc, r0      /* r0 

    ldr r2, _TEXT_BASE      /* r1 

    bic r2, r2, r0      /* r0 

    cmp     r1, r2                  /* compare r0, r1                  */

    beq     after_copy      /* r0 == r1 then skip flash copy   */

再次判断代码运行位置,sram还是ddr,本次判断的目的不同(上次判断是为了决定是否要执行初始化时钟和DDR的代码)这次判断是为了决定是否进行uboot的relocate。

当前情况是uboot的前一部分(16kb或者8kb)开机自动从SD卡加载到SRAM中正在运行,uboot的第二部分(其实第二部分是整个uboot)还躺在SD卡的某个扇区开头的N个扇区中。此时uboot的第一阶段已经即将结束了(第一阶段该做的事基本做完了),结束之前要把第二部分加载到DDR中链接地址处(33e00000),这个加载过程就叫重定位。

/* If BL1 was copied from SD/MMC CH2 */

    ldr r0, =0xD0037488

    ldr r1, [r0]

    ldr r2, =0xEB200000

    cmp r1, r2

    beq     mmcsd_boot

D0037488这个内存地址在SRAM中,这个地址中的值是被硬件自动设置的。硬件根据我们实际电路中SD卡在哪个通道中,会将这个地址中的值设置为相应的数字。譬如我们从SD0通道启动时,这个值为EB000000;从SD2通道启动时,这个值为EB200000。

确定是从MMCSD启动之后,跳转到mmcsd_boot函数中去执行重定位动作。

 mmcsd_boot:

   bl      movi_bl2_copy

    b       after_copy

void movi_bl2_copy(void)

{

    ulong ch;

    ch = *(volatile u32 *)(0xD0037488);

    copy_sd_mmc_to_mem copy_bl2 =

        (copy_sd_mmc_to_mem) (*(u32 *) (0xD0037F98));//这个地址是三星固化的读取mmcsd卡的api

    u32 ret;

    if (ch == 0xEB000000) {

        ret = copy_bl2(0, MOVI_BL2_POS, MOVI_BL2_BLKCNT,

            CFG_PHY_UBOOT_BASE, 0);

    }

    else if (ch == 0xEB200000) {

        ret = copy_bl2(2, MOVI_BL2_POS, MOVI_BL2_BLKCNT,

            CFG_PHY_UBOOT_BASE, 0);

    }

    else

        return;

    if (ret == 0)

        while (1)

            ;

    else

        return;

}

物理地址就是物理设备设计生产时赋予的地址。像裸机中使用的寄存器的地址就是CPU设计时指定的,这个就是物理地址。物理地址是硬件编码的,是设计生产时确定好的,一旦确定了就不能改了。

MMU就是memory management unit,内存管理单元。MMU实际上是SOC中一个硬件单元,它的主要功能就是实现虚拟地址到物理地址的映射。

MMU单片在CP15协处理器中进行控制,也就是说要操控MMU进行虚拟地址映射,方法就是对cp15协处理器的寄存器进行编程。

地址映射的额外收益1:访问控制

(1)访问控制就是:在管理上对内存进行分块,然后每块进行独立的虚拟地址映射,然后在每一块的映射关系中同时还实现了访问控制(对该块可读、可写、只读、只写、不可访问等控制)

(2)回想在C语言中编程中经常会出现一个错误:Segmentation fault。实际上这个段错误就和MMU实现的访问控制有关。当前程序只能操作自己有权操作的地址范围(若干个内存块),如果当前程序指针出错访问了不该访问的内存块则就会触发段错误。

地址映射的额外收益2:cache

(1)cache的工作和虚拟地址映射有关系。

(2)cache是快速缓存,意思就是比CPU慢但是比DDR块。CPU嫌DDR太慢了,于是乎把一些DDR中常用的内容事先读取缓存在cache中,然后CPU每次需要找东西时先在cache中找。如果cache中有就直接用cache中的;如果cache中没有才会去DDR中寻找。

after_copy:

#if defined(CONFIG_ENABLE_MMU)

enable_mmu:

    /* enable domain access */

    ldr r5, =0x0000ffff

    mcr p15, 0, r5, c3, c0, 0       @load domain access register

使能域访问(cp15的c3寄存器)

(1)cp15协处理器内部有c0到c15共16个寄存器,这些寄存器每一个都有自己的作用。我们通过mrc和mcr指令来访问这些寄存器。所谓的操作cp协处理器其实就是操作cp15的这些寄存器。

(2)c3寄存器在mmu中的作用是控制域访问。域访问是和MMU的访问控制有关的。

    /* Set the TTB register */

    ldr r0, _mmu_table_base

    ldr r1, =CFG_PHY_UBOOT_BASE

    ldr r2, =0xfff00000

    bic r0, r0, r2

    orr r1, r0, r1

    mcr p15, 0, r1, c2, c0, 0

设置TTB(cp15的c2寄存器)

(1)TTB就是translation table base,转换表基地址。TTB其实就是转换表的基地址。

(2)转换表是建立一套虚拟地址映射的关键。转换表分2部分,表索引和表项。表索引对应虚拟地址,表项对应物理地址。一对表索引和表项构成一个转换表单元,能够对一个内存块进行虚拟地址转换。(映射中基本规定中规定了内存映射和管理是以块为单位的,至于块有多大,要看你的MMU的支持和你自己的选择。在ARM中支持3种块大小,细表1KB、粗表4KB、段1MB)。真正的转换表就是由若干个转换表单元构成的,每个单元负责1个内存块,总体的转换表负责整个内存空间(0-4G)的映射。

(3)整个建立虚拟地址映射的主要工作就是建立这张转换表

(4)转换表放置在内存中的,放置时要求起始地址在内存中要xx位对齐。转换表不需要软件去干涉使用,而是将基地址TTB设置到cp15的c2寄存器中,然后MMU工作时会自动去查转换表。

    /* Enable the MMU */

mmu_on:

    mrc p15, 0, r0, c1, c0, 0

    orr r0, r0, #1

    mcr p15, 0, r0, c1, c0, 0

    nop

    nop

    nop

    nop

#endif

使能MMU单元(cp15的c1寄存器)

(1)cp15的c1寄存器的bit0控制MMU的开关。只要将这一个bit置1即可开启MMU。开启MMU之后上层软件层的地址就必须经过TT的转换才能发给下层物理层去执行

宏观上理解转换表:整个转换表可以看作是一个int类型的数组,数组中的一个元素就是一个表索引和表项的单元。数组中的元素值就是表项,这个元素的数组下标就是表索引。

ARM的段式映射中长度为1MB,因此一个映射单元只能管1MB内存,那我们整个4G范围内需要4G/1MB=4096个映射单元,也就是说这个数组的元素个数是4096.实际上我们做的时候并没有依次单个处理这4096个单元,而是把4096个分成几部分,然后每部分用for循环做相同的处理。

VA PA length

0-10000000 0-10000000 256MB

10000000-20000000 0 256MB

20000000-60000000 20000000-60000000 1GB 512-1.5G

60000000-80000000 0 512MB 1.5G-2G

80000000-b0000000 80000000-b0000000 768MB 2G-2.75G

b0000000-c0000000 b0000000-c0000000 256MB 2.75G-3G

c0000000-d0000000 30000000-40000000 256MB 3G-3.25G

d-完 d-完 768MB 3.25G-4G

DRAM有效范围:

DMC0: 0x30000000-0x3FFFFFFF

DMC1: 0x40000000-0x4FFFFFFF

结论:虚拟地址映射只是把虚拟地址的c0000000开头的256MB映射到了DMC0的30000000开头的256MB物理内存上去了。其他的虚拟地址空间根本没动,还是原样映射的。

思考:为什么配置时将链接地址设置为c3e00000,因为这个地址将来会被映射到33e00000这个物理地址。

skip_hw_init:

    /* Set up the stack                         */

stack_setup:

#if defined(CONFIG_MEMORY_UPPER_CODE)

    ldr sp, =(CFG_UBOOT_BASE + CFG_UBOOT_SIZE - 0x1000)

#else

    ldr r0, _TEXT_BASE      /* upper 128 KiB: relocated uboot   */

    sub r0, r0, #CFG_MALLOC_LEN /* malloc area                      */

    sub r0, r0, #CFG_GBL_DATA_SIZE /* bdinfo                        */

#if defined(CONFIG_USE_IRQ)

    sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ)

#endif

    sub sp, r0, #12     /* leave 3 words for abort-stack    */

#endif

第三次设置栈。这次设置栈还是在DDR中,之前虽然已经在DDR中设置过一次栈了,但是本次设置栈的目的是将栈放在比较合适(安全,紧凑而不浪费内存)的地方。

(2)我们实际将栈设置在uboot起始地址上方2MB处,这样安全的栈空间是:2MB-uboot大小-0x1000=1.8MB左右。这个空间既没有太浪费内存,又足够安全。

clear_bss:

    ldr r0, _bss_start      /* find start of bss segment        */

    ldr r1, _bss_end        /* stop here                        */

    mov     r2, #0x00000000     /* clear                            */

clbss_l:

    str r2, [r0]        /* clear loop...                    */

    add r0, r0, #4

    cmp r0, r1

    ble clbss_l

 清理bss

(1)清理bss段代码和裸机中讲的一样。注意表示bss段的开头和结尾地址的符号是从链接脚本u-boot.lds得来的。

    ldr pc, _start_armboot

_start_armboot:

    .word start_armboot

(1)start_armboot是uboot/lib_arm/board.c中,这是一个C语言实现的函数。这个函数就是uboot的第二阶段。这句代码的作用就是将uboot第二阶段执行的函数的地址传给pc,实际上就是使用一个远跳转直接跳转到DDR中的第二阶段开始地址处。

(2)远跳转的含义就是这句话加载的地址和当前运行地址无关,而和链接地址有关。因此这个远跳转可以实现从SRAM中的第一阶段跳转到DDR中的第二阶段。

(3)这里这个远跳转就是uboot第一阶段和第二阶段的分界线。

总结:uboot的第一阶段做了哪些工作

(1)构建异常向量表

(2)设置CPU为SVC模式

(3)关看门狗

(4)开发板供电置锁

(5)时钟初始化

(6)DDR初始化

(7)串口初始化并打印"OK"

(8)重定位

(9)建立映射表并开启MMU

(10)跳转到第二阶段

start_armboot解析1

1. init_fnc_t

(1)typedef int (init_fnc_t) (void); 这是一个函数类型

(2)init_fnc_ptr是一个二重函数指针,回顾高级C语言中讲过:二重指针的作用有2个(其中一个是用来指向一重指针),一个是用来指向指针数组。因此这里的init_fuc_ptr可以用来指向一个函数指针数组。

DECLARE_GLOBAL_DATA_PTR

(1)#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r8")

定义了一个全局变量名字叫gd,这个全局变量是一个指针类型,占4字节。用volatile修饰表示可变的,用register修饰表示这个变量要尽量放到寄存器中,后面的asm("r8")是gcc支持的一种语法,意思就是要把gd放到寄存器r8中。

(2)综合分析,DECLARE_GLOBAL_DATA_PTR就是定义了一个要放在寄存器r8中的全局变量,名字叫gd,类型是一个指向gd_t类型变量的指针。

(3)为什么要定义为register?因为这个全局变量gd(global data的简称)是uboot中很重要的一个全局变量(准确的说这个全局变量是一个结构体,里面有很多内容,这些内容加起来构成的结构体就是uboot中常用的所有的全局变量),这个gd在程序中经常被访问,因此放在register中提升效率。因此纯粹是运行效率方面考虑,和功能要求无关。并不是必须的。

(4)gd_t定义在include/asm-arm/global_data.h中。

gd_t中定义了很多全局变量,都是整个uboot使用的;其中有一个bd_t类型的指针,指向一个bd_t类型的变量,这个bd是开发板的板级信息的结构体,里面有不少硬件相关的参数,譬如波特率、IP地址、机器码、DDR内存分布。

2.6.3.内存使用排布

2.6.3.1、为什么要分配内存

(1)DECLARE_GLOBAL_DATA_PTR只能定义了一个指针,也就是说gd里的这些全局变量并没有被分配内存,我们在使用gd之前要给他分配内存,否则gd也只是一个野指针而已。

(2)gd和bd需要内存,内存当前没有被人管理(因为没有操作系统统一管理内存),大片的DDR内存散放着可以随意使用(只要使用内存地址直接去访问内存即可)。但是因为uboot中后续很多操作还需要大片的连着内存块,因此这里使用内存要本着够用就好,紧凑排布的原则。所以我们在uboot中需要有一个整体规划。

2.6.3.2、内存排布

(1)uboot区 CFG_UBOOT_BASE-xx(长度为uboot的实际长度)

(2)堆区 长度为CFG_MALLOC_LEN,实际为912KB

(3)栈区 长度为CFG_STACK_SIZE,实际为512KB

(4)gd 长度为sizeof(gd_t),实际36字节

(5)bd 长度为sizeof(bd_t),实际为44字节左右

(6)内存间隔 为了防止高版本的gcc的优化造成错误。

2.6.4.start_armboot解析2

2.6.4.1、for循环执行init_sequence

(1)init_sequence是一个函数指针数组,数组中存储了很多个函数指针,这些指向指向的函数都是init_fnc_t类型(特征是接收参数是void类型,返回值是int)。

(2)init_sequence在定义时就同时给了初始化,初始化的函数指针都是一些函数名。(C语言高级专题中讲过:函数名的实质)

(3)init_fnc_ptr是一个二重函数指针,可以指向init_sequence这个函数指针数组。

(4)用for循环肯定是想要去遍历这个函数指针数组(遍历的目的也是去依次执行这个函数指针数组中的所有函数)。思考:如何遍历一个函数指针数组?有2种方法:第一种也是最常用的一种,用下标去遍历,用数组元素个数来截至。第二种不常用,但是也可以。就是在数组的有效元素末尾放一个标志,依次遍历到标准处即可截至(有点类似字符串的思路)。

我们这里使用了第二种思路。因为数组中存的全是函数指针,因此我们选用了NULL来作为标志。我们遍历时从开头依次进行,直到看到NULL标志截至。这种方法的优势是不用事先统计数组有多少个元素。

(5)init_fnc_t的这些函数的返回值定义方式一样的,都是:函数执行正确时返回0,不正确时返回-1.所以我们在遍历时去检查函数返回值,如果遍历中有一个函数返回值不等于0则hang()挂起。从分析hang函数可知:uboot启动过程中初始化板级硬件时不能出任何错误,只要有一个错误整个启动就终止,除了重启开发板没有任何办法。

(6)init_sequence中的这些函数,都是board级别的各种硬件初始化。

2.6.4.2、cpu_init

(1)看名字这个函数应该是cpu内部的初始化,所以这里是空的。

2.6.4.3、board_init

(1)board_init在uboot/board/samsung/x210/x210.c中,这个看名字就知道是x210开发板相关的初始化。

(2)DECLARE_GLOBAL_DATA_PTR在这里声明是为了后面使用gd方便。可以看出把gd的声明定义成一个宏的原因就是我们要到处去使用gd,因此就要到处声明,定义成宏比较方便。

(3)网卡初始化。CONFIG_DRIVER_DM9000这个宏是x210_sd.h中定义的,这个宏用来配置开发板的网卡的。dm9000_pre_init函数就是对应的DM9000网卡的初始化函数。开发板移植uboot时,如果要移植网卡,主要的工作就在这里。

(4)这个函数中主要是网卡的GPIO和端口的配置,而不是驱动。因为网卡的驱动都是现成的正确的,移植的时候驱动是不需要改动的,关键是这里的基本初始化。因为这些基本初始化是硬件相关的。

(1)board_init中除了网卡的初始化之外,剩下的2行用来初始化DDR。

gd->bd->bi_arch_number

(1)bi_arch_number是board_info中的一个元素,含义是:开发板的机器码。所谓机器码就是uboot给这个开发板定义的一个唯一编号。

(5)uboot中配置的这个机器码,会作为uboot给linux内核的传参的一部分传给linux内核,内核启动过程中会比对这个接收到的机器码,和自己本身的机器码相对比,如果相等就启动,如果不想等就不启动。

(6)理论上来说,一个开发板的机器码不能自己随便定。理论来说有权利去发放这个机器码的只有uboot官方,所以我们做好一个开发板并且移植了uboot之后,理论上应该提交给uboot官方审核并发放机器码(好像是免费的)。但是国内的开发板基本都没有申请(主要是因为国内开发者英文都不行,和国外开源社区接触比较少),都是自己随便编号的。随便编号的问题就是有可能和别人的编号冲突,但是只要保证uboot和kernel中的编号是一致的,就不影响自己的开发板启动。

gd->bd->bi_boot_params

(1)bd_info中另一个主要元素,bi_boot_params表示uboot给linux kernel启动时的传参的内存地址。也就是说uboot给linux内核传参的时候是这么传的:uboot事先将准备好的传参(字符串,就是bootargs)放在内存的一个地址处(就是bi_boot_params),然后uboot就启动了内核(uboot在启动内核时真正是通过寄存器r0 r1 r2来直接传递参数的,其中有一个寄存器中就是bi_boot_params)。内核启动后从寄存器中读取bi_boot_params就知道了uboot给我传递的参数到底在内存的哪里。然后自己去内存的那个地方去找bootargs。

(2)经过计算得知:X210中bi_boot_params的值为0x30000100,这个内存地址就被分配用来做内核传参了。所以在uboot的其他地方使用内存时要注意,千万不敢把这里给淹没了。

interrupt_init

(1)看名字函数是和中断初始化有关的,但是实际上不是,实际上这个函数是用来初始化定时器的(实际使用的是Timer4)。

(2)使用Timer4来定时,因为没有中断支持,所以CPU不能做其他事情同时定时,CPU只能使用轮询方式来不断查看TCNTO寄存器才能知道定时时间到了没。因为Timer4的定时是不能实现微观上的并行。uboot中定时就是通过Timer4来实现定时的。所以uboot中定时时不能做其他事

(5)interrupt_init函数将timer4设置为定时10ms。关键部位就是get_PCLK函数获取系统设置的PCLK_PSYS时钟频率,然后设置TCFG0和TCFG1进行分频,然后计算出设置为10ms时需要向TCNTB中写入的值,将其写入TCNTB,然后设置为auto reload模式,然后开定时器开始计时。

总结:在学习这个函数时,注意标准代码和之前裸机代码中的区别,重点学会:通过定义结构体的方式来访问寄存器,通过函数来自动计算设置值以设置定时器。

env_init

(1)env_init,看名字就知道是和环境变量有关的初始化。

(2)为什么有很多env_init函数,主要原因是uboot支持各种不同的启动介质(譬如norflash、nandflash、inand、sd卡·····),我们一般从哪里启动就会把环境变量env放到哪里。而各种介质存取操作env的方法都是不一样的。因此uboot支持了各种不同介质中env的操作方法。所以有好多个env_xx开头的c文件。实际使用的是哪一个要根据自己开发板使用的存储介质来定(这些env_xx.c同时只有1个会起作用,其他是不能进去的,通过x210_sd.h中配置的宏来决定谁被包含的),对于x210来说,我们应该看env_movi.c中的函数。

(3)经过基本分析,这个函数只是对内存里维护的那一份uboot的env做了基本的初始化或者说是判定(判定里面有没有能用的环境变量)。当前因为我们还没进行环境变量从SD卡到DDR中的relocate,因此当前环境变量是不能用的。

(4)在start_armboot函数中(776行)调用env_relocate才进行环境变量从SD卡中到DDR中的重定位。重定位之后需要环境变量时才可以从DDR中去取,重定位之前如果要使用环境变量只能从SD卡中去读取。

init_baudrate

(1)init_baudrate看名字就是初始化串口通信的波特率的。

(2)getenv_r函数用来读取环境变量的值。用getenv函数读取环境变量中“baudrate”的值(注意读取到的不是int型而是字符串类型),然后用simple_strtoul函数将字符串转成数字格式的波特率。

(3)baudrate初始化时的规则是:先去环境变量中读取"baudrate"这个环境变量的值。如果读取成功则使用这个值作为环境变量,记录在gd->baudrate和gd->bd->bi_baudrate中;如果读取不成功则使用x210_sd.h中的的CONFIG_BAUDRATE的值作为波特率。从这可以看出:环境变量的优先级是很高的。

console_init_f

(1)console_init_f是console(控制台)的第一阶段初始化。_f表示是第一阶段初始化,_r表示第二阶段初始化。有时候初始化函数不能一次一起完成,中间必须要夹杂一些代码,因此将完整的一个模块的初始化分成了2个阶段。(我们的uboot中start_armboot的826行进行了console_init_r的初始化)

(2)console_init_f在uboot/common/console.c中,仅仅是对gd->have_console设置为1而已,其他事情都没做。

display_banner

(1)display_banner用来串口输出显示uboot的logo

(2)display_banner中使用printf函数向串口输出了version_string这个字符串。那么上面的分析表示console_init_f并没有初始化好console怎么就可以printf了呢?

(3)通过追踪printf的实现,发现printf->puts,而puts函数中会判断当前uboot中console有没有被初始化好。如果console初始化好了则调用fputs完成串口发送(这条线才是控制台);如果console尚未初始化好则会调用serial_puts(再调用serial_putc直接操作串口寄存器进行内容发送)。

(4)控制台也是通过串口输出,非控制台也是通过串口输出。究竟什么是控制台?和不用控制台的区别?实际上分析代码会发现,控制台就是一个用软件虚拟出来的设备,这个设备有一套专用的通信函数(发送、接收···),控制台的通信函数最终会映射到硬件的通信函数中来实现。uboot中实际上控制台的通信函数是直接映射到硬件串口的通信函数中的,也就是说uboot中用没用控制台其实并没有本质差别。

(5)但是在别的体系中,控制台的通信函数映射到硬件通信函数时可以用软件来做一些中间优化,譬如说缓冲机制。(操作系统中的控制台都使用了缓冲机制,所以有时候我们printf了内容但是屏幕上并没有看到输出信息,就是因为被缓冲了。我们输出的信息只是到了console的buffer中,buffer还没有被刷新到硬件输出设备上,尤其是在输出设备是LCD屏幕时)

(6)U_BOOT_VERSION在uboot源代码中找不到定义,这个变量实际上是在makefile中定义的,然后在编译时生成的include/version_autogenerated.h中用一个宏定义来实现的。

uboot学习实践

(1)对uboot源代码进行完修改(修改内容根据自己的理解和分析来修改)

(2)make distclean然后make x210_sd_config然后make

(3)编译完成得到u-boot.bin,然后去烧录。烧录方法按照裸机第三部分讲的linux下使用dd命令来烧写的方法来烧写。

(4)烧写过程:

第一步:进入sd_fusing目录下、

第二步:make clean

第三步:make

第四步:插入sd卡,ls /dev/sd*得到SD卡在ubuntu中的设备号(我这里是 /dev/mmcblk0)

第五步:./sd_fusing.sh /dev/sdb完成烧录(注意不是sd_fusing2.sh)(我这里修改了sd_fusing.sh,原版的烧录文件名是错误的,同时SD卡的设备名也根据第四步进行了修改)

dram_init

(1)dram_init看名字是关于DDR的初始化。疑问:在汇编阶段已经初始化过DDR了否则也无法relocate到第二部分运行,怎么在这里又初始化DDR?

(2)dram_init都是在给gd->bd里面关于DDR配置部分的全局变量赋值,让gd->bd数据记录下当前开发板的DDR的配置信息,以便uboot中使用内存。

(3)从代码来看,其实就是初始化gd->bd->bi_dram这个结构体数组。

init_sequence总结

(1)都是板级硬件的初始化以及gd、gd->bd中的数据结构的初始化。譬如:

网卡初始化、机器码(gd->bd->bi_arch_number)、内核传参DDR地址(gd->bd->bi_boot_params)、Timer4初始化为10ms一次、波特率设置(gd->bd->bi_baudrate和gd->baudrate)、console第一阶段初始化(gd->have_console设置为1)、打印uboot的启动信息、打印cpu相关设置信息、检查并打印当前开发板名字、DDR配置信息初始化(gd->bd->bi_dram)、打印DDR总容量。

CFG_NO_FLASH

(1)虽然NandFlash和NorFlash都是Flash,但是一般NandFlash会简称为Nand而不是Flash,一般讲Flash都是指的Norflash。这里2行代码是Norflash相关的。

(2)flash_init执行的是开发板中对应的NorFlash的初始化、display_flash_config打印的也是NorFlash的配置信息(Flash: 8 MB就是这里打印出来的)。但是实际上X210中是没有Norflash的。所以着两行代码是可以去掉的(我也不知道为什么没去掉?猜测原因有可能是去掉着两行代码会导致别的地方工作不正常,需要花时间去移植调试,然后移植的人就懒得弄。实际上不去掉除了显示有8MB Flash实际没用之外也没有别的影响)

CONFIG_VFD和CONFIG_LCD是显示相关的,这个是uboot中自带的LCD显示的软件架构。但是实际上我们用LCD而没有使用uboot中设置的这套软件架构,我们自己在后面自己添加了一个LCD显示的部分。

mem_malloc_init

(1)mem_malloc_init函数用来初始化uboot的堆管理器。

(2)uboot中自己维护了一段堆内存,肯定自己就有一套代码来管理这个堆内存。有了这些东西uboot中你也可以malloc、free这套机制来申请内存和释放内存。我们在DDR内存中给堆预留了896KB的内存。

代码实践,去掉Flash看会不会出错。

结论:在include/configs/x210_sd.h加上CONFIG_NOFLASH宏之后编译出错,说明代码移植的不好,那个文件的包含没有被这个宏控制。于是乎移植的人就直接放这没管。

开发板独有初始化:mmc初始化

(1)从536到768行为开发板独有的初始化。意思是三星用一套uboot同时满足了好多个系列型号的开发板,然后在这里把不同开发板自己独有的一些初始化写到了这里。用#if条件编译配合CONFIG_xxx宏来选定特定的开发板。

(2)X210相关的配置在599行到632行。

(3)mmc_initialize看名字就应该是MMC相关的一些基础的初始化,其实就是用来初始化SoC内部的SD/MMC控制器的。函数在uboot/drivers/mmc/mmc.c里。

(4)uboot中对硬件的操作(譬如网卡、SD卡···)都是借用的linux内核中的驱动来实现的,uboot根目录底下有个drivers文件夹,这里面放的全都是从linux内核中移植过来的各种驱动源文件。

(5)mmc_initialize是具体硬件架构无关的一个MMC初始化函数,所有的使用了这套架构的代码都掉用这个函数来完成MMC的初始化。mmc_initialize中再调用board_mmc_init和cpu_mmc_init来完成具体的硬件的MMC控制器初始化工作。

(6)cpu_mmc_init在uboot/cpu/s5pc11x/cpu.c中,这里面又间接的调用了drivers/mmc/s3c_mmcxxx.c中的驱动代码来初始化硬件MMC控制器。这里面分层很多,分层的思想一定要有,否则完全就糊涂了。

(1)env_relocate是环境变量的重定位,完成从SD卡中将环境变量读取到DDR中的任务。

(2)环境变量到底从哪里来?SD卡中有一些(8个)独立的扇区作为环境变量存储区域的。但是我们烧录/部署系统时,我们只是烧录了uboot分区、kernel分区和rootfs分区,根本不曾烧录env分区。所以当我们烧录完系统第一次启动时ENV分区是空的,本次启动uboot尝试去SD卡的ENV分区读取环境变量时失败(读取回来后进行CRC校验时失败),我们uboot选择从uboot内部代码中设置的一套默认的环境变量出发来使用(这就是默认环境变量);这套默认的环境变量在本次运行时会被读取到DDR中的环境变量中,然后被写入(也可能是你saveenv时写入,也可能是uboot设计了第一次读取默认环境变量后就写入)SD卡的ENV分区。然后下次再次开机时uboot就会从SD卡的ENV分区读取环境变量到DDR中,这次读取就不会失败了。

(3)真正的从SD卡到DDR中重定位ENV的代码是在env_relocate_spec内部的movi_read_env完成的。

IP地址、MAC地址的确定

(1)开发板的IP地址是在gd->bd中维护的,来源于环境变量ipaddr。getenv函数用来获取字符串格式的IP地址,然后用string_to_ip将字符串格式的IP地址转成字符串格式的点分十进制格式。

(2)IP地址由4个0-255之间的数字组成,因此一个IP地址在程序中最简单的存储方法就是一个unsigend int。但是人类容易看懂的并不是这种类型,而是点分十进制类型(192.168.1.2)。这两种类型可以互相转换。

devices_init

(1)devices_init看名字就是设备的初始化。这里的设备指的就是开发板上的硬件设备。放在这里初始化的设备都是驱动设备,这个函数本来就是从驱动框架中衍生出来的。uboot中很多设备的驱动是直接移植linux内核的(譬如网卡、SD卡),linux内核中的驱动都有相应的设备初始化函数。linux内核在启动过程中就有一个devices_init(名字不一定完全对,但是差不多),作用就是集中执行各种硬件驱动的init函数。

(2)uboot的这个函数其实就是从linux内核中移植过来的,它的作用也是去执行所有的从linux内核中继承来的那些硬件驱动的初始化函数。

jumptable_init

(1)jumptable跳转表,本身是一个函数指针数组,里面记录了很多函数的函数名。看这阵势是要实现一个函数指针到具体函数的映射关系,将来通过跳转表中的函数指针就可以执行具体的函数。这个其实就是在用C语言实现面向对象编程。在linux内核中有很多这种技巧。

(2)通过分析发现跳转表只是被赋值从未被引用,因此跳转表在uboot中根本就没使用。

console_init_r

(1)console_init_f是控制台的第一阶段初始化,console_init_r是第二阶段初始化。实际上第一阶段初始化并没有实质性工作,第二阶段初始化才进行了实质性工作。

(2)uboot中有很多同名函数,使用SI工具去索引时经常索引到不对的函数处(回忆下当时start.S中找lowlevel_init.S时,自动索引找到的是错误的,真正的反而根本没找到。)

(3)console_init_r就是console的纯软件架构方面的初始化(说白了就是去给console相关的数据结构中填充相应的值),所以属于纯软件配置类型的初始化。

(4)uboot的console实际上并没有干有意义的转化,它就是直接调用的串口通信的函数。所以用不用console实际并没有什么分别。(在linux内console就可以提供缓冲机制等不用console不能实现的东西)。

enable_interrupts

(1)看名字应该是中断初始化代码。这里指的是CPSR中总中断标志位的使能。

(2)因为我们uboot中没有使用中断,因此没有定义CONFIG_USE_IRQ宏,因此我们这里这个函数是个空壳子。

(3)uboot中经常出现一种情况就是根据一个宏是否定义了来条件编译决定是否调用一个函数内部的代码。uboot中有2种解决方案来处理这种情况:方案一:在调用函数处使用条件编译,然后函数体实际完全提供代码。方案二:在调用函数处直接调用,然后在函数体处提供2个函数体,一个是有实体的一个是空壳子,用宏定义条件编译来决定实际编译时编译哪个函数进去。

loadaddr、bootfile两个环境变量

(1)这两个环境变量都是内核启动有关的,在启动linux内核时会参考这两个环境变量的值。

board_late_init

(1)看名字这个函数就是开发板级别的一些初始化里比较晚的了,就是晚期初始化。所以晚期就是前面该初始化的都初始化过了,剩下的一些必须放在后面初始化的就在这里了。侧面说明了开发板级别的硬件软件初始化告一段落了。

(2)对于X210来说,这个函数是空的。

eth_initialize

(1)看名字应该是网卡相关的初始化。这里不是SoC与网卡芯片连接时SoC这边的初始化,而是网卡芯片本身的一些初始化。

(2)对于X210(DM9000)来说,这个函数是空的。X210的网卡初始化在board_init函数中,网卡芯片的初始化在驱动中。

2.6.16.2、x210_preboot_init(LCD和logo显示)

(1)x210开发板在启动起来之前的一些初始化,以及LCD屏幕上的logo显示。

check menukey to update from sd

(1)uboot启动的最后阶段设计了一个自动更新的功能。就是:我们可以将要升级的镜像放到SD卡的固定目录中,然后开机时在uboot启动的最后阶段检查升级标志(是一个按键。按键中标志为"LEFT"的那个按键,这个按键如果按下则表示update mode,如果启动时未按下则表示boot mode)。如果进入update mode则uboot会自动从SD卡中读取镜像文件然后烧录到iNand中;如果进入boot mode则uboot不执行update,直接启动正常运行。

(2)这种机制能够帮助我们快速烧录系统,常用于量产时用SD卡进行系统烧录部署。

死循环

(1)解析器

(2)开机倒数自动执行

(3)命令补全

uboot启动2阶段总结

启动流程回顾、重点函数标出

(1)第二阶段主要是对开发板级别的硬件、软件数据结构进行初始化。

(2)

init_sequence

cpu_init 空的

board_init 网卡、机器码、内存传参地址

dm9000_pre_init 网卡

gd->bd->bi_arch_number 机器码

gd->bd->bi_boot_params 内存传参地址

interrupt_init 定时器

env_init 没做实际的事情

init_baudrate gd数据结构中波特率

serial_init 空的

console_init_f 空的

display_banner 打印启动信息

print_cpuinfo 打印CPU时钟设置信息

checkboard 检验开发板名字

dram_init gd数据结构中DDR信息

display_dram_config 打印DDR配置信息表

mem_malloc_init 初始化uboot自己维护的堆管理器的内存

mmc_initialize inand/SD卡的SoC控制器和卡的初始化

env_relocate 环境变量重定位

gd->bd->bi_ip_addr gd数据结构赋值

gd->bd->bi_enetaddr gd数据结构赋值

devices_init 空的

jumptable_init 不用关注的

console_init_r 真正的控制台初始化

enable_interrupts 空的

loadaddr、bootfile 环境变量读出初始化全局变量

board_late_init 空的

eth_initialize 空的

x210_preboot_init LCD初始化和显示logo

check_menu_update_from_sd 检查自动更新

main_loop 主循环

启动过程特征总结

(1)第一阶段为汇编阶段、第二阶段为C阶段

(2)第一阶段在SRAM中、第二阶段在DRAM中

(3)第一阶段注重SoC内部、第二阶段注重SoC外部Board内部

移植时的注意点

(1)x210_sd.h头文件中的宏定义

(2)特定硬件的初始化函数位置(譬如网卡)

运行时必须先加载到DDR中链接地址处

(1)uboot在第一阶段中进行重定位时将第二阶段(整个uboot镜像)加载到DDR的0xc3e00000地址处,这个地址就是uboot的链接地址。

(2)内核也有类似要求,uboot启动内核时将内存从SD卡读取放到DDR中(其实就是个重定位的过程),不能随意放置,必须放在内核的链接地址处,否则启动不起来。譬如我们使用的内核链接地址是0x30008000。

内核启动需要必要的启动参数

(1)uboot是无条件启动的,从零开始启动的。

(2)内核是不能开机自动完全从零开始启动的,内核启动要别人帮忙。uboot要帮助内核实现重定位(从SD卡到DDR),uboot还要给内核提供启动参数。

启动内核第一步:加载内核到DDR中

(1)uboot要启动内核,分为2个步骤:第一步是将内核镜像从启动介质中加载到DDR中,第二步是去DDR中启动内核镜像。(内核代码根本就没考虑重定位,因为内核知道会有uboot之类的把自己加载到DDR中链接地址处的,所以内核直接就是从链接地址处开始运行的)

静态内核镜像在哪里?

(1)SD卡/iNand/Nand/NorFlash等:raw分区

常规启动时各种镜像都在SD卡中,因此uboot只需要从SD卡的kernel分区去读取内核镜像到DDR中即可。读取要使用uboot的命令来读取(譬如X210的iNand版本是movi命令,X210的Nand版本就是Nand命令)

这种启动方式来加载ddr,使用命令:movi read kernel 30008000。其中kernel指的是uboot中的kernel分区(就是uboot中规定的SD卡中的一个区域范围,这个区域范围被设计来存放kernel镜像,就是所谓的kernel分区)

(2)tftp、nfs等网络下载方式从远端服务器获取镜像

uboot还支持远程启动,也就是内核镜像不烧录到开发板的SD卡中,而是放在主机的服务器中,然后需要启动时uboot通过网络从服务器中下载镜像到开发板的DDR中。

分析总结:最终结果要的是内核镜像到DDR中特定地址即可,不管内核镜像是怎么到DDR中的。以上2种方式各有优劣。产品出厂时会设置为从SD卡中启动(客户不会还要搭建tftp服务器才能用···);tftp下载远程启动这种方式一般用来开发。

镜像要放在DDR的什么地址?

(1)内核一定要放在链接地址处,链接地址去内核源代码的链接脚本或者Makefile中去查找。X210中是0x30008000。

zImage和uImage的区别联系

bootm命令对应do_bootm函数

(1)命令名前加do_即可构成这个命令对应的函数,因此当我们bootm命令执行时,uboot实际执行的函数叫do_bootm函数,在cmd_bootm.c。

(2)do_bootm刚开始定义了一些变量,然后用宏来条件编译执行了secureboot的一些代码(主要进行签名认证),先不管他;然后进行了一些一些细节部分操作,也不管他。然后到了CONFIG_ZIMAGE_BOOT,用这个宏来控制进行条件编译一段代码,这段代码是用来支持zImage格式的内核启动的。

vmlinuz和zImage和uImage

(1)uboot经过编译直接生成的elf格式的可执行程序是u-boot,这个程序类似于windows下的exe格式,在操作系统下是可以直接执行的。但是这种格式不能用来烧录下载。我们用来烧录下载的是u-boot.bin,这个东西是由u-boot使用arm-linux-objcopy工具进行加工(主要目的是去掉一些无用的)得到的。这个u-boot.bin就叫镜像(image),镜像就是用来烧录到iNand中执行的。

(2)linux内核经过编译后也会生成一个elf格式的可执行程序,叫vmlinux或vmlinuz,这个就是原始的未经任何处理加工的原版内核elf文件;嵌入式系统部署时烧录的一般不是这个vmlinuz/vmlinux,而是要用objcopy工具去制作成烧录镜像格式(就是u-boot.bin这种,但是内核没有.bin后缀),经过制作加工成烧录镜像的文件就叫Image(制作把78M大的精简成了7.5M,因此这个制作烧录镜像主要目的就是缩减大小,节省磁盘)。

du -h 文件名 可以看到文件的具体大小

(3)原则上Image就可以直接被烧录到Flash上进行启动执行(类似于u-boot.bin),但是实际上并不是这么简单。实际上linux的作者们觉得Image还是太大了所以对Image进行了压缩,并且在image压缩后的文件的前端附加了一部分解压缩代码。构成了一个压缩格式的镜像就叫zImage。(因为当年Image大小刚好比一张软盘(软盘有2种,1.2M的和1.44MB两种)大,为了节省1张软盘的钱于是乎设计了这种压缩Image成zImage的技术)。

(4)uboot为了启动linux内核,还发明了一种内核格式叫uImage。uImage是由zImage加工得到的,uboot中有一个工具,可以将zImage加工生成uImage。注意:uImage不关linux内核的事,linux内核只管生成zImage即可,然后uboot中的mkimage工具再去由zImage加工生成uImage来给uboot启动。这个加工过程其实就是在zImage前面加上64字节的uImage的头信息即可。

(4)原则上uboot启动时应该给他uImage格式的内核镜像,但是实际上uboot中也可以支持zImage,是否支持就看x210_sd.h中是否定义了LINUX_ZIMAGE_MAGIC这个宏。所以大家可以看出:有些uboot是支持zImage启动的,有些则不支持。但是所有的uboot肯定都支持uImage启动。

zImage启动细节

do_bootm函数中一直到397行的after_header_check这个符号处,都是在进行镜像的头部信息校验。校验时就要根据不同种类的image类型进行不同的校验。所以do_bootm函数的核心就是去分辨传进来的image到底是什么类型,然后按照这种类型的头信息格式去校验。校验通过则进入下一步准备启动内核;如果校验失败则认为镜像有问题,所以不能启动。

LINUX_ZIMAGE_MAGIC

(1)这个是一个定义的魔数,这个数等于0x016f2818,表示这个镜像是一个zImage。也就是说zImage格式的镜像中在头部的一个固定位置存放了这个数作为格式标记。如果我们拿到了一个image,去他的那个位置去取4字节判断它是否等于LINUX_ZIMAGE_MAGIC,则可以知道这个镜像是不是一个zImage。

(2)命令 bootm 0x30008000,所以do_boom的argc=2,argv[0]=bootm argv[1]=0x30008000。但是实际bootm命令还可以不带参数执行。如果不带参数直接bootm,则会从CFG_LOAD_ADDR地址去执行(定义在x210_sd.h中)。

(3)zImage头部开始的第37-40字节处存放着zImage标志魔数,从这个位置取出然后对比LINUX_ZIMAGE_MAGIC。

uImage启动

(1)LEGACY(遗留的),在do_bootm函数中,这种方式指的就是uImage的方式。

(2)uImage方式是uboot本身发明的支持linux启动的镜像格式,但是后来这种方式被一种新的方式替代,这个新的方式就是设备树方式(在do_bootm方式中叫FIT)

(3)uImage的启动校验主要在boot_get_kernel函数中,主要任务就是校验uImage的头信息,并且得到真正的kernel的起始位置去启动。

总结1:uboot本身设计时只支持uImage启动,原来uboot的代码也是这样写的。后来有了fdt方式之后,就把uImage方式命令为LEGACY方式,fdt方式命令为FIT方式,于是乎多了写#if #endif添加的代码。后来移植的人又为了省事添加了zImage启动的方式,又为了省事把zImage启动方式直接写在了uImage和fdt启动方式之前,于是乎又有了一对#if #endif。

总结2:第二阶段校验头信息结束,下面进入第三阶段,第三阶段主要任务是启动linux内核,调用do_bootm_linux函数来完成。

do_bootm_linux函数

镜像的entrypoint

(1)ep就是entrypoint的缩写,就是程序入口。一个镜像文件的起始执行部分不是在镜像的开头(镜像开头有n个字节的头信息),真正的镜像文件执行时第一句代码在镜像的中部某个字节处,相当于头是有一定的偏移量的。这个偏移量记录在头信息中。

(2)一般执行一个镜像都是:第一步先读取头信息,然后在头信息的特定地址找MAGIC_NUM,由此来确定镜像种类;第二步对镜像进行校验;第三步再次读取头信息,由特定地址知道这个镜像的各种信息(镜像长度、镜像种类、入口地址);第四步就去entrypoint处开始执行镜像。

(3)theKernel = (void (*)(int, int, uint))ep;将ep赋值给theKernel,则这个函数指向就指向了内存中加载的OS镜像的真正入口地址(就是操作系统的第一句执行的代码)。

机器码的再次确定

(1)uboot在启动内核时,机器码要传给内核。uboot传给内核的机器码是怎么确定的?第一顺序备选是环境变量machid,第二顺序备选是gd->bd->bi_arch_num(x210_sd.h中硬编码配置的)

从110行到144行就是uboot在给linux内核准备传递的参数处理。

传参详解

tag方式传参

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

(2)tag_header和tag_xxx。tag_header中有这个tag的size和类型编码,kernel拿到一个tag后先分析tag_header得到tag的类型和大小,然后将tag中剩余部分当作一个tag_xxx来处理。

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

(4)tag传参的方式是由linux kernel发明的,kernel定义了这种向我传参的方式,uboot只是实现了这种传参方式从而可以支持给kernel传参。

x210_sd.h中配置传参宏

(1)CONFIG_SETUP_MEMORY_TAGS,tag_mem,传参内容是内存配置信息。

(2)CONFIG_CMDLINE_TAG,tag_cmdline,传参内容是启动命令行参数,也就是uboot环境变量的bootargs.

(3)CONFIG_INITRD_TAG

(4)CONFIG_MTDPARTITION,传参内容是iNand/SD卡的分区表。

(5)起始tag是ATAG_CORE、结束tag是ATAG_NONE,其他的ATAG_XXX都是有效信息tag。

uboot最终是调用theKernel函数来执行linux内核的,uboot调用这个函数(其实就是linux内核)时传递了3个参数。这3个参数就是uboot直接传递给linux内核的3个参数,通过寄存器来实现传参的。(第1个参数就放在r0中,第二个参数放在r1中,第3个参数放在r2中)第1个参数固定为0,第2个参数是机器码,第3个参数传递的就是大片传参tag的首地址。

移植时注意事项

(1)uboot移植时一般只需要配置相应的宏即可

(2)kernel启动不成功,注意传参是否成功。传参不成功首先看uboot中bootargs设置是否正确,其次看uboot是否开启了相应宏以支持传参。

uboot启动内核的总结

启动4步骤

第一步:将内核搬移到DDR中

第二步:校验内核格式、CRC等

第三步:准备传参

第四步:跳转执行内核

涉及到的主要函数是:do_boom和do_bootm_linux

uboot能启动的内核格式:zImage uImage fdt方式

跳转与函数指针的方式运行内核.

uboot命令体系的实现代码在uboot/common/cmd_xxx.c中。有若干个.c文件和命令体系有关。(还有command.c main.c也是和命令有关的)

每个命令对应一个函数

(1)每一个uboot的命令背后都对应一个函数。这就是uboot实现命令体系的一种思路和方法。这个东西和我们在裸机第十六部分shell中实现shell命令的方法是一样的。

命令结构体cmd_tbl_t

struct cmd_tbl_s {

char *name; /* Command Name */

int maxargs; /* maximum number of arguments */

int repeatable; /* autorepeat allowed? */

/* Implementation function */

int (*cmd)(struct cmd_tbl_s *, int, int, char *[]);

char *usage; /* Usage message (short) */

#ifdef CFG_LONGHELP

char *help; /* Help message (long) */

#endif

#ifdef CONFIG_AUTO_COMPLETE

/* do auto completion on the arguments */

int (*complete)(int argc, char *argv[], char last_char, int maxv, char *cmdv[]);

#endif

};

typedef struct cmd_tbl_s cmd_tbl_t;

(1)name:命令名称,字符串格式。

(2)maxargs:命令最多可以接收多少个参数

(3)repeatable:指示这个命令是否可重复执行。重复执行是uboot命令行的一种工作机制,就是直接按回车则执行上一条执行的命令。

(4)cmd:函数指针,命令对应的函数的函数指针,将来执行这个命令的函数时使用这个函数指针来调用。

(5)usage:命令的短帮助信息。对命令的简单描述。

(6)help:命令的长帮助信息。细节的帮助信息。

(7)complete:函数指针,指向这个命令的自动补全的函数。

总结:uboot的命令体系在工作时,一个命令对应一个cmd_tbl_t结构体的一个实例,然后uboot支持多少个命令,就需要多少个结构体实例。uboot的命令体系把这些结构体实例管理起来,当用户输入了一个命令时,uboot会去这些结构体实例中查找(查找方法和存储管理的方法有关)。如果找到则执行命令,如果未找到则提示命令未知。

2.8.3.3、uboot实现命令管理的思路

(1)填充1个结构体实例构成一个命令

(2)给命令结构体实例附加特定段属性(用户自定义段),链接时将带有该段属性的内容链接在一起排列(挨着的,不会夹杂其他东西,也不会丢掉一个带有这种段属性的,但是顺序是乱序的)。

(3)uboot重定位时将该段整体加载到DDR中。加载到DDR中的uboot镜像中带有特定段属性的这一段其实就是命令结构体的集合,有点像一个命令结构体数组。

(4)段起始地址和结束地址(链接地址、定义在u-boot.lds中)决定了这些命令集的开始和结束地址。

uboot命令定义具体实现分析

(1)U_BOOT_CMD宏基本分析

这个宏定义在uboot/common/command.h中。

U_BOOT_CMD(

version, 1, 1, do_version,

"version - print monitor version\n",

NULL

);

这个宏替换后变成:

cmd_tbl_t __u_boot_cmd_version __attribute__ ((unused,section (".u_boot_cmd"))) = {#name, maxargs, rep, cmd, usage, help}

总结:这个U_BOOT_CMD宏的理解,关键在于结构体变量的名字和段属性。名字使用##作为连字符,附加了用户自定义段属性,以保证链接时将这些数据结构链接在一起排布。

(2)链接脚本。

链接脚本(.lds)文件中的变量,可以在.c文件中(用extern声明之后)使用。

find_cmd函数详解

(1)find_cmd函数的任务是从当前uboot的命令集中查找是否有某个命令。如果找到则返回这个命令结构体的指针,如果未找到返回NULL。

(2)函数的实现思路很简单,如果不考虑命令带点的情况(md.b md.w这种)就更简单了。查找命令的思路其实就是for循环遍历数组的思路,不同的是数组的起始地址和结束地址是用地址值来给定的,数组中的元素个数是结构体变量类型。

2.8.4.3、U_BOOT_CMD宏详解

(1)这个宏其实就是定义了一个命令对应的结构体变量,这个变量名和宏的第一个参数有关,因此只要宏调用时传参的第一个参数不同则定义的结构体变量不会重名。

uboot中增加自定义命令

2.8.5.1、在已有的c文件中直接添加命令

(1)在uboot/common/command.c中添加一个命令,叫:mycmd

(2)在已有的.c文件中添加命令比较简单,直接使用U_BOOT_CMD宏即可添加命令,给命令提供一个do_xxx的对应的函数这个命令就齐活了。

(3)添加完成后要重新编译工程(make distclean; make x210_sd_config; make),然后烧录新的uboot去运行即可体验新命令。

(4)还可以在函数中使用argc和argv来验证传参。

2.8.5.2、自建一个c文件并添加命令

(1)在uboot/common目录下新建一个命令文件,叫cmd_aston.c(对应的命令名就叫aston,对应的函数就叫do_aston函数),然后在c文件中添加命令对应的U_BOOT_CMD宏和函数。注意头文件包含不要漏掉。

(2)在uboot/common/Makefile中添加上aston.o,目的是让Make在编译时能否把cmd_aston.c编译链接进去。

(3)重新编译烧录。重新编译步骤是:make distclean; make x210_sd_config; make

2.8.5.3、体会:uboot命令体系的优点

(1)uboot的命令体系本身稍微复杂,但是他写好之后就不用动了。我们后面在移植uboot时也不会去动uboot的命令体系。我们最多就是向uboot中去添加命令,就像本节课所做的这样。

(2)向uboot中添加命令非常简单。

多个字符串挨在一起的时候,编译器会自动将其连接在一起,变成一个字符串

Makefile中

COBJS-$(CONFIG_CMD_AMBAPP) += CMD_AMBAPP.O

意思是,如果(CONFIG_COM_AMBAPP)=y,则COBJS中会加入CMD_AMBAPP.O;相应的:

COBJS-y += CMD_AMBAPP.O

意思就是,无论如何,COBJS添加CMD_AMBAPP.O

2.10.1.uboot与linux驱动

2.10.1.1、uboot本身是裸机程序

(1)裸机本来是没有驱动的概念的(狭义的驱动的概念就是操作系统中用来具体操控硬件的那部分代码叫驱动)

(2)裸机程序中是直接操控硬件的,操作系统中必须通过驱动来操控硬件。这两个有什么区别?本质区别就是分层。

2.10.1.2、uboot的虚拟地址对硬件操作的影响

(1)操作系统(指的是linux)下MMU肯定是开启的,也就是说linux驱动中肯定都使用的是虚拟地址。而纯裸机程序中根本不会开MMU,全部使用的是物理地址。这是裸机下和驱动中操控硬件的一个重要区别。

(2)uboot早期也是纯物理地址工作的,但是现在的uboot开启了MMU做了虚拟地址映射,这个东西驱动也必须考虑。查uboot中的虚拟地址映射表,发现除了0x30000000-0x3FFFFFFF映射到了0xC0000000-0xCFFFFFFF之外,其余的虚拟地址空间全是原样映射的。而我们驱动中主要是操控硬件寄存器,而S5PV210的SFR都在0xExxxxxx地址空间,因此驱动中不必考虑虚拟地址。

2.10.1.3、uboot借用(移植)了linux驱动

(1)linux驱动本身做了模块化设计。linux驱动本身和linux内核不是强耦合的,这是linux驱动可以被uboot借用(移植)的关键。

(2)uboot移植了linux驱动源代码。uboot是从源代码级别去移植linux驱动的,这就是linux系统的开源性。

(3)uboot中的硬件驱动比linux简单。linux驱动本身有更复杂的框架,需要实现更多的附带功能,而uboot本质上只是个裸机程序,uboot移植linux驱动时只是借用了linux驱动的一部分而已。

2.10.2.iNand/SD驱动解析1

2.10.2.1、从start_armboot开始

(1)驱动整体比较庞大,涉及很多个文件夹下的很多文件,函数更多,贸然插入根本不知道看哪里。学习时必须有顺序。

2.10.2.2、mmc_initialize

(1)函数位于:uboot/drivers/mmc/mmc.c。

(2)从名字可以看出,这个函数的作用就是初始化开发板上MMC系统。MMC系统的初始化应该包含这么几部分:SoC里的MMC控制器初始化(MMC系统时钟的初始化、SFR初始化)、SoC里MMC相关的GPIO的初始化、SD卡/iNand芯片的初始化。

(3)mmc_devices链表全局变量,用来记录系统中所有已经注册的SD/iNand设备。所以向系统中插入一个SD卡/iNand设备,则系统驱动就会向mmc_devices链表中插入一个数据结构表示这个设备。

2.10.2.3、cpu_mmc_init

(1)函数位于:uboot/cpu/s5pc11x/cpu.c中。实质是通过调用3个函数来完成的。

(1)setup_hsmmc_clock,在uboot/cpu/s5pc11x/setup_hsmmc.c中。看名字函数是用来初始化SoC中MMC控制器中的时钟部分的。

(2)setup_hsmmc_cfg_gpio,在uboot/cpu/s5pc11x/setup_hsmmc.c中。看名字函数是用来配置SoC中MMC控制器相关的GPIO的。

2.10.3.iNand/SD驱动解析2

2.10.3.1、smdk_s3c_hsmmc_init

(1)函数位于:uboot/drivers/mmc/s3c_hsmmc.c中。

(2)函数内部通过宏定义USE_MMCx来决定是否调用s3c_hsmmc_initialize来进行具体的初始化操作。

2.10.3.2、s3c_hsmmc_initialize

(1)函数位于:uboot/drivers/mmc/s3c_hsmmc.c中。

(2)定义并且实例化一个struct mmc类型的对象(定义了一个指针,并且给指针指向有意义的内存,或者说给指针分配内存),然后填充它的各种成员,最后调用mmc_register函数来向驱动框架注册这个mmc设备驱动。

(3)mmc_register功能是进行mmc设备的注册,注册方法其实就是将当前这个struct mmc使用链表连接到mmc_devices这个全局变量中去。

(4)我们在X210中定义了USE_MMC0和USE_MMC2,因此在我们的uboot初始化时会调用2次s3c_hsmmc_initialize函数,传递参数分别是0和2,因此完成之后系统中会注册上2个mmc设备,表示当前系统中有2个mmc通道在工作。

(5)至此cpu_mmc_init函数分析完成。

find_mmc_device

(1)这个函数位于:uboot/drivers/mmc/mmc.c中。

(2)这个函数其实就是通过mmc设备编号来在系统中查找对应的mmc设备(struct mmc的对象,根据上面分析系统中有2个,编号分别是0和2)。

(3)函数工作原理就是通过遍历mmc_devices链表,去依次寻找系统中注册的mmc设备,然后对比其设备编号和我们当前要查找的设备编号,如果相同则就找到了要找的设备。找到了后调用mmc_init函数来初始化它。

2.10.3.4、mmc_init

(1)函数位于:drivers/mmc/mmc.c中。

(2)分析猜测这个函数应该要进行mmc卡的初始化了(前面已经进行了SoC端控制器的初始化)

(3)函数的调用关系为:

mmc_init

mmc_go_idle

mmc_send_cmd

mmc_send_if_cond

mmc_send_cmd

······

具体分析可以看出,mmc_init函数内部就是依次通过向mmc卡发送命令码(CMD0、CMD2那些)来初始化SD卡/iNand内部的控制器,以达到初始化SD卡的目的。

2.10.3.3、总结

(1)至此整个MMC系统初始化结束。

(2)整个MMC系统初始化分为2大部分:SoC这一端的MMC控制器的初始化,SD卡这一端卡本身的初始化。前一步主要是在cpu_mmc_init函数中完成,后一部分主要是在mmc_init函数中完成。

(3)整个初始化完成后去使用sd卡/iNand时,操作方法和mmc_init函数中初始化SD卡的操作一样的方式。读写sd卡时也是通过总线向SD卡发送命令、读取/写入数据来完成的。

(4)顺着操作追下去,到了mmc_send_cmd函数处就断了,真正的向SD卡发送命令的硬件操作的函数找不到。这就是学习驱动的麻烦之处。

(5)struct mmc结构体是关键。两部分初始化之间用mmc结构体来链接的,初始化完了后对mmc卡的常规读写操作也是通过mmc结构体来链接的。

2.10.4.iNand/SD驱动解析3

2.10.4.1、struct mmc

(1)驱动的设计中有一个关键数据结构。譬如MMC驱动的结构体就是struct mmc这些结构体中包含一些变量和一些函数指针,变量用来记录驱动相关的一些属性,函数指针用来记录驱动相关的操作方法。这些变量和函数指针加起来就构成了驱动。驱动就被抽象为这个结构体。

(2)一个驱动工作时主要就分几部分:驱动构建(构建一个struct mmc然后填充它)、驱动运行时(调用这些函数指针指针的函数和变量)

2.10.4.2、分离思想

(1)分离思想就是说在驱动中将操作方法和数据分开。

(2)操作方法就是函数,数据就是变量。所谓操作方法和数据分离的意思就是:在不同的地方来存储和管理驱动的操作方法和变量,这样的优势就是驱动便于移植。

2.10.4.3、分层思想

(1)分层思想是指一个整个的驱动分为好多个层次。简单理解就是驱动分为很多个源文件,放在很多个文件夹中。譬如本课程讲的mmc的驱动涉及到drivers/mmc下面的2个文件和cpu/s5pc11x下的好几个文件。

(2)以mmc驱动为例来分析各个文件的作用:

uboot/drivers/mmc/mmc.c:本文件的主要内容是和MMC卡操作有关的方法,譬如MMC卡设置空闲状态的、卡读写数据等。但是本文件中并没有具体的硬件操作函数,操作最终指向的是struct mmc结构体中的函数指针,这些函数指针是在驱动构建的时候和真正硬件操作的函数挂接的(真正的硬件操作的函数在别的文件中)。

uboot/drivers/mmc/s3c_hsmmc.c:本文件中是SoC内部MMC控制器的硬件操作的方法,譬如向SD卡发送命令的函数(s3c_hsmmc_send_command),譬如和SD卡读写数据的函数(s3c_hsmmc_set_ios),这些函数就是具体操作硬件的函数,也就是mmc.c中需要的那些硬件操作函数。这些函数在mmc驱动初始化构建时(s3c_hsmmc_initialize函数中)和struct mmc挂接起来备用。

分析:mmc.c和s3c_hsmmc.c构成了一个分层,mmc.c中调用了s3c_hsmmc.中的函数,所以mmc.c在上层,s3c_hsmmc.c在下层。这两个分层后我们发现mmc.c中不涉及具体硬件的操作,s3c_hsmmc.c中不涉及驱动工程时的时序操作。因此移植的时候就有好处:譬如我们要把这一套mmc驱动移植到别的SoC上mmc.c就不用动,s3c_hsmmc.c动就可以了;譬如SoC没变但是SD卡升级了,这时候只需要更换mmc.c,不需要更换s3c_hsmmc.c即可。

(3)cpu/s5pc11x/下面还有一个setup_hsmmc.c,也和MMC驱动有关。但是这些代码为什么不能放到drivers目录下去,而要放到cpu目录下去?因为这里面的2个函数(setup_hsmmc_clock和setup_hsmmc_cfg_gpio)都是和SoC有关的初始化函数,这两个函数不能放到drivers目录下去。实际上如果非把这两个函数放在uboot/drivers/mmc/s3c_hsmmc.c文件中也凑活能说过去。

2.10.4.4、linux驱动前奏

使用三星官方s5pv210的uboot移植

代码分析&问题查找

运行结果是:第一,串口无输出;第二,开发板供电锁存成功。

分析运行结果:uboot中串口最早的输出在"OK",在lowlevel_init.S中初始化串口时打印出来的;串口无输出"O"说明在打印"O"之前代码已经死掉了;开发板供电锁存在lowlevel_init.S中,开发板供电锁存成功说明这个代码之前的部分是没问题的。两个结合起来得到结论:错误在开发板供电锁存代码和串口初始化打印"O"代码之间。

整个程序运行是从start.S开始的,看代码也从这里开始。

实际上只要屏蔽掉bl PMIC_InitIp 这一行代码,然后重新编译,整个uboot就启动起来了。但是很多配置信息是有问题的,很多功能应该也是不能用的,都要去一一查验。

(还屏蔽了u-boot-samsung-dev/drivers/mtd/nand/nand.c文件下的void nand_init(void)函数中的

nand_init_chip(&nand_info[i], &nand_chip[i], base_address[i]);调用)

DDR配置信息的更改

(1)从运行信息以及bdinfo命令看到的结果,显示DRAM bank0和1的size值都设置错了。

(2)使用md和mw命令测试内存,发现20000000和40000000开头的内存都是可以用的,说明代码中DDR初始化部分是正确的,只是size错了。

(3)内存部分配置成:

#define CONFIG_NR_DRAM_BANKS 2 /* we have 2 bank of DRAM */

//#define SDRAM_BANK_SIZE 0x20000000 /* 512 MB */

#define SDRAM_BANK_SIZE 0x10000000 /* 256 MB */

#define PHYS_SDRAM_1 MEMORY_BASE_ADDRESS /* SDRAM Bank #1 */

#define PHYS_SDRAM_1_SIZE SDRAM_BANK_SIZE

//#define PHYS_SDRAM_2 (MEMORY_BASE_ADDRESS + SDRAM_BANK_SIZE) /* SDRAM Bank #2 */

#define PHYS_SDRAM_2 0x40000000

#define PHYS_SDRAM_2_SIZE SDRAM_BANK_SIZE

DDR地址另外配置

2.11.5.1、目标:将DDR端口0地址配置为30000000开头

(1)更改有2个目的:第一是让大家体验内存配置的更改过程;第二是3开头的地址和DRAM bank1上40000000开头的地址就连起来了。这样我们就得到了地址连续的512MB内存,而原来我们得到的512MB内存地址是断续的。

DDR初始化参数更改

(1)根据裸机中讲DDR初始化部分的课程,和uboot前面分析uboot中DDR初始化部分的代码的课程,得出结论就是:DDR的初始化代码部分是在lowlevel_init.S中写的,是不动的。代码部分就是对相应寄存器做相应值的初始化;要动的是值,而uboot为了具有可移植性把值都宏定义在include/configs/xxx.h中了。因此我们只需要去这个配置头文件中更改配置值即可。

(2)更改内容是:#define DMC0_MEMCONFIG_0 0x20E01323改为:

#define DMC0_MEMCONFIG_0 0x30E01323 注意20改为30了。

2.11.5.3、smdkv210single.h中相关宏定义修改

(1)寄存器的值改了后相当于是硬件配置部分做了更改。但是uboot中DDR相关的一些软件配置值还没更改,还在原来位置,所以要去更改。

(2)#define MEMORY_BASE_ADDRESS 0x20000000改为:

#define MEMORY_BASE_ADDRESS 0x30000000

2.11.5.4、虚拟地址映射表中相应修改

(1)uboot中开启了MMU对内存进行了段式映射,有一张内存映射表。之前课程中分析过,分析方法是一样的。

(2)经过实际分析,发现这个内存映射只是把20000000开始的256MB映射到C0000000开头的256MB。我们更改方法是将2改成3.

(3)为了安全起见,再去配置头文件smdkv210single.h中查一遍,看看有没有其他的宏定义值和内存配置有关联的。

重新配置编译,烧录运行查看结果。

2.11.6.DDR初始化参数更改2

2.11.6.1、修改DMC0的配置参数

(1)修改DDR中DMC0的memconfig_0寄存器的配置值,将

#define DMC0_MEMCONFIG_0 0x30E01323 改为:

#define DMC0_MEMCONFIG_0 0x30F01323

(2)然后重新同步、编译烧写运行,发现uboot第二阶段运行了,但是整个uboot还是不成功。

(3)分析问题,寻找解决方案。分析方法有2种:第一种靠经验、靠发现能力、靠直觉去找;第二种就是在整个代码中先基本定位错误地方,然后通过在源代码中添加打印信息来精确定位出错的代码,然后找到精确的出错位置后再去分析错误原因,从而找到解决方案。

2.11.6.2、修改修改虚拟地址到物理地址的映射函数

(1)修改uboot/board/samsung/smdkc110/smdkc110.c中的virt_to_phy_smdkc110,将其中的20000000改为30000000即可。

(2)同步代码,然后重新编译烧录运行。

2.11.6.3、总结:牵一发而动全身

通过浏览代码上下文,发现这个函数是在读取SD/iNand的ext_csd寄存器的值。通过浏览代码结合出错地方,可以判断出:从卡端读取ext_csd寄存器是成功的,并且从读取结果中拿到了卡的版本号信息。然后代码对版本号进行了判断,并且如果版本号大于5就会报错并且函数错误退出。这就是问题所在。

(1)当前板子上有一个iNand接在SD0上,有一个外置SD卡接在SD2上。那uboot中初始化的这个是iNand而不是SD卡。也就是说uboot中实际用的是SD0而不是SD2.

(2)大家可以尝试,使用外置SD卡时,这个版本号的问题不会出现。从这里可以推测出SD卡和iNand的区别,至少从一个角度可以看出:SD卡版本低,iNand的版本比较高。

控制台串口更换为串口0

(1)uboot中默认使用串口2来做控制台输入输出的。

(2)SOC中一共有4个串口(串口0、1、2、3),开发板X210上用DB9接口引出了2个串口,分别是串口2和串口0.(靠边的是串口2,靠里那个是串口0)。

(3)三星公司推荐使用串口2来作为调试串口,所以在三星移植的uboot和内核版本中都是以串口2默认为控制台串口的。

(4)有时候项目需要将调试串口修改为另外的串口(譬如串口0),这时候需要修改uboot的代码,做移植让uboot工作在串口0的控制台下。

(5)uboot中真正去硬件初始化串口控制器的代码在lowlevel_init.S中的uart_asm_init中,其中初始化串口的寄存器用ELFIN_UART_CONSOLE_BASE宏作为串口n的寄存器的基地址,结合偏移量对寄存器进行寻址初始化。所以uart_asm_init中到底初始化的是串口几(从0到3)?取决于ELFIN_UART_CONSOLE_BASE宏。这个宏的值又由CONFIG_SERIALn(n是从1到4)来决定

(6)同步代码、编译烧录运行,发现串口线插在串口2上,crt上只打印:SD checksum error.(这个是内部iROM打印出来的,内部iNand校验失败的信息);然后将串口线改插到串口0上,启动,所有的信息出现。实验成功。

修改默认网络地址设置

(1)修改配置头文件smdkv210single.h中的CONFIG_IPADDR等宏,则可以修改uboot的默认环境变量。

(2)更改完成后如果环境变量还是原来的,正常。因为原来uboot执行过saveenv,因此环境变量已经被保存到iNand中的ENV分区中去了。uboot启动后校验时iNand的ENV分区中的环境变量是正确的,因此会优先加载。我们在uboot源代码中修改的只是默认的环境变量。解决方案是擦除掉iNand中的那一份环境变量,然后迫使uboot启动时使用uboot代码中自带的默认的这一份环境变量,就可以看到了。

(3)可以使用mmc write 0 30000000 11# 32(表示将DDR的0x30000000开头的一段内存中的内容写入iNand中的第17个扇区开始的32个扇区内,写入长度是32个扇区长度(16KB))

网卡芯片与开发板的连接方式

(1)SoC的SROM bank和网卡芯片的CS引脚(SROM就是SRAM/ROM)。SoC的SROMController其实就是SoC提供的对外总线式连接SRAM/ROM的接口。如果SoC要外部外接一些SRAM/ROM类的存储芯片(或者伪装成SROM接口的芯片,譬如网卡芯片)就要通过SROM Controller来连接。网卡接在SROM中好处就是网卡芯片好像一个存储芯片一样被扩展在SoC的一个地址空间中,主机SoC可以直接用一个地址来访问网卡芯片内部寄存器。

(2)网卡芯片内部寄存器使用相对地址访问。网卡芯片内部很多寄存器有一个地址,这个地址是从00开始的,但是实际上我们SoC不能用0地址去访问这个网卡的芯片内部寄存器。SoC访问网卡芯片00寄存器时的地址应该是:起始地址+00这里的起始地址就是网卡芯片对应接在SROM bankn中的bankn对应的基地址。

(3)主机SoC上网,其实就是通过操控网卡芯片内部的寄存器、缓冲区等资源来上网的。也就是说其实SoC是通过网卡芯片来间接上网的。

(4)总结:实际上也是一种总线式连接方式。优势是SoC内部不需要内置网卡控制器,所有的SFR全都在外部网卡芯片中,而且还可以通过地址直接访问(IO与内存统一编址),不用像Nand/SD接口一样使用时序来访问。

(5)从逻辑上来看,网卡更像是串口,而不像是SD/Nand。

原理图浏览

(1)210的SROM控制器允许8/16bit的接口,我们实际使用的是16位接口。

(2)网线有8根线,但是实际只有4根有效通信线,另外4根都是GND,用来抗干扰的。4根通信线中管发送的有2根(Tx-和Tx+),管接收的有2根(Rx+和Rx-)。因为网线上传输的是差分信号。

(3)网卡芯片有个CS引脚,(CS就是chip select,片选信号,主机向CS发送有效信号则从机芯片工作,主机向CS发送无效信号则从机芯片不工作。),这个引脚要接主机SoC的片选信号引脚,主机S5PV210的每一个SROM bank中有一个片选信号CSn(n=0-5),从原理图可以看出,我们X210上将DM9000的CS引脚接到了CSn1上,对应SROM bank1(推断出DM9000的总线地址基地址是0x88000000)。

(4)DM9000的CMD引脚接到了S5PV210的ADDR2引脚上。DM9000为了减少芯片引脚数,数据线和地址线是复用的(DATA0到DATA15这16根线是有时候做数据线传输数据,有时候做地址线传输地址的。什么时候做什么用就由CMD引脚决定。)通过查询数据手册知道:当CMD为高电平时对应传输是DATA,当CMD为低电平时对应传输为INDEX(offset,寄存器地址)。

注明:这些引脚上的电平变化都是控制器自动的,不需要程序员手工干预。程序员所需要做的就是在配置寄存器值时充分考虑到硬件电路的接法,然后给相应寄存器配置正确的数值即可。

网卡驱动文件介绍

(1)uboot中本来就提供了很多网卡芯片的驱动程序,在uboot/drivers/net/dm9000x.c和dm9000x.h。这个驱动来自于linux kernel源代码。所以我们uboot中是移植而不是编写。

(2)要想彻底看懂这个驱动,必须对linux的驱动模型中网络设备驱动有一定的理解才可以。因为我们还没学驱动,因此这个源代码就不用看了。

(3)这个驱动是linux内核中做好的,根本不用动可以在uboot中直接使用的。而且因为linux驱动设计的很合理(数据和代码是分开的,这里驱动主要是代码,数据是由硬件开发板中的接法决定的,数据由一定的数据结构来提供。),所以驱动本身具有可移植性。这个就决定了我们移植DM9000驱动时这个驱动文件dm9000x.c和h不用动,要动的是数据。

网卡移植的关键:初始化

(1)uboot在第二阶段init_sequences中进行了一系列的初始化,其中就有网卡芯片的初始化。这个初始化就是关键,在这里的初始化中只要将网卡芯片正确的初始化了,则网卡芯片就能工作(意思是网卡驱动dm9000x.c和dm9000x.h依赖于这里的初始化而工作)。

(2)网卡初始化代码地方在:

start_armboot

init_sequence

board_init

dm9000_pre_init 这个函数就是移植的关键

(3)dm9000_pre_init函数主要功能就是初始化DM9000网卡。这个初始化过程和我们开发板上DM9000网卡芯片的硬件连接方式有关。必须要结合开发板原理图来分析,然后决定这个函数怎么编程。

(4)原来的代码是三星的工程师根据三星的开发板SMDKV210的硬件接法来写的程序,我们要根据自己的开发板的硬件接法去修改这个程序,让网卡在我们的开发板上能工作。

(5)#define DM9000_16BIT_DATA这个宏用来表示DM9000工作在16位总线模式下。根据上节课的硬件原理图的分析,可以看到我们开发板上DM9000确实工作在16位模式下。

(6)从三星版本的代码中可以看出,它操作的是bit20-bit23,对照数据手册中寄存器定义,可以看出三星的开发板DM9000是接在Bank5上的。而我们接在bank1上的,因此我们需要操作的bit位是bit4-bit7

(7)总结:三个寄存器的修改。主要是三星的开发板DM9000接在bank5,我们接在了bank1上,因此要做一些修改。

2.11.10.3、基地址的配置等

(1)之前说过,驱动分为2部分:代码和数据。代码不用动,数据要修改。

(2)CONFIG_DM9000_BASE是DM9000网卡通过SROM bank映射到SoC中地址空间中的地址。这个地址的值取决于硬件接到了哪个bank,这个bank的基地址是SoC自己定义好的。譬如我们这里接到了bank1上,bank1的基地址是0x88000000.

(3)DM9000_IO表示访问芯片IO的基地址,直接就是CONFIG_DM9000_BASE;DM9000_DATA表示我们访问数据时的基地址,因为DM9000芯片的CMD引脚接到了ADDR2,因此这里要+4(0b100,对应ADDR2)

(4)本来这样配置就完了,重新编译运行网卡就应该工作了。但是实际测试发现不工作,要怎么样修改呢?修改方式是将CONFIG_DM9000_BASE改成0x88000300就工作了。

问题?这个0x300从哪里来的?我得出的感觉最靠谱的解释是:跟DM9000网卡芯片型号版本有关,我认为这个0x300是DM9000网卡本身的问题,他本身的内部寄存器就有一个0x300的一个偏移量。

linux系统中网卡驱动的典型工作方式简介

(1)在linux系统中,网卡算是一个设备,这个设备驱动工作后会生成一个设备名叫ethn(n是0、1、2、····)(无线网卡名字一般叫wlan0、wlan1····)。然后linux系统用一些专用命令来操作网卡,譬如ifconfig命令。

(2)linux下的应用程序如何使用网卡驱动来进行网络通信?最通用的方法就是socket接口。linux系统中有一系列的API和库函数,提供了一个socket编程接口,linux下的应用程序都是通过socket来实现上网的,socket内部就是间接调用的网卡驱动实现网络通信的。

(3)linux设计是非常完备的,应用层和驱动层是严格分离的。也就是说写网络编程应用层的人根本不用管驱动,只要会用socket接口即可;写底层驱动的人根本不用管应用层,只要面向linux的网络驱动框架模型即可。

2.11.11.3、uboot中网卡驱动的工作方式简介

(1)一定要记住:uboot本身是一个裸机程序,是一个整体,没有分层。所以uboot中根本没有驱动和应用的概念。

(2)按照逻辑来说,ping这样的命令实现的代码就是网络应用的应用程序,像dm9000x.c和dm9000x.h这样的代码属于驱动程序。所以在uboot中这些东西是揉在一起的,应用是直接调用驱动实现的。也就是说ping命令内部是直接调用了dm9000的网卡驱动中的函数来实现自己的。

2.11.11.4、以ping命令为例查找代码验证分析

(1)ping命令是uboot的众多命令之一,ping命令实现的函数叫do_ping

(2)函数的调用关系:

do_ping

NetLoop

PingStart

PingSend

ArpRequest

eth_send(dm9000x.c中)

(3)验证了2.11.11.3中说的uboot中应用程序(ping)调用驱动程序(dm9000x.c)的方式。这就是一种直接调用的方式。

从uboot官方标准uboot开始移植

主Makefile浏览及boards.cfg文件

(1)2013.10版本的uboot的Makefile中使用了boards.cfg文件,因此在配置uboot时make xxx_config,这个xxx要到boards.cfg文件中查找。

(2)其实就相当于把以前的版本的uboot中各种开发板的配置部分规则抽离出来写到了Makefile中,然后把配置信息部分写到了一个独立文件boards.cfg。

2.12.2.4、mkconfig脚本浏览及符号连接的分析

(1)下节课详细分析,给出结论。

2.12.2.5、结论:

(1)参照物开发板为:s5p_goni

(2)配置对应的cpu、board文件夹分别为:

cpu: u-boot-2013.10\arch\arm\cpu\armv7

board: u-boot-2013.10\board\samsung\goni

.mkconfig脚本分析

2.12.3.1、脚本功能浏览

(1)首先我们在命令行配置uboot时,是:make s5p_goni_config,对应Makefile中的一个目标。

(2)新版本的Makefile中:

%_config:: unconfig

@$(MKCONFIG) -A $(@:_config=)

从这里分析得出结论,实际配置时是调用mkconfig脚本,然后传参2个:-A和s5p_goni

(3)到了mkconfig脚本中了。在24到35行中使用awk正则表达式将boards.cfg中与刚才$1(s5p_goni)能够匹配上的那一行截取出来赋值给变量line,然后将line的内容以空格为间隔依次分开,分别赋值给$1、$2···$8。

(4)注意在解析完boards.cfg之后,$1到$8就有了新的值。

$1 = Active

$2 = arm

$3 = armv7

$4 = s5pc1xx

$5 = samsung

$6 = goni

$7 = s5p_goni

$8 = -

几个传参和其含义

(1)几个很重要的变量

arch=arm

cpu=armv7

vendor=samsung

soc=s5pc1xx

2.12.3.3、符号链接

(1)include/asm -> arch/arm/include/asm

(2)include/asm/arch -> include/asm/arch-s5pc1xx

(3)include/asm/proc -> include/asm/proc-armv

最后创建了include/config.h文件。

2.12.3.4、Makefile中添加交叉编译工具链

(1)官方原版的uboot中CROSS_COMPLIE是没有定义的,需要自己去定义。如果没定义就直接去编译,就会用gcc编译。

(2)添加一行:

CROSS_COMPILE = /usr/local/arm/arm-2009q3/bin/arm-none-linux-gnueabi-

2.12.3.5、配置编译测试

(1)编译过程:

make distclean

make s5p_goni_config

make

(2)结果:得到u-boot.bin即可

如何烧录uboot

(1)烧录u-boot.bin到SD卡中有2种方法:windows下用烧录软件;linux下用dd命令烧录脚本来烧录。因为windows下的工具不开源,出了问题没法调试,所以不推荐。推荐linux下用烧录脚本来烧录(实质是dd命令进行sd卡扇区写入)

(2)移植原来的版本的uboot中的sd_fusing文件夹到官方uboot版本中,使用这个文件夹中的sd_fusing.sh脚本来进行烧录。

分析:为什么烧录运行不正确?

(1)串口接串口2,串口有输出。但是这个串口输出不是uboot输出的,而是内部iROM中的BL0运行时输出的。

(2)输出错误信息分析:

第一个SD checksum Error:是第一顺序启动设备SD0(iNand)启动时校验和失败打印出来的;

第二个SD checksum Error:是第二顺序启动设备SD2(外部SD卡)启动时校验和失败打印出来的;

剩下的是串口启动和usb启动的东西,可以不管。

总结:从两个SD checksum Error,可以看出:外部SD卡校验和失败了。

分析:SD卡烧录出错了,导致SD卡校验和会失败。

2.12.4.3、解决方案分析

(1)为什么SD卡烧录会出错?可能原因:烧录方法错误、烧录原材料错误。

(2)经过分析,sd_fusing这个文件夹下的mkbl1这个程序肯定没错,上一层目录下的u-boot.bin是存在的,校验和失败不失败和u-boot.bin无关。

(3)经过分析和查找,发现是mkbl1程序和start.S中前16个字节校验和的处理上面不匹配造成的,解决方法是在start.S最前面加上16个字节的占位。

2.12.4.4、代码实践

(1)重新编译烧录运行,发现结果只显示一个SD checksum Error。这一个就是内部SD0通道的inand启动校验和失败打印出来的。剩下的没有了说明外部SD卡校验和成功了,只是SD卡上的uboot是错误的,没有串口输出内容,所以没有输出了。

start.S文件分析与移植1

2.12.5.1、start.S流程分析

(1)#define CONFIG_SYS_TEXT_BASE 0x34800000 可以看出我们的uboot的连接地址是在0x34800000位置。

(2)save_boot_params是个空函数,里面直接返回的。

(3)cpu_init_cp15这个函数功能是设置MMU、cache等。这个版本的uboot中未使用虚拟地址,因此MMU在这里直接关掉。

(4)cpu_init_crit,这个函数里只有一句跳转指令,短跳转到lowlevel_init函数。

注意:uboot中有2个lowlevel_init.S文件(文件中还都有lowlevel_init函数),凭一般分析无法断定2个中哪个才是我们想要的。通过分析两个文件所在文件夹下面的Makefile可以判定board/samsung/goni目录下的才是真正包含进来的,arch/arm/cpu/armv7目录下的并没有被包含进来。

还可以通过实践验证的方法来辅助判断。通过查看之前已经编译过的uboot源码目录,看哪个被编程为.o文件了,就知道哪个是真正被使用的了。

(5)lowlevel_init函数在board/samsung/goni目录下,主要作用是时钟设置、串口设置、复位状态判断···这个函数是S5PC100和S5PC110两个CPU共用的。

(6)经过浏览,发现lowlevel_init函数中做的有意义的事情有:关看门狗、调用uart_asm_init来初始化串口、并没有做时钟初始化(下面有时钟初始化的函数,但是实际没调用。如果uboot中没有初始化时钟,那么时钟就是iROM中初始化的那种配置)

2.12.5.2、添加开发板制锁和串口打印字符"O"

(1)我们为了调试uboot的第一阶段,就要看到现象。为了看到现象,我们向lowlevel_init函数中添加2个代码,一个是开发板制锁,一个是串口打印"O"

(2)这两段代码可以直接从ARM裸机全集课程中的代码中来。其实也可以从三星移植版本的uboot中来,但是因为三星移植版本中用到了很多寄存器定义,涉及到头文件的,所以移植过来不方便。

(3)实践添加。

2.12.5.3、实践结果及分析

(1)实验结果是:没看到开发板制锁,串口也没有输出任何东西。实验失败。

(2)结论:因为开发板制锁没有成功,所以我们判定,在开发板制锁代码运行之前uboot就已经挂掉了。下面就是去跟踪代码运行,然后判定问题点再去解决。

start.S文件分析与移植2

2.12.6.1、添加LED点亮代码跟踪程序运行

(1)在基础代码阶段,串口还没有运行,串口调试工具还无法使用时,使用LED点亮的方式来调试程序就是一个有力的手段。

(2)有些情况下可以用Jlink等调试工具来调试这种基础代码。

(3)从程序的基本运行路径端出发,隔一段给他添加一个LED点亮代码,然后运行时根据现象来观察,判定哪里执行了哪里没执行。从而去定位问题。

(4)从以前的裸机代码中组织出一个标准的LED点亮然后延时一段的一个标准代码段:

ldr r0, =0x11111111

ldr r1, =0xE0200240

str r0, [r1]

ldr r0, =((1

ldr r1, =0xE0200244

str r0, [r1]

ldr r2, =9000000

ldr r3, =0x0

delay_loop:

sub r2, r2, #1

cmp r2, r3

bne delay_loop

(5)之前做实验时发现一个现象:我们的uboot运行时按住电源开关时所有4颗LED都是亮的。所以我们做实验时给LED点亮是看不到现象的,所以我们的代码关键是要熄灭某些LED来判断。

(6)我们将熄灭LED的函数在start.S中隔一段的关键部位放上1个,然后运行时通过观察LED的点亮熄灭状态,就知道程序运行到哪里了。

(7)经过判断我们发现:start.S中工作一切正常,但是函数一旦放到lowlevel_init.S中就完全不工作了。通过分析得出结论:b lowlevel_init这句代码出了问题。

2.12.6.2、修改u-boot.lds将lowlevel_init.S放到前部

(1)问题分析:跳转代码出了问题。分析问题出在代码的连接上。

(2)三星S5PV210要求BL1大小为8KB,因此uboot第一阶段代码必须在整个uboot镜像的前8KB内,否则跳转不到。

(3)对比三星移植版本的uboot的u-boot.lds和官方版本uboot的连接脚本u-boot.lds(注意这两个版本的uboot的连接脚本的位置是不同的),就发现lowlevel_init.S的代码段没有被放在前面。

(4)在u-boot.lds中start.o后面添加board/samsung/goni/lowlevel_init.o (.text*),这个就保证了lowlevel_init函数被连接到前面8kb中去。

(5)报错,lowlevel_init重复定义了。

2.12.6.3、修改board/samsung/goni/Makefile解决编译问题

(1)问题分析:为什么会重复定义。因为lowlevel_init这个函数被连接时连接了2次。一次是board/samsung/goni这个目录下生成libgoni.o时连接了1次,第2次是连接脚本最终在连接生成u-boot时又连接了一次,所以重复定义了。

(2)这个错误如何解决?思路是在libgoni.o中不要让他连接进lowlevel_init,让他只在最终连接u-boot时用1次,就可以避免重复定义。

(3)参考当前版本的uboot的start.S文件的处理技巧,解决了这个问题。

2.12.6.4、实践验证。

结果是开发板制锁和串口输出'O'都成功了。

添加DDR初始化1

2.12.7.1、分析下一步移植路线

(1)cpu_init_crit函数成功初始化串口、时钟后,转入_main函数,函数在arch/arm/lib/crt0.S文件中。

(2)在crt0.S中首先设置栈,将sp指向DDR中的栈地址;然后调用board_init_f函数进行板级初始化。函数在arch/arm/lib/board.c中。

(3)在这个版本的uboot中,把以前uboot的第二阶段start_armboot函数分成了2部分:board_init_f和board_init_r。所以在这里就和以前版本的uboot接轨上了,推测board_init_f中肯定是做了板级初始化,board_init_r中进入了uboot的命令行。

(4)分析到这里,在uboot2013.10版本中思路已经很清晰了:uboot的第二阶段就在crt0.S文件中,第二阶段的入口就是_main函数。第一阶段工作主要就是cpu_init_crit函数,所以我们要在cpu_init_crit函数中添加DDR初始化和uboot的重定位。

(5)分析到这里,下一步工作方向就确定了。我们要先在cpu_init_crit函数中添加DDR初始化,然后在start.S中bl _main之前添加uboot的重定位,然后将bl _main改成ldr pc, __main(__main: .word _main)长跳转。然后在crt0.S中board_init_f后删除那些重定位代码,至此uboot的第二阶段就应该能启动起来了。后续的移植就是第二阶段了。

动手移植

(1)添加cpu_init.S文件到uboot2013.10中。注意,这里的代码必须保证在前8kb内,所以必须和lowlevel_init.S文件一样的链接处理。主要是在board/samsung/goni/Makefile中和arch/arm/cpu/u-boot.lds文件中做修改添加。

(2)添加头文件s5pc110.h到include目录下。

(3)对cpu_init.S文件代码进行修整,把一些无用的代码去掉,把一些相关的条件编译人工处理一下。

(4)在SourceInsigt工程中添加入这两个文件。然后重新解析一遍。然后对新添加的代码进行分析修整,把里面一些明显的宏定义缺失给补上。

添加DDR初始化2

移植必要的宏定义

(1)DDR配置参数,从三星版本的smdkv210single.h(九鼎版本的x210_nand.h)中复制到s5p_goni.h中。

(2)s5pc110.h中进行修整。

2.12.8.2、代码同步、编译、再修整

(汇编语音里分号代表注释,但是,在本次实验中不是,必须是‘//’,待研究,可能跟编译器相关)

2.12.8.3、添加调试信息,验证DDR初始化完成。

(1)调试信息有LED点亮和串口输出两种。优先选用串口调试的方法。

(2)在DDR初始化完成后,添加串口输出字符"K",这样启动时如果看到了"OK"就说明DDR已经被成功初始化了。

(3)结果:看到了"OK"标志,说明DDR添加实验成功。

添加uboot第二阶段重定位1

2.12.9.1、在重定位代码前加调试信息定位

(1)逻辑上来说,重定位部分代码应该在DDR初始化之后和uboot第二阶段来临前之间。

(2)uboot的第一阶段和第二阶段的划分并不是绝对的,唯一必须遵循的原则就是第一阶段不能大于8KB。所以uboot的第一阶段最少要完成DDR初始化和重定位,最多不能超过8KB。在满足这些条件时,第一阶段和第二阶段的接点可以随便挑。

(3)找到合适的地方来写重定位代码,重定位之后远跳转到第二阶段的入口。

(4)

2.12.9.2、重定位代码移植

2.12.9.3、清bss段移植

2.12.9.4、movi_bl2_copy函数移植

(1)从三星版本的uboot中赋值movi.c和movi.h到uboot2013.10中。

(2)改makefile和u-boot.lds。

加uboot第二阶段重定位2

2.12.10.1、_mian函数中基本处理

(1)主要就是把里面的重定位代码部分给删除掉。剩下就是:设置栈、调用board_init_f函数和board_init_r函数。

2.12.10.2、代码同步及编译

(1)主要是crt0.S和movi.h。

2.12.10.3、编译中出现问题解决

(1)movi.h中宏定义出错,最后在s5p_goni.h中添加了 CONFIG_EVT1这个宏解决了

(2)连接错误:u-boot contains relocations other than R_ARM_RELATIVE

在uboot下用grep "R_ARM_RELATIVE" -nR *搜索,发现Makefile中有一个检查重定位的规则,屏蔽掉这个规则后编译连接成功。

2.12.10.4、结果验证及下阶段展望

(1)看到了uboot启动打印出来的一系列信息,但是uboot没有进入命令行。

(2)这说明uboot中的DDR初始化和重定位功能都已经完美实现,后面就是第二阶段的继续移植了。

CPU时钟信息显示移植

2.12.13.1、问题分析

(1)时钟显示ARMCLK是400MHz。

(2)调试,把m、p、s和apll_ratio打印出来后,发现这几个值的设置和之前的uboot的设置是不同的。原因在于我们当前版本的uboot中并未对SoC的时钟进行过设置,当前uboot中的时钟是iROM代码默认设置的。

(3)我自己之前一直认为iROM中把210的时钟设置为了1000MHz,然后三星版本的uboot中设置的时钟也是按照这个数据手册356页推荐的这个最佳性能配置时钟设置的。所以以前认为uboot中可以没有时钟设置也是一样的。

(4)但是实际上不是这样的,实际上内部iROM中设置的时钟APLL输出是800MHz,ARMCLK是400MHz。如果uboot中不做时钟的设置实际得到的就是这个时钟。所以我们之前代码得到的结果是400MHz。

(5)所以要解决这个时钟不对的问题,要在lowlevel_init.S中添加上时钟初始化的代码即可。

关于MACH_TYPE的定义问题。

(1)在uboot2013.10中和uboot1.3.4中设计有所不同。在uboot1.3.4中这个东西是分散定义在各个配置头文件当中的。但是在uboot2013.10中我们把MACH_TYPE集中定义在一个文件arch/arm/include/asm/mach-types.h中了。

(2)集中定义其实是uboot从linux内核中学来的。在linux kernel中MACH_TYPE就是在文件中集中定义的。集中定义的好处是方便查阅,不容易定义重复。

(3)这个MACH_TYPE是和开发板绑定的,原则上每一个开发板型号都有一个MACH_TYPE,这个机器码由linux内核管理者来分配的,如果需要应该向这些人申请。

uboot2013.10中SD/MMC驱动浏览

2.12.16.1、从初始化代码开始浏览

(1)

2.12.16.2、相关函数和文件

(1)

drivers/mmc/mmc.c、

drivers/mmc/sdhci.c

board/samsung/goni/goni.c

arch/arm/include/asm/arch-s5pc1xx/mmc.h

当前错误定位及解决方案分析

(1)错误发生路径定位

board_init_r

mmc_initialize

do_preinit

mmc_start_init

mmc_go_idle

mmc_send_cmd

sdhci_send_command

sdhci_transfer_data 错误在这个函数中

(2)错误原因分析

sdhic.c中的所有函数构成了三星210CPU的SD/MMC控制器的驱动。这里面的函数是三星公司的工程师写的,内容就是用来控制210CPU的内部的SD/MMC控制器和外部的SD卡通信的。这就是所谓的驱动。

sdhci_transfer_data函数出错,说明是SoC的SD/MMC控制器和外部SD卡(其实现在用的是SD0的iNand)的数据传输出了问题。(细节分析发现是控制器内部有一个中断状态错误标志被置位了。)

(3)解决方案分析:

两条思路:第一是去逐行的分析SD卡驱动实现(分析中要对SD卡通信协议和210这个SoC的SD控制器非常熟悉),然后发现错误所在,然后修改代码解决问题;第二个是投机取巧的方法,就是把原来三星移植版本的uboot中的SD/MMC驱动整个移植过来替换掉uboot2013.10中的MMC驱动。其实还有第三条折中思路,就是综合第一种和第二种,譬如参考三星移植版本的uboot中的驱动实现来修补uboot2013.10中的驱动实现。

SD卡驱动移植1

2.12.17.1、分析两个版本的uboot中SD卡驱动差异

(1)uboot2013.10中:驱动相关的文件主要有:

drivers/mmc/mmc.c

drivers/mmc/sdhci.c

drivers/mmc/s5p_sdhci.c

board/samsung/goni/goni.c

(2)三星移植版本中,驱动相关的文件主要有:

drivers/mmc/mmc.c

drivers/mmc/s3c_hsmmc.c

cpu/s5pc11x/cpu.c

cpu/s5pc11x/setup_hsmmc.c

(3)经过分析发现:SD卡驱动要工作要包含2部分内容,一部分是drivers/mmc目录下的是驱动,另外一部分是uboot自己提供的初始化代码(譬如GPIO初始化、时钟初始化)

2.12.17.2、复制必要的文件并修改相应Makefile

(1)首先解决drivers/mmc目录下的文件替换。

(2)修改初始化代码。

2.12.17.3、代码浏览及修补

(1)按照代码运行时的流程来逐步浏览代码,看哪里需要修补。

SD卡驱动移植2

2.12.18.1、继续修补驱动代码

(1)include/mmc.h

(2)include/s3c_hsmmc.h

2.12.18.2、同步及编译、问题解决

(1)出错1:cmd_mmc.c中出错。原因是cmd_mmc.c和mmc驱动密切相关,所以改了驱动后这个实现文件也要跟着改,解决方法是从三星版本的直接同名文件复制过来替换

(2)出错2:drivers/mmc/mmc_write.c编译出错。原因是这个文件和本来版本中的mmc.c文件相关,但是mmc.c被替换掉了所以这个文件编译报错。解决方案就是修改makefile去掉这个文件的依赖,让他不被编译。

(3)出错3:#include注释掉,然后添加#include

环境变量的移植

(3)环境变量应该被放在哪里?虽然无法确定ENV一定要放在哪里,但是有一些地方肯定是不能放的,否则将来会出问题。原则是同一个SD卡扇区只能放一种东西,不能叠加,否则就会被覆盖掉。uboot烧录时使用的扇区数是:SD2的扇区1-16和49-x(x-49大于等于uboot的大小)(可以从烧录脚本查看,sd_fusing.sh)

(3)从uboot的烧录情况来看,SD2的扇区0空闲,扇区1-16被uboot的BL1占用,扇区17-48空闲,扇区49-x被uboot的BL2占用。再往后就是内核、rootfs等镜像的分区了。系统移植工程师可以根据kernel镜像大小、rootfs大小等来自由给SD分区。

(4)从uboot的分区情况来看,ENV不能往扇区1-16或者49-x中来放置,其他地方都可以商量。ENV的大小是16K字节也就是32个扇区(在include/configs路径下的头文件中有定义(s5p_goni.h),也就是我们自己定义的)。

环境变量相关代码浏览

(1)目前情况是uboot在SD2中,而ENV在SD0中,所以现在ENV不管放在哪个扇区都能工作,不会有问题。但是我们还是得找到ENV分区所在并且改到不会和uboot冲突,因为将来部署系统时我们会将uboot和kernel、rootfs等都烧录到iNnand中去,那时候也要确保不会冲突。

(2)static inline int write_env(struct mmc *mmc, unsigned long size,

unsigned long offset, const void *buffer)

类似于这种函数,在代码分析中,关键是弄明白各种参数的意义。mmc表示要写的mmc设备,size表示要写的大小,offset表示要写到SD卡的哪个扇区去,buffer是要写的内容。

(3)CONFIG_ENV_OFFSET这个宏决定了我们的ENV在SD卡中相对SD卡扇区0的偏移量,也就是ENV写到SD卡的哪里去了。经过分析发现这个宏的值为0.所以我们的ENV 被写到了0扇区开始的32个扇区中。

(4)写到这里肯定不行,因为和uboot的BL1冲突了。解决方案是改变这个CONFIG_ENV_OFFSET的值,将ENV写到别的空闲扇区去。

(5)#define MOVI_BL2_POS ((eFUSE_SIZE / MOVI_BLKSIZE) + MOVI_BL1_BLKCNT + MOVI_ENV_BLKCNT) 后面这三个其实分别是1+16+32=49

其中的1就是扇区0(空闲的),16是就是扇区1-16(uboot的BL1),32就是扇区17-48(存放ENV的),49自然就是uboot的BL2开始扇区了。这种安排是三星移植的uboot版本中推荐的SD卡的分区方式,不一定是唯一的。

(6)我们参考这个设计,即可实现环境变量不冲突。所以只要将ENV放到17扇区起始的地方即可 #define CONFIG_ENV_OFFSET 17*512。

网卡驱动的移植1

2.12.22.1、添加网络支持

(1)uboot中对各种功能也是一个条件编译可以配置可以裁剪的设计(从linux内核学来的),默认情况下我们的uboot没有选择支持网络。

(2)在配置头文件中添加一行 #define CONFIG_CMD_NET

(3)添加了网络支持宏之后,在uboot初始化时就会执行eth_initialize函数,从而网络相关代码初始化就会被执行,将来网络就有可能能用。

2.12.22.2、添加ping和tftp命令

(1)在linux系统中网络底层驱动被上层应用调用的接口是socket,是一个典型的分层结构,底层和上层是完全被socket接口隔离的。

(2)但是在uboot中网络底层驱动和上层应用是黏在一起的,不分层。意思就是上层网络的每一个应用都是自己去调用底层驱动中的操作硬件的代码来实现的。

(3)uboot中有很多预先设计的需要用到网络的命令,和我们直接相关的就是ping和tftp这两个命令。这两个命令在uboot中也是需要用相应的宏开关来打开或者关闭的。

(4)经过代码检查,发现ping命令开关宏为CONFIG_CMD_PING,而tftp命令的开关为CONFIG_CMD_NET,确认添加。

2.12.22.3、代码实践。结果是ping和tftp命令都被识别了,但是都提示no ethernet found`````网络不通。为什么不通?因为还没做初始化等移植

2.12.22.4、移植网卡初始化代码

2.12.23.网卡驱动的移植2

2.12.23.1、实验现象分析

(1)因为我们没有自定义的网卡初始化函数(board_eth_init或者cpu_eth_init),所以uboot启动时初始化网卡时打印:Net: Net Initialization Skipped

(2)eth.c中有2个很重要的全局变量:eth_devices(用来指向一个链表,这个链表中保存了当前系统中所有的网卡信息)和eth_current(eth_current指针指向当前我们正在操作的那个网卡)。

(3)在linux的网卡驱动体系中,有一个数据结构(struct eth_device)用来表示(封装)一个网卡的所有信息,系统中注册一个网卡时就是要建立一个这个结构体的实例,然后填充这个实例中的各个元素,最后将这个结构体实例加入到eth_devices这个链表上,就完成了注册。了解了这些之后,你就明白了网卡驱动在初始化时必须负责将自己注册到系统的网卡驱动体系中(其实就是把自己的eth_device结构体实例添加到eth_devices链表中)。如果你不做这个过程就会出现:网卡找不到的错误。

(4)分析当前的问题是:在305行判断eth_devices是否为NULL之前没有去做网卡驱动的注册,所以这里为NULL,所以打印出了“No ethernet found.”

2.12.23.2、DM9000驱动浏览

(1)想解决这个问题,就是要在305行之前去注册网卡驱动。注册网卡驱动的代码不能随便乱写,一定要遵守linux网卡驱动架构的要求。这一块的代码一般属于网卡驱动的一部分,像这里就在dm9000x.c中。

(2)dm9000x.c中的最后一个函数int dm9000_initialize(bd_t *bis),这个函数就是用来注册dm9000网卡驱动的。

2.12.24.3、问题修复

(1)根据之前分析uboot函数,发现前面有2个函数预留的可以用来放网卡初始化函数的,经过对比感觉board_eth_init函数稍微合适点,于是乎去添加。

2.12.24.4、对比和总结

背光电路分析

(1)LCD的背光源有2种设计。一种是简单设计,背光只能点亮和熄灭两种状态,不能调亮度;另一种设计类似手机屏幕可以调节亮度。第一种设计很简单,就是开和关。第二种模式比较复杂,需要一个额外的PWM调光IC来支持,X210的LCD模组上使用MP3202芯片来做调光。

(2)分析原理图和MP3202的数据手册,可以得出结论:

第一:PWMTOUT0(GPD0_0)接在了IC的FB引脚上,SoC应该通过该引脚输出一个频率合适的PWM波形给调光IC,这个波形的占空比就会控制MP3202输出的电流大小,从而控制屏幕亮度。

第二:L_DISP(DISP、SYS_OE、GPF3_5)接在了MP3202的EN引脚上,SoC应该给该引脚一个高电平来让背光工作,或者给一个低电平来让背光不工作。

(3)综合分析:背光要点亮,要同时满足以上两个条件。GPD0_0要输出低电平或者PWM信号,同时GPF3_5要输出一个高电平。一般来说我们在uboot中都把GPD0_0设置成输出模式然后输出低电平来点亮背光。

logo显示和LCD屏幕分辨率适配

2.13.2.1、LCD驱动mpadfb.c分析

(1)fb_init函数给framebuffer相关的数据结构赋值。左值的info是一个结构体,这个结构体描述fb驱动中的硬件设备参数的,右值的lcd是自己定义的一个数据结构,里面的值都是我们人为配置给驱动体系的。

(2)lcd_port_init看名字是lcd端口的初始化,端口就是GPIO,所以这个函数是在初始化LCD相关的SoC的引脚。

(3)lcd_reg_init看名字是LCD的寄存器的初始化,就是SoC的LCD控制器中的那些寄存器的初始化。

(4)以上三个函数调用执行完后,LCD初始化就结束了。然后向LCD写东西就能显示了。但是你要注意背光是否被点亮了。后面剩下的事情就是写东西和开背光两个了。

(5)display_logo(&s5pv210_fb);这个函数负责把logo图片写到LCD的fb中去。backlight_brigness_init(1);负责开背光。

什么是fastboot

(1)fastboot是android使用的一种刷机方法.android系统设计了2种刷机方式:fastboot和recovery。

(2)fastboot使用usb作为物理传输。刷机其实就是镜像传输+烧录,fastboot刷机时就是通过usb线来传输镜像的。

(3)fastboot是uboot中的一个命令。uboot进入命令行中后,如果需要刷机,则可以在命令行执行fastboot命令就可以让uboot进入fastboot模式,刷机就是在fastboot模式下进行的。

(4)fastboot需要主机端的fastboot软件配合。要实现fastboot刷机,只有开发板端uboot是不行的,还需要在主机上有fastboot.exe的软件配合。

(5)fastboot在开发板和主机间定义了一套协议。其实fastboot是我们在开发板和主机之间定义的一套协议,这套协议以usb为底层传输物理层,协议规定了主机fastboot软件和开发板fastboot软件之间的信息传输规则。消息传递可以实现功能有:主机可以向开发板发送命令、开发板可以向主机发送回复、主机可以向开发板发送文件(download)

fastboot的工作原理

(1)uboot的fastboot命令将开发板伪装成一个usb从设备。开发板本身并不是一个usb设备,所以开发板直接插到电脑上电脑是没有反应,没有提示发现设备需要装驱动的。伪装之后开发板就被主机windows识别成一个安卓手机了。

(2)主机的fastboot软件和开发板的fastboot程序通信来工作。平时工作时,开发板端只要执行了fastboot命令进入fastboot模式即可,剩下的就不用管了。主机端通过运行fastboot命令,传递不同的参数来实现主机端和开发板端的通信。

譬如主机端执行fastboot devices,则这个命令通过USB线被传递到开发板中被开发板的fastboot程序接收,接收后去处理然后向主机端发送反馈信息,主机端接收到反馈信息后显示出来。

(3)我们学习fastboot时分析代码的思路就是:

主机端:fastboot.exe的源代码没有,fastboot协议虽然能找到但是很枯燥,所以主机端没有去分析的。

开发板端:主要分析点就是uboot如何进入fastboot模式,fastboot模式下如何响应主机发送的各种命令。

uboot的fastboot代码分析1

2.13.4.1、do_fastboot函数

(1)do_fastboot函数本身涉及到很多操作SD/Nand等磁盘的,主要目的是为了刷机。要完整的分析fastboot的函数细节很复杂很麻烦,我们并不是要做这个。

2.13.4.2、关键点:rx_handler(顺藤摸瓜一路找下去)

do_fastboot

fastboot_poll

fboot_usb_int_hndlr

fboot_usb_pkt_receive

fboot_usb_int_bulkout

fastboot_interface->rx_handler(函数指针)

指向cmd_fastboot.c/rx_handler

找的过程涉及到USB物理层通信的一些概念和理解,相对比较复杂。最终uboot这边的fastboot是通过rx_handler函数来处理主机端fastboot软件发送过来的信息的。fastboot协议的命令实现都在这个函数中提现。所以这个函数的分析就是重点。

2.13.4.3、代码分析

(1)大文件download机制

rx_handler函数中通过if和else分成了两部分,if部分负责处理download,else部分负责处理命令。usb传输单次传输最大只能有限个字节(64、256),因此当我们发送比较小的东西(譬如命令)时可以单次传输完毕;当我们发送比较大的东西(譬如文件)时就必须要分包发送。

(2)down后的响应机制。开发板端通过fastboot_tx_status函数向主机发送响应,主机显示这个响应。

uboot的fastboot代码分析2

(1)uboot中fastboot有关的一些宏定义设置值

CFG_FASTBOOT_TRANSFER_BUFFER 配置fastboot工作时的缓冲区地址,fastboot在执行某些功能时需要大块内存做缓冲区(譬如download时),这里就是在给他配置缓冲区。

CFG_FASTBOOT_TRANSFER_BUFFER_SIZE fastboot缓冲区的大小。

注意:很多同学在之前刷机时,烧录uboot和zImage都正常,但是烧录android镜像x210.img时错误,提示:image too large...(意思是文件太大)。这个错误的原因就是x210.img太大了,超出了CFG_FASTBOOT_TRANSFER_BUFFER_SIZE所以放不下了,所以uboot的fastboot这里报错了。

2.13.5.1、fastboot命令响应之:fastboot reboot

(1)作用:在PC机这边可以远程重启开发板

2.13.5.2、fastboot命令响应之:fastboot getvar

(1)作用是得到一些fastboot中定义的变量名的值,譬如version、product。。。。。

2.13.5.3、fastboot命令响应之:fastboot erase

2.13.5.4、fastboot命令响应之:fastboot download

2.13.5.5、fastboot命令响应之:fastboot boot

2.13.5.6、fastboot命令响应之:fastboot flash

2.13.5.7、fastboot命令响应之:fastboot oem

(1)oem命令是用户自定义的。其他命令全都是fastboot协议定义的,但是有时候自带的命令不足以使用,oem厂商可能希望定义一些自己专有的命令,则可以使用oem命令。

驱动属于内核的一部分

(1)驱动就是内核中的硬件设备管理模块

(2)驱动工作在内核态。

(3)驱动程序故障可能导致整个内核崩溃

(4)驱动程序漏洞会使内核不安全

内核和根文件系统

(1)根文件系统提供根目录。

(2)进程1存放在根文件系统中

(3)内核启动最后会去装载根文件系统。

(4)总结:根文件系统为操作系统启动提供了很多必备的资源:根目录、进程1

linux内核源码目录结构2

(1)arch。arch是architecture的缩写,意思是架构。arch目录下是好多个不同架构的CPU的子目录,譬如arm这种cpu的所有文件都在arch/arm目录下,X86的CPU的所有文件都在arch/x86目录下。

(2)block。英文是块的意思,在linux中block表示块设备(以块(多个字节组成的整体,类似于扇区)为单位来整体访问),譬如说SD卡、iNand、Nand、硬盘等都是块设备。你几乎可以认为块设备就是存储设备。block目录下放的是一些linux存储体系中关于块设备管理的代码。

(3)crypto。英文意思是加密。这个目录下放了一些各种常见的加密算法的C语言代码实现。譬如crc32、md5、sha1等。

(4)Documentation。里面放了一些文档。

(5)drivers。驱动目录,里面分门别类的列出了linux内核支持的所有硬件设备的驱动源代码。

(6)firmware。固件。什么是固件?固件其实是软件,不过这个软件是固话到IC里面运行的叫固件。就像S5PV210里的iROM代码。

(7)fs。fs就是file system,文件系统,里面列出了linux支持的各种文件系统的实现。

(8)include。头文件目录,公共的(各种CPU架构共用的)头文件都在这里。每种CPU架构特有的一些头文件在arch/arm/include目录及其子目录下。

(9)init。init是初始化的意思,这个目录下的代码就是linux内核启动时初始化内核的代码。

(10)ipc。ipc就是inter process commuication,进程间通信,里面都是linux支持的IPC的代码实现。

(11)kernel。kernel就是内核,就是linux内核,所以这个文件夹下放的就是内核本身需要的一些代码文件。

(12)lib。lib是库的意思,这里面都是一些公用的有用的库函数,注意这里的库函数和C语言的库函数不一样的。在内核编程中是不能用C语言标准库函数,这里的lib目录下的库函数就是用来替代那些标准库函数的。譬如在内核中要把字符串转成数字用atoi,但是内核编程中只能用lib目录下的atoi函数,不能用标准C语言库中的atoi。譬如在内核中要打印信息时不能用printf,而要用printk,这个printk就是我们这个lib目录下的。

(13)mm。mm是memory management,内存管理,linux的内存管理代码都在这里。

(14)net。该目录下是网络相关的代码,譬如TCP/IP协议栈等都在这里。

(15)scripts。脚本,这个目录下全部是脚本文件,这些脚本文件不是linux内核工作时使用的,而是用来辅助对linux内核进行配置编译生产的。我们并不会详细进入分析这个目录下的脚本,而是通过外围来重点学会配置和编译linux内核即可。

(16)security。安全相关的代码。不用去管。

(17)sound。音频处理相关的。

(18)tools。linux中用到的一些有用工具

(19)usr。目录下是initramfs相关的,和linux内核的启动有关,暂时不用去管。

(20)virt。内核虚拟机相关的,暂时不用管。

总结:这么多目录跟我们关系很紧密的就是arch和drivers目录,然后其他有点相关的还有include、block、mm、net、lib等目录。

make xx_defconfig和make menuconfig相配合

(1)我们为了对.config文件中的两三千个配置项做逐一合适的配置,专门发明了两步结合的配置方式。

(2)其实只要人的记忆足够好,大脑足够厉害,完全可以手工去书写/修改.config文件完成内核配置,最终只要.config中内容是正确的,就不影响编译过程。

(3)第一步:make xxx_defconfig解决的问题是大部分的配置项(这一步结束后99%的配置项就已经正确了),下来就是对个别不同的针对我们的开发板进行细节调整,细节调整就通过make menuconfig来完成。

(4)make xxx_defconfig这一步其实是参考别人已经做好的,这样做有很多好处:减少很多工作量,避开了很多自己不懂的配置项(譬如对内存管理的、调度系统的等模块的配置项),我们只用管自己需要管的。

(5)make menuconfig其实就是读取第一步得到的.config,然后给我们一个图形化的界面,让我们可以更加容易的找到自己想要修改的配置项,然后更改配置他。

make xx_defconfig到底做了什么?

(1)make x210ii_qt_defconfig其实相当于:cp arch/arm/configs/x210ii_qt_defconfig .config

(2)arch/arm/configs目录下的这么多个xxx_defconfig哪里来的?其实这些文件都是别人手工配置好适合一定的开发板的.config文件后自己把.config文件保存过去的。譬如说我们用S5PV210这个SoC,针对这个SoC的开发板的最初配置肯定是三星的工程师去做的。

menuconfig的使用和演示

2.15.5.1、使用说明解释

箭头按键导航整个菜单,回车按键选择子菜单(注意选项后面有 --->的选项才是有子菜单的,没有这个标识的没有子菜单),高亮的字母是热键(快捷键),键盘按键Y、N、M三个按键的作用分别是将选中模块编入、去除、模块化。双击ESC表示退出,按下?按键可以显示帮助信息,按下/按键可以输入搜索内容来全局搜索信息(类似于vi中的搜索),[]不可以模块化,<>的才可以模块化。

注:linux内核中一个功能模块有三种编译方法:一种是编入、一种去去除、一种是模块化。所谓编入就是将这个模块的代码直接编译连接到zImage中去,去除就是将这个模块不编译链接到zImage中,模块化是将这个模块仍然编译,但是不会将其链接到zImage中,会将这个模块单独链接成一个内核模块.ko文件,将来linux系统内核启动起来后可以动态的加载或卸载这个模块。

在menuconfig中选项前面的括号里,*表示编入,空白表示去除,M表示模块化

menuconfig的工作原理

2.15.6.1、menuconfig本身由一套软件支持

(1)linux为了实现图形化界面的配置,专门提供了一套配置工具menuconfig。

(2)ncurses库是linux中用来实现文字式的图形界面,linux内核中使用了ncurses库来提供menuconfig

(3)scripts\kconfig\lxdialog目录下的一些c文件就是用来提供menuconfig的那些程序源代码。

2.15.6.2、menuconfig读取Kconfig文件

(1)menuconfig本身的软件只负责提供menuconfig工作的这一套逻辑(譬如在menuconfig中通过上下左右箭头按键来调整光标,Enter ESC键等按键按下的响应),而并不负责提供内容(菜单里的项目)。

(2)menuconfig显示的菜单内容(一方面是菜单的目录结构,另一方面是每一个菜单项目的细节)是由内核源码树各个目录下的Kconfig文件来支持的。Kconfig文件中按照一定的格式包含了一个又一个的配置项,每一个配置项在make menuconfig中都会成为一个菜单项目。而且menuconfig中显示的菜单目录结构和源码目录中的Kconfig的目录结构是一样的。

(3)在相应的Kconfig文件中删除一个config项,则再次make menuconfig时这个项目已经看不到了。

2.15.6.3、menuconfig读取/写入.config文件

(1)刚才已经知道menuconfig的菜单内容来自于Kconfig文件,但是每一个菜单的选择结果(Y、N、M)却不是保存在Kconfig文件中的。Kconfig文件是不变的,Kconfig文件只是决定有没有这个菜单项,并不管这个菜单项的选择结果。

(2)menuconfig工作时在我们make menuconfig打开时,他会读取.config文件,并且用.config文件中的配置选择结果来初始化menuconfig中各个菜单项的选择值。

总结:菜单项的项目内容从Kconfig文件来,菜单项的选择值从.config文件来

(3)当我们每次退出make menuconfig时,menuconfig机制会首先检查我们有没有更改某些配置项的值,如果我们本次没有更改过任意一个配置项目的值那直接退出;如果我们有改动配置项的值则会提示我们是否保存。此时如果点保存,则会将我们更改过的配置重新写入.config文件中记录,下一次再次打开make menuconfig时会再次加载.config,最终去编译内核时编译连接程序会考虑.config中的配置值指导整个编译连接过程。

总结:本节课主要内容就是讲:menuconfig和Kconfig和.config的关系。

Kconfig文件详解1

2.15.7.1、Kconfig的格式

(3)#开头的行是注释行

(4)menuconfig表示菜单(本身属于一个菜单中的项目,但是他又有子菜单项目)、config表示菜单中的一个配置项(本身并没有子菜单下的项目)。

(5)menuconfig或者config后面空格隔开的大写字母表示的类似于 NETDEVICES 的就是这个配置项的配置项名字,这个字符串前面添加CONFIG_后就构成了.config中的配置项名字。

(6)一个menuconfig后面跟着的所有config项就是这个menuconfig的子菜单。这就是Kconfig中表示的目录关系。

(7)内核源码目录树中每一个Kconfig都会source引入其所有子目录下的Kconfig,从而保证了所有的Kconfig项目都被包含进menuconfig中。这个也告诉我们:如果你自己在linux内核中添加了一个文件夹,一定要在这个文件夹下创建一个Kconfig文件,然后在这个文件夹的上一层目录的Kconfig中source引入这个文件夹下的Kconfig文件。

2.15.7.2、tristate和bool的含义

(1)tristate意思是三态(3种状态,对应Y、N、M三种选择方式),bool是要么真要么假(对应Y和N)。所以tristate的意思就是这个配置项可以被三种选择,bool的意思是这个配置项只能被2种选择。

把menuconfig中的菜单项、Kconfig中的配置项、.config中的一行、 Makefile中的一行,这4个东西结合起来理解,则整个linux内核的配置体系就明了了。

menuconfig的实验学习思路

2.15.9.1、验证menuconfig和.config的关系

(1)make menuconfig时,会读取.config中的配置值来初始化menuconfig中的配置项。

验证:如果理论正确的,那么我自己手工修改了.config的配置后,再次make menuconfig时看到的初始值就应该是我手工修改的。

(2)menuconfig中修改了(按Y、N、M)配置项的值,然后退出时保存,则这个保存结果会修改.config文件中的相应行。

验证:如果结论是正确的,那么在menucofig中修改了配置后保存退出,再次去手工打开.config文件则可以看到相应配置的一行内容被修改了。

2.15.9.2、验证menuconfig和Kconfig的关系

(1)menuconfig读取Kconfig的内容作为菜单项目内容。

验证1:在Kconfig中删除一个config项,则再次make menuconfig时就看不到这个项目了。(上课时已经验证过了)

验证2:在Kconfig中自己添加创建一个config项,则再次make menuconfig时就能看到多了一个项目。

2.15.9.3、验证验证menuconfig和Makefile的关系

(1)我找一个模块,把他配制成y,然后去make编译连接,最后得到的zImage中这个模块就应该被编译连接进去到zImage中了。

验证:

方法一:去这个模块对应的源代码目录看一下这个源码有没有被编译

方法二:去zImage对应的elf格式的vmlinux中查看符号

方法三:将vmlinux反编译(objdump)后得到的文件中找模块对应的符号

arm-none-linux-gnueabi-objdump -D vmlinux > vmlinux.txt

方法四:将zImage下载到开发板中启动,启动后看你的模块能不能工作

在make编译内核时,也可以通过命令行给内核makefile传参(跟uboot配置编译时传参一样)。譬如make O=xxx可以指定不在源代码目录下编译,而到另外一个单独文件夹下编译。

(6)kernel的顶层Makefile中定义了2个变量很重要,一个是ARCH,一个是CROSS_COMPILE。ARCH决定当前配置编译的路径,譬如ARCH = arm的时候,将来在源码目录下去操作的arch/arm目录。CROSS_COMPILE用来指定交叉编译工具链的路径和前缀。

(7)CROSS_COMPILE = xxx和ARCH = xxx和O=xxx这些都可以在make时通过命令行传参的方式传给顶层Makefile。

所以有时候你会看到别人编译内核时:make O=/tmp/mykernel ARCH=arm CROSS_COMPILE=/usr/local/arm/arm-2009q3/bin/arm-none-linux-gnueabi-

链接脚本分析

(1)分析连接脚本的目的就是找到整个程序的entry

(2)kernel的连接脚本并不是直接提供的,而是提供了一个汇编文件vmlinux.lds.S,然后在编译的时候再去编译这个汇编文件得到真正的链接脚本vmlinux.lds。

(3)vmlinux.lds.S在arch/arm/kernel/目录下。

(4)思考:为什么linux kernel不直接提供vmlinux.lds而要提供一个vmlinux.lds.S然后在编译时才去动态生成vmlinux.lds呢?

猜测:.lds文件中只能写死,不能用条件编译。但是我们在kernel中链接脚本确实有条件编译的需求(但是lds格式又不支持),于是乎kernel工作者找了个投机取巧的方法,就是把vmlinux.lds写成一个汇编格式,然后汇编器处理的时候顺便条件编译给处理了,得到一个不需要条件编译的vmlinux.lds。

(5)入门在哪里?从vmlinux.lds中ENTRY(stext)可以知道入口符号是stext,在SI中搜索这个符号,发现arch/arm/kernel/目录下的head.S和head-nommu.S中都有。

(6)head.S是启用了MMU情况下的kernel启动文件,相当于uboot中的start.S。head-nommu.S是未使用mmu情况下的kernel启动文件。

head.S文件分析1

2.16.2.1、内核运行的物理地址与虚拟地址

(1)KERNEL_RAM_VADDR(VADDR就是virtual address),这个宏定义了内核运行时的虚拟地址。值为0xC0008000

(2)KERNEL_RAM_PADDR(PADDR就是physical address),这个宏定义内核运行时的物理地址。值为0x30008000

(3)总结:内核运行的物理地址是0x30008000,对应的虚拟地址是0xC0008000。

内核的真正入口

(1)内核的真正入口就是ENTRY(stext)处

(2)前面的__HEAD定义了后面的代码属于段名为.head.text的段

2.16.2.3、内核运行的硬件条件

(1)内核的起始部分代码是被解压代码调用的。回忆之前讲zImage的时候,uboot启动内核后实际调用运行的是zImage前面的那段未经压缩的解压代码,解压代码运行时先将zImage后段的内核解压开,然后再去调用运行真正的内核入口。

(2)内核启动不是无条件的,而是有一定的先决条件,这个条件由启动内核的bootloader(我们这里就是uboot)来构建保证。

/*

 * Kernel startup entry point.

 * ---------------------------

 *

 * This is normally called from the decompressor code.  The requirements

 * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,

 * r1 = machine nr(机器码), r2 = atags pointer(传参指针).

 *

 * This code is mostly position independent, so if you link the kernel at

 * 0xc0008000, you call this at __pa(0xc0008000)(__pa()作用:虚拟地址转换为物理地址).

 *

 * See linux/arch/arm/tools/mach-types for the complete list of machine

 * numbers for r1.

 *

 * We're trying to keep crap to a minimum; DO NOT add any machine specific

 * crap here - that's what the boot loader (or in extreme, well justified

 * circumstances, zImage) is for.

 */

(3)ARM体系中,函数调用时实际是通过寄存器传参的(函数调用时传参有两种设计:一种是寄存器传参,另一种是栈内存传参)。所以uboot中最后theKernel (0, machid, bd->bi_boot_params);执行内核时,运行时实际把0放入r0中,machid放入到了r1中,bd->bi_boot_params放入到了r2中。ARM的这种处理技巧刚好满足了kernel启动的条件和要求。

(4)kernel启动时MMU是关闭的,因此硬件上需要的是物理地址。但是内核是一个整体(zImage)只能被连接到一个地址(不能分散加载),这个连接地址肯定是虚拟地址。因此内核运行时前段head.S中尚未开启MMU之前的这段代码就很难受。所以这段代码必须是位置无关码,而且其中涉及到操作硬件寄存器等时必须使用物理地址。

2.16.2.4、内核启动要求的传参方式

内核启动的汇编阶段

2.16.3.1、__lookup_processor_type

(1)我们从cp15协处理器的c0寄存器中读取出硬件的CPU ID号,然后调用这个函数来进行合法性检验。如果合法则继续启动,如果不合法则停止启动,转向__error_p启动失败。

(2)该函数检验cpu id的合法性方法是:内核会维护一个本内核支持的CPU ID号码的数组,然后该函数所做的就是将从硬件中读取的cpu id号码和数组中存储的各个id号码依次对比,如果没有一个相等则不合法,如果有一个相等的则合法。

(3)内核启动时设计这个校验,也是为了内核启动的安全性着想。

2.16.3.2、__lookup_machine_type

(1)该函数的设计理念和思路和上面校验cpu id的函数一样的。不同之处是本函数校验的是机器码。

2.16.3.3、__vet_atags

(1)该函数的设计理念和思路和上面2个一样,不同之处是用来校验uboot给内核的传参ATAGS格式是否正确。这里说的传参指的是uboot通过tag给内核传的参数(主要是板子的内存分布memtag、uboot的bootargs)

(2)内核认为如果uboot给我的传参格式不正确,那么我就不启动。

(3)uboot给内核传参的部分如果不对,是会导致内核不启动的。譬如uboot的bootargs设置不正确内核可能就会不启动。

2.16.3.4、__create_page_tables

(1)顾名思义,这个函数用来建立页表。

(2)linux内核本身被连接在虚拟地址处,因此kernel希望尽快建立页表并且启动MMU进入虚拟地址工作状态。但是kernel本身工作起来后页表体系是非常复杂的,建立起来也不是那么容易的。kernel想了一个好办法

(3)kernel建立页表其实分为2步。第一步,kernel先建立了一个段式页表(和uboot中之前建立的页表一样,页表以1MB为单位来区分的),这里的函数就是建立段式页表的。段式页表本身比较好建立(段式页表1MB一个映射,4GB空间需要4096个页表项,每个页表项4字节,因此一共需要16KB内存来做页表),坏处是比较粗不能精细管理内存;第二步,再去建立一个细页表(4kb为单位的细页表),然后启用新的细页表废除第一步建立的段式映射页表。

(4)内核启动的早期建立段式页表,并在内核启动前期使用;内核启动后期就会再次建立细页表并启用。等内核工作起来之后就只有细页表了。

2.6.3.5、__switch_data

(1)建立了段式页表后进入了__switch_data部分,这东西是个函数指针数组。

(2)分析得知下一步要执行__mmap_switched函数

(3)复制数据段、清除bss段(目的是构建C语言运行环境)

(4)保存起来cpu id号、机器码、tag传参的首地址。

(5)b start_kernel跳转到C语言运行阶段。

总结:汇编阶段其实也没干啥,主要原因是uboot干了大部分活。汇编阶段主要就是校验启动合法性、建立段式映射的页表并开启MMU以方便使用内存、跳入C阶段。

内核启动的C语言阶段2

2.16.5.1、杂碎

(1)smp。smp就是对称多处理器(其实就是我们说的多核心CPU)

(2)lockdep。锁定依赖,是一个内核调试模块,处理内核自旋锁死锁问题相关的。

(3)cgroup。control group,内核提供的一种来处理进程组的技术。

2.16.5.2、打印内核版本信息

(1)代码位于:kernel/init/main.c中的572行

(2)printk函数是内核中用来从console打印信息的,类似于应用层编程中的printf。内核编程时不能使用标准库函数,因此不能使用printf,其实printk就是内核自己实现的一个printf。

(3)printk函数的用法和printf几乎一样,不同之处在于可以在参数最前面用一个宏来定义消息输出的级别。为什么要有这种级别?主要原因是linux内核太大了,代码量太多,里面的printk打印信息太多了。如果所有的printk都能打印出来而不加任何限制,则最终内核启动后得到海量的输出信息。

(4)为了解决打印信息过多,无效信息会淹没有效信息这个问题,linux内核的解决方案是给每一个printk添加一个打印级别。级别定义0-7(注意编程的时候要用相应的宏定义,不要直接用数字)分别代表8种输出的重要性级别,0表示最重要,7表示最不重要。我们在printk的时候自己根据自己的消息的重要性去设置打印级别。

(5)linux的控制台监测消息的地方也有一个消息过滤显示机制,控制台实际只会显示级别比我的控制台定义的级别高的消息。譬如说控制台的消息显示级别设置为4,那么只有printk中消息级别为0-3(也可能是0-4)的才可以显示看见,其余的被过滤掉了。

(6)linux_banner的内容解析。

内核启动的C语言阶段3

2.16.6.1、setup_arch函数简介

(1)从名字看,这个函数是CPU架构相关的一些创建过程。

(2)实际上这个函数是用来确定我们当前内核的机器(arch、machine)的。我们的linux内核会支持一种CPU的运行,CPU+开发板就确定了一个硬件平台,然后我们当前配置的内核就在这个平台上可以运行。之前说过的机器码就是给这个硬件平台一个固定的编码,以表征这个平台。

(3)当前内核支持的机器码以及硬件平台相关的一些定义都在这个函数中处理。

2.16.6.2、Machine查找

(1)setup_processor函数用来查找CPU信息,可以结合串口打印的信息来分析。

(2)setup_machine函数的传参是机器码编号,machine_arch_type符号在include/generated/mach-types.h的32039-32050行定义了。经过分析后确定这个传参值就是2456.

(3)函数的作用是通过传入的机器码编号,找到对应这个机器码的machine_desc描述符,并且返回这个描述符的指针。

(4)其实真正干活的函数是lookup_machine_type,找这个函数发现在head-common.S中,真正干活的函数是__lookup_machine_type

(5)__lookup_machine_type函数的工作原理:内核在建立的时候就把各种CPU架构的信息组织成一个一个的machine_desc结构体实例,然后都给一个段属性.arch.info.init,链接的时候会保证这些描述符会被连接在一起。__lookup_machine_type就去那个那些描述符所在处依次挨个遍历各个描述符,比对看机器码哪个相同。

内核启动的C语言阶段4

2.16.6.3、setup_arch函数进行了基本的cmdline处理

(1)这里说的cmdline就是指的uboot给kernel传参时传递的命令行启动参数,也就是uboot的bootargs。

(2)有几个相关的变量需要注意:

default_command_line:看名字是默认的命令行参数,实际是一个全局变量字符数组,这个字符数组可以用来存东西。

CONFIG_CMDLINE:在.config文件中定义的(可以在make menuconfig中去更改设置),这个表示内核的一个默认的命令行参数。

(3)内核对cmdline的处理思路是:内核中自己维护了一个默认的cmdline(就是.config中配置的这一个),然后uboot还可以通过tag给kernel再传递一个cmdline。如果uboot给内核传cmdline成功则内核会优先使用uboot传递的这一个;如果uboot没有给内核传cmdline或者传参失败,则内核会使用自己默认的这个cmdline。以上说的这个处理思路就是在setup_arch函数中实现的。

2.6.6.4、实验验证内核的cmdline确定

(1)验证思路:首先给内核配置时配置一个基本的cmdline,然后在uboot启动内核时给uboot设置一个bootargs,然后启动内核看打印出来的cmdline和uboot传参时是否一样。

(2)在uboot中去掉bootargs,然后再次启动内核看打印出来的cmdline是否和内核中设置的默认的cmdline一样。

注意:uboot给内核传递的cmdline非常重要,会影响内核的运行,所以要谨慎。有时候内核启动有问题,可以分析下是不是uboot的bootargs设置不对。

注意:这个传参在这里确定出来之后,还没完。后面还会对这个传参进行解析。解析之后cmdline中的每一个设置项都会对内核启动有影响。

思考:内核为什么要这样设计?

内核启动的C语言阶段5

2.16.8.1、setup_command_line

(1)也是在处理和命令行参数cmdline有关的任务。

2.16.8.2、parse_early_param&parse_args

(1)解析cmdline传参和其他传参

(2)这里的解析意思是把cmdline的细节设置信息给解析出来。譬如cmdline:console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3,则解析出的内容就是就是一个字符串数组,数组中依次存放了一个设置项目信息。

console=ttySAC2,115200 一个

root=/dev/mmcblk0p2 rw 一个

init=/linuxrc 一个

rootfstype=ext3 一个

(3)这里只是进行了解析,并没有去处理。也就是说只是把长字符串解析成了短字符串,最多和内核里控制这个相应功能的变量挂钩了,但是并没有去执行。执行的代码在各自模块初始化的代码部分。

2.16.8.3、杂碎

(1)trap_init 设置异常向量表

(2)mm_init 内存管理模块初始化

(3)sched_init 内核调度系统初始化

(4)early_irq_init&init_IRQ 中断初始化

(5)console_init 控制台初始化

总结:start_kernel函数中调用了很多的xx_init函数,全都是内核工作需要的模块的初始化函数。这些初始化之后内核就具有了一个基本的可以工作的条件了。

如果把内核比喻成一个复杂机器,那么start_kernel函数就是把这个机器的众多零部件组装在一起形成这个机器,让他具有可以工作的基本条件。

2.16.8.4、rest_init

(1)这个函数之前内核的基本组装已经完成。

(2)剩下的一些工作就比较重要了,放在了一个单独的函数中,叫rest_init。

总结:start_kernel函数做的主要工作:打印了一些信息、内核工作需要的模块的初始化被依次调用(譬如内存管理、调度系统、异常处理···)、我们需要重点了解的就是setup_arch中做的2件事情:机器码架构的查找并且执行架构相关的硬件的初始化、uboot给内核的传参cmdline。

内核启动的C语言阶段6

2.16.9.1、操作系统去哪了

(1)rest_init中调用kernel_thread函数启动了2个内核线程,分别是:kernel_init和kthreadd

(2)调用schedule函数开启了内核的调度系统,从此linux系统开始转起来了。

(3)rest_init最终调用cpu_idle函数结束了整个内核的启动。也就是说linux内核最终结束了一个函数cpu_idle。这个函数里面肯定是死循环。

(4)简单来说,linux内核最终的状态是:有事干的时候去执行有意义的工作(执行各个进程任务),实在没活干的时候就去死循环(实际上死循环也可以看成是一个任务)。

(5)之前已经启动了内核调度系统,调度系统会负责考评系统中所有的进程,这些进程里面只有有哪个需要被运行,调度系统就会终止cpu_idle死循环进程(空闲进程)转而去执行有意义的干活的进程。这样操作系统就转起来了。

2.16.9.2、什么是内核线程

(1)进程和线程。简单来理解,一个运行的程序就是一个进程。所以进程就是任务、进程就是一个独立的程序。独立的意思就是这个程序和别的程序是分开的,这个程序可以被内核单独调用执行或者暂停。

(2)在linux系统中,线程和进程非常相似,几乎可以看成是一样的。实际上我们当前讲课用到的进程和线程的概念就是一样的。

(3)进程/线程就是一个独立的程序。应用层运行一个程序就构成一个用户进程/线程,那么内核中运行一个函数(函数其实就是一个程序)就构成了一个内核进程/线程。

(4)所以我们kernel_thead函数运行一个函数,其实就是把这个函数变成了一个内核线程去运行起来,然后他可以被内核调度系统去调度。说白了就是去调度器注册了一下,以后人家调度的时候会考虑你。

2.16.9.3、进程0、进程1、进程2

(1)截至目前为止,我们一共涉及到3个内核进程/线程。

(2)操作系统是用一个数字来表示/记录一个进程/线程的,这个数字就被称为这个进程的进程号。这个号码是从0开始分配的。因此这里涉及到的三个进程分别是linux系统的进程0、进程1、进程2.

(3)在linux命令行下,使用ps命令可以查看当前linux系统中运行的进程情况。

(4)我们在ubuntu下ps -aux可以看到当前系统运行的所有进程,可以看出进程号是从1开始的。为什么不从0开始,因为进程0不是一个用户进程,而属于内核进程。

(5)三个进程

进程0:进程0其实就是刚才讲过的idle进程,叫空闲进程,也就是死循环。

进程1:kernel_init函数就是进程1,这个进程被称为init进程。

进程2:kthreadd函数就是进程2,这个进程是linux内核的守护进程。这个进程是用来保证linux内核自己本身能正常工作的。

总结1:本节课的重点在于理解linux内核启动后达到的一个稳定状态。注意去对比内核启动后的稳定状态和uboot启动后的稳定状态的区别。

总结2:本节课的第二个重点就是初步理解进程/线程的概念。

总结3:你得明白每个进程有个进程号,进程号从0开始依次分配的。明白进程0是idle进程(idle进程是干嘛的);进程2是ktheadd进程(基本明白干嘛的就行)

总结4:分析到此,发现后续的料都在进程1.所以后面课程会重点从进程1出发,分析之后发生的事情。

init进程详解1

2.16.10.1、init进程完成了从内核态向用户态的转变

(1)一个进程2种状态。init进程刚开始运行的时候是内核态,它属于一个内核线程,然后他自己运行了一个用户态下面的程序后把自己强行转成了用户态。因为init进程自身完成了从内核态到用户态的过度,因此后续的其他进程都可以工作在用户态下面了。

(2)内核态下做了什么?重点就做了一件事情,就是挂载根文件系统并试图找到用户态下的那个init程序。init进程要把自己转成用户态就必须运行一个用户态的应用程序(这个应用程序名字一般也叫init),要运行这个应用程序就必须得找到这个应用程序,要找到它就必须得挂载根文件系统,因为所有的应用程序都在文件系统中。

内核源代码中的所有函数都是内核态下面的,执行任何一个都不能脱离内核态。应用程序必须不属于内核源代码,这样才能保证自己是用户态。也就是说我们这里执行的这个init程序和内核不在一起,他是另外提供的。提供这个init程序的那个人就是根文件系统。

(3)用户态下做了什么?init进程大部分有意义的工作都是在用户态下进行的。init进程对我们操作系统的意义在于:其他所有的用户进程都直接或者间接派生自init进程。

(4)如何从内核态跳跃到用户态?还能回来不?

init进程在内核态下面时,通过一个函数kernel_execve来执行一个用户空间编译连接的应用程序就跳跃到用户态了。注意这个跳跃过程中进程号是没有改变的,所以一直是进程1.这个跳跃过程是单向的,也就是说一旦执行了init程序转到了用户态下整个操作系统就算真正的运转起来了,以后只能在用户态下工作了,用户态下想要进入内核态只有走API这一条路了。

2.16.10.2、init进程构建了用户交互界面

(1)init进程是其他用户进程的老祖宗。linux系统中一个进程的创建是通过其父进程创建出来的。根据这个理论只要有一个父进程就能生出一堆子孙进程了。

(2)init启动了login进程、命令行进程、shell进程

(3)shell进程启动了其他用户进程。命令行和shell一旦工作了,用户就可以在命令行下通过./xx的方式来执行其他应用程序,每一个应用程序的运行就是一个进程。

总结:本节的主要目的是让大家认识到init进程如何一步步发展成为我们平时看到的那种操作系统的样子。

init进程详解2

2.16.11.1、打开控制台

(1)linux系统中每个进程都有自己的一个文件描述符表,表中存储的是本进程打开的文件。

(2)linux系统中有一个设计理念:一切届是文件。所以设备也是以文件的方式来访问的。我们要访问一个设备,就要去打开这个设备对应的文件描述符。譬如/dev/fb0这个设备文件就代表LCD显示器设备,/dev/buzzer代表蜂鸣器设备,/dev/console代表控制台设备。

(3)这里我们打开了/dev/console文件,并且复制了2次文件描述符,一共得到了3个文件描述符。这三个文件描述符分别是0、1、2.这三个文件描述符就是所谓的:标准输入、标准输出、标准错误。

(4)进程1打开了三个标准输出输出错误文件,因此后续的进程1衍生出来的所有的进程默认都具有这3个三件描述符。

2.16.11.2、挂载根文件系统

(1)prepare_namespace函数中挂载根文件系统

(2)根文件系统在哪里?根文件系统的文件系统类型是什么? uboot通过传参来告诉内核这些信息。

uboot传参中的root=/dev/mmcblk0p2 rw 这一句就是告诉内核根文件系统在哪里

uboot传参中的rootfstype=ext3这一句就是告诉内核rootfs的类型。

(3)如果内核挂载根文件系统成功,则会打印出:VFS: Mounted root (ext3 filesystem) on device 179:2.

如果挂载根文件系统失败,则会打印:No filesystem could mount root, tried: yaffs2

(4)如果内核启动时挂载rootfs失败,则后面肯定没法执行了,肯定会死。内核中设置了启动失败休息5s自动重启的机制,因此这里会自动重启,所以有时候大家会看到反复重启的情况。

(5)如果挂载rootfs失败,可能的原因有:

最常见的错误就是uboot的bootargs设置不对。

rootfs烧录失败(fastboot烧录不容易出错,以前是手工烧录很容易出错)

rootfs本身制作失败的。(尤其是自己做的rootfs,或者别人给的第一次用)

2.16.11.3、执行用户态下的进程1程序

(1)上面一旦挂载rootfs成功,则进入rootfs中寻找应用程序的init程序,这个程序就是用户空间的进程1.找到后用run_init_process去执行他

(2)我们如果确定init程序是谁?方法是:

先从uboot传参cmdline中看有没有指定,如果有指定先执行cmdline中指定的程序。cmdline中的init=/linuxrc这个就是指定rootfs中哪个程序是init程序。这里的指定方式就表示我们rootfs的根目录下面有个名字叫linuxrc的程序,这个程序就是init程序。

如果uboot传参cmdline中没有init=xx或者cmdline中指定的这个xx执行失败,还有备用方案。第一备用:/sbin/init,第二备用:/etc/init,第三备用:/bin/init,第四备用:/bin/sh。

如果以上都不成功,则认命了,死了。

cmdline常用参数

2.16.12.1、格式简介

(1)格式就是由很多个项目用空格隔开依次排列,每个项目中都是项目名=项目值

(2)整个cmdline会被内核启动时解析,解析成一个一个的项目名=项目值的字符串。这些字符串又会被再次解析从而影响启动过程。

2.16.12.2、root=

(1)这个是用来指定根文件系统在哪里的

(2)一般格式是root=/dev/xxx(一般如果是nandflash上则/dev/mtdblock2,如果是inand/sd的话则/dev/mmcblk0p2)

(3)如果是nfs的rootfs,则root=/dev/nfs。

2.16.12.3、rootfstype=

(1)根文件系统的文件系统类型,一般是jffs2、yaffs2、ext3、ubi

2.16.12.4、console=

(1)控制台信息声明,譬如console=/dev/ttySAC0,115200表示控制台使用串口0,波特率是115200.

(2)正常情况下,内核启动的时候会根据console=这个项目来初始化硬件,并且重定位console到具体的一个串口上,所以这里的传参会影响后续是否能从串口终端上接收到内核的信息。

2.16.12.5、mem=

(1)mem=用来告诉内核当前系统的内存有多少

2.16.12.6、init=

(1)init=用来指定进程1的程序pathname,一般都是init=/linuxrc

2.16.12.7、常见cmdline介绍

(1)console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3

第一种这种方式对应rootfs在SD/iNand/Nand/Nor等物理存储器上。这种对应产品正式出货工作时的情况。

(2)root=/dev/nfs nfsroot=192.168.1.141:/root/s3c2440/build_rootfs/aston_rootfs ip=192.168.1.10:192.168.1.141:192.168.1.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC0,115200

第二种这种方式对应rootfs在nfs上,这种对应我们实验室开发产品做调试的时候。

内核中架构相关代码简介

2.16.13.1、内核代码基本分为3块

(1)arch。 本目录下全是cpu架构有关的代码

(2)drivers 本目录下全是硬件的驱动

(3)其他 相同点是这些代码都和硬件无关,因此系统移植和驱动开发的时候这些代码几乎都是不用关注的。

2.16.13.2、架构相关的常用目录名及含义

(1)mach。(mach就是machine architecture)。arch/arm目录下的一个mach-xx目录就表示一类machine的定义,这类machine的共同点是都用xx这个cpu来做主芯片。(譬如mach-s5pv210这个文件夹里面都是s5pv210这个主芯片的开发板machine);mach-xx目录里面的一个mach-yy.c文件中定义了一个开发板(一个开发板对应一个机器码),这个是可以被扩展的。

(2)plat(plat是platform的缩写,含义是平台)plat在这里可以理解为SoC,也就是说这个plat目录下都是SoC里面的一些硬件(内部外设)相关的一些代码。

在内核中把SoC内部外设相关的硬件操作代码就叫做平台设备驱动。

(3)include。这个include目录中的所有代码都是架构相关的头文件。(linux内核通用的头文件在内核源码树根目录下的include目录里)

2.16.13.3、补充

(1)内核中的文件结构很庞大、很凌乱(不同版本的内核可能一个文件存放的位置是不同的),会给我们初学者带来一定的困扰。

(2)头文件目录include有好几个,譬如:

kernel/include 内核通用头文件

kernel/arch/arm/include 架构相关的头文件

kernel/arch/arm/include/asm

kernel\arch\arm\include\asm\mach

kernel\arch\arm\mach-s5pv210\include\mach

kernel\arch\arm\plat-s5p\include\plat

(3)内核中包含头文件时有一些格式

#include kernel/include/linux/kernel.h

#include kernel/arch/arm/include/asm/mach/arch.h

#include kernel\arch\arm\include\asm/setup.h

#include kernel\arch\arm\plat-s5p\include\plat/s5pv210.h

(4)有些同名的头文件是有包含关系的,有时候我们需要包含某个头文件时可能并不是直接包含他,而是包含一个包含了他的头文件。

编译 /xbw_data/arm_linux_learn/android-kernel-samsung-dev/:

1. 修改Makefile :

定义

ARCH = arm

CROSS_COMPILE   = /xbw_data/arm_linux_learn/arm-2009q3/bin/arm-none-linux-gnueabi-

(cross_compile版本不能太高,否则会报错:include/linux/compiler-gcc.h:91:1: fatal error: linux/compiler-gcc9.h: 没有那个文件或目录

91 | #include gcc_header(__GNUC__))

2. make smdkv210_android_defconfig(内核配置选项根据开发板选择,查看arch/arm/configs/目录下)

3. make menuconfig (什么都不改 直接Exit)

4. make

报错:  

Can't use 'defined(@array)' (Maybe you should just omit the defined()?) at kernel/timeconst.pl line 373.


解决:

    vi kernel/timeconst.pl     将373行的         if (!defined(@val)) {     改为         if (!(@val)) {

编译成功。


原因:

查了一下更新,发现其中有一项是perl版本升级到了v5.22.1,然后查了perl官方文档,发现官网因为一个bug,该版本将defined(@array)去掉了。可以直接使用数组判断非空。

编译完成后,镜像为:arch/arm/boot/zImage

sudo cp arch/arm/boot/zImage /mnt/hgfs/share/ -f

复制的目标目录为tftp服务器配置的目录

修改tftpd的配置参数

sudo vim /etc/default/tftpd-hpa #修改tftpd的目录与IP地址,保存并退出VIM编辑器

配置文件内容如下:

# /etc/default/tftpd-hpa

TFTP_USERNAME="tftp"

#该参数为tftp服务器目录,需要传输的文件放在这里

TFTP_DIRECTORY="/mnt/hgfs/share"

TFTP_ADDRESS=":69"

TFTP_OPTIONS="--secure --create"

~

烧录

使用UBOOT命令

传输镜像

tftp 30008000 zImage

启动镜像

bootm 30008000

tftp 30008000 zImage;bootm 30008000

2.17.2.初步移植以看到启动信息

分析问题

(1)根据运行结果,分析发现:linux内核的自解压代码都没有运行(因为没有看到:Uncompressing Linux... done, booting the kernel.)

(2)说明zImage根本没有被解压成功,内核代码根本就没有被运行,当然没有输出信息了。所以问题出在解压相关的部分。

(3)问题出在内核配置的解压后代码放置的内存地址处。

(4)内核配置的解压地址应该等于连接地址,否则自解压之后内核无法运行。现在问题变成:第一,内核的连接地址等于多少?第二,内核中配置的解压地址是多少?

(5)这里面还有个问题:内核的连接地址是一个虚拟地址,而自解压代码解压内核时需要物理地址,因此上面说的等于,其实是连接地址对应的物理地址等于自解压地址。

(6)连接地址和他对应的物理地址在head.S中可以查到,分别是0xC0008000和0x30008000。那么自解压代码配置的解压地址应该是30008000.

(7)自解压代码对应的自解压地址在mach/Makefile.boot文件中。在其中修改,加入两行:

# override for SMDKV210

zreladdr-$(CONFIG_MACH_SMDKV210) := 0x30008000

params_phys-$(CONFIG_MACH_SMDKV210) := 0x30000100

(8)同步代码,并且编译,得到的zImage复制到/mnt/hgfs/share/,然后重新下载运行查看结果。

(9)结果就是:还是没运行,但是有效果。自解压代码解压打印信息已经出来了。但是内核还没运行

(1)/xbw_data/arm_linux_learn/android-kernel-samsung-dev/arch/arm/mach-s5pv210/include/mach/memory.h定义的物理地址不对,从20000000改到30000000即可

#define PHYS_OFFSET     UL(0x30000000)

2.17.3.内核中机器码的确定

MACHINE_START宏

/xbw_data/arm_linux_learn/android-kernel-samsung-dev/arch/arm/mach-s5pv210/mach-smdkv210.c 105行

(1)这个宏用来定义一个机器码的数据结构的。这个宏的使用其实是用来定义一个结构体类型为machine_desc类型的结构体变量,名为__mach_desc_SMDKV210。这个结构体变量会被定义到一个特定段.arch.info.init,因此这个结构体变量将来会被链接器链接到这个.arch.info.init段中。

static const struct machine_desc __mach_desc_SMDKV210 \

__used \

__attribute__((__section__(".arch.info.init"))) = { \

.nr = MACH_TYPE_SMDKV210, \

.name = "SMDKV210",

.phys_io = S3C_PA_UART & 0xfff00000,

.io_pg_offst = (((u32)S3C_VA_UART) >> 18) & 0xfffc,

.boot_params = S5P_PA_SDRAM + 0x100,

.init_irq = s5pv210_init_irq,

.map_io = smdkv210_map_io,

.init_machine = smdkv210_machine_init,

.timer = &s5p_systimer,

};

经过分析,发现一个mach-xxx.c文件中定义了一个机器码的开发板的machine_desc结构体变量,这个结构体变量放到.arch.info.init段中后,那么就表示当前内核可以支持这个机器码的开发板。

(3)落实到当前开发板和当前内核中来分析,当前我们移植的目标开发板使用S5PV210的CPU,开发板名字叫X210.我们在三星官方版本的内核中是找不到mach-x210.c的,所以我们又不想从零开始去移植,因此我们的思路是在三星移植的mach-s5pv210目录下找一个mach-xx.c,这个开发板和我们的X210开发板最为接近,然后以此为基础来移植。

(4)经过查看,发现mach-s5pc110.c和mach-s5pv210.c和我们的X210开发板最为接近。我们一般确定的一个原则是:看我们的开发板和三星官方的哪个开发板最为相似。我们的X210开发板抄的是三星的SMDKV210,因此要找这个对应的那个文件。

(5)结合mach-s5pv210目录下的Makefile来分析,得知.config中定义了CONFIG_MACH_SMDKV210后,实际绑定的是mach-smdkc110.c这个文件。所以实际上mach-smdkv210.c这个文件根本没用到。启示就是不要光看名字。

硬件驱动的加载和初始化函数执行

(1).init_machine = smdkc110_machine_init,

(2)这个元素定义了一个机器硬件初始化函数,这个函数非常重要,这个函数中绑定了我们这个开发板linux内核启动过程中会初始化的各种硬件的信息。

解决内核启动中的错误

(2)内核启动中的错误信息有一些特征:

Unable to handle kernel NULL pointer dereference at virtual address 00000060

Internal error: Oops: 5 [#1] PREEMPT

PC is at dev_driver_string+0xc/0x44

LR is at max8698_pmic_probe+0x150/0x32c

(3)从以上错误信息中的PC和LR的值可以看出,程序是执行到dev_driver_string或者max8698_pmic_probe(这两个是函数或者汇编中的标号)符号部分的时候出错了。我们就从这两个符号出发去寻找、思考可能出错的地方然后试图去解决。

2.17.5.iNand的问题和安排

2.17.5.1、错误分析

(1)得到的内核错误信息:Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)。从错误信息字面意思来分析,就是内核试图挂载根文件系统时失败,失败的原因是unknown-block(不能识别的块设备)

(2)backstrace分析,可以得知错误信息的来源,再结合之前的内核启动流程分析,就更加确定了出错的地方。

(3)下一个问题:分析这个错误出现的原因。unknown-block(0,0)。在kernel启动时uboot会传给内核一个cmdline,其中用root=xx来指定了rootfs在哪个设备上,内核就会到相应的地方去挂载rootfs。譬如我们传参中:root=/dev/mmcblk0p2,这里的/dev/mmcblk0p2就是rootfs的设备地址,这个设备文件编号的含义就是mmc设备0的第2个分区(设备0就是在SD0通道上的设备,也就是iNand),这里的问题就是没找到mmc设备0的第2分区。

(4)下一步问题:为什么没找到mmc设备0的第2分区。一定是因为kernel启动过程中加载mmc驱动的时候有问题,驱动没有发现mmc设备0.问题定位在MMC相关的驱动方面。

(5)对比九鼎版本的内核启动信息,即可发现我们的内核启动并没有找到MMC设备(内置的iNand和外置的SD卡都没找到),没找到肯定是驱动的问题,这就要去移植MMC驱动了。

问题阐述

(1)SD/iNand本身都是由一个一个的扇区组成的,回忆裸机中讲到的210的启动时,BL1在SD卡的1扇区开始往后存放,SD卡的0扇区是不用的。SD卡的0扇区是用来放置MBR的。

(2)MBR就是用来描述块设备的分区信息的,事先定义了一个通用的数据结构来描述块设备的分区,我们只要按照这个标准将分区信息写入MBR中即可对该设备完成分区。MBR默认就是在块设备的第0个扇区上存放的。

(3)我们内核中读到iNand分4个分区,我们哪里分区的?uboot中有一个命令fdisk -c 0时就对iNand进行了分区。uboot的fdisk命令内部已经写死了iNand的分区表,到内核中时内核直接读取MBR就知道了分区。所以在uboot和内核之间iNand设备的分区信息是靠iNand自己传递的,所以uboot不用给内核传参时传递分区表信息。

(4)如果开发板用的是nandFlash的话,分区表一般是在内核中自己用代码构建的。所以nand版本的内核移植的时候一般都需要去移植更改nand分区表。

2.17.6.网卡驱动的移植和添加实验

2.17.6.1、移植标准

(1)网卡驱动移植ok时,启动信息为:

[ 1.452008] dm9000 Ethernet Driver, V1.31

[ 1.455870] eth0: dm9000c at e08f4300,e08f8304 IRQ 42 MAC: 00:09:c0:ff:ec:48 (platform data)

(2)当前内核中网卡驱动尚未移植,因此内核启动时有错误的打印信息:

[ 1.130308] dm9000 Ethernet Driver, V1.31

[ 1.133113] ERROR : resetting

[ 1.135700] dm9000 dm9000.0: read wrong id 0x2b2a2928

[ 1.140915] dm9000 dm9000.0: read wrong id 0x2b2a2928

[ 1.145941] dm9000 dm9000.0: read wrong id 0x2b2a2928

[ 1.150963] dm9000 dm9000.0: read wrong id 0x2b2a2928

[ 1.155992] dm9000 dm9000.0: read wrong id 0x2b2a2928

[ 1.161018] dm9000 dm9000.0: read wrong id 0x2b2a2928

[ 1.166041] dm9000 dm9000.0: read wrong id 0x2b2a2928

[ 1.171070] dm9000 dm9000.0: read wrong id 0x2b2a2928

[ 1.176092] dm9000 dm9000.0: wrong id: 0x2b2a2928

[ 1.180774] dm9000 dm9000.0: not found (-19).

(3)移植的目标就是让我们的版本的内核可以打印出正确情况下的启动信息,那我们就相信内核启动后网卡是可以工作的。

2.17.6.2、make menuconfig中添加DM9000支持

(1)menuconfig中选择Y

(2)其实这一步本来就是Y,所以在我们这里是不用管的。但是你自己遇到的一个内核可能默认不是Y,因此要设置。

2.17.6.3、mach-smdkc110.c中逻辑分析

(1)mach-smdkc110.c中的smdkc110_machine_init是整个开发板的所有硬件的初始化函数,在这里加载了的硬件将来启动时就会被初始化,在这里没有的将来启动时就不管。

(2)smdkc110_devices和smdkc110_dm9000_set()这两个地方是和DM9000有关的,要分别去做移植。

(3)smdkc110_dm9000_set这个函数就是DM9000相关的SROM bank的寄存器设置,相当于uboot中dm9000移植时的dm9000_pre_init函数。只是读写寄存器的函数名称不同了。

2.17.6.4、修改相应的配置参数

(1)DM9000相关的数据配置在arch/arm/plat-s5p/devs.c中更改

(2)在arch/arm/mach-s5pv210/include/mach/map.h中定义了DM9000的IO基地址,和DM9000接在哪个bank有关。

(3)还有+2改成+4,IRQ_EINT9改成10即可。

2.17.6.5、代码实践

(1)同步代码、编译生成zImage

(2)下载启动后看启动信息。

内核启动第一阶段的调试方法

2.17.7.1、问题点描述

(1)内核启动在head.S中首先进行了三个校验(CPU id的校验、机器码的校验、tag的校验),然后创建页表,然后做了一些不太会出错的事情,然后b start_kernel。基本上能运行到start_kernel内核移植就不太会出问题了。

(2)有时候移植的内核启动后的现象是:根本没有启动信息出来。这时候有可能是内核启动运行了但是运行出错了没启动起来所以没有打印信息;也有可能是内核根本没得以运行。都有可能但是没法确定。我们希望能有一种调试手段来确定问题所在。

2.17.7.2、调试方法和原理

(1)调试方法就是在内核启动的第一阶段添加汇编操作led点亮/熄灭的方法来标明代码运行的轨迹。

(2)我们找之前裸机中汇编操作led点亮/熄灭的代码过来,复制粘贴到head.S中合适位置。然后内核启动后根据led的表现来标明代码有无运行。

2.17.7.3、动手测试

(1)整理好led操作的代码段(寄存器r0 r1 r2 r5 r8都不要使用,r0 r1 r2是uboot用来给kernel传参的寄存器,r5 r8在其他代码中也有使用),在head.S中合适的地方添加led这个函数,然后在head.S的内核起始运行阶段添加调用led函数,然后重新编译内核,运行内核看这段代码有无被运行。

(2)如果被运行了,证明在这个调用led的步骤之前的部分都是没问题的,那么如果有错肯定错误在后边;如果没有被运行则证明错误在之前,那么就要去之前的部分debug。

根文件系统概述

2.18.1.1、为什么需要根文件系统

(1)init进程的应用程序在根文件系统上

(2)根文件系统提供了根目录/

(3)内核启动后的应用层配置(etc目录)在根文件系统上。几乎可以认为:发行版=内核+rootfs

(4)shell命令程序在根文件系统上。譬如ls、cd等命令

总结:一套linux体系,只有内核本身是不能工作的,必须要rootfs(上的etc目录下的配置文件、/bin /sbin等目录下的shell命令,还有/lib目录下的库文件等···)相配合才能工作。

根文件系统的实质是什么

(1)根文件系统是特殊用途的文件系统。

(2)根文件系统也必须属于某种文件系统格式。rootfstype=

(3)究竟文件系统是用来干嘛的。ZnFAT

首先,存储设备(块设备,像硬盘、flash等)是分块(扇区)的,物理上底层去访问存储设备时是按照块号(扇区号)来访问的。这就很麻烦。

其次,文件系统是一些代码,是一套软件,这套软件的功能就是对存储设备的扇区进行管理,将这些扇区的访问变成了对目录和文件名的访问。我们在上层按照特定的目录和文件名去访问一个文件时,文件系统会将这个目录+文件名转换成对扇区号的访问。

最后,不同的文件系统的差异就在于对这些扇区的管理策略和方法不同,譬如坏块管理、碎片管理。

根文件系统的形式

2.18.2.1、镜像文件形式

(1)使用专用工具软件制作的可供烧录的镜像文件

(2)镜像中包含了根文件系统中的所有文件

(3)烧录此镜像类似于对相应分区格式化。

(4)镜像文件系统具有一定的格式,格式是内化的,跟文件名后缀是无关的。

2.18.2.2、文件夹形式

(1)根文件系统其实就是一个包含特定内容的文件夹而已

(2)根文件系统可由任何一个空文件夹添加必要文件构成而成

(3)根文件系统的雏形就是在开发主机中构造的文件夹形式的

2.18.2.3、总结

(1)镜像文件形式的根文件系统主要目的是用来烧录到块设备上,设备上的内核启动后去挂载它。镜像文件形式的根文件系统是由文件夹形式的根文件系统使用专用的镜像制作工具制作而成的。

(2)最初在开发主机中随便mkdir创建了一个空文件夹,然后向其中添加一些必要的文件(包括etc目录下的运行时配置文件、/bin等目录下的可执行程序、/lib目录下的库文件等···)后就形成了一个文件夹形式的rootfs。然后这个文件夹形式的rootfs可以被kernel通过nfs方式来远程挂载使用,但是不能用来烧录块设备。我们为了将这个rootfs烧录到块设备中于是用一些专用的软件工具将其制作成可供烧录的一定格式的根文件系统镜像。

(3)文件夹形式的rootfs是没有格式的,制作成镜像后就有了一定的rootfs格式了,格式是由我们的镜像制作过程和制作工具来决定的。每一种格式的镜像制作工具的用法都不同。

自己制作ext3格式的根文件系统

2.18.3.1、mke2fs介绍

(1)mke2fs是一个应用程序,在ubuntu中默认是安装了的。这个应用程序就是用来制作ext2、ext3、ext4等格式的根文件系统的。

(2)一般用来制作各种不同格式的rootfs的应用程序的名字都很相似,类似于mkfs.xxx(譬如用来制作ext2格式的rootfs的工具叫mkfs.ext2、用来制作jffs2格式的rootfs的工具就叫mkfs.jffs2)

(3)ubuntu14.04中的mkfs.ext2等都是mke2fs的符号链接而已。

2.18.3.2、动手制作ext3格式的根文件系统

(1)创建rootfs.ext2文件并且将之挂载到一个目录下方便访问它

《参考资料:http://blog.csdn.net/zhengmeifu/article/details/24174513》

dd if=/dev/zero of=rootfs.ext2 bs=1024 count=2048

losetup /dev/loop1 rootfs.ext2

mke2fs -m 0 /dev/loop1 2048

mount -t ext2 /dev/loop1 ./rootfs/

(2)我们向镜像中写入一个普通文件linuxrc。这个文件就会成为我们制作的镜像中的/linuxrc。内核挂载了这个镜像后就会尝试去执行/linuxrc。然后执行时必然会失败。我们将来实验看到的现象就应该是:挂载成功,执行/linuxrc失败。

(3)将来真正去做有用的rootfs时,就要在这一步添加真正可以执行的linuxrc程序,然后还要添加别的/lib目录下的库文件,/etc目录下的配置文件等。

(4)卸载掉,然后镜像就做好了。

umount /dev/loop1

losetup -d /dev/loop1

2.18.3.3、烧录制作的rootfs.ext3

(1)烧录过程参考裸机中第三部分的刷机过程。注意bootargs传参设置

nfs方式启动自制简易文件夹形式的rootfs

2.18.4.1、什么是nfs

(1)nfs是一种网络通讯协议,由服务器和客户端构成。

(2)nfs的作用。利用nfs协议可以做出很多直接性应用,我们这里使用nfs主要是做rootfs挂载。开发板中运行kernel做nfs客户端,主机ubuntu中搭建nfs服务器。在主机ubuntu的nfs服务器中导出我们制作的文件夹形式的rootfs目录,则在客户端中就可以去挂载这个文件夹形式的rootfs进而去启动系统。

(3)搭建nfs服务器。

2.18.4.2、配置内核以支持nfs作为rootfs

(1)设置nfs启动方式的bootargs

(2)在menuconfig中配置支持nfs启动方式

1、配置网络部分,主要是使能CONFIG_IP_PNP以在2中能够看到Root file system on NFS选项
Networking support
    Networking options
        TCP/IP networking
                IP: kernel level autoconfiguration
                    [*] IP: DHCP support
                    [*] IP: BOOTP support
                    
2、配置开启nfs服务
File systems  --->    
    Network File Systems  --->
        <*> NFS client support
        [*] NFS client support for NFS version 3                                  [*] NFS client support for the NFSv3 ACL protocol extension
        [*] NFS client support for NFS version 4 (EXPERIMENTAL)
        [*] NFS client support for NFSv4.1 (DEVELOPER ONLY)
        [*] Root file system on NFS  
                    
3、在uboot中设置如下启动参数(IP根据实际使用更改)
setenv bootargs root=/dev/nfs nfsroot=192.168.0.102:/xbw_data/nfs_folder ip=192.168.0.101:192.168.0.102:192.168.0.1:255.255.255.0::eth0:off  init=/linuxrc console=ttySAC2,115200

2.18.4.3、总结

(1)nfs方式启动相当于开发板上的内核远程挂载到主机上的rootfs

(2)nfs方式启动不用制作rootfs镜像

(3)nfs方式不适合真正的产品,一般作为产品开发阶段调试使用

实操时,遇到了无法挂载的情况:参考https://www.cnblogs.com/-rzx-/p/12264901.html解决

Ubuntu20.04中禁用了v2版本的nfs协议

root@ubuntu:/xbw_data/abc# sudo cat /proc/fs/nfsd/versions

-2 +3 +4 +4.1 +4.2

开发板内核版本较低,默认是使用v2版本的NFS协议,因此修改bootargs,增加指定nfs版本 ,v3参数

x210 # print bootcmd=movi read kernel 30008000; movi read rootfs 30B00000 300000; bootm 30008000 30B00000 mtdpart=80000 400000 3000000 bootdelay=3 baudrate=115200 ethaddr=00:40:5c:26:0a:5b ipaddr=192.168.0.101 serverip=192.168.0.102 gatewayip=192.168.0.1 netmask=255.255.255.0 bootargs=root=/dev/nfs nfsroot=192.168.0.102:/xbw_data/nfs_folder,v3 ip=192.168.0.101:192.168.0.102:192.168.0.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC2,115200 Environment size: 437/16380 bytes x210 #

到底什么是linuxrc

2.18.5.1、/linuxrc是一个可执行的应用程序

(1)/linuxrc是应用层的,和内核源码一点关系都没有

(2)/linuxrc在开发板当前内核系统下是可执行的。因此在ARM SoC的linux系统下,这个应用程序就是用arm-linux-gcc编译链接的;如果是在PC机linux系统下,那么这个程序就是用gcc编译连接的。

(3)/linuxrc如果是静态编译连接的那么直接可以运行;如果是动态编译连接的那么我们还必须给他提供必要的库文件才能运行。但是因为我们/linuxrc这个程序是由内核直接调用执行的,因此用户没有机会去导出库文件的路径,因此实际上这个/linuxrc没法动态连接,一般都是静态连接的。

2.18.5.2、/linuxrc执行时引出用户界面

(1)操作系统启动后在一系列的自己运行配置之后,最终会给用户一个操作界面(也许是cmdline,也许是GUI),这个用户操作界面就是由/linuxrc带出来的。

(2)用户界面等很多事并不是在/linuxrc程序中负责的,用户界面有自己专门的应用程序,但是用户界面的应用程序是直接或者间接的被/linuxrc调用执行的。用户界面程序和其他的应用程序就是进程2、3、4·····,这就是我们说的进程1(init进程,也就是/linuxrc)是其他所有应用程序进程的祖宗进程。

2.18.5.3、/linuxrc负责系统启动后的配置

(1)就好像一个房子建好之后不能直接住,还要装修一样;操作系统启动起来后也不能直接用,要配置下。

(2)操作系统启动后的应用层的配置(一般叫运行时配置,英文简写etc)是为了让我们的操作系统用起来更方便,更适合我个人的爱好或者实用性。

2.18.5.4、/linuxrc在嵌入式linux中一般就是busybox

(1)busybox是一个C语言写出来的项目,里面包含了很多.c文件和.h文件。这个项目可以被配置编译成各个平台下面可以运行的应用程序。我们如果用arm-linux-gcc来编译busybox就会得到一个可以在我们开发板linux内核上运行的应用程序。

(2)busybox这个程序开发出来就是为了在嵌入式环境下构建rootfs使用的,也就是说他就是专门开发的init进程应用程序。

(3)busybox为当前系统提供了一整套的shell命令程序集。譬如vi、cd、mkdir、ls等。在桌面版的linux发行版(譬如ubuntu、redhat、centOS等)中vi、cd、ls等都是一个一个的单独的应用程序。但是在嵌入式linux中,为了省事我们把vi、cd等所有常用的shell命令集合到一起构成了一个shell命令包,起名叫busybox。

busybox的移植实战

2.19.2.1、busybox源码下载

(1)busybox是一个开源项目,所以源代码可以直接从网上下载。

(2)busybox的版本差异不大,版本新旧无所谓。

(3)下载busybox可以去linuxidc等镜像网站,也可以去www.busybox.net官方网站下载。

2.19.2.2、修改Makefile

(1) ARCH = arm

(2) CROSS_COMPILE = /usr/local/arm/arm-2009q3/bin//arm-none-linux-gnueabi-

2.19.2.3、make menuconfig进行配置

(1)参照网盘中章节目录下的《busybox menuconfig配置.txt》文件中的记录进行配置。

2.19.2.4、make 然后 make install

(1)make编译,如果有错误解决之

(2)make install执行的时候其实是在执行busybox顶层目录下的一个目标install。

(3)make install在所有的linux下的软件中作用都是安装软件。在传统的linux系统中安装软件时都是选择源代码方式安装的。我们下载要安装的软件源代码,然后配置、编译、安装。make install的目的就是将编译生成的可执行程序及其依赖的库文件、配置文件、头文件安装到当前系统中指定(一般都可以自己指定安装到哪个目录下,如果不指定一般都有个默认目录)的目录下

make install的目录,需要在make menuconfig里修改

Settings --->

│ │ [*] Use -static-libgcc │ │

│ │ --- Installation Options ("make install" behavior) │ │

│ │ What kind of applet links to install (as soft-links) ---> │ │

│ │ (/xbw_data/nfs_folder) Destination path for 'make install' //这里修改make install的目录

2.19.2.5、设置bootargs挂载添加了busybox移植的rootfs

(1)之前建立了一个空的文件夹然后自己touch linuxrc随便创建了一个不能用的/linuxrc然后去nfs挂载rootfs,实验结果是:挂载成功,执行/linuxrc失败。

(2)现在我们移植了busybox后/linuxrc就可以用了,然后再次去nfs挂载这个rootfs。预计看到的效果是:挂载成功,执行/linuxrc也能成功。

(3)注意uboot的bootargs设置成:setenv bootargs root=/dev/nfs nfsroot=192.168.1.141:/root/porting_x210/rootfs ip=192.168.1.10:192.168.1.141:192.168.1.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC2,115200

(4)实验结果:挂载成功,执行/linuxrc(也就是busybox)成功,但是因为找不到/etc/init.d/rcS和/dev/tty2等文件所以一直在打印错误提示信息,但是其实有进入命令行。

inittab详解

2.19.3.1、添加一个典型的inittab

(1)将我提供的典型的inittab文件复制到我们制作的rootfs的根目录下的/etc/目录下

(2)再次启动内核挂载这个rootfs看效果

(3)实验现象是成功启动并且挂载rootfs进入了控制台命令行。当前制作的最小rootfs成功了

2.19.3.2、inittab格式解析

(1)inittab的工作原理就是被/linuxrc(也就是busybox)执行时所调用起作用。

(2)inittab在/etc目录下,所以属于一个运行时配置文件,是文本格式的(内容是由一系列的遵照一个格式组织的字符组成的),实际工作的时候busybox会(按照一定的格式)解析这个inittab文本文件,然后根据解析的内容来决定要怎么工作。

(3)busybox究竟如何完成解析并且解析结果如何去工作(busybox中实现/etc/inittab的原理)并不是我们的目标,我们的重点是inittab的格式究竟怎样的?我们看到一个inittab后怎么去分析这个inittab对启动的影响。

(4)inittab的格式在busybox中定义的,网上可以搜索到详细的格式说明,具体去参考即可:

第一个:#开始的行是注释

第二个:冒号在里面是分隔符,分隔开各个部分。

第三个:inittab内容是以行为单位的,行与行之间没有关联,每行都是一个独立的配置项,每一个配置项表示一个具体的含义。

第四个:每一行的配置项都是由3个冒号分隔开的4个配置值共同确定的。这四个配置值就是id:runlevels:action:process。值得注意得是有些配置值可以空缺,空缺后冒号不能空缺,所以有时候会看到连续2个冒号。

第五个:每一行的配置项中4个配置值中最重要的是action和process,action是一个条件/状态,process是一个可被执行的程序的pathname。合起来的意思就是:当满足action的条件时就会执行process这个程序。

注意:理解inittab的关键就是明白“当满足action的条件时就会执行process这个程序。” 你去分析busybox的源代码就会发现,busybox最终会进入一个死循环,在这个死循环中去反复检查是否满足各个action的条件,如果某个action的条件满足就会去执行对应的process。

第六个:明白各个action什么意思

实操时,发现 Please press Enter to activate this console. 没有打印出来,同时仍然报错tty设备,现象与没有inittable一样

尝试在开发板中cat ect/inittab 提示权限不够

遂在虚拟机中将权限修改

xuebowen@ubuntu:~/桌面$ cd /xbw_data/nfs_folder/etc/

xuebowen@ubuntu:/xbw_data/nfs_folder/etc$ chmod 777 inittab

再次启动,现象正常

busybox源码分析1

2.19.4.1、源码目录梳理

2.19.4.2、SourceInsight工程建立

2.19.4.3、整个程序入口确认

(1)分析一个程序,不管多庞大还是小,最好的路线都是按照程序运行时的逻辑顺序来。所以找到一个程序的入口至关重要。

(2)学C语言的时候都知道程序的主函数main函数就是整个程序的入口。这种情况适应于操作系统下工作的应用程序的情况。

(3)在uboot和linux kernel这两个大的C语言的项目中,main函数都没有,都不是入口。在我们这种裸机程序中入口不是main函数,而是由连接脚本来指定的。

(4)busybox是linux启动起来后工作的一个应用程序,因此其中必然有main函数,而且main就是入口。

2.19.4.4、busybox中main函数全解析

(1)busybox入口就是main函数,其中有很多个main但是只有一个起作用了,其他的是没起作用的。真正的busybox工作时的入口是libbb/appletlib.c中的main函数

(2)busubox中有很多xxx_main函数,这些main函数每一个都是busybox支持的一个命令的真正入口。譬如ls_main函数就是busybox当作ls函数使用时的入口程序。

(3)ls或者cd等命令其实都是busybox一个程序,但是实际执行时的效果却是各自的效果。busybox是如何实现一个程序化身万千还能各自工作的?答案就是main转xxx_main。也就是说busybox每次执行时都是先执行其main,在main函数中识别(靠main函数的传参argv[0]来识别)我们真正要执行的函数(譬如ls)然后去调用相应的xxx_main(譬如ls_main)来具体实现这个命令。

busybox源码分析2

2.19.5.1、inittab解析与执行

(1)inittab的解析是在busybox/init/init.c/init_main函数中

(2)执行逻辑是:先通过parse_inittab函数解析/etc/inittab(解析的重点是将inittab中的各个action和process解析出来),然后后面先直接执行sysinit和wait和once(注意这里只执行一遍),然后在while(1)死循环中去执行respwan和askfirst。

2.19.5.2、pwd命令执行路径分析

(1)根据上节讲的,我们在busybox命令行下执行pwd命令时实际执行的是pwd_main这个函数。

2.19.5.3、busybox的体积优势原理

(1)busybox实际上就是把ls、cd、mkdir等很多个linux中常用的shell命令集成在一起了。集成在一起后有一个体积优势:就是busybox程序的大小比busybox中实现的那些命令的大小加起来要小很多。

(2)busybox体系变小的原因主要有2个:第一个是busybox本身提供的shell命令是阉割版的(busybox中的命令支持的参数选项比发行版中要少,譬如ls在发行版中可以有几十个-x,但是在busybox中只保留了几个常用的选项,不常用的都删除掉了);第二个是busybox中因为所有的命令的实现代码都在一个程序中实现,而各个命令中有很多代码函数都是通用的(譬如ls和cd、mkdir等命令都会需要去操作目录,因此在busybox中实现目录操作的函数就可以被这些命令共用),共用会降低重复代码出现的次数,从而减少总的代码量和体积。

(3)经过分析,busybox的体积优势是嵌入式系统本身的要求和特点造成的。

rcS文件介绍1

2.19.6.0、/etc/init.d/rcS文件是linux的运行时配置文件中最重要的一个,其他的一些配置都是由这个文件引出来的。这个文件可以很复杂也可以很简单,里面可以有很多的配置项。

2.19.6.1、PATH=xxx

(1)首先从shell脚本的语法角度分析,这一行定义了一个变量PATH,值等于后面的字符串

(2)后面用export导出了这个PATH,那么PATH就变成了一个环境变量。

(3)PATH这个环境变量是linux系统内部定义的一个环境变量,含义是操作系统去执行程序时会默认到PATH指定的各个目录下去寻找。如果找不到就认定这个程序不存在,如果找到了就去执行它。将一个可执行程序的目录导出到PATH,可以让我们不带路径来执行这个程序。

(4)rcS中为什么要先导出PATH?就是因为我们希望一旦进入命令行下时,PATH环境变量中就有默认的/bin /sbin /usr/bin /usr/sbin 这几个常见的可执行程序的路径,这样我们进入命令行后就可以ls、cd等直接使用了。

(5)为什么我们的rcS文件还没添加,系统启动就有了PATH中的值?原因在于busybox自己用代码硬编码为我们导出了一些环境变量,其中就有PATH。

2.19.6.2、runlevel=

(1)runlevel也是一个shell变量,并且被导出为环境变量。

(2)runlevel这个环境变量到底有什么用?

(3)runlevel=S表示将系统设置为单用户模式

2.19.6.3、umask=

(1)umask是linux的一个命令,作用是设置linux系统的umask值。

(2)umask值决定当前用户在创建文件时的默认权限。

2.19.6.4、mount -a

(1)mount命令是用来挂载文件系统的

(2)mount -a是挂载所有的应该被挂载的文件系统,在busybox中mount -a时busybox会去查找一个文件/etc/fstab文件,这个文件按照一定的格式列出来所有应该被挂载的文件系统(包括了虚拟文件系统)

rcS文件实战1

2.19.7.1、PATH&runlevel

(1)我们实战发现rcS文件明明存在但是却提示不存在,问题原因就是rcS文件在windows下创建的,行尾换行符为'\r\n',多了点东西。但是因为ubuntu中的vi对行尾做了优化,所以在ubuntu中是看不出来多了东西的。但是在securecrt下一看就发现每一行末尾多出来了一个^M。

(2)这个故事告诉我们:shell脚本文件如果格式不对,运行时可能会被提示文件不存在。

(3)扩展讲一个:有时候一个应用程序执行时也会提示文件不存在,问题可能是这个程序所调用的一个动态链接库找不到。

(4)测试结果:PATH本来在busybox中就已经用代码导出过了,所以rcS中再次导出没有任何明显的现象,因此看不出什么差别;runlevel实际执行结果一直是unknown,问题在于busybox并不支持runlevel这个特性。

2.19.7.2、umask测试

(1)umask是022的时候,默认touch创建一个文件的权限是644

(2)umask是044的时候,默认touch创建一个文件的权限是622

(3)umask是444的时候,默认touch创建一个文件的权限是222

总结:umask的规律就是:umask值和默认创建文件的权限值加起来是666.

2.19.7.3、mount测试

(1)挂载时全部出错:

mount: mounting proc on /proc failed: No such file or directory

mount: mounting sysfs on /sys failed: No such file or directory

mount: mounting tmpfs on /var failed: No such file or directory

mount: mounting tmpfs on /tmp failed: No such file or directory

mount: mounting tmpfs on /dev failed: No such file or directory

(2)原因是因为根文件系统中找不到挂载点。所谓挂载点就是我们要将目标文件系统(当然这里都是虚拟文件系统)挂载到当前文件系统中的某一个目录中,这个目录就是挂载点。

(3)解决方案就是自己在制作的rootfs根目录下创建这些挂载点目录即可。

(4)验证是否挂载成功,可以看挂载时输出信息;还可以启动后去看proc和sys文件夹,如果有文件出现则证明挂载成功了,如果没东西就证明失败了。rcS文件实战1

2.19.7.1、PATH&runlevel

(1)我们实战发现rcS文件明明存在但是却提示不存在,问题原因就是rcS文件在windows下创建的,行尾换行符为'\r\n',多了点东西。但是因为ubuntu中的vi对行尾做了优化,所以在ubuntu中是看不出来多了东西的。但是在securecrt下一看就发现每一行末尾多出来了一个^M。

(2)这个故事告诉我们:shell脚本文件如果格式不对,运行时可能会被提示文件不存在。

(3)扩展讲一个:有时候一个应用程序执行时也会提示文件不存在,问题可能是这个程序所调用的一个动态链接库找不到。

(4)测试结果:PATH本来在busybox中就已经用代码导出过了,所以rcS中再次导出没有任何明显的现象,因此看不出什么差别;runlevel实际执行结果一直是unknown,问题在于busybox并不支持runlevel这个特性。

2.19.7.2、umask测试

(1)umask是022的时候,默认touch创建一个文件的权限是644

(2)umask是044的时候,默认touch创建一个文件的权限是622

(3)umask是444的时候,默认touch创建一个文件的权限是222

总结:umask的规律就是:umask值和默认创建文件的权限值加起来是666.

2.19.7.3、mount测试

(1)挂载时全部出错:

mount: mounting proc on /proc failed: No such file or directory

mount: mounting sysfs on /sys failed: No such file or directory

mount: mounting tmpfs on /var failed: No such file or directory

mount: mounting tmpfs on /tmp failed: No such file or directory

mount: mounting tmpfs on /dev failed: No such file or directory

(2)原因是因为根文件系统中找不到挂载点。所谓挂载点就是我们要将目标文件系统(当然这里都是虚拟文件系统)挂载到当前文件系统中的某一个目录中,这个目录就是挂载点。

(3)解决方案就是自己在制作的rootfs根目录下创建这些挂载点目录即可。

(4)验证是否挂载成功,可以看挂载时输出信息;还可以启动后去看proc和sys文件夹,如果有文件出现则证明挂载成功了,如果没东西就证明失败了。

rcS文件介绍2

2.19.8.1、mdev

(1)mdev是udev的嵌入式简化版本,udev/mdev是用来配合linux驱动工作的一个应用层的软件,udev/mdev的工作就是配合linux驱动生成相应的/dev目录下的设备文件。

(2)因为这个问题涉及到驱动,因此详细讲解要等到驱动部分。这里我们只是通过一些直观的现象来初步理解udev/mdev的工作效果。

(3)在rcS文件中没有启动mdev的时候,/dev目录下启动后是空的;在rcS文件中添加上mdev有关的2行配置项后,再次启动系统后发现/dev目录下生成了很多的设备驱动文件。

(4)/dev目录下的设备驱动文件就是mdev生成的,这就是mdev的效果和意义。

2.19.8.2、hostname

(1)hostname是linux中的一个shell命令。命令(hostname xxx)执行后可以用来设置当前系统的主机名为xxx,直接hostname不加参数可以显示当前系统的主机名。

(2)/bin/hostname -F /etc/sysconfig/HOSTNAME -F来指定了一个主机名配置文件(这个文件一般文件名叫hostname或者HOSTNAME)

2.19.8.3、ifconfig

(1)有时候我们希望开机后进入命令行时ip地址就是一个指定的ip地址(譬如192.168.1.30),这时候就可以在rcS文件中ifconfig eth0 192.168.1.30

profile文件和用户登录理论

2.19.9.1、profile文件添加

(1)之前添加了/bin/hostname在/etc/sysconfig/HOSTNAME文件中定义了一个hostname(aston210),实际效果是:命令行下hostname命令查到的host名字确实是aston210。但是问题就是命令行的提示符是没有显示的。

(2)这个问题的解决就要靠profile文件。将提供的profile文件放入/etc/目录下即可。

(3)添加了之后的实验现象:命令行提示符前面显示:[@aston210 ]#

结论是:第一,profile文件起了作用,hostname显示出来了。第二,还有个问题,登录用户名没显示出来。原因就是我们直接进入了命令行而没有做登录。等我们添加了用户登录功能,并且成功登陆后这个问题就能解决。

(4)profile文件工作原理是:profile文件也是被busybox(init进程)自动调用的,所以是认名字的。

2.19.9.2、如何看到用户登录界面

(1)linux中有一个原则就是用一个小程序来完成一个功能。如果我们产品确实需要很复杂的综合型的功能,我们倾向于先使用很多个小程序完成其中的一个功能,然后再将这些小程序集成起来完成整个大功能的产品。

(2)这种集成很多个小程序来完成一个大的功能,有很多种技术实现。譬如shell脚本,还有一些别的技术,譬如linux启动中的inittab。

(3)因为我们之前intttab中有一个配置项 ::askfirst:-/bin/sh,这个配置项作用就是当系统启动后就去执行/bin/sh,执行这个就会出现命令行。因此我们这样的安排就会直接进入命令行而不会出现登录界面。

(4)我们要出现登录界面,就不能直接执行/bin/sh,而应该执行一个负责出现登录界面并且负责管理用户名和密码的一个程序,busybox中也集成了这个程序(就是/bin/login和/sbin/gettty),因此我们要在inittab中用/bin/login或者/sbin/getty去替代/bin/sh。

2.19.9.3、用户名和密码的设置

(1)用户名和密码的设置是和登录程序有关联的,但是/bin/login和/sbin/getty在用户名和密码的管理上是一样的。其实常见的所有的linux系统的用户名和密码的管理几乎都是一样的。

(2)密码一般都是用加密文字的,而不是用明文。意思就是系统中的密码肯定是在系统中的一个专门用来存密码的文件中存储的,用明文存密码有风险,因此linux系统都是用密文来存储密码的。关于密文密码的使用下节课实践时会详细讲。

2.19.10.用户登录实战

2.19.10.1、添加/bin/login到sysinit

(1)在inittab中修改,去掉/bin/sh,换上/bin/login,则系统启动后出现登录界面。可以输入用户名和密码。

(2)实验现象:成功出现用户登录界面,但是死活密码不对。

2.19.10.2、添加passwd和shadow文件

(1)为什么用户名和密码不对?因为我们根本没有为root用户设置密码。

(2)linux系统中用来描述用户名和密码的文件是passwd和shadow文件,这两个文件都在etc目录下。passwd文件中存储的是用户的密码设置,shadow文件中存储的是加密后的密码。

(3)我们直接复制ubuntu系统中的/etc/passwd和/etc/shadow文件到当前制作的rootfs目录下,然后再做修改即可。

(4)/etc/passwd和/etc/shadow修理好后,shadow中默认有一个加密的密码口令,这个口令和你拷贝的shadow本身有关,像我的ubuntu中root用户的密码就是root,因此复制过来后登陆时的密码还是root。

2.19.10.3、重置密码实践

(1)ubuntu刚装好的时候默认登录是用普通用户登录的,默认root用户是关闭的。普通用户的密码是在装系统的时候设置的,普通用户登陆后可以使用su passwd root给root用户设置密码,设置了密码后root用户才可以登录。

(2)其实这个原因就是root用户在/etc/shadow文件中加密口令是空白的。所以是不能登录的。

(3)busybox中因为没有普通用户,所以做法是:默认root用户如果加密口令是空的则默认无密码直接登录。等我们登陆了之后还是可以用passwd root给root用户设置密码。

(4)平时有时候我们忘记了自己的操作系统的密码,怎么办?有一种解决方法就是用其他系统(WindowsPE系统或者ubuntu的单用户模式等···)来引导启动,启动后挂载到我们的硬盘上,然后找到/etc/shadow文件,去掉密文密码后保存。然后再重启系统后密码就没了。

2.19.10.4、getty实战

(1)大家后面做项目会发现,inittab中最常见的用于登录的程序不是/bin/login,反而是/sbin/getty。

(2)这两个的差别不详,但是在busybox中这两个是一样的。这两个其实都是busybox的符号链接而已。因此不用严格区分这两个

(3)我们可以在inittab中用getty替换login程序来实现同样的效果。

动态链接库的拷贝

2.19.11.1、静态编译链接helloworld程序并执行

(1)任务:自己写一个helloworld程序,然后交叉编译连接,然后丢到开发板根文件系统中,开机后去运行。

(2)C程序如果使用gcc来编译则可以在主机ubuntu中运行,但是不能在开发板运行;要在开发板运行需要用arm-linux-gcc来交叉编译,但是这时候就不能在主机ubuntu中运行了。我们可以用file xx命令来查看一个elf可执行程序是哪个架构的。

(3)静态链接:arm-linux-gcc hello.c -o hello_satic -static

(4)实验结果:静态编译连接后生成的hello_satic已经可以成功运行。

2.19.11.2、动态编译连接helloworld程序并执行

(1)动态链接:arm-linux-gcc hello.c -o hello_dynamic

(2)实验结果:-sh: ./hello_dynamic: not found运行时提示找不到程序。

(3)错误分析:动态连接的hello程序中调用到了printf函数,而printf函数在动态连接时要在运行时环境(开发板的rootfs)中去寻找对应的库文件(开发板rootfs中部署的动态链接库中包含了printf函数的那个库文件)。如果找到了则printf函数就会被成功解析,然后hello_dynamic程序就会被执行;如果找不到则程序就不能被执行,命令行会提示错误信息-sh: ./hello_dynamic: not found

(4)解决方案:将arm-linux-gcc的动态链接库文件复制到开发板rootfs的/lib目录下即可解决。

2.19.11.3、找到并复制动态链接库文件到rootfs中

(1)我们用的arm-2009q3这个交叉编译工具链的动态链接库在/usr/local/arm/arm-2009q3/arm-none-linux-gnueabi/libc/lib目录下。其他的一些交叉编译工具链中动态链接库的目录不一定在这里,要去找一下。找的方法就是find

(2)复制动态链接库到roots/lib目录下。复制时要注意参数用-rdf,主要目的就是符号链接复制过来还是符号链接。

复制命令:cp lib/*so* /root/porting_x210/rootfs/rootfs/lib/ -rdf

(3)现在再去测试./hello_dynamic看看是否可以运行,实验结果是可以运行。

2.19.11.4、使用strip工具去掉库中符号信息

动态链接库so文件中包含了调试符号信息,这些符号信息在运行时是没用的(调试时用的),这些符号会占用一定空间。在传统的嵌入式系统中flash空间是有限的,为了节省空间常常把这些符号信息去掉。这样节省空间并且不影响运行。

去掉符号命令:arm-linux-strip *so*

实际操作后发现库文件由3.8M变成了3.0M,节省了0.8M的空间。

2.19.12.开机自启动与主流rcS格式介绍

2.19.12.1、修改rcS实现开机自启动

(1)开机自启动指的是让一些应用程序能够开机后自动执行

(2)开机自启动的实现原理就是在开机会自动执行的脚本rcS中添加上执行某个程序的语句代码即可

2.19.12.2、前台运行与后台运行

(1)程序运行时占用了当前的控制台,因此这个程序不结束我们都无法使用控制台,这就叫前台运行。默认执行程序就是前台运行的。

(2)后台运行就是让这个程序运行,并且同时让出控制台。这时候运行的程序还能照常运行而且还能够不影响当前控制台的使用。

(3)让一个程序后台运行的方法就是 ./xxx &

2.19.12.3、开机装载驱动等其他开机自动执行

2.19.12.4、实际开发中rootfs的rcS是怎样的

(1)我们以X210开发板九鼎科技做的rootfs中rcS部分来分析

(2)分析inittab发现:sysinit执行rcS,shutdown时执行rcK。

(3)分析/etc/init.d/rcS和rcK文件发现,rcS和rcK都是去遍历执行/etc/init.d/目录下的S开头的脚本文件,区别是rcS传参是start,rcK传参是stop。

(4)由此可以分析出来,正式产品中的rcS和rcK都是一个引入,而不是真正干活的。真正干活的配置脚本是/etc/init.d/S??*。这些文件中肯定有一个判断参数是start还是stop,然后start时去做一些初始化,stop时做一些清理工作。

制作ext2格式的镜像并烧录启动

2.19.13.1、确定文件夹格式的rootfs可用

(1)设置bootargs为nfs启动方式,然后从主机ubuntu中做好的文件夹格式的rootfs去启动,然后看启动效果,作为将来的参照物。

2.19.13.2、动手制作ext2格式的镜像

(1)

dd if=/dev/zero of=rootfs.ext2 bs=1024 count=10240

losetup /dev/loop1 rootfs.ext2

mke2fs -m 0 /dev/loop1 10240

mount -t ext2 /dev/loop1 ./ext2_rootfs/

(2)向./rootfs中复制内容,用cp ../rootfs/* ./ -rf

(3)umount /dev/loop1

losetup -d /dev/loop1

(4)完成后得到的rootfs.ext2就是我们做好的rootfs镜像。拿去烧录即可。

2.19.13.3、烧录镜像并设置合适的bootargs

(1)使用fastboot烧录制作好的rootfs.ext2到开发板inand中

fastboot flash system rootfs.ext2

烧录完成后重启系统

(2)设置bootargs为:set bootargs console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext2

(3)启动后发现现象和之前nfs方式启动挂载rootfs后一样的,至此rootfs制作实验圆满完成。

2.19.13.4、总结

(1)13节课将rootfs的制作步骤和原理已经完全清楚的讲完了

(2)制作过程本身如果有文档指导非常容易,非常简单,但是我们学习时侧重于不是rootfs制作本身,而是rootfs工作的原理分析。

X210的bsp介绍

2.20.1.1、嵌入式linux产品的bsp介绍

(1)大部分的ARM架构的linux平台的bsp的内容和结构都是相似的。

(2)bsp一般是芯片厂家/板卡厂家提供的。

2.20.1.2、X210的linux+QT bsp整体介绍

(1)tslib_x210_qtopia.tgz是用来支持QT的触摸屏操作的应用层库

(2)xboot和uboot是X210支持的2个bootloader源代码。kernel文件夹中是内核源代码,buildroot文件夹是用来构建根文件系统的文件夹。tools里是一些有用工具。

(2)mk脚本是用来管理和编译整个bsp的。

2.20.1.3、mk的帮助信息

(1)linux下的惯例就是,执行程序时加-h或者--help就可以看到这个程序执行的帮助信息。

(2)mk脚本的主要作用是编译bsp中的所有的源代码(包括bootloader、kernel、rootfs等),但是我们可以完整编译也可以部分编译,我们通过执行mk 后面加不同的参数来指定mk脚本去编译相应的部分。

譬如:

mk -a 即可编译所有的bsp源代码

mk -x 即可只编译xboot

mk -ui 即可只编译uboot针对inand版本开发板的源代码

mk -r 即可只编译buildroot,-r只是得到了文件夹形式的rootfs,并没有将其制作成镜像文件。

mk -re 即可编译buildroot并且制作得到ext3格式的rootfs镜像

mk -rj 即可编译buildroot并且制作得到jffs2格式的rootfs镜像

2.20.1.4、工作示例

(1)编译uboot ./mk -ui

(2)编译内核

注:./mk和mk都是执行mk这个脚本文件,区别在于./mk是带路径的,mk是不带路径的。还有source mk,这个和前两个的区别是source执行时不需要mk文件具有可执行权限,而前面两种要求mk必须在当前用户下具有可执行权限。

mk文件分析

2.20.2.1、shell程序结构

(1)shell脚本程序分为:变量定义、函数、代码。shell脚本程序的结构非常类似于C语言程序

(2)shell程序和C语言程序很大的一个差别就是shell没有main函数,shell脚本执行时也是先执行主函数的,不过主函数没有放在一个类似于main这样的函数中,而是直接放在全局下的一些代码。

(3)shell程序执行时首先执行变量定义,然后执行主函数,其他函数在主函数代码中被调用执行。

2.20.2.2、主函数

(1)上节课说过mk可以编译整个bsp的源代码,也可以只编译其中一部分。怎么实现控制编译哪个不编译哪个?实现原理就是:用一个函数来完成编译一个(譬如编译内核用build_kernel函数,编译inand的uboot用build_bootloader_uboot_inand),然后用相应的一些变量来控制这个函数要不要被编译(譬如uboot_inand变量=yes就表示要编译inand版本的uboot,=no就表示不要编译),我们编译时通过-xxxxx来传参时,这些传参会影响这些变量的值=yes或者=no

(2)如果我们直接./mk并不传参,则$1为空,这时候按照一套默认的配置来编译。

2.20.2.3、各个源文件的编译函数

2.20.3.buildroot引入

2.20.3.1、buildroot作用介绍

(1)之前自己从零开始构建根文件系统,一路下来事情还挺多,步骤比较麻烦。

(2)交叉编译工具链arm-linux-gcc,我们目前都是从soc官方直接拿来使用的,官方的工具链从何而来?实际上交叉编译工具链都是由gcc配置编译生成的,这个配置编译过程比较复杂,一般人自己去配置编译得到自己的交叉编译工具链是比较麻烦的,所以经常都是用别人最好的。

(3)buildroot就是一个集成包,这个包里集成了交叉编译工具链的制作,以及整个rootfs的配置编译过程。也就是说,使用buildroot可以很简便的得到一个做好的文件夹形式的根文件系统。

(4)buildroot将很多东西集成进来后,移植了linux kernel的make xxx_defconfig+make menuconfig的2步配置法,我们可以在buildroot的配置界面下完成集成在里边的所有东西的配置,然后直接make就可以最终得到文件夹形式的rootfs。

2.20.3.2、make x210ii_defconfig

(1)先make xxx_defconfig

(2)make menuconfig

(3)make

2.20.3.3、make及其错误解决

(1)直接make会遇到很多错误,这些错误原因都是因为ubuntu中缺乏一些必要软件包造成的。解决方案是先安装这些必要的软件包。

(2)编译过程会需要从网上下载一些软件包,因此整个编译过程需要在联网状态下进行。

2.20.2.4、编译后结果查看与分析

(1)编译后生成的文件夹格式的rootfs在buildroot/output/images/rootfs.tar。我们将其复制到了根目录下的release目录下去,这个文件就是一个完整的可以工作的文件夹形式的rootfs。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值