此文档主要是针对有一定C/C++编程基础,并打算用Keil从事C51开发的开发人员。C51涉及的知识比较多,但是入门基本的开发,还是容易的。
C51继承于C语言,主要运行于51内核的单片机平台。单片机,单片微型计算机器(SingleChipMicrocomputer)的简称,又称微控制单元(MicroControllerUnit,MCU)。MCU由CPU、RAM、ROM、I/O、中断系统、晶振等组成。51内核的单片机都是8位的,因为cpu访问RAM、ROM以及I/O都是8Bit操作的。基于51内核的单片机有很多种,如8051、80515等。
存储器:包括片内存储器、片外存储器。片内存储器一般包括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脚。
基本数据类型 |
名称 |
长度 |
取值满园 |
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 |
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管脚
存储器类型 |
描述 |
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字节。
指明了指针指向内容的存储器类型的指针大小为内容存储器的大小。
注:因为指针的大小及类型不是固件的,所以两个类型不同的指针不能赋值。
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修饰)
中断,顾名思义就是中断当前代码的执行流。中断函数即中断之后响应的函数。中断根据中断源类型不同,主要分为:外部中断,定时器中断、串口中断等。中断的实现原理是,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;
}
因为C51是直接与硬件交互,为了提高代码的执行效率,硬件的一些特殊功能可能会直接访问指定地址的变量和执行指定地址的函数。这些指定地址是固定不变的,是绝对地址。
char idata myVar_at_0x40; // _at_关键字,指定idata区域的绝对地址0x40;编译链接之后在MAP文件中可以找到如下信息00000040H IDATA BYTE myVar,即表明myVar在idata区的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表示函数绝对地址
一个函数如果被主流程调用,另外又被中断函数调用,那么这个函数即重入了。这样的函数可能存在问题。如下列,主流程执行到2,然后触发中断函数了,并同样执行了下列函数,那么寄存器Exam可能已经被修改。即使此时退回到主流执行2,结果可能是错误的。这个时候Keil引入了reentrant,通用模拟堆栈用来避免此类问题。因为是模拟的,效率低,非必须不要使用。
unsigned int Test(int nVal)reentrant
{
unsigned int nRet = 0;
Exam = nRet; // 1
nRet =Square_Exam( ); // 2
return nRet;
}
看门狗(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;
}
Keil μVision2是一个集成开发环境(IDE,支持编辑、编译、链接、调试)。Keil主要包括以下几大模块:
l Cx51:C文件编译器,负责将C文编译成可重定位的目标文件。
l Ax51:汇编文件编译器,负责将汇编文件编译成可重定位的目标文件。
l BL51:链接器/定位器,组成可重定位的目标文件,生成绝对目标文件。
l LX51:扩展链接器/定位器,优化了BL51的功能,可以生成更小的目标文件。
l LIB51:库管理器,从目标文件生成链接器可使用的库文件。
l OH51:将目标文件转换成Inter Hex文件。
Device主要是用来设计代码运行单片机的内核的,这里的设置主要影响两个地方,一是影响默认包含的寄存器头文件,二是影响仿真调试。像1581因为是选择自定义的寄存器头文件,所以无论是选择80515还是89C51,其实都不影响的。此处的关键是要勾选UseExtended Liner(LX51)insteated of BL51,因为能够提升链接性,降低链接目标文件大小。
LX51 Locate,定位器,主要是变量及函数地址定位的设置。
LX51 Misc,链接器有关杂项的设置。如果少量函数定位地址,建议在Locate选项里直接设置。如果不通过文件设置链接器,则可以在Control Misc中设置REMOVEUNUSED,这样未被使用到的函数便不会被链接,但是会被编译检测语法。这样可以少用一些宏来限制链接函数链接,提升代码可读性。
有时候我们需要将Bin文件放在指定的SRAM处运行来达到特殊的目的。假如我们需要将Bin生成到指定的SRAM运行地址为0xA000处。我们需要如何处理:
1. /* BYTE Register */ SFR(SpecialFunction Register),范围0x80~0xFF.
2. sfr P0 = 0x80; // 对应P0.0~P0.7,通常用来控制8位Flash数据,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时,防止中断打断初始化,所以关闭中断