作者:宫易政 时间:2012年4月4日
引言:最近在完成“用MicroBlaze下的嵌入式C编写testbench实现对芯片功能验证”的过程中查阅了FPGA的EDK的相关知识、MicroBlaze的相关知识、嵌入式C的主要语法、编程技巧等,获得了很多知识,并成功应用到实验中,顺利完成了预期的任务。现将读书总结和任务经验列于下,目的一是将某些想法和技巧列于笔端,检验当前的掌握程度,二是不断总结之前的经验为以后的相关工作做准备,三是为他人的相关工作做“铺路人”。
MicroBlaze 是基于Xilinx公司FPGA的微处理器IP核,和其它外设IP核一起,可以完成可编程系统芯片(SOPC)的设计。MicroBlaze 处理器采用RISC架构和哈佛结构的32位指令和数据总线,可以全速执行存储在片上存储器和外部存储器中的程序,并访问其的数据。
MicroBlaze内部有32个32位通用寄存器和2个32位特殊寄存器—PC指针和MSR状态标志寄存器。为了提高性能,MicroBlaze还具有指令和数据缓存。所有的指令字长都是32位,有3个操作数和2种寻址模式。指令按功能划分有逻辑运算、算术运算、分支、存储器读/写和特殊指令等。指令执行的流水线是并行流水线,它分为3级流水:取指、译码和执行。
MicroBlaze可以响应软件和硬件中断,进行异常处理,通过外加控制逻辑,可以扩展外部中断。利用微处理器调试模块(MDM)IP核,可通过JTAG接口来调试处理器系统。多个MicroBlaze处理器可以用1个MDM来完成多处理器调试。
MicroBlaze处理器具有8个输入和8个输出快速单一链路接口(FSL)。FSL通道是专用于单一方向的点到点的数据流传输接口。FLS和MicroBlaze的接口宽度是32位。每一个FSL通道都可以发送和接收控制或数据字。
在由电源、MicroBlaze以及FPGA构成的主板和内存所组成的最小系统下,我们就可以像在PC机上使用C语言一样进行编程,从而对硬件进行操作。硬件可以是系统内存,也可以是双端口RAM、 SRAM等。只要该外设与最小系统的总线相连,那么我们就可以通过指针的方式来完成各种操作,这也是为何只有高级语言C语言才可以用来编写嵌入式程序。
由C语言代码形成可执行程序(二进制文件),需要经过编译-汇编-连接三个阶段。编译过程把C语言文本文件生成汇编程序,汇编过程把汇编程序形成二进制机器代码,连接过程则将各个源文件生成的二进制机器代码文件组合成一个文件。C语言编写的程序经过编译-连接后,将形成一个统一文件,它由几部分组成。在程序运行时又会产生其他几个部分,各个部分代表了不同的存储区域。
这些区域包括代码段(Code)、只读数据段(RODATA)、已初始化读写数据段(RWDATA)、未初始化数据段(BSS)、堆(heap)、栈(stack)。
其中Code、RO DATA、RW DATA、BSS属于静态存储区,这类内存直到程序运行完成才进行释放,例如我们经常使用的关键字static、const修饰的变量,全局变量,在程序开始的变量初始化以及整个代码指令;heap和stack属于动态内存,在程序运行过程中动态分配和释放。栈内存由编译器进行控制,例如对于函数传递的参数或在函数内部定义的自动局部变量,都存储在栈内存中,当退出函数后栈会被清空。堆内存的分配和释放通过调用库函数(#include
与PC系统的软件设计相比,嵌入式系统的C编程更重视对硬件的操作以及字节的位操作。
嵌入式系统的内存包括真正的内存、内部寄存器以及外部部件的地址映射等部分。无论上述哪种内存,一般都将映射到处理器的内存空间中。从编程的角度来看,嵌入式系统和PC系统的软件设计的一个重要的区别即在于嵌入式系统可以更重视对硬件的操作。而对于硬件的操作需要通过操作内部寄存器和外围部件内存映射的地址实现,其实现方式都是对内存的读、写操作。
不论是哪种地址,在内存中都是以32位的形式在内存中进行存储,基于这种观点我们不难理解通过指针进行的各种操作。
我们可以让指针指向一个变量,或者一段内存空间:
int a;
int *p = &a;
或者
void *p =malloc(1024);
上述操作主要用于系统内存的操作,例如初始化变量(静态内存)或申请堆内存时采用。
我们也可以定义指针为确定的整数(int或unsigned long型)值,例如我们可以定义指针变量p为0x826300ff:
void *p = (void*)0x826300ff;
这种方式对于硬件的操作是最常用的,通常我们会通过宏命令定义我们操作的硬件的总线地址,然后通过指针赋初值和定义指针类型来完成之后的读写操作。
例如我们可以定义p为指向32位整数的指针,那么就可以通过强制内存转换的方式:
p = (int *)p;
对内存的读写我们直接通过赋值语句即可,编译器和系统会帮我们寻址和相关硬件操作。我们一般采用宏定义的方式:
#definewrite32b(addr,data) *( (volatile unsigned long *)(addr)) = (unsigned long)data
通过上述代码,我们完成了32位数据的写操作,需要的注意的是采用宏定义方式而不是函数的方式主要是为了减少参数压栈出栈所消耗的时间,这个后面会进一步进行说明。另外需要注意的是使用了关键字volatile,是为了告诉编译器地址addr的数据可能会被意想不到地改变,在这种情况下,编译器就不会去假设这个变量的值,即优化器会在用到这个变量时必须每次都重新读取它的值。volatile在嵌入式系统中普遍用于可能具有并行操作性质的数据,这些变量可能是被外部改变或者内部并行的程序改变,尤其是对于硬件的操作。
除了硬件操作外,嵌入式C最主要的操作就是位操作。在嵌入式系统的编程中,位操作比其他运算更常用。这是由于在嵌入式系统中涉及了更多有关硬件寄存器的操作。我们经常会遇到对制定位进行判断或者改变指定位的操作。
l 对某几位进行判断
判断无规律的某几位,例如第31位,第27位:
通过x&0x88000000,或者x|~0x88000000来得到x第31位和第27位的数据,进行下一步的判断。
另外更常用的是获取一个32位无符号整数的m位到第n位的值,我们通过以下宏完成:
#define BITS(value32u,m,n) ((unsignedint)(value32u<<(31-(n)) >> ((31-(n)+(m)))
l 改变指定位
例如对单个位的置0和置1操作:
#define SETBIT(x,n) x = x | (1< #define CLRBIT(x,n) x = x & ~(1< 多位的操作可以转化为多个单个位的操作。 嵌入式系统与PC系统相比,内存空间小,主频低,因此嵌入式系统对程序性能是非常敏感的,有以下几个方面的开销:首先是程序各段执行的效率,这是程序开销的主要方面;其次是函数的参数和返回值传递中入栈和出栈的时间。因此在编程的过程中有以下技巧可以对程序进行优化: l 使用宏定义常用的函数 由于函数入栈出栈消耗额外的时间,利用宏定义可以将这类时间去除,而且不用申请相应的栈空间,上面介绍的多种宏定义函数都是出于这样的考虑。 l 优化运行频率高的程序 对于一些运行效率较高的程序,某些命令实际上并不是每次运行都有效用,那么我们就可以通过技巧将这些无用的命令去掉,缩短整体运行时间,提高运行效率。如下例: for(i= 0;i 对于上面代码的get_count(),每循环一次都要运行一次,但是实际上不需要,如果写成: int k = ger_count(); for(i= 0;i 以上代码段将判断语句的部分操作移到循环外部,通过临时变量来代替函数调用,这样在循环次数较多的情况下,减少大量不必要的函数调用。 l 自定义函数内部操作优化 函数传递尽量以指针变量为主,在函数内部少申请栈空间。例如: struct s100{ char cc[100];}; int fs(struct s100 a); 当使用函数fs时,就需要将规模为100字节的结构体全部保存其中。编译器将会生成相关的代码,调用fs函数的时候,将首先在栈上按照结构体s100的大小开辟100字节的空间;然后将传奇给参数的结构体struct中的值全部复制到栈上的空间内,再进行操作。显而易见这种操作既耗内存效率也低。如果采用指针传递参数,那么我们就会在内存和效率方面都会得到显著地提高: int fs(struct s100 *a); 在调用fs时,只需要在栈上保存一个指针即可,同样可以达到对相应内存的操作,若不希望改变传递参数的内容,那么可以通过以下操作: int fs(const struct s100 *a); l 小数操作 在嵌入式系统中,浮点数的操作对资源的开销是很大的,对于某些特殊的小数我们可以通过定点数来表示,例如我们可以用定点数0x0005.2c来表示5.171875,在计算的过程中我们用整数来代替小数,计算完成后再转化为小数。例如要求sum = 0.326*a+0.413*b;那么我们可以先求sum=326*a+413*b,然后sum = sum/1000,这样在计算过程中就取消了浮点数的使用,大大提高了操作效率。 l 其他技巧 利用脑力替CPU完成某部分操作,例如查表法:在一个4位的二进制数中,确定有几位为1,如果按照经典的一般的思路就是使用循环的方法让程序在这个4位的数中依次查找各个位是否为1,最后累加得出1的数目。如果采用穷举的列表法就可以大大提高速度: const int table[16] = {0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4}; int getnumber (unsigned int a) { Return table[a&0xf]; } 如上,我们就可以利用数组来得到结果。 总之,只要我们知道嵌入式系统的操作规律,熟悉了嵌入式系统的特点,并且善于发动自己的智慧,那么我们就会很容易的对嵌入式C程序进行有效的优化。四、嵌入式程序的优化