/*---------------------------------------------------------*/ Hi-Tech PICC 8bit MCU 的学习笔记 ---------- 混合编程篇 /*---------------------------------------------------------*/ 第七节 混合编程
C语言与汇编语言{zd0}的区别:汇编程序执行效率较高,C语言移植容易。 使用PICC,可用两种方法在C程序中,嵌入汇编代码: a. #asm和#endasm指令, 分别用于表示 嵌入汇编程序块的开头和结属 b. asm(" ") 用于将单条汇编指令嵌入到编译器生成的代码中 c. 将汇编作为一个独立的模块。 举例说明如下: void func1(void){ // 方式 a #asm nop rlf_var,f #endasm
// 方式 b asm("rlf_var,f"); } 注意: 嵌入汇编不是完整意义上的汇编,是一种伪汇编指令,使用时必须注意它们与编译器生成代码之间的互相影响。 汇编作为一个独立的模块的操作: 汇编作为一个独立的文件,用汇编编译器(ASPIC)生成目标文件,然后用链接器和C语言生成的其它模块的目标文件链接在一起。 如果变量要公用时,则在另一个模块中说明为外部类型,并允许使用形式参数和返回值。 举例说明如下:如果在C模块中使用汇编模块中的函数,那么在C中可知下声明 : extern char rotate_left(char);
本声明说明了要调用的这个外部函数有一个char型形式参数,并返回一个char型的值。 rotate_left()函数的真正函数体在外部可以被ASPIC编译的汇编模块(文件名后缀.as)中, 具体代码可以如下编写:
processor16C84 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
举例说明如下: 嵌入行内汇编的方法 for (;;) { asm("clrwdt"); //清看门狗 Task(); ClockRun(); asm("sleep"); asm("nop"); } 逐行嵌入汇编的方式 下面的一段嵌入汇编指令实现了将 0x20~0x7F 间的RAM 全部清零: #asm movlw 0x20 movwf _FSR clrf _INDF incf _FSR,f btfss _FSR,7 goto $-3 #endasm 整段嵌入汇编的方式 按C 语言的语法标准,所有 C 中定义的符号在编译后将自动在前面添加一下划线符“_”,因此,若要在汇编指令中寻址 C 语言定义的各类变量,一定要在变量前加上一“_”符号。 FSR 和 INDF 等所有特殊寄存器是以 C 语言语法定义的,因此汇编中需要对其寻址时前面必须添加下划线。对于 C 语言中用户自定义的全局变量,用行内汇编指令寻址时也同样必须加上“_” .
volatile unsigned char tmp; //定义位于 bank0 的字符型全局变量 void Test(void) { #asm clrf _STATUS movlw 0x10 movwf _tmp #endasm if (tmp==0x10) { ; } }
行内汇编寻址 C 全局变量(位于 bank0) 上面的例子说明了汇编指令中寻址 C 语言所定义变量的基本方法。PICC 在编译处理嵌入的行内汇编指令时将会原封不动地把这些指令复制成{zh1}的机器码。所有对 C 编译器所作的优化设定对这些行内汇编指令而言将不起任何作用。编程员必须自己负责编写{zg}效的汇编代码,同时处理变量所在的 bank 设定。对于定义在其它 bank 中的变量,还必须在汇编指令中加以明确指示。
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 bsf _STATUS,5 movlw 0x10 movwf _tmpBank1^0x80 bsf _STATUS,6 bcf _STATUS,5 movlw 0x20 movwf _tmpBank1^0x100 //tmpBank2=0x20 bsf _STATUS,6 bsf _STATUS,5 movlw 0x30 movwf _tmpBank1^0x180 //tmpBank1=0x30 #endasm } 行内汇编寻址 C 全局变量(非 bank0 变量) 通过上实例,我们可以掌握这样一个规律:在行内汇编指令中寻址 C 语言定义的全局变量时,除了在寻址前设定正确的 bank 外,在指令描述时还必须在变量上异或其所在 bank 的起始地址,实际上位于 bank0 的变量在汇编指令中寻址时也可以这样理解,只是异或的是 0x00,可以省略。如果你了解 PIC 单片机的汇编指令编码格式,上面异或的 bank起始地址是无法在真正的汇编指令中体现的,其目的纯粹是为了告诉 PICC 连接器变量所在的 bank,以便连接器进行 bank 类别检查。 汇编指令寻址 C 函数的局部变量 PICC 对自动型局部变量(包括函数调用时的入口参数)采用一种“静态覆盖”技术对每一个变量确定一个固定地址(位于 bank0),因此嵌入的汇编指令对其寻址时只需采用数据寄存器的直接寻址方式即可,{wy}要考虑的是如何才能在编写程序时知道这些局部变量的寻址符号(具体地址在{zh1}连接后才能决定,编程时无需关心)。一个最实用也是最可靠的方法是先编写一小段 C 代码,其中有最简单的局部变量操作指令,然后参考图 11-5(B)对话框选择“Compile to assembly only”,把此 C 原代码编译成对应的 PICC 汇编指令;查看 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 PICC 实现局部变量操作的寻址方式 基于上面得到的 PICC 编译后局部变量的寻址方式,我们在 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 += rrf ?_Test,w /inVar2 循环右移一位 } rrf ?_Test,f #endasm //结束嵌入汇编 嵌入汇编指令实现局部变量寻址操作 如果局部变量为多字节形式组成,例如整型数、长整型等,必须按照 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 rrf ?_RR_Shift16+1,f rrf ?_RR_Shift16+0,f #endasm } return(var); 嵌入汇编指令对多字节变量的操作 混合编程经验: C和汇编语言混合编程可以使单片机应用程序的开发效率和程序本身的运行效率达到 {zj0}的配合。 慎用汇编指令 相比于汇编语言,用 C 语言编程的优势是毋庸置疑的:开发效率大大提高、人性化的语句指令加上模块化的程序易于日常管理和维护、程序在不同平台间的移植方便。所以既然用了 C 语言编程,就尽量避免使用嵌入汇编指令或整个地编写汇编指令模块文件。PICC 已具备高效的优化功能,如果在写 C 原程序时就十分注意程序的编译和运行效率问题,加上PICC 的后道编译优化,{zh1}得到的代码效率不会比全部用汇编编写的代码差多少,尤其是程序量较大时。另外,PICC 对数据存储空间的利用率肯定比用户人工定位变量时的利用率要高,同时还提供完整的库函数支持。C 语言的语法功能强大,能够高效率地实现绝大部分控制和运算功能。因此,除了一些十分强调单片机运行时间的代码或 C 语言没有直接对应的操作可以考虑用汇编指令实现外,其它部分都应该用 C 语言编写。 变量的循环右移操作用 C 语言实现非常不方便,PIC 单片机已有对应的移位操作汇编指令,因此用嵌入汇编的形式实现效率{zg}。同时对移位次数的控制,本质上说变量 count 的递减判零也可以直接用汇编指令实现,但这样做节约不了多少代码,用标准的 C 语言描述更直观,更易于维护。 一句话:用了 C 语言后,就不要再老想着用汇编。 尽量使用嵌入汇编 这和上面的慎用汇编指令的说法并不矛盾。如果确实需要用汇编指令实现部分代码以提高运行效率,应尽量使用行内汇编,避免编写纯汇编文件(*.asm 文件)。虽然 PICC 支持 C 和汇编原程序模块存在于同一个项目中,但要编写纯汇编文件必须首先了解 PICC 特有的汇编语法结构。Hitech 公司提供了完整的文档介绍其汇编器的使用方法,有兴趣者可以从其网站上下载 PICC 的用户使用手册查看。 类似于纯汇编文件的代码也可以在 C 语言框架下实现,方法是基于 C 标准语法定义所有的变量和函数名,包括需要传递的形式参数、返回参数和局部变量,但函数内部的指令基本用嵌入汇编指令编写,只有{zh1}的返回参数用 C 语句实现。这样做后函数的运行效率和纯汇编编写时几乎一模一样,但各参数的传递统一用 C 标准实现,这样管理和维护就比较方便。例如下面的例 11-15 实现一个字节变量的偶校验位计算。
bit EvenParity(unsigned char data) { #asm swapf ?a_EvenParity+0,w 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 的{zd1}位即为偶校验位 if (data&0x01) return(1); else return(0); } //入口参数 data 的寻址符为 ?a_EvenParity+0
C 函数框架中使用嵌入汇编指令, 尽量使用全局变量进行参数传递 使用全局变量{zd0}的好处是寻址直观,只需在 C 语言定义的变量名前增加一个下划线符即可在汇编语句中寻址;使用全局变量进行参数传递的效率也比形参高。编写单片机的 C程序时不能死硬强求教科书上的模块化编程而大量采用行参和局部变量的做法,在开发编程时应视实际情况灵活变通,一切以{zg}的代码效率为目标
注意: 以C模块中声明的函数名称,在汇编模块中是以下划线开头的。 GLOBAL定义的全局变量,等同于C模块中的extern,SIGNAL 强制链接器在链接各个目标文件模块时进行类型匹配检查。 FNSIZE定义局部变量和形式参数的内存分配。 这种方法比较麻烦,如果对某一模块的执行效率要求较高时,可以采取这种办法; 但是,为了保证汇编程序能正常运行,必须严格遵守函数参数传递和返回规则。 当然,为避免这些规则带来的麻烦,一般情况下,可以先用C语言大致编写一个类似功能的函数, 预先定义好各种变量,采用PICC-S选项对程序进行编译,然后手工优化编译器产生的汇编代码后将其作为独立的模块就可以了。
|