29.arm裸板驱动总结(makefile+lds链接脚本+裸板调试)

  在裸板2440中,当我们使用nand启动时,2440会自动将前4k字节复制到内部sram中,如下图所示:29.arm裸板驱动总结(makefile+lds链接脚本+裸板调试)
  然而此时的SDRAM、nandflash的控制时序等都还没初始化,所以我们就只能使用前0~4095地址,在前4k地址里来初始化SDRAM,nandflash,初始化完成后,才能将nandflash的4096至后面的地址内容存放到SDRAM里去.

  而裸板驱动的步骤如下所示:

  1.写makefile
  2.写lds链接脚本 (供makefile调用)
  3.写真正要执行的文件代码,比如初始化nand,sdram,串口等

  为什么要写lds链接脚本?
  首先lds链接脚本的作用就是将多个*.o文件的各个段链接在一起,告诉链接器这些各个段存放的地址先后顺序,它的好处就是,确保裸板2440的前4k地址里存放的是初始化SDRAM,nandflash的内容

1.写makefile

(参考:https://blog.csdn.net/qq_16933601/article/details/104326785)
在写裸板之前首先要来写Makefile,如下所示:

objs := head.o init.o nand.o main.o   
//定义objs变量,表示obj文件,包含生成boot.bin目标文件需要的依赖文件, 使用$(objs)就可以使用这个变量了//‘:=’:有关位置的等于(比如:”x:=a  y:=$(x)  x:=b”,那么y的值取决于当时位置的a,而不是b) //‘=’:无关位置的等于(比如:”x=a  y=$(x)  x=b”,那么y的值永远等于最后的b ,而不是a)                                                     

 nand.bin : $(objs)   //冒号前面的是表示目标文件, 冒号后面的是依赖文件,这里是将所有*.o文件编译出nand.bin可执行文件arm-linux-ld -Tnand.lds    -o nand_elf $^   //将*.o文件生成nand_elf链接文件//-T:指向链接脚本, $^:指向所有依赖文件,arm-linux-objcopy -O binary -S nand_elf $@ //将nand_elf链接文件生成nand.bin文件//$@:指向目标文件:nand.bin//-O :选项,其中binary就是表示生成的文件为.bin文件arm-linux-objdump -D -m arm  nand_elf > nand.dis //将nand.bin文件反汇编出nand.dis文件//-D :反汇编nand.bin里面所有的段, -m arm:指定反汇编文件的架构体系,这里arm架构

 %.o:%.c            //冒号前面的是目标文件,冒号后面的是依赖文件,%.o表示所有.o文件,arm-linux-gcc -Wall -c -O2 -o $@ $<         //将*.c文件生成*.o文件//$<:指向第一个依赖文件, 也就是.c文件//$@:指向目标文件,也就是.o文件//-Wall:编译若有错,便打印警告信息     -O2:编译优化程度为2级

 %.o:%.S                       
    arm-linux-gcc -Wall -c -O2 -o $@ $<    //将*.S文件生成*.o文件

 clean:                           //输入make clean,即进入该项,来删除所有生成的文件rm -f  nand.dis nand.bin nand_elf *.o   //通过rm命令来删除

2.写lds链接脚本

(参考:https://blog.csdn.net/qq_16933601/article/details/104327082)

SECTIONS {. = 0x30000000;             //指定当前的链接地址=0x30000000.text          :   {head.o(.text)    //添加第一个目标文件,里面会调用这些函数init.o(.text)      //添加第二个目标文件,里面存放关看门狗,初始化SDRAM等函数nand.o(.text)   //添加第三个目标文件,里面存放初始化nand函数*(.text)    // *(.text) 表示添加剩下的全部文件的.text代码段}.rodata ALIGN(4) : {*(.rodata)}       //指定只读数据段.data ALIGN(4) : { *(.data) }     //指定读写数据段,     *(data):添加所有文件的数据段__bss_start = .;     //把__bss_start赋值为当前地址位置,即bss段的开始位置.bss ALIGN(4)  : { *(.bss)  *(COMMON) }     //指定bss段,里面存放未被使用的变量__bss_end = .;        //把_end赋值为当前地址位置,即bss段的结束位置}

  上面的链接地址=0x30000000,表示程序运行的地方应该位于0x30000000处,0x30000000就是我们的SDRAM基地址,而一上电后,nand的前4k地址会被2440自动装载到内部ram中,所以我们初始化了sdram和nand后,就需要把程序所有内容都复制到链接地址0x30000000上才行

2.1为什么要在bss段的前后设置两个符号__bss_start, __bss_end?

  定义__bss_start和__bss_end符号,是用来程序开始之前将这些未定义的变量清0,节省内存且__bss_start -0x30000000就等于该bin文件的字节大小,实现动态复制

2.3为什么链接地址在0x30000000处,为什么在初始化sdram和nand之前,还能运行前4k地址的内容?

我们先来看看head.S第一个目标文件,就知道了:

.text                                                           @设置代码段

           @函数disable_watch_dog, memsetup, init_nand, nand_read_ll在init.c中定义
            ldr     sp, =4096               @设置堆栈
            bl      disable_watch_dog       @关WATCH DOG
            bl      memsetup                @初始化SDRAM
            bl      nand_init               @初始化NAND Flash

            ldr sp,=0x34000000              @64Msdram,所以设置栈SP=0x34000000,避免堆栈溢出
                                            
                           @nand_read_ll函数需要3个参数:
            ldr     r0,     =0x30000000     @1. 目标地址=0x30000000,这是SDRAM的起始地址
            mov     r1,     #0              @2.  源地址   = 0         
            ldr     r2,     =__bss_start         
            sub           r2,r2,r0          @3.  复制长度= __bss_start-0x30000000bl      nand_read               @调用C函数nand_read,将nand的内容复制到SDRAM中

            ldr     lr, =halt_loop          @设置返回地址

            ldr     pc, =main                @使用ldr命令 绝对跳转到SDRAM地址上
halt_loop:                                   @若main函数跳出后,便进入死循环,避免程序跑飞
            b       halt_loop

(参考位置无关码(bl)与绝对位置码(ldr): http://www.cnblogs.com/lifexy/p/7117345.html)

  从上面代码来看,可以发现在复制数据到sdram之前,都是使用的相对跳转命令bl,bl是一个位置无关码,也就是说无论该代码放在内存的哪个地址,都能正确运行.

  而ldr就是绝对跳转命令,是一个绝对位置码,当一上电时,我们的链接地址0x30000000上是没有程序的,因为程序都存在nand flash上,也就是0地址上,而如果在复制数据到sdram之前,使用ldr去执行的话,就会直接跳转到0x30000000上,就会运行出错.

  而且在复制数据到sdram之前,执行的代码里都不能用静态变量、全局变量、以及数组,因为这些初始值量的地址与位置有关的,必须将nand的内容复制到sdram地址中,才能用.

2.4比如,下面memsetup ()函数,就是个会出错的函数

其中的mem_cfg_val[]数组的内存是存在链接地址0x30000000上,就是与位置有关,在未复制内容之前使用将会出错

#define   MEM_CTL_BASE            0x48000000           //SDRAM寄存器基地址void memsetup(){
   int   i = 0;
   unsigned long *p = (unsigned long *)MEM_CTL_BASE;

 /* SDRAM 13个寄存器的值 */unsigned long  const    mem_cfg_val[]={ 0x22011110,     //BWSCON0x00000700,     //BANKCON00x00000700,     //BANKCON10x00000700,     //BANKCON20x00000700,     //BANKCON3 0x00000700,     //BANKCON40x00000700,     //BANKCON50x00018005,     //BANKCON60x00018005,     //BANKCON70x008C07A3,     //REFRESH0x000000B1,     //BANKSIZE0x00000030,     //MRSRB60x00000030,     //MRSRB7};

 

       for(; i < 13; i++)  p[i] = mem_cfg_val[i];}

  如下3个图所示,通过反汇编来看,上面的数组内容都是存在SDRAM的链接地址上面的rodata段0x300005d0里,在我们没有初始化SDRAM,复制数据到SDRAM之前,这些数据是无法读取到的

  图1,使用bl跳到相对地址0x30000094处:
29.arm裸板驱动总结(makefile+lds链接脚本+裸板调试)
  图2,使用ldr,使ip跳到绝对地址0x300005d0:29.arm裸板驱动总结(makefile+lds链接脚本+裸板调试)
  图3,0x300005d0里保存的.redata只读数据段,也就是 mem_cfg_val[]的内容:29.arm裸板驱动总结(makefile+lds链接脚本+裸板调试)

2.5所以要修改memsetup ()函数为以下才行:

#define   MEM_CTL_BASE            0x48000000           //SDRAM寄存器基地址void memsetup(){   unsigned long *p = (unsigned long *)MEM_CTL_BASE;/* 设置SDRAM 13个寄存器的值 */
   p[0]  =0x22011110,     //BWSCON
   p[1]  =0x00000700,     //BANKCON0
   p[2]  =0x00000700,     //BANKCON1
   p[3]  =0x00000700,     //BANKCON2
   p[4]  = 0x00000700,     //BANKCON3 
   p[5]  =0x00000700,     //BANKCON4
   p[6]  =0x00000700,     //BANKCON5
   p[7]  =0x00018005,     //BANKCON6
   p[8]  = 0x00018005,     //BANKCON7
   p[9] =0x008C07A3,     //REFRESH
   p[10] =0x000000B1,     //BANKSIZE
   p[11] = 0x00000030,     //MRSRB6
   p[12] =0x00000030,     //MRSRB7}

  通过反汇编来看,可以看到这些赋值,都是靠mov,add等命令来加加减减拼出来的

  如下图,我们以上面的代码p[0] =0x22011110为例:29.arm裸板驱动总结(makefile+lds链接脚本+裸板调试)

3.在裸板中调试有以下几步

3.1点灯法:

LED_SHOW:   ldr   r0,  =0x56000050                        
               ldr  r1,  =(1<<(4*2))                 @设置GPFCON寄存器的GPF4为输出引脚
               str  r1,  [r0]                                           
               ldr  r0,  =0x56000054                       @GPFDAT寄存器
               ldr  r1,  =0                                @设置GPF4=0,亮灯
               ldr  r2,  =(1<<4)                           @设置GPF4=1,灭灯                            

LED_LOOP:                        @死循环闪灯

               str  r1,  [r0]           @亮灯
               bl   DELAY
               str  r2,  [r0]         @灭灯
               bl   DELAY   
               b    LED_LOOP 

DELAY:                           @延时

             ldr r3,=300001:   sub  r3,  r3,   #1   cmp  r3,  #0   bne     1b
               mov  pc, lr     @跳出循环  PS:寄存器之间赋值只能用mov

  在调试汇编中:就可以使用 “b LED_SHOW”,若LED闪烁,便说明程序已跑过,通过点灯来定位程序在哪出错,

缺点在于需要多次烧写才能得出结果,调试非常麻烦

3.2串口打印

  首先需要通过寄存器来初始化串口

  在2440中,当没有初始化MPLLCON和CLKDIVN寄存器时,所有的时钟都由12Mhz晶振提供,所以PCLK=12MHZ,则波特率最高就是57600,因为UBRDIV0=12000000/(57600*16-1)=13.02,所以串口代码如下所示:

#define S3C_PCLK            12000000    // PCLK初始值为12MHz#define S3C_UART_CLK        PCLK        //  UART0的时钟源设为PCLK#define S3C_UART_BAUD_RATE  57600      // 波特率#define S3C_UART_BRD        ((UART_CLK  / (UART_BAUD_RATE * 16)) - 1)#define S3C_GPHCON              (*(volatile unsigned long *)0x56000070)#define S3C_GPHDAT              (*(volatile unsigned long *)0x56000074)#define S3C_GPHUP               (*(volatile unsigned long *)0x56000078)/*UART registers*/#define S3C_ULCON0              (*(volatile unsigned long *)0x50000000)#define S3C_UCON0               (*(volatile unsigned long *)0x50000004)#define S3C_UFCON0              (*(volatile unsigned long *)0x50000008)#define S3C_UMCON0              (*(volatile unsigned long *)0x5000000c)#define S3C_UTRSTAT0            (*(volatile unsigned long *)0x50000010)#define S3C_UTXH0               (*(volatile unsigned char *)0x50000020)#define S3C_URXH0               (*(volatile unsigned char *)0x50000024)#define S3C_UBRDIV0             (*(volatile unsigned long *)0x50000028)#define TXD0READY   (1<<2)#define RXD0READY   (1)void uart0_init(void){S3C_GPHCON  |= 0xa0;    // GPH2,GPH3用作TXD0,RXD0S3C_GPHUP   = 0x0c;     // GPH2,GPH3内部上拉S3C_ULCON0  = 0x03;     // 8N1(8个数据位,无较验,1个停止位)S3C_UCON0   = 0x05;     // 查询方式,UART时钟源为PCLKS3C_UFCON0  = 0x00;     // 不使用FIFOS3C_UMCON0  = 0x00;     // 不使用流控S3C_UBRDIV0 = S3C_UART_BRD; // 波特率为115200}/*
 * 发送一个字符
 */void putc(unsigned char c){/* 等待,直到发送缓冲区中的数据已经全部发送出去 */while (!(S3C_UTRSTAT0 & TXD0READY));  /* 向UTXH0寄存器中写入数据,UART即自动将它发送出去 */S3C_UTXH0 = c;}/*
 * 打印一串数字
 * num:数据
 */void putnum(unsigned long num) //0xFFFF FFFF         (7:0){int i ,start=0;unsigned char c;uart0_init();for(i=0;i<100;i++);putc('0');putc('x');   for(i=7;i>=0;i--)            //从[7:0]中打印数字,去除有效数字前面的0   {  c=( num >> (i*4) )&0xf;  if(c!=0)  { if(c>9)    putc(c-10+'A');                //打印A~F elseputc(c+'0');                   //打印1~9                     if(!start)  start=1;                         //start=1,说明为有效数字  }  else if((start||!i)&&c==0)       //若是有效数字,便打印0,且在个位上时,不管是否有效都要打印  { putc('0');  }   }   putc('\r');   putc('\n');}

  在调试汇编中,就可以使用:

mov   r0,#0x100     //参数等于0x100bl    putnum       //调用打印函数

  即可打印0x100数字, 能快速定位出程序在哪出错

在c中,直接通过调用函数即可

3.3 使用JTAG调试器

  JTAG用于芯片的测试与程序调试,JTAG位于CPU内部,当CPU收发引脚上的数据时,都会通过JTAG单元,而JTAG单元会从CPU内部引出TMS,TCK,TDI,TDO,四个引脚,便可以通过OpenJTAG调试器连接电脑USB,而另一端连接这些JTAG脚来控制CPU

  OpenJTAG可以实现:
  1.读写某个地址上的数据
  2.将文件下载到2440的某个地址上,或读取出某个地址到文件中
  3.查询CPU当前状态、中断CPU运行、恢复CPU运行、复位CPU等
  4.设置CPU的地址断点,比如设置为0x30000000,当CPU运行到这个地址时,便会停止运行

  断点在调试中分为两种:
  硬件断点,在2240中,共有两个硬件断点,也就是最多设置两个硬件断点

  软件断点,可以设置无数个断点

1)为什么软件断点可以设置无数个?

  实际JTAG后台会把每个需要暂停的地址断点里的数据复制到指定地址里,并赋为某个特殊值(如deeedeee),然后CPU运行时,当某个变量=这个特殊值(如deeedeee),便知道到了软件断点,并从指定地址里把原来的值换回去,然后暂停运行

  注意:
由于软件断点,会后台保存断点数据到另一个地址中,前提要必须保证地址可直接读写,所以在nor flash,nand flash下则无法实现调试,若链接地址在SDRAM地址上,则软件断点的地址必须设置在SDRAM初始化后的地址上

3.3.1.通过OCD对JATG进行命令行调试

1)安装OpenOCD

  OpenOCD:既可以烧写nor flash,也可以烧写nand flash,并可以通过JTAG调试器来进行调试
  接上OpenJTAG,并安装OpenJTAG驱动

2)使用OpenOCD工具连接OpenJTAG调试器
29.arm裸板驱动总结(makefile+lds链接脚本+裸板调试)
  如上图所示:

  步骤1,选择jtag类型,CPU类型.

  步骤2,点击连接按钮

  步骤3,可以看到2440只支持2个硬件断点

  其中,work dir 就是需要烧写的文件根目录, 或读取CPU某个地址内容到文件的文件根目录

3)然后通过telent控制台进行调试

  telent的主要目的,就是发送命令行给连接的OpenJTAG调试器,然后OpenJTAG通过命令来对CPU进行操作

  首先,在win7下,若没打开telnet客服端:

  点击开始 ->控制面板-> 程序和功能-> 打开或关闭Windows功能->打开“telnet客服端”

  然后在cmd控制台下,输入 “telnet 127.0.0.1 4444”命令,进入telent控制台,如下图所示:29.arm裸板驱动总结(makefile+lds链接脚本+裸板调试)
  4)接下来便可以通过命令行来实现调试(需要参考反汇编文件,来实现调试)

  常用的命令如下所示:

  poll

  查看当前状态

  halt

  暂停CPU运行

  step

  单步执行,如果指定了 address,则从 address 处开始执行一条指令

  reg

  显示CPU的r0、r1、r2、sp、lr、pc等寄存器的值(需要halt后才能看到)

  resume [addr]

  恢复CPU运行,若指定了地址,便从指定地址运行(需要halt后才能使用)

  例如: resume 0 //从0地址运行

  md<w|h|b> [size]

  read读地址,读出size个内容,w:字,h:半字,b:字节.如下图所示:
29.arm裸板驱动总结(makefile+lds链接脚本+裸板调试)
  mw<w|h|b>

  word写地址,写入size个内容,使用方法和上面类似

(PS:不能直接读写nand和nor上的地址,只能读写2440的内部地址(4096),若SDRAM已初始化,也可以实现读写)

  load_image


  将文件载入地址为 address 的内存,格式有“bin”, “ihex”、 “elf”

例如:

load_image  led.bin  0              //烧写led.bin到0地址

(PS:该文件的目录位于之前在OpenOCD工具的界面里的work dir里)

  dump_image


将内存从地址 address 开始的 size 字节数据读出,保存到文件中

  bp [hw]

  在地址 addr 处设置断点,hw 表示硬件断点,length为指令集字节长度,,若未指定表示软件断点,比如: stm32是2个字节长,2440是4个字节长,部分MCU拥有多套指令集,长度不固定,如下图所示:
29.arm裸板驱动总结(makefile+lds链接脚本+裸板调试)
  rbp

  删除地址 addr 处的断点

  bp

  打印断点信息

3.3.2通过GDB对JATG实现源码级别的调试

  在linux中,使用arm-linux-gdb软件

  在win7中,则使用arm-none-eabi-gdb软件

  使用GDB工具,就不需要像上个OCD调试那么麻烦了

1)比如说,想在“int i=0;”处打上断点:

OCD调试:

  就需要查看调试的反汇编文件,找到i=0所在的运行地址,然后通过命令在地址上打断点

GDB调试:

  则可以直接在i=0处的源码上打断点,后台会通过带调试信息的编译文件,来找到i=0处的运行地址,并向OpenOCD发送打断点命令

2)上面的带调试信息的编译文件又是怎么来的?

  通过Makefile里的arm-linux -g 来的, -g:表示生成的编译文件里包含gdb调试信息

  然后我们将上面第1节的Makefile修改,如下图:
29.arm裸板驱动总结(makefile+lds链接脚本+裸板调试)
3)使用gdb之前,需要保证:

  1.调试的源码里面的内容必须位于同一个链接地址上, 各个段也要分开存储,调试的链接脚本和上面第2节的类似,
  2.如果程序的链接地址是SDRAM, 使用openocd初始化SDRAM
4)常用命令如下所示(以调试上图的nand_elf文件为例):

  arm-none-eabi-gdb nand_elf

  启动GDB,指定调试文件为nand_elf

  target remote 127.0.0.1:3333

  与OpenOCD建立连接

  load

  载入nand_elf调试文件

  break [file]:[row]

  打断点,比如:

break main.c:21     //在main.c文件的第21行处打断点

  info br

  查看断点

  delete num

  删除第几个断点,如下图所示:
29.arm裸板驱动总结(makefile+lds链接脚本+裸板调试)
  c

  恢复程序运行,若使用load后,使用c便是启动程序, 按ctrl+c便暂停运行

  step

  单步执行

  monitor <cmd…>

  调用OCD的命令使用,比如 :

monitor resume 0              //使用OCD的resume命令,使程序从0地址运行

  quit

  退出
(PS:也可以通过eclipse平台软件来调用GDB,GDB最终转换为命令行,再调用OCD来实现调试,如下图所示)
29.arm裸板驱动总结(makefile+lds链接脚本+裸板调试)

上一篇:详细示范简单VCS的Makefile怎么写


下一篇:makefile文件详解