[注]这篇文章是我做了半年的一个项目-开发富晶半导体的C-Compiler及debug tool(合称:HTFSC)。FSC(富晶半导体)的MCU是基于RISC架构的,类PIC。所以我们这次的C-Compiler主要是与开发过picc的Hi-TECH公司合作,共同完成。他们只是做compiler模块,我们来做其他的工作,例如测试,debug tool都是我们自己完成的-主要是我在负责。下面我就把这个编译器的一些介绍奉献给大家,里面有很多实用的编程技巧和资料,藉此与诸位探讨一下(文中有引用一些网友的成果,在此一并感谢!)原创文章,谢绝转载!
1. FSC系列单片机C语言编程简介:<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
1.1 用C语言开发FSC单片机的优点:
C语言以其结构化和能够产生高效代码的优点,已经逐渐成为单片机应用编程的首选开发工具之一。用C语言开发单片机有如下几个优点:
1.可以大幅度地加快开发进度,特别是一些需要复杂计算的单片机系统,程序量越大,用C语言开发就越有优势;
2.无需精通单片机的指令集和具体的硬件结构,只要有一定的了解就能够编写出具有很高专业水平的单片机程序;
3.可以实现软件的结构化编程,便于集体开发和分工合作;
4.用C语言编写的源程序具有逻辑结构清晰,条理性强,可读性和可维护性好的特点,从而可以提高整个系统的可靠性;
5.可省去人工分配单片机资源的工作,只需要在代码中申明变量的类型,C编译器就会自动地分配相关的资源,无需人工干预,从而有效地避免了人工分配单片机资源的差错;
6.用C语言编写的源程序可移植性好,当需要移植到不同型号的单片机中时,只要对一些与硬件相关的语句作适当的修改即可。
因此,用C语言进行FSC系列单片机的程序开发与使用汇编语言相比具有很多的优点。
1.2 相关的C语言基础知识:
C语言是一种应用广泛的高级语言,它具有编写代码效率高,软件调试直观,维护升级方便,代码的重复利用率高,便于跨平台的代码移植等等的优点,同时它还具有位操作等许多低级语言的特点,非常适合用来开发单片机等的嵌入式应用程序。
目前用于FSC系列单片机开发的C语言编译器主要是由HI-TECH公司(www.htsoft.com) 开发的HTFSC编译器,同时结合富晶公司自主开发的Debug Tool共同组合而成,HTFSC编译器符合ANSI标准的C语言程序开发,同时又针对FSC单片机的一些特点进行了必要的扩展。
通常情况下,一个完整的C语言源程序大致包括:包含文件(头文件),变量定义,常量说明,函数定义,函数体,注释等部分,如例1-1所示。本节将对C语言的概念作一些介绍和说明,具体可以参考附件1及其他的C语言教程。
例1-1:
#include // 包含单片机内部的寄存器预定义
#include “LCD.h” // 包含自己定义的头文件
// 编译预处理及常量定义
#define uchar unsigned char
#define uint unsigned int
# define ulong unsigned long
#define Lcdch0 0B01111101
#define Lcdch1 0B01100000
#define Lcdch2 0B00111110
#define Lcdch3 0B01111010
#define Lcdch4 0B01100011
#define Lcdch5 0B01011011
#define Lcdch6 0B01011111
#define Lcdch7 0B01110001
#define Lcdch8 0B01111111
#define Lcdch9 0B01111011
#define LcdchA 0B01110111
#define Lcdchb 0B01001111
#define LcdchC 0B00011101
#define Lcdchd 0B01101110
#define LcdchE 0B00011111
#define LcdchF 0B00010111
#define Lcdchi 0B00000100
#define LcdchL 0B00001101
#define Lcdchu 0B01001100
#define LcdchUu 0B01101101
#define Lcdcho 0B01001110
#define Lcdchr 0B00000110
#define Lcdchn 0B01000110
#define LcdchP 0B00110111
#define LcdchH 0B01100111
#define Lcdcht 0B00001111
#define LcdchUt 0B00010101
#define LcdchY 0B01101011
#define LcdDash 0B00000010
#define Lcdequ 0B00001010
// 声明本模块中所调用的函数类型
void Clock(void) ;
void delay(void) ;
void LCD_display(unsigned char a) ;
void systemini(void) ;
// 定义变量
const unsigned char lcd_num[] = { Lcdch0, Lcdch1, Lcdch2,Lcdch3, Lcdch4, Lcdch5,Lcdch6, Lcdch7,Lcdch8,Lcdch9, LcdchA, Lcdchb, LcdchC, Lcdchd, LcdchE,LcdchH,LcdchL } ;
uchar x,y ;
// 函数和子程序
void main(void) {
systemini (); // 单片机的状态寄存器的设定
LCD1=LCD2=LCD3=LCD4=LCD5=LCD6=0;
LCDENR=0X27;
LCD2= lcd_num[15] ; // “H”
LCD3= lcd_num[14] ; // “E”
LCD4= lcd_num[16] ; // “L”
LCD5= lcd_num[16] ; // “L”
LCD6= lcd_num[0] ; // “O”
While(1){
asm(“CLRWDT”);// 清看门狗
asm (“nop”) ;
asm (“nop”) ;
asm (“SLEEP”) ;
delay();
}
}
Void delay(void){
Unsigned char i ;
For(i=0;i=<20;i++ )
Asm ( “nop”) ;
}
类型 | 大小(位) | 数字类型 | 值 |
bit | 1 | 逻辑类型 | 0 或 1 |
signed char | 8 | 有符号字符 | -128..+127* |
unsigned char | 8 | 无符号字符 | 0..255 |
signed short | 16 | 有符号整数 | -32768..+32767 |
unsigned short | 16 | 无符号整数 | 0..65535 |
signed int | 16 | 有符号整数 | -32768..+32767 |
unsigned int | 16 | 无符号整数 | 0..65535 |
signed long | 32 | 有符号整数 | -2147483648..+2147483647 |
unsigned long | 32 | 无符号整数 | 0..4294967295 |
float | 24 | 浮点 |
|
double | 24 or 32 | 浮点 | ** |
进制 | 格式 | 举例 |
二进制 | 0bnumber or 0Bnumber | 0b10010101 |
八进制 | 0number | 0763 |
十进制 | Number | 123 |
十六进制 | 0xnumber or 0Xnumber | 0x2E |
bit型位变量只能是全局的或者静态的。HTFSC把同一地址区域内的8个位变量合并成一个字节存放在一个固定地址。因此所有针对位变量的操作将直接使用FSC单片机的位操作汇编指令高效实现。基于此,位变量不能是局部自动型变量,也无法将其组合成复合型高级变量。PICC 对整个数据存储空间实行位编址,0x000 单元的第0 位是位地址0x0000,以此后推,每个字节有8 个位地址。编制位地址的意义纯粹是为了编译器最后产生汇编级位操作指令而用,对编程人员来说基本可以不管。但若能了解位变量的位地址编址方式就可以在最后
程序调试时方便地查找自己所定义的位变量,如果一个位变量flag1 被编址为0x123,那么实际的存储空间位于:
字节地址=0x123/8 = 0x24
位偏移 =0x123%8 = 3
即flag1 位变量位于地址为0x24 字节的第3 位。在程序调试时如果要观察flag1 的变化,必须观察地址为0x24 的字节而不是0x123。FSC单片机的位操作指令是非常高效的。因此,HTFSC 在编译原代码时只要有可能,对普通变量的操作也将以最简单的位操作指令来实现。假设一个字节变量tmp 最后被定位在地址0x20,那么:
tmp |= 0x80 => bsf 0x20,7
tmp &= 0xf7 => bcf 0x20,3
if (tmp&0xfe) => btfsc 0x20,0
即所有只对变量中某一位操作的C 语句代码将被直接编译成汇编的位操作指令。虽然编程时可以不用太关心,但如果能了解编译器是如何工作的,那将有助于引导我们写出高效简洁的C 语言原程序。
当程序中把非位变量强制转换成位变量或者对位变量赋为非0或者1的值时,编译器只对普通变量或者常量的最低位进行判断:如果是0则转换为位变量为0,如果是1则转换为1。而标准的ANSI-C 做法是判整个变量值是否为0。另外,函数可以返回一个位变量,实际上此返回的位变量将存放于单片机的进位位中带出返回。
1.3.3 HTFSC 中的浮点数
HTFSC 中描述浮点数是以IEEE-754 标准格式实现的。此标准下定义的浮点数为32 位长,在单片机中要用4 个字节存储。为了节约单片机的数据空间和程序空间,HTFSC专门提供了一种长度为24 位的截短型浮点数,它损失了浮点数的一点精度,但浮点运算的效率得以提高。在程序中定义的float 型标准浮点数的长度固定为24 位,双精度double 型浮点数一般也是24 位长,但可以在程序编译选项中选择double 型浮点数为32 位,以提高计算的精度。一般控制系统中关心的是单片机的运行效率,因此在精度能够满足的前提下尽量选择24 位的浮点数运算。
1.3.4 HTFSC中变量的绝对定位
一般来说,在用C语言写程序时变量一般由编译器和连接器最后定位,在写程序的时候无需关心变量最后被放在了哪个地址。
真正需要绝对定位的是真正需要绝对定位的只是单片机中的那些特殊功能寄存器,而这些寄存器的地址定位在HTFSC编译环境所提供的头文件中已经实现,无需用户操心。编程员所要了解的也就是HTFSC是如何定义这些特殊功能寄存器和其中的相关控制位的名称。好在HTFSC的定义标准基本上按照芯片的数据手册中的名称描述进行,这样就秉承了变量命名的一贯性。一个变量绝对定位的例子如下:
unsigned char tmpData @ 0x20; //tmpData 定位在地址0x20
千万注意,HTFSC对绝对定位的变量不保留地址空间。换句话说,上面变量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 事先没有被绝对定位,那就不能用上面的位变量定位方式。
1.3.5 HTFSC的其它变量修饰关键词
● static — 静态局部变量声明
该类型变量与auto类型变量有相同的作用范围,都是定义在函数体内,但是生命周期不一样,函数返回后该类型的地址不会被系统收回。其的定义如下:
void delay(){
auto unsigned char i ;
static unsigned char j ;
……………………
}
● global — 全局变量声明
在main()等函数以外定义的该类型变量称为全局变量(或公共变量),可以用来做函数之间的变量传递。
● extern — 外部变量声明
如果在一个C 程序文件中要使用一些变量但其原型定义写在另外的文件中,那么在本文件中必须将这些变量声明成“extern”外部类型。例如程序文件code1.c 中有如下定义:
unsigned char var1, var2; //定义了bank1 中的两个变量
在另外一个程序文件code2.c 中要对上面定义的变量进行操作,则必须在程序的开头定义:
extern unsigned char var1, var2; //声明位于bank1 的外部变量
● volatile — 易变型变量声明
HTFSC中还有一个变量修饰词在普通的C 语言介绍中一般是看不到的,这就是关键词“volatile”。顾名思义,它说明了一个变量的值是会随机变化的,即使程序没有刻意对它进行任何赋值操作。在单片机中,作为输入的IO 端口其内容将是随意变化的;在中断内被修改的变量相对主程序流程来讲也是随意变化的;很多特殊功能寄存器的值也将随着指令的运行而动态改变。所有这种类型的变量必须将它们明确定义成“volatile”类型,例如:
volatile unsigned char STATUS @ 0x03;
volatile bit commFlag;
“volatile”类型定义在单片机的C 语言编程中是如此的重要,是因为它可以告诉编译器的优化处理器这些变量是实实在在存在的,在优化过程中不能无故消除。假定你的程序定义了一个变量并对其作了一次赋值,但随后就再也没有对其进行任何读写操作,如果是非volatile 型变量,优化后的结果是这个变量将有可能被彻底删除以节约存储空间。另外一种情形是在使用某一个变量进行连续的运算操作时,这个变量的值将在第一次操作时被复制到中间临时变量中,如果它是非volatile 型变量,则紧接其后的其它操作将有可能直接从临时变量中取数以提高运行效率,显然这样做后对于那些随机变化的参数就会出问题。只要将其定义成volatile 类型后,编译后的代码就可以保证每次操作时直接从变量地址处取数。
● const — 常数型变量声明
如果变量定义前冠以“const”类型修饰,那么所有这些变量就成为常数,程序运行过程中不能对其修改。除了位变量,其它所有基本类型的变量或高级组合变量都将被存放在程序空间(ROM 区)以节约数据存储空间。显然,被定义在ROM 区的变量是不能再在程序中对其进行赋值修改的,这也是“const”的本来意义。实际上这些数据最终都将以“retlw”的指令形式存放在程序空间,但HTFSC会自动编译生成相关的附加代码从程序空间读取这些常数,编程员无需太多操心。例如:
const unsigned char name[]=”This is a demo”; //定义一个常量字符串
如果定义了 “const”类型的位变量,那么这些位变量还是被放置在RAM 中,但程序不能对其赋值修改。本来,不能修改的位变量没有什么太多的实际意义,相信大家在实际编程时不会大量用到。
● persistent — 非初始化变量声明
按照标准C 语言的做法,程序在开始运行前首先要把所有定义的但没有预置初值的变量全部清零。HTFSC会在最后生成的机器码中加入一小段初始化代码来实现这一变量清零操作,且这一操作将在main 函数被调用之前执行。问题是作为一个单片机的控制系统有很多变量是不允许在程序复位后被清零的。为了达到这一目的,HTFSC 提供了“persistent”修饰词以声明此类变量无需在复位时自动清零,编程员应该自己决定程序中的那些变量是必须声明成“persisten”类型,而且须自己判断什么时候需要对其进行初始化赋值。例如:
persistent unsigned char hour,minute,second; //定义时分秒变量
经常用到的是如果程序经上电复位后开始运行,那么需要将persistent 型的变量初始化,如果是其它形式的复位,例如看门狗引发的复位,则无需对persistent 型变量作任何修改。FSC单片机内提供了各种复位的判别标志,用户程序可依具体设计灵活处理不同的复位情况。
1.4 HTFSC中的指针
HTFSC 中指针的基本概念和标准C 语法没有太多的差别。但是在FSC单片机这一特定的架构上,指针的定义方式还是有几点需要特别注意。
● 指向RAM 的指针
如果是汇编语言编程,实现指针寻址的方法肯定就是用FSR 寄存器,HTFSC也不例外。为了生成高效的代码,HTFSC在编译C 原程序时将指向RAM 的指针操作最终用FSR 来实现间接寻址。这样就势必产生一个问题:FSR 能够直接连续寻址的范围是256 字节(例如:0x80~0xFF),要覆盖512 字节或者更大的内部数据存储空间,又该如何让定义指针?HTFSC利用二次间接寻址的方式来实现,这就存在一个问题,这样就会造成执行效率的下降,因此在编程的时候,建议尽量使需要指针寻址的变量存放在0x80~0xFF的区域内。
● 指向ROM 常数的指针
如果一组变量是已经被定义在ROM 区的常数,那么指向它的指针可以这样定义:
const unsigned char company[]=”FSC”; //定义ROM 中的常数
const unsigned char *romPtr; //定义指向ROM 的指针
程序中可以对上面的指针变量赋值和实现取数操作:
romPtr = company; //指针赋初值
data = *romPtr++; //取指针指向的一个数,然后指针加1
反过来,下面的操作将是一个错误,因为该指针指向的是常数型变量,不能赋值。
*romPtr = data; //往指针指向的地址写一个数
● 指向函数的指针
单片机编程时函数指针的应用相对较少,但作为标准C 语法的一部分,HTFSC 同样支持函数指针调用。如果你对编译原理有一定的了解,就应该明白在FSC单片机这一特定的架构上实现函数指针调用的效率是不高的:HTFSC将在RAM 中建立一个调用返回表,真正的调用和返回过程是靠直接修改PC 指针来实现的。因此,除非特殊算法的需要,建议大家尽量不要使用函数指针。
● 指针的类型修饰
前面介绍的指针定义都是最基本的形式。和普通变量一样,指针定义也可以在前面加上特殊类型的修饰关键词,例如“persistent”、“volatile”等。考虑指针本身还要限定其作用域,因此HTFSC 中的指针定义初看起来显得有点复杂,但只要了解各部分的具体含义,理解一个指针的实际用途就变得很直接。
//定义指向易变型字符变量的指针,指针变量自身为非易变型
volatile unsigned char *ptr0;
//定义指向非易变型字符变量的指针,指针变量自身为易变型
unsigned char * volatile ptr0;
//定义指向ROM 区的指针,指针变量本身也是存放于ROM 区的常数
const unsigned char * const ptr0;
亦即出现在前面的修饰词其作用对象是指针所指处的变量;出现在后面的修饰词其作用对象就是指针变量自己。
1.5 HTFSC 中的子程序和函数
(1)函数的长度限制
HTFSC 决定了C 原程序中的一个函数经编译后生成的机器码一定会放在同一个程序页面内。FSC单片机其一个程序页面的长度是2K 字,换句话说,用C 语言编写的任何一个函数最后生成的代码不能超过2K 字。一个良好的程序设计应该有一个清晰的组织结构,把不同的功能用不同的函数实现是最好的方法,因此一个函数2K 字长的限制一般不会对程序代码的编写产生太多影响。如果为实现特定的功能确实要连续编写很长的程序,这时就必须把这些连续的代码拆分成若干函数,以保证每个函数最后编译出的代码不超过一个页面空间。
(2)函数调用层次的限制
FSC单片机的硬件堆栈深度最大为8 级,考虑中断响应需占用一级堆栈,所有函数调用嵌套的最大深度不要超过7 级。编程员必须自己控制子程序调用时的嵌套深度以符合这一限制要求。HTFSC 在最后编译连接成功后可以生成一个连接定位映射文件(*.map),在此文件中有详细的函数调用嵌套指示图“call graph”,建议大家要留意一下。其信息大致如下(取自于一示范程序的编译结果):
Call graph:
*_main size 0,0 offset 0
_RightShift_C
* _Task size 0,1 offset 0
lwtoft
ftmul size 0,0 offset 0
ftunpack1
ftunpack2
ftadd size 0,0 offset 0
ftunpack1
ftunpack2
ftdenorm
例1-4 C函数调用层次图
上面所举的信息表明整个程序在正常调用子程序时嵌套最多为两级(没有考虑中断)。因为main 函数不可能返回,故其不用计算在嵌套级数中。其中有些函数调用是编译代码时自动加入的库函数,这些函数调用从C 原程序中无法直接看出,但在此嵌套指示图上则一目了然。
(3)函数类型声明
HTFSC 在编译时将严格进行函数调用时的类型检查。一个良好的习惯是在编写程序代码前先声明所有用到的函数类型。例如:
void Task(void);
unsigned char Temperature(void);
void BIN2BCD(unsigned char);
void TimeDisplay(unsigned char, unsigned char);
这些类型声明确定了函数的入口参数和返回值类型,这样编译器在编译代码时就能保证生成正确的机器码。笔者在实际工作中有时碰到一些用户声称发现C 编译器生成了错误的代码,最后究其原因就是因为没有事先声明函数类型所致。建议大家在编写一个函数的原代码时,立即将此函数的类型声明复制到源文件的起始处,见例1-1;或是复制到专门的包含头文件中,再在每个原程序模块中引用。
(4)中断函数的实现
HTFSC 可以实现C 语言的中断服务程序。中断服务程序有一个特殊的定义方法:
void interrupt ISR(void);
其中的函数名“ISR”可以改成任意合法的字母或数字组合,但其入口参数和返回参数类型必须是“void”型,亦即没有入口参数和返回参数,且中间必须有一个关键词“interrupt”。中断函数可以被放置在原程序的任意位置。因为已有关键词“interrupt”声明,HTFSC 在最后进行代码连接时会自动将其定位到0x04 中断入口处,实现中断服务响应。编译器也会实现中断函数的返回指令“retfie”。一个简单的中断服务示范函数如下:
void interrupt isr(void){
if (E0IF) {
LCD6 = lcd_num[++DisplayFlag];
if (DisplayFlag > 3)
DisplayFlag = 0;
LCDENR = 0x27;
_delay(154195);
}
INTF = 0;
return;
}
例1-5 C语言中断函数举例
HTFSC 会自动加入代码实现中断现场的保护,并在中断结束时自动恢复现场,所以编程员无需象编写汇编程序那样加入中断现场保护和恢复的额外指令语句。但如果在中断服务程序中需要修改某些全局变量时,是否需要保护这些变量的初值将由编程员自己决定和实施。
用C 语言编写中断服务程序必须遵循高效的原则:
● 代码尽量简短,中断服务强调的是一个“快”字。
● 避免在中断内使用函数调用。虽然PICC 允许在中断里调用其它函数,但为了解决递归调用的问题,此函数必须为中断服务独家专用。既如此,不妨把原本要写在其它函数内的代码直接写在中断服务程序中。
● 避免在中断内进行数学运算。数学运算将很有可能用到库函数和许多中间变量,就算不出现递归调用的问题,光在中断入口和出口处为了保护和恢复这些中间临时变量就需要大量的开销,严重影响中断服务的效率。
(5) 标准库函数
HTFSC 提供了较完整的C 标准库函数支持,其中包括数学运算函数和字符串操作函数。在程序中使用这些现成的库函数时需要注意的是入口参数必须在0x80~0xFF中。如果需要用到数学函数,则应在程序前 “#include ” 包含头文件;如果要使用字符串操作函数,就需要包含“#include ”头文件。在这些头文件中提供了函数类型的声明。通过直接查看这些头文件就可以知道HTFSC提供了哪些标准库函数。如果是自己编写的头文件需要使用这样的格式,才可以正确地使用:#include “LCD.h”,引号内为你编写的头文件名。
C 语言中常用的格式化打印函数“printf/sprintf”用在单片机的程序中时要特别谨慎。printf/sprintf 是一个非常大的函数,一旦使用,你的程序代码长度就会增加很多。除非是在编写试验性质的代码,可以考虑使用格式化打印函数以简化测试程序;一般的最终产品设计都是自己编写最精简的代码实现特定格式的数据显示和输出。本来,在单片机应用中输出的数据格式都相对简单而且固定,实现起来应该很容易。对于标准C 语言的控制台输入(scanf)/输出(printf)函数,HTFSC 需要用户自己编写其底层函数getch()和putch()。在单片机系统中实现scanf/printf 本来就没什么太多意义,如果一定要实现,只要编写好特定的getch()和putch()函数,你就可以通过任何接口输入或输出格式化的数据。
1.6 C 和汇编混合编程
有两个原因决定了用C 语言进行单片机应用程序开发时使用汇编语句的必要性:单片机的一些特殊指令操作在标准的C 语言语法中没有直接对应的描述,例如FSC 单片机的清看门狗指令“clrwdt”和休眠指令“sleep”;单片机系统强调的是控制的实时性,为了实现这一要求,有时必须用汇编指令实现部分代码以提高程序运行的效率。这样,一个项目中就会出现C 和汇编混合编程的情形,我们在此讨论一些混合编程的基本方法和技巧。
1.6.1 嵌入行内汇编的方法
在C 原程序中直接嵌入汇编指令是最直接最容易的方法。如果只需要嵌入少量几条的汇编指令,PICC 提供了一个类似于函数的语句:
asm(“clrwdt”);
双引号中可以编写任何一条PIC 的标准汇编指令。例如:
for (;;) {
asm("clrwdt"); //清看门狗
Task();
ClockRun();
asm("sleep"); //休眠
asm("nop"); //空操作延时
}
例1-6 逐行嵌入汇编的方式
如果需要编写一段连续的汇编指令,PICC 支持另外一种语法描述:用“#asm”开始汇编指令段,用“#endasm”结束。例如下面的一段嵌入汇编指令实现了将0x20~0x7F间的RAM 全部清零:
#asm
movlw 0x20
movwf _FSR
clrf _INDF
incf _FSR,f
btfss _FSR,7
goto $-3
#endasm
例1-7 整段嵌入汇编的方式
1.6.2汇编指令寻址C 语言定义的全局变量
C 语言中定义的全局或静态变量寻址是最容易的,因为这些变量的地址已知且固定。按C 语言的语法标准,所有C 中定义的符号在编译后将自动在前面添加一下划线符“_”,因此,若要在汇编指令中寻址C 语言定义的各类变量,一定要在变量前加上一“_”符号,我们在上面例1-7 中已经体现了这一变量引用的法则,因为FSR 和INDF 等所有特殊寄存器是以C 语言语法定义的,因此汇编中需要对其寻址时前面必须添加下划线。对于C 语言中用户自定义的全局变量,用行内汇编指令寻址时也同样必须加上“_” ,下面的例1-8 说明了具体的引用方法:
volatile unsigned char tmp; //定义字符型全局变量
void Test(void) //测试程序
{
tmp=0 ;
STATUS=0;// 必需的
#asm //开始行内汇编
movlw 0x10 //设定初值
movwf _tmp //tmp=0x10
#endasm //结束行内汇编
if (tmp==0x10) { //开始C 语言程序
;
}
}
例1-8 行内汇编寻址C 全局变量
上面的例子说明了汇编指令中寻址C 语言所定义变量的基本方法。HTFSC在编译处理嵌入的行内汇编指令时将会原封不动地把这些指令复制成最后的机器码。所有对C 编译器所作的优化设定对这些行内汇编指令而言将不起任何作用。因此这就存在一个问题,如果定义一个全局变量(没有进行任何的赋值操作),但是该变量只在行内汇编中使用到,编译时,该变量会被优化掉,在执行汇编的指令时,编译器会找不到该变量的地址,因此在使用行内汇编的时候,我们建议必须对你在行内汇编要进行操作的寄存器或者变量进行赋值操作,例如:FSR0=0;该指令不会增加任何的代码空间。同样在行内汇编寻址局部变量时也必须这样做。
1.6.3 汇编指令寻址C 函数的局部变量
前面已经提到,HTFSC 对自动型局部变量(包括函数调用时的入口参数)采用一种“静态覆盖”技术对每一个变量确定一个固定地址,因此嵌入的汇编指令对其寻址时只需采用数据寄存器的直接寻址方式即可,唯一要考虑的是如何才能在编写程序时知道这些局部变量的寻址符号(具体地址在最后连接后才能决定,编程时无需关心)。一个最实用也是最可靠的方法是先编写一小段C 代码,其中有最简单的局部变量操作指令,然后把C源代码编译成对应的HTFSC 汇编指令,查看C 编译器生成的汇编指令是如何寻址这些局部变量的,你自己编写的行内汇编指令就采用同样的寻址方式。例如,例1-9中的一小段C 原代码编译出的汇编指令:
//C 原程序代码
void Test(unsigned char inVar1, inVar2)
{
unsigned char tmp1, tmp2;
inVar1++;
inVar2--;
tmp1 = 1;
tmp2 = 2;
}
//编译器生成的汇编指令
_Test
; _tmp1 assigned to ?a_Test+0 //tmp1 的寻址符为 ?a_Test+0
_Test$tmp1 set ?a_Test
; _tmp2 assigned to ?a_Test+1 //tmp2 的寻址符为 ?a_Test+1
_Test$tmp2 set ?a_Test+1
; _inVar1 assigned to ?a_Test+2 //inVar1 的寻址符为 ?a_Test+2
_Test$inVar1 set ?a_Test+2
line 44
;_inVar1 stored from w //第一个字符型行参由W 寄存器传递
bcf 3,5
bcf 3,6
movwf ?a_Test+2
;ht16.c: 43: unsigned char tmp1, tmp2;
incf ?a_Test+2
line 45
;ht16.c: 45: inVar2--;
decf ?_Test //行参inVar2 的寻址符为 ?_Test
line 46
;ht16.c: 46: tmp1 = 1;
clrf ?a_Test
incf ?a_Test
line 47
;ht16.c: 47: tmp2 = 2;
movlw 2
movwf ?a_Test+1
line 48
;ht16.c: 48: }
return
例1-9 HTFSC 实现局部变量操作的寻址方式
基于上面得到HTFSC 编译后局部变量的寻址方式,我们在C 语言程序中用嵌入汇编指令时必须采样同样的寻址符以实现对应变量的存取操作,见下面的例1-10:
//C 原程序代码
void Test(unsigned char inVar1, inVar2)
{
unsigned char tmp1=0, tmp2=0;
#asm //开始嵌入汇编
incf ?a_Test+0,f //tmp1++;
decf ?a_Test+1,f //tmp2--;
movlw 0x10
addwf ?a_Test+2,f //inVar1 += 0x10;
rrf ?_Test,w //inVar2 循环右移一位
rrf ?_Test,f
#endasm //结束嵌入汇编
}
例1-10 嵌入汇编指令实现局部变量寻址操作
如果局部变量为多字节形式组成,例如整型数、长整型等,必须按照HTFSC约定的存储格式进行存取。前面已经说明了HTFSC采用“Little endian”格式,低字节放在低地址,高字节放在高地址。下面的例1-11 实现了一个整型数的循环移位,在C 语言中没有直接针对循环移位的语法操作,用标准C 指令实现的效率较低。
//16 位整型数循环右移若干位
unsigned int RR_Shift16(unsigned int var, unsigned char count)
{
while(count--) //移位次数控制
{
#asm //开始嵌入汇编
rrf ?_RR_Shift16+0,w //最低位送入C
rrf ?_RR_Shift16+1,f //var 高字节右移1 位,C 移入最高位
rrf ?_RR_Shift16+0,f //var 低字节右移1 位
#endasm //结束嵌入汇编
}
return(var); //返回结果
}
例1-11 嵌入汇编指令对多字节变量的操作
从上面的分析过程可以看出来,使用全局变量最大的好处是寻址直观,只需在C 语言定义的变量名前增加一个下划线“_”符即可在汇编语句中寻址;使用全局变量进行参数传递的效率也比形参高。编写单片机的C程序时不能死硬强求教科书上的模块化编程而大量采用行参和局部变量的做法,在开发编程时应视实际情况灵活变通,一切以最高的代码效率为目标,需要注意的是,嵌入汇编不是完整意义上的汇编,是一种伪汇编指令,使用时必须注意它们与编译器生成代码之间的互相影响。
1.6.4 C语言调用外部的汇编模块
当然,HTFSC也有其的汇编编译器,使用者也可以编写后缀为.as的汇编文件,基于此,C与汇编混合编程还有一种方法是将汇编作为一个独立的模块,用汇编编译器生成目标文件,然后用链接器和C语言生成的其它模块的目标文件链接在一起。如果变量要公用时,则在另一个模块中说明为外部类型,并允许使用形式参数和返回值。
例如,如果在C模块中使用汇编模块中的函数,那么在C中可如下声明:
extern char rotate_left(char);
本声明说明了要调用的这个外部函数有一个char型形式参数,并返回一个char型的值。而rotate_left()函数的真正函数体在外部可以被汇编编译器编译的汇编模块(文件名后缀.as)中,具体代码可以如下编写:
PSECT text0,class=CODE,local,delta=2;这段代码照抄即可,具体含义可以参见文献【1】
GLOBAL _rotate_left
SIGNAT _rotate_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,SIGNAL强制链接器在链接各个目标文件模块时进行类型匹配检查,FNSIZE定义局部变量和形式参数的内存分配。
这种方法比较麻烦,如果对某一模块的执行效率要求较高时,可以采取这种办法;但是,为了保证汇编程序能正常运行,必须严格遵守函数参数传递和返回规则。当然,为避免这些规则带来的麻烦,一般情况下,可以先用C语言大致编写一个类似功能的函数,预先定义好各种变量,采用HTFSC-S选项对程序进行编译,然后手工优化编译器产生的汇编代码后将其作为独立的模块就可以了。
同时在混合编程时须注意:1.无法使用" INCLUDE"的方式; 2. 在ASM的Compiler中ADDPCW之后不要使用label。总体说来,混合编程有其的好处,可以提高代码的执行效率,充分发掘单片机的性能,但也有其不方便的地方,需要编程者既要熟悉软件,又要精通硬件,至于以什么方式进行编程,那要看使用者对硬件的了解程度,一切以提高代码的执行效率为目标。
1.7 C 语言中的运算符
1.7.1 算术运算符:
C 语言中有7种算术运算符,如表1-3所示:
符号 | 含 义 | 说 明 |
+ | 加法运算或者表示正数 | 进行加法运算 |
- | 减法运算或者表示负数 | 进行减法运算 |
* | 乘法运算符 | 进行乘法运算 |
/ | 除法运算符 | 进行除法运算 |
% | 取模运算(取余) | 运算数都是整数,例5%2=1 |
++ | 自增运算符 | 操作数+1 |
―― | 自减运算符 | 操作数-1 |
表1-3:C 语言中的算术运算符
类 型 | 符 号 | 含 义 | 说 明 |
关 | > | 大于 |
|
系 | >= | 大于或等于 |
|
运 | == | 等于 |
|
算 | < | 小于 |
|
符 | <= | 小于或等于 |
|
| != | 不等于 |
|
逻辑 | && | 逻辑“与” |
|
运算符 | || | 逻辑“或” |
|
| ! | 逻辑“非” | 一元运算符,!1=0,!0=1 |
符号 | 含 义 | 说 明 |
& | AND(按位“与”) | 两位都是1为1,否则为0 |
| | OR(按位“或”) | 两位都是0为0,否则为1 |
^ | XOR(按位“异或”) | 两位不同结果为1,否则为0 |
~ | 按位取反 | 单操作运算 |
>>
| 位右移(相当于除于2) | 右边移出位舍去,非负数左边补0,否则补1 |
<< | 位左移(相当于乘于2) | 左边移出位舍去,右边补0 |
对程序进行优化,通常是指优化程序代码或程序执行速度。优化代码和优化速度实际上是一个矛盾的统一,一般是优化了代码的尺寸,就会带来执行时间的增加,如果优化了程序的执行速度,通常会带来代码增加的副作用,很难鱼与熊掌兼得,只能在设计时掌握一个平衡点。
一、程序结构的优化
1、程序的书写结构
虽然书写格式并不会影响生成的代码质量,但是在实际编写程序时还是应该尊循一定的书写规则,一个书写清晰、明了的程序,有利于以后的维护。在书写程序时,特别是对于While、for、do…while、if…elst、switch…case 等语句或这些语句嵌套组合时,应采用“缩格”的书写形式;
2、标识符
程序中使用的用户标识符除要遵循标识符的命名规则以外,一般不要用代数符号(如a、b、x1、y1)作为变量名,应选取具有相关含义的英文单词(或缩写)或汉语拼音作为标识符,以增加程序的可读性,如:count、number1、red、work 等。
3、程序结构
C 语言是一种高级程序设计语言,提供了十分完备的规范化流程控制结构。因此在采用C 语言设计单片机应用系统程序时,首先要注意尽可能采用结构化的程序设计方法,这样可使整个应用系统程序结构清晰,便于调试和维护。于一个较大的应用程序,通常将整个程序按功能分成若干个模块,不同模块完成不同的功能。各个模块可以分别编写,甚至还可以由不同的程序员编写,一般单个模块完成的功能较为简单,设计和调试也相对容易一些。在C 语言中,一个函数就可以认为是一个模块。所谓程序模块化,不仅是要将整个程序划分成若干个功能模块,更重要的是,还应该注意保持各个模块之间变量的相对独立性,即保持模块的独立性,尽量少使用全局变量等。对于一些常用的功能模块,还可以封装为一个应用程序库,以便需要时可以直接调用。但是在使用模块化时,如果将模块分成太细太小,又会导致程序的执行效率变低(进入和退出一个函数时保护和恢复寄存器占用了一些时间)。
4、定义常数
在程序化设计过程中,对于经常使用的一些常数,如果将它直接写到程序中去,一旦常数的数值发生变化,就必须逐个找出程序中所有的常数,并逐一进行修改,这样必然会降低程序的可维护性。因此,应尽量当采用预处理命令方式来定义常数,而且还可以避免输入错误。
5、减少判断语句
能够使用条件编译(ifdef)的地方就使用条件编译而不使用if 语句,有利于减少编译生成的代码的长度。
6、表达式
对于一个表达式中各种运算执行的优先顺序不太明确或容易混淆的地方,应当采用圆括号明确指定它们的优先顺序。一个表达式通常不能写得太复杂,如果表达式太复杂,时间久了以后,自己也不容易看得懂,不利于以后的维护。
7、函数
对于程序中的函数,在使用之前,应对函数的类型进行说明,对函数类型的说明必须保证它与原来定义的函数类型一致,对于没有参数和没有返回值类型的函数应加上“void”说明。如果果需要缩短代码的长度,可以将程序中一些公共的程序段定义为函数,在Keil 中的高级别优化就是这样的。如果需要缩短程序的执行时间,在程序调试结束后,将部分函数用宏定义来代替。注意,应该在程序调试结束后再定义宏,因为大多数编译系统在宏展开之后才会报错,这样会增加排错的难度。
8、尽量少用全局变量,多用局部变量。因为全局变量是放在数据存储器中,定义一个全局变量,MCU 就少一个可以利用的数据存储器空间,如果定义了太多的全局变量,会导致编译器无足够的内存可以分配。而局部变量大多定位于MCU 内部的寄存器中,在绝大多数MCU 中,使用寄存器操作速度比数据存储器快,指令也更多更灵活,有利于生成质量更高的代码,而且局部变量所的占用的寄存器和数据存储器在不同的模块中可以重复利用。
9、设定合适的编译程序选项
许多编译程序有几种不同的优化选项,在使用前应理解各优化选项的含义,然后选用最合适的一种优化方式。通常情况下一旦选用最高级优化,编译程序会近乎病态地追求代码优化,可能会影响程序的正确性,导致程序运行出错。因此应熟悉所使用的编译器,应知道哪些参数在优化时会受到影响,哪些参数不会受到影响。
二、代码的优化
1、 选择合适的算法和数据结构
应该熟悉算法语言,知道各种算法的优缺点,具体资料请参见相应的参考资料,有很多计算机书籍上都有介绍。将比较慢的顺序查找法用较快的二分查找或乱序查找法代替,插入排序或冒泡排序法用快速排序、合并排序或根排序代替,都可以大大提高程序执行的效率。.选择一种合适的数据结构也很重要,比如你在一堆随机存放的数中使用了大量的插入和删除指令,那使用链表要快得多。数组与指针具有十分密码的关系,一般来说,指针比较灵活简洁,而数组则比较直观,容易理解。对于大部分的编译器,使用指针比使用数组生成的代码更短,执行效率更高。但是在Keil 中则相反,使用数组比使用的指针生成的代码更短。
2、 使用尽量小的数据类型
能够使用字符型(char)定义的变量,就不要使用整型(int)变量来定义;能够使用整型变量定义的变量就不要用长整型(long int),能不使用浮点型(float)变量就不要使用浮点型变量。当然,在定义变量后不要超过变量的作用范围,如果超过变量的范围赋值,C 编译器并不报错,但程序运行结果却错了,而且这样的错误很难发现。尽量使用基本型参数(%c、%d、%x、%X、%u 和%s 格式说明符),少用长整型参数(%ld、%lu、%lx 和%lX 格式说明符),至于浮点型的参数(%f)则尽量不要使用,其它C 编译器也一样。在其它条件不变的情况下,使用%f 参数,会使生成的代码的数量增加很多,执行速度降低。
3、 使用自加、自减指令
通常使用自加、自减指令和复合赋值表达式(如a-=1 及a+=1 等)都能够生成高质量的程序代码,编译器通常都能够生成inc 和dec 之类的指令,而使用a=a+1 或a=a-1 之类的指令,有很多C 编译器都会生成二到三个字节的指令。
4、减少运算的强度
可以使用运算量小但功能相同的表达式替换原来复杂的的表达式。如下:
(1)、求余运算。
a=a%8;
可以改为:
a=a&7;
说明:位操作只需一个指令周期即可完成,而大部分的C 编译器的“%”运算均是调用子程序来完成,代码
长、执行速度慢。通常,只要求是求2n 方的余数,均可使用位操作的方法来代替。
(2)、平方运算
a=pow(a,2.0);
可以改为:
a=a*a;
如果是求3 次方,如:
a=pow(a,3.0);
更改为:
a=a*a*a;
则效率的改善更明显。
(3)、用移位实现乘除法运算
a=a*4;
b=b/4;
可以改为:
a=a<<2;
b=b>>2;
说明:通常如果需要乘以或除以2n,都可以用移位的方法代替。实际上,只要是乘以或除以一个整数,均可以用移位的方法得到结果,如:
a=a*9
可以改为:
a=(a<<3)+a
5、循环
(1)、循环语
对于一些不需要循环变量参加运算的任务可以把它们放到循环外面,这里的任务包括表达式、函数的调用、指针运算、数组访问等,应该将没有必要执行多次的操作全部集合在一起,放到一个init 的初始化程序中进行。
(2)、延时函数:
通常使用的延时函数均采用自加的形式:
void delay (void)
{
unsigned int i;
for (i=0;i<1000;i++)
;
}
将其改为自减延时函数:
void delay (void)
{
unsigned int i;
for (i=1000;i>0;i--)
;
}
两个函数的延时效果相似,但几乎所有的C 编译对后一种函数生成的代码均比前一种代码少1~3 个字节,因为几乎所有的MCU 均有为0 转移的指令,采用后一种方式能够生成这类指令。在使用while 循环时也一样,使用自减指令控制循环会比使用自加指令控制循环生成的代码更少1~3 个字母。但是在循环中有通过循环变量“i”读写数组的指令时,使用预减循环时有可能使数组超界,要引起注意。
(3)while 循环和do…while 循环
用while 循环时有以下两种循环形式:
unsigned int i;
i=0;
while (i<1000)
{
i++;
//用户程序
}
或:
unsigned int i;
i=1000;
do(i--;){
//用户程序
}
while (i>0);
在这两种循环中,使用do…while 循环编译后生成的代码的长度短于while 循环。
6、查表
在程序中一般不进行非常复杂的运算,如浮点数的乘除及开方等,以及一些复杂的数学模型的插补运算,
对这些即消耗时间又消费资源的运算,应尽量使用查表的方式,并且将数据表置于程序存储区。如果直接生成所需的表比较困难,也尽量在启动时先计算,然后在数据存储器中生成所需的表,后以在程序运行直接查表就可以了,减少了程序执行过程中重复计算的工作量。
7、其它
比如使用在线汇编及将字符串和一些常量保存在程序存储器中,均有利于优化。
用户1427508 2008-12-31 12:59
用户68022 2007-4-5 14:41
受益非浅,很好的东西
用户21270 2007-3-29 21:17
用户1007196 2007-3-29 08:48
太精彩了!
用户1318081 2007-3-6 17:15
呵呵,我们就是请Hi-TECH公司写的编译器内核,所以基本上都是差不多的。还有就是你不能说是类PIC的,因为现在类似PIC的单片机都是采用一个架构RISC,你不能说大家都是抄PIC的,就像intel和amd一样。
bellstudio_534338181 2007-3-6 14:57
小汗一下,基本是picc的翻版
这年头类pic的MCU泛滥了
用户81954 2007-3-1 11:36
谢谢!关于"HTFSC中变量的绝对定位", 我一直有一个疑问,HI-TECH C手册中也有类似的说法:“....对绝对定位的变量不保留地址空间。换句话说,上面变量tmpData 的地址是0x20,但最后0x20 处完全有可能又被分配给了其它变量使用,这样就发生了地址冲突。"。
而实际上MCU内部的功能寄存器在头文件中也是用绝对定位的方式定义的。显然MCU的功能寄存器不会被编译器“挪用”分配给其他变量。这究竟是怎么实现的呢?也就是说,编译器对功能寄存器是如何避免被挪用的?
ash_riple_768180695 2007-3-1 09:34
太好了,精品!
用户1318081 2007-1-26 08:31