C51简介及Keil的使用

前言

         此文档主要是针对有一定C/C++编程基础,并打算用Keil从事C51开发的开发人员。C51涉及的知识比较多,但是入门基本的开发,还是容易的。

 

C51简介

1.  C51概念

 

C51继承于C语言,主要运行于51内核的单片机平台。单片机,单片微型计算机器(SingleChipMicrocomputer)的简称,又称微控制单元(MicroControllerUnit,MCU)MCUCPURAMROMI/O、中断系统、晶振等组成。51内核的单片机都是8位的,因为cpu访问RAMROM以及I/O都是8Bit操作的。基于51内核的单片机有很多种,如805180515等。

存储器:包括片内存储器、片外存储器。片内存储器一般包括256字节的片内RAM,和8K字节的程序存储器ROM。片外储存器XRAM可达64K字节。

256字节的片内RAM

名称

地址范围

备注

工作寄存器组0

0x00—0x07

此组用作默认的寄存器

工作寄存器组1

0x08—0x0F

此组常用作中断函数寄存器

工作寄存器组2

0x10—0x17

工作寄存器若未使用,可用作堆栈计算

工作寄存器组3

0x18—0x1F

位寻址区

0x20—0x2F

此区域可位寻址(bit)

堆栈区

0x30—0x7F

此区域可用于堆栈计算

特殊功能寄存器 

0x80—0xFF

地址为8倍数SFR可位寻址

 

引脚:51单片机的管理一般主要包括电源、时钟、控制和I/O脚。

  • 电源:VCC,接电源;VSS接地。
  • 时钟:接晶振。
  • 控制线:ALEEA等控制脚。
  •  I/O线:48位并行I/O端口,分别为P0P1P2P3口,每个口8个引脚共32引脚。

2. C51语法

1. 数据类型

基本数据类型

名称

长度

取值满园

unsigned char

无符号类型

1byte

0~255

signed char

有符号类型

1byte

-128~127

unsigned int

无符号类型

2byte

0~65536

signed int

有符号类型

2byte

-32768~32767

unsigned long

无符号类型

4byte

0~4294967295

signed long

有符号类型

4byte

-2147483648~2147483647

float

浮点类型

4byte

+-1.75494E-38~+-3.402834E+38

bit

位类型

1bit

0/1

Sfr

8位特殊功能寄存器

1byte

0~255

sfr16

16位特殊功能寄存器

2byte

0~65536

sbit

可寻址位类型

1bit

0/1

  •  定义:临时变量只能定义在代码块的最开始,即”{“后面,且变量定义前不能有非定义语句。因为Keil能够在编译时就比较好确定栈内存,如果超过限定内存会报编译错误。
  • 申明:如果外部文件需要使用全局变量,使用extern来限定。
  • 初始化:全局变量和局部变量,如果没有指定初始化值,默认为0
  • bit:位类型,bit指定的变量可以直接按拉寻址,所以需要特殊的内存匹配,即位寻址区(0x20—0x2F),可申明最多128个bit类型的变量。
  • sfr:特殊功能寄存器(special function register),只能定义在函数体外,且必须指定特殊功能寄存器地址。
  • sfr16:16位的特殊功能寄存器,可以完成一些需要16位的特殊功能。
  • sbit:特殊功能寄存器位变量,是针对sfr的。但是因为位寻址效率的问题,所以只有地址为8倍数的sfr才可以被位寻址。sbit也只能定义在函数体外。用法如下:
sfr IE = 0xA8;

sbit EXO = IE^0; // 0xA8的第1位,相当于IE口的第0管脚

sbit EX7 = IE^7; // 0xA8的第8位,相当于IE口的第7管脚

sbit EX01 = 0xA8;// 0xA8的第1位,相当于IE口的第0管脚

sbit EX71 = 0xAF;// 0xA8的第8位,相当于IE口的第7管脚

sbit EXO2 =0xA8^0; // 0xA8的第1位,相当于IE口的第0管脚

sbit EX72 =0xA8^7; // 0xA8的第8位,相当于IE口的第7管脚

 

2. 储存器类型和指针

存储器类型

描述

data

直接寻址的片内RAM低128Byte,访问速度快

badta

片内RAM的可位寻址区(20H~2FH),允许字节和位混合访问

idata

间接寻址访问的片内RAM,允许访问全部片内RAM

pdada

用Rn间接访问的片外低256Byte

xdata

用DPTR间接访问片外RAM,允许访问全部64KB的片外RAM

code

程序存储器64KB ROM

char* pStr;  // 指针占3个字节,第1字节标识存储器类型,第2字节为指针存储地址的高字节,第3字节为指针存储地址的低字节。(未注明存储器类型即为默认存储器类型,由Keil的编译环境控制,且默认的存储器类型是修饰指针的)

char* idatapStr1; // 指针占3个字节,此处指定指针值的存储器类型为idata。

char* xdatapStr2; // 指针占用3个字节

char* codepStr3; // 指针占用3个字节,code的作用类似于const

char idata *pStr4; // 指针占用1个字节,idata是修饰pStr4指向的内容。idata表示的片内RAM最多只256字节,所以pStr4也只需要1个字节即可表示。

char xdata *pStr5; // 指针占用2个字节,xdata修饰的是pStr5指向的内容,而xdata表示的片外内存最多64K,所以2个字节足够。

char code *pStr6; // 指针占用2个字节

char idata *xdata pStr7; // 指针占用1个字节,因为pStr7指向的内容是idata

char xdata *idata pStr8; // 指针占用2个字节,因为pStr8指向的内容是xdata

综上所述,指针的大小分两类,未指明指向内容的存储器类型,此类指针大小为3字节。

指明了指针指向内容的存储器类型的指针大小为内容存储器的大小。

注:因为指针的大小及类型不是固件的,所以两个类型不同的指针不能赋值。

3.  volatile

volatile是用来限定变量告诉编译器,此变量可能会被特殊的情况修改,所以对此变量的操作不要做优化,直接从内存上存取。Keil编译器为了执行效率,都是将变量存放到寄存器上再来操作的,这就导致第二次取变量值时,可能是直接从寄存器中取值,而不是从内存上读取。那么有一些特殊操作可能发生在二次取值过程中,导致第二次取值没有更新到。

    char xdata g_value1;           

    char volatile xdatag_value2;

    int main()

    {    

       char cArg = g_value1; // 1

       900000   MOV     DPTR,#C_STARTUP(0x0000)

       E0       MOVX    A,@DPTR

       FE       MOV     R6,A

       if (g_value1 != cArg) // 2

       6E       XRL      A,R6 // g_value1此刻是直接从寄存器中取值

           cArg = 6;

 

       cArg =g_value2;

       900001   MOV     DPTR,#g_value2(0x0001)

       E0       MOVX    A,@DPTR

       FE       MOV     R6,A

       if (g_value2 != cArg)

       E0       MOVX    A,@DPTR // g_value2此刻是从内存中取值

       6E       XRL     A,R6

           cArg = 5;

       return 0;

    }

上面的代码很好的体现了volatile的特殊作用。如果在1和2之间有一些特殊情况修改了变量g_value1将导致错误。所以这样的一些特殊情况涉及到的变量,必须用volatile修饰,具体如下:

  •  汇编代码修改的变量(一般情况下汇编代码不要修改外部变量)
  • 硬件寄存器修改的变量(即指硬件寄存器值本身)
  • 中断函数修改的全局变量

(注:VS中的全局变量都是直接从内存中存取,不需要volatile修饰)

4. 中断函数

中断,顾名思义就是中断当前代码的执行流。中断函数即中断之后响应的函数。中断根据中断源类型不同,主要分为:外部中断,定时器中断、串口中断等。中断的实现原理是,CPU执行每条代码前,都会去检查中断标志位,如果中断标志位有信号,即响应中断函数。函数的执行是需要堆栈和寄存器的,那么中断函数的执行如何不破坏当前函数的堆栈和寄存器呢?C51有一个特殊的实现方式,即提供多套工作寄存器器,且堆栈和当前代码共用。这样当中断函数执行时,原有代码的运行环境就得以保存,中断函数结束后,就可以恢复当前代码执行流程。

中断函数的格式如下:void ExternINT0(void) interrupt 0using1

interrupt 0表示0号中断。using 1表示使用工作寄存器组1,如果不指定则使用默认工作寄存器组0,可能会与通用函数的工作寄存器冲突。另外中断函数中的使用的全局变量如果和主流存在同时写操作,那么在主流写时,需要关闭中断防止写冲突,等写完成之后再开启中断。

void ExternINT0(void) interrupt 0using1

{

    g_value1++;

}

int main()

{

    EA = 1;   // 开启所有中断

    EX0 = 1;   //开启外部中断0,对应0号中断

    IT0 = 1; // 外部中断0触发方式

    IE0 = 1; // 手动触发中断0,中断函数执行后,此标志变为0

    return 0;

}

5. 绝对地址

因为C51是直接与硬件交互,为了提高代码的执行效率,硬件的一些特殊功能可能会直接访问指定地址的变量和执行指定地址的函数。这些指定地址是固定不变的,是绝对地址。

  • 变量绝对地址定位

char idata myVar_at_0x40; // _at_关键字,指定idata区域的绝对地址0x40;编译链接之后在MAP文件中可以找到如下信息00000040H   IDATA   BYTE      myVar,即表明myVaridata区的0x40

char xdata myVal _at_ 0x400; // 指定xdata区域的0x400;

struct link list idata _at_ 0x40;  // list at idata 0x40 
char xdata text[256]   _at_ 0xE000; // array at xdata 0xE000 
 

在Options for target->LX51 Locate->User Segments编译框中添加:

?CO?text(x:0xE000)
  • 函数绝对地址定位

在Options for target->LX51 Locate->User Segments编译框中添加:

?PR?MyTest?MAIN(0x4000)
PR表示program   Executable program code   
MyTest表示函数名
MAIN表示所在文件名
0x4000表示函数绝对地址
  • 绝对地址的访问
  1. 可以通过汇编直接访问绝对地址变量及函数
  2. 可以通过将绝对地址赋值给指针访问变量及函数

6. 可重入(reentrant)

一个函数如果被主流程调用,另外又被中断函数调用,那么这个函数即重入了。这样的函数可能存在问题。如下列,主流程执行到2,然后触发中断函数了,并同样执行了下列函数,那么寄存器Exam可能已经被修改。即使此时退回到主流执行2,结果可能是错误的。这个时候Keil引入了reentrant,通用模拟堆栈用来避免此类问题。因为是模拟的,效率低,非必须不要使用。

unsigned int Test(int nVal)reentrant

{

    unsigned int nRet = 0;

    Exam = nRet;             // 1

       nRet =Square_Exam( );   // 2

    return nRet;

}

7. 看门狗

看门狗(Watch Dog),其作用是监控单片机程序的运行,如果程序跑飞或者进入死循环等意外状态,看门狗则将单片机进行复位,让程序重新开始运行。看门狗的实现方式,有硬件方式和软件方式。硬件方式是通过外部芯片来监控,并由外部芯片来控制复位。软件方式,即单片机本身通过定时器来对程序运行状态进行定时监控,如果在发现状态则将单片机复位。因为看门狗主要是通过定时器实现,所以看门狗定时器(Watch Dog Timer,WDT)一般会作为一个独立的组成部分。

看门狗(WDT)设计原理是,一个定时器模块,一个输入端(叫喂狗,kicking the dog or servicethe dog),还有一个控制单片机的RST端。每隔一段时间必须喂狗将WDT清0。如果指定时间没有检测到喂狗,看门狗定时器即会超时,然后重置RST端让单片机复位。

89C51默认是不带看门狗的。89S51是在89C51的基础上进行的扩展,添加了看门狗功能,此处用89S51为例说明看门狗具体用法。以下资料来自89C51的Datasheets。

WDT是为了解决CPU程序运行时可能进入混乱或死循环而设置,它由一个14Bit计数器和看门狗复位SFR(WDTRST)构成。外部复位时,WDT默认为关闭状态,要打开WDT,用户必须按顺序将0IEH和OEIH写到WDTRST寄存器(SFR地址为OA6H),当启动了WDT,它会随晶体振荡器在每个机器周期计数,除硬件复位或WDT溢出复位外没有其它方法关闭WDT,当WDT溢出,将使RST引脚输出高电平的复位脉冲。

89S51的看门狗计数器是固定的,在指定的时间内必须喂狗,否则会溢出复位。其他有些看门狗能够设置溢出时间,每次溢出时重新设置看门狗计数或定时。看门狗设计的重点在喂狗,如果喂狗设计不合理可能导致正常代码也触发WDT溢出复位。

#include

sfr WDTRST = 0xA6;

int main()

{

    WDTRST=0x1E;

    WDTRST=0xE1; // 初始化看门狗

    while (1)

    {

       WDTRST=0x1E;

       WDTRST=0xE1; //喂狗,如果不喂狗,则会Reset

    }

    return 0;

}

8.  其他

  • C51不支持引用。
  • static修饰函数和变量,让函数和变量只能为本文件所使用,避免命名冲突。
  • C51支持位域,位域的效率比bit低。位域立即数写相对消耗资源少,如果是变量赋值,会涉及到保存恢复累加器A,会消耗非常多的资源且效率低。
  • C51的变量会有默认初始值0,但是显式初始值会使代码更直观清晰。

Keil的使用

1. Kei的基本功能

Keil μVision2是一个集成开发环境(IDE,支持编辑、编译、链接、调试)。Keil主要包括以下几大模块:

l  Cx51:C文件编译器,负责将C文编译成可重定位的目标文件。

l  Ax51:汇编文件编译器,负责将汇编文件编译成可重定位的目标文件。

l  BL51:链接器/定位器,组成可重定位的目标文件,生成绝对目标文件。

l  LX51:扩展链接器/定位器,优化了BL51的功能,可以生成更小的目标文件。

l  LIB51:库管理器,从目标文件生成链接器可使用的库文件。

l  OH51:将目标文件转换成Inter Hex文件。

2. Keil使用C51的基本设置

1. Device

C51简介及Keil的使用_第1张图片

Device主要是用来设计代码运行单片机的内核的,这里的设置主要影响两个地方,一是影响默认包含的寄存器头文件,二是影响仿真调试。像1581因为是选择自定义的寄存器头文件,所以无论是选择80515还是89C51,其实都不影响的。此处的关键是要勾选UseExtended Liner(LX51)insteated of BL51,因为能够提升链接性,降低链接目标文件大小。

2. Target

C51简介及Keil的使用_第2张图片

  • 频率在此设置主要是针对通用性单片机的,并且用来仿真用的。具体的单片机需要使用中具体设置相关寄存器。
  • Memeoy Model:主要是设置默认变量的内存类型,如果选择Small,表明栈及默认(未显式指明内存类型)的变量都是idata类型。如果选择Compact,则表明是pdata类型。如果选择Large,则表明是xdata类型。一般默认选择Small。
  • Off-chip Code Momory是用来指定代码段的位置,如果不设置由系统自动分配。
  • Off-chip Xdata Memroy是用来指定xdata类型变量的代码位置,此处指定为0x6000,大小0x0C00。

3. Output

C51简介及Keil的使用_第3张图片

4. Listing

C51简介及Keil的使用_第4张图片

5. User

C51简介及Keil的使用_第5张图片

6. C51

C51简介及Keil的使用_第6张图片

7.   A51

C51简介及Keil的使用_第7张图片

8.  LX51 Locate

LX51 Locate,定位器,主要是变量及函数地址定位的设置。

C51简介及Keil的使用_第8张图片

9. LX51 Misc

LX51 Misc,链接器有关杂项的设置。如果少量函数定位地址,建议在Locate选项里直接设置。如果不通过文件设置链接器,则可以在Control Misc中设置REMOVEUNUSED,这样未被使用到的函数便不会被链接,但是会被编译检测语法。这样可以少用一些宏来限制链接函数链接,提升代码可读性。

C51简介及Keil的使用_第9张图片

10. Manage Project Item

C51简介及Keil的使用_第10张图片

11. Options For File

C51简介及Keil的使用_第11张图片

3. Keil调试C51

  • Keil自带的仿真器还是非常强大的,可以用来调试通用的C51代码,对于学习C51了解C51的运行机制,帮助非常大。
  • 调试时查看汇编代码,能够让我们考虑如何更好的优化代码,以及学习汇编代码。
  • Peripherals提供的仿真外围器,能够设置查看中断器、定时器、看门狗、I/O口等。
  • Registers、Watch窗口能够查看寄存器、变量的实时值。
  • Memroy区间,在Address框后面输入“字母:数据”即可以查看相应区域的内存值。其中字母C、D、I、X,分别代表代码存储空间、直接寻址的片内存储空间、间接寻址的片内存储空间、扩展的外部RAM单元值、键入C:0即可显示从0开始的ROM单元中的值,即查看程序的二进制代码。

4. Keil生成文件介绍

  • l  START.A51,启动文件,完成单片机的堆栈的初始化。如果想代码复位时,变量不进行默认初始化,则需要修改此文件。默认一般不修改。如果不使用此文件,编译器会用默认的启动文件。
  •  .uvproj,Keil4的项目启动文件。
  • .LST,记录对应文件在编译器的行号,以及最后统计占用的代码空间。
  • .OBJ,编译后生成的目标,供链接器使用。
  • ._i,记录文件编译的优化级别,当前项目宏,以及生成文件路径等。
  • .MAP,记录内存映射情况,例如一些指定绝对函数的函数变量均可以在此处查看。另外,有时如果要查看还有哪些绝对地址空余,也可以查看此文件。最后生成总结Program Size: data=15.0 xdata=2 const=0 code=206,总结各区间的大小,code是最终代码的大小,而不是单指代码段大小。
  • .hex,Keil默认生成的是hex文件,是一种Inter主导的在能够在单片机上运行的文件格式,其内容是16进制,两个字符表示1个BYTE数。所以hex文件会比二进制bin文件大近一倍。Hex文件描述性更强,有比Bin更强大的描述性。
  •  .bin,bin文件Keil默认是不生成的,一般是通过一些小工具如Hex2Bin.exe等按照一定的规则将hex文件转换成bin文件,这样能够节省大量空间。

5. Keil将Bin文件生成到指定SRAM位置

有时候我们需要将Bin文件放在指定的SRAM处运行来达到特殊的目的。假如我们需要将Bin生成到指定的SRAM运行地址为0xA000处。我们需要如何处理:

  • 修改启动文件中的代码CSEG    AT      0xA000
  • 修改target
  • C51简介及Keil的使用_第12张图片
  • 修改C51中的interrupt vector at address:
  • C51简介及Keil的使用_第13张图片

6. Keil生成使用Lib文件以及C51的模块化

  • l  Keil建立Lib工程非常简单,添加相应的文件,然后在Output窗口,勾选上“Create Library”即可。
  • l  Keil使用Lib文件,同样简单,即在目标工程中,添加要使用的Lib文件,然后在要使用的文件里添加相应的头文件即可。
  • l  模块化是所有编程语言最重要的概念之一,模块化是提高代码复用,降低代码耦合的最重要手段之一。因为C51是面向过程的开发语言,所以缺乏很多高格语言的模块化手段。但是我们依然可以通过现有的条件,尽量做到模块化。模块化从大到小,依次是工程->文件->函数。
  • 函数是最小的模块化模块,所以函数应当尽可能只做一件事,做到尽可能的独立。
  • 文件是中等模块,一个文件应该是一类功能函数的集合。甚至可以借鉴面向对象语言的一些优点,即一个文件也可以只在相应的h文件中暴露部分函数接口。并且一些函数可以用static限定为当前文件作用域,防止其他文件使用。因为没有其他高级语言那样有类名或作用域名对接口进行修饰,表明接口是某个模块下的。所以可以用缩写的文件名作为函数前缀用以标记函数属于某个模块。
  • 工程模块,当一个项目较大时,可以建立多个工程,其中一个执行文件工程和多个Lib库工程。建立多个工程,能够有效降低代码的耦合性。

附件

1.  SFR的讲解

1.  /* BYTE Register */ SFR(SpecialFunction Register),范围0x80~0xFF.

2.  sfr P0 = 0x80; // 对应P0.0~P0.7,通常用来控制8Flash数据,RAM地址为0x80

3.  sfr P1 = 0x90; // 对应P1.0~P1.7,暂未使用,RAM地址为0x90

4.  sfr P2 = 0xA0; // 对应P2.0~P2.7,CE0~CE4,以及串口相关

5.  sfr P3 = 0xB0; // 对应P3.0~P3.7,控制ALE,CLE,DQS

6.  sfr PSW = 0xD0; // Program Status Word,详见/* PSW */

7.  sfr ACC = 0xE0; // 累加器A,多用来作计算中间值

8.  sfr B = 0xF0; // 累加器B

9.   

10.sfr SP = 0x81; // Stack Pointer,存放栈顶指针

11.sfr DPL = 0x82; // DPTR低位

12.sfr DPH = 0x83; // DPTR高位

13. 

14.sfr PCON = 0x87; // Power Control Register,,默认不修改

15.sfr TCON = 0x88; // Timer Control Register,详见/* TCON */

16.sfr TMOD = 0x89; // Timer/Counter Mode Control Register,详见/* TCON */

17.sfr TL0 = 0x8A; // Timer Low 0

18.sfr TL1 = 0x8B; // Timer Low 1

19.sfr TH0 = 0x8C; // Timer High 0

20.sfr TH1 = 0x8D; // Timer High 1

21.sfr CKCON = 0x8E; // Clock Contrl,0为最快模式

22. 

23.sfr EIF = 0x91; // Extern Interrupt Flag,默认不修改

24.sfr WTST = 0x92; // Wait Status,控制等待状态周期,0为最快模式

25.sfr DPX0 = 0x93; // 中断优先0,默认不修改

26.sfr SCON = 0x98; // Serial Control Register,详见/* SCON */

27. 

28.sfr IE = 0xA8; // Interrupt Enable Regiester,详见/* IE */

29.sfr IP = 0xB8; // Interrupt Priority,0表示所有中断同一级别

30. 

31.sfr EIE = 0xE8; // Error Interrupt Enable,默认不修改

32. 

33.sfr MXAX = 0xEA; // 默认不修改

34. 

35.sfr EIP = 0xF8; // 指令指针寄存器

36. 

37.// 只有地址被8整除的SFR才可以位寻址

38./* BIT Register */

39./* PSW */

40.sbit CY = 0xD7; // Carry,进位标志位

41.sbit AC = 0xD6; // Assistant Carry,辅助进位标志位

42.sbit F0 = 0xD5; // 软件标志

43.sbit RS1 = 0xD4; // 工作寄存器组选择1

44.sbit RS0 = 0xD3; // 工作寄存器组选择0

45.sbit OV = 0xD2; // 溢出标志

46.sbit P = 0xD0; // 代码执行奇偶标志位P

47. 

48./* TCON */

49.sbit TF1 = 0x8F; // 定时器T1溢出标志位,默认不修改

50.sbit TR1 = 0x8E; // 功能同上

51.sbit TF0 = 0x8D; // 定时器T0溢出标志位,默认不修改

52.sbit TR0 = 0x8C; // 功能同上

53.sbit IE1 = 0x8B; // 外部中断1请求标志位,默认不修改

54.sbit IT1 = 0x8A; // 外部中断1触发方式控制位,默认不修改

55.sbit IE0 = 0x89; // 外部中断0请求标志位,默认0复位

56.sbit IT0 = 0x88; // 外部中断0触发方式控制位,默认0复位

57. 

58./* SCON */ // SG1581未提供此功能,可以不用关注

59.sbit SM0 = 0x9F;

60.sbit SM1 = 0x9E;

61.sbit SM20 = 0x9D;

62.sbit REN0 = 0x9C;

63.sbit TB80 = 0x9B;

64.sbit RB80 = 0x9A;

65.sbit TI0 = 0x99;

66.sbit RI0 = 0x98;

67. 

68./* IE */

69.sbit EAL = 0xAF; // Enable All Interrupt

70.sbit WDT = 0xAE; // 未使用

71.sbit ET2 = 0xAD; // Enable Timer 2 interrupt

72.sbit ES0 = 0xAC; // Enable UART1 interrupt,通用异步收发传输器(Universal Asynchronous Receiver/Transmitter)

73.sbit ET1 = 0xAB; // Enable Timer 1 interrupt

74.sbit EX1 = 0xAA; // Enable INT1 interrupt

75.sbit ET0 = 0xA9; // Enable Timer 0 interrupt

76.sbit EX0 = 0xA8; // Enable INT0 interrupt

77.// 1. 初始化MCU,防止中断打断初始化,所以关闭中断

78.// 2. 传输数据时,防止中断打断数据传输,所以关闭中断

79.// 3. 切换Code,防止中断打断初始化,所以关闭中断

 

你可能感兴趣的:(C-C++)