PIC 单片机 C 语言编程简介(2)
2016-03-28 来源:eefocus
首先必须强调,在用
之时无需知道所定义的变量具体被放在哪个地址(除了 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;
bit tmpBit1 @ tmpData*8+1;
bit tmpBit2 @ tmpData*8+2;
如果 tmpData 事先没有被绝对定位,那就不能用上面的位变量定位方式。
11.5.8
&O1540;
如果在一个 C
文件中必须将这些变量声明成“extern”外部类型。例如程序文件 code1.c 中有如下定义:
bank1 unsigned char var1, var2;
//定义了 bank1 中的两个变量
在另外一个程序文件 code2.c 中要对上面定义的变量进行操作,则必须在程序的开头定义:
extern bank1 unsigned char var1, var2;
&O1540;
PICC
“volatile”。顾名思义,它说明了一个变量的值是会随机变化的,即使程序没有刻意对它进
行任何赋值操作。在单片机中,作为输入的 IO 端口其内容将是随意变化的;在中断内被修
改的变量相对主程序流程来讲也是随意变化的;很多特殊功能寄存器的值也将随着指令的运
行而动态改变。所有这种类型的变量必须将它们明确定义成“volatile”类型,例如:
volatile unsigned char STATUS @ 0x03;
volatile bit commFlag;
“volatile”类型定义在单片机的
器的优化处理器这些变量是实实在在存在的,在优化过程中不能无故消除。假定你的程序定
义了一个变量并对其作了一次赋值,但随后就再也没有对其进行任何读写操作,如果是非
volatile
情形是在使用某一个变量进行连续的运算操作时,这个变量的值将在第一次操作时被复制到
中间临时变量中,如果它是非 volatile 型变量,则紧接其后的其它操作将有可能直接从临时
变量中取数以提高运行效率,显然这样做后对于那些随机变化的参数就会出问题。只要将其
定义成 volatile 类型后,编译后的代码就可以保证每次操作时直接从变量地址处取数。
&O1540;
如果变量定义前冠以“const”类型修饰,那么所有这些变量就成为常数,程序运行过
程中不能对其修改。除了位变量,其它所有基本类型的变量或高级组合变量都将被存放在程
序空间(ROM 区)以节约数据存储空间。显然,被定义在 ROM 区的变量是不能再在程序
中对其进行赋值修改的,这也是“const”的本来意义。实际上这些数据最终都将以“retlw”
的指令形式存放在程序空间,但 PICC 会自动编译生成相关的附加代码从程序空间读取这些
常数,编程员无需太多操心。例如:
const unsigned char name[]=”This is a demo”;
如果定义了
不能对其赋值修改。本来,不能修改的位变量没有什么太多的实际意义,相信大家在实际编
程时不会大量用到。
&O1540;
按照标准
量全部清零。PICC 会在最后生成的机器码中加入一小段初始化代码来实现这一变量清零操
作,且这一操作将在 main 函数被调用之前执行。问题是作为一个单片机的控制系统有很多
变量是不允许在程序复位后被清零的。为了达到这一目的,PICC
词以声明此类变量无需在复位时自动清零,编程员应该自己决定程序中的那些变量是必须声
明成“persisten”类型,而且须自己判断什么时候需要对其进行初始化赋值。例如:
persistent unsigned char hour,minute,second;
经常用到的是如果程序经上电复位后开始运行,那么需要将 persistent 型的变量初始化,
如果是其它形式的复位,例如看门狗引发的复位,则无需对 persistent 型变量作任何修改。
PIC
形。
11.5.9
PICC 中指针的基本概念和标准 C 语法没有太多的差别。但是在 PIC 单片机这一特定的
架构上,指针的定义方式还是有几点需要特别注意。
&O1540;
如果是汇编语言编程,实现指针寻址的方法肯定就是用 FSR 寄存器,PICC 也不例外。
为了生成高效的代码,PICC 在编译 C 原程序时将指向 RAM 的指针操作最终用 FSR 来实现
间接寻址。这样就势必产生一个问题:FSR 能够直接连续寻址的范围是 256 字节(bank0/1
或 bank2/3),要覆盖最大 512 字节的内部数据存储空间,又该如何让定义指针?PICC 还是
将这一问题留给编程员自己解决:在定义指针时必须明确指定该指针所适用的寻址区域,例
如:
unsigned char *ptr0; //①定义覆盖 bank0/1 的指针
bank2 unsigned char *ptr1;
bank3 unsigned char *ptr2;
上面定义了三个指针变量,其中①指针没有任何 bank 限定,缺省就是指向 bank0 和 bank1;
②和③一个指明了 bank2,另一个指明了 bank3,但实际上两者是一样的,因为一个指针可
以同时覆盖两个 bank 的存储区域。另外,上面三个指针变量自身都存放在 bank0 中。我们
将在稍后介绍如何在其它 bank 中存放指针变量。
既然定义的指针有明确的 bank 适用区域,在对指针变量赋值时就必须实现类型匹配,
下面的指针赋值将产生一个致命错误:
unsigned char *ptr0;
bank2 unsigned char buff[8];
程序语句:
//定义指向 bank0/1 的指针
//定义 bank2 中的一个缓冲区
ptr0 = buff;
若出现此类错误的指针操作,PICC 在最后连接时会告知类似于下面的信息:
Fixup overflow in expression_r(...)
同样的道理,若函数调用时用了指针作为传递参数,也必须注意 bank 作用域的匹配,
而这点往往容易被忽视。假定有下面的函数实现发送一个字符串的功能:
void SendMessage(unsigned char *);
那么被发送的字符串必须位于 bank0 或 bank1 中。如果你还要发送位于 bank2 或 bank3 内的
字符串,必须再另外单独写一个函数:
void SendMessage_2(bank2 unsigned char *);
这两个函数从内部代码的实现来看可以一模一样,但传递的参数类型不同。
按笔者的应用经验体会,如果你看到了“Fixup overflow”的错误指示,几乎可以肯定
是指针类型不匹配的赋值所至。请重点检查程序中有关指针的操作。
&O1540;
如果一组变量是已经被定义在 ROM 区的常数,那么指向它的指针可以这样定义:
const unsigned char company[]=”Microchip”;
const unsigned char *romPtr;
程序中可以对上面的指针变量赋值和实现取数操作:
romPtr
data = *romPtr++;
//定义 ROM 中的常数
//定义指向 ROM 的指针
反过来,下面的操作将是一个错误,因为该指针指向的是常数型变量,不能赋值。
*romPtr = data; //往指针指向的地址写一个数
&O1540;
单片机编程时函数指针的应用相对较少,但作为标准 C 语法的一部分,PICC 同样支持
函数指针调用。如果你对编译原理有一定的了解,就应该明白在
构上实现函数指针调用的效率是不高的: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 语言编程时基本不用太多关心
代码的分页问题。因为所有函数或子程序调用时的页面设定(如果代码超过一个页面)都由
编译器自动生成的指令实现。
11.6.1
PICC 决定了 C 原程序中的一个函数经编译后生成的机器码一定会放在同一个程序页面
内。中档系列的 PIC 单片机其一个程序页面的长度是 2K 字,换句话说,用 C 语言编写的任
何一个函数最后生成的代码不能超过 2K 字。一个良好的程序设计应该有一个清晰的组织结
构,把不同的功能用不同的函数实现是最好的方法,因此一个函数 2K 字长的限制一般不会
对程序代码的编写产生太多影响。如果为实现特定的功能确实要连续编写很长的程序,这时
就必须把这些连续的代码拆分成若干函数,以保证每个函数最后编译出的代码不超过一个页
面空间。
11.6.2
中档系列 PIC 单片机的硬件堆栈深度为 8 级,考虑中断响应需占用一级堆栈,所
有函数调用嵌套的最大深度不要超过 7 级。编程员必须自己控制子程序调用时的嵌套深
度以符合这一限制要求。
PICC 在最后编译连接成功后可以生成一个连接定位映射文件(*.map),在此文件
中有详细的函数调用嵌套指示图“call graph”,建议大家要留意一下。其信息大致如下
(取自于一示范程序的编译结果):
Call graph:
*_main size 0,0 offset 0
*
例 11-4
上面所举的信息表明整个程序在正常调用子程序时嵌套最多为两级(没有考虑中断)。因为
main
加入的库函数,这些函数调用从 C 原程序中无法直接看出,但在此嵌套指示图上则一目了
然。
11.6.3
PICC 在编译时将严格进行函数调用时的类型检查。一个良好的习惯是在编写程序代码
前先声明所有用到的函数类型。例如:
void Task(void);
unsigned char Temperature(void);
void BIN2BCD(unsigned char);
void TimeDisplay(unsigned char, unsigned char);
这些类型声明确定了函数的入口参数和返回值类型,这样编译器在编译代码时就能保证生成
正确的机器码。笔者在实际工作中有时碰到一些用户声称发现 C 编译器生成了错误的代码,
最后究其原因就是因为没有事先声明函数类型所致。
建议大家在编写一个函数的原代码时,立即将此函数的类型声明复制到原文件的起始
处,见例 11-1;或是复制到专门的包含头文件中,再在每个原程序模块中引用。
11.6.4
PICC 可以实现 C 语言的中断服务程序。中断服务程序有一个特殊的定义方法:
void interrupt ISR(void);
其中的函数名“ISR”可以改成任意合法的字母或数字组合,但其入口参数和返回参数类型
必须是“void”型,亦即没有入口参数和返回参数,且中间必须有一个关键词“interrupt”。
中断函数可以被放置在原程序的任意位置。因为已有关键词“interrupt”声明,PICC 在
最后进行代码连接时会自动将其定位到 0x0004 中断入口处,实现中断服务响应。编译器也
会实现中断函数的返回指令“retfie”。一个简单的中断服务示范函数如下:
void
{
T0IF = 0;
//判 TMR0 中断
//清除 TMR0 中断标志
TMR1IF0;
}
//清除 TMR1 中断标志
//中断结束并返回
下一篇:PIC单片机C语言编程教程(1)
- 基于PIC24在血糖仪上的应用分析
- 贸泽开售Microchip Technology PIC32CZ CA MCU 保护工业和汽车应用安全
- Microchip推出搭载硬件安全模块的PIC32CK 32位单片机, 轻松实现嵌入式安全功能
- Microchip推出集成微型FPGA的PIC16 微控制器,售价不到 50 美分
- Microchip 发布PIC16F13145系列MCU,促进可定制逻辑的新发展
- Microchip推出PIC18-Q24 系列单片机 为增强代码安全性设置新标准
- 基于AT45DB161B存储器和PIC16LC73B单片机实现微型压力测量装置设计
- 将DHT11与PIC16F877A连接进行温度和湿度的测量
- 贸泽备货Microchip PIC32CM Lx MCU 同时支持安全子系统和Arm TrustZone技术
- Microchip推出32位单片机PIC32CXMT系列产品