开发高效率的程序涉及很多方面,包括编程风格、算法实现、针对目标的特 殊优化等等。这部分主要从 ARM 的体系结构特点出发,介绍几个程序开发中的注意点。如何根据目标硬件的存储器配置,对运行程序的映像文件进行优化布局 的方法,在前面的专题中已经有过介绍。<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
Tag:嵌入式系统 嵌入式程序开发 arm
1. 变量定义 变量定义虽然很简单,但是也有很多值得注意的地方。
第二个问题是局部变量的类型定义。一般情况下人们总是设法使用 short 或 char 来定义变量以节省存储器空间;但是,当一个函数的局部变量数目有限的情 况下,编译器会把局部变量分配给内部寄存器,每个变量占用一个寄存器。这样, 使用 short 和 char 型变量不但起不到节省空间的作用,还会带来其他的副作用,请看图 2。假定 a1 是任意可能的寄存器,存储函数的局部变量。同样完成加一 的操作,32 位的 int 型变量最快,只用一条加法指令。而 8 位和 16 位变量,完 成加法操作后,还需要在 32 位的寄存器中进行符号扩展,其中带符号的变量, 要用逻辑左移(LSL)接算术右移(ASR)两条指令才能完成符号扩展;无符号 的变量,要使用一条逻辑与(AND)指令对符号位进行清零。所以,使用 32 位 的 int 或 unsigned int 局部变量最有效率。某些情况下,函数从外部存储器读入局 部变量进行计算,这时候,往往值得先把不是 32 位的变量转换成 32 位(至于把8 位或 16 位变量扩展成 32 位后,隐藏了原来可能的溢出异常这个问题,需要进
一步的仔细考虑)。
变量定义中还有一个与习惯思维相悖的地方是冗于局部变量的使用。一般情况下程序员总是竭力避免使用冗余变量,以精简程序。通常情况下这是正确的, 但是也有例外,请看下面一个例子:
int | f(void); |
|
int | g(void); | // f()和 g()不访问全局变量errs |
int | errs; | // 全局变量 |
void test1(void)
{ errs += f();
errs += g();
}
void test2(void)
{ int localerrs = errs; // 定义冗余的局部变量
localerrs += f();
localerrs += g();
errs = localerrs;
}
在第一种情况 test1()里,每次访问全局变量 errs 时都要先从相应的存储器 load 到寄存器里,经 f()或 g()函数调用后再 store 回原来的存储器里面,在 这个例子里一共要进行两次这样的 load/store 操作。而在第二种情况 test2()里, 局部变量 localerrs 被分配以寄存器,这样一来,整个函数就只需要一次 load/store 全局变量存储器了;能够节省存储器访问的次数对于系统性能的提高是非常有好处的。
这个例子说明了增加局部变量可以减少存储器的访问。
2. 参数传递
在 ARM 的工具链里,定义了统一的函数过程调用标准 ATPCS(ARM-Thumb Procedure Call Standard)。ATPCS 定义了寄存器组中的{R0 – R3}作为参数传递和 结果返回寄存器,如果参数数目超过四个,则使用堆栈进行传递。我们知道内部 寄存器的访问速度是远远大于存储器的,所以要尽量使参数传递在寄存器里面进行,即应尽量控制函数的参数在四个以下。这是理解 ATPCS 后应该实现的一种 编程风格。但是,利用 ATPCS 我们还可以得到更多,我们可以用它来实现 C 与 汇编之间直接的函数调用。见图 3 中的例子:
从 C 中直接调用汇编函数
extern void strcopy(char *d, const char *s);
int main(void)
{
const char *src = “Source”;
char dest[10];
...
strcopy(dest, src);
...
}
AREA StrCopy, CODE, READONLY EXPORT strcopy
strcopy
LDRB R2, [R1], #1
STRB R2, [R0], #1
CMP R2, #0
BNE strcopy
MOV PC, LR
END
这个例子中的函数 strcopy(dest, src)用汇编来实现,根据 ATPCS 的定义, 函数参数从左到右由寄存器进行传递,所以在汇编中可以直接由 R0 和 R1 进行 引用。有了这条途径,在 C 和汇编之间进行相互调用就容易实现了。
3. 循环条件
记数循环是程序中十分常用的流程控制结构。在 C 中,类似下面的 for 循环 比比皆是:
for (loop = 1; loop <= limit; loop++)
这种累加计数的方法符合一般的自然思维习惯,所以比下面的递减计数方法使用 更多:
for (loop = limit; loop != 0; loop--)
这两者在逻辑上并没有效率差异,但是映射到具体的体系结构中,就产生了
因此,在 ARM 的体系结构下编程,最好采用递减至零的方法来设置循环条 件。
4. 条件执行
上面已经提及了 ARM 指令的条件执行,充分利用这个特性,可以有助于缩短代码长度,优化流程控制。
5. 混合编程
汇编和 C/C++语言的混合编程,在一个追求效率的程序中是比较常见的。前 面已经讲到过在汇编和 C/C++之间进行函数调用时,要遵循 ATPCS 的定义。这 里介绍在 C/C++里加入汇编程序的两种方法:内联汇编(Inline Assemble)和嵌 入式汇编(Embedded Assemble)。
内联汇编是指在 C/C++函数定义中插入汇编语句的方法,如下面的例子:
void enable_IRQ(void)
{
int tmp;
<?xml:namespace prefix = v ns = "urn:schemas-microsoft-com:vml" /><?xml:namespace prefix = w ns = "urn:schemas-microsoft-com:office:word" />asm // 内联汇编定义
{
MRS tmp, CPSR // 可以引用外部的 C 变量定义
BIC tmp, tmp, #0x80
MSR CPSR_c, tmp
}
}
内联汇编的用法跟真实汇编之间有很大的区别,并且不支持 Thumb,在内联
汇编之中不能直接访问物理寄存器(CPSR 除外),即使使用寄存器名进行编程, 也会被编译器进行重新分配。
与内联汇编不同,嵌入式汇编具有真实汇编的所有特性,同时支持 ARM 和 Thumb,但是不能直接引用 C/C++的变量定义,数据交换必须通过 ATPCS 进行。 嵌入式汇编在形式上表现为独立定义的函数体,如下所示:
asm int add(int i, int j) // 定义嵌入式汇编
{
ADD R0, R0, R1 // Value of i in R0 and j in R1, result in R0
MOV PC, LR
}
void main()
{
printf("12345 + 67890 = %d\n", add(12345, 67890));
}
灵活使用内联汇编和嵌入式汇编,可以帮助提高程序效率。
6. 性能分析 很多时候需要对程序的执行效率和性能进行分析,直接测试当然是最真实的
途径,但是这种方法除了在运行时间上进行定量外,很难得到确切的数据信息。 而指令集仿真这种方法(ARMulator 或 ISS),恰恰为程序执行过程中处理器的 行为提供了一个参数统计方法。
通过在感兴趣的代码段两端设置参考点,把执行过程中处理器的各种状态周 期精确地统计出来,作为性能分析最直接的分析数据。图 6 中所示是一段例程在 ARM7TDMI 上面运行的状态统计,可以计算出平均每条指令花费的处理器时钟 为 1.9 左右,进行代码优化时,目标就是减少非顺序访问周期和内部等待周期数。
7. 小结 代码优化是个很大的题目,这里只是抛砖引玉,索引几个要点进行讨论。更
多这方面的知识,可以参阅这方面的众多论述和著作。
至此,关于在 ARM 体系结构下进行嵌入式系统编程的 6 个专题已经全部结 束。对此感兴趣的读者可以访问 http://www.armodm.com 进一步了解更多有关 ARM 的技术资料。
文章评论(0条评论)
登录后参与讨论