arm裸板驅動總結(makefile+lds鏈接腳本+裸板調試)


在裸板2440中,當我們使用nand啟動時,2440會自動將前4k字節復制到內部sram中,如下圖所示:

然而此時的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

(參考makefile初步制作:http://www.cnblogs.com/lifexy/p/7065175.html)

在寫裸板之前首先要來寫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鏈接腳本

(參考lds腳本解析: http://www.cnblogs.com/lifexy/p/7089873.html)

 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-0x30000000 bl 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,     //BWSCON

                                            0x00000700,     //BANKCON0

                                            0x00000700,     //BANKCON1

                                            0x00000700,     //BANKCON2

                                            0x00000700,     //BANKCON3 

                                            0x00000700,     //BANKCON4

                                            0x00000700,     //BANKCON5

                                            0x00018005,     //BANKCON6

                                            0x00018005,     //BANKCON7

                                            0x008C07A3,     //REFRESH

                                            0x000000B1,     //BANKSIZE

                                            0x00000030,     //MRSRB6

                                            0x00000030,     //MRSRB7

                                    };

 

       for(; i < 13; i++)

              p[i] = mem_cfg_val[i];

}

如下3個圖所示,通過反匯編來看,上面的數組內容都是存在SDRAM的鏈接地址上面的rodata段0x300005d0里,在我們沒有初始化SDRAM,復制數據到SDRAM之前,這些數據是無法讀取到的

圖1,使用bl跳到相對地址0x30000094處:

 

圖2,使用ldr,使ip跳到絕對地址0x300005d0:

 

圖3,0x300005d0里保存的.redata只讀數據段,也就是 mem_cfg_val[]的內容:

 

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為例:

 

 

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,=30000
1:
               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,RXD0
    S3C_GPHUP   = 0x0c;     // GPH2,GPH3內部上拉
    S3C_ULCON0  = 0x03;     // 8N1(8個數據位,無較驗,1個停止位)
    S3C_UCON0   = 0x05;     // 查詢方式,UART時鍾源為PCLK
    S3C_UFCON0  = 0x00;     // 不使用FIFO
    S3C_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
                     else
                            putc(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     //參數等於0x100
bl    putnum       //調用打印函數

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

在c中,直接通過調用函數即可

 

3.3 使用JTAG調試器 

JTAG用於芯片的測試與程序調試,JTAG位於CPU內部,當CPU收發引腳上的數據時,都會通過JTAG單元,而JTAG單元會從CPU內部引出TMS,TCK,TDI,TDO,四個引腳,便可以通過OpenJTAG調試器連接電腦USB,而另一端連接這些JTAG腳來控制CPU

OpenJTAG可以實現:

  • 讀寫某個地址上的數據
  • 將文件下載到2440的某個地址上,或讀取出某個地址到文件中
  • 查詢CPU當前狀態、中斷CPU運行、恢復CPU運行、復位CPU等
  • 設置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調試器

如上圖所示:

步驟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控制台,如下圖所示:

 

 

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> <addr>  [size]      

read讀地址,讀出size個內容,w:字,h:半字,b:字節.如下圖所示:

 

mw<w|h|b> <addr>  <size> 

word寫地址,寫入size個內容,使用方法和上面類似

(PS:不能直接讀寫nand和nor上的地址,只能讀寫2440的內部地址(4096),若SDRAM已初始化,也可以實現讀寫)

load_image <file> <address>  

將文件<file>載入地址為 address 的內存,格式有“bin”, “ihex”、 “elf”

例如: 

load_image  led.bin  0              //燒寫led.bin到0地址

 

(PS:該文件的目錄位於之前在OpenOCD工具的界面里的work dir里)

dump_image <file> <address> <size>

將內存從地址 address 開始的 size 字節數據讀出,保存到文件<file>中

bp <addr> <length> [hw]

在地址 addr 處設置斷點,hw 表示硬件斷點,length為指令集字節長度,,若未指定表示軟件斷點,比如: stm32是2個字節長,2440是4個字節長,部分MCU擁有多套指令集,長度不固定,如下圖所示: 

rbp <addr>

刪除地址 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修改,如下圖:

 

 

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>

刪除第幾個斷點,如下圖所示:

 

c

恢復程序運行,若使用load后,使用c便是啟動程序, 按ctrl+c便暫停運行

step

單步執行

monitor  <cmd...>

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

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

quit

退出

 

 


(PS:也可以通過eclipse平台軟件來調用GDB,GDB最終轉換為命令行,再調用OCD來實現調試,如下圖所示)

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM