再上一篇:第19章
上一篇:19.2 获取 GNU 工具链
主页
下一篇:19.4 访问特殊功能寄存器
再下一篇:19.5 使用未支持的指令
文章列表

19.3 示例程序

《Cortex-M3 权威指南》,嵌入式处理器开发教程。

让我们开开眼,看一看 GNU工具链下的源代码的众生相。

19.3.1 例 1:第一个程序

作为启蒙,让我们把在第 10章引入的简单程序使用 GCC重写一遍。这个程序计算 10+9+8+…+1
的值,如下所示:

========== example1.s ==========

/* 定义常数 */

.equ STACK_TOP, 0x20000800

.text

.global _start

.code 16

.syntax unified

/* .thumbfunc */

/ * .thumbfunc仅仅在2006Q3-26之前的CodeSourcery工具中需要*/

_start:

.word STACK_TOP, start

.type start, function

/* 主程序入口点 */

start:

movs r0, #10 movs r1, #0

/* 计算 10+9+8...+1 */

loop:

adds r1, r0 subs r0, #1 bne loop

/* Result is now in R1 */

deadloop:

b deadloop

.end

========== end of file ==========

 .word指示字定义 MSP起始值为 0x2000_0800,并且把”start”作为复位向量。
 .text也是一个预定义的指示字,表示从这以后是一个代码区,需要予以汇编。
 .global使_start标号可以由其它目标文件使用。
 .code 16指示程序代码使用 thumb写成。
 .syntax unified指示使用了统一汇编语言语法。
 _start是一个标号,指示出程序区的入口点
 start是另一个标号,它指示复位向量。
 .type start, function宣告了 start是一个函数。对于所有处于向量表中的异常向量,这 种宣告都是必要的,否则汇编器会把向量的 LSB清零——这在 thumb中是不允许的。
 .end指示程序文件的结束。
与 ARM 汇编器不同的是,GNU 汇编器中的标号要以“:”结尾;注释可以使用/*和*/,并且指 示字要以一个”.”作为前缀。
要注意:在 thumb代码(.code 16)里面,复位向量(start)被定义成了一个函数(.type start, function)。这是为了使复位向量的 LSB被强制为 1,从而表示这是以 Thumb状态开始执 行。否则,处理器就会尝试以 ARM态开始,从而引起一个硬 fault。
程序写好后,使用 as来汇编这个源程序,命令格式为:

$> arm-none-eabi-as -mcpu=cortex-m3 -mthumb example1.s -o example1.o

执行了这个命令,就产生了目标文件 example1.o。命令行中的-mcpu 和-mthumb 决定使用的 指令集。接下来执行连接,命令如下

$> arm-none-eabi-ld -Ttext 0x0 -o example1.out example1.o

然后,使用目标拷贝命令(objcopy)来产生二进制文件:

$> arm-none-eabi-objcopy -Obinary example1.out example1.bin

我们还可以使用目标倾倒(dump)命令(objdump)来创建一个反汇编代码来检查生成的目标文
件:

$> arm-none-eabi-objdump -S example1.out > example1.list

生成的反汇编应如下所示:

example1.out: file format elf32-littlearm

Disassembly of section .text:

00000000 <_start>:

0: 0800 lsrs r0, r0, #32

2: 2000 movs r0, #0

4: 0009 lsls r1, r1, #0

...

00000008 <start>:

8: 200a movs r0, #10 a: 2100 movs r1, #0

0000000c <loop>:

c: 1809 adds r1, r1, r0 e: 3801 subs r0, #1

10: d1fc bne.n c <loop>

00000012 <deadloop>:

12: e7fe b.n 12 <deadloop>

19.3.2 例 2:连接多个文件

如前所述,我们可以创建多个目标文件,并且把它们连接到一起。在这个例子里,我们有两个 汇编程序文件,分别是 example2a.s 和 example2b.s。前者只包含向量表,而后者包含了正常的 程序代码。这里,.global指示字就派上用场了——在文件之前传递全局符号。

========== example2a.s ==========

/* 定义常数*/

.equ STACK_TOP, 0x20000800

.global vectors_table

.global start

.global nmi_handler

.code 16

.syntax unified vectors_table:

.word STACK_TOP, start, nmi_handler, 0x00000000

.end

========== end of file ==========

========== example2b.s ==========

/* 主程序 */

.text

.global _start

.global start

.global nmi_handler

.code 16

.syntax unified

.type start, function

.type nmi_handler, function

_start:

/* 主程序入口点*/

start:

movs r0, #10 movs r1, #0

/* 计算 10+9+8...+1 */

loop:

adds r1, r0 subs r0, #1 bne loop

/* 结果存储在R1中 */

deadloop:

b deadloop

/* 为演示而设置的空NMI服务例程 */

nmi_handler:

bx lr

.end

========== end of file ==========

创建可执行映像的步骤为:

1. 汇编 example2a.s

$> arm-none-eabi-as -mcpu=cortex-m3 -mthumb example2a.s -o example2a.o

2. 汇编 example2b.s

$> arm-none-eabi-as -mcpu=cortex-m3 -mthumb example2b.s -o example2b.o

3. 把 2 个目标文件连接成单一的映像。要注意的是,目标文件在命令行中的顺序是重要的,它会 影响在最终的目标文件中,把这两个目标文件的代码编排的顺序。

$> arm-none-eabi-ld -Ttext 0x0 -o example2.out example2a.o example2b.o

4. 产生二进制文件

$> arm-none-eabi-objcopy -Obinary example2.out example2.bin

5. 如上例,可以创建一个反汇编文件来检查所产生目标文件的内容。

$> arm-none-eabi-objdump -S example2.out > example2.list

当目标文件增多时,为简化处理过程,我们可以使用 make 来管理工程。另外,开发套件也常 常有各自内建的功能来简化编译过程。

19.3.3 例 3:一个简单的”Hello World”程序

前两个例子算是热身,现在该动真格的了。让我们试一个“hello world”程序。但是在这里 为了突出主题,我们省去了 UART初始化代码。第 20章给出了一个 C语言写成的 UART示例代码。

========== example3a.s ==========

/* 定义常数 */

.equ STACK_TOP, 0x20000800

.global vectors_table

.global _start

.code 16

.syntax unifi ed vectors_table:

.word STACK_TOP, _start

.end

========== end of file ==========

========== example3b.s ==========

.text

.global _start

.code 16

.syntax unifi ed

.type _start, function

_start:

/* 主程序入口点 */

movs

r0,

#0

movs

r1,

#0

movs

r2,

#0

movs

r3,

#0

movs

r4,

#0

movs

r5,

#0

ldr bl

r0, puts

=hello

movs

r0,

#0x4

bl

putc

deadloop:

b deadloop hello:

.ascii "Hello\n"

.byte 0

.align puts:

/* 该子程序向UART发送字符串 */

/* 入口条件: r0 = 字符串的起始地址 */

/* 字符串要以零结尾 */

push {r0, r1, lr} /* 保存寄存器 */

mov r1, r0 /* 把地址拷贝到R1,因为 */

/* R0 还要用于作putc的参数 */

putsloop:

ldrb.w

r0,

[r1], #1 /* 读取一个字符并且自增地址 */

cbz

r0,

putsloopexit /* 如果字符为NULL,则跳转到结束 */

bl

putc

b putsloop putsloopexit:

pop {r0, r1, pc} /* 返回 */

.equ UART0_DATA, 0x4000C000

.equ UART0_FLAG, 0x4000C018 putc:

/* 该子程序通过UART发送一个字符 */

/* 入口条件: R0 = 要发送的字符 */

push {r1, r2, r3, lr} /* 保存寄存器 */

ldr r1, =UART0_FLAG

putcwaitloop:

ldr

r2,

[r1]

/* 获取状态位 */

tst.w

r2,

#0x20

/* 检查发送缓冲区满标志 */

bne putcwaitloop /* 如果已满则循环等待 */

ldr

r1,

=UART0_DATA /* 否则继续往发送缓冲区里送数据 */

str

r0,

[r1]

pop {r1, r2, r3, pc} /* 返回 */

.end

========== end of file ==========

在这个例子里,我们使用了.ascii 和.byte 指示字来创建一个零结尾的字符串。在定义了字符串 之后,我们又使用了.align 来确保下一条指令会以正确的位置开始。如果不使用.align,汇编器 则可能把下一条指令放到未对齐的地址。
创建目标代码的步骤如下所示,读者应理解下述命令的含义和作用。

$> arm-none-eabi-as -mcpu_cortex-m3 -mthumb example3a.s -o example3a.o

$> arm-none-eabi-as -mcpu_cortex-m3 -mthumb example3b.s -o example3b.o

$> arm-none-eabi-ld -Ttext 0x0 -o example3.out example3a.o example3b.o

$> arm-none-eabi-objcopy -Obinary example3.out example3.bin

$> arm-none-eabi-objdump -S example3.out > example3.list

19.3.4 例 4:把数据放到 RAM中

RW数据需要放到 RAM中,本例就演示在 RAM中定义变量的方法。

========== example4.s ==========

.equ STACK_TOP, 0x20000800

.text

.global _start

.code 16

.syntax unified

_start:

.word STACK_TOP, start

.type start, function start:

movs r0, #10 movs r1, #0

/* 计算10+9…+1 */

loop:

adds

r1,

r0

subs

r0,

#1

bne

loop

/* 结果现在存储到R1中了 */

ldr str

r0, r1,

=result

[r0]

deadloop:

b deadloop

/* 数据区 */

.data

result:

.word 0

.end

========== end of fi le ==========

本例的核心就是粗体的.data指示字。使用它创建一个数据区。在该区中,使用一个.word指 示字来保留一个 4字节的空间,并且取名为 Result(其实 result就相当于 C中的变量名)。欲连 接本程序,需要告诉连接器 RAM 在何处,这可以使用-Tdata 选项来实现,它把数据段设置到所需 的位置上:

$> arm-none-eabi-as -mcpu_cortex-m3 -mthumb example4.s -o example4.o

$> arm-none-eabi-ld -Ttext 0x0 -Tdata 0x20000000 -o example4.out example4.o

$> arm-none-eabi-objcopy -Obinary –R .data example4.out example4.bin

$> arm-none-eabi-objdump -S example4.out > example4.list

还要注意的是,在 objcopy中对-R .data选项的使用。它避免在二进制输出文件中把数据存 储区也包含进去。

19.3.5 例 5:纯 C程序

想必大家已经受够了在汇编下过日子了吧!在 GNU工具链中的一个主要组件就是 C编译器。在 本例中,整个可执行程序——甚至是复位向量和 MSP初值都由 C写成。此外,还添加了一个连接器 脚本,用来把各段放到正确的位置。那么,先让我们看一看 C程序文件。

========== example5.c ==========

#define STACK_TOP 0x20000800

#define NVIC_CCR ((volatile unsigned long *)(0xE000ED14))

// 声明函数原型

void myputs(char *string1); void myputc(char mychar); int main(void);

void nmi_handler(void);

void hardfault_handler(void);

// 定义向量表

attribute__ (( section(“vectors”) )) void (* const VectorArray[])(void) =

{

STACK_TOP, main, nmi_handler,

hardfault_handler

};

// 主程序入口点

int main(void)

{

const char *helloworld[]="Hello world\n";

*NVIC_CCR = *NVIC_CCR | 0x200; /* 设置NVIC的STKALIGN */

myputs(*helloworld);

while(1);

return(0);

}

// 函数

void myputs(char *string1)

{

char mychar;

int j;

j=0;

do

{

mychar = string1[j];

if (mychar!=0)

{

myputc(mychar);

j++;

}

} while (mychar != 0);

return;

}

void myputc(char mychar)

{

#define UART0_DATA ((volatile unsigned long *)(0x4000C000))

#define UART0_FLAG ((volatile unsigned long *)(0x4000C018))

// Wait until busy fl ag is clear while ((*UART0_FLAG & 0x20) != 0);

// Output character to UART

*UART0_DATA = mychar;

return;

}

//空的服务例程

void nmi_handler(void)

{

return;

}

void hardfault_handler(void)

{

return;

}

========== end of file ==========

注意粗体字显示的部分,它使用 attribute(())(注意,是双小括号)来指定特殊的属性。 在这里则指出那个函数指针数组是放到 vectors 段中的。然而,这个 C 程序并没有指定 vectors 段在何处。那么在哪里指定 vectors段的位置呢?现在该请出我们的连接器脚本文件了,工作就在 这里完成。本例的连接器脚本文件为 simple.ld,内容如下:

========== simple.ld ==========

/* MEMORY命令:定义允许的存储器区域 */

/* 本部分定义了连接器允许放入数据的各存储器区域,这是 */

/* 一个可选的功能,但是对于开发很有益,它使连接器在在 */

/* 程序太大时能给你警告 */ MEMORY

{

/* ROM是可读的(r)和可执行的(x) */

rom (rx) : ORIGIN = 0, LENGTH = 2M

/* RAM是可读的(r),可写的(w),可执行的(x) */

ram (rwx) : ORIGIN = 0x20000000, LENGTH = 4M

}

/* SECTIONS 命令 : 定义各输入段到输出段的映射 */

SECTIONS

{

. = 0x0; /* 从0x00000000开始 */

.text : {

*(vectors) /* 向量表 */

*(.text) /* 程序代码 */

*(.rodata) /* 只读数据 */

}

. = 0x20000000; /* 从0x20000000开始 */

.data : {

*(.data) /* 数据存储器 */

}

.bss : {

*(.bss) /* 预留的数据存储器,必须初始化为零 */

}

}

========== end of file ==========

为使用连接脚本,需要在编译阶段把 simple.ld传给编译器。

$> arm-none-eabi-gcc -mcpu_cortex-m3 -mthumb example5.c -nostartfiles

-T simple.ld -o example5.o

然后在连接时,需要再次使用 simple.ld。

$> arm-none-eabi-ld -T simple.ld -o example5.out example5.o

本例中我们只有一个源文件,因此连接过程其实是可以省略的。最后再创建二进制目标文件和
反汇编文件。

$> arm-none-eabi-objcopy -Obinary example5.out example5.bin

$> arm-none-eabi-objdump -S example5.out > example5.list

读者可能还注意到了,在本例中我们使用了另一个称为-nostartfiles的编译器开关。使用它, 就可以让编译器不再往可执行映像中插入启动代码(crt),这样做的目的之一就是减少程序映像的 尺寸。不过,使用该选项的主要原因,其实是在于 GNU工具链的启动代码是与发布包的提供者相关 的,而有些人提供的启动代码不适合 CM3——它们往往是用于传统的 ARM处理器的——如 ARM7(典 型地这些启动代码使用了 ARM代码,而没有使用 Thumb代码)。
但是,在许多情况下,取决于应用程序和使用的库,都必须使用启动代码来执行初始化的过程, 最主要的就是对数据的初始化(例如,把 bss 区的存储单元全部清零)。在最后一个例子中,我们 将演示这个过程。

19.3.6 例 6:纯 C程序,带有标准 C启动代码

在正常情况下,当编译 C程序时,会自动地把标准 C库的启动代码包含在目标文件中,它保证 运行时库得以正确地初始化。标准 C运行时库的启动代码由 GNU工具链提供,但是不同提供者提供 的工具链可能有不同的启动代码。下例是基于 CodeSourceryGNUARM工具链 2006q3-26版本的。 因此,最好检查一下从工具链中的启动代码,或者从供应者处获取最新的启动代码。对于这个版本 的 CodeSourcery 提供的工具链,其启动代码目标文件为 armv7m-crt0.o。但是这个版本提供的 启动代码是错误的——使用了ARM代码来编写。到了2006q3-27及更晚的版本中才修正了这个bug。 不同提供者的 GNU工具链会有不同的启动代码,而且文件名也常常不同。此时,就需要检查你所使 用的 GNU工具链之帮助文档来获取准确信息了。
在编译 C源代码之前,例 5中的 C程序需要一些小改动。缺省情况下,armv7m-crt0已经包含 了一张向量表,并且在它里面,NMI 服务例程和硬 fault 服务例程分别取名为_nmi_isr 和

_fault_isr。因此,需要移除例 5中的向量表,并且重命名 NMI和硬 Fault的服务例程,如下所 示:

// 声明函数原型

void myputs(char *string1); void myputc(char mychar); int main(void);

void _nmi_isr(void);

void _fault_isr(void);

// 主程序入口点

int main(void)

{

const char *helloworld[]="Hello world\n";

myputs(*helloworld);

while(1);

return(0);

}

// 函数

void myputs(char *string1)

{

char mychar;

int j;

j=0;

do

{

mychar = string1[j];

if (mychar!=0)

{

myputc(mychar);

j++;

}

} while (mychar != 0);

return;

}

void myputc(char mychar)

{

#define UART0_DATA ((volatile unsigned long *)(0x4000C000))

#define UART0_FLAG ((volatile unsigned long *)(0x4000C018))

// Wait until busy fl ag is clear while ((*UART0_FLAG & 0x20) != 0);

// Output character to UART

*UART0_DATA = mychar;

return;

}

//空的服务例程

void _nmi_isr(void)

{

return;

}

void _fault_isr(void)

{

return;

}

在安装了 CodeSourcery后,已经包含了一系列的连接脚本,可以从 codesourcery/sourcery

g++/arm-none-eabi/lib目录下找到。在下例中,我们就使用了 lm3s8xx-rom.ld文件。这个连 接器脚本顾名思义,是用于 LM3S8XX系列芯片的。

在当前目录之外,当C程序代码定位后,一个名为“lib”的库子目录也在在当前目录下创建,
(Aside from the current directory, when the C program code is located, a library subdirectory called lib is also created in the current directory)这使得库搜索路径的设置更加简单——所需的目标文件 armvrm-crt0.o以及连接器脚本都被拷贝到这个“lib”目录下。在下一个例子中,我们就使用-Llib 选项来把“lib”添加到库的搜索路径中。
现在我们可以编译这个C程序了:

$> arm-none-eabi-gcc –mcpu=cortex-m3 -mthumb example6.c -L lib –T

lm3s8xx-rom.ld -o example6.out

执行了上条命令后,就创建并且连接了目标文件example6.out。因为只有一个目标文件,二 进制文件可以直接由它来生成:

$> arm-none-eabi-objcopy -Obinary example6.out example6.bin

产生反汇编的方式则与上例相同:

$> arm-none-eabi-objdump -S example6.out > example6.list