在裸板2440中,當我們使用nand啟動時,2440會自動將前4k位元組複製到內部sram中,如下圖所示: 然而此時的SDRAM、nandflash的控制時序等都還沒初始化,所以我們就只能使用前0~4095地址,在前4k地址里來初始化SDRAM,nandflash,初始化完成後,才能將nandflas ...
在裸板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中,當沒有初始化PCLK時鐘時,PCLK=12MHZ,而波特率最高就是57600,因為UBRDIV0=12000000/(57600*16-1)=13.02,所以串口代碼如下所示:
#define PCLK 12000000 // PCLK初始值為12MHz #define UART_CLK PCLK // UART0的時鐘源設為PCLK #define UART_BAUD_RATE 57600 // 波特率 #define UART_BRD ((UART_CLK / (UART_BAUD_RATE * 16)) - 1) /* * 初始化UART0 * 115200,8N1,無流控 */ void uart0_init(void) { GPHCON |= 0xa0; // GPH2,GPH3用作TXD0,RXD0 GPHUP = 0x0c; // GPH2,GPH3內部上拉 ULCON0 = 0x03; // 8N1(8個數據位,無較驗,1個停止位) UCON0 = 0x05; // 查詢方式,UART時鐘源為PCLK UFCON0 = 0x00; // 不使用FIFO UMCON0 = 0x00; // 不使用流控 UBRDIV0 = UART_BRD; // 波特率為115200 } /* * 發送一個字元 */ void putc(unsigned char c) { /* 等待,直到發送緩衝區中的數據已經全部發送出去 */ while (!(UTRSTAT0 & TXD0READY)); /* 向UTXH0寄存器中寫入數據,UART即自動將它發送出去 */ UTXH0 = c; } /* * 列印一串數字 * num:數據 */ void putnum(unsigned long num) //0xFFFF FFFF (7:0) { int i ,start=0; unsigned char c; 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'); }
在調試彙編中,就可以使用:
bl uart0_init //初始化uart ... ... 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-gcc -c -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來實現調試,如下圖所示)