跳至主要内容

【转】vivi开发笔记(十五):vivi源代码分析2

文章说明:calmarrow(lqm)原创

文章引自:http://piaoxiang.cublog.cn 

 
    现在进入bootloader之vivi分析的第二阶段,这部分使用C语言实现,部分代码采取内嵌汇编的方式。这里需要用到GNU GCC内嵌汇编的知识,这部分基础还没有具备,需要学习。
 
    下面先按照流程进行分析。需要注意的是,此部分内容并非完全按照原版的vivi源代码,而是加入了自己的理解。另外,对非常简单、google出一片而且有分析正确的部分,在这里就简化了,不做详细分析,只是对网上没有分析到位而又影响理解的部分进行深入分析。我想,这部分内容应该是对《s3c2410完全开发》中vivi源代码分析部分的补充和完善。
 
stage 2:【init/main.c】
 
    第二阶段的入口就是init/main.c,按照源代码的组织流程,根据模块化划分的原则,应该分为8个功能模块,源代码注释以step区分,非常清晰。现在首先解决一个问题,就是关于main的形参。vivi源代码中对main的原型使用了:int main(int argc, char *argv[])的标准形式,在第一阶段的【arch/s3c2410/head.S】中,利用APCS设定了相应的入口参数,如下:
 

@ get read to call C functions
        ldr sp, DW_STACK_START @ setup stack pointer
        mov fp, #0 @ no previous frame, so fp=0
        mov a2, #0 @ set argv to NULL

        bl main @ call main 

 
    这里的sp、fp、a2都是APCS中名字,与之对应的寄存器分别为R13、R11、R2。这里理解的重点在于fp(frame pointer,帧指针),也就是栈帧指针。这是比较复杂的一个地方,对栈需要有深入的分析。搜集了一些资料,看了APCS标准,后续会把关于fp和APCS的部分单独拿出,总结成文。不管怎样,通过其入口地址的设置也可以看出,main是不需要入口地址的。那么,为了理解上的方便,不妨把main原型改为int main(void),这样,相应的入口地址就不需要设置了。更改后的head.S对应部分如下:
 

@ jump to ram
        @ a technology about trampoline
        ldr pc, =on_the_ram

on_the_ram:
        bl main
        @ if main ever returns, reboot
        mov pc, #FLASH_BASE

 
    清晰而且符合规则,前提是init/main.c中main原型修改为int main(void)。当然,对main还动了一些手术,现在把main的主流程部分放在这里,后面会对为什么如此改动详细说明。
 
 

int main(void)
{
        int ret;

        
/*
         * Step 1:
         * Print Vivi version information
         */

        putstr("\r\n");
        putstr(vivi_banner);

        
/*
         * Step 2:
         * initialize board environment
         */

        ret = board_init();
        if (ret) {
                putstr("Failed a board_init() procedure\r\n");
                error();
        }

        
/*
         * Step 3:
         * MMU management
         * When it's done, vivi is running on the ram and MMU is enabled.
         */

        mem_map_init();
        mmu_init();
        putstr("Succeed memory mapping.\r\n");

        
/*
         * Step 4:
         * initialize the heap area
         */

        ret = heap_init();
        if (ret) {
                putstr("Failed initailizing heap region\r\n");
                error();
        }

        
/*
         * Step 5:
         * initialize the MTD device
         */

        ret = mtd_dev_init();

        
/*
         * Step 6:
         * initialize the private data
         */

        init_priv_data();

        
/*
         * Step 7:
         * initialize the humanmachine environment
         */

        misc();
        init_builtin_cmds();

        
/*
         * Step 8:
         * boot kernel or step into vivi
         */

        boot_or_vivi();

        return 0;
}

 
(1)step 1:打印版本信息
 
    这一部分其实是作为调试和增强人机交互行而用的,如果不用,对vivi的主要功能也不会产生影响。本来是最为简单的一个部分,但是实际上确实我理解上问题最多的一个部分,对这块动的手术也最多。事实上,这个部分的reset_handler存在bug。具体分析一下。
 
    源代码step 1部分如下:

putstr("\r\n");
putstr(vivi_banner);

reset_handler();

 
    打印的vivi_banner在【init/version.c】中,如下:
 

#include "version.h"
#include "compile.h"

const char *vivi_banner =
                       "VIVI version " VIVI_RELEASE " (" VIVI_COMPILE_BY "@"
                       VIVI_COMPILE_HOST ") (" VIVI_COMPILER ") " UTS_VERSION "\r\n";

 
   vivi_banner就是字符串,中间有四个未知的宏,这是非常明显的。显然就是头文件中给出的。但是find,没有version.h、compile.h,这在前面Makefile分析时也详细讲解过了,而且还把version.h合并入compile.h里面了。可以just for fun,增加一些个性化的打印信息,比如,我的version.c修改如下:
 

#include "compile.h"

const char *vivi_banner =
                        "\r\n\t^_^ Well done, boy! Go on -->\r\n"
                        "VIVI version " VIVI_RELEASE " (" VIVI_COMPILE_BY "@"
                        VIVI_COMPILE_HOST ") (" VIVI_COMPILER ") " UTS_VERSION "\r\n";

 
    下面进入疑惑的reset_handler功能部分。作者的本意是利用reset_handler();实现软复位跟硬复位的处理。
 

【lib/reset_handle.c】
void
reset_handler(void)
{
    int pressed;

    pressed = is_pressed_pw_btn();

    if (pressed == PWBT_PRESS_LEVEL) {
        DPRINTK("HARD RESET\r\n");
        hard_reset_handle();
    } else {
        DPRINTK("SOFT RESET\r\n");
        soft_reset_handle();
    }
}

 
    首先看一下is_pressed_pw_btn,按照函数字面意思,应该是判断电源复位键是否按下,如果按下,则证明是硬复位;如果没有检测到键按下,那么就是软复位。具体代码如下:

static int
is_pressed_pw_btn(void)
{
    return read_bt_status();
}

--> read_bt_status

static int
read_bt_status(void)
{
    ulong status;

    
//status = ((GPLR & (1 << GPIO_PWBT)) >> GPIO_PWBT);

    status = ((PWBT_REG & (1 << PWBT_GPIO_NUM)) >> PWBT_GPIO_NUM);
    
    if (status)
        return HIGH;
    else
        return LOW;
}

 
    可是,PWBT_REG是没有定义的,PWBT_GPIO_NUM也没有定义,也就是说,这个函数实际上是不可能编译通过的。从表面上分析,如同网上大部分讨论一样,我可以知道作者是什么意图,但是这段代码真正有效吗?从上面分析看,答案显然是这个reset_handle.c根本就是无效的。那么为什么vivi移植成功都没有注意这个问题,还只是按照表面意思分析代码呢?这个可以看reset_handle.h的头文件。
 

#ifdef CONFIG_RESET_HANDLING
void reset_handler(void);
#else
#define reset_handler()    (void)(0)
#endif

 
    很显然,在配置的时候,CONFIG_RESET_HANDLING是没有定义的,那么reset_handler()为空,也就是说这部分根本就是空代码,并没有实际执行功能。如果还不放心,那就做测试,如果把CONFIG_RESET_HANDLING选中(具体是把General setup部分的support reset handler选中),那么就会出现错误:
 

reset_handle.c: In function `read_bt_status':
reset_handle.c:31: `PWBT_REG'
undeclared (first use in this function)
reset_handle.c:31: (Each undeclared identifier is reported only once
reset_handle.c:31: for each function it appears in.)
reset_handle.c:31: `PWBT_GPIO_NUM
' undeclared (first use in this function)
reset_handle.c:28: warning: `status'
might be used uninitialized in this function
reset_handle.c: In function `hard_reset_handle
':
reset_handle.c:52: `USER_RAM_BASE'
undeclared (first use in this function)
reset_handle.c:52: `USER_RAM_SIZE
' undeclared (first use in this function)
reset_handle.c: In function `reset_handler'
:
reset_handle.c:68: `PWBT_PRESS_LEVEL
' undeclared (first use in this function)
make[2]: *** [reset_handle.o] Error 1
make[2]: Leaving directory `/home/armlinux/embedded_Linux/s3c2410/bootloader/m-boot-1.0.0/lib'

make[1]: *** [first_rule] Error 2
make[1]: Leaving directory `/home/armlinux/embedded_Linux/s3c2410/bootloader/m-boot-1.0.0/lib

 
    可见这部分功能是多余的。可以选择把这部分设置完全去掉,方法如下:
 
    ・【init/main.c】去掉行reset_handler();,去掉#include <reset_handle.h>。
    ・删除【lib/reset_handle.c】,删除【include/reset_handle.h】
    ・【arch/config.in】,删除行bool 'support reset handler' CONFIG_RESET_HANDLING,这样就彻底把此项配置部分也删除了。如果还有原来的默认配置文件,可以把# CONFIG_RESET_HANDLING is not set删除。
 
    经过上面三步,就可以把reset handler功能去掉了。这些在你了解了vivi的配置机制后是很容易操作的,它们之间的关系并不复杂,就是一条链,顺着找就可以了。
 
    我现在第一步想做的是把vivi进行"瘦身",只需要完成在EDUKIT-III上从nand flash启动引导内核的功能就可以,从中也可以了解核心技术和主要流程。但是,在整个的软件架构上是保持不变的。如果我想增加功能,因为对这个软件架构熟悉了,所以很容易扩展,而且也容易自己重新做一个功能更好一些的bootloader。
 
(2)step 2:
 
    主要是初始化GPIO。这个在前面实验中做过了,基本的思路和方法就是在把握好整个系统硬件资源的前提下,根据datasheet把所有的初始值设定,在这里利用这个函数就可以完成初始化了。
 
(3)step 3:
 
    MMU初始化。这部分在MMU基础实验中完成了。关于GNU GCC内嵌汇编部分还不是太清晰,还有待于在后续工作中加强。
 
(4)step 4:
 
    堆初始化。堆与栈的区别已经比较清晰了,在动手分析vivi的过程中,更为明确了。在这里,实际上就是实现动态内存分配策略。具体实现部分在【lib/heap.c】。因为以前自己没有写过动态内存分配,所以要仔细分析这部分是如何实现的。这部分的工作主要有两个:一是分析封装调试宏的技巧和printk的实现方法,这部分在这里还是挺重要的。二是heap基本的原理是什么?具体如何实现?
 
    下面首先进行第一个重点分析。关于调试手段,在分析ARM的基本调试手段时也提到过,使用串口打印调试信息是一个非常有效且常用的手段,vivi中采取的也是这种方式。当然,如果你只是实现最为简单的打印字符串等,那么初始化串口后,封装一个基本的输出函数就可以了。但是,这个基本函数的功能是非常有限的。我们在Linux用的printk则要强大好用的多。vivi的思想就是把Linux kernel的printk拿过来,稍微裁减一下(因为vivi不需要打印级别,但是需要打印手段的多样化)。这样,自己的工作量并不大,但是调试手段则要完善得多了。在这里,关于printk的代码细节不作为重点,vivi也只是借用了Linux kernel的printk的实现,并做了简单的修改,把console映射到了串口0上。
 
    手头上暂时有linux-2.4.18的内核,暂时以这个为依据来探讨vivi中printk的实现。

    ・复制【lib/vsprintf.c】到vivi的lib目录下,更改名称为printk.c。然后只保留vsnprintf,及其用到的number函数、skip_atoi函数。skip_atoi中用到了isdigit,所以把【include/linux/ctype.h】复制到vivi的include目录下。另外,还要用到do_div和strnlen两个函数。其中do_div是宏,在【include/asm-arm/div64.h】中实现,直接复制到vivi的include文件夹中。strnlen应该在string.c中实现,可以从【lib/string.c】复制然后添加到vivi的lib下的string.c文件中,最后把声明加到include下的vivi_string.h中。这样,printk需要的基础部分就具备了。
    ・复制【kernel/printk.c】,然后把printk的实现部分摘出来,去掉打印等级等功能,参考vivi的就可以封装起来了。
 
    可见,vivi中的printk只是把Linux kernel中的代码拿过来,做了及其少量的修改。我现在已经重现了这个过程,并且对整个vivi工程文件做出一些修改,编译下载,测试功能稳定。
 
    实现了printk,往往需要封装一个调试宏。在【lib/heap.c】中和其他一些文件中,调试宏都是这样的形式:

#ifdef DEBUG_HEAP
#define DPRINTK(args...)    printk(##args)
#else
#define DPRINTK(args...)
#endif

 
    分析这样是不妥的。原因就在于stdarg.h(注意,这里只需要定义头文件<stdarg.h>就可以把变长参数表的功能引入了。该头文件的实现因为不同的机器而不同,但是提供的接口是一致的。具体可以看《The C Programming Language》。但是一个细节就是你如果find,会找不到stdarg.h这个头文件,原因就是gcc直接把stdarg.h放到编译器里。)规定的变长参数表必须至少包括一个有名参数,va_start会将最后一个有名参数作为起点。这里封装的printk缺少了有名参数,这里可以做测试。测试工程如下:
 
文件: printk_test.tar.gz
大小: 1KB
下载: 下载
 
    通过这个测试手段,发现如果选择方式#define DPRINTK(fmt, args...)   printk(fmt, ##args),那么结果如下:
 

[armlinux@lqm printk_test]$ make
gcc -Wall -g -O2 -c -o printk.o printk.c
gcc -Wall -g -O2 -c -o test.o test.c
gcc -Wall -g -O2 printk.o test.o -o test
[armlinux@lqm printk_test]$ ls
Makefile printk.c printk.h printk.o test test.c test.o
[armlinux@lqm printk_test]$ ./test
test: i = 5, j = 10

 
    如果选择#define DPRINTK(args...)        printk(##args),那么结果如下:
 

[armlinux@lqm printk_test]$ make
gcc -Wall -g -O2 -c -o printk.o printk.c
gcc -Wall -g -O2 -c -o test.o test.c
test.c:23:47: warning: pasting "(" and ""test: i = %d, j = %d\n"" does not give a valid preprocessing token
gcc -Wall -g -O2 printk.o test.o -o test
[armlinux@lqm printk_test]$ ls
Makefile printk.c printk.h printk.o test test.c test.o
[armlinux@lqm printk_test]$ ./test
test: i = 5, j = 10

 
    可见第二种方式是不合适的。于是修改如下:
 

#ifdef DEBUG_HEAP
#define DPRINTK(fmt, args...) printk(fmt, ##args)
#else
#define DPRINTK(fmt, args...)
#endif

 
    这样的调试宏就没有问题了。也算是宏的一个小技巧吧,在Linux内核中查看,可以看到不少的printk的宏封装都是这样的。
 
    接下来关于heap的实现细节,这部分在《s3c2410完全开发》上分析比较到位,这里就不做具体分析了,可以参考《s3c2410完全开发》(thisway_diy)。

评论

此博客中的热门博文

【转】AMBA、AHB、APB总线简介

AMBA 简介 随着深亚微米工艺技术日益成熟,集成电路芯片的规模越来越大。数字IC从基于时序驱动的设计方法,发展到基于IP复用的设计方法,并在SOC设计中得到了广泛应用。在基于IP复用的SoC设计中,片上总线设计是最关键的问题。为此,业界出现了很多片上总线标准。其中,由ARM公司推出的AMBA片上总线受到了广大IP开发商和SoC系统集成者的青睐,已成为一种流行的工业标准片上结构。AMBA规范主要包括了AHB(Advanced High performance Bus)系统总线和APB(Advanced Peripheral Bus)外围总线。   AMBA 片上总线        AMBA 2.0 规范包括四个部分:AHB、ASB、APB和Test Methodology。AHB的相互连接采用了传统的带有主模块和从模块的共享总线,接口与互连功能分离,这对芯片上模块之间的互连具有重要意义。AMBA已不仅是一种总线,更是一种带有接口模块的互连体系。下面将简要介绍比较重要的AHB和APB总线。 基于 AMBA 的片上系统        一个典型的基于AMBA总线的系统框图如图3所示。        大多数挂在总线上的模块(包括处理器)只是单一属性的功能模块:主模块或者从模块。主模块是向从模块发出读写操作的模块,如CPU,DSP等;从模块是接受命令并做出反应的模块,如片上的RAM,AHB/APB 桥等。另外,还有一些模块同时具有两种属性,例如直接存储器存取(DMA)在被编程时是从模块,但在系统读传输数据时必须是主模块。如果总线上存在多个主模块,就需要仲裁器来决定如何控制各种主模块对总线的访问。虽然仲裁规范是AMBA总线规范中的一部分,但具体使用的算法由RTL设计工程师决定,其中两个最常用的算法是固定优先级算法和循环制算法。AHB总线上最多可以有16个主模块和任意多个从模块,如果主模块数目大于16,则需再加一层结构(具体参阅ARM公司推出的Multi-layer AHB规范)。APB 桥既是APB总线上唯一的主模块,也是AHB系统总线上的从模块。其主要功能是锁存来自AHB系统总...

【转】GPIO编程模拟I2C入门

ARM编程:ARM普通GPIO口线模拟I2C  请教个问题: 因为需要很多EEPROM进行点对点控制,所以我现在要用ARM的GPIO模拟I2C,管脚方向我设 置的是向外的。我用网上的RW24C08的万能程序修改了一下,先进行两根线的模拟,SDA6, SCL6,但是读出来的数不对。我做了一个简单的实验,模拟SDA6,SCL6输出方波,在示波 器上看到正确方波,也就是说,我的输出控制是没问题的。 哪位大哥能指点一下,是否在接收时管脚方向要设为向内?(不过IOPIN不管什么方向都可 以读出当前状态值的阿) 附修改的RW24C08()程序: #define  SomeNOP() delay(300); /**/ /* *********************************  RW24C08   **************************************** */ /**/ /* ----------------------------------------------------------------------------- ---  调用方式:void I2CInit(void)   函数说明:私有函数,I2C专用 ------------------------------------------------------------------------------- -- */ void  I2CInit( void ) ... {  IO0CLR  =  SCL6;      // 初始状态关闭总线  SomeNOP();  // 延时   I2CStop();  // 确保初始化,此时数据线是高电平 }   /**/ /* ---------------------------------------------------------------------------- ----  调用方式:void I2CSta...

【转】cs8900网卡的移植至基于linux2.6内核的s3c2410平台

cs8900网卡的移植至基于linux2.6内核的s3c2410平台(转) 2008-03-11 20:58 硬件环境:SBC-2410X开发板(CPU:S3C2410X) 内核版本:2.6.11.1 运行环境:Debian2.6.8 交叉编译环境:gcc-3.3.4-glibc-2.3.3 第一部分 网卡CS8900A驱动程序的移植 一、从网上将Linux内核源代码下载到本机上,并将其解压: #tar jxf linux-2.6.11.1.tar.bz2 二、打开内核顶层目录中的Makefile文件,这个文件中需要修改的内容包括以下两个方面。 (1)指定目标平台。 移植前:         ARCH?= $(SUBARCH) 移植后: ARCH            :=arm (2)指定交叉编译器。 移植前: CROSS_COMPILE ?= 移植后: CROSS_COMPILE   :=/opt/crosstool/arm-s3c2410-linux-gnu/gcc-3.3.4-glibc-2.3.3/bin/arm-s3c2410-linux-gnu- 注:这里假设编译器就放在本机的那个目录下。 三、添加驱动程序源代码,这涉及到以下几个方面。(1)、从网上下载了cs8900.c和cs8900.h两个针对2.6.7的内核的驱动程序源代码,将其放在drivers/net/arm/目录下面。 #cp cs8900.c ./drivers/net/arm/ #cp cs8900.h ./drivers/net/arm/ 并在cs8900_probe()函数中,memset (&priv,0,sizeof (cs8900_t));函数之后添加如下两条语句: __raw_writel(0x2211d110,S3C2410_BWSCON); __raw_writel(0x1f7c,S3C2410_BANKCON3); 注:其原因在"第二部分"解释。 (2)、修改drivers/net/arm/目录下的Kconfig文件,在最后添加如...