用PICC编译器开发PIC系列单片机的代码

用PICC编译器语言开发PIC系列单片机的代码
Develop Codes of PIC series Microcontrollers Using PICC Complier

关键词:PIC PICC编译器 汇编语言 Hi-Tech
Key Words: PIC PICC Complier Assembly Language Hi-Tech

摘 要:介绍了PIC系列单片机C语言的发展,并以HI-TECH Software公司的HI-TECH PICC为例介绍了PICC编译器的特点和用其开发PIC系列单片机时的应注意的一些问题。
Abstract: The article introduces the developing process of C language used in PIC series microcontrollers, explains the features of HI-TECH PICC, one kind of PICC complier manufactured by HI-TECH Software. It also gives some notes on developing PIC utilizing HI-TECH PICC.

一. 前言
目前在市场上应用最广泛的应该属于8位单片机,Microchip Technoloogy公司推出的8位PIC系列单片机目前在国内市场上深受用户欢迎,已经逐渐成为单片机应用的新潮流,但遗憾的是,目前国内介绍它的C语言开发工具的书籍和文章却比较少,而且用的人也不多,广大的程序员在用其开发的过程中都在慢慢摸索,可能会走一些弯路,笔者最近在用PIC的C语言时就遇到了好些问题,在这里笔者想就最近一段时间用PIC的C语言的一些经验和广大的底层软件程序员做一下交流和介绍,希望本文对用PICC开发PIC系列单片机的人有所帮助。
目前在国内用的比较多的是Hi-Tech的HI-TECH PICC编译器,而且目前市场上一些国内的PIC单片机仿真器也开始支持HI-TECH PICC编译格式,因此本文主要以Hi-Tech的PICC为基础介绍一下PIC的C语言的基本特点。

二. HI-TECH PICC的C语言开发工具的语言特点
PICC的C语言按ANSI C来定义,并进行了C语言的扩展,PICC和ANSI C有一个根本的区别就是PICC不支持函数的递归调用,这是因为PIC单片机内的堆栈大小是由硬件决定的,资源有限,所以不支持递归调用。它的数据也遵从标准C的数据结构,PICC的数据结构是以数据类型的形式出现的。PICC编译器支持的数据类型有位类型(bit)、无符号字符(unsigned char)、有符号字符(signed char)、无符号整形(unsigned int)、有符号整形(signed int)、无符号长整型(unsigned long)、有符号长整型(signed long)、浮点(float)和指针类型等,需要注意的是,PICC支持的多字节数据都采用低字节在前,高字节在后的原则,即一个多字节数,比如int型,在内存单元中存储顺序为低位字节存储在地址低的存储单元中,高位字节存储在地址高的存储单元中,程序员在用union定义变量时一定要注意这一特点。
PIC的C语言变量分为局部变量和全局变量,所有变量在使用前必须先定义后使用。全局变量是在任何函数之外说明的、可被任意模块使用的、在整个程序执行期间都保持有效的变量;局部变量在函数内部说明,局部变量有两种:自动变量和静态变量,缺省类型为自动变量,除非明确将其声明为静态变量,而且,所有的自动变量都被分配在寄存器页0,所以bank限定词不能用于自动变量,但可以用于静态的局部变量,当程序退出时,自动变量占用的空间释放,自动变量也就失去意义;静态变量是一种局部变量,因此只在声明他的函数内部有效,但它占用固定的存储单元,而这个存储单元不会被别的函数使用,因此其它函数可以通过指针访问或修改静态变量的值;静态变量在程序开始只初始化一次,因此若需只在某函数内部使用一变量,而又希望其值在2次函数调用期间保持不变,为实现程序模块化,则可将其声明为静态变量。例如以下声明中,有些为合法,有些为非法:
void max(void)
{
unsigned char var1; //合法声明
unsigned char bank1 var2; //非法声明
static unsigned char bank1 var3; //合法声明
unsigned char var4 = 0x02; //合法声明,每次调用都初始化
static unsigned char bank1 var5 = 0x02; //合法声明,但只初始化一次

…………
}

PICC编译器对局部变量及传递参数使用RAM覆盖技术,编译时,连接器会自动把一些不可能被同时调用的函数的自动变量区重叠在一起,以达到内存的高效利用,因此其内部RAM的利用效率非常高。

1、 位变量的使用
需要说明的是虽然PICC允许利用bit定义位变量,比如:
static bit init_flag;
但位变量不能定义为自变量,也能不能作为函数参数,但可以作为函数的返回值。而且位变量也不能被静态初始化。比如若只想在某一个函数中使用位变量flag,用如下两种方法都是错误的:

void max(void)
{
bit flag; //非法,位变量不能定义为局部自变量
…………
}
void max(void)
{
static bit flag = 1; //非法,位变量不能被静态初始化
…………
}
PICC 支持在结构中定义位成员,位成员按最低有效位在前的方式存储,位成员总是以8bit为单位进行分配,当当前字节分配满后再分配下一个字节,但位成员不会跨字节存放.,例如定义:

struct {
unsigned lo: 1;
unsigned mid: 1;
unsigned hi: 7;
} flag @ 0x60;

结构flag占用两个单元0x60和0x61, lo分配到0x60单元的第0位, mid分配到0x60单元的第1位, 由于位成员不会跨字节存放,故hi分配到0x61单元的0-6位(第6位为最高有效位),而0x60单元的2-7位则跳过。
当然,上述的结构定义也可不使用绝对地址定义:
struct {
unsigned lo: 1;
unsigned mid: 1;
unsigned hi: 7;
} flag;

  另外,不使用的位可用未命名的位成员来定义, 如果我们不使用mid, 就可定义为:
struct {
unsigned lo: 1;
unsigned : 1;
unsigned hi: 7;
}flag;

如果将一个整型数赋给位变量,只是将最低位赋给位变量,如果你是想要将一个整型
变量是否为0赋值给一个位变量,必须用:
bitvar = (char_var != 0);
而不能象有些编译器那样可以用:bitvar = char_var;
也不能用类型强制转换:bitvar = (bit)char_var;

2、 绝对地址变量的定义
在某些特殊情况下,程序员可能不需要PICC动态分配某些特殊的全局或静态的局部变量,而使用@address结构定义全局或静态的局部变量,例如:
static unsigned char var1 @ 0x60;
定义了一个称为var1的变量,使其定位于单元0x60,但是需要程序员注意的是,编译器并不保留任何存储空间,而只是将此变量分配到此地址,在编译时,编译器也不能给出任何的警告或错误提示,因此程序员在用绝对地址定义变量时,应该自己查看编译器产生的映象文件或符号表,必须保证绝对变量被分配了唯一的地址,尤其是当此绝对地址正好落在了PICC分配的自动变量区时,查错是很困难的。当然简单的方法就是程序员自己使用高端的一块寄存器区定义绝对变量,因为,PICC的变量先从低端分配的。

3、 使用可位寻址绝对地址变量
在某些特殊情况下,为编程方便,程序员可能需要定义如下变量:
static unsigned char flag_var; //非法
static bit flag0 @ (unsigned)& flag_var *8+0;
static bit flag1 @ (unsigned)& flag_var *8+1;
static bit flag2 @ (unsigned)& flag_var *8+2;
static bit flag3 @ (unsigned)& flag_var *8+3;
static bit flag4 @ (unsigned)& flag_var *8+4;
程序员希望这个变量(假设是故障标志)既可以按字节访问,也可按位访问,这在有些编译器中是可以的,但是在PICC中这样定义是不合法的,而只有将flag_var按如下绝对地址变量定义才可以定义可位寻址位:
static unsigned char flag_var @0x7f;
static bit flag0 @ (unsigned)& flag_var *8+0;
…………………………
但这样定义以后flag_var就成为绝对地址变量,就要程序员自己来保证此地址不会和PICC自己分配的变量冲突,这又带来一定的麻烦,经笔者测试,要解决此问题,还有一种方法,那就是使用联合和结构位成员的方法,例如上例可如下定义:
union{
struct {
unsigned flag0:1;
unsigned flag1:1;
unsigned flag2:1;
unsigned flag3:1;
unsigned flag4:1;
}bit_type;
unsigned char byte_type;

}flag_var;
这样定义以后,程序员既可以按照字节访问此故障标志,也可按位访问某一类型故障。例如:
flag_var.bit_type.flag0 = 1; //按位访问
if(flag_var.byte_type) //按字节访问
{
……………
}
这两种访问都是合法的,而且程序员也不用在担心绝对地址是否有冲突的问题。

4、const类型限定符
const类型限定符用来通知编译器一个目标具有常数值,不能被改变,被const定义的常量被放在ROM中,例如:
unsigned char const var1[] ={"Microchip"};
unsigned char const var2[] ={0x00,0x01,0x02,0x03};
这两种定义都是合法的,但若通过指针访问这些数组变量,必须将指针定义为常数字符指针才能访问。例如某函数声明为:
void func1(const unsigned char *ptr);
则调用常数数组的方法为:
void func2(void)
{
…………
func1((const unsigned char *)var1);
…………
}
这一点程序员一定要认真对待,以免编译运行错误很难查找故障。

5、可变型变量volatile类型限定符
可变型变量volatile类型限定符用来通知编译器某一变量不能保证在连续访问的条件下,其值不被改变,例如所有的与I/O口有关的变量在编译器自带的头文件中都是被声明为volatile类型,如下所示:
static volatile unsigned char PORTA @ 0x05;
需要程序员注意的是有可能在中断时被改变的变量应该被定义为volatile类型,尤其是编译时选择全局优化级别较高时,定义为volatile可以禁止编译器对此变量进行优化,这能够防止编译器进行程序优化时,将认为明显多余的可变型变量删除,我们可以查看一下编译后生成的编译列表文件就会发现,编译器对volatile目标的访问与对non-volatile的访问是不同的, 如对non-volatile目标置1是先将该变量清0后加1, 而对volatile目标置1是先将1放在W中后再将W赋值给可变型变量。
6、persistent类型限定符
按C的标准, PICC在编译时,所有的C变量在启动时都会调用clear_ram模块将其清为0,但在某些情况下,程序员希望在处理器复位后仍保持一些变量的值,比如在做抗干扰处理时,希望若是由于看门狗溢出造成的复位则保留变量的值,若还按照缺省定义,则C程序无法实现,此时则可使用persistent类型限定符使被其限定的变量在启动时不被清0,而保留原有的值,程序员可在程序中根据看门狗的情况自己判断是否清零,例如可用如下方法:
persistent unsigned char var1;//定义为变量var1为persistent类型

void initialization(void) //初始化函数
{
……………
if(TO) //发生看门狗中断时
{
…………
var1 = 0; //发生看门狗中断时不清变量var1
…………

…………


此处须注意的是自动变量不能使用persistent限定词。

7、bank1, bank2及bank3类型限定符
PICC在缺省情况下,将变量分配到RAM区的寄存器页0,所有的动态变量和函数参数也被缺省分配到寄存器页0,但是当寄存器页0分配满以后,PICC并不能自动将变量分配到寄存器页1,因此程序员在定义变量时,要根据情况将互相关联较多的一类变量应用bank1, bank2及bank3类型限定符指定在同一寄存器页,这样编译器在优化程序时,就不会频繁的修改寄存器页选择位。但须注意的是,动态变量不能使用bank限定符。例如以下定义都属于合法的声明:
static bank1 unsigned char var1; //var1变量被定位于bank1
bank3 unsigned char *ptr1;//声明指向定位于bank3的无符号字符的指针
static bank3 unsigned char * bank1 ptr1;
//声明指向定位于bank3的无符号字符的指针,但此指针定位于bank1


三. 函数调用时参数的传递
PICC函数参数的传递是根据被传参数的长度用W、被调函数的自动变量区域或被调函数的参数区域传递,传递代码比较高效,传递给函数的参数可以通过一个由问号“?”、下划线“_”及函数名加一个偏移量构成的标号获取,下面为一调用求和子程序的源代码:

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

void main(void)
{
unsigned char temp1,temp2,temp3;
temp3 = 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


四. PICC语言和汇编语言的混合编程
一般情况下,主程序都是用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)中,具体代码可以如下编写:

processor16C84

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

GLOBAL _rotate_left

SIGNAT _rotate_left,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,SIGNAT强制连接器在在连接各个目标文件模块时进行类型匹配检查,FNSIZE定义局部变量和形式参数的内存分配,关于这些伪指令的详细说明见参考文献3。
这种方法比较麻烦一些,但是如果程序员对某一模块的执行效率要求较高时,可以采取这种办法,但是,为了保证汇编程序能正常运行,必须严格遵守函数参数传递和返回规则,当然,为避免这些规则带来的麻烦,一般情况下,程序员可以先用C语言大致编写一个类似功能的函数,预先定义好各种变量,采用PICC-S选项对程序进行编译,然后手工优化编译器产生的汇编代码后将其作为独立的模块就可以了。

五. 注意事项
使用PICC时,为了更有效的利用资源,应注意以下几点:
(1) 尽量使用无符号数和字节变量。
(2) 在寄存器资源允许的情况下,对某些对执行效率要求较高的平级无相互调用函数中用到的内部变量,可将其定义为全局临时变量,编程时覆盖使用,这样可减少很多编译代码。而对于中断函数内部用到的变量,可用全局变量
(3)寄对于有一定汇编经验的人在开始使用PICC时,应多注意观看经编译后产生的汇编源代码,并应经常观看经正确编译连接后产生的映象文件(.MAP文件),在该文件中详细列出了分配给变量和代码的地址和生成代码的大小等信息。使用者可了解代码是否优化,变量分配是否合理,堆栈是否溢出等,从而写出高效简洁的C源代码。
(4)对于有一定汇编经验的人在开始使用PICC时,应多注意观看经编译后产生的汇编源代码,并应经常观看经正确编译连接后产生的映象文件(.MAP文件),在该文件中详细列出了分配给变量和代码的地址和生成代码的大小等信息。使用者可了解代码是否优化,变量分配是否合理,堆栈是否溢出等,从而写出高效简洁的C源代码。
(5) PICC在好多情况下,不支持类型强制转换,即在类型不匹配时须查验编译后的汇编代码,看是否正确,尤其是对指针操作的时候一定要注意。
(6) 对某位变量自操作时,比如求反,不可以直接简写,例如: !flag;
编译后不能正确产生代码,而须写成:flag = !flag;
(7)尽量选择全局优化编译选项,为保证寄存器页(包括程序存储期页面和RAM寄存器页)的正确转换,PICC的编译代码中有大量的变换寄存器页的代码,选择全局优化PICC会优化去大有关RP0、RP1、PCLAPH所增加的变换代码,从而加快程序执行速度,并节省大量的程序空间。
(8) 若有某一代码很短的函数被多个函数经常调用,最好将其定义为宏,因为若函数代码很短时,由于被调函数和调用函数不在同一代码页所产生的附加代码可能都会超过函数代码本身的长度。


六. 结论
经过作者在开发项目过程中的实践,PICC编译器产生的代码在有些时候虽然比较繁琐,但结构和逻辑性很强,开发效率大大提高,调试与维护都很方便。不论是从程序的开发速度、软件质量还是从程序的可维护性和可移植性上讲,PICC的优点绝非汇编语言所能比拟的。

相关