这里是 单片微型计算机原理及接口技术 的第四章知识点
单片机C语言程序设计
51汇编语言能直接操作单片机的系统硬件,指令执行速度快,代码效率高。但其程序可读性差,且编写、移植困难。
KEIL C51是为51系列单片机设计的一种C语言,其特点:
结构化语言,代码紧凑——效率可与汇编语言媲美
接近真实语言,程序可读性强——易于调试、维护
库函数丰富,编程工作量小——产品开发周期短
机器级控制能力,功能很强——适合于嵌入式系统开发
与汇编指令无关,易于掌握——在单片机基础上上手快
Keil C51编译器功能强大,界面友好,项目管理方便,深受广大单片机工程师喜爱
C51语言已成为51系列单片机程序开发的主流软件方法。
C51的程序结构
C51与标准C语言对比
相同之处:语法规则、程序结构、编程方法
举例:LED闪烁控制功能,采用Proteus仿真完成。
LOOP: CLR P1.0
ACALL DEL50
SETB P1.0
SJMP LOOP
DEL50: MOV R7,#200
DEL1: MOV R6,#125
DJNZ R6,$
DJNZ R7,DEL1
RET
END
简单C51程序:P1.0口接发光二极管,采用软件延时实现亮灭闪烁显示
C51与标准C语言对比: C51是C语言的扩展和补充,能够实现对单片机硬件资源的访问与控制。
不同之处:
数据结构、中断处理、绝对地址访问
学习重点:
1. 掌握C51的对C语言的扩充部分
2. 学习C51模块化软件开发方法
C51的对C语言的扩充
1. C51的变量的扩充
在C语言编程中,数值可以发生改变的量称为变量。
例如
变量名与存储单元地址相对应,变量值与存储单元的内容相对应。
在51单片机多存储空间中如何确定变量与地址的关系?
C51变量定义的四要素:
【存储种类】 | 数据类型 | 【存储类型】 | 变量名 |
---|---|---|---|
(标准C) | (标准C+C51) | (C51特有) | (标准C) |
- 括号项——可以缺省(但需有缺省值)
【存储种类】
存储种类用于说明变量的作用范围:
1、auto(自动型)——变量的作用范围在定义它的函数体或语句块内。执行结束后,变量所占内存即被释放。
2、extern(外部型) ——在一个源文件中被定义为外部型的变量,在其它源文件中需要通过extern说明方可使用。
3、static(静态型) ——利用static可使变量定义所在的函数或语句块执行结束后,其分配的内存单元继续保留。
4、register(寄存器型) ——目前已不推荐使用。
- 缺省存储种类为auto (自动)型变量
数据类型
数据类型用于表示数据存放格式
标准C语言的数据类型
- 有符号数类型可以忽略signed标识符
除上述常规格式外,51单片机还有三种新的存储格式:
C51扩充的3种数据类型:bit、sfr或sfr16、sbit
bit型存储格式
关键词bit用于定义一个位变量,语法规则:
bit bit_name [= 0或1];
例如:
bit door = 0 ;
//定义一个叫door的位变量且初值为0
与标准C的数据类型声明的语法规则是一致的,如:
int int_name [ = 常数];
特别注意: bit型变量不能定义指针或者数组!
sfr或sfr16型存储格式
关键词sfr或sfr16用于定义SFR字节地址变量,语法规则:
sfr sfr_name = 字节地址常数;
sfr16 sfr_name = 字节地址常数;
例如:
sfr P0 = 0x80; //定义P0口地址80H
sfr PCON = 0x87; //定义PCON地址87H
sfr16 DPTR = 0x82; //定义DPTR的低地址82H
sbit型存储格式
关键词sbit用于定义SFR位地址变量
位地址表达形式:绝对位地址、相对位地址
D0^7 | D0^6 | D0^5 | D0^4 | D0^3 | D0^2 | D0^1 | D0^0 | 相对位地址 | |
---|---|---|---|---|---|---|---|---|---|
PSW | CY | AC | F0 | RS1 | RS0 | 0V | F1 | P | D0H字节地址 |
D7H | D6H | D5H | D4H | D3H | D2H | D1H | D0H | 绝对位地址 |
sbit型可用三种定义形式
1)将SFR的绝对位地址定义为位变量名
sbit bit_name = 位地址常数;
例如
sbit CY = 0xD7;
2)将SFR的相对位地址定义为位变量名
sbit bit_name = sfr字节地址 ^ 位位置;
例如
sbit CY = 0xD0^7;
3)将SFR的相对位位置定义位变量名
sbit bit_name = sfr_name ^ 位位置;
例如
sbit CY = PSW^7;
小结
C51编译器在头文件“REG51.H”中定义了全部sfr/sfr16和sbit变量。用一条预处理命令#include
STC15系列单片机内部功能模块增加,相应的特殊功能寄存器也进行了扩充,具体定义可以参考STC公司提供的stc15.h。
编程举例
【存储类型】
存储类型体现了变量的存放区域。51系列单片机共有6个存储类型(分布在3个逻辑存储空间中)。
不同存储类型的特点
存储类型 | 存储空间位置 | 说明 | 编译模式 |
---|---|---|---|
data | 片内低 128 B 存储区 | 访问速度快,可作为常用变量或者临时性变量存储器 | SMALL系统 |
bdata | 片内可位寻址存储区 | 允许位与字节混合访问 | / |
idata | 片内高 128 B 存储区 | 只有52系列才有 | / |
pdata | 片外页 RAM | 常用于外部设备访问 | COMPACT系统 |
xdata | 片外 64KB RAM | 常用于存放不常用的变量或等待处理的数据 | LARGE系统 |
code | 程序 ROM | 常用于存放数据表格等固定信息 | / |
- 三种编译模式分别对应于三种缺省存储类型:
- 约定:若无特殊声明,一般均为“SMALL编译模式”
变量名
变量名可以由字母、数字和下划线三种字符组成,且第一个字符必须为字母或下划线,变量名长度随编译系统而定。
变量名具有字母大小写的敏感性,如SUM和sum代表不同的变量。
- 强调:头文件中定义的变量都是大写的,若程序采取小写变量则需要重新定义。
变量名不得使用标准C语言和C51语言的关键字。
关键字 | 用途 | 说明 |
---|---|---|
_at_ | 地址定位 | 为变量进行存储器绝对空间地址定位 |
alien | 函数特性声明 | 声明与 PL/M-51 编译器的接口 |
bdata | 存储器类型说明 | 可位寻址的内部数据存储器 |
bit | 位变量声明 | 声明一个位变量或位函数 |
code | 存储器类型说明 | 程序存储器 |
compact | 存储模式声明 | 声明一个紧凑编译存储模式 |
data | 存储器类型说明 | 直接寻址的内部数据存储器 |
far | 远变量声明 | Keil 用 3BYTE 指针来引用它 |
idata | 存储器类型说明 | 间接寻址的内部数据存储器 |
interrupt | 中断函数声明 | 定义一个中断服务函数 |
large | 存储模式声明 | 声明一个大编译存储模式 |
数据结构定义举例
例1
__ unsigned char data system_status = 0;
__ | unsigned char | data | system_status | = 0; |
---|---|---|---|---|
自动型 | 无符号字符型 | 位与片内RAM区 | 变量名为system_status | 初值为零 |
//定义system_status为无符号字符型自动变量,该变量位于data区中且初值为0。
例2
unsigned char bdata status_byte;
//定义status_byte为无符号字符型自动变量,该变量位于bdata区
例3
unsigned int code unit_id[2]={0x1234, 0x89ab};
//定义unit_id[2]为无符号整型自动变量,该变量位于code区中,是长度为2的数组,且初值为0x1234和0x89ab。
例4
static char m, n;
//定义m和n为2个位于data区中的有符号字符型静态变量。
2. C51的指针的扩充
C语言指针的一般定义形式为:
数据类型 *指针变量名 [= &被指向变量名];
其中,指针变量指向一个由“数据类型”说明的变量。被指向变量和指针变量都位于C编译器默认的内存区中。例如:
int a =’A’;
int *p1= &a;
这表示p1是一个指向int型变量的指针变量,此时p1的值为int型变量a的地址,而a和p1两个变量都位于C编译器默认的内存区中。
对于C51,除了数据类型外,指针定义中还应能说明:
- 1)指针变量自身位于哪个存储区中?
- 2)被指向变量位于哪个存储区中?
C51指针的一般定义形式:
数据类型 [存储类型1] * [存储类型2] 变量名 [=&被指向变量名];
数据类型——被指向变量的类型,如int型或char型
存储类型1——被指向变量所在的存储区,缺省时由地址赋值关系决定
存储类型2——指针变量所在的存储区,缺省时为编译器默认的存储区
例1 若采用SMALL编译模式,试解释下述定义的含义。
char xdata a = ‘A’;
char *ptr = &a;
解:ptr是一个指向char型变量的指针,它本身位于SMALL编译模式默认的data存储区里,此时它指向位于xdata存储区里的char型变量a的地址。
例2 试解释下述定义的含义
char xdata a = ‘A’;
char *ptr = &a;
char idata b = ‘B’;
ptr = &b;
解:以char *ptr形式定义的指针变量,既可指向位于xdata存储区的char型变量a的地址,也可指向位于idata存储区的char型变量b的地址(由赋值操作关系决定)。
例3:试解释以下指针定义的含义
char xdata a = ‘A’;
char xdata *ptr = &a;
解:ptr是位于data存储区且固定指向xdata存储区的char型变量的指针变量,此时ptr的值为变量a的地址(不能像例2那样再将idata存储区的char型变量b的地址赋予ptr)。
例4:试解释以下指针定义的含义
char xdata a = ‘A’;
char xdata *idata ptr = &a;
解:ptr是固定指向xdata存储区的char型变量的指针变量,它自身存放在idata存储区中,此时ptr指向位于xdata存储区中的char型变量a的地址。
3.C51的中断处理函数
中断服务函数在C51中的定义格式是统一的:
void 函数名 (void)interrupt n using m
这里的interrupt 和using 都是C51增加的关键词:
- interrupt 表示该函数是一个中断函数,整数n是与中断源对应的中断号。
- using 表示指定第m组工作寄存器存放中断相关数据,m=0~3。若每个中断都指定不同的工作寄存器组,则中断函数调用时就可以不必对相关参数进行现场保护,简化编程。m不指定时默认使用当前工作寄存器组(由PSW中的RS0和RS1位确定)
在C51中使用中断函数时应该注意一下几点:
- 中断函数既没有返回值也没有调用参数。
- 中断函数只能由硬件调用,不能被其他函数调用
- 为提高中断响应的实时性,中断函数应尽量简短。
4.C51中的绝对地址的访问
在C51程序中,经常需要明确指定变量的绝对地址。前面所讲的sfr、sfr16和sbit定义就属于指定绝对地址的变量定义。
实现绝对地址定义方法:
使用宏定义访问绝对地址
定义格式如下:
#include <absacc.h>
#define 变量名 DBYTE [绝对地址] //在内部RAM中定义绝对地址字节变量
#define 变量名 DWORD [绝对地址] //在内部RAM中定义绝对地址字变量
#define 变量名 CBYTE [绝对地址] //在ROM中定义绝对地址字节变量
#define 变量名 CWORD [绝对地址] //在ROM中定义绝对地址字变量
#define 变量名 XBYTE [绝对地址] //在外部RAM中定义绝对地址字节变量
#define 变量名 XWORD [绝对地址] //在外部RAM中定义绝对地址字变量
#define 变量名 PBYTE [绝对地址] //在外部RAM中某一页中定义绝对地址字节变量
#define 变量名 PWORD [绝对地址] //在外部RAM中某一页中定义绝对地址字变量
例4-1 用define定义绝对地址变量
#include <absacc.h>
#define PA8255 XBYTE[0X0000]
#define PB8255 XBYTE[0X0001]
#define PC8255 XBYTE[0X0002]
#define COM8255 XBYTE [0x0003]
COM8255=0x83; //把控制字写入8255控制寄存器
PA8255=0x0f; //把数据写入8255的PA口
unsigned char a1,a2;
a1=PB8255; //把8255PB口的数据写入变量a1
a2=XBYTE[0x0002] //把外部RAM地址为0002H单元的数据送给变量a2
使用关键字_at_指定绝对地址
格式
[存储器类型] 数据类型 变量名_at_ 地址常数;
例:使用关键字_at_对绝对地址访问
#include <reg52.h>
data unsigned char x1_at_0x30; //指定变量x1在内部RAM的30H单元
xdata unsigned char x2_at_0x3000;
main()
{
x1=0x0f;
x2=0xf0;
}
注意:使用_at_定义的变量必须为全局变量!
使用指针访问
例: 使用指针实现对绝对地址访问
#include <reg52.h>
xdata unsigned char *px ;定义指向符号变量的指针px,位于外部ram区
void main (void )
{
px = 0x0003; // 指向0003单元
*px = 0xFF; // 外部RAM0003单元置为FFH
px = 0x0004;// 指向0004单元
*px =0xFE; // 外部RAM0004单元置为FFH
}
C51与汇编语言混合编程
C51语言编程可胜任单片机的基本测量与控制任务。
对于某些特殊的I/O 接口处理、中断处理、强调程序执行速度等场合,仍希望采用汇编程序。
C51 编译器提供了与汇编语言程序的接口规则,可方便地实现C51 与汇编语言程序的相互调用。
本节仅讨论在C51中调用汇编函数和在C51中嵌入汇编代码两种方法。
在C51中调用汇编程序
C51程序中调用汇编语言,需要解决三个问题:
1)程序的寻址,main.c中调用的max函数,如何与汇编文件中的相应代码对应起来;
2)参数传递,main.c中传递给max()函数的参数a和b,存放在何处可使汇编程序能够获取到它们的值;
3)返回值传递,汇编语言计算得到的结果,存放在何处可使C语言程序能够获取到。
举例:在两个数据中选出较大的数据,并赋值给变量c。其中,要求选数任务采用汇编子程序完成。
//以下代码在main.c文件中实现
void max(char a,char b);//由汇编语言实现
main(){
char a=30,b=40,c;
c=max(a,b);
}
程序的寻址问题
通过在汇编文件中定义同名的“函数”来实现。
C程序的函数声明 | 汇编语言的符号名 | 解释 |
---|---|---|
void func(void) | FUNC | 无参数传递或不含寄存器参数的函数名不做改变传入目标文件中,名字只是简单地转换为大写形式 |
void func(char) | _FUNC | 带寄存器参数的函数名转为大写,并且加上 “ _ ” 前缀 |
void func(void) reentrant | _?FUNC | 重入函数须使用前缀“ ? _ ” |
void max(char a,char b);//由汇编语言实现
main(){
char a=30,b=40,c;
c=max(a,b);
}
参数传递问题
参数类型 | char | int | long/float | 一般指针 |
---|---|---|---|---|
第一个参数 | R7 | R6,R7 | R4,R5,R6,R7 | R1,R2,R3 |
第二个参数 | R5 | R4,R5 | R4,R5,R6,R7 | R1,R2,R3 |
第三个参数 | R3 | R2,R3 | / | R1,R2,R3 |
void max(char a,char b);//由汇编语言实现
main(){
char a=30,b=40,c;
c=max(a,b);
}
返回值传递问题
返回值 | 寄存器 | 说明 |
---|---|---|
bit | C | 进位标志 |
(unsigned)char | R7 | / |
(unsigned)int | R6,R7 | 高位在R6,低位在R7 |
(unsigned)long | R4,R5,R6,R7 | 高位在R4,低位在R7 |
float | R4,R5,R6,R7 | 32位IEEE格式,指数和符号位在R7 |
指针 | R1,R2,R3 | R3防寄存器类型,高位在R2位,低位在R1位 |
void max(char a,char b);//由汇编语言实现
main(){
char a=30,b=40,c;
c=max(a,b);
}
C51中嵌入汇编代码
程序中需要用到一些简短的汇编指令时,可以通过语句
“# pragma”嵌入汇编代码的办法实现。
实例:
#include<reg51.h>
void main(void){
unsigned char i=0; //定义变量i
#pragma asm //嵌入汇编代码
MOV R0,#0AH
LOOP:INC A //累加器循环加一
DJNZ R0,LOOP
#pragma endasm
i=++ACC; //输出累加结果
}
说明:
汇编代码必须放在两条预处理命令
#pragma asm
#pragma endasm
之间,预处理命令必须用小写字母,汇编代码则大小写不限。
本实例可实现用汇编语句进行累加器A循环加1和将累加结果传递给C51变量的功能。
C51模块化程序设计
C51继承了C语言模块化程序设计的优势,可以将复杂项目分解为多个独立模块,分别进行设计。本节以一个简单项目设计为例,简要介绍C51模块化设计的方法和步骤。
项目要求:如图所示,8个发光二极管接在P2口,共阳极连接。要求实现8个数码管多种花样流水灯显示。
C51单文件编程:所有的代码全部都放在同一个.c 源文件中,解决的问题越多、越复杂则程序越长。
缺点:复杂问题编程时,随着代码量的增加会使得程序的结构混乱、可读性和可移植性变差,调试排错和维护都比较困难,软件的成本随之增大。
解决办法:根据软件工程的模块化编程的原理,采用模块化多文件编程
C51完美支持模块化编程思想:将复杂问题分解成为多个独立功能模块。
KEIL C51 工程管理器:每一个功能模块创建一个xxxx.c文件(源文件)和xxxx.h文件(头文件)
xxxx.h(头文件):被外部模块调用的函数声明、宏定义和局部变量定义,尽量不用或者少用全局变量。
xxxx.c(源文件):函数体代码实现函数功能
将所有模块的xxxx.c文件添加到工程,其它函数如果需要使用其中的函数,只需要在其代码段添加#include xxxx.h就可以了。
头文件编写格式:
1.#ifndef __ 文件名_H__
2.#define __ 文件名_H__
// 此处开始添加代码
void pattern1(void); //仅需声明
.........
4.#endif
防重复包含处理由条件预编译语句组成。将.h 文件的文件名全部大写,把点“.”替换成一个下划线“_”,然后在首尾各添加2 个下划线“__”。
注意:变量定义也可以放在.C文件中。当本模块需要使用其他模块中定义的变量时也要声明,但前边要加关键字extern!
例程讲解:实现流水灯需要控制IO口和延时,故设置两个模块:
延时模块 : Delay.h+Delay.c
显示模块 : Led.h+Led.c
主控模块 : main.c
程序代码:main.c 8个LED花样闪烁
/* delay实现延时,控制流水灯速度,led实现灯花样显示*/
/* 8个LED接P2,低电平点亮,采用查表指令实现花样闪烁
#include <reg51.h> //系统头文件
#include "Delay.h" //包含延时模块头文件
#include "Led.h" //包含延时模块头文件
#define u8 unsigned char //定义数据类型简写形式
code unsigned char forPattern1[]={0xFE,0xFD,0xFB,0xF7,0xEF,0xDF,0xBF,0x7F};
//单灯流水
code unsigned char forPattern2[]={0xFC,0xF9,0xE7,0xCF,0x9F,0x3F};
//双灯流水花样
void main(void){
while(1){
Pattern1(); //单灯流水显示
DelayNms(20); //延时20ms
Pattern2(); //双灯流水显示
DelayNms(20); //延时20ms
}
}
程序代码:Delay.h
//软件延时程序代码
//软件延时模块函数声明
#ifndef _DELAY_H_
#define _DELAY_H_
void DelayNms(unsigned char N); //延时N毫秒 N=1~255
void Delay1s(void);
#endif
程序代码:Delay.c
//软件延时程序代码
#include "Delay.h"
void DelayNms(unsigned N){ //延时N毫秒,N=1~255 //@12MHz
unsigned char i, j;
do{
i = 4;
j = 100;
while(i--){
while(j--);
}
}while(--N);
}
void Delay1s(void){ //延时1s函数
unsigned char i=5;
while(i--){
DelayNms(10);
}
}
程序代码:Led.h ///Led花样显示程序声明
#ifndef _LED_H_
#define _LED_H_
void Pattern1(void); //第一种花样显示
void Pattern2(void); //第二种花样显示
#endif
Led.c中变量声明:
extern code unsigned char forPattern1[]; //单灯流水
extern code unsigned char forPattern2[]; //双灯流水
使用关键词extern是因为其定义在main.c中
程序代码:Led.c //LED花样显示代码
#include "Delay.h" //要用到延时函数,因此包含delay模块头文件
#include "Led.h"
#include <reg51.h> //系统头文件
extern code unsigned char forPattern1[]; //单灯流水
extern code unsigned char forPattern2[]; //双灯流水
void Pattern1(void){ //延时N毫秒 N=1~255
//@12MHz 示波器测试验证
unsigned char i;
for (i=0;i<8;i++){
P2=forPattern1[i];
Delay1s();
}
}
void Pattern2(void){ //延时Nx0.1秒 N=1~255 //@12MHz 示波器测试验证
unsigned char i;
for (i=0;i<6;i++){
P2=forPattern2[i];
Delay1s();
}
}
程序编译与运行仿真:
在Keil+Proteus可以实现项目的代码输入、仿真动态调试和全速运行
STC单片机模块化程序设计:为提高单片机开发效率,STC以库函数形式提供了STC单片机内部各个功能模块的开发代码。在公司网站上提供下载。
本章小结
- C51变量定义必须考虑单片机的多空间存储结构。其一般定义格式为:【存储种类】数据类型【存储类型】变量名。
- 在Keil下进行C51编程的基本步骤是:建立工程→输入源程序→添加源程序→【工程设置】→编译源程序→【动态调试 】→ 运行。
- Keil C51支持多文件模块化项目管理,提高软件设计效率。
- 单片机各个功能模块的C51编程将在本课程其它章节详细讲解。后续课程请大家充分利用STC公司提供的库函数编程。