由前幾章可知,JZ2440內存控制器能夠直接訪問SDRAM、NOR Flash、SRAM(片內4K內存),以及各類控制器(包括Nand Flash控制器),可是不能直接訪問Nand Flash。html
此時就涉及2個問題:linux
解決辦法以下:c++
代碼段
拷貝到SDRAM中,而後讓CPU在SDRAM上繼續執行。數據段
(保存全局變量和靜態變量)拷貝到SDRAM中。因此,不管是Nor啓動,仍是Nand啓動,咱們都須要拷貝代碼到SDRAM中。數組
把一個程序從一個位置移動到另外一個位置,稱之爲
重定位
。sass
main.cmarkdown
#include "uart.h"
void delay(volatile int d) {
while (d--);
}
char g_Char = 'A'; //定義一個全局變量
const char g_Char2 = 'B'; //定義固定的全局變量
int g_A = 0;
int g_B;
int main(void) {
uart0_init();
while(1)
{
putchar(g_Char);
g_Char++;
delay(1000000);
}
return 0;
}
複製代碼
其他的Makefile、start.S、uart.c、uart.h都與前章基本一致。此時編譯出的bin文件大小有33k,顯然是不對的。 查看反編譯dis文件:app
代碼中,各個段的含義:工具
咱們能夠清楚地發現:text段到data段之間,有一片很大的空白區域,咱們稱之爲黑洞區
。oop
爲了減小黑洞區
的大小,使得bin文件小於4k,讓其可以支持Nand啓動,只須要修改Makefile,手動指定data段的位置爲0x800:測試
all:
arm-linux-gcc -c -o uart.o uart.c
arm-linux-gcc -c -o main.o main.c
arm-linux-gcc -c -o start.o start.S
arm-linux-ld -Ttext 0 -Tdata 0x800 start.o uart.o main.o -o sdram.elf
arm-linux-objcopy -O binary -S sdram.elf sdram.bin
arm-linux-objdump -D sdram.elf > sdram.dis
clean:
rm *.bin *.o *.elf *.dis
複製代碼
而後編譯,發現bin文件變爲3k大小,如何分別燒寫到Nor Flash和Nand Flash,並啓動,觀察打印內容:
Nor啓動時,打印AAAA......
Nand啓動時,打印ABCD......
複製代碼
可見:
Nor Flash只能像普通內存那樣讀,而不能像普通內存那樣寫。
爲何經過Nor啓動和Nand啓動打印不同呢?咱們來分析一下緣由。
當經過Nor啓動時,0地址爲Nor Flash的基地址,全局變量g_Char被放在0x800的地方,也屬於Nor Flash的範圍,因此此時g_Char++會無效。
當經過Nand啓動時,0地址爲SRAM(片內4k內存)的基地址,CPU上電後,硬件會將Nand Flash前4k內容所有拷貝到SRAM中,CPU開始從SRAM執行。而此時全局變量g_Char也被拷貝到SRAM中,因此此時g_Char++有效。
爲了解決Nor Flash裏面的變量不能寫的問題,咱們須要把變量所在的數據段放在SDRAM中。 但是若是隻是簡單的修改Makefile,指定數據段爲0x30000000,以下:
arm-linux-ld -Ttext 0 -Tdata 0x30000000 start.o uart.o main.o -o sdram.elf
複製代碼
編譯後,bin文件有700多M,這麼大的黑洞區
,誰都難以忍受。
那如何才能將數據段在bin文件中,緊靠代碼段放置,而在運行時,放到0x30000000的地方?
有如下兩個辦法:
把數據段的g_Char和代碼段靠在一塊兒;
燒寫在Nor Flash上面;
運行時把g_char(全局變量)複製到SDRAM,即0x3000000位置;
讓文件直接從0x30000000開始,全局變量在0x3......;
燒寫Nor Flash上 0地址處;
運行會把整個代碼段數據段(整個程序)從0地址複製到SDRAM的0x30000000;
上面兩個方法的區別在於:前者只重定位了數據段,後者重定位了整個程序。
若是想使用重定位,須要使用連接腳本 Using LD, the GNU linker
若是咱們不使用連接腳本,bin文件中,代碼存放的順序,是按照連接順序來存放的。
修改Makefile
all:
arm-linux-gcc -c -o init.o init.c
arm-linux-gcc -c -o uart.o uart.c
arm-linux-gcc -c -o main.o main.c
arm-linux-gcc -c -o start.o start.S
#arm-linux-ld -Ttext 0 -Tdata 0x800 start.o uart.o main.o -o sdram.elf
arm-linux-ld -T sdram.lds start.o init.o uart.o main.o -o sdram.elf
arm-linux-objcopy -O binary -S sdram.elf sdram.bin
arm-linux-objdump -D sdram.elf > sdram.dis
clean:
rm *.bin *.o *.elf *.dis
複製代碼
建立連接腳本sdram.lds:
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x800) { *(.data) }
.bss : { *(.bss) *(.COMMON)}
}
複製代碼
SECTIONS中,表示數據的組織形式:
.text
表示存放代碼段,0
表示將內容放入0地址,{*(.text)}
表示全部文件的代碼段。因此這句話的意思是:把全部文件的代碼段放到0地址。.rodata
表示存放只讀數據段,緊跟在.text
內容後面,{ *(.rodata) }
將表示全部文件的只讀數據段。咱們編譯源碼,查看反編譯內容:
Disassembly of section .rodata:
000003a0 <g_Char2>:
3a0: Address 0x000003a0 is out of bounds.
Disassembly of section .data:
30000000 <g_Char>:
30000000: Address 0x30000000 is out of bounds.
Disassembly of section .bss:
30000004 <g_A>:
30000004: 00000000 andeq r0, r0, r0
30000008 <g_B>:
30000008: 00000000 andeq r0, r0, r0
複製代碼
能夠看到,數據段中的g_Char變量,地址爲0x30000000,但是咱們bin文件只有3k大小。也就是說:
這些代碼雖然存放在bin文件的0x800的位置,可是,後面這些代碼會被在0x30000000位置進行執行。
那如何將這些存放在0x800的代碼"搬運"到0x30000000呢? 這就須要咱們手動去完成,修改start.S:
.text
.global _start
_start:
/* 省略如下代碼:
一、關閉看門狗
二、設置時鐘
三、設置棧指針
*/
bl sdram_init
/* 重定位data段,只copy 32位(4字節) */
mov r1, #0x800
ldr r0, [r1]
mov r1, #0x30000000
str r0, [r1]
bl main
halt:
b halt
複製代碼
因爲須要從data段拷貝4字節到SDRAM,因此須要提早初始化SDRAM。
再次編譯,而後燒寫到Nor和Nand Flash中,程序都正常執行。
上個程序中,咱們只重定位data段的4個字節,可是,當咱們程序有多個全局變量,就會出現問題:
main.c
#include "uart.h"
void delay(volatile int d) {
while (d--);
}
char g_Char1 = 'A'; //定義一個全局變量
char g_temp1 = '-';
char g_temp2 = '-';
char g_Char2 = 'a';
char g_Char3 = '1'; //定義一個全局變量
// 因爲只copy 4字節,因此g_Char3無效
const char g_Char = 'B'; //定義固定的全局變量
int g_A = 0;
int g_B;
int main(void) {
uart0_init();
while(1)
{
putchar(g_Char1);
putchar(g_Char2);
putchar(g_Char3);
g_Char1++;
g_Char2++;
g_Char3++;
delay(1000000);
}
return 0;
}
複製代碼
運行上面程序,發現g_Char一、g_Char2運行正常,而g_Char3運行不正確,這是由於咱們未將g_Char3的內容重定位到SDRAM。
此時,咱們須要修改連接腳本,使得其能重定位多個字節。
sdram.lds
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x800)
{
data_load_addr = LOADADDR(.data);
data_start = . ;
*(.data)
data_end = . ;
}
.bss : { *(.bss) *(.COMMON)}
}
複製代碼
LOADADDR
宏,能夠獲得data段在bin文件的地址,即加載地址。
而data_start = .
,表示運行地址。
也就是說,咱們只需將data_load_addr
的內容copy到data_start
上,拷貝長度爲data_end - data_start
。
從新修改start.S重定位的內容:
.text
.global _start
_start:
/* 省略如下代碼:
一、關閉看門狗
二、設置時鐘
三、設置棧指針
*/
bl sdram_init
/* 重定位data段 */
ldr r1, =data_load_addr /* data段在bin文件中的地址,加載地址 */
ldr r2, =data_start /* data段在重定位地址,運行時的地址 */
ldr r3, =data_end /* data段結束地址 */
cpy:
ldrb r4, [r1]
strb r4, [r2]
add r1, r1, #1
add r2, r2, #1
cmp r2, r3
bne cpy
bl main
halt:
b halt
複製代碼
編譯並燒寫,運行後,程序輸出預期結果。
連接腳本的通用格式以下:
SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
...
}
複製代碼
secname
,段名,這裏能夠隨便取start
,起始地址:運行時地址,重定位地址ldadr
,加載地址,默認對於運行時地址。contents
,內容:能夠直接指定某個文件(如
start.o
),把整個文件放在整個段中。
也能夠指定某個段(如*(.text)
),把全部文件的這個段放到該段中。
或者start.o *(.text)
,先存放start.o整個文件,再將其他文件的代碼段存入。
對應裸板,是JTAG調試工具 對於app,加載器也是app
BSS段:用來存放爲無初值或初值爲0的全局變量。
上節中,咱們重定位了數據段,但沒有清除BSS段。這會致使咱們訪問這些變量時,會獲得髒數據,因此咱們須要手動清除BSS段。
注意:bss段並不會保存在bin文件和elf文件中,由於這樣作是毫無心義的。
sdram.lds
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x800)
{
data_load_addr = LOADADDR(.data);
. = ALIGN(4);
data_start = . ;
*(.data)
data_end = . ;
}
. = ALIGN(4);
bss_start = .;
.bss : { *(.bss) *(.COMMON)}
bss_end = .;
}
複製代碼
start.S
.text
.global _start
_start:
/* 省略如下代碼:
一、關閉看門狗
二、設置時鐘
三、設置棧指針
*/
bl sdram_init
/* 重定位data段 */
ldr r1, =data_load_addr /* data段在bin文件中的地址,加載地址 */
ldr r2, =data_start /* data段在重定位地址,運行時的地址 */
ldr r3, =data_end /* data段結束地址 */
cpy:
ldr r4, [r1]
str r4, [r2]
add r1, r1, #4
add r2, r2, #4
cmp r2, r3
ble cpy
/* 清除BSS段 */
ldr r1, =bss_start
ldr r2, =bss_end
mov r3, #0
clean:
str r3, [r1]
add r1, r1, #4
cmp r1, r2
ble clean
bl main
halt:
b halt
複製代碼
這裏咱們作了如下優化:
假如咱們不作4字節對齊操做,那麼在清除bss段時,假如清除的地址不是4字節的整數倍,CPU會向下取整,可能會清除別的段中的數據。
因此在連接腳本中,咱們添加了. = ALIGN(4);
,用於段的4字節對齊。
打印bss段中的變量,看看清除是否有效:
uart.c
//......
void printHex(unsigned int val) {
int i;
unsigned char arr[8];
/* 先取每一位值 */
for (i = 0; i < 8; i++)
{
arr[i] = val & 0xf;
val >>= 4;
}
/* 打印 */
puts("0x");
for (i = 7; i >=0; i--)
{
if (arr[i] <= 9)
{
putchar(arr[i] + '0');
}
else
{
putchar(arr[i] - 10 + 'A');
}
}
}
複製代碼
main.c
#include "uart.h"
void delay(volatile int d) {
while (d--);
}
char g_Char1 = 'A'; //定義一個全局變量
char g_temp1 = '-';
char g_temp2 = '-';
char g_Char2 = 'a';
char g_Char3 = '1'; //定義一個全局變量
//因爲只copy 4字節,因此g_Char3無效
const char g_Char = 'B'; //定義固定的全局變量
int g_A = 0;
int g_B;
int main(void) {
uart0_init();
puts("\n\rg_A = ");
printHex(g_A);
puts("\n\r");
while(1)
{
putchar(g_Char1);
putchar(g_Char2);
putchar(g_Char3);
g_Char1++;
g_Char2++;
g_Char3++;
delay(1000000);
}
return 0;
}
複製代碼
運行後,結果與預期一致:g_A = 0x00000000
。
以前也介紹過,解決黑洞區有2種方法:
一、只重定位數據段。
二、重定位整個程序。
在前面章節中,咱們都是使用的第一種方法,但第二種方法其實對應Linux開發板來講,更加適用,緣由以下:
一、Linux不一樣於單片機,它對內存大小沒那麼高的要求。
二、第一種方式只適用於可以運行程序的Flash,假如從Nand Flash、SD卡加載運行,就只能用第二種方式,將程序拷貝到內存中運行。
三、JTAG調試工具只支持第二種連接腳本,不支持第一種分體式的連接腳本。
sdram.lds
SECTIONS
{
. = 0x30000000;
. = ALIGN(4);
.text : { *(.text) }
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}
複製代碼
這裏直接指定程序開始的運行地址爲0x30000000。
start.S
.text
.global _start
_start:
/* 省略如下代碼:
一、關閉看門狗
二、設置時鐘
三、設置棧指針
*/
bl sdram_init
/* 重定位text, rodata, data段 */
mov r1, #0
ldr r2, =_start /* 第1條指令運行時的地址 */
ldr r3, =__bss_start /* bss段的起始地址 */
cpy:
ldr r4, [r1]
str r4, [r2]
add r1, r1, #4
add r2, r2, #4
cmp r2, r3
ble cpy
/* 清除BSS段 */
ldr r1, =__bss_start
ldr r2, =_end
mov r3, #0
clean:
str r3, [r1]
add r1, r1, #4
cmp r1, r2
ble clean
bl main
halt:
b halt
複製代碼
上述的代碼在運行時,前面部分會將整個程序複製到SDRAM,並清除BSS段。可是在編譯腳本里,整個程序的運行地址被指定到SDRAM上,這就引出一個問題:
爲何當咱們經過Nor啓動或Nand啓動時,運行地址爲SDRAM的程序,也能在Nor Flash或SRAM上正常運行?
此時CPU依舊運行在原先的內存芯片上,並未在SDRAM上執行。也就是說:
這就意味着,前面這部分的代碼,必須與位置無關。查看反編譯dis文件:
裏面有一行代碼eb000018 bl 300000c4 <sdram_init>
,但是,此時SDRAM並未初始化,訪問300000c4
確定會出問題,這是怎麼回事呢?
原來,這句代碼其實並無直接跳到300000c4
,而是跳到pc + offset
位置,即該條指令與位置無關,不管放到哪一個位置執行,都能正確被執行。
咱們能夠修改連接腳本的運行地址爲0x32000000
,這條指令的變成eb000018 bl 320000c4 <sdram_init>
,機器碼並無改變,剛好驗證咱們的猜想。
在dis文件中,
bl 0x3xxxxxxx
只是起方便查看的做用,而不是真正的跳轉到這個地址上。
那怎麼寫位置無關的程序?
不可訪問全局變量、靜態變量。
不可訪問有初始值數組(初始化值存放到rodata中,使用絕對地址來訪問)
ldr pc, =xxx
,跳轉到Runtime Add,好比:ldr pc, =main
因此如今,咱們須要讓CPU在SDRAM上執行,而不是原先的內存芯片上。這是咱們只須要將相對跳轉指令:
bl main // bl相對跳轉,程序仍在NOR/sram執行
複製代碼
修改成絕對跳轉指令:
ldr pc, =main // 絕對跳轉,跳到SDRAM
複製代碼
編譯燒寫運行,發現程序運行速度明顯變快。
start.S
.text
.global _start
_start:
/* 省略如下代碼:
一、關閉看門狗
二、設置時鐘
三、設置棧指針
*/
bl sdram_init
bl copy2sdram
bl clean_bss
bl uart0_init
//bl main /*bl相對跳轉,程序仍在NOR/sram執行*/
ldr pc, =main /*絕對跳轉,跳到SDRAM*/
halt:
b halt
複製代碼
init.c
void copy2sdram() {
extern int _start, __bss_start;
unsigned int* src = (unsigned int*)0;
unsigned int* start = (unsigned int*)&_start;
unsigned int* end = (unsigned int*)&__bss_start;
while(start < end)
{
*start++ = *src++;
}
}
void clean_bss() {
extern int __bss_start, _end;
unsigned int* start = (unsigned int*)&__bss_start;
unsigned int* end = (unsigned int*)&_end;
while(start < end)
{
*start++ = 0;
}
}
複製代碼
這裏,咱們在c語言中使調用了lds連接腳本和start.S啓動文件中的變量。這些變量,被保存在了symbol table符號表中。
這些變量,在彙編代碼中能夠直接使用,而在c語言裏,須要經過extern
關鍵字引入,而後取址得到。
main.c
#include "uart.h"
void delay(volatile int d) {
while (d--);
}
char g_Char1 = 'A'; //定義一個全局變量
char g_temp1 = '-';
char g_temp2 = '-';
char g_Char2 = 'a';
char g_Char3 = '1'; //定義一個全局變量
//因爲只copy 4字節,因此g_Char3無效
const char g_Char = 'B'; //定義固定的全局變量
int g_A = 0;
int g_B;
int main(void) {
puts("\n\rg_A = ");
printHex(g_A);
puts("\n\r");
while(1)
{
putchar(g_Char1);
putchar(g_Char2);
putchar(g_Char3);
g_Char1++;
g_Char2++;
g_Char3++;
delay(1000000);
}
return 0;
}
複製代碼
編譯燒寫運行 ,一切正常。