由前几章可知,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;
}
复制代码
编译烧写运行 ,一切正常。