Hitech-PICC: https://www.wendangku.net/doc/274721944.html,
IAR:https://www.wendangku.net/doc/274721944.html,
CCS:https://www.wendangku.net/doc/274721944.html,/picc.shtml
ByteCraft:https://www.wendangku.net/doc/274721944.html,/mpccaps.html
本章将介绍 Hitech-PICC 编译器的一些基本概念,由于篇幅所限将不涉及 C 语言的标准
语法和基础知识介绍,因为在这些方面都有大量的书籍可以参考。重点突出针对 PIC 单片机的特点而所需要特别注意的地方。
11.2
Hitech-PICC 编译器
PICC 基本上符合 ANSI 标准,除了一点:它不支持函数的递归调用。其主要原因是因
为 PIC 单片机特殊的堆栈结构。在前面介绍 PIC 单片机架构时已经详细说明了 PIC 单片机中的堆栈是硬件实现的,其深度已随芯片而固定,无法实现需要大量堆栈操作的递归算法;
另外在 PIC 单片机中实现软件堆栈的效率也不是很高,为此,PICC 编译器采用一种叫做“静态覆盖”的技术以实现对 C 语言函数中的局部变量分配固定的地址空间。经这样处理后产生出的机器代码效率很高,按笔者实际使用的体会,当代码量超过 4K 字后,C 语言编译出
的代码长度和全部用汇编代码实现时的差别已经不是很大(<10%),当然前提是在整个 C
代码编写过程中须时时处处注意所编写语句的效率,而如果没有对 PIC 单片机的内核结构、各功能模块及其汇编指令深入了解,要做到这点是很难的。
11.3
MPLAB-IDE 内挂接 PICC
PICC 编译器可以直接挂接在 MPLAB-IDE 集成开发平台下,实现一体化的编译连接和
原代码调试。使用 MPLAB-IDE 内的调试工具 ICE2000、ICD2 和软件模拟器都可以实现原
代码级的程序调试,非常方便。
首先必须在你的计算机中安装PICC编译器,无论是完全版还是学习版都可以和
MPLAB-IDE挂接。安装成功后可以进入IDE,选择菜单项Project Set Language Tool Locations…,打开语言工具挂接设置对话框,如图 11-1 所示:
图 11-1 MPLAB-IDE 语言工具设置对话框
在对话框中选择“HI-TECH PICC Toolsuite”栏,展开可执行文件组“Executable”后,
列出了将被 MPLAB-IDE 后台调用的编译器所用到的所有可执行文件,其中有汇编编译器“PICC Assembler”、C 原程序编译器“PICC Compiler”和连接定位程序“PICC Linker”。同
时在此列表中还显示了对应的可执行程序名,请注意在这里都是“PICC.EXE”。用鼠标分别
点击选中这三项可执行文件,观察对话框下面“Location”一栏中显示的文件路径,用“Browse…”按纽,从计算机中已经安装的 PICC 编译器文件夹中选择 PICC.EXE 文件。实
际上 PICC.EXE 只是一个调度管理程序,它会按照所输入的文件扩展名自动调用对应的编译
器和连接器,用户要注意的是 C 语言原程序扩展名用“.c”,汇编原程序用“.as”即可。
工具挂接完成后,在建立项目时可以选择语言工具为“HI-TECH PICC”,具体步骤可以
参阅第三章 3.1.3 节,此处不再重复。项目建立完成后可以加入 C 或汇编原程序,也可以加
入已有的库文件或已经编译的目标文件。最常见的是只加入 C 原程序。用 C 语言编程的好
处是可以实现模块化编程。程序编写者应尽量把相互独立的控制任务用多个独立的 C 原程序文件实现,如果程序量较大,一般不要把所有的代码写在一个文件内。
图 11-2 列出的是笔者建立的一个项目中所有 C 原程序模块,其中主控、数值计算、I2C 总线操作、命令按键处理和液晶显示驱动等不同的功能分别在不同的独立的原程序模块中实现。
图 11-2 C 语言多模块编程
11.4 PIC 单片机的 C 语言原程序基本框架
基于 PICC 编译环境编写 PIC 单片机程序的基本方式和标准 C 程序类似,程序一般由以
下几个主要部分组成:
&O1540; 在程序的最前面用#include 预处理指令引用包含头文件,其中必须包含一个编译器提供的“pic.h”文件,实现单片机内特殊寄存器和其它特殊符号的声明;
&O1540; 用“__CONFIG”预处理指令定义芯片的配置位;
&O1540; 声明本模块内被调用的所有函数的类型,PICC 将对所调用的函数进行严格的类型匹配检查;
&O1540; 定义全局变量或符号替换;
&O1540; 实现函数(子程序),特别注意 main 函数必须是一个没有返回的死循环。
下面的例 11-1 为一个 C 原程序的范例,供大家参考。
#include //包含单片机内部资源预定义
#include “pc68.h” //包含自定义头文件
//定义芯片工作时的配置位
__CONFIG (HS & PROTECT & PWRTEN & BOREN & WDTDIS);
//声明本模块中所调用的函数类型
void SetSFR(void);
void Clock(void);
void KeyScan(void);
void Measure(void);
void LCD_Test(void);
void LCD_Disp(unsigned char);
//定义变量
unsigned char second, minute, hour;
bit flag1,flag2;
//函数和子程序
void main(void)
{
SetSFR();
PORTC = 0x00;
TMR1H += TMR1H_CONST; LED1 = LED_OFF;
LCD_Test();
//程序工作主循环
while(1) {
asm(“clrwdt”);
Clock();
KeyScan();
Measure();
SetSFR();
}
}
//清看门狗
//更新时钟
//扫描键盘
//数据测量
//刷新特殊功能寄存器
11.5
PICC 中的变量定义
例 11-1 C 语言原程序框架举例
11.5.1 PICC 中的基本变量类型
PICC 遵循 Little-endian 标准,多字节变量的低字节放在存储空间的低地址,高字节放在高地址。
11.5.2 PICC 中的高级变量
基于表 11-1 的基本变量,除了 bit 型位变量外,PICC 完全支持数组、结构和联合等复合型高级变量,这和标准的 C 语言所支持的高级变量类型没有什么区别。例如:
数组:unsigned int data[10];
结构:struct commInData {
unsigned char inBuff[8];
unsigned char getPtr, putPtr;
};
联合:union int_Byte {
unsigned char c[2];
unsigned int i;
};
例 11-2 C 语言高级变量举例
11.5.3 PICC 对数据寄存器 bank 的管理
为了使编译器产生最高效的机器码,PICC 把单片机中数据寄存器的 bank 问题交由编程
员自己管理,因此在定义用户变量时你必须自己决定这些变量具体放在哪一个 bank 中。如果没有特别指明,所定义的变量将被定位在 bank0,例如下面所定义的这些变量:unsigned char buffer[32];
bit flag1,flag2;
float val[8];
除了 bank0 内的变量声明时不需特殊处理外,定义在其它 bank 内的变量前面必须加上
相应的 bank 序号,例如:
bank1 unsigned char buffer[32]; //变量定位在 bank1 中
bank2 bit flag1,flag2;
bank3 float val[8];
//变量定位在 bank2 中
//变量定位在 bank3 中
中档系列 PIC 单片机数据寄存器的一个 bank 大小为 128 字节,刨去前面若干字节的特
殊功能寄存器区域,在 C 语言中某一 bank 内定义的变量字节总数不能超过可用 RAM 字节数。如果超过 bank 容量,在最后连接时会报错,大致信息如下:
Error[000] : Can't find 0x12C words for psect rbss_1 in segment BANK1
连接器告诉你总共有 0x12C(300)个字节准备放到 bank1 中但 bank1 容量不够。显然,只有把一部分原本定位在 bank1 中的变量改放到其它 bank 中才能解决此问题。
虽然变量所在的 bank 定位必须由编程员自己决定,但在编写原程序时进行变量存取操
作前无需再特意编写设定 bank 的指令。C 编译器会根据所操作的对象自动生成对应 bank 设定的汇编指令。为避免频繁的 bank 切换以提高代码效率,尽量把实现同一任务的变量定位在同一个 bank 内;对不同 bank 内的变量进行读写操作时也尽量把位于相同 bank 内的变量归并在一起进行连续操作。
11.5.4 PICC 中的局部变量
PICC 把所有函数内部定义的 auto 型局部变量放在 bank0。为节约宝贵的存储空间,它
采用了一种被叫做“静态覆盖”的技术来实现局部变量的地址分配。其大致的原理是在编译器编译原代码时扫描整个程序中函数调用的嵌套关系和层次,算出每个函数中的局部变量字节数,然后为每个局部变量分配一个固定的地址,且按调用嵌套的层次关系各变量的地址可以相互重叠。利用这一技术后所有的动态局部变量都可以按已知的固定地址地进行直接寻址,用 PIC 汇编指令实现的效率最高,但这时不能出现函数递归调用。PICC 在编译时会严格检查递归调用的问题并认为这是一个严重错误而立即终止编译过程。
既然所有的局部变量将占用 bank0 的存储空间,因此用户自己定位在 bank0 内的变量字
节数将受到一定的限制,在实际使用时需注意。
11.5.5 PICC 中的位变量
bit 型位变量只能是全局的或静态的。PICC 将把定位在同一 bank 内的 8 个位变量合并
成一个字节存放于一个固定地址。因此所有针对位变量的操作将直接使用 PIC 单片机的位
操作汇编指令高效实现。基于此,位变量不能是局部自动型变量,也无法将其组合成复合型
高级变量。
PICC 对整个数据存储空间实行位编址,0x000 单元的第 0 位是位地址 0x0000,以此后
推,每个字节有 8 个位地址。编制位地址的意义纯粹是为了编译器最后产生汇编级位操作指
令而用,对编程人员来说基本可以不管。但若能了解位变量的位地址编址方式就可以在最后
程序调试时方便地查找自己所定义的位变量,如果一个位变量 flag1 被编址为 0x123,那么
实际的存储空间位于:
字节地址=0x123/8 = 0x24
位偏移=0x123%8 = 3
即 flag1 位变量位于地址为 0x24 字节的第 3 位。在程序调试时如果要观察 flag1 的变化,必须观察地址为 0x24 的字节而不是 0x123。
PIC 单片机的位操作指令是非常高效的。因此,PICC 在编译原代码时只要有可能,对
普通变量的操作也将以最简单的位操作指令来实现。假设一个字节变量 tmp 最后被定位在
地址 0x20,那么
tmp |= 0x80
tmp &= 0xf7
=> bsf
=> bcf
0x20,7
0x20,3
if (tmp&0xfe)
=> btfsc 0x20,0
即所有只对变量中某一位操作的 C 语句代码将被直接编译成汇编的位操作指令。虽然编程时可以不用太关心,但如果能了解编译器是如何工作的,那将有助于引导我们写出高效简介的 C 语言原程序。
在有些应用中需要将一组位变量放在同一个字节中以便需要时一次性地进行读写,这一
功能可以通过定义一个位域结构和一个字节变量的联合来实现,例如:
union {
struct {
unsigned b0: 1;
unsigned b1: 1;
unsigned b2: 1;
unsigned b3: 1;
unsigned b4: 1;
unsigned b5: 1;
unsigned : 2; //最高两位保留
} oneBit;
unsigned char allBits;
} myFlag;
例 11-3 定义位变量于同一字节
需要存取其中某一位时可以
myFlag.oneBit.b3=1; //b3 位置 1
一次性将全部位清零时可以
myFlag.allBits=0; //全部位变量清 0
当程序中把非位变量进行强制类型转换成位变量时,要注意编译器只对普通变量的最低
位做判别:如果最低位是 0,则转换成位变量 0;如果最低位是 1,则转换成位变量 1。而标准的 ANSI-C 做法是判整个变量值是否为 0。另外,函数可以返回一个位变量,实际上此返回的位变量将存放于单片机的进位位中带出返回。
11.5.6 PICC 中的浮点数
PICC 中描述浮点数是以 IEEE-754 标准格式实现的。此标准下定义的浮点数为 32 位长,
在单片机中要用 4 个字节存储。为了节约单片机的数据空间和程序空间,PICC 专门提供了
一种长度为 24 位的截短型浮点数,它损失了浮点数的一点精度,但浮点运算的效率得以提高。在程序中定义的 float 型标准浮点数的长度固定为 24 位,双精度 double 型浮点数一般也是 24 位长,但可以在程序编译选项中选择 double 型浮点数为 32 位,以提高计算的精度。
一般控制系统中关心的是单片机的运行效率,因此在精度能够满足的前提下尽量选择
24 位的浮点数运算。
11.5.7 PICC 中变量的绝对定位
首先必须强调,在用 C 语言写程序时变量一般由编译器和连接器最后定位,在写程序
之时无需知道所定义的变量具体被放在哪个地址(除了 bank 必须声明)。
真正需要绝对定位的只是单片机中的那些特殊功能寄存器,而这些寄存器的地址定位在
PICC 编译环境所提供的头文件中已经实现,无需用户操心。编程员所要了解的也就是 PICC
是如何定义这些特殊功能寄存器和其中的相关控制位的名称。好在 PICC 的定义标准基本上
按照芯片的数据手册中的名称描述进行,这样就秉承了变量命名的一贯性。一个变量绝对定
位的例子如下:
unsigned char tmpData @ 0x20; //tmpData 定位在地址 0x20
千万注意,PICC 对绝对定位的变量不保留地址空间。换句话说,上面变量 tmpData 的
地址是 0x20,但最后 0x20 处完全有可能又被分配给了其它变量使用,这样就发生了地址冲突。因此针对变量的绝对定位要特别小心。从笔者的应用经验看,在一般的程序设计中用户自定义的变量实在是没有绝对定位的必要。
如果需要,位变量也可以绝对定位。但必须遵循上面介绍的位变量编址的方式。如果一
个普通变量已经被绝对定位,那么此变量中的每个数据位就可以用下面的计算方式实现位变量指派:
unsigned char tmpData @ 0x20; //tmpData 定位在地址 0x20
bit tmpBit0 @ tmpData*8+0; //tmpBit0 对应于 tmpData 第 0 位
bit tmpBit1 @ tmpData*8+1; //tmpBit0 对应于 tmpData 第 1 位
bit tmpBit2 @ tmpData*8+2; //tmpBit0 对应于 tmpData 第 2 位
如果 tmpData 事先没有被绝对定位,那就不能用上面的位变量定位方式。
11.5.8 PICC 的其它变量修饰关键词
&O1540; extern —外部变量声明
如果在一个 C 程序文件中要使用一些变量但其原型定义写在另外的文件中,那么在本
文件中必须将这些变量声明成“extern”外部类型。例如程序文件 code1.c 中有如下定义:bank1 unsigned char var1, var2;
//定义了 bank1 中的两个变量
在另外一个程序文件 code2.c 中要对上面定义的变量进行操作,则必须在程序的开头定义:extern bank1 unsigned char var1, var2; //声明位于 bank1 的外部变量
&O1540; volatile —易变型变量声明
PICC 中还有一个变量修饰词在普通的 C 语言介绍中一般是看不到的,这就是关键词“volatile”。顾名思义,它说明了一个变量的值是会随机变化的,即使程序没有刻意对它进行任何赋值操作。在单片机中,作为输入的 IO 端口其内容将是随意变化的;在中断内被修改的变量相对主程序流程来讲也是随意变化的;很多特殊功能寄存器的值也将随着指令的运行而动态改变。所有这种类型的变量必须将它们明确定义成“volatile”类型,例如:volatile unsigned char STATUS @ 0x03;
volatile bit commFlag;
“volatile”类型定义在单片机的 C 语言编程中是如此的重要,是因为它可以告诉编译器的优化处理器这些变量是实实在在存在的,在优化过程中不能无故消除。假定你的程序定义了一个变量并对其作了一次赋值,但随后就再也没有对其进行任何读写操作,如果是非volatile 型变量,优化后的结果是这个变量将有可能被彻底删除以节约存储空间。另外一种情形是在使用某一个变量进行连续的运算操作时,这个变量的值将在第一次操作时被复制到
中间临时变量中,如果它是非 volatile 型变量,则紧接其后的其它操作将有可能直接从临时变量中取数以提高运行效率,显然这样做后对于那些随机变化的参数就会出问题。只要将其定义成 volatile 类型后,编译后的代码就可以保证每次操作时直接从变量地址处取数。
&O1540; const —常数型变量声明
如果变量定义前冠以“const”类型修饰,那么所有这些变量就成为常数,程序运行过
程中不能对其修改。除了位变量,其它所有基本类型的变量或高级组合变量都将被存放在程序空间(ROM 区)以节约数据存储空间。显然,被定义在 ROM 区的变量是不能再在程序
中对其进行赋值修改的,这也是“const”的本来意义。实际上这些数据最终都将以“retlw”的指令形式存放在程序空间,但 PICC 会自动编译生成相关的附加代码从程序空间读取这些常数,编程员无需太多操心。例如:
const unsigned char name[]=”This is a demo”; //定义一个常量字符串
如果定义了“const”类型的位变量,那么这些位变量还是被放置在 RAM 中,但程序
不能对其赋值修改。本来,不能修改的位变量没有什么太多的实际意义,相信大家在实际编程时不会大量用到。
&O1540; persistent —非初始化变量声明
按照标准 C 语言的做法,程序在开始运行前首先要把所有定义的但没有预置初值的变
量全部清零。PICC 会在最后生成的机器码中加入一小段初始化代码来实现这一变量清零操作,且这一操作将在 main 函数被调用之前执行。问题是作为一个单片机的控制系统有很多变量是不允许在程序复位后被清零的。为了达到这一目的,PICC 提供了“persistent”修饰词以声明此类变量无需在复位时自动清零,编程员应该自己决定程序中的那些变量是必须声
明成“persisten”类型,而且须自己判断什么时候需要对其进行初始化赋值。例如:persistent unsigned char hour,minute,second; //定义时分秒变量
经常用到的是如果程序经上电复位后开始运行,那么需要将 persistent 型的变量初始化,
如果是其它形式的复位,例如看门狗引发的复位,则无需对 persistent 型变量作任何修改。PIC 单片机内提供了各种复位的判别标志,用户程序可依具体设计灵活处理不同的复位情形。
11.5.9 PICC 中的指针
PICC 中指针的基本概念和标准 C 语法没有太多的差别。但是在 PIC 单片机这一特定的
架构上,指针的定义方式还是有几点需要特别注意。
&O1540; 指向 RAM 的指针
如果是汇编语言编程,实现指针寻址的方法肯定就是用 FSR 寄存器,PICC 也不例外。
为了生成高效的代码,PICC 在编译 C 原程序时将指向 RAM 的指针操作最终用 FSR 来实现
间接寻址。这样就势必产生一个问题:FSR 能够直接连续寻址的范围是 256 字节(bank0/1 或 bank2/3),要覆盖最大 512 字节的内部数据存储空间,又该如何让定义指针?PICC 还是将这一问题留给编程员自己解决:在定义指针时必须明确指定该指针所适用的寻址区域,例如:
unsigned char *ptr0; //①定义覆盖 bank0/1 的指针
bank2 unsigned char *ptr1; //②定义覆盖 bank2/3 的指针
bank3 unsigned char *ptr2; //③定义覆盖 bank2/3 的指针
上面定义了三个指针变量,其中①指针没有任何 bank 限定,缺省就是指向 bank0 和 bank1;
②和③一个指明了 bank2,另一个指明了 bank3,但实际上两者是一样的,因为一个指针可以同时覆盖两个 bank 的存储区域。另外,上面三个指针变量自身都存放在 bank0 中。我们将在稍后介绍如何在其它 bank 中存放指针变量。
既然定义的指针有明确的 bank 适用区域,在对指针变量赋值时就必须实现类型匹配,
下面的指针赋值将产生一个致命错误:
unsigned char *ptr0;
bank2 unsigned char buff[8];
程序语句:
//定义指向 bank0/1 的指针
//定义 bank2 中的一个缓冲区
ptr0 = buff; //错误!试图将 bank2 内的变量地址赋给指向 bank0/1 的指针
若出现此类错误的指针操作,PICC 在最后连接时会告知类似于下面的信息:
Fixup overflow in expression (...)
同样的道理,若函数调用时用了指针作为传递参数,也必须注意 bank 作用域的匹配,
而这点往往容易被忽视。假定有下面的函数实现发送一个字符串的功能:
void SendMessage(unsigned char *);
那么被发送的字符串必须位于 bank0 或 bank1 中。如果你还要发送位于 bank2 或 bank3 内的字符串,必须再另外单独写一个函数:
void SendMessage_2(bank2 unsigned char *);
这两个函数从内部代码的实现来看可以一模一样,但传递的参数类型不同。
按笔者的应用经验体会,如果你看到了“Fixup overflow”的错误指示,几乎可以肯定
是指针类型不匹配的赋值所至。请重点检查程序中有关指针的操作。
&O1540; 指向 ROM 常数的指针
如果一组变量是已经被定义在 ROM 区的常数,那么指向它的指针可以这样定义:
const unsigned char company[]=”Microchip”;
const unsigned char *romPtr;
程序中可以对上面的指针变量赋值和实现取数操作:
romPtr = company; //指针赋初值
data = *romPtr++; //取指针指向的一个数,然后指针加 1
//定义 ROM 中的常数
//定义指向 ROM 的指针
反过来,下面的操作将是一个错误,因为该指针指向的是常数型变量,不能赋值。
*romPtr = data; //往指针指向的地址写一个数
&O1540; 指向函数的指针
单片机编程时函数指针的应用相对较少,但作为标准 C 语法的一部分,PICC 同样支持
函数指针调用。如果你对编译原理有一定的了解,就应该明白在 PIC 单片机这一特定的架
构上实现函数指针调用的效率是不高的:PICC 将在 RAM 中建立一个调用返回表,真正的
调用和返回过程是靠直接修改 PC 指针来实现的。因此,除非特殊算法的需要,建议大家尽
量不要使用函数指针。
&O1540; 指针的类型修饰
前面介绍的指针定义都是最基本的形式。和普通变量一样,指针定义也可以在前面加上
特殊类型的修饰关键词,例如“persistent”、“volatile”等。考虑指针本身还要限定其作用域,因此 PICC 中的指针定义初看起来显得有点复杂,但只要了解各部分的具体含义,理解一个
指针的实际用图就变得很直接。
㈠ bank 修饰词的位置含义
前面介绍的一些指针有的作用于 bank0/1,有的作用于 bank2/3,但它们本身的存放位置
全部在 bank0。显然,在一个程序设计中指针变量将有可能被定位在任何可用的地址空间,
这时,bank 修饰词出现的位置就是一个关键,看下面的例子:
//定义指向 bank0/1 的指针,指针变量为于 bank0 中
unsigned char *ptr0;
//定义指向 bank2/3 的指针,指针变量为于 bank0 中
bank2 unsigned char *ptr0;
//定义指向 bank2/3 的指针,指针变量为于 bank1 中
bank2 unsigned char * bank1 ptr0;
从中可以看出规律:前面的 bank 修饰词指明了此指针的作用域;后面的 bank 修饰词定义了此指针变量自身的存放位置。只要掌握了这一法则,你就可以定义任何作用域的指针且可以
将指针变量放于任何 bank 中。
㈡ volatile、persistent 和 const 修饰词的位置含义
如果能理解上面介绍的 bank 修饰词的位置含义,实际上 volatile、persistent 和 const 这些关键词出现在前后不同位置上的含义规律是和 bank 一词相一致的。例如:
//定义指向 bank0/1 易变型字符变量的指针,指针变量位于 bank0 中且自身为非易变型volatile unsigned char *ptr0;
//定义指向 bank2/3 非易变型字符变量的指针,指针变量位于 bank1 中且自身为易变型bank2 unsigned char * volatile bank1 ptr0;
//定义指向 ROM 区的指针,指针变量本身也是存放于 ROM 区的常数
const unsigned char * const ptr0;
亦即出现在前面的修饰词其作用对象是指针所指处的变量;出现在后面的修饰词其作用对象
就是指针变量自己。
11.6
PICC 中的子程序和函数
中档系列的 PIC 单片机程序空间有分页的概念,但用 C 语言编程时基本不用太多关心