PIC 单片机 C 语言编程简介
用 C语言来开发单片机系统软件最大的好处是编写代码效率高、软件调试直观、维护升级方便、
代码的重复利用率高、便于跨平台的代码移植等等,因此C 语言编程在单片机系统设计中已得到越
来越广泛的运用。针对 PIC单片机的软件开发,同样可以用 C 语言实现。
但在单片机上用 C 语言写程序和在 PC机上写程序绝对不能简单等同。现在的 PC 机资
源十分丰富,运算能力强大,因此程序员在写 PC机的应用程序时几乎不用关心编译后的可
执行代码在运行过程中需要占用多少系统资源,也基本不用担心运行效率有多高。写单片机
的 C程序最关键的一点是单片机内的资源非常有限,控制的实时性要求又很高,因此,如
果没有对单片机体系结构和硬件资源作详尽的了解,以笔者的愚见认为是无法写出高质量实
用的 C语言程序。这就是为什么前面所有章节中的的示范代码全部用基础的汇编指令实现
的原因,希望籍此能使读者对PIC单片机的指令体系和硬件资源有深入了解,在这基础之
上再来讨论 C 语言编程,就有水到渠成的感觉。
本书围绕中档系列 PIC 单片机来展开讨论,Microchip公司自己没有针对中低档系列 PIC
单片机的 C语言编译器,但很多专业的第三方公司有众多支持 PIC 单片机的 C语言编译器
提供,常见的有 Hitech、CCS、IAR、Bytecraft等公司。其中笔者最常用的是 Hitech 公司的
PICC 编译器,它稳定可靠,编译生成的代码效率高,在用PIC 单片机进行系统设计和开发
的工程师群体中得到广泛认可。其正式完全版软件需要购置,但在其网站上有限时的试用版
供用户评估。另外,Hitech 公司针对广大 PIC的业余爱好者和初学者还提供了完全免费的学
习版 PICC-Lite编译器套件,它的使用方式和完全版相同,只是支持的 PIC单片机型号限制
在 PIC16F84、PIC16F877 和 PIC16F628 等几款。这几款Flash 型的单片机因其所具备的丰富
的片上资源而最适用于单片机学习入门,因此笔者建议感兴趣的读者可从PICC-Lite 入手掌
握 PIC 单片机的 C 语言编程。
在此列出几个主要的针对 PIC 单片机的 C编译器相关连接网址,供读者参考:
Hitech-PICC:www.htsoft.com
IAR:www.iar.com
CCS:www.ccsinfo.com/picc.shtml
ByteCraft:www.bytecraft.com/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 SetLanguage Tool
Locations…,打开语言工具挂接设置对话框,如图 11-1所示:
图 11-1 MPLAB-IDE语言工具设置对话框
在对话框中选择“HI-TECH PICCToolsuite”栏,展开可执行文件组“Executable”后,
列出了将被MPLAB-IDE后台调用的编译器所用到的所有可执行文件,其中有汇编编译器
“PICC Assembler”、C 原程序编译器“PICCCompiler”和连接定位程序“PICC Linker”。同
时在此列表中还显示了对应的可执行程序名,请注意在这里都是“PICC.EXE”。用鼠标分别
点击选中这三项可执行文件,观察对话框下面“Location”一栏中显示的文件路径,用
“Browse…”按纽,从计算机中已经安装的 PICC编译器文件夹中选择 PICC.EXE 文件。实
际上 PICC.EXE只是一个调度管理程序,它会按照所输入的文件扩展名自动调用对应的编译
器和连接器,用户要注意的是 C语言原程序扩展名用“.c”,汇编原程序用“.as”即可。
工具挂接完成后,在建立项目时可以选择语言工具为“HI-TECHPICC”,具体步骤可以
参阅第三章 3.1.3节,此处不再重复。项目建立完成后可以加入 C或汇编原程序,也可以加
入已有的库文件或已经编译的目标文件。最常见的是只加入 C 原程序。用 C语言编程的好
处是可以实现模块化编程。程序编写者应尽量把相互独立的控制任务用多个独立的C 原程序文件实
现,如果程序量较大,一般不要把所有的代码写在一个文件内。
图 11-2 列出的是笔者建立的一个项目中所有 C原程序模块,其中主控、数值计算、I2C 总线操
作、命令按键处理和液晶显示驱动等不同的功能分别在不同的独立的原程序模块中实现。
图 11-2 C 语言多模块编程
11.4PIC 单片机的 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.1PICC 中的基本变量类型
PICC 遵循 Little-endian标准,多字节变量的低字节放在存储空间的低地址,高字节放
在高地址。
11.5.2PICC 中的高级变量
基于表 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.3PICC 对数据寄存器 bank 的管理
为了使编译器产生最高效的机器码,PICC把单片机中数据寄存器的 bank 问题交由编程
员自己管理,因此在定义用户变量时你必须自己决定这些变量具体放在哪一个bank 中。如
果没有特别指明,所定义的变量将被定位在bank0,例如下面所定义的这些变量:
unsigned char buffer[32];
bit flag1,flag2;
float val[8];
除了 bank0 内的变量声明时不需特殊处理外,定义在其它bank 内的变量前面必须加上
相应的 bank 序号,例如:
bank1 unsigned charbuffer[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.4PICC 中的局部变量
PICC 把所有函数内部定义的 auto 型局部变量放在bank0。为节约宝贵的存储空间,它
采用了一种被叫做“静态覆盖”的技术来实现局部变量的地址分配。其大致的原理是在编译
器编译原代码时扫描整个程序中函数调用的嵌套关系和层次,算出每个函数中的局部变量字
节数,然后为每个局部变量分配一个固定的地址,且按调用嵌套的层次关系各变量的地址可
以相互重叠。利用这一技术后所有的动态局部变量都可以按已知的固定地址地进行直接寻
址,用 PIC汇编指令实现的效率最高,但这时不能出现函数递归调用。PICC在编译时会严
格检查递归调用的问题并认为这是一个严重错误而立即终止编译过程。
既然所有的局部变量将占用 bank0的存储空间,因此用户自己定位在 bank0 内的变量字
节数将受到一定的限制,在实际使用时需注意。
11.5.5PICC 中的位变量
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)
=> btfsc0x20,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 charallBits;
} myFlag;
例 11-3定义位变量于同一字节
需要存取其中某一位时可以
myFlag.oneBit.b3=1; //b3 位置 1
一次性将全部位清零时可以
myFlag.allBits=0;//全部位变量清 0
当程序中把非位变量进行强制类型转换成位变量时,要注意编译器只对普通变量的最低
位做判别:如果最低位是 0,则转换成位变量0;如果最低位是 1,则转换成位变量 1。而标
准的 ANSI-C 做法是判整个变量值是否为0。另外,函数可以返回一个位变量,实际上此返
回的位变量将存放于单片机的进位位中带出返回。
11.5.6PICC 中的浮点数
PICC 中描述浮点数是以 IEEE-754标准格式实现的。此标准下定义的浮点数为 32 位长,
在单片机中要用 4个字节存储。为了节约单片机的数据空间和程序空间,PICC 专门提供了
一种长度为 24位的截短型浮点数,它损失了浮点数的一点精度,但浮点运算的效率得以提
高。在程序中定义的 float 型标准浮点数的长度固定为24 位,双精度 double 型浮点数一般
也是 24 位长,但可以在程序编译选项中选择 double型浮点数为 32 位,以提高计算的精度。
一般控制系统中关心的是单片机的运行效率,因此在精度能够满足的前提下尽量选择
24 位的浮点数运算。