×
单片机 > 单片机程序设计 > 详情

PIC单片机asm与C混合编程

发布时间:2020-05-16 发布时间:
|

一、如何从汇编转向PICC

首先要求你要有C 语言的基础。C代码的头文件一定要有#include,它是很多头文件的集合,C 编译器在pic.h 中根据你的芯片自动载入相应的其它头文件。这点比汇编好用。载入的头文件中其实是声明芯片的寄存器和一些函数。顺便摘抄一个片段:
static volatile unsigned char TMR0 @ 0x01;
static volatile unsigned char PCL @ 0x02;
static volatile unsigned char STATUS @ 0x03;
可以看出和汇编的头文件中定义寄存器是差不多的。如下:
TMR0 EQU 0X01;
PCL EQU 0X02;
STATUS EQU 0X03;
都是把无聊的地址定义为大家公认的名字。
1、如何赋值?
如对TMR0 附值,汇编中:
MOVLW 200;
MOVWF TMR0;
当然得保证当前页面在0,不然会出错。
C 语言:
TMR0=200;//无论在任何页面都不会出错。
可以看出来C 是很直接了当的。并且最大好处是操作一个寄存器时候,不用考虑页面的问题。一切由
C 自动完成。
2、如何位操作?
汇编中的位操作是很容易的。在C 中更简单。C 的头文件中已经对所有可能需要位操作的寄存器的每
一位都有定义名称:
如:PORTA 的每一个I/O 口定义为:RA0、RA1、RA2。。。RA7。OPTION 的每一位定义为:PS0、
PS1、PS2 、PSA 、T0SE、T0CS、INTEDG 、RBPU。可以对其直接进行运算和附值。
如:
RA0=0;
RA2=1;
在汇编中是:
BCF PORTA,0;
BSF PORTA,2;
可以看出2 者是大同小异的,只是C 中不需要考虑页面的问题。
3、内存分配问题
在汇编中定义一个内存是一件很小心的问题,要考虑太多的问题,稍微不注意就会出错。比如16 位的
运算等。用C 就不需要考虑太多。下面给个例子:
16 位的除法(C 代码):
INT X=5000;
INT Y=1000;
INT Z=X/Y;
而在汇编中则需要花太多精力。
给一个小的C 代码,用RA0 控制一个LED 闪烁:
#include
void main()
{
int x;
CMCON=0B111; //掉A 口比较器,要是有比较器功能的话。
ADCON1=0B110; //掉A/D 功能,要是有A/D 功能的话。
TRISA=0; //RA 口全为输出。
loop:RA0=!RA0;
for(x=60000;--x;){;} //延时
goto loop;
}
说说RA0=!RA0 的意思:PIC 对PORT 寄存器操作都是先读取----修改----写入。上句的含义是程序先
读RA0,然后取反,最后把运算后的值重新写入RA0,这就实现了闪烁的功能。


二、浅谈PICC 的位操作
由于PIC 处理器对位操作是最高效的,所以把一些BOOL 变量放在一个内存的位中,既可以达到运算
速度快,又可以达到最大限度节省空间的目的。在C 中的位操作有多种选择。
*********************************************
如:char x;    x=x  |  0B00001000;
       char x;    x=x & 0B11011111;
把上面的变成公式则是:
#define bitset(var,bitno)(var |=1<#define bitclr(var,bitno)(var &=~(1<则上面的操作就是:

char x;    bitset(x,4);
char x;    bitclr (x,5);
*************************************************
但上述的方法有缺点,就是对每一位的含义不直观,最好是能在代码中能直观看出每一位代表的意思,
这样就能提高编程效率,避免出错。如果我们想用X 的0-2 位分别表示温度、电压、电流的BOOL 值可以如下:
unsigned char x @ 0x20;
bit temperature@ (unsigned)&x*8+0;
bit voltage@ (unsigned)&x*8+1;
bit current@ (unsigned)&x*8+2;
这样定义后X的位就有一个形象化的名字,不再是枯燥的1、2、3、4 等数字了。可以对X 全局修改,也可以对每一位进行操作:
char=255;
temperature=0;
if(voltage)......
*****************************************************************
还有一个方法是用C 的struct 结构来定义,如:
struct cypok{
temperature:1;
voltage:1;
current:1;
none:4;
}x @ 0x20;
这样就可以用
x.temperature=0;
if(x.current)....
等操作了。
**********************************************************
上面的方法在一些简单的设计中很有效,但对于复杂的设计中就比较吃力。如象在多路工业控制上。
前端需要分别收集多路的多路信号,然后再设定控制多路的多路输出。如:有2 路控制,每一路的前端信
号有温度、电压、电流。后端控制有电机、喇叭、继电器、LED。如果用汇编来实现的话,是很头疼的事
情,用C 来实现是很轻松的事情,这里也涉及到一点C 的内存管理(其实C 的最大优点就是内存管理)。
采用如下结构:
union cypok{
struct out{
               motor:1;
               relay:1;
               speaker:1;
               led1:1;
               led2:1;
}out;
struct in{
            none:5;
            temperature:1;
            voltage:1;
            current:1;
}in;
char x;
};
union cypok an1;
union cypok an2;
上面的结构有什么好处呢?
细分了信号的路an1 和an2;
细分了每一路的信号的类型(是前端信号in 还是后端信号out):
an1.in ;
an1.out;
an2.in;
an2.out;
然后又细分了每一路信号的具体含义,如:
an1.in.temperature;
an1.out.motor;
an2.in.voltage;
an2.out.led2;等
这样的结构很直观的在2 个内存中就表示了2 路信号。并且可以极其方便的扩充。
如添加更多路的信号,只需要添加:
union cypok an3;
union cypok an4;
从上面就可以看出用C 的巨大好处。


三、PICC 之延时函数和循环体优化。
很多朋友说C 中不能精确控制延时时间,不能象汇编那样直观。其实不然,对延时函数深入了解一下
就能设计出一个理想的框架出来。一般的我们都用for(x=100;--x;){;}此句等同与x=100;while(--x){;};
或for(x=0;x<100;x++){;}。
来写一个延时函数。
在这里要特别注意:X=100,并不表示只运行100 个指令时间就跳出循环。
可以看看编译后的汇编:
x=100;while(--x){;}
汇编后:
movlw 100
bcf 3,5
bcf 3,6
movwf _delay
l2 decfsz _delay
goto l2
return
从代码可以看出总的指令是是303 个,其公式是8+3*(X-1)。注意其中循环周期是X-1 是99 个。这
里总结的是x 为char 类型的循环体,当x 为int 时候,其中受X 值的影响较大。建议设计一个char 类型的
循环体,然后再用一个循环体来调用它,可以实现精确的长时间的延时。下面给出一个能精确控制延时的
函数,此函数的汇编代码是最简洁、最能精确控制指令时间的:
void delay(char x,char y){
char z;
do{
z=y;
do{;}while(--z);
}while(--x);
}
其指令时间为:7+(3*(Y-1)+7)*(X-1)如果再加上函数调用的call 指令、页面设定、传递参数
花掉的7 个指令。则是:14+(3*(Y-1)+7)*(X-1)。如果要求不是特别严格的延时,可以用这个函数:
void delay(){
unsigned int d=1000;
while(--d){;}
}
此函数在4M 晶体下产生10003us 的延时,也就是10mS。如果把D 改成2000,则是20003uS,以此类推。有朋友不明白,为什么不用while(x--)后减量,来控制设定X 值是多少就循环多少周期呢?现在看看编译它的汇编代码:
bcf 3,5
bcf 3,6
movlw 10
movwf _delay
l2
decf _delay
incfsz _delay,w
goto l2
return
可以看出循环体中多了一条指令,不简洁。所以在PICC 中最好用前减量来控制循环体。
再谈谈这样的语句:
for(x=100;--x;){;}和for(x=0;x<100;x++){;}
从字面上看2 者意思一样,但可以通过汇编查看代码。后者代码雍长,而前者就很好的汇编出了简洁的代
码。所以在PICC 中最好用前者的形式来写循环体,好的C 编译器会自动把增量循环化为减量循环。因为
这是由处理器硬件特性决定的。PICC 并不是一个很智能的C 编译器,所以还是人脑才是第一的,掌握一些
经验对写出高效,简洁的代码是有好处的。


四、深入探讨PICC之位操作
1、用位操作来做一些标志位,也就是BOOL变量.可以简单如下定义:
bit a,b,c;
PICC会自动安排一个内存,并在此内存中自动安排一位来对应a,b,c.由于我们只是用它们来简单的
表示一些0,1信息,所以我们不需要详细的知道它们的地址\位究竟是多少,只管拿来就用好了。
2、要是需要用一个地址固定的变量来位操作,可以参照PIC.H里面定义寄存器。
如:用25H内存来定义8个位变量.
static volatile unsigned char myvar @ 0x25;
static volatile bit b7 @ (unsigned)&myvar*8+7;
static volatile bit b6 @ (unsigned)&myvar*8+6;
static volatile bit b5 @ (unsigned)&myvar*8+5;
static volatile bit b4 @ (unsigned)&myvar*8+4;
static volatile bit b3 @ (unsigned)&myvar*8+3;
static volatile bit b2 @ (unsigned)&myvar*8+2;
static volatile bit b1 @ (unsigned)&myvar*8+1;
static volatile bit b0 @ (unsigned)&myvar*8+0;
这样即可以对myvar操作,也可以对B0--B7直接位操作.
但不好的是,此招在低档片子,如C5X系列上可能会出问题.
还有就是表达起来复杂,你不觉得输入代码累么?呵呵
3、这也是一些常用手法
#define   testbit(var, bit)    ((var) & (1 <//测试某一位,可以做BOOL运算
#define   setbit(var, bit)   ((var) |= (1 << (bit)))    //把某一位置1
#define   clrbit(var, bit)    ((var) &= ~(1 << (bit)))  //把某一位清0
附上一段代码,可以用MPLAB调试观察
#i nclude "pic.h"
#define   testbit(var, bit)   ((var) & (1 <#define   setbit(var, bit)    ((var) |= (1 << (bit)))
#define   clrbit(var, bit)     ((var) &= ~(1 << (bit)))
char a,b;
void main()

{
char myvar;
myvar=0B10101010;
a=testbit(myvar,0);
setbit(myvar,0);
a=testbit(myvar,0);
clrbit(myvar,5);
b=testbit(myvar,5);


if(!testbit(myvar,3))
a=255;
else
a=100;


while(1){;}
}
4、用标准C的共用体来表示
#include "pic.h"
union var

{
unsigned char byte;
struct

{
unsigned b0:1, b1:1, b2:1, b3:1, b4:1, b5:1, b6:1, b7:1;
} bits;
};


char a,b;
void main()

{
static union var myvar;
myvar.byte=0B10101010;
a=myvar.bits.b0;
b=myvar.bits.b1;


if(myvar.bits.b7)
a=255;
else
a=100;
while(1){;}
}
5、用指针转换来表示
#include  "pic.h"


typedef struct

{
unsigned b0:1, b1:1, b2:1, b3:1, b4:1, b5:1, b6:1, b7:1;
} bits;     //先定义一个变量的位


#define mybit0 (((bits *)&myvar)->b0)    //取myvar的地址(&myvar)强制转换成bits 类型的指针
#define mybit1 (((bits *)&myvar)->b1)
#define mybit2 (((bits *)&myvar)->b2)
#define mybit3 (((bits *)&myvar)->b3)
#define mybit4 (((bits *)&myvar)->b4)
#define mybit5 (((bits *)&myvar)->b5)
#define mybit6 (((bits *)&myvar)->b6)
#define mybit7 (((bits *)&myvar)->b7)


char myvar;
char a,b;


void main()

{
myvar=0B10101010;
a=mybit0;
b=mybit1;
if(mybit7)
a=255;
else
a=100;
while(1){;}
}

6、5的方法还是烦琐,可以用粘贴符号的形式来简化它。
#include "pic.h"


typedef struct

{
unsigned b0:1, b1:1, b2:1, b3:1, b4:1, b5:1, b6:1, b7:1;
} bits;


#define _paste(a,b) a##b
#define bitof(var,num) (((bits *)&(var))->_paste(b,num))


char myvar;
char a,b;


void main()

{
a=bitof(myvar,0);
b=bitof(myvar,1);
if(bitof(myvar,7))
a=255;
else
a=100;
while(1){;}
}
有必要说说#define _paste(a,b) a##b 的意思:
此语句是粘贴符号的意思,表示把b 符号粘贴到a 符号之后。
例子中是
a=bitof(myvar,0);--->(((bits*)&(myvar))->_paste(b,0))--->(((bits *)&(var))->b0)
可以看出来,_paste(b,0)的作用是把0 粘贴到了b 后面,成了b0 符号。
总结:C语言的优势是能直接对低层硬件操作,代码可以非常非常接近汇编,上面几个例子的位操作代码
是100%的达到汇编的程度的;另一个优势是可读性高,代码灵活。上面的几个位操作方法任由你选,
你不必担心会产生多余的代码量出来。


五、在PICC 中使用常数指针
常数指针使用非常灵活,可以给编程带来很多便利。我测试过,PICC 也支持常数指针,并且也会自动分页,实在是一大喜事。
定义一个指向8 位RAM 数据的常数指针(起始为0x00):
#define   DBYTE    ((unsigned char volatile *) 0)

定义一个指向16 位RAM 数据的常数指针(起始为0x00):
#define   CWORD   ((unsigned int volatile *) 0)
((unsigned char volatile *) 0)中的0 表示指向RAM 区域的起始地址,可以灵活修改它。
DBYTE[x]中的x 表示偏移量。
1、下面是一段代码
char a1,a2,a3,a4;
#define DBYTE ((unsigned char volatile *) 0)
void main(void)   //主函数

{
long cc=0x89abcdef;
a1=DBYTE[0x24];
a2=DBYTE[0x25];
a3=DBYTE[0x26];
a4=DBYTE[0x27];
while(1);
}
2、
char a1,a2,a3,a4;
#define   DBYTE   ((unsigned char volatile *) 0)
void pp(char y)    //子函数

{
a1=DBYTE[y++];
a2=DBYTE[y++];
a3=DBYTE[y++];
a4=DBYTE[y];
}
void main(void)    //主函数

{
long cc=0x89abcdef;
char x;
x=&cc;
pp(x);
while(1);
}
3、
char a1,a2,a3,a4;
#define DBYTE ((unsigned char volatile *) 0)
void pp(char y)    //子函数

{
a1=DBYTE[y++];
a2=DBYTE[y++];
a3=DBYTE[y++];
a4=DBYTE[y];
}
void main(void)  //主函数

{
bank1 static long cc=0x89abcdef;
char x;
x=&cc;
pp(x);
while(1);
}


六、PICC 关于unsigned 和 signed 的几个关键问题
unsigned 是表示一个变量(或常数)是无符号类型。signed 表示有符号。它们表示数值范围不一样。
PICC 默认所有变量都是unsigned 类型的,哪怕你用了signed 变量。因为有符号运算比无符号运算耗资源,
而且MCU 运算一般不涉及有符号运算。在PICC 后面加上-SIGNED_CHAR 后缀可以告诉PICC 把signed
变量当作有符号处理。
在PICC 默认的无符号运算下看这样的语句:
char i;
for(i=7;i>=0;i--){
; //中间语句
}
这样的C 代码看上去是没有丁点错误的,但编译后,问题出现了:
movlw 7
movwf i
loop
//中间语句
decf i //只是递减,没有判断语句!!!
goto loop
原因是当i 是0 时候,条件还成立,还得循环一次,直到i 成负1 条件才不成立。而PICC 在默认参数下是不能判断负数的,所以编译过程出现问题。那么采用这样的语句来验证:
char i;
i=7;
while(1){
i--;
//中间语句
if(i==0)break; //告诉PICC 以判断i 是否是0 来作为条件
}
编译后代码正确:
movlw 7
movwf i
loop
//中间语句
decfsz i //判断是否是0
goto loop
再编译这样的语句:(同样循环8 次)
for(i=8;i>0;i--){
;
}
movlw 8
movwf i
loop
decfsz i //同上编译的代码。
goto loop
再次验证了刚才的分析。
在PICC 后面加上-SIGNED_CHAR 后缀,则第一个示例就正确编译出来了,更证明了刚才的分析是正确的。
代码如下:
movlw 7
movwf i
loop
//中间语句
decf i //递减
btfss i,7 //判断i 的7 位来判断是否为负数
goto l94
总结:在PICC 无符号编译环境下,对于递减的for 语句的条件判断语句不能是>=0 的形式。
最后谈谈PICC 的小窍门:
在PICC 默认的无符号环境下,对比如下代码:
a 语句:
char i,j[8];
i=7;
while(1){
j[i]=0;
i--;
if(i==0)break;
}
b 语句:
char i,j[8];
for(i=8;i>0;i--){
j[i-1]=0;
}
表面看上去,一般会认为下面的代码编译后要大一点点,因为多了j[i-1]中的i-1。
其实编译后代码量是一摸一样的。
原因如下:
movlw 8 或7 //a 语句是7,b 语句是8
movf i
loop
//a 语句在这里提取i 给j 数组
//i 递减判断语句
//b 语句在这里提取i 给j 数组
goto loop
可以看出只是代码位置不同而已,并没添加代码量。b 语句同样达到了从7 到0 的循环。
小总结:对于递减到0 的for 语句推荐用>0 判断语句来实现,不会出现编译错误的问题,并且不会增加代码量,尤其对于数组操作的方面。
另:对于PICC 或CCS,在其默认的无符号编译环境下,如果出现负数运算就会出问题。
如(-100)+50 等,所以在编写代码时候要特别小心!!!


七、用PICC 写高效的位移操作

在许多模拟串行通信中需要用位移操作。
以1-Wire总线的读字节为例,原厂的代码是:
unsigned char read_byte(void)
{
unsigned char i;
unsigned char value = 0;
for (i = 0; i < 8; i++)
{
if(read_bit()) value| = 0 x 01<// reads byte in, one byte at a time and then
// shifts it left
delay(10); // wait for rest of timeslot
}
return(value);
}
虽然可以用,但编译后执行效率并不高效,这也是很多朋友认为C 一定不能和汇编相比的认识提供了
说法。其实完全可以深入了解C 和汇编之间的关系,写出非常高效的C 代码,既有C 的便利,又有汇编的效率。首先对 for (i = 0; i < 8; i++)做手术,改成递减的形式:for(i=8;i!=0;i--),因为CPU 判断一个数是否是0(只需要一个指令),比判断一个数是多大来的快(需要3 个指令)。再对value| = 0 x 01<仔细研究C 语言的位移操作,可以发现C 总是先把标志位清0,然后再把此位移入字节中,也就是说,当
前移动进字节的位一定是0。那么,既然已经是0 了,我们就只剩下一个步骤:判断总线状态是否是高来
决定是否改写此位,而不需要判断总线是低的情况。于是改写如下代码:
for(i=8;i!=0;i--){
value>>=1; //先右移一位,value 最高位一定是0
if(read_bit()) value|=0x80; //判断总线状态,如果是高,就把value 的最高位置1
}
这样一来,整个代码变得极其高效,编译后根本就是汇编级的代码。再举一个例子:
在采集信号方面,经常是连续采集N 次,最后求其平均值。
一般的,无论是用汇编或C,在采集次数上都推荐用8,16,32、64、128、256 等次数,因为这些数都比
较特殊,对于MCU 计算有很大好处。
我们以128 次采样为例:注:sampling()为外部采样函数。
unsigned int total;
unsigned char i,val;
for(i=0;i<128;i++){
total+=sampling();
}
val=total/128;
以上代码是很多场合都可以看见的,但是效率并不怎么样,狂浪费资源。
结合C 和汇编的关系,再加上一些技巧,就可以写出天壤之别的汇编级的C 代码出来,首先分析128 这个
数是0B10000000,发现其第7 位是1,其他低位全是0,那么就可以判断第7 位的状态来判断是否到了128次采样次数。在分析除以128 的运算,上面的代码用了除法运算,浪费了N 多资源,完全可以用右移的方法来代替之,val=total/128 等同于val=(unsigned char)(total>>7);再观察下去:total>>7 还可以变通成
(total<<1)>>8,先左移动一位,再右移动8 位,不就成了右移7 位了么?可知道位移1,4,8 的操作只需要一个指令哦。有上面的概验了,就可以写出如下的代码:
unsigned int total;
unsigned char i=0
unsigned char val;
while(!(i&0x80)){ //判断i 第7 位,只需要一个指令。
total+=sampling();
i++;
}
val=(unsigned char)((total<<1)>>8); //几个指令就代替了几十个指令的除法运算
哈哈,发现什么?代码量竟然可以减少一大半,运算速度可以提高几倍。
再回头,就可以理解为什么采样次数要用推荐的一些特殊值了。


八、C 程序优化
对程序进行优化,通常是指优化程序代码或程序执行速度。优化代码和优化速度实际上是一个予盾的统一,一般是优化了代码的尺寸,就会带来执行时间的增加,如果优化了程序的执行速度,通常会带来代码增加的副作用,很难鱼与熊掌兼得,只能在设计时掌握一个平衡点。
1、程序结构的优化
1)、程序的书写结构
虽然书写格式并不会影响生成的代码质量,但是在实际编写程序时还是应该尊循一定的书写规则,一个书写清晰、明了的程序,有利于以后的维护。在书写程序时,特别是对于While、for、do…while、if…else、switch…case 等语句或这些语句嵌套组合时,应采用“缩格”的书写形式。
2)、标识符
程序中使用的用户标识符除要遵循标识符的命名规则以外,一般不要用代数符号(如a、b、x1、y1)作
为变量名,应选取具有相关含义的英文单词(或缩写)或汉语拼音作为标识符,以增加程序的可读性,如:
count、number1、red、work 等。
3)、程序结构
C 语言是一种高级程序设计语言,提供了十分完备的规范化流程控制结构。因此在采用C 语言设计单
片机应用系统程序时,首先要注意尽可能采用结构化的程序设计方法,这样可使整个应用系统程序结构清
晰,便于调试和维护。于一个较大的应用程序,通常将整个程序按功能分成若干个模块,不同模块完成不
同的功能。各个模块可以分别编写,甚至还可以由不同的程序员编写,一般单个模块完成的功能较为简单,
设计和调试也相对容易一些。在C 语言中,一个函数就可以认为是一个模块。所谓程序模块化,不仅是要
将整个程序划分成若干个功能模块,更重要的是,还应该注意保持各个模块之间变量的相对独立性,即保
持模块的独立性,尽量少使用全局变量等。对于一些常用的功能模块,还可以封装为一个应用程序库,以
便需要时可以直接调用。但是在使用模块化时,如果将模块分成太细太小,又会导致程序的执行效率变低(进
入和退出一个函数时保护和恢复寄存器占用了一些时间)。
4)、定义常数
在程序化设计过程中,对于经常使用的一些常数,如果将它直接写到程序中去,一旦常数的数值发生变化,就必须逐个找出程序中所有的常数,并逐一进行修改,这样必然会降低程序的可维护性。因此,应尽量当采用预处理命令方式来定义常数,而且还可以避免输入错误。
5)、减少判断语句
能够使用条件编译(ifdef)的地方就使用条件编译而不使用if 语句,有利于减少编译生成的代码的长度。
6)、表达式
对于一个表达式中各种运算执行的优先顺序不太明确或容易混淆的地方,应当采用圆括号明确指定它们的优先顺序。一个表达式通常不能写得太复杂,如果表达式太复杂,时间久了以后,自己也不容易看得懂,不利于以后的维护。
7)、函数
对于程序中的函数,在使用之前,应对函数的类型进行说明,对函数类型的说明必须保证它与原来定
义的函数类型一致,对于没有参数和没有返回值类型的函数应加上“void”说明。如果果需要缩短代码的长度,可以将程序中一些公共的程序段定义为函数,在Keil 中的高级别优化就是这样的。如果需要缩短程序的执行时间,在程序调试结束后,将部分函数用宏定义来代替。注意,应该在程序调试结束后再定义宏,因为大多数编译系统在宏展开之后才会报错,这样会增加排错的难度。
8)、尽量少用全局变量,多用局部变量。因为全局变量是放在数据存储器中,定义一个全局变量,MCU 就少一个可以利用的数据存储器空间,如果定义了太多的全局变量,会导致编译器无足够的内存可以分配。而局部变量大多定位于MCU 内部的寄存器中,在绝大多数MCU 中,使用寄存器操作速度比数据存储器快,指令也更多更灵活,有利于生成质量更高的代码,而且局部变量所占用的寄存器和数据存储器在不同的模块中可以重复利用。
9)、设定合适的编译程序选项
许多编译程序有几种不同的优化选项,在使用前应理解各优化选项的含义,然后选用最合适的一种优
化方式。通常情况下一旦选用最高级优化,编译程序会近乎病态地追求代码优化,可能会影响程序的正确
性,导致程序运行出错。因此应熟悉所使用的编译器,应知道哪些参数在优化时会受到影响,哪些参数不
会受到影响。
在ICCAVR 中,有“Default”和“Enable Code Compression”两个优化选项。
在CodeVisionAVR 中,“Tiny”和“small”两种内存模式。
在IAR 中,共有7 种不同的内存模式选项。
在GCCAVR 中优化选项更多,一不小心更容易选到不恰当的选项。
2、代码的优化
1)、 选择合适的算法和数据结构
应该熟悉算法语言,知道各种算法的优缺点,具体资料请参见相应的参考资料,有很多计算机书籍上都有介绍。将比较慢的顺序查找法用较快的二分查找或乱序查找法代替,插入排序或冒泡排序法用快速排序、合并排序或根排序代替,都可以大大提高程序执行的效率。.选择一种合适的数据结构也很重要,比如你在一堆随机存放的数中使用了大量的插入和删除指令,那使用链表要快得多。
数组与指针具有十分密码的关系,一般来说,指针比较灵活简洁,而数组则比较直观,容易理解。对于大部分的编译器,使用指针比使用数组生成的代码更短,执行效率更高。但是在Keil 中则相反,使用数组比使用的指针生成的代码更短。
2)、 使用尽量小的数据类型
能够使用字符型(char)定义的变量,就不要使用整型(int)变量来定义;能够使用整型变量定义的变量就不要用长整型(long int),能不使用浮点型(float)变量就不要使用浮点型变量。当然,在定义变量后不要超过变量的作用范围,如果超过变量的范围赋值,C 编译器并不报错,但程序运行结果却错了,而且这样的错误很难发现。在ICCAVR 中,可以在Options 中设定使用printf 参数,尽量使用基本型参数(%c、%d、%x、%X、%u 和%s 格式说明符),少用长整型参数(%ld、%lu、%lx 和%lX 格式说明符),至于浮点型的参数(%f)则尽量不要使用,其它C 编译器也一样。在其它条件不变的情况下,使用%f 参数,会使生成的代码的数量增加很多,执行速度降低。
3)、 使用自加、自减指令
通常使用自加、自减指令和复合赋值表达式(如a-=1 及a+=1 等)都能够生成高质量的程序代码,编译器通常都能够生成inc 和dec 之类的指令,而使用a=a+1 或a=a-1 之类的指令,有很多C 编译器都会生成二到三个字节的指令。在AVR 单片适用的ICCAVR、GCCAVR、IAR 等C 编译器以上几种书写方式生成的代码是一样的,也能够生成高质量的inc 和dec 之类的的代码。
4)、减少运算的强度
可以使用运算量小但功能相同的表达式替换原来复杂的的表达式。如下:
(1)、求余运算。
a=a%8;
可以改为:
a=a&7;
说明:位操作只需一个指令周期即可完成,而大部分的C 编译器的“%”运算均是调用子程序来完成,代码长、执行速度慢。通常,只要求是求2^n方的余数,均可使用位操作的方法来代替。
(2)、平方运算
a=pow(a,2.0);
可以改为:
a=a*a;
说明:在有内置硬件乘法器的单片机中(如51 系列),乘法运算比求平方运算快得多,因为浮点数的求平方
是通过调用子程序来实现的,在自带硬件乘法器的AVR 单片机中,如ATMega163 中,乘法运算只需2 个
时钟周期就可以完成。既使是在没有内置硬件乘法器的AVR 单片机中,乘法运算的子程序比平方运算的子
程序代码短,执行速度快。
如果是求3 次方,如:
a=pow(a,3.0);
更改为:
a=a*a*a;
则效率的改善更明显。
(3)、用移位实现乘除法运算
a=a*4;
b=b/4;
可以改为:
a=a<<2;
b=b>>2;
说明:通常如果需要乘以或除以2n,都可以用移位的方法代替。在ICCAVR 中,如果乘以2n,都可以生成左移的代码,而乘以其它的整数或除以任何数,均调用乘除法子程序。用移位的方法得到代码比调用乘除法子程序生成的代码效率高。实际上,只要是乘以或除以一个整数,均可以用移位的方法得到结果,如:
a=a*9;
可以改为:
a=(a<<3)+a;  //a<<3相当于乘8,再加一次,就是乘9
5)、循环
(1)、循环语
对于一些不需要循环变量参加运算的任务可以把它们放到循环外面,这里的任务包括表达式、函数的调用、
指针运算、数组访问等,应该将没有必要执行多次的操作全部集合在一起,放到一个init 的初始化程序中
进行。
(2)、延时函数:
通常使用的延时函数均采用自加的形式:
void delay (void)
{
unsigned int i;
for (i=0;i<1000;i++)
;
}
将其改为自减延时函数:
void delay (void)
{
unsigned int i;
for (i=1000;i>0;i--)
;
}
两个函数的延时效果相似,但几乎所有的C 编译对后一种函数生成的代码均比前一种代码少1~3 个字节,
因为几乎所有的MCU 均有为0 转移的指令,采用后一种方式能够生成这类指令。
在使用while 循环时也一样,使用自减指令控制循环会比使用自加指令控制循环生成的代码更少1~3 个字节。
但是在循环中有通过循环变量“i”读写数组的指令时,使用预减循环时有可能使数组超界,要引起注意。
(3)while 循环和do…while 循环
用while 循环时有以下两种循环形式:
unsigned int i;
i=0;
while (i<1000)
{
i++;
//用户程序
}
或:
unsigned int i;
i=1000;
do
i--;
//用户程序
while (i>0);
在这两种循环中,使用do…while 循环编译后生成的代码的长度短于while 循环。
6)、查表
在程序中一般不进行非常复杂的运算,如浮点数的乘除及开方等,以及一些复杂的数学模型的插补运算,对这些即消耗时间又消费资源的运算,应尽量使用查表的方式,并且将数据表置于程序存储区。如果直接生成所需的表比较困难,也尽量在启动时先计算,然后在数据存储器中生成所需的表,以后在程序运行直接查表就可以了,减少了程序执行过程中重复计算的工作量。
7)、其它
比如使用在线汇编及将字符串和一些常量保存在程序存储器中,均有利于优化。


九、关于PIC的C语言中嵌入汇编语言

C 编译器选项设定(PICC Compiler)
项目中所有的C 原程序都将通过C 编译器编译成机器码,这些选项决定了C 编译器是如何工作的。所有选项又分为两组:普通选项(General)和高级选项(Advanced),分别见图11-5A 和11-5B。
C 编译器的普通选项最重要的就是针对代码优化的设定。如果没有特殊原因,应该设定全局优化级别为9 级(最高级别优化),同时使用汇编级优化,这样最终得到的代码效率最高(长度和执行速度两方面)。按笔者的使用经验,仅从代码长度去比较,使用最高级别优化后代码长度至少可以减少20%(2K 字以上的程序)。而且PICC 的优化器相当可靠,一般不会因为使用优化从而使生成的程序出现错误。碰到的一些问题也基本都是用户编写的原程序有漏洞所导致,例如一些变量应该是volatile 型但编程员没有明确定义,在优化前程序可以正常运行,一旦使用优化,程序运行就出现异常。显然,把出现的这些问题归罪到编译器是毫无道理的。


使用优化后可能对原程序级的调试带来一些不便之处。因PICC 可能会重组编译后的代码,例如多处重复的代码可能会改成同一个子程序调用以节约程序空间,这样在调试过程中跟踪原程序时可能会出现程序乱跳的现象,这基本是正常的。若为了强调更直观的代码调试过程,你可以将优化级别降低甚至关闭所有优化功能,这样调试时程序的运行就可以按部就班了。
(A) 常用选项 (B) 高级选项
图11-5 C 编译器选项设定
C 编译器的高级选项设定基本都是针对诊断信息输出的,和生成的代码无关。用得相对较多的选项有:
.. Generate assembly list file:编译器生成C 原程序的汇编列表文件(*.lst)。在此文件中列出了每一行C 原代码对应的汇编指令,但这些都是优化前的代码。简单的一条C 语句被翻译成汇编指令后可能有好几条。有时汇编列表文件可以作为解决问题的辅助手段。如果你怀疑编译器生成的代码有错误,不妨先产生对应的汇编列表文件,看看在优化前一条C 语句被编译后的汇编码到底是什么。
.. Compile to assembly only:这一选项的作用是把C 原程序编译成汇编指令文件(*.as),此时将不生成目标文件,也不进行最后的连接定位。这一选项在C 和汇编混合编程时特别有用。通过解读C 程序对应的汇编指令,可以掌握C 程序中存取变量的具体方法,然后用在自己编写的汇编指令中。我们将在稍后专门做介绍。
11.8.5 连接器选项设定(PICC Linker)
连接器PICC Linker 的选项基本不用作太多的改变,在图11-6 的对话框中显示了可设定的各类项目。其中有两项有用的信息输出可以考虑加以利用:
.. Generate map file:生成连接定位映射文件。在此映射文件中详细列出了所有程序用到的变量的具体物理地址;所有函数的入口地址;函数相互之间调用的层次关系和深度等。这些信息对于程序的调试将非常有用。此文件将以扩展名“*.map”的形式存放在同一个项目路径下,需要时可以用任何文本编辑器打开观察。
.. Display memory-segment usage:显示详细的内存分配和使用情况报告。用户可以了解到程序空间和数据存储器空间资源分配的细节。下面列举了在一个项目编译后实际的内存使用信息,为方便理解笔者用“//”添加了一些注释:
Psect Usage Map: //程序段定位表
Psect | Contents | Memory Range
----------|------------------------------|--------------------
powerup | Power on reset code | $0000 - $0003
intentry | Interrupt service routine | $0004 - $000C
intcode | Interrupt service routine | $000D - $002C
intret | Interrupt service routine | $002D - $0035
init | Initialization code | $0036 - $003D
end_init | Initialization code | $003E - $0040
图11-6 PICC 连接器选项设定

clrtext | Memory clearing code | $0041 - $0047
const3 | Strings and constant data | $0048 - $0060
const | Strings and constant data | $0061 - $0071
const2 | Strings and constant data | $0072 - $0076
text | Program and library code | $0576 - $0582
text | Program and library code | $0583 - $07C7
float_te | Arithmetic routine code | $07C8 - $07FF
rbss_0 | Bank 0 RAM variables | $0021 - $0042
temp | Temporary RAM data | $0043 - $0047
nvram | Persistent RAM data | $0048 - $004A
intsave | Registers saved on interrupt | $004B - $004D
intsave | Registers saved on interrupt | $007F - $007F
intsave_1 | Saved copy of W in bank 1 | $00FF - $00FF
rbit_0 | Bank 0 bit variables | $0100 - $0104
config | User-programmed CONFIG bits | $2007 - $2007
Memory Usage Map: //存储空间使用情况报告
//程序空间代码定位地址分布
Program ROM $0000 - $0076 $0077 ( 119) words
Program ROM $0576 - $07FF $028A ( 650) words
$0301 ( 769) words total Program ROM
//bank0 数据空间变量地址分布
Bank 0 RAM $0021 - $004D $002D ( 45) bytes
Bank 0 RAM $007F - $007F $0001 ( 1) bytes
$002E ( 46) bytes total Bank 0 RAM
//bank1 数据空间变量地址分布
Bank 1 RAM $00FF - $00FF $0001 ( 1) bytes total Bank 1 RAM
//bank0 数据空间位变量地址分布
Bank 0 Bits $0100 - $0104 $0005 ( 5) bits total Bank 0 Bits
//配置字地址
Config Data $2007 - $2007 $0001 ( 1) words total Config Data
Program statistics: //程序总体资源消耗统计
Total ROM used 769 words (18.8%) //生成代码字总数和程序空间使用率
Total RAM used 48 bytes (25.0%) //使用数据字节数和数据空间使用率
例11-7 编译后程序使用的内存信息
11.8.6 汇编器选项设定(PICC Assembler)
PICC 环境提供了自己的汇编编译器,它和Microchip 公司提供的MPASM 编译器在原程序的语法表达方面要求稍有不同。另外,PICC 的汇编编译器要求输入原程序文件的扩展名是“*.as”,而MPASM 缺省认定的原程序以“*.asm”为扩展名。

在基于PICC 编译环境下开发PIC 单片机的C 语言应用程序时基本无需关心其汇编编译器,除非是在混合语言编程时用汇编语言编写完整的汇编原程序模块文件。其编译选项设定的对话框见图11-7,最重要的是优化使能控制项“Enable optimization”,一般情况下应该使用汇编器的优化以节约程序空间。


C 和汇编混合编程
有两个原因决定了用C 语言进行单片机应用程序开发时使用汇编语句的必要性:单片机的一些特殊指令操作在标准的C 语言语法中没有直接对应的描述,例如PIC 单片机的清看门狗指令“clrwdt”和休眠指令“sleep”;单片机系统强调的是控制的实时性,为了实现这一要求,有时必须用汇编指令实现部分代码以提高程序运行的效率。这样,一个项目中就会出现C 和汇编混合编程的情形,我们在此讨论一些混合编程的基本方法和技巧。


嵌入行内汇编的方法
在C 原程序中直接嵌入汇编指令是最直接最容易的方法。如果只需要嵌入少量几条的汇编指令,PICC 提供了一个类似于函数的语句:
asm(“clrwdt”);
双引号中可以编写任何一条PIC 的标准汇编指令。例如:
for (;;) {
asm("clrwdt"); //清看门狗
Task();
ClockRun();
asm("sleep"); //休眠
asm("nop"); //空操作延时
}
例11-8 逐行嵌入汇编的方式 图11-7 PICC 汇编器选项设定

如果需要编写一段连续的汇编指令,PICC 支持另外一种语法描述:用“#asm”开始汇编指令段,用“#endasm”结束。例如下面的一段嵌入汇编指令实现了将0x20~0x7F 间的RAM 全部清零:
#asm
movlw 0x20
movwf _FSR
clrf _INDF
incf _FSR,f
btfss _FSR,7
goto $-3
#endasm


 整段嵌入汇编的方式
 汇编指令寻址C 语言定义的全局变量
C 语言中定义的全局或静态变量寻址是最容易的,因为这些变量的地址已知且固定。按C 语言的语法标准,所有C 中定义的符号在编译后将自动在前面添加一下划线符“_”,因此,若要在汇编指令中寻址C 语言定义的各类变量,一定要在变量前加上一“_”符号,我们在上面例11-9 中已经体现了这一变量引用的法则,因为FSR 和INDF 等所有特殊寄存器是以C 语言语法定义的,因此汇编中需要对其寻址时前面必须添加下划线。对于C 语言中用户自定义的全局变量,用行内汇编指令寻址时也同样必须加上“_” ,下面的例11-10 说明了具体的引用方法:
volatile unsigned char tmp; //定义位于bank0 的字符型全局变量
void Test(void) //测试程序
{
#asm //开始行内汇编
clrf _STATUS //选择bank0
movlw 0x10 //设定初值
movwf _tmp //tmp=0x10
#endasm //结束行内汇编
if (tmp==0x10) { //开始C 语言程序
;
}
}
例11-10 行内汇编寻址C 全局变量(位于bank0)上面的例子说明了汇编指令中寻址C 语言所定义变量的基本方法。PICC 在编译处理嵌入的行内汇编指令时将会原封不动地把这些指令复制成最后的机器码。所有对C 编译器所作的优化设定对这些行内汇编指令而言将不起任何作用。编程员必须自己负责编写最高效的汇编代码,同时处理变量所在的bank 设定。对于定义在其它bank 中的变量,还必须在汇编指令中加以明确指示,见例11-11 的代码范例。

volatile bank1 unsigned char tmpBank1; //定义位于bank1 的字符型全局变量
volatile bank2 unsigned char tmpBank2; //定义位于bank2 的字符型全局变量
volatile bank3 unsigned char tmpBank3; //定义位于bank3 的字符型全局变量
void Test(void) //测试程序
{
#asm //开始行内汇编
bcf _STATUS,6 //选择bank1
bsf _STATUS,5
movlw 0x10 //设定初值
movwf _tmpBank1^0x80 //tmpBank1=0x10
bsf _STATUS,6 //选择bank2
bcf _STATUS,5
movlw 0x20 //设定初值
movwf _tmpBank1^0x100 //tmpBank2=0x20
bsf _STATUS,6 //选择bank3
bsf _STATUS,5
movlw 0x30 //设定初值
movwf _tmpBank1^0x180 //tmpBank1=0x30
#endasm //结束行内汇编
}
例11-11 行内汇编寻址C 全局变量(非bank0 变量)
通过上面的代码实例,我们可以掌握这样一个规律:在行内汇编指令中寻址C 语言定义的全局变量时,除了在寻址前设定正确的bank 外,在指令描述时还必须在变量上异或其所在bank 的起始地址,实际上位于bank0 的变量在汇编指令中寻址时也可以这样理解,只是异或的是0x00,可以省略。如果你了解PIC 单片机的汇编指令编码格式,上面异或的bank起始地址是无法在真正的汇编指令中体现的,其目的纯粹是为了告诉PICC 连接器变量所在的bank,以便连接器进行bank 类别检查。


 汇编指令寻址C 函数的局部变量
前面已经提到,PICC 对自动型局部变量(包括函数调用时的入口参数)采用一种“静态覆盖”技术对每一个变量确定一个固定地址(位于bank0),因此嵌入的汇编指令对其寻址时只需采用数据寄存器的直接寻址方式即可,唯一要考虑的是如何才能在编写程序时知道这些局部变量的寻址符号(具体地址在最后连接后才能决定,编程时无需关心)。一个最实用也是最可靠的方法是先编写一小段C 代码,其中有最简单的局部变量操作指令,然后参考图11-5(B)对话框选择“Compile to assembly only”,把此C 原代码编译成对应的PICC 汇编指令;查看C 编译器生成的汇编指令是如何寻址这些局部变量的,你自己编写的行内汇编指令就采用同样的寻址方式。例如,例11-12 的一小段C 原代码编译出的汇编指令
//C 原程序代码
void Test(unsigned char inVar1, inVar2)

{
unsigned char tmp1, tmp2;
inVar1++;
inVar2--;
tmp1 = 1;
tmp2 = 2;
}
//编译器生成的汇编指令
_Test
; _tmp1 assigned to ?a_Test+0 //tmp1 的寻址符为 ?a_Test+0
_Test$tmp1 set ?a_Test
; _tmp2 assigned to ?a_Test+1 //tmp2 的寻址符为 ?a_Test+1
_Test$tmp2 set ?a_Test+1
; _inVar1 assigned to ?a_Test+2 //inVar1 的寻址符为 ?a_Test+2
_Test$inVar1 set ?a_Test+2
line 44
;_inVar1 stored from w //第一个字符型行参由W 寄存器传递
bcf 3,5
bcf 3,6
movwf ?a_Test+2
;ht16.c: 43: unsigned char tmp1, tmp2;
incf ?a_Test+2
line 45
;ht16.c: 45: inVar2--;
decf ?_Test //行参inVar2 的寻址符为 ?_Test
line 46
;ht16.c: 46: tmp1 = 1;
clrf ?a_Test
incf ?a_Test
line 47
;ht16.c: 47: tmp2 = 2;
movlw 2
movwf ?a_Test+1
line 48
;ht16.c: 48: }
return
例11-12 PICC 实现局部变量操作的寻址方式基于上面得到的PICC 编译后局部变量的寻址方式,我们在C 语言程序中用嵌入汇编指令时必须采样同样的寻址符以实现对应变量的存取操作,见下面的例11-13。
//C 原程序代码
void Test(unsigned char inVar1, inVar2)
{

unsigned char tmp1, tmp2;
#asm //开始嵌入汇编
incf ?a_Test+0,f //tmp1++;
decf ?a_Test+1,f //tmp2--;
movlw 0x10
addwf ?a_Test+2,f //inVar1 += 0x10;
rrf ?_Test,w //inVar2 循环右移一位
rrf ?_Test,f
#endasm //结束嵌入汇编
}
例11-13 嵌入汇编指令实现局部变量寻址操作
如果局部变量为多字节形式组成,例如整型数、长整型等,必须按照PICC 约定的存储格式进行存取。前面已经说明了PICC 采用“Little endian”格式,低字节放在低地址,高字节放在高地址。下面的例11-14 实现了一个整型数的循环移位,在C 语言中没有直接针对循环移位的语法操作,用标准C 指令实现的效率较低。
//16 位整型数循环右移若干位
unsigned int RR_Shift16(unsigned int var, unsigned char count)
{
while(count--) //移位次数控制
{
#asm //开始嵌入汇编
rrf ?_RR_Shift16+0,w //最低位送入C
rrf ?_RR_Shift16+1,f //var 高字节右移1 位,C 移入最高位
rrf ?_RR_Shift16+0,f //var 低字节右移1 位
#endasm //结束嵌入汇编
}
return(var); //返回结果
}
例11-14 嵌入汇编指令对多字节变量的操作
11.9.4 混合编程的一些经验
C 和汇编语言混合编程可以使单片机应用程序的开发效率和程序本身的运行效率达到最佳的配合。笔者从实际应用中得到一些经验供读者一起分享。
㈠ 慎用汇编指令
相比于汇编语言,用C 语言编程的优势是毋庸置疑的:开发效率大大提高、人性化的语句指令加上模块化的程序易于日常管理和维护、程序在不同平台间的移植方便。所以既然用了C 语言编程,就尽量避免使用嵌入汇编指令或整个地编写汇编指令模块文件。PICC 已具备高效的优化功能,如果在写C 原程序时就十分注意程序的编译和运行效率问题,加上PICC 的后道编译优化,最后得到的代码效率不会比全部用汇编编写的代码差多少,尤其是
程序量较大时。另外,PICC 对数据存储空间的利用率肯定比用户人工定位变量时的利用率要高,同时还提供完整的库函数支持。C 语言的语法功能强大,能够高效率地实现绝大部分控制和运算功能。因此,除了一些十分强调单片机运行时间的代码或C 语言没有直接对应的操作可以考虑用汇编指令实现外,其它部分都应该用C 语言编写。
以上面的例11-14 进一步说明,变量的循环右移操作用C 语言实现非常不方便,PIC 单片机已有对应的移位操作汇编指令,因此用嵌入汇编的形式实现效率最高。同时对移位次数的控制,本质上说变量count 的递减判零也可以直接用汇编指令实现,但这样做节约不了多少代码,用标准的C 语言描述更直观,更易于维护。一句话:用了C 语言后,就不要再老想着用汇编。


㈡ 尽量使用嵌入汇编
这和上面的慎用汇编指令的说法并不矛盾。如果确实需要用汇编指令实现部分代码以提高运行效率,应尽量使用行内汇编,避免编写纯汇编文件(*.as 文件)。虽然PICC 支持C 和汇编原程序模块存在于同一个项目中,但要编写纯汇编文件必须首先了解PICC 特有的汇编语法结构。Hitech 公司提供了完整的文档介绍其汇编器的使用方法,有兴趣者可以从其网站上下载PICC 的用户使用手册查看。


笔者认为,类似于纯汇编文件的代码也可以在C 语言框架下实现,方法是基于C 标准语法定义所有的变量和函数名,包括需要传递的形式参数、返回参数和局部变量,但函数内部的指令基本用嵌入汇编指令编写,只有最后的返回参数用C 语句实现。这样做后函数的运行效率和纯汇编编写时几乎一模一样,但各参数的传递统一用C 标准实现,这样管理和维护就比较方便。例如下面的例11-15 实现一个字节变量的偶校验位计算。
bit EvenParity(unsigned char data)
{
#asm
swapf ?a_EvenParity+0,w //入口参数data 的寻址符为 ?a_EvenParity+0
xorwf ?a_EvenParity+0,f
rrf ?a_EvenParity+0,w
xorwf ?a_EvenParity+0,f
btfsc ?a_EvenParity+0,2
incf ?a_EvenParity+0,f
#endasm
//至此,data 的最低位即为偶校验位
if (data&0x01) return(1);
else return(0);
}
例11-15 C 函数框架中使用嵌入汇编指令
㈢ 尽量使用全局变量进行参数传递
使用全局变量最大的好处是寻址直观,只需在C 语言定义的变量名前增加一个下划线。

符即可在汇编语句中寻址;使用全局变量进行参数传递的效率也比形参高。编写单片机的C程序时不能死硬强求教科书上的模块化编程而大量采用行参和局部变量的做法,在开发编程时应视实际情况灵活变通,一切以最高的代码效率为目标。

十、PICC中实现C语言与汇编语言混合编程

pset  program section 程序段定位

函数调用时参数的传递:

可以通过一个由问号?、下划线_及函数名加一个偏移量构成的标号获取。

事例如下:

unsigned char add_function(unsigned char augend,unsigned char addend);

void main(void)

{

unsigned char temp1,temp2,temp3;

tem3=add_function(temp1,temp2);

}

unsigned char add_function(unsigned char augend,unsigned char addend)

{

return(augend + addend);

}

编译后生成的汇编程序:

_main

; _temp2 assigned to?a_main+0

;_temp3 assigned to ?a_main+1

; _temp1 assigned to ?a_main+2

bcf status,5

bcf status,6

movf (((?a_main+0))),w

movwf(((?_add_function)))

movf (((?a_main+2))),w

fcall (_add_function)

movwf(((?a_main+1)))

_add_function

; _augend assigned to ?a_add_function+0

; _augend stored from w

bcf status,5

bcf status,6

movwf(((?a_add_function+0)))

movf (((?a_add_function+0))),w

addwf (((?_add_function+0))),w

return

        一般情况下,主程序都是用C语言编写的。C语言与汇编语言最大的区别在于,汇编程序执行效率较高,因为,C语言首先要用C编译器生成汇编代码,在不少情况下,C编译器生成的汇编代码不如用手工生成的汇编代码效率高。在PICC中,可以用两种方法在C程序中调用汇编程序。一种方法是使用#asm,#endasm及asm()在C语言中直接嵌入汇编代码,#asm和#endasm指令分别用于标示嵌入汇编程序块的开头和结属;asm()用于将单条汇编指令嵌入到编译器生成的代码中,如下所示:

void func1(void){

asm("NOP");

#asm

nop

rlf_var,f

#endasm

asm("rlf_var,f");

}

需要注意的是,嵌入汇编不是完整意义上的汇编,是一种伪汇编指令,使用时必须注意它们与编译器生成代码之间的互相影响。

另一种方法是将汇编作为一个独立的模块,用汇编编译器(ASPIC)生成目标文件,然后用链接器和C语言生成的其它模块的目标文件链接在一起。如果变量要公用时,则在另一个模块中说明为外部类型,并允许使用形式参数和返回值。

例如,如果在C模块中使用汇编模块中的函数,那么在C中可知下声明:

extern char rotate_left(char);

本声明说明了要调用的这个外部函数有一个char型形式参数,并返回一个char型的值。而rotate_left()函数的真正函数体在外部可以被ASPIC编译的汇编模块(文件名后缀.as)中,具体代码可以如下编写:

processor 16C84

PSECT text0,class=CODE,local,delta=2

GLOBAL _rotate_left

SIGNAT _rotate_4201

_rotate_left

movwf?a_rotate_left

rlf?a_rotate_left,w

return

FNSIZE _rotate_left,1,0

GLOBAL ?a_rotate_left

END

需要注意的是,以C模块中声明的函数名称,在汇编模块中是以下划线开头的。GLOBAL定义了一个全局变量,也等同于C模块中的extern,SIGNAL强制链接器在链接各个目标文件模块时进行类型匹配检查,FNSIZE定义局部变量和形式参数的内存分配。

这种方法比较麻烦,如果对某一模块的执行效率要求较高时,可以采取这种办法;但是,为了保证汇编程序能正常运行,必须严格遵守函数参数传递和返回规则。当然,为避免这些规则带来的麻烦,一般情况下,可以先用C语言大致编写一个类似功能的函数,预先定义好各种变量,采用PICC-S选项对程序进行编译,然后手工优化编译器产生的汇编代码后将其作为独立的模块就可以了。




『本文转载自网络,版权归原作者所有,如有侵权请联系删除』

热门文章 更多
STM32中断向量表的位置.重定向