单片机与嵌入式
51单片机的寻址方式
51单片机的汇编指令系统
51汇编语言的伪指令
51汇编语言程序结构与程序示例
Keil C51程序设计
Keil C51的库函数
PIC单片机的基本结构
PIC单片机的汇编语言指令
PIC单片机的C语言编程
ATmega16单片机基本结构
ATmega16单片机的汇编语言
ATmega128单片机的结构
STM32单片机基础
使用STM32CubeMX开发STM32单片机
uC/OS-II嵌入式操作系统
uC/OS-II在STM32F10xx上的移植
FreeRTOS系统介绍
Linux系统介绍
Linux系统编程
嵌入式Linux编程
C语言是一种函数型高级语言,嵌入式C程序是用用一条条指令构成函数,然后用函数组合起来构成程序。
1. main()函数:
每个C语言至少要有一个函数,也就是main()函数。main()函数是整个C语言程序的基础,是程序代码执行的起始点。所有其他函数是由main()函数直接或间接调用的。
main()函数被认为是最低等级的任务,是系统启动程序时第一个被调用的函数。最简单的嵌入式C程序为:
void main()
{
while(1) //
;
}
嵌入式微控制器的应用程序一定包含一个无限循环,因为没有操作系统,任何时候都不允许执行完毕。
C语言是一种无格式的语言,会忽略空白区域,除非被引号括起来。空白区域包括空格、Tab、换行(回车和换行)。
2. 标识符:
标识符是变量或函数的名字,由字母或下划线开头,其后是由字母、数字或下划线构成的一个串。标识符通常区分大小写,编译器中#case指令可以用来控制是否区分大小写。
标识符可以是任意长度,但一些编译器只识别有限长度的字符,例如前32位。一些特殊的字符串是编译器的保留字,有特定意义,不能作为标识符。保留字如下:
auto |
default |
for |
int32 |
return |
union |
break |
defined |
goto |
register |
short |
unsigned |
bit |
do |
if |
return |
signed |
void |
byte |
double |
inline |
short |
sizeof |
volatile |
case |
else |
int |
unsigned |
static |
while |
char |
enum |
int1 |
struct |
const |
extern |
int8 |
long |
switch |
continue |
float |
int16 |
typedef |
|
|
|
|
|
3. 变量:
变量由一个表明变量类型和比特数的保留字加上一个标识符来声明。变量存储在微控制器的有限的内存中,为了避免浪费内存空间,编译器需要知道需要分配多少空间,所以变量必须声明,指明变量的类型和比特数。
标准类型 |
编译器默认类型 |
比特数 |
取值范围 |
bit |
int1,shout |
1 |
0,1 |
char |
int8,int,signed char,short int |
8 |
-128~+127 |
unsigned char |
unsigned int,unsigned char |
8 |
0~255 |
signed char |
int8,int,signed char,short int |
8 |
-128~+127 |
int |
int16,long int |
16 |
-32768~32767 |
short int |
|
16 |
-32768~32767 |
unsigned int |
unsigned long int |
16 |
0~65535 |
signed int |
|
16 |
-32768~32767 |
long int |
int32 |
32 |
-2147483648~2147483647 |
unsigned long int |
unsigned int32 |
32 |
0~4294967295 |
signed long int |
signed int32 |
32 |
-2147483648~2147483647 |
float |
float |
32 |
±1.175e-38~±3.402e38 |
double |
|
32 |
±1.175e-38~±3.402e38 |
变量可以声明为局部变量和全局变量。局部变量是在输入函数时函数分配的内存空间,这些变量不能被其他函数访问,即局部变量的使用范围仅限于声明它的函数内。全局变量和外部变量可以被程序中所有的函数使用,全局变量可以被任何函数修改和使用。
4. 常量:
常量是固定不变的值,在程序执行时不能被改变。常量一般存储在ROM中,而不是存储在可修改的RAM的分配区域中。
数值常量可以有多种声明方式。整数或长整数常量可以写成下面的形式:
·十进制形式:无前缀,例如1234。
·二进制形式:以0b为前缀,例如0b101001。
·十六进制形式:以0x为前缀,例如0xff。
·八进制形式:以0为前缀,例如0777。
还有一些常量的修饰符,用以更好地定义常量的比特数和用途:
·无符号整数常量可以后缀U,例如10000U。
·长整数常量可以加上后缀L,例如99L。
·无符号长整数常量可以加上后缀UL,例如99UL。
·浮点数常量可以加上后缀F,例如1.234F。
·字符常量必须由单引号括起来,例如’a’或’A’。
字符常量是指可以输出的或者不可输出的字符。可输出的字符(0~9和A~Z等)可以用单引号括起来,也可以用反斜杠后跟字符的八进制或十六进制的ASCII码来表示,如:
‘t’可以表示成’\164’(八进制)或’\x74’(十六进制)
不可输出字符,包括换行、回车等。下面列出了一些C语言识别的封装好的不可输出字符:
BEL(beep) |
‘\a’ |
‘\x07’ |
|
VT |
‘\v’ |
‘\x0b’ |
Backspace |
‘\b’ |
‘\x08’ |
|
FF |
‘\f’ |
‘\x0c’ |
TAB |
‘\t’ |
‘\x09’ |
|
CR |
‘\r’ |
‘\x0d’ |
LF(new line) |
‘\n’ |
‘\x0a’ |
|
|
|
|
因为反斜杠(\)和单引号(‘’)已被使用,字符输出时必须在前面加上反斜杠以免使编译器混淆。例如,’\’是一个单引号字符,’\\’是一个反斜杠字符。
枚举,就是可列表表示的常量。用保留字enum用来将连续的整型常数值赋给一系列的标识符,例如:
enum{start=10,next1,next2,end_val);
5. 存储分类:
变量可以声明成三种存储类型:automatic、static和register。
①自动类型Automatic:
自动类型的局部变量在获得分配空间时是未初始化的,其存储空间在函数退出时会释放,其值会丢失,而且在函数再次进入时又会是一个无效值。自动类型是默认的存储类型,保留字auto可以省略。
②静态类型Static:
静态局部变量的使用范围仅限于定义它的函数内(其他函数不能访问),但是它的存储空间是分配在全局存储空间内的。动态变量在函数首次进入时初始化为0,在函数退出时会保持其值,函数再入时静态变量的值总是有效的而且是最新的。
③寄存器类型Register:
寄存器类型的局部变量也是暂时性的和未初始化的,与自动类型相似。但编译器会使用微处理器中的实际硬件寄存器来存储寄存器变量,以减少访问变量所需的时钟周期。微处理器内部的寄存器数量比较少,因此寄存器变量需要节省使用,只有明确需要加速某个进程的时候才会使用。
6. 类型转换:
有些情况下,程序员可能希望暂时强制改变变量的类型和比特数。类型转换由类型保留字加上括号构成,作用于其后给出的一些声明和赋值:示例:
int x=12;
char y;
y=(char)x+3;
x=(int)y;
7. 运算符的优先级:
运算符是表达式中的连接标识符,指示编译器要执行的类型操作。一个表达式中往往有多个运算符,其操作顺序依照优先级执行。
类别 |
级别 |
运算符 |
组合 |
基础 |
1(高) |
() [] -> |
从左至右 |
一元 |
2 |
! ~ - (type) * & ++ -- sizeof |
从右至左 |
二元 |
3 |
* / % |
从左至右 |
算术 |
4 |
+ - |
从左至右 |
移位 |
5 |
<< >> |
从左至右 |
关系 |
6 |
< <= > >== |
从左至右 |
相等性 |
7 |
== != |
从左至右 |
位 |
8 |
& |
从左至右 |
位 |
9 |
^ |
从左至右 |
位 |
10 |
| |
从左至右 |
逻辑 |
11 |
&& |
从左至右 |
逻辑 |
12 |
|| |
从左至右 |
条件 |
13 |
?: |
从右至左 |
赋值 |
14(低) |
= += -= /= *= %= <<= >>= &= ^= |= |
从右至左 |
8. 函数:
函数,是一个封装起来的可在一个程序中多次调用的程序块。函数通常用来将程序的功能分解成一些基本的部分。在开发出执行特定任务的函数之后,可以保存它们并在以后用于不同的应用中,甚至可以被其他程序员使用,因此节省了开发的时间,也保持了稳定性。
函数包含一个类型标识符,后跟一个函数名,再跟一个基本运算符()。函数的类型或者它的参数可以是任何有效的变量类型。
函数的类型和它的参数类型必须在调用之前声明。C编译器提供了很多库函数,可以通过”#include”指令引入到程序中。
9. 指针:
指针类型变量中存储的是变量、常量、函数或数据对象的地址或位置信息。声明指针类型变量需要用间接运算符(*)。示例:
char *p; //声明一个指向字符变量的指针
int *fp; //声明一个指向整数变量的指针
指针数据类型会在内存中分配一个足够保存变量的机器地址的空间。例如,典型的8位微控制器的内存位置的地址为16bit,因此一个指针会是16位的值,虽然对应的字节只是8位的。
一旦声明了指针变量,那么操作的就是变量对应的地址,而不是指针变量的数值。取地址运算符(&)可用来获得一个变量的地址,这个地址可以赋给一个指针变量。间接运算符(*)可用来获得指针变量所指向的空间的数值。示例:
char *p; //声明一个指向字符变量的指针p
char a,b; //声明两个字符变量a和b
p=&a; //p为指向字符变量a的指针
示例中,a的地址值赋给了p,于是p就指向了字符变量a。
为了得到p指向的变量的值,使用间接运算符(*),当执行指令时间间接运算符把p的值作为地址来查询内存的位置,然后就可以根据保护间接运算符的表达式对这个位置的值进行读出或写入操作。示例:
b=*p; //b为指针p指向字符变量的内容
所以,上述代码组合起来,得到的结果与下面语句的结果相同:
b=a;
间接运算符也可以出现在赋值语句的右边:
char *p; //声明一个指向字符变量的指针p
char a,b; //声明两个字符变量a和b
p=&a; //p为指向字符变量a的指针
*p=b; //b的值赋给了p中保存的地址值指向的内存单元
上述操作,都可以理解为“p所指向的变量的值赋给了b”和“a的地址赋给了p”。
指针也是访问系统外部设备的采用方法,例如I/O口。示例:读取内存中位于0x1010地址的一个8位并行输出端口:
unsigned char *my_port; //声明一个指向无符号字符变量的指针my_port
my_port=0x1010; //为my_port赋地址值
*my_port=0xaa; //my_port地址单元的内容赋值0xaa
在上面的代码中,值0xaa会赋给my_port指向的地址单元。
在C语言中,还可以定义指向指针的指针,而且这种间接操作也没有深度限制,只是容易造成混淆。
因为指针是有效地址值,所以可以通过一些非常少的指令用各种方法来完成移动、复制和改变内存。
指针可以用来扩充一个函数返回的信息量。因为一个函数只能通过return返回一个项,如果把指针作为参数传递给函数,就可以返回更多信息。示例:
void swap2(int *a, int *b)
{
int temp;
temp=*b;
*b=*a;
*a=temp;
}
上述函数实现了a和b所指向的变量值的交换。调用者将欲交换的变量的地址传给函数:
int v1,v2;
swap2(&v1, &v2)
因为swap2()函数使用的是传给它的地址值,实现了变量v1和v2的值的直接交换。这种传递指针的方式经常用到,在标准函数库中就有,例如函数scanf()。scanf()函数允许由标准输入端依照一定格式输入多个参数,并将它们存储在相应的内存空间中。调用格式:
int x,y,z;
scanf(“%d %d %d”,&v1, &x,&y,&z);
上面的函数scanf()调用时会从标准输入端获取3个十进制整数,并将它们分别赋值给x、y和z。
10. 数组:
数组是一组声明了类型并依据一定顺序排列的数据的集合。示例:
char str[20]; //声明一个有20个字符变量的数组
int digits[10]; //声明一个有10个整数的数组
对数组元素的引用需要用到索引,也称为下标。下标范围从0到声明的数组长度值减1。
str[0],str[1],str[2],......str[19]
数组声明可以包含初始化设置。变量数组,初始化设置的数值会保存在程序的内存空间中,并在main()函数执行前复制到实际的数组空间中;而常量数组,会分配在程序空间中,以节省微控制器上有限的RAM存储空间。示例:
int array[5]={12,15,27,56,94};
C语言没有提供对数组边界的检查,如果待赋值的元素的索引超出了数组的边界,那么可能带来无法预料的结果。
最普遍的数组类型是字符数组,通常被看成一个字符串。在常量字符串中,C编译器会使用null作为终止,或在串的末尾加上一个0。在声明字符串时,不论是常量还是变量,都要将数组的大小声明成比内容需要的长度大1,以留出null终止符的位置。
一个数组名后面跟一个下标值可以引用任何类型的数组的单个元素,也可以仅用数组的名字来引用数组的第一个元素。当没有具体的下标值时,数组名被当成数组的第一个元素的地址看待。
C语言支持多维数组,当一个多维数组声明后,它应当被看成是一个数组的数组。示例:
int two_d[5][10];
数组的元素会以连续行的方式存储如下:
two_d[0][0],two_d[0][1],two_d[0][2],...,two_d[0][9],
two_d[1][0],two_d[1][1],two_d[1][2],...,two_d[1][9],
two_d[2][0],two_d[2][1],two_d[2][2],...,two_d[2][9],
two_d[3][0],two_d[3][1],two_d[3][2],...,two_d[3][9],
two_d[4][0],two_d[4][1],two_d[4][2],...,two_d[4][9],
当一个二维数组初始化时,其布局也是一样,也是连续的行:
int matrix[3][4]={0,1,2,3,4,5,
6,7,8,9,10,11|;
另一种较常用的二维数组是字符串数组。示例:
char day_of_the_week[7][10]={“Sunday”,”Monday”,
“Tuesday”,”Wednesday”,
“Thursday”,”Friday”,”Saturday”};
在上面的数组中,个字符串的长度是不等的。编译器放置一个null终止符在每个字符串的最后。像printf()这样的函数从头开始显示字符串直到遇到null终止符为止,这样就可以正确地显示不同长度的字符串。
字符串的名称可以当成第一个字符的地址来用。
11. 函数的指针:
使用函数指针,可以让函数通过查询表操作的结果来调用。函数指针还可以让函数以引用的方式传递给其他函数。这可以用来创建一个动态的执行流程,有时也称“自修改”的代码。
(*fp)(); //执行fp所指向的函数
Fp保存一个函数的入口地址。因为PIC单片机的栈空间有限,有些编译器不支持指向函数的指针。
12. 结构:
结构体是用一个或多个变量来创建一个单个数据对象的方法,包含在结构体内部的变量称为结构体的成员,这种方法使得一组成员可以通过一个统一的名字来引用。一些高级语言中,将结构类型的对象称为一个记录。与数组不同,结构内的变量不必是同一种类型。
结构体声明的格式:
struct struct_name{
type member_1;
type member_2;
......
type member_x;
};
或
struct struct_name{
type member_1;
type member_2;
......
type member_x;
}struct_var_name;
一旦定义了一个结构体模板,struct_name就成了一个通用的描述符,可用来在程序中声明此种类型的结构体。下面声明的就是两个结构体,var1和var2及一个结构体数组var3:
struct struct_name var1,var2,var3[5];
结构体模板可以包含各种类型的变量,包括其他类型的结构体、函数指针、结构体指针等。当定义了一个模板后,并没有分配任何的内存空间,而要在实际的结构体变量声明时才分配内存空间。
结构体中的成员可通过成员运算符(.)访问,成员运算符连接成员名和其他相关的结构体:
struct_var_name.member_1
与数组相似,结构体可以通过在结构体名后面接上由括起来的一系列初始值来初始化:
struct DATE{
int month;
int day;
int year;
};
struct DATE date_of_birth={2,21,1961};
上面的代码与下面的赋值语句结果相同:
date_of_birth.month=2;
date_of_birth.day=21;
date_of_birth.year=1961;
因为结构体本身就是一个有效的类型,所以对在结构体内的成员嵌套没有限制。结构体可以作为参数传递给函数,也可以作为函数的返回值。
也能声明结构体数组,结构体数组可以通过在结构体名后面跟上由括号括起来的一列初始值来初始化,只需要将数组中的每个结构体的成员的初始值顺次排列即可。
有时需要以通用的方式来操作结构体的成员,使用指针来引用结构体。一个结构体的指针声明为:
struct struct_name *struct_var_name;
指针运算符(*)表明struct_var_name是一个struct_name类型的结构体的指针。当使用结构体指针来引用结构体对象时,结构体指针运算符->会被用来间接访问结构体的成员:
struct_var_name->member_1=1234;
另一种方式表示:先通过间接运算符指向结构体的第一个位置,再用成员运算符来访问成员:
*(struct_var_name.member_1);
结构体可以包含指向其他类型结构体的指针,也可以包含指向同种类型结构体的指针。结构体不能包含自身作为成员,即不能递归声明。通过包含一个指向相同类型结构体的指针作为成员,结构体可以达到“自引用”的效果。自引用的结构体通常用于数据操作,例如链表和快速排序。
13. 共用体(联合):
共用体的声明和访问与结构体很类型。
struct union_name{
type member_1;
type member_2;
......
type member_x;
};
或
struct union_name{
type member_1;
type member_2;
......
type member_x;
}union_var_name;
共用体与结构体之间的主要区别在于内存的分配方式。共用体的成员实际上是共享一段内存空间,而这个空间是按照最大成员的大小分配的。
共用体有时作为节省宝贵内存空间的一种方法。如果某些变量只有一段暂时的用途,而且它们不可能被同时用的,就可以定义一个共用体来包含这些变量作为成员,从而共享一块缓冲存储区。
更常见的情况是:共用体被用作一种从较大的数据对象提取出一些较小的数据块的方法。例如,编译器采用big-endian或little-endian存储方式,存储数据的方式各不相同。声明共用体可以节省将数据从一种格式转换成另一种格式的步骤。
14. typedef操作符:
C语言支持创建新的数据类型,typedef操作符允许将一个名字声明为与一个现有类型相同的类型。示例:
typedef unsigned char byte;
typedef unsigned int word;
通过上面的声明,byte和word就成了unsigned char和unsigned int类型的别名,可以用来声明其他的变量:
byte var1;
word var2;
定义别名的方法也适用于结构体和共用体类型:
typedef struct{
char name[20];
char age;
int room_num;
}student;
student Bob;
student Sally;
#define语句有时也用来定义别名,它是通过在编译器的预处理阶段实行文本替换来实现的。Typedef则是由编译器直接处理的,而且可以和其他的声明、类型转换等操作一起使用。
15. 位和位段:
一些编译器支持bit类型或int1类型,这些类型也是由编译器自动分配空间,并且可以像其他类型的变量一样引用。示例:
bit running=1;
位的值只有0和1两种。
位段在较大型的更通用的系统中使用得更加普遍,但不是所有编译器都支持。其声明与结构相关:
struct struct_name{
unsigned int bit_1:1;
unsigned int bit_2:1;
......
unsigned int bit_15:1;
};
或
struct struct_name{
unsigned int bit_1:1;
unsigned int bit_2:1;
......
unsigned int bit_15:1;
}struct_var_name;
位段的声明由一个成员名(只能是unsigned int类型)加上一个冒号(:),后面再跟着一个表明数值所占位数的数字构成。一个成员的宽度最小为1,最大为unsigned int类型的宽度(16位)。这使得一个结构体中的多个位段可以用一个无符号整数存储区间来代表。
struct {
unsigned int running:1;
unsigned int stopped:1;
unsigned int counter:4;
}machine;
上面定义的几个位段可以通过它们成员的名来访问,就像访问结构体一样:
machine.stopped=1;
machine.running=0;
machine.counter++;
有时,在嵌入式系统中,位段用来描述I/O端口的情况:
typedef struct{
unsigned int bit_0:1;
unsigned int bit_1:1;
unsigned int bit_2:1;
unsigned int bit_3:1;
unsigned int bit_4:1;
unsigned int bit_5:1;
unsigned int bit_6:1;
unsigned int bit_7:1;
}bits;
下面的#define标记了位于0x08内存单元中的存储内容,这可以使得程序员在整个程序中用PORTD标记来访问位于0x08的内存单元,把它当作一个变量:
#define PORTD (*(bits *) 0x08)
bits位段使得I/O端口PORTD的各个位可以独立地访问(有时称为bit banged):
PORTD.bit_3=1;
PORTD.bit_5=0;
16. sizeof操作符:
C语言支持sizeof一元操作符,用于编译阶段,操作结果是创建与数据对象的大小或其类型相关的常量值。Sizeof操作的形式有如下两种:
sizeof(type_name) //type_name可以是int、char、long等数据类型
sizeof(object) //object可以是变量、数组、结构、共用变量名
这些操作产生了等于其括号内操作数比特数的一个整数值。
17. 存储器类型:
PIC微控制器使用Harvard体系结构,区分数据空间(SRAM)、程序空间(FLASH或EPROM)和EEPROM空间。如果不使用存储器描述关键字,默认或自动的变量空间分配在SRAM中;常量通过const关键字标识存储在FLASH中。对于存储在EEPROM中的变量,读写其中的数据比较复杂,有相关类库函数来处理。
除了少数例外,PIC单片机不允许对其程序空间进行直接访问,指令集中有一条指令RETLW,当程序调用这条指令时就从程序空间RETLW指令所在位置的一个W寄存器读取一个常量值返回。常量数据是指令的一部分。示例:
int const table[5]={1,3,5,7,9};
这个表会被放入到FLASH程序空间,可以按照如下方式访问它:
x=table[i];
实际上,编译器处理这个操作是按类似于下面的这个方法来执行的:
int table(int index)={
switch(i)={
case 0:return1;
case 1:return3;
case 2:return5;
case 3:return7;
case 4:return9;
end;
};
x=table(i);
对应的汇编语言代码则是:
BCF 0A.0
BCF 0A.1
BCF 0A.2
ADDWF 02,F
RETLW 01
RETLW 03
RETLW 05
RETLW 07
RETLW 09
为了节省有限的SRAM存储空间,常量往往放置于程序空间。
EEPROM空间的变量分配和访问都要由程序员手动完成,使用结构体、枚举和#define语句可以提高代码的可读性。编译器的类库会提供访问EEPROM的函数,例如:
unsigned char read_eeprom(unsigned char address);
void write_eeprom(unsigned char address,unsigned char value);
在上面的两个函数中,地址的取值范围为0~255,与PIC的型号也有关系。数据宽度8位,长整数和浮点数需要多次读取才能获得完整的值。如果应用中需要大小不同的数据类型,往往使用基址、数据和需要移动的字节数的方法来调用函数。因为EEPROM有擦写次数的限制,使用EEPROM设计软件时,通常用于保存不常改变的数据,适用于低速的数据日志、校准表、实时的时间尺度和软件的安装和配置值等情况。
PIC微控制器的SRAM区域中包含一个名为寄存器的区域,这个区域包含I/O端口、定时器及其他外围设备的控制和设置。如果要告诉编译器将变量分配到寄存器,则必须使用存储类型修饰符register。示例:
register int abc;
即使不使用register,编译器也可以选择自动为变量分配寄存器。如果要防止变量分配到寄存器,必须使用volatile修饰符,这样可以警告编译器变量在计算时可能要受到外界变化的影响。
I/O端口和外部设备的控制被保存在寄存器中,要用一些特殊的指令来告诉编译器寄存器用作存储变量。#bit和#byte关键字指示编译器编译器某些特定的地址和访问指令。
#byte PORTD=0x08
#bit PORTD0=PORTD.0
#byte PORTC=0x07
#bit PORTC2=PORTD.2
对I/O寄存器的位级访问,可以通过在I/O寄存器后面添加位选择符来实现。因为对I/O寄存器的位级访问是通过使用汇编语言的指令BSF、BCF、BTFSC和BTFSS来完成,所以定义位名时必须使用#bit关键字。
通常#bit和#byte关键字是先在头文件中出现,再通过#include预处理指令引入程序中,这些头文件往往是针对某个特定的处理器写的,头文件中提供了为I/O端口和其他一些要用在特定应用中的微控制器中的寄存器预先确定的名字。
下面是16F877.h头文件的一部分:
#define CCP_OFF 0
#define CCP_CAPTURE_FE 4
#define CCP_CAPTURE_RE 5
#define CCP_CAPTURE_DIV_4 6
#define CCP_CAPTURE_DIV_16 7
#define CCP_CAPTURE_SET_ON_MATCH 8
#define CCP_CAPTURE_CLR_ON_MATCH 9
#define CCP_COMPARE_INT 0xA
#define CCP_COMPARE_RESET_TIMER 0xB
#define CCP_PWM 0xC
#define CCP_PWM_PLUS_1 0x1C
#define CCP_PWM_PLUS_2 0x2C
#define CCP_PWM_PLUS_3 0x3C
long CCP_1;
#byte CCP_1 0x15
#byte CCP_1_LOW= 0x15
#byte CCP_1_HIGH= 0x16
long CCP_2;
#byte CCP_2 0x1B
#byte CCP_2_LOW= 0x1B
#byte CCP_2_HIGH= 0x1C
18. 预处理指令:
预处理指令实际上并不是C语言语法的一部分,是独立于一个程序实际编译过程的一个步骤,它是发生在实际编译过程开始之前。
⑴#include:
#include指令用来将其他文件引入到源文件中。可以按照需要或者按照编译器允许的规则将任意数量的文件包括进来,而且这种包括是可以嵌套的,即包括的文件中还可以包含另外一个文件,但是不能递归地使用。一般情况下,嵌套的深度极限一般是8~16个文件。
语句格式:
#include <file_name>
或
#include “file_name”
<>用作分隔符,表示包含的文件是标准库函数的一部分或者库函数文件的一个集合,一般会在编译器目录”\inc”中寻找相应文件。当文件名是用引号分隔符时,编译器会首先在与被编译的C文件同一目录中寻找相应文件,如果这个文件不存在,那么再去默认的库文件目录”\inc”中寻找需要的文件。
#include指令可以在程序的任何部位使用,但一般来说都会在程序模块的顶部使用,这样可以增强程序的可读性。
⑵#define:
#define可以被看作是“文本替代”,也就是宏命令。标准形式如下:
#define NAME Replacement_Text
一般情况下,使用#define声明变量,但是NAME以及Replacement_Text同样也可以有参数,编译器会将NAME替代为Replacement_Text部分。示例:
#define ALPHA 0xff
#define SUM(a,b) a+b
那么接下来的表达式:
int x=0x3def&ALPHA;
int i=SUM(2,3);
将会被替代成:
int x=0x3def&0xff;
int i=2+3;
#define指令的多样性以及功能在一定程度上依赖于编译器的预处理指令的复杂程度。
定义宏时,可以使用#操作符来将宏参数转换成字符串。例如:
#define print_msg(t) print(#t)
将会把表达式:
print_msg(Hello);
替代为如下的表达式:
print(“Hello”);
也可以使用##操作符,例如:
#define alpha(a,b) a##b
char alpha(x,y)=1;
将被替换为:
char xy=1;
一般情况下,替代文本连同标记名称一起定义在同一行程序中。可以在一行末尾使用反斜杠符号“\”来将宏定义扩展到新的一行。
一个宏可以用#undef指令来取消定义,主要用于条件编译。#undef指令允许以前定义过的宏可以重新定义。
⑶#ifdef、#ifndef、#else和#endif:
#ifdef、#ifndef、#else和#endif指令用于条件编译。语法为:
#ifdef macro_name
[set of statements 1]
#else
[set of statements 2]
#endif
如果macro_name是一个定义过的宏名称,那么#ifdef表达式求值就为TRUE,于是就会编译set of statements 1语句;否则就会编译set of statements 2语句。#else和set of statements 2是可选的。如果macro_name未被定义过,那么#ifndef表达式求值就为TRUE,可以用以下语法:
#ifndef macro_name
[set of statements 1]
#else
[set of statements 2]
#endif
#if、#elif、#else和#endif指令也可以用作条件编译:
#if expression1
[set of statements 1]
#elif expression2
[set of statements 2]
#else
[set of statements 3]
#endif
以上条件编译语句和C语言的if语句很相似。编译器会逐条计算表达式的值,直到有一条的值为TRUE,紧跟着第一条表达式为TRUE的语句将会被编译到程序中,而接下来直到#endif的表达式及其语句都会被忽略。如果没有一个表达式的值为TRUE,并且有#else语句,那么紧跟着这条#else语句后面的语句将会被编译到程序中。
在创建一个运行在几个不同设置中的程序时,条件编译非常有用,使一个源代码可以应用在几个操作设置中。条件编译也常用于调试。
⑷#error:
在对源文件进行编译的过程中,如果遇到一个#error指令,那么它会强迫编译器产生一个错误。此指令用来警告用户在编译过程中的非法情况。语法:
#error text
在text中可以包括宏,显示过程中会自动进行扩展。示例:
#define BUFFER_SIZE 18
#if BUFFER_SIZE>16
#error Buffer size is too large-Current declared as BUFFER_SIZE
#endif
生成的错误信息是:缓冲区过大-当前声明值为18。
⑸#pragma:
#pragma语句是同编译器有着很大关联的,有些编译器支持,而有些不支持。
19. MPLAB C30关键字对ANSI C的扩展:
1)指定变量的属性:
MPLAB C30的关键字__attribute__用来指定变量或结构位域的特殊属性,后面双括号中的内容是属性说明。也可以通过在变量前后使用“__”来指定属性(例如用__aligned__来代替aligned),这样将使在头文件中使用时不必考虑会出现与宏同名的情况。要指定多个属性,可在双括号内用逗号将属性分隔开,例如__attribute__((aligned(16),packed))。
目前支持的变量属性:
⑴address(addr):
Address属性为变量指定决定地址。这个属性不能与section属性同时使用,address属性优先。带address属性的变量不能存放到PSV空间,这样做会产生警告,且编译器在进行编译时,会将存放到PSV空间的变量忽略。
如果要将变量存放到PSV段,地址应为程序存储器地址。例如“
int var __attribute__((address(0x800)));
⑵aligned(alignment):
该属性为变量指定最小的对齐方式,用字节表示,对齐方式必须是2的幂。例如:
int x __attribute__((aligned(16)))=0;
含义是使编译器按照16字节分配全局变量x。
对于dsPIC器件,可以访问需要对齐的操作数的DSP指令和寻址模式的asm语句配合使用。可以显式地指定希望编译器对给定变量使用的对齐方式(用字节表示),也可以省略对齐方式。当省略了对齐属性说明中的对齐方式,编译器会自动将以声明变量的对齐方式设置为目标单片机任何数据类型所使用的最大对齐方式。在dsPIC器件中,为双字节(1个字)。
aligned属性只能增大对齐,但可以通过指定packed属性来减小对齐。
aligned属性与reverse属性冲突,同时指定两者会产生错误。
⑶deprecated:
Deprecated属性使得包含这一属性的声明能被编译器特别识别到。当使用deprecated函数或变量时,编译器会发出警告,但deprecated定义仍将被编译器执行,并被反映到目标文件中。例如,编译以下程序:
int __attribute__((__deprecated__)) i;
int main(){
return i;
}
将产生警告:
deprecated.c:4:warning:’i’is deprecated (declared at deprecated.c:1)
但在生成的目标文件中,仍以通常的方式定义了i。
⑷far:
far属性告知编译器不必将变量分配到near(前8KB)数据空间中(即变量可以分配到数据存储器中的任何地址)。
⑸mode(mode):
在变量声明中,使用该属性来指定与模式mode对应的数据类型。实际上就是允许根据变量的宽度指定整数或浮点数类型。模式的有效值如下表:
模式 |
QI |
HI |
SI |
DI |
SF |
DF |
宽度/位 |
8 |
16 |
32 |
64 |
32 |
64 |
MPLAB C30 类型 |
char |
int |
long |
long long |
float |
long double |
这一属性对于编写可在所有MPLAB C30支持的目标单片机之间移植的代码很有用。例如,如下的函数将2个32位有符号整数相加,并返回1个32位有符号整数的结果:
typedef int __attribute__((__mode__(SI))) int32;
int32
add32(int32 a, int32 b)
{
return(a+b);
}
可以通过byte或__type__模式指明对应于单字节整数,通过word或__word__模式指明对应于单字整数,pointer或__pointer__模式用于表示指针。
⑹near:
near属性指明应该为变量分配到near数据空间(数据存储器的前8KB)。对这种变量的存取有时比存取未分配(或不知已分配)到near数据空间的变量效率高。例如:
int num __attribute__((near));
⑺noload:
noload属性指明应该为变量分配空间,但不应为变量装入初值。这一属性对于设计在运行时将变量装入存储器(如从串行EEPROM)的应用程序会有用。例如:
int table1[50] __attribute__((noload))={0};
⑻packed:
Packed属性指定变量或结构位域采用最小的可能对齐方式--变量占1字节、位域占1位(除非用aligned属性指定了一个更大的值)。下面的结构中,位域x被压缩,所以它紧接在a之后:
struct foo{
char a;
int x[2] __attribute__((packed));
};
注:dsPIC器件要求按偶数字节对齐,因此在使用packed属性时要特别小心,避免运行时寻址错误。
⑼persistent:
Persistent属性指定在启动时变量不应被初始化或清零。具有persistent属性的变量可用于存储器件复位后保持有效的状态信息。例如:
int last_mode __attribute__((persistent));
⑽reverse(alignment):
reverse属性为变量的结束地址加1指定最小对齐。对齐以字节指定,必须是2的次幂。反向对齐的变量可用于递减dsPIC汇编语言中的模缓冲区。如果应用程序要求在C中定义的变量可从汇编语言访问,这一属性会有用。例如:
int buf1[128] __attribute__((reverse(256)));
reverse属性与aligned和section属性冲突。试图为反向对齐的变量指定一个段将被忽略,并发出警告。为同一个变量同时指定reverse和aligned会产生错误。带有reverse属性的变量不能存放到auto_psv空间,试图这样做将导致警告,且编译器会将变量存放到PSV空间。
⑾section(“section-name”):
默认情况下,编译器将其生成的目标代码存放在.data和.bss段中。Section属性允许指定变量(或函数)存放到特定的段中。例如:
struct array{int i[32];}
struct array buf __attribute__((section(“userdata”)))={0};
section属性与address和reverse属性冲突。在这两种冲突情况下,段名将被忽略,并发出警告。这一属性还可能与space属性冲突。
⑿sfr(address):
sfr属性告知编译器将变量分配到near数据空间(数据存储器的前8KB),同时使用address参数指定变量运行时的地址。对这种变量的存取有时比存取未分配(或不知已分配)到near数据空间的变量效率高。例如:
extern volatile __attribute__((sfr(0x200)))u1mod;
为避免产生错误,需要使用extern说明符。
⒀space(space):
一般来说,编译器在一般数据空间内分配变量,但可使用space属性来指示编译器将变量分配到特定存储空间。Space属性接受的参数包括:data、xmemory、ymenory、prog、aotu_psv、psv和eedata。
·data:将变量分配到一般数据空间。可使用普通C语句访问一般数据空间中的变量,这是默认的分配。
·xmenory:将变量分配到x数据空间。可使用普通C语句访问x数据空间中的变量。示例:
int x[32] __attribute__((space(xmemory)));
·ymenory:将变量分配到y数据空间。可使用普通C语句访问y数据空间中的变量。示例:
int y[32] __attribute__((space(ymemory)));
·prog:将变量分配到程序空间中为可执行代码指定的段。程序空间中的变量不能使用普通C语句访问,这些变量必须由编程人员显式访问,通常通过表访问行内汇编指令,或使用程序空间可视性(PSV)窗口访问。
·aotu_psv:将变量分配到程序空间中为自动程序空间可视性窗口访问指定的编译器管理段。auto_psv空间中的变量可使用普通C语句来读(但不能写),且变量的分配空间最大为32KB。当指定space(auto_psv)时,不能使用section属性指定段名,任何段名将被忽略并产生警告。auto_psv空间中的变量不能存放到特定地址或反对齐。
注:在启动时,分配到auto_psv段中的变量不装载到数据存储器,这对减少RAM使用有用。
·psv:将变量分配到程序空间中为程序空间可视性窗口访问指定的段。链接器将定位段,因此可以通过PSVPAG寄存器的设置来访问整个变量。PSV空间中的变量不是由编译器管理的,不能使用普通C语句访问。这些变量必须由编程人员显式访问,通常使用表访问行内汇编指令,或使用程序空间可视性窗口访问。
·eedata:将变量分配到EEDATA空间。EEDATA空间中的变量不能使用普通C语句访问,这些变量由编程人员显式访问,通常使用表访问行内汇编指令,或使用程序空间可视性窗口访问。
⒁transparent_union:
这是属于union型函数参数的属性,即相应的参数可以是任何联合成员的类型,但以第一个联合成员的类型传递参数。使用transparent联合的第一个成员的调用约定将参数传递给函数,而不是使用联合本身的调用约定。联合的所有成员用相同的机器码表示,这对于保证参数传递正常进行是必须的。
⒂unordered:
unordered属性表明变量存放的地址可以相对于所在C源文件中其他变量的位置而改变。这不符合ANSI C,但可使链接器更好地利用存储空隙。例如:
const int __attribute__((unordered)) i;
⒃unused:
这一变量属性表明变量可能不被使用。MPLAB C30不会为这种变量产生未使用变量警告。
⒄weak:
weak属性声明weak符号,weak符号可能被全局定义取代。当对外部符号的引用使用weak时,则链接时不需要该符号。例如:
extern int __attribute__((__weak__)) s;
int foo(){
if(&s) return s;
return 0; //possibly some other value
}
在上面的程序中,如果s没有被其他模块定义,程序仍会链接,但不会给s分配地址。若条验证s已被定义,就返回其值(如果有其值的话);否则将返回0值。这个特性很有用,主要用于提供与任意库链接的调用代码。
weak属性可以应用于函数和变量,例如:
extern int __attribute__((__weak__)) compress_data(void *buf);
int process(void *buf){
if(compress_data){
if (compress_data(buf)==-1); //error
}
//process buf
}
在上述代码中,函数compress_data()只有在于其他模块链接时才使用。是否使用该特性,是在链接时决定的,而不是在编译时决定的。
weak属性对定义的影响更为复杂,需要多个文件加以说明,例如:
/* weak1.c */
int __attribute__((__weak__)) i;
int foo(){
i=1;
}
/* weak2.c */
int i;
extern void foo(void);
void bar(){
i=2;
}
/* weak2.c */
main(){
foo();
bar();
}
在weak2.c中,对i的定义使符号成为强定义,链接时不会出现两个i指向同一个存储位置的错误。在weak1.c中为i分配存储空间,但这个空间不可访问。
不能保证两段程序里的i具有相同的类型,如果将weak2.c中的i改为float型,仍然允许链接,但是函数foo()的操作将无法预测,foo()将向32位浮点值的最低有效部分写入一个值。如果把weak1.c中i的weak定义改为float型,将导致灾难性结果。这样会把一个32位的浮点值写到16位的整型地址中,并覆盖掉紧接着i之后存储的任何变量。在只存在weak定义的情况下,链接器才会为第一个这种定义分配存储空间,其他定义是不可访问的。无论变量属于什么类型,操作是相同的,函数和变量具有相同的操作。
2)指定函数的属性:
在MPLAB C30中,可以对程序中调用的函数进行某些声明,以帮助编译器优化函数调用,以及能更准确地检查代码。关键字__attribute__允许在声明函数时指定特殊的属性,关键字后面双括号中的内容是属性说明。也可以通过在关键字前后使用双下划线“__”来指定属性(例如用__shadow__代替shadow),这样使得头文件中使用它们时,不必考虑会出现与宏同名的情况。如果想在声明中指定多个属性,可以在双括号内用逗号将属性分隔开,或者在一个属性声明后紧跟另一个属性声明。目前支持的函数属性有:
⑴address(addr):
address属性为函数指定绝对地址。例如:
void foo() __attribute__((address(0x100))){
……
}
这个属性不能与section属性同时使用,address属性优先。
⑵alias(“target”):
alias属性为另一个符号声明一个别名,必须指定这个符号。
使用这一属性会产生对对象的外部引用,必须在链接时解析该引用。
⑶const:
许多函数除了检查自身的参数外,不会检查任何其他值,这会影响到函数的返回值,所以可像算术运算符一样,对这种函数进行公共子表达式删除和循环优化。这些函数应该用属性const来声明。例如:
int square(int) __attribute__((const int));
也就是说,上述假设square函数的实际被调用次数即使比程序指定的次数少一些也是安全的。要注意,如果函数有指针参数,且检查指针指向的数据,那么这种函数一定不能用const声明;调用非const函数的函数通常也不声明为const;具有void返回值类型的const函数没有什么意义。
⑷deprecated:
会使编译器发出警告,参见变量属性的相关信息。
⑸far:
far属性告知编译器不应该用更有效的调用指令形式来调用该函数。
⑹format(archetype, string-index, first-to-check):
format属性指定一个函数具有printf、scanf和strftime三种类型参数之一,要根据格式字符串检查这些参数的类型。例如,考虑以下声明:
extern int my_printf(void *my_object, const char *my_format, ...)
__attribute__((format(printf,2,3)));
以上语句使编译器检查my_printf()中调用的参数,确定是否与printf类型的格式字符串参数my_format一致。
参数archetype确定如何解释格式字符串,应该为printf、scanf和strftime之一。参数string-index指定哪个参数是格式字符串参数(参数从左至右编号,从1开始)。参数first -to-check指定根据格式字符串检查的第一个参数的编号。对于不能检查参数的函数(如vprintf()),应指定第三个参数为0.这种情况下,编译器仅检查格式字符串的一致性。在上面的例子中,格式字符串(my_format)是函数my_print()的第二个参数,从第三个参数开始检查,所以format属性的正确参数是2和3。
Format属性允许识别以格式字符串作为参数的用户自定义函数,所以MPLAB C30可以检查对这些函数的调用有无错误。每当要求这种警告(使用-Wformat)时,编译器总会检查ANSI库函数printf()、fprintf()、sprintf()、scanf()、sscanf()、strftime()、vprintf()、vfprintf()和vsprintf()的格式,所以不必修改头文件stdio.h。
⑺format_arg(string-index):
format_arg属性指定一个函数具有printf或者scanf类型的参数,修改这个函数(如将它翻译为另外一种语言),该函数的结果将传递给printf或scanf类型的函数。例如:
extern char *my_dgettext(char *my_domain, const char *my_format)
__attribute__((format_arg(2)));
上述语句使编译器检查函数my_dgettext()中调用的参数,该函数的结果传递给printf、scanf和strftime三种类型函数之一,确定是否与printf类型的格式字符串参数my_format一致。参数string-index指定哪个参数是格式字符串参数(从1开始)。format_arg属性允许识别修改格式字符串的用户定义函数,所以MPLAB C30可以检查对printf()、scanf()或strftime()函数的调用,这些函数的操作数是对用户定义函数的调用。
⑻near:
near属性告知编译器可以使用call指令更有效的形式来调用函数。
⑼no_instrument_function:
如果指定命令行选项-finstrument-function,那么几乎所有用户函数的入口和出口处在编译时都会被插入profiling()函数。但当函数被指定此选项时,将不执行上述操作。
⑽noload:
noload属性指明应该为函数分配空间,但不应把实际代码装入存储器。如果应用程序设计为在运行时将函数装入存储器(如从EEPROM),这一属性很有用。函数声明如下:
void bar() __attribute__((noload)){
……
}
⑾noreturn:
一些标准库函数是不能返回的,例如abort()和exit(),MPLAB C30可自动清楚这些情况。有些程序自定义了不会返回的函数,就可以将这些函数声明为noreturn来告知编译器。例如:
void fatal(int i) __attribute__((noreturn));
void fatal(int i) {
//Print error message
exit(1);
}
noreturn关键字告知编译器fatal()函数不会返回。这样可以优化代码,而不必考虑如果fatal返回会怎样,并且这还有助于避免未初始化变量的假警告。对于noreturn()函数,非void的返回值类型并没有什么意义。
⑿section(“section-name”):
通常,编译器将生成的代码存放在.text段中,但有时可能需要其他的段,或者需要将某些函数存放在特殊的段中。section属性指定将一个函数存放在特定的段中。例如:
extern void foobar(void) __attribute__((section(“.libtext”)));
上述语句将函数foobar()存放在.libtext段中。
section属性与address属性有冲突,忽略段名会导致警告。
⒀shadow:
Shadow属性使编译器使用影子寄存器而不是软件堆栈来保存寄存器。该属性通常与interrupt属性同时使用。例如:
void __attribute__((interrupt, shadow)) _T1Interrupt(void)
interrupt [([save(list)][,irq(irqid)][,altirq(altirqid)]
[,preprologue(asm)])]
使用shadow选项来指明指定的函数是一个中断服务程序。当指定该属性时,编译器将生成适合在中断服务程序中使用的函数prologue()和epilogue()序列。可选参数save指定在函数prologue()和epilogue()中各自要保存和恢复的一系列变量。可选参数irq和altirq指定要使用的中断向量表ID。可选参数preprologue指定要在编译器生成的prologue()代码前生成的汇编代码。
⒁unused:
这个函数属性表明函数可能不会被使用。MPLAB C30不会为这种函数发出未使用函数的警告。
⒂weak:
参加变量属性的相关说明。
3)内联函数:
通过声明一个函数为inline,可以指示MPLAB C30将这个函数的代码集成到调用函数的代码中。通常这样可避免函数调用的开销,使代码执行速度更快。另外,若任何实际的参数值为常数,则其已知值可允许在编译时进行简化,这样不用包含所有的内联函数代码,对代码量的影响是不容易预估的。使用内联函数时,机器代码量视具体情况可能更大,也有可能更小。(注:仅在函数定义可见,即非只有函数原型时,才能使用函数内联。为将一个函数内联到多个源文件中,可将函数定义放在每个源文件包含的头文件中。)
为将函数声明为内联,在其声明中使用inline关键字,例如:
inline int inc(int *a){
(*a)++;
}
如果使用-traditional选择或-ansi选项,则用_inline_代替inline。另外,还可以通过使用命令行选项-finline-functions将所有“足够简单”的函数内联。
编译器可以根据对函数大小的估计,直观地决定哪些函数足够简单,然后进行集成。函数定义中的某些用法可能使函数不适合于内联替代,这些用法包括:varargs、alloca和长度可变数据的使用,以及相对goto和非局部goto的使用。如果使用了命令行选项-winline,当标识为inline的函数不能被替代时,则会发出警告,并给出失败原因。
在MPLAB C30语法中,关键字inline不会影响函数的链接。当一个函数同时为inline和static时,如果对该函数的所有调用都集成到调用函数中,且从不使用该函数的地址,那么该函数自身的汇编程序代码从不会被引用。在这种情况下,MPLAB C30实际上并不输出该函数的汇编代码,除非指定命令行选项-fkeep-inline-function。有些调用由于各种原因不能被集成,特别是在函数定义之前的调用不能被集成,定义内的递归调用也不能被集成。
如果存在非集成的调用,那么会以通常方式将函数编译成汇编代码。如果程序引用函数的地址,也必须以通常的方式编译函数,因为它不能被内联。仅在内联函数被声明为static,且函数定义在函数使用之前的情况下,编译器才会删除内联函数。
当inline函数不是static时,编译器必须假定其他源文件可能调用这个函数。因为全局符号只能在所有程序中定义一次,不能在其他源文件中定义该函数,所以其他源文件中的调用不能集成。因此,非static的内联函总是以通常的方式编译。
如果在函数定义中同时指定inline和extern,这样定义的函数就只能用来内联,不能以通常的方式编译该函数,即使显式地引用其地址。因为这种地址变成了一个外部引用,如同只是声明了函数却没有定义它。
同时使用inline和extern对于宏有类似的影响。使用这些关键字将一个函数的定义放在头文件中,并且将定义的另外一份拷贝(不带inline和extern)放在库文件中。头文件中的定义使得对于该函数的大多数调用被内联,如果还有任何使用该函数的地方,将引用库文件中的拷贝。
4)指定寄存器中的变量:
MPLAB C30允许把几个全局变量存放到指定的硬件寄存器中。全局寄存器变量在整个程序执行过程中保留寄存器的值,这在程序中可能很有用。
特定寄存器中的局部寄存器变量并不保留寄存器的值。编译器的数据流分析可以确定何时指定寄存器包含有效的的值,何时可将指定寄存器用于其它用途。局部寄存器变量不使用时,其中存储的值可被删除。如果要将汇编指令的一个输出直接写到某个特定的寄存器,那么局部变量有时会便于扩展行内汇编使用(只要指定的寄存器符合为行内汇编语句中的操作数指定的约束就可以)。
①定义全局寄存器变量:在MPLAB C30中,可通过以下语句来定义一个全局寄存器变量:
register int *foo asm(“w8”);
其中,w8是要使用的寄存器名。选择一个可被函数调用,并可正常保存和恢复的寄存器(W8~W13),这样库函数就不会被破坏它的值。将一个全局寄存器变量定义到某个寄存器中,可完全保留该寄存器的值,至少在当前的编译中可以做到。在当前的编译过程中,寄存器不会被分配给函数中的任何其他用途,也不会被这些函数保存和恢复,即使该寄存器的内容不被使用,也不会被删除。但是对该寄存器的引用可被删除、移动或简化。
从信号处理程序或者从多个控制线程访问全局寄存器变量是不安全的,因为系统函数可能临时使用寄存器做别的工作(除非特别为待处理任务重新编译它们)。同样不安全的是使用一个采用全局寄存器变量的函数时,通过函数lose()来调用另外一个这样的函数foo(),而编译函数lose()时未知该全局变量(即使未声明该变量的源文件中),这是因为lose()可能会将其他某个值保存到该寄存器中。例如,不能在比较函数中使用传递给qsort的全局寄存器变量,因为qsort可能已经把其他值存放到该寄存器中了。用相同的全局变量定义来重新编译qsort,可以避免此问题。如果想重新编译实际上没有使用该全局寄存器变量的qsort或其他源文件,由于这些源文件不会将该寄存器用于其它用途,那么指定编译器命令行选项-ffixed-reg就足够了。这种情况下,实际上不需要在其源代码中加入一个全局寄存器声明。
一个函数若能改变一个全局寄存器变量的值,它就不能安全地被不保存和恢复该变量编译的函数调用,因为这可能破坏调用函数返回时期望找到的值。因此,若一个程序片段使用了全局寄存器变量,则作为该程序片段入口的函数必须显式地保存和恢复属于其调用函数的值。
库函数longjmp()将恢复每个全局寄存器变量在setjmp时的值。
所有全局寄存器变量的声明必须在所有函数定义之前。如果这种声明在函数定义之后,寄存器可能被声明之前的函数用于其它用途。全局寄存器变量不能有初值,因为可执行文件不能为一个寄存器提供初值。
②为局部变量指定寄存器:
可以通过以下语句用一个指定的寄存器定义局部寄存器变量:
register int *foo asm(“w8”);
其中,w8是使用的寄存器名。这与定义全局寄存器变量的语法相同,但是对于局部变量,这种定义应该出现在一个函数中。
定义这种寄存器时,不保留寄存器的值。流控制确定变量的值无效时,其他用途仍可使用这种寄存器。使用这一功能,可能使编译某些函数时可用寄存器太少。同时,该选项并不能保证MPLAB C30生成的代码始终将这一变量存放在指定的寄存器中。
在asm语句中,不可以编写对该寄存器的显式引用,并假定它总是引用这个变量。局部寄存器变量不被使用时,其分配可被删除。对局部寄存器变量的引用也可以被删除、移动或简化。
③复数:
MPLAB C30支持复数数据类型。可以用关键字__complex__来声明整数复数和浮点型复数。例如:
__complex__ float x; //定义x为实部和虚部都是浮点型的变量
__complex__ short int y; //定义y为实部和虚部都是short int型的变量
要写一个复数数据类型的常量,使用后缀i或j(两者之一,是等同的)。例如,2.5fi是__complex__ float型的,3i是__complex__ int型的。这种常量只有虚部值,但是可以通过将其与实常数相加来形成任何复数值。要提取复数值符号exp的实部,写__real__ exp;用__imag__来提取虚部。例如:
__complex__ float z;
float r;
float i;
r=__real__ z;
i=__imag__ z;
当对复数型值使用算子“~”时,执行复数的共轭。
MPLAB C30可以采用非邻近的方式分配复数自动变量,甚至可以将实部分配到寄存器中,而将虚部分配到堆栈中,反之亦然。调试信息格式无法表示这种非邻近的分配,所以MPLAB C30把非邻近的复数变量描述为两个独立的非复数类型变量。如果实际变量名是foo,那么两个假设变量命名为foo$real和foo$imag。
④双字整型:
MPLAB C30支持长度为long int两倍的整型数据类型。对于有符号整型,使用long long int;而对于无符号整型,使用unsigned long long int。可以通过在整型上添加后缀LL,得到long long int类型的整型常量;在整数上添加后缀ULL,得到unsigned long long int类型的整型常量。
可以在算术运算中像使用其他整型一样使用这些类型。这些数据类型的加、减和位逻辑布尔运算是开放源代码的,但是这些数据类型的除法与移位不是开放源代码的。这些不开放源代码的运算要使用MPLAB C30自带的特殊库函数。
⑤用typeof引用类型:
引用表达式类型的另一种方法是使用typeof关键字。使用这个关键字的语法与sizeof相似,但是其结构在语义上类型于用typedef定义的类型名。
可以通过两种方法写typeof的参数:使用表达式或者使用类型。
以下为使用表达式的例子:
typeof(x[0](1))
这里假设x是函数数组,描述的类型就是函数值的类型。
以下为使用类型名作为参数的例子:
typeof(int *)
这里,描述的类型是指向int的指针。
如果写一个包含在ANSI C程序中时,必须有效的头文件,要使用__typeof__,而不要使用typeof。
typeof结构可用于可使用typedef名的任何地方。例如,可以在声明和强制类型转换中,或者在sizeof和typeof的内部使用它。
用x指向类型声明y:
typeof(*x)y;
将y声明为这种值的数组:
typeof(*x)y[4];
将y声明为指向字符的指针数组:
typeof(typeof(char *)[4])y;
它等同于如下的传统C声明:
char *y[4];
为了弄清楚typeof声明的含义,以及该方法的好处,用以下宏改写typeof:
#define pointer(T) typeof(T*)
#define array(T,N) typeof(T [N])
现在声明可以这样改写:
array(pointer(char),4) y;
这样,array(pointer(char),4)是指向char的4个指针的数组类型。
20. MPLAB C30语句对ANSI C的扩展:
1)将标号作为值:
可以用单目运算符“&&”获得当前函数(或包含函数)中定义的标号地址。值的类型为“void *”,且为常量,并可在这种类型的常量有效的任何地方使用这个值。例如:
void *ptr;
… …
ptr=&&foo;
为使用这些值,需要能跳转到该值,可通过相对语句“goto *exp;”来实现。例如:
goto *ptr;
它可使用于“void *”类型的任何表达式。
这些常量的一个用途是,用于初始化作为跳转表的静态数组。例如:
static void *array[]={&&foo, &&bar, &&hack};
然后就可以通过索引来这样选择标号:
goto *array[i]; //并不检查下标是否超出范围
这种标号值数组的用途与switch语句很类似,但switch语句更整齐,比数组更好。标号值的另外一个用途是在线程代码的解释程序中使用。解释程序函数中的标号可存储在线程代码中,用于快速调度。
这种机制可能被错误使用而跳转到其他函数的代码中。编译器不能阻止这种现象的发生,因此必须小心,以确保目标地址对于当前函数有效。
2)省略操作数的条件表达式:
条件表达式的中间操作数可以被省略。如果第一个操作数为非零,则其值就是条件表达式的值。因此,对于表达式:
x?:y
如果x的值为非零,表达式的值就是x的值;否则,就是y的值。这个例子完全等同于:
x?x:y
在这个简单的例子中,省略中间操作数并不是特别有用的。但当第一个操作数存在或者可能存在(如果它是一个宏参数)副作用时,省略中间操作数就会变得特别有用,因为重复中间操作数将产生两次副作用。省略中间操作数使用了已经计算过的值,避免了因重新计算而产生的不希望的影响。
3)case范围:
可以在单个case标号中指定一个连续值的范围。例如:
case low ... high:
这与各个case标号使用的数字有相同的作用,每个数字对应从low到high中的每个整数值。
这一功能对于指定ASCII字符码范围特别有用。例如:
case ‘A’ ... ’z’:
注:在“...”两边要留空格,否则与整数一起使用时可能出现解析错误。
21. MPLAB C30对ANSI C表达式的扩展:
前面有0b或0B的一串二进制数字视为二进制整数。二进制数字由数字0和1组成。例如,十进制数字255可用二进制表示为0b11111111。像其他整型常量一样,二进制常量可以以字母u或U为后缀来指定为无符号型。二进制常量也可以以字母l或L为后缀,指定为长整型。类似地,后缀ll或LL表示双字整型的二进制常量。
22. MPLAB C30支持的处理器:
dsPIC C30支持的处理器如下表:
低端处理器 |
中端处理器 |
高端处理器 |
||
30F2010 |
30F3010 |
30F4011 |
30F5011 |
30F6010 |
30F2011 |
30F3011 |
30F4012 |
30F5013 |
30F6010A |
30F2012 |
30F3012 |
30F4013 |
30F5015 |
30F6011 |
|
30F3013 |
|
30F5016 |
30F6011A |
|
30F3014 |
|
|
30F6012 |
|
|
|
|
30F6012A |
|
|
|
|
30F6013 |
|
|
|
|
30F6013A |
|
|
|
|
30F6014 |
|
|
|
|
30F6014A |
|
|
|
|
30F6015 |
23. MPLAB C30支持的数据类型:
DsPIC C30编译器支持8位、16位、32位和64位数据,所有多字节数据遵守的存储格式为低字节存储在低地址,高字节存储在高地址中,即一个数的低位字节存储在低地址的存储单元中。下表列出了dsPIC C30支持的整型数据类型及对应的大小。
类型 |
位 |
最小值 |
最大值 |
char, signed char |
8 |
-128 |
127 |
unsigned char |
8 |
0 |
255 |
short, signed short |
16 |
-32768 |
32767 |
unsigned short |
16 |
0 |
65535 |
int, signed int |
16 |
-32768 |
32767 |
unsigned int |
16 |
0 |
65535 |
long, signed long |
32 |
||
unsigned long |
32 |
0 |
|
long long**, signed long long** |
64 |
||
unsigned long long** |
54 |
0 |
下表列出了dsPIC C30支持的浮点型数据类型及对应的大小:
类型 |
位 |
E最小值 |
E最大值 |
N最小值 |
N最大值 |
float |
32 |
-126 |
127 |
2128 |
|
double* |
32 |
-126 |
127 |
2128 |
|
long double |
64 |
-1022 |
1023 |
21024 |
所有MPLAB C30指针都是16位宽的,这对于整个数据空间(64KB)的访问和小代码模型(32K字的代码)足够了。在大代码模型(大于32K字的代码)中,指针可解析为“句柄”,即指针是位于程序空间前32K字的GOTO指令的地址。
一个整型常量的类型应该是可以装得下该值,且使之不溢出的最小的数据类型。在常数后面加L或l,可以使常数的数据类型为有符号或无符号长整型;如加后缀u或U,则可使常数为无符号数据类型;如同时加L或l,和u或U,则表示无符号长整型。浮点常数加后缀f或F时,为浮点数据类型;否则,认为是双精度数据类型。在双精度数据类型后加后缀l或L,则表示dsPIC C30的双精度数据类型。
字符常数应加单引号表示,如’a’,字符常数的数据类型为字符型。dsPIC C30不支持多字节字符常数。
字符串常数或常量需要加双引号表示,如”dsPIC C30”,字符串常量的数据类型为字符常量指针类型(const char *),被存储在Flash中。如果指定字符串常量为非常量字符指针,编译器将产生一个警告。例如:
char *P1=”demo”; //将字符串常量复制到一个非常量char *指针会产生警告
const char *P2=”test”; //正确,复制到常量指针
char P3[]=”test”; //正确,放在数组中
一个由字符串数组初始化的非常数数组,如上面的最后一句,将产生一个RAM数组。这个数组在程序开始运行前被字符串常数”test”初始化,而其他方法表示的字符串常数将产生一个未命名的常数数组,通过直接访问Flash获得。
24. MPLAB C30的器件支持文件:
MPLAB C30编译使用的器件支持文件主要有:处理器头文件、寄存器定义文件、使用特殊功能寄存器、使用宏和从C代码访问EEDATA。
1)处理器头文件:
处理器头文件随语言工具提供,这些头文件定义了每个daPIC器件中可用的特殊功能寄存器SFR。要在C中使用头文件,使用#include <p30fxxxx.h>,其中xxxx对应器件型号。C头文件包含在“support\h”目录中。
例如下面的模块,是为PIC30F2010器件编写的,包括两个函数:一个函数用于使能PSV窗口;另一个函数用于禁止PSV窗口。
#include <p30f2010.h>
void EnablePSV(void){
CORCONbits.PSV=1;
}
void DisablePSV(void){
CORCONbits.PSV=0;
}
处理器头文件的约定是,使用器件数据手册中的寄存器名对每一个SFR命名。例如,CORCON寄存器指内核控制器。SFR中有一些重要的位,头文件中还有为该SFR定义的结构,结构名与SFR名字相同,只是在后面附加了“bits”。例如,CORCONbits是内核控制寄存器的结构。可使用数据手册中位的名字命名结构中的各位(或位域),如PSV表示CORCON寄存器中的PSV位。下面是对CORCON的完整定义:
/* CORCON: CPU Mode control Register */
extern volatile unsigned int CORCON __attribute__((__near__));
typedef struct tagCORCONBITS{
unsigned IF :1; //Interger/Fractional mode
unsigned RND :1; //Rounding mode
unsigned PSV :1; //Program Space Visibility enable
unsigned IPL3 :1;
unsigned ACCSAT :1; //Acc saturation mode
unsigned SATDW :1; //Data space write saturation mode
unsigned SATB :1; //Acc B saturation enable
unsigned SATA :1; //Acc A saturation enable
unsigned DL :3; //DO loop nesting level status
unsigned :4;
}CORCONBITS;
extern volatile CORCONBITS CORCONbits __attribute__((__near__));
注:CORCON和CORCONbits指同一个寄存器,在链接时将解析为同一地址。
2)寄存器定义文件:
前述“处理器头文件”中描述的处理器头文件,指定了每个器件的所有SFR,但并未定义SFR的地址。Support/gld目录中有每个寄存器的链接描述文件,链接描述文件定义了SFR的地址。要使用链接描述文件,须指定链接器命令行选项:
-T p30fxxxx.gld
其中,xxxx指器件的型号。
假定有一个名为app2010.c的文件,它包含PIC30F2010器件的一个应用程序,则可使用下面的命令行编译和链接这个文件:
pic30 -gcc -o app2010.o -T p30f2010.gld app2010.c
其中,“-o”命令行选项命名输出COFF可执行文件,“-T”选项给出PIC30F2010器件的名称。
如果在当前的目录中找不到p30f2010.gld,链接器将在已知的库路径中搜索。对于默认的安装,链接描述文件包含在PIC30_LIBRARAY_PATH中。
3)使用特殊功能寄存器:
在应用程序中使用特殊功能寄存器时,须遵循以下3个步骤:
①包含所使用器件的处理器头文件。这样能提供该型号器件特殊功能寄存器的源代码。
例如,下面的语句中,包含了PIC30F6014器件的头文件:
#include <p30f6014.h>
②像访问任何其他C变量一样访问特殊功能寄存器。源代码可对特殊功能寄存器进行读和写。例如,下面的语句,将Timer1特殊功能寄存器中的所有位清零:
TMR1=0;
下面一条语句中的T1CONbits,TON表示T1CON寄存器中的第15位,即定时器开启位。这条语句将名为TON的位置1来启动定时器:
T1CONbits.TON=1;
③链接相应器件的寄存器定义文件或链接描述文件。链接器提供特殊功能寄存器的地址(位结构在链接时具有和SFR相同的地址)。
下面是实时时钟的示例代码,其中使用了几个特殊功能寄存器,这些特殊功能寄存器的描述参见p30f6014.h文件。该文件将和特定于器件的链接描述文件p30f6014.gld相链接。
/* dsPIC实时时钟实例使用Timer1,TCY时钟模式和与时钟匹配的中断 */
#include <p30f6014.h>
/* 晶振频率FOSC=20MHz, Timer1产生1ms时钟周期 */
#define TMR1_PERIOD 0x1388
struct clockType{
unsigned int timer; //减量计数变量timer, us
unsigned int ticks; //计数变量ticks, us
unsigned int seconds; //秒计数变量seconds, s
}volatile RTclock;
void reset_clock(void){
RTclock.timer=0;
RTclock.ticks=0;
RTclock.seconds=0;
TMR1=0; //清timer1寄存器
PR1=TMR1_PERIOD; //设定period1寄存器
T1CONbits.TCS=0; //设定内部时钟源
IPCObits.T1RP=4; //设定优先级
IFSObits.T1IF=0; //清中断标志
IECObits.T1IE=1; //使能中断
SRbits.IPL=3; //使能CPU的优先级
T1CONbits.TON=1; //timer1开始运行
}
void __attribute__((__interrupt__)) _T1Interrupt(void){
static int sticks=0;
if (RTclock.timer>0) //如果减量计数变量timer 激活
RTclock.timer-=1; //计数变量timer减1
RTclock.ticks++; //计算变量ticks增1
if (sticks++>1000)
{
//时间轮循
sticks=0; //清除秒变量ticks
RTclock.seconds++; //秒计算变量seconds自增
}
IFS0bits.T1IF=0; //清中断标志
return;
}
4)使用宏:
处理器头文件除定义了特殊功能寄存器外,还为dsPIC30F系列数字信号控制器定义了有用的宏。
①配置位设置宏:
提供了可用来设置配置位的宏。例如,为使用宏设置FOSC位,可在C源代码开头前插入下面的代码:
_FOSC(CSW_FSCM_ON&EC_PLL16);
这将使能外部时钟,PLL被设置为16x,同时使能时钟切换和时钟失效保护监测。
同样,设置FBORPOR位:
_FBORPOR(PBOR_ON&BORV_27&PWRT_ON_64&MCLR_DIS);
这将使能2.7V的欠压复位,将上电延时定时器初始化为64ms,并将MCLR引脚配置为普通I/O口。配置位的设置列表参见处理器头文件。
②行内汇编使用的宏:
下面列出了用于在C中定义汇编代码的宏:
#define Nop() {__asm__ volatile(“nop”);}
#define ClrWdt() {__asm__ volatile(“clrwdt”);}
#define Sleep() {__asm__ volatile(“pwrsav #0”);}
#define Idle() {__asm__ volatile(“pwrsav #1”);}
③数据存储器分配宏:
可用于分配数据存储空间的宏有两种类型:需要参数的宏和不需要参数的宏。下面的宏需要一个参数N来指定对齐。N必须是2的幂,最小值为2。
#define _XBSS(N) __attribute__ ((space(xmemory),aligned(N)))
#define _XDATA(N) __attribute__ ((space(xmemory),aligned(N)))
#define _YBSS(N) __attribute__ ((space(ymemory),aligned(N)))
#define _YDATA(N) __attribute__ ((space(ymemory),aligned(N)))
#define _EEDATA(N) __attribute__ ((space(eedata),aligned(N)))
例如,声明一个未初始化数组位于X存储区中,对齐到32位字节地址:
int _XBSS(32) xbuf[16];
声明一个未初始化数组位于数据EEPROM中,没有特殊对齐方式:
int _EEDATA(2) table1[]={0,1,1,2,3,5,8,13,21};
下面的宏不需要参数。这些宏可用于将变量分配到持久数据存储区或near数据存储区中。
#define _PERSISTENT __attribute__ ((persistent))
#define _NEAR __attribute__ ((near))
例如,声明器件复位后能保留其值的两个变量:
int _PERSISTENT var1,var2;
④中断服务程序声明宏:
下面的宏可用于声明中断服务程序ISR:
#define _ISR __attribute__ ((interrupt))
#define _ISRFAST __attribute__ ((interrupt,shadow))
例如,声明timer()中断的中断服务程序:
void _ISR __INT0Iterrupt(void);
声明SPI1中断的快速现场保护中断服务程序:
void _ISRFAST __SPI1Iterrupt(void);
注:如果中断服务程序使用中断表中给出的保留名,那么中断服务程序的地址将自动填充到中断向量表中。
5)从C代码访问EEDATA:
MPLAB C30提供了一些方便的宏定义来允许将数据存放在器件的EE数据区中。这实现起来很简单:
int _EEDATA(2) user_data[]={0,1,2,3,4,5,6,7,8,9};
user_data将被存放在EE数据空间中,为给定的初值保留10个字。
daPIC器件为编程人员提供了两种方法来访问这种存储区:一种方法是通过程序空间可视性(PSV)窗口;另一种方法是使用特殊机器指令(TBLRDx)。
①通过PSV访问EEDATA:
编译器通常通过管理PSV窗口来访问存储在程序存储器中的常量。此外,PSV窗口可用于访问EEDATA存储器。
要使用PSV窗口,PSVPAG寄存器必须设置为要访问的程序存储器的正确地址。对于EE数据,该地址为0xFF,但最好使用__builtin_psvpage()函数。另外,还应通过置位CORCON寄存器中的PSV位来使能PSV窗口。如果该位没有置位,则使用PSV窗口时将始终读为0x0000。
上述步骤仅须执行一次,除非改变了PSVPAG,否则可通过像普通C变量一样引用EE数据空间中的变量来读这些变量。通过PSV访问EEDAT的程序代码示例如下:
#include <p30fxxxx.h>
int main(void){
PSVPAG =__builtin_psvpage(&user_data);
CORCONbits.PSV=1;
/* ... */
if(user_data[2]); //do something
}
注:这一访问模式与编译器管理的PSV(-mconst-in-code)模式不兼容,要注意防止出现冲突。
②使用TBLRDx指令访问EEDATA:
编译器不直接支持TBLRDx指令,但可以通过行内汇编使用这些指令。像PSV访问一样,通过一个SFR值形成一个32位的地址,并将地址编码为指令的一部分。
要使用TBLRDx指令,TBLPAG寄存器必须设置为要访问的程序存储器的正确地址。对于EE数据存储器,这一地址为0x7F,但最好使用__builtin_tblpage()函数。另外,TBLRDx指令只能通过__asm__语句访问。
通过表读访问EEDATA的代码示例如下:
#include <p30fxxxx.h>
#define eedata_read(src,dest){ \
register int eedata_addr; \
register int eedata_val; \
\
eedata_addr=__builtin_tbloffset(&src); \
__asm__(“tblrdl[%1],%0”:”=r”(eedata_val):”r”(eedata_addr)); \
dest=eedata_val; \
}
int main(void){
int value;
TBLPAG=__builtin_tblpage(&user_data);
eedata_read(user_data[2],value);
if(value); //do something
}
25. C语言的中断处理:
中断处理对于大多数单片机应用来说都是一个很重要的方面。中断用来使软件操作与实时发生的事件同步。当发生中断时,软件的正常执行流程被打断,并调用专门的函数来处理事件。当中断处理结束时,恢复先前的现场信息并继续正常执行流程。
dsPIC30F器件支持多个内部和外部中断源,允许高优先级中断中断任何正在处理的低优先级中断。
MPLAB C30编译器完全支持在C或行内汇编代码中进行中断处理。
1)编写中断服务程序:
可以将一个或多个C函数指定为中断服务程序,在发生中断时调用。为了获得最好的性能,通常将需要较长时间计算或需要调用库函数的操作放在主应用程序中。当中断事件发生得很快时,这种方式可以优化性能,并最大程度减小信息丢失的可能性。
①编写中断服务程序的要领:
编写ISR的要领如下:
·不带参数并以void返回值类型声明ISR(强制);
·不要通过一般程序调用ISR(强制);
·不要用ISR调用其他函数(建议)。
MPLAB C30的ISR和任何其他C函数一样,可以有局部变量,可以访问全局变量。但是,ISR必须声明为没有参数和没有返回值,这是因为ISR作为对硬件中断或陷阱的响应,对它的调用与一般C程序异步。
ISR只能通过硬件中断或陷阱调用,不能通过其他C函数调用。ISR使用中断返回(RETFIE)指令退出函数,而不是一般的RETURN指令。不使用RETFIE指令退出中断服务程序会破坏处理器资源,如status寄存器的值。
最后,由于中断响应时间的原因,建议不要使用ISR调用其他函数。
②编写中断服务程序的语法:
为将C函数声明为中断服务程序,需要指定函数的interrupt属性。interrupt属性的语法如下:
__attribute__((interrupt[([save(symbol-list)][,irq(irqid)]
[,altirq(altirqid)][,preprologue(asm)])]))
可以在interrupt属性名和参数名的前后加双下划线字符。因此,interrupt和__interrupt __是等同的,save和__save__也是等同的。
可选save参数指定进入和退出ISR时,需要保护和恢复的一个或多个变量。变量名列表包含在括号内,变量名中间用逗号分隔开。
如果不想导出全局变量的值,应该保护可能在ISR中修改的全局变量。被ISR修改的全局变量应该用volatile限定。
可选的irq参数允许将一个中断向量对应于一个特定的中断,可选的altirq参数允许将一个中断向量对应于一个指定的备用中断。每个参数都需要一个括号括起来的中断ID号。
可选的preprologue参数允许在生成的代码中插入汇编语句,这些汇编语句位于编译器生成的函数prologue()之前。
③为中断服务程序编写代码:
下面的原型声明了函数isr0()为中断服务程序:
void __attribute__((__interrupt__)) isr0(void);
由原型可以看出,中断函数必须不带参数,没有返回值。如果需要的话,编译器将保护所有工作寄存器,以及status寄存器和重复计数寄存器。将其他变量指定为interrupt属性的参数,可以保护这些变量。例如,要使编译器自动保护和恢复变量var1和var2,使用下面的原型:
void __attribute__((__interrupt__(__save__(var1,var2)))) isr0(void);
为请求编译器使用快速现场保护(使用push.s和pop.s指令),需要指定函数的shadow属性:
void __attribute__((__interrupt__,__shadow__)) isr0(void);
④使用宏声明简单的中断服务程序:
如果一个中断服务程序不需要interrupt属性的任何可选参数,则可使用简单的语法。在针对器件的头文件中定义了下面的宏:
#define _ISR __attribute__((interrupt))
#define _ISRFAST __attribute__((interrupt,shadow))
声明timer0中断的中断服务程序:
#include <p30fxxxx.h>
void __ISR _TIMER0Interrupt(void);
用快速现场保护声明SPI1中断的中断服务程序:
#include <p30fxxxx.h>
void __ISRFAST _SPI1Interrupt(void);
2)写中断向量:
当发生中断时,dsPIC30F器件使用中断向量来转移应用控制。中断向量是程序存储器中的专用地址,指定ISR的地址。为使用中断,应用必须在这些地址中包含有效的函数地址。
dsPIC器件有两个中断向量表:主表和备用表。每个表都包括62个异常向量。62个异常源有主异常向量和备用异常向量与之相关联,每个向量占一个程序字,如下表:
IRQ# |
向量函数 |
主向量名 |
备用向量名 |
n/a |
保留 |
_ReservedTrap0 |
_AltReservedTrap0 |
n/a |
振荡器失效陷阱 |
_OscillatorFail |
_AltOscillatorFail |
n/a |
地址错误陷阱 |
_AddressError |
_AltAddressError |
n/a |
堆栈错误陷阱 |
_StackError |
_AltStackError |
n/a |
数学错误陷阱 |
_MathError |
_AltMathError |
n/a |
保留 |
_ReservedTrap5 |
_AltReservedTrap5 |
n/a |
保留 |
_ReservedTrap6 |
_AltReservedTrap6 |
n/a |
保留 |
_ReservedTrap7 |
_AltReservedTrap7 |
0 |
INT0(外部中断0) |
_INT0Interrupt |
_AltINT0Interrupt |
1 |
IC1(输入捕捉1) |
_IC1Interrupt |
_AltIC1Interrupt |
2 |
OC1(输出比较1) |
_OC1Interrupt |
_AltOC1Interrupt |
3 |
TMR1(定时器1) |
_T1Interrupt |
_AltT1Interrupt |
4 |
IC2(输入捕捉2) |
_IC2Interrupt |
_AltIC2Interrupt |
5 |
OC2(输出比较2) |
_OC2Interrupt |
_AltOC2Interrupt |
6 |
TMR2(定时器2) |
_T2Interrupt |
_AltT2Interrupt |
7 |
TMR3(定时器3) |
_T3Interrupt |
_AltT3Interrupt |
8 |
SPI1(串行外设接口1) |
_SPI1Interrupt |
_AltSPI1Interrupt |
9 |
UART1RX(UART1接收器) |
_U1RXInterrupt |
_AltU1RXInterrupt |
10 |
UART1TX(UART1发送器) |
_U1TXInterrupt |
_AltU1TXInterrupt |
11 |
ADC(ADC转换完成) |
_ADCInterrupt |
_AltADCInterrupt |
12 |
NVM(NVM写完成) |
_NVMInterrupt |
_AltNVMInterrupt |
13 |
从I2C中断 |
_SI2CInterrupt |
_AltSI2CInterrupt |
14 |
主I2C中断 |
_MI2CInterrupt |
_AltMI2CInterrupt |
15 |
CN(输入变化中断) |
_CNInterrupt |
_AltCNInterrupt |
16 |
INT1(外部中断1) |
_INT1Interrupt |
_AltINT1Interrupt |
17 |
IC7(输入捕捉7) |
_IC7Interrupt |
_AltIC7Interrupt |
18 |
IC8(输入捕捉8) |
_IC8Interrupt |
_AltIC8Interrupt |
19 |
OC3(输出比较3) |
_OC3Interrupt |
_AltOC3Interrupt |
20 |
OC4(输出比较4) |
_OC4Interrupt |
_AltOC4Interrupt |
21 |
TMR4(定时器4) |
_T4Interrupt |
_AltT4Interrupt |
22 |
TMR5(定时器5) |
_T5Interrupt |
_AltT5Interrupt |
23 |
INT2(外部中断2) |
_INT2Interrupt |
_AltINT2Interrupt |
24 |
UART2RX(UART1接收器) |
_U2RXInterrupt |
_AltU2RXInterrupt |
25 |
UART2TX(UART1发送器) |
_U2TXInterrupt |
_AltU2TXInterrupt |
26 |
SPI2(串行外设接口2) |
_SPI2Interrupt |
_AltSPI2Interrupt |
27 |
CAN1(组合IRQ) |
_C1Interrupt |
_AltC1Interrupt |
28 |
IC3(输入捕捉3) |
_IC3Interrupt |
_AltIC3Interrupt |
29 |
IC4(输入捕捉4) |
_IC4Interrupt |
_AltIC4Interrupt |
30 |
IC5(输入捕捉5) |
_IC5Interrupt |
_AltIC5Interrupt |
31 |
IC6(输入捕捉7) |
_IC6Interrupt |
_AltIC6Interrupt |
32 |
OC5(输出比较5) |
_OC5Interrupt |
_AltOC5Interrupt |
33 |
OC6(输出比较6) |
_OC6Interrupt |
_AltOC6Interrupt |
34 |
OC7(输出比较7) |
_OC7Interrupt |
_AltOC7Interrupt |
35 |
OC8(输出比较8) |
_OC8Interrupt |
_AltOC8Interrupt |
36 |
INT3(外部中断3) |
_INT3Interrupt |
_AltINT3Interrupt |
37 |
INT4(外部中断4) |
_INT4Interrupt |
_AltINT4Interrupt |
38 |
CAN2(组合IRQ) |
_C2Interrupt |
_AltC2Interrupt |
39 |
PWM(PWM周期匹配) |
_PWMInterrupt |
_AltPWMInterrupt |
40 |
QEI(位置计数器比较) |
_QEIInterrupt |
_AltQEIInterrupt |
41 |
DCI(CODEC传输完成) |
_DCIInterrupt |
_AltDCIInterrupt |
42 |
PLVD(低电压检测) |
_LVDInterrupt |
_AltLVDInterrupt |
43 |
FLTA(MPWM故障A) |
_FLTAInterrupt |
_AltFLTAInterrupt |
44 |
FLTB(MPWM故障B) |
_FLTBInterrupt |
_AltFLTBInterrupt |
45 |
保留 |
_Interrupt45 |
_AltInterrupt45 |
46 |
保留 |
_Interrupt46 |
_AltInterrupt46 |
47 |
保留 |
_Interrupt47 |
_AltInterrupt47 |
48 |
保留 |
_Interrupt48 |
_AltInterrupt48 |
49 |
保留 |
_Interrupt49 |
_AltInterrupt49 |
50 |
保留 |
_Interrupt50 |
_AltInterrupt50 |
51 |
保留 |
_Interrupt51 |
_AltInterrupt51 |
52 |
保留 |
_Interrupt52 |
_AltInterrupt52 |
53 |
保留 |
_Interrupt53 |
_AltInterrupt53 |
当INTCON2寄存器中的ALTIVT位置位时,使用备用向量名。dsPIC器件复位不通过中断向量表处理,而是在器件复位时清零dsPIC程序计数器,这使处理器从地址0处开始执行。按照约定,链接描述文件在该地址处构建GOTO指令来转移控制到C运行时启动模块。
为响应一个中断,必须将函数的地址填充到一个向量表的恰当地址,且函数必须保护它使用的任何系统资源。函数必须使用RETFIE处理器指令返回到前台服务。中断函数可用C编写。当将一个C函数指定为中断处理函数时,编译器将保护编译器使用的所有系统资源,并使用恰当的指令从函数返回。编译器可以可选地用中断函数的地址填充中断向量表,为使编译器填充指向中断函数的中断向量,按照上表命名函数。例如,如果定义了下列函数,堆栈错误向量将自动被填充:
void __attribute__((__interrupt__)) _StackError(void);
类似地,如果定义了下面的函数,备用堆栈错误向量将自动填充:
void __attribute__((__interrupt__)) _AltStackError(void);
对于没有指定处理函数的所有中断向量,将自动填充一个默认的中断处理函数。默认的中断处理函数由链接器提供,仅在出错时用于复位器件。应用程序也可通过用名字_Default Interrupt声明一个中断函数来提供默认的中断处理函数。上表中的最后9个中断向量没有预定义的硬件处理函数,可通过使用表中给出的名字,或者更适合应用的名字,填充这些向量。同时,仍使用interrupt属性的irq或altirq参数填充适当的向量入口。例如,为指定一个函数使用主中断向量52,使用下面的语句:
void __attribute__((__interrupt__(__irq__(52)))) MyIRQ(void);
类似地,指定一个函数使用备用中断向量52,使用下面的语句:
void __attribute__((__interrupt__(__altirq__(52)))) MyAltIRQ(void);
irq/altirq号可以为中断请求编号45~53中的一个。如果使用了interrupt属性的irq参数,编译器将生成外部符号名_Interruptn,其中的n为向量号。因此,C标识符_Interrupt 45~_Interrupt53被编译器保留。同样,如果使用了interrupt属性的altirq参数,编译器将生成外部符号名_AltInterruptn,其中n为向量号。因此,C标识符__AltInterrupt45~__Interrupt53被编译器保留。
3)中断服务程序现场保护:
为了保证从中断返回到代码后,条件状态与中断前相同,必须保护特定寄存器的现场信息。
中断,就其本质来说,会在不可预测的时刻发生。因此,被中断的代码必须能以与中断发生时相同的机器状态继续运行。为了正确处理中断返回,中断函数的设置(prologue)代码在堆栈中自动保护编译器管理的工作寄存器和特殊功能寄存器,以便在ISR末尾恢复这些寄存器内容。可使用interrupt属性值,可选的save参数指定要保护和恢复的其他变量和特殊功能寄存器。
在某些应用中,可能需要在中断服务程序内,在编译器生成的prologue函数前,插入汇编语句。例如,在中断服务程序的入口可能需要递增一个信号,可以这样来实现:
void __attribute__((__interrupt__(__preprologue__(“inc_semaphore”))))
MyAltIRQ(void);
4)中断响应时间:
从中断事件发生到执行ISR第一条指令之间的时间,就是中断响应时间。有两个因素影响中断源发生到执行ISR代码第一条指令之间的周期数:
·处理器处理中断时间:即处理器识别中断并跳转到中断向量第一个地址的时间。这个值与具体器件和所使用中断源有关,为确定这个值的大小,请参考相应器件的数据手册。
·ISR代码:MPLAB C30在ISR 中保存它使用的寄存器,包括工作寄存器和RCOUNT特殊功能寄存器。如果ISR调用一个普通的函数,则编译器要保存所有的工作寄存器和RCOUNT特殊功能寄存器,即使在ISR中没有显式使用这些寄存器。一般来说,编译器不知道被调用函数使用了哪些资源,所以必须要保存这些寄存器。
5)中断嵌套:
MPLAB C30支持中断嵌套,dsPIC器件支持中断嵌套。在ISR中,将处理器资源保存在堆栈中,对嵌套ISR的编码与非嵌套中断的编码相同。通过清零INTCON1寄存器中的NSTDIS位(中断嵌套禁止位)来使能中断嵌套。(dsPIC复位时是使能嵌套中断的,是默认设置。)
在中断优先级控制寄存器(IPCn)中,为每个中断源分配了一个优先级。如果有一个处于等待状态的中断请求(IRQ),其优先级等于或高于处理器状态寄存器中的(ST寄存器中的CPUPRI字段)当前处理器优先级,处理器将响应中断。
6)使能/禁止中断:
有两种方式使能和禁止中断源:全局和单独。
可单独禁止或使能每个中断源。每个IRQ都有一个中断使能位,位于中断使能控制寄存器(IECn)中。将中断使能位置1,将使能响应的中断;将中断使能位清零,将禁止相应的中断。dsPIC器件复位时,所有中断使能位都被清零。另外,处理器还有一个禁止中断指令(dis able interrupt instruction,DISI),可在指定的指令周期数内禁止所有中断。
通过行内汇编,可在C程序中使用DISI指令。例如,下面的行内汇编语句:
__asm__ volatile(“disi #16”);
将在源程序中这条语句的所在处发出指定的DISI指令。采用这种方式使用DISI的一个缺点是,C编程人员不能总是确定C编译器如何将C源代码翻译为机器指令,因此可能难以确定DISI指令的周期数。解决这个问题的方法是,通过DISI指令把中断中要保护的代码用括号括起来。DISI指令的第一条指令将周期数设置为最大值,第二条指令将周期数设置为0。例如:
__asm__ volatile(“disi #0x3FFF”); //disable interrupts
/* ……protect C code…… */
__asm__ volatile(“disi #0x0000”); //enable interrupts
另一种可选方案是直接写DISCNT寄存器,这在硬件上和DISI指令中的作用是相同的,但对于C程序来说,具有避免使用行内汇编的优点。这是需要的,因为在函数中使用行内汇编时编译器可能不会执行某些优化。所以可以不使用上面的指令序列,而使用:
DISICNT=0x3FFF; //disable interrupts
/* ……protect C code…… */
DISICNT=0x0000; //enable interrupts
注:陷阱不能禁止(如地址错误陷阱),只有IRQ是可以被禁止的。
7)使用模板文件编写实时中断代码:
模板文件是可作为基本框架来构建应用程序的源代码文件。利用模板文件可以很容易地开始一个应用程序的项目,因为在这种简单的文件中,提供了C语言的语法结构和格式,只须向其中添加应用程序的详细代码。模板中包含了MPLAB C30源代码中许多具有通用功能的示例C语句,包括变量和常量、特定处理器头文件、中断向量和相关的中断代码,以及可向其中插入应用代码的代码段。
①一个简单的模板:
模板中还附有注释来帮助识别关键的语法结构,在许多情况下,定义了宏来简化代码。下面是一个简化的模板:
#include “p30f6014.h”
#define CONSTANT1 10 //sample constant definition
int array1[CONSTANT1] __attribute__((__space__(xmemory),__aligned__(32)));
int array5[CONSTANT2]; //simple array
int variable1 __attribute__((__space__(xmemory)));
int variable3; //simple variable
int main(void){
/* Application code goed here */
}
int __attribute__((__interrupt__(__save__(variable1,variable2))))
_INT0Interrupt(void)
{
/* Interrupt Service Routine code goes here */
}
这段模板代码以“#include”语句开头,来包含具有特定处理器的针对处理器特殊功能寄存器定义的头文件。随后是一个简单的常量定义(#define),可以对其进行修改和复制,以形成应用程序的一系列常量定义。
后面是一个数组定义,表明了如何定义数组的各种属性,指定其在存储器中的段,以及它在dsPIC存储器结构中的对齐方式。第二个数组定义,是一个简单数组。接下来定义两个变量,和数组一样,可以为变量指定属性或不指定属性。再下来是main()的代码段,这里是放置应用程序的地方。在main()后面是一个中断的代码结构。
实际的应用程序可能会使用不同的中断和不同的属性,并且可能比这个例子要复杂很多,但这个模板提供了一个简单的开始方式。可将未修改的模板和适当的链接描述文件添加到一个新的项目中,这个项目将会毫无错误地通过编译。模板保存在dsPIC工具安装目录名为\support\templates的文件夹中,并在相应的\asm和\c文件夹中提供了汇编和C源文件。
②新项目中使用模板:
可通过以下的步骤将上面的模板复制到新项目目录中。可以在windows资源管理器中对文件夹/文件进行操作:
·在MPLAB C30安装目录下的Example目录中创建一个名为T1_Interrupt的新文件夹。
·将C:\pic30_tools\support\templates\c\temp_6014.c复制到这个文件夹中。
·将T1_Interrupt文件夹中复制的模板文件temp_6014.c重命名为T1Clock.c。
·返回MPLAB IDE。
按照“创建项目”步骤使用项目向导,在这个目录中创建一个新的项目TiClock。将T1Clock.c作为唯一的源文件添加到项目中,并添加dsPIC30F6014的链接描述文件;然后双击项目窗口中的文件名T1Clock.c,将出现编辑界面。
可以把这个调用模板的一些头文件注释删除,并把针对应用的信息输入到新的项目中。文件开头的头文件应包含新项目的信息。
然后,根据需要定义常量、变量和数组。
③使用MPLAB SIM软件模拟器进行调试:
用确保选择了Debugger>Select Tool>MPLAB SIM,然后选择Debugger>Settings,对模拟器的处理器时钟速度进行设置。在振荡器(Osc/Trace)选项卡对话框中,可以设置模拟的dsPIC 30F6014时钟频率。将时钟频率设置为20MHz。
软件模拟器的运行速度由PC机决定,不可能以对话框中设置的dsPIC30F MCU的实际速度运行。但是,由于所有的定时计算都是基于这个时钟设置的,因此当使用模拟器进行时序测量时,测量的时间是与器件在该频率下的实际运行时间相对应的。
用模拟器测量时间的一个方法是使用跑表功能。选择Debugger>Stopwatch来打开Stopwatch对话框,确保选中Clear Simulation Time on Reset复选框。在amin()中递增main_counter的代码上设置一个断点(在该行上右击并选择Set Breakpoint),然后单击RUN图标或选择Debugger>Run。程序运行到断点后,跑表显示测量时间。
如果运行成功,就可以设置一个Watch窗口来检查程序变量。选择View>Watch来调出Watch窗口,并从Add Symbol旁边的下拉框中选择要添加的变量。
第一次执行到断点时,查看Watch窗口中的变量,有些变量或等于0,这是因为初始化。然后每次单步执行一次循环,变量值就会有相应变化。
跑表总是在对话框右边的窗口中跟踪总的时间;左边窗口用来对独立的测量进行计时。