在裸板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() : {*(.rodata)} //指定只读数据段 .data ALIGN() : { *(.data) } //指定读写数据段, *(data):添加所有文件的数据段 __bss_start = .; //把__bss_start赋值为当前地址位置,即bss段的开始位置 .bss ALIGN() : { *(.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, = @设置堆栈
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 @. 目标地址=0x30000000,这是SDRAM的起始地址
mov r1, # @. 源地址 =
ldr r2, =__bss_start
sub r2,r2,r0 @. 复制长度= __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 = ;
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 < ; 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[] =0x22011110, //BWSCON
p[] =0x00000700, //BANKCON0
p[] =0x00000700, //BANKCON1
p[] =0x00000700, //BANKCON2
p[] = 0x00000700, //BANKCON3
p[] =0x00000700, //BANKCON4
p[] =0x00000700, //BANKCON5
p[] =0x00018005, //BANKCON6
p[] = 0x00018005, //BANKCON7
p[] =0x008C07A3, //REFRESH
p[] =0x000000B1, //BANKSIZE
p[] = 0x00000030, //MRSRB6
p[] =0x00000030, //MRSRB7
}

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

如下图,我们以上面的代码p[0]  =0x22011110为例:

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

3.1点灯法:

LED_SHOW:
ldr r0, =0x56000050
ldr r1, =(<<(*)) @设置GPFCON寄存器的GPF4为输出引脚
str r1, [r0]
ldr r0, =0x56000054 @GPFDAT寄存器
ldr r1, = @设置GPF4=,亮灯
ldr r2, =(<<) @设置GPF4=,灭灯 LED_LOOP: @死循环闪灯 str r1, [r0] @亮灯
bl DELAY
str r2, [r0] @灭灯
bl DELAY
b LED_LOOP DELAY: @延时 ldr r3,=
:
sub r3, r3, #
cmp r3, #
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=;
unsigned char c;
uart0_init();
for(i=;i<;i++); putc('');
putc('x');
for(i=;i>=;i--) //从[7:0]中打印数字,去除有效数字前面的0
{
c=( num >> (i*) )&0xf;
if(c!=)
{
if(c>)
putc(c-+'A'); //打印A~F
else
putc(c+''); //打印1~9
if(!start) start=; //start=1,说明为有效数字
}
else if((start||!i)&&c==) //若是有效数字,便打印0,且在个位上时,不管是否有效都要打印
{
putc('');
}
}
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                //烧写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:     //在main.c文件的第21行处打断点

info br

查看断点

delete <num>

删除第几个断点,如下图所示:

c

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

step

单步执行

monitor  <cmd...>

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

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

quit

退出


(PS:也可以通过eclipse平台软件来调用GDB,GDB最终转换为命令行,再调用OCD来实现调试,如下图所示)

裸板驱动总结(makefile+lds链接脚本+裸板调试)的更多相关文章

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

    在裸板2440中,当我们使用nand启动时,2440会自动将前4k字节复制到内部sram中,如下图所示: 然而此时的SDRAM.nandflash的控制时序等都还没初始化,所以我们就只能使用前0~40 ...

  2. makefile使用.lds链接脚本以及 $@ ,$^, $,< 解析

    先来分析一个简单的.lds链接脚本 例1,假如现在有head.c init.c nand.c main.c这4个文件: 1.1 首先创建链接脚本nand.lds: SECTIONS { firtst ...

  3. makefile使用.lds链接脚本以及 $@ ,$^, $,< 解析【转】

    转自:http://www.cnblogs.com/lifexy/p/7089873.html 先来分析一个简单的.lds链接脚本 例1,假如现在有head.c init.c nand.c main. ...

  4. [转]Linux下的lds链接脚本详解

    转载自:http://linux.chinaunix.net/techdoc/beginner/2009/08/12/1129972.shtml     一. 概论 每一个链接过程都由链接脚本(lin ...

  5. Linux下的lds链接脚本简介

    转载:http://hubingforever.blog.163.com/blog/static/171040579201192472552886/   一. 概论 每一个链接过程都由链接脚本(lin ...

  6. Linux下的lds链接脚本详解【转】

    转自:http://www.cnblogs.com/li-hao/p/4107964.html 转载自:http://linux.chinaunix.net/techdoc/beginner/2009 ...

  7. Linux下的lds链接脚本详解

    1. 概论2. 基本概念3. 脚本格式4. 简单例子5. 简单脚本命令6. 对符号的赋值7. SECTIONS命令8. MEMORY命令9. PHDRS命令10. VERSION命令11. 脚本内的表 ...

  8. Linux下的lds链接脚本简介(二)

    七. SECTIONS命令 SECTIONS命令告诉ld如何把输入文件的sections映射到输出文件的各个section: 如何将输入section合为输出section; 如何把输出section ...

  9. Linux下的lds链接脚本基础

    转载:http://soft.chinabyte.com/os/104/12255104.shtml   今天在看uboot引导Linux部分,发现要对链接脚本深入了解,才能知道各个目标文件的内存分布 ...

随机推荐

  1. jQuery 常用操作(转)

    一.书写规则 支持链式操作: 在变量前加"$"符号(var $variable = jQuery 对象): 注:此规定并不是强制要求. 二.寻找元素 选择器 基本选择器 层级选择器 ...

  2. LeetCode 562. Longest Line of Consecutive One in Matrix(在矩阵中最长的连续1)$

    Given a 01 matrix M, find the longest line of consecutive one in the matrix. The line could be horiz ...

  3. 正则和grep——再做正则就去死

    grep 文本过滤工具 基本正则表达式 grep 语法 基本正则表达式的元字符 次数匹配 位置锚定 分组 扩展正则表达式 基本正则表达式的元字符 次数匹配 位置锚定 分组 或者 grep的介绍 lin ...

  4. Unity3D手机斗地主游戏开发实战(01)_发牌功能实现

    园子荒废多年,闲来无事,用Unity3D来尝试做一个简单的小游戏,一方面是对最近研究的Unity3D有点总结,一方面跟广大的园友相互学习和提高.话不多说,进入正题~ 一.创建项目 1.创建Unity2 ...

  5. 1016: [JSOI2008]最小生成树计数

    1016: [JSOI2008]最小生成树计数 Time Limit: 1 Sec  Memory Limit: 162 MBSubmit: 6200  Solved: 2518[Submit][St ...

  6. 使用(Unicode字符)让inline水平元素换行

      为了实现上面效果: <dl>     <dt>提问:</dt><dd>为什么没有男朋友?</dd> </dl> <dl ...

  7. 淘宝轮播JS

    taobao首页轮播原生js面对对象封装版 Author:wyf 2012/2/25

  8. 《Linux命令行与shell脚本编程大全》 第六章环境变量

    很多程序和脚本都通过环境变量来获取系统信息.存储临时数据和配置信息. 6.1 什么是环境变量: bash shell用一个叫环境变量(environment variable)的特性来存储有关shel ...

  9. SVN服务迁移备份操作步骤

    SVN服务备份操作步骤 1.准备源服务器和目标服务器 源服务器:192.168.1.250 目标服务器:192.168.1.251 root/rootroot 2.对目标服务器(251)装SVN服务器 ...

  10. params修饰符

    http://msdn.microsoft.com/zh-cn/library/w5zay9db.aspx params 关键字可以指定采用数目可变的参数的方法参数. 可以发送参数声明中所指定类型的逗 ...