KEIL C51

KEIL C51对ANSI C的扩展

深入理解并应用C51对ANSI C的扩展是学习C51的关键之一,因为大多数扩展功能都是直接针对8051系列CPU硬件的。

一、扩展关键字(见附录)

二、存储区

2.1 Pragram Area。

由Code说明可有多达64kBytes的程序存储器。

2.2 Internal Data Memory。

内部数据存储器可用以下关键字说明。

  • data 直接寻址区,为内部RAM的低128字节00H-7FH
  • idata 间接寻址区,包括整个内部RAM区00H-FFH
  • bdata 可位寻址区,20H-2FH

2.3 External Data Memory。

外部RAM视使用情况可由以下关键字标识。

  • xdata 可指定多达64KB的外部直接寻址区 地址范围0000H-0FFFFH
  • pdata 能访问1页(25bBytes)的外部RAM,主要用于紧凑模式(Compact Model)

2.4 Speciac Function Register Memory。

8051提供128Bytes的SFR寻址区,这区域可位寻址、字节寻址或字寻址,用以控制定时器、计数器、串口、I/O及其它部件,可由以下几种关键字说明。

  • sfr字节寻址,比如sfr P0=0x80;为PO口地址为80H-FFH之间的常数
  • sfr16字寻址,如sfr16 T2=0xCC;指定Timer2口地址T2L=0xCC,T2H=0xCD
  • sbit位寻址,如sbit EA=0xAF;指定第0xAF位为EA,即中断允许

还可以有如下定义方法

  • sbit 0V=PSW^2 (定义0V 为PSW 的第2 位)
  • sbit 0V=0XDO^2 (同上)
  • bit 0V=0xD2 (同上)

 

 三、存储模式

存储模式决定了没有明确指定存储类型的变量、函数参数等的缺省存储区域。
3.1 Small 模式
所有缺省变量参数均装入内部RAM,优点是访问速度快,缺点是空间有限,只适用于小程序
3.2 Compact 模式
所有缺省变量均位于外部RAM区的一页(256Bytes)具体哪一页可由P2口指定,在STARTUP.A51文件中说明可用pdata指定,空间较Small为宽裕,速度较Small慢较large要快,是一种中间状态。

3.3 large 模式
所有缺省变量可放在多达64KB的外部RAM区,优点是空间大、可存变量多,缺点是速度较慢。

 

四、存储类型

变量或参数的存储类型可由存储模式指定缺省类型,也可由关键字直接声明指定。各类型分别用code,data,idata,xdata,pdata说明。

 

五、变量或数据类型

bit 位变量值为0或1

sbit 从字节中定义的位变量0或1

sfr 字节地址 0-255

sfr16 字地址 0-65535

其余数据类型如char,enum,short,int,long,float等与ANSIC相同

 

六、位变量与声明

6.1 bit型变量
bit型变量可用变量类型、函数声明、函数返回值等,存贮于内部RAM20H-2FH,但要注意:

  • 用 pragma disable 说明函数和用 usign”指定的函数 不能返回bit 值
  • 一个bit 变量不能声明为指针 如bit *ptr 是错误的
  • 不能有bit 数组如 bit arr[5] 错误

6.2 可位寻址区说明20H-2FH
可作如下定义

int bdata I
char bdata arr[3]

然后

sbit bito in0 sbit bit15=I^15
sbit arr07=arr[0]^7 sbit arr15=arr[i]^7

 

七、指针

C51支持一般指针(Generic Pointer)和存储器指针(Memory_Specific Pointer)。
7.1 一般指针
一般指针的声明和使用均与标准C相同 不过同时还可以说明指针的存储类型,例如long * state为一个指向long 型整数的指针,而state 本身则依存储模式存放;char * xdata ptr为一个指向char数据的指针,而ptr本身放于外部RAM区。以上的long,char 等指针指向的数据可存放于任何存储器中,一般指针本身用3个字节存放,分别为存储器类型、高位偏移、低位偏移量。

7.2 存储器指针
基于存储器的指针说明时即指定了存贮类型,例如char data * str指向data区中char型数据;int xdata * pow指向外部RAM的int型整数。这种指针存放时只需1个字节或2个字节就够了,因为只需存放偏移量。

7.3 指针转换
即指针在上两种类型之间转化,当基于存储器的指针作为一个实参传递给需要一般指针的函数时,指针自动转化。如果不说明外部函数原型,基于存储器的指针自动转化为一般指针导致错误,因而请用include说明所有函数原型可以强行改变指针类型。

7.4 函数指针的使用注意

 

Keil公司推出的C51编译器是事实上80C51 C编程的工业标准,它针对8051系列CPU硬件在标准ANSI C的基础上进行了扩展;但由于编译器及8051体系结构的限制,造成了在使用函数指针时有很多与ANSI C不同的地方。
指向固定地址的指针

 

在程序设计中,常需要跳转到某一特定的地址上执行,如引导程序的设计。可通过如下C语言实现:

int main(void)
{
  ((void(code *)(void))0x2000)();
  return 0;
)

 

此代码使得主函数执行位于0x2000地址的程序代码。其中((void(code* )(void))是一种数据类型,表示一指向代码段函数的指针,该函数无参数和无返回值。它对数据0x2000进行了强制类型转换,使函数指针指向地址为0x2000的代码段地址。

 

无参数的函数指针

 

Keil C51中不带参数的函数指针的使用方法与ANSI C基本相同。示例如下:

void foo(void)
{
  return ;
}
int main(void)
{
  void( pfoo)(void);  //声明函数指针p{oo
  p{oo— foo;      //对该指针赋值,使该指针指向某一函数
  (*pfoo)();      //通过指针调用其指向的函数
  return 0;
}

带参数的函数指针

 

一般来说,函数参数是通过堆栈来传递,用PUSH和POP汇编指令来实现的;但由于8051体系及其编译器的一些限制,使得其函数参数的传递需要一些特殊的方法。通过函数指针调用函数属于函数的间接调用,根据C51的规定,所有的函数参数都需要通过寄存器传递。由于8051的寄存器数目的限制,函数指针最多只能传递3个参数。其声明与调用方式如下:

void( pfun)(charshortint); //声明函数指针
( pfun)( C ,0x12340x5678); //调用该函数

 

如果需要传递3个以上函数的参数,可以把参数存放到结构体里面,再用一个指针指向该结构体作为参数传递给函数指针。也可以使用reentrant关键字将函数声明为可重入函数。

 

分析调用树正确使用指针函数

 

Keil C51编译器与ANSC C编译器的区别之一是,它并不把函数参数压入堆栈中,而是把函数参数放在寄存器(register)或固定的内存位置(fixed memory location)~ 中。调用树(call tree)是由Keil链接器自动生成的,用于描述函数的调用关系。链接器通过分析调用树来确定哪些寄存器或内存位置是可安全覆盖的。这样两个不同时调用的函数就可以共享同一块memory作为传递参数使用。但对于函数指针来说,编译器并不知道函数指针将指向哪个函数。这导致了调用树构造出错的可能,函数的参数也可能被错误覆盖。

 

 

八、函数

8.1 中断函数声明
中断声明方法如下

void serial_ISR () interrupt 4 [using 1]
{
/* ISR */
}

为提高代码的容错能力,在没用到的中断入口处生成iret语句 定义没用到的中断

/* define not used interrupt, so generate "IRET" in their entrance */
void extern0_ISR() interrupt 0{} /* not used */
void timer0_ISR () interrupt 1{} /* not used */
void extern1_ISR() interrupt 2{} /* not used */
void timer1_ISR () interrupt 3{} /* not used */
void serial_ISR () interrupt 4{} /* not used */

8.2 通用存储工作区
8.3 选通用存储工作区由using x 声明,见上面例子
8.4 指定存储模式
由 small compact 及large 说明 例如void fun1(void) small { }提示small说明的函数内部变量全部使用内部RAM,关键的经常性的耗时的地方可以这样声明 以提高运行速度。
8.5 #pragma disable
在函数前声明,只对一个函数有效,该函数调用过程中将不可被中断。
8.6 递归或可重入函数指定
在主程序和中断中都可调用的函数,容易产生问题,因为51和PC不同。PC使用堆栈传递参数且静态变量以外的内部变量都在堆栈中,而51一般使用寄存器传递参数 内部变量一般在RAM中,函数重入时会破坏上次调用的数据,可以用以下两种方法解决函数重入。

  • 在相应的函数前使用前述#pragma disable声明,即只允许主程序或中断之一调用该函数
  • 将该函数说明为可重入的 如下void func(param...) reentrant;KeilC51 编译后将生成一个可重入变量堆栈 然后就可以模拟通过堆栈传递变量的方法。由于一般可重入函数由主程序和中断调用,所以通常中断使用与主程序不同的R寄存器组。另外,对可重入函数,在相应的函数前面加上开关 #pragma noaregs” 以禁止编译器使用绝对寄存器寻址,可生成不依赖于寄存器组的代码。

8.7 由 alien 指定PL/M 51函数

 

九、库函数

9.1 本征库函数

C51提供的本征函数是指编译时直接将固定的代码插入当前行,而不是用ACALL和LCALL语句来实现,这样就大大提供了函数访问的效率。而非本征函数则必须由ACALL及LCALL调用。C51的本征库函数只有9个,数目虽少,但都非常有用,使用时,必须包含#inclucle <intrins.h>一行。本征库函数列举如下:

  • _crol_,_cror_     将char 型变量循环向左(右)移动指定位数后返回
  • _iror_,_irol_       将int 型变量循环向左(右)移动指定位数后返回
  • _lrol_,_lror_       将long 型变量循环向左(右)移动指定位数后返回
  • _nop_                相当于插入NOP
  • _testbit_            相当于JBC bitvar 测试该位变量并跳转同时清除
  • _chkfloat_         测试并返回源点数状态

9.2 非本征库函数

  • 专用寄存器include文件。例如8031、8051均为REG51.h,其中包括了所有8051的SFR及其位定义,一般系统都必须包括本文件。
  • 绝对地址include文件absacc.h。该文件中实际只定义了几个宏(如CBYTE/XBYTE/PWORD/DBYTE/CWORD/XWORD/PBYTE/DWORD)以确定各存储空间的绝对地址
  • 动态内存分配函数,位于stdlib.h中。
  • 缓冲区处理函数位于string.h中,包括拷贝、比较、移动等函数,很方便地对缓冲区进行处理。
  • 输入输出流函数位于stdio.h中。流函数通 8051 的串口或用户定义的I/O 口读写数据 缺省为8051 串口 如要修改 比如改为LCD 显示 可修改lib 目录中的getkey.c 及putchar.c 源文件 然后在库中替换它们即可。

 

 附录 扩展关键字

1.1 _at_:用于在定义变量时指定变量所在地址

struct link
{
  struct link idata *next;
  char code *test;
};
struct link list idata \_at\_ 0x40; /* 定义位于内部RAM低128字节区0x40地址处的 结构体 */
char xdata text[256] \_at\_ 0xE000; /* 定义位于外部RAM 0xE000地址处的 数组 */
int xdata i1 \_at\_ 0x8000; /* 定义位于外部RAM 0x8000地址处的 整型变量 */
volatile char xdata IO \_at\_ 0xFFE8; /* 定义地址为 0xFFE8的外部IO */
char far ftext[256] \_at\_ 0x02E000; /* 定义位于外部RAM 0x2E000地址处的数组 */

在一个.c文件中如上定义,在另一个.c文件中可用如下方法引用这些变量:

extern struct link idata list; 
extern char xdata text[256]; 
extern int xdata i1; 
extern volatile char xdata IO; 

 

1.2 alien:为了C51编译器与PL/M-51编译器兼容

1.3 bdata:使用bdata定义的变量即可字寻址,也可比特位寻址

Keil C51编译器把使用bdata定义的变量放置在8051内部RAM可位寻址区。bdata定义的变量必须为全局变量,不能在任一函数内部定义bdata类型的变量。

bdata型变量定义

Keil C51编译器总是像对sfr16那样把可位寻址的变量视为little endian字节序,标准C类型如int ,long则以big endian方式存储。

int bdata x1; /* 定义可位寻址的整型变量*/
char bdata bary [4]; /* 定义可位寻址的数组*/

bdata型变量某一位定义

此时,x1,bary均可按比特位寻址。使用sbit关键词定义可操作其任一比特位的变量。注意:使用sbit定义针对非bdata类型变量的某一比特位,毫无意义。

sbit mybit0 = x1 ^ 0; /* x1的比特0*/
sbit mybit15 = x1 ^ 15; /* x1的比特15 */
sbit Ary07 = bary[0] ^ 7; /* bary[0]的比特7*/
sbit Ary37 = bary[3] ^ 7; /* bary[3]的比特7 */

调用其他源文件中定义的sbit变量

extern bit mybit0; 
extern bit mybit15; 
extern bit Ary07; 
extern bit Ary37;

操作bdata声明的对象

Ary37 = 0; /* 对bary[3]的比特7清零*/
bary[3] = 'a'; /* bary[3]直接赋值*/
x1 = -1; /* x1直接赋值 */
mybit15 = 1; /* 对x1的比特15置一*/

 

1.4 bit:可定义位变量,可用于函数参数、返回值类型

static bit done_flag = 0; /* 位变量*/
bit testfunc ( /* 返回值类型为位变量 */
bit flag1, /* 参数类型为位变量 */
bit flag2)
{
  。。。
  return (0); 
}

位变量位于8051单片机内部RAM位寻址区,由于位寻址区为16字节,因此最多可定义128个位变量。如果要指定位变量的存储类型,只能使用data 或者idata,其他存储类型声明无效。

bit关键词的使用受限情况:

  • 不能声明指向bit变量的指针。
bit *ptr; /* 错误 */
  • 不能定义bit类型数组。
bit ware [5]; /* 错误 */
  • 使用#pragma disable声明禁用中断的函数,以及使用using n声明的函数,返回值类型不能为bit。

 

1.5 code:指定存储位置位于程序存储器

程序存储器只读,因此code类型的变量,是无法再次赋值的。

unsigned char code ary[ ] = :"Read only"; /* 数组ary位于程序存储器 */
ary[0]='a'; /* 错误,不可修改*/

 

1.6 small/compact/large

small内存模型下函数参数以及局部变量存储在 8051 内部ram区,数据存取效率高于 compact 和 large 内存模型。
compact:函数的参数和局部变量存储在内存模型指定的默认区域内。

#pragma small /* 默认内存模型为small*/

extern int calc (char i, int b) large reentrant;
extern int func (int i, float f) large;
extern void *tcp (char xdata *xp, int ndx) compact;
int mtest (int i, int y) /* 内存模型指定为small */
{
  return (i * y + y * i + func(-1, 4.75));
}
int large_func (int i, int k) large /* 内存模型指定为large */
{
  return (mtest (i, k) + 2);
}

 

1.7 data/idata

8051单片机内部256字节RAM,低128字节既可直接寻址,又可间接寻址,高128字节RAM只能间接寻址(直接寻址方式下,高128字节被映射到SFR特殊功能寄存器)。内部RAM中包含16字节可位寻址区域,该区域起始地址为20H。

内部RAM存储区被分为三种不同的存储区类型:data,idata,bdata。

  • data指低128字节,以直接寻址方式访问存在该区域的变量。
  • idata指全部256字节,该存储类型编译后的代码以间接寻址方式访问内存。
  • bdata指16字节可位寻址区,该区域地址范围为20H-2FH。使用bdata能够定义可位寻址的变量。

汇编语言内部RAM直接寻址方式,示例:

MOV A,10H /* 将内部RAM 10H地址处的数据送到累加器A */

汇编语言内部RAM间接寻址方式,示例:

MOV R0,#90H
MOV A,@R0 /* 将内部RAM 90H地址处的数据送到累加器A */

 

1.8 pdata/xdata

Keil C51提供两种外部数据存储方式,pdata和xdata。外部ram通过movx指令存取。

  • 1)xdata存储类型,指示变量可存储在外部ram 64KB地址范围内任意位置。large内存模型下,变量即为此种默认存储类型。
  • 2)pdata存储类型,指示变量可存储在外部ram 256字节页范围内任意位置。compact内存模型下,变量即为此种存储类型。

 

1.9 _task_、_priority_:RTX51 Full 和 RTX51 Tiny 实时多任务操作系统相关

1.10 sfr:用来定义特殊功能寄存器

语法:

sfr name = address;
name 为寄存器名字
address 为寄存器的地址,必须为数字常量,不能包含+ -等操作符,数值也不是随意的,传统类型8051单片机支持的地址范围为0x80 - 0xFF

示例:

sfr P0 = 0x80; /* P0口,地址为0x80 */
sfr P1 = 0x90; /* P1口,地址为0x90 */
sfr P2 = 0xA0; /* P2口,地址为0xa0 */
sfr P3 = 0xB0; /* P3口,地址为0xb0 */

 

1.11 sfr16:定义一个16位的特殊功能寄存器

一些8051系列产品拥有16位的特殊功能寄存器,比如8052使用地址0XCC、0XCD表示定时/计数器2的低字节、高字节。C51编译器提供sfr16数据类型以便将两个8位特殊功能寄存器当做一个16位寄存器来访问。sfr16只能在小端模式下使用,低字节的地址作为16位特殊功能寄存器的地址。

sfr16 T2 = 0xCC; /* Timer 2: T2L 0CCh, T2H 0CDh */
sfr16 RCAP2 = 0xCA; /* RCAP2L 0CAh, RCAP2H 0CBh */

 

1.12 sbit:用来定义一个特殊功能寄存器的某一位

sbit提供了访问可位寻址特殊功能寄存器以及其他可位寻址对象的方法。只有地址能够被8整除的特殊功能寄存器才是可位寻址的。
三种定义方法:

sbit name = sfr-name ^ bit-position;
sbit name = sfr-address ^ bit-position;
sbit name = sbit-address;

 

1.13 interrupt:函数声明时使用interrupt关键词,该函数将被编译器视为中断服务程序

interrupt 关键词后跟一个整数,表示中断号,取值范围0-31。中断号必须为常数,不允许使用操作符表达式。

作为中断服务程序的函数必须遵循的规则:

  • 中断服务函数不能有参数;
  • 中断服务函数返回值类型必须为void类型;
  • 不能直接调用或者通过函数指针调用中断服务函数,中断服务函数只能由硬件自动调用,我们在程序中调用中断服务函数,将引起RETI指令的执行,该指令被人为执行,而其对应的硬件中断请求并不存在,这将导致单片机硬件中断系统产生不确定的致命错误;
  • Keil Cx51编译器支持中断号范围0-31,具体可用的中断号须要结合用户所用的具体芯片手册;

interrupt关键词影响了该函数的目标代码:

  • 该中断服务程序被调用时,寄存器ACC、B、DPH、DPL、PSW的内容被压入堆栈;
  • 该函数声明时,如果没有使用using关键词指定寄存器组,则在其被调用时,它所使用的所有工作寄存器的内容被压入堆栈;
  • 被压入堆栈的工作寄存器、特殊寄存器在该退出中断服务程序前被恢复;
  • 该中断服务程序以RETI指令结束;

 

1.14 reentrant:声明的函数为可重入函数

可重入的函数能够被多个进程同时调用。可重入函数在执行时,另外的进程可以中断当前执行的函数,并且调用同一个函数。正常情况下,C51程序中的函数不能被递归地调用,这是由于函数的参数和局部变量都被保存在固定的地址,在递归调用时操作了相同存储位置,导致数据被覆盖。

 

1.15 using

在8051系列单片机中,内部ram的前32个字节被分为4组,每组8个寄存器。每组的8个寄存器名字都为R0-R7。通过设置PSW寄存器的两个位,可以选择使用4组寄存器中的哪一组。寄存器组在处理中断或者使用实时操作系统时非常有用,可以在进入中断或者切换任务时使用不同寄存器组,而不用把8个寄存器的内容保存到堆栈。在退出中断或返回原任务时,只需切换回原来的寄存器组即可。

using通常在中断服务函数定义时使用,我们可以为不同的中断服务函数指定不同的寄存器组,这样可以减少堆栈操作,提高程序运行效率。

 

1.16 volatile

volatile int i=10;
int a = i;
... //其他代码,并未明确告诉编译器,对 i 进行过操作
int b = i;

volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i 的地址中读取,因而编译器生成的汇编代码会重新从 i 的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i 读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据(上次读的数据存放在缓存中)放在 b 中。而不是重新从 i 里面读。这样以来,如果 i 是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。

 

1.17 typedef

参考链接:https://zhuanlan.zhihu.com/p/380264864

有如下声明

 

typedef int PARA;

这种形式跟#define int PARA几乎一样,不少人用#define的思维来看待typedef,把int与PARA分开来看,int是一部分,PARA是另一部分。但实际上就像int i;声明一样,typedef int PARA;是一个整体声明,只不过int i定义了一个变量,而typedef定义了一个别名。持有这种错误的观念,就会无法理解如下一些声明:

 

typedef int a[10];
typedef void (*p)(void);

会以为a[10]是int的别名,(*p)(void)是void的别名,但这样的别名看起来又似乎不是合法的名字,于是陷入困惑之中。实际上,上面的语句把a声明为具有10个int元素的数组的类型别名,p是一种函数指针的类型别名。

虽然在功能上,typedef可以看作一个跟int PARA分离的动作,但语法上typedef属于存储类声明说明符,因此严格来说,typedef int PARA整个是一个完整的声明。

下面定义一个函数指针类型。比如原函数是void func(void);,那么定义的函数指针类型就是typedef void (*Fun)(void);(可以把Fun看做函数指针类型的名字,类似int、long等等),然后用此类型生成一个指向函数的指针:Fun func1;,当func1获取函数地址之后func1 = func;,就可以向调用原函数那样来使用这个函数指针:(*func1)(void);。也可以对地址进行强制类型转换,实现地址跳转执行,如((void (*)(void))0x2000)();。

 

posted @ 2021-07-12 21:59  斥包乐氶的  阅读(1216)  评论(0编辑  收藏  举报