c51语言写数据排序程序,51系列单片机学习5—C编程程序语句

原标题:51系列单片机学习5—C编程程序语句

曾经在BBS上有朋友问过我{}是什么意思?什么作用?在 C 中是有不少的括号,如{},[],()等,确实会让一些初入门的朋友不解。在 VB 等一些语言中同一个()号会有不一样的作用,它能用于组合若干条语句形成功能块,能用做数组的下标等,而在 C 中括号的分工较为明显,{}号是用于将若干条语句组合在一起形成一种功能块,这种由若干条语句组合而成的语句就叫复合语句。复合语句之间用{}分隔,而它内部的各条语句还是需要以分号“;” 结束。复合语句是允许嵌套的,也是就是在{}中的{}也是复合语句。复合语句在程序运行时,{}中的各行单语句是依次顺序执行的。单片机C语言中能将复合语句视为一条单语句,也就是说在语法上等同于一条单语句。对于一个函数而言,函数体就是一个复合语句,也许大家会因此知道复合语句中不单能用可执行语句组成,还能用变量定义语句组成。要注意的是在复合语句中所定义的变量,称为局部变量,所谓局部变量就是指它的有效范围只在复合语句中,而函数也算是复合语句,所以函数内定义的变量有效范围也只在函数内部。下面用一段简单的例子简单说明复合语句和局部变量的使用。 #include

#include

void main(void)

{

unsigned int a,b,c,d; //这个定义会在整个 main 函数中?

SCON = 0x50; //串行口方式 1,允许接收 TMOD = 0x20; //定时器 1 定时方式 2

TH1 = 0xE8; //11.0592MHz 1200 波特率 TL1 = 0xE8;

TI = 1; TR1 = 1; //启动定时器

a = 5; b = 6; c = 7; d = 8; //这会在整个函数有效

printf("0: %d,%d,%d,%dn",a,b,c,d);

{ //复合语句 1

unsigned int a,e; //只在复合语句 1 中有效

a = 10,e = 100;

printf("1: %d,%d,%d,%d,%dn",a,b,c,d,e);

{ //复合语句 2

unsigned int b,f; //只在复合语句 2 中有效

b = 11,f = 200;

printf("2: %d,%d,%d,%d,%d,%dn",a,b,c,d,e,f);

}//复合语句 2 结束

printf("1: %d,%d,%d,%d,%dn",a,b,c,d,e);

}//复合语句 1 结束

printf("0: %d,%d,%d,%dn",a,b,c,d);

while(1);

}

运行结果:

0:5,6,7,8

1: 10,6,7,8,100

2: 10,11,7,8,100,200

1: 10,6,7,8,100

0:5,6,7,8 结合以上的说明想想为何结果会是这样。

读完前面的文章大家都会大概对条件语句这个概念有所认识吧?是的,就如学习语文中 的条件语句一样,C 语言也一样是“如果 XX 就 XX”或是“如果 XX 就 XX 不然 XX”。也就是当条件符合时就执行语句。条件语句又被称为分支语句,也有人会称为判断语句,其关键字 是由 if 构成,这大众多的高级语言中都是基本相同的。C 语言供给了 3 种形式的条件语句:

1: if (条件表达式) 语句 当条件表达式的结果为真时,就执行语句,不然就跳过。 如 if (a==b) a++; 当 a 等于 b 时,a 就加 1

2: if (条件表达式) 语句 1

else 语句 2

当条件表达式成立时,就执行语句 1,不然就执行语句 2 如 if (a==b)

a++;

else

a--;

当 a 等于 b 时,a 加 1,不然 a-1。

3:if (条件表达式 1) 语句 1

else if (条件表达式 2) 语句 2

else if (条件表达式 3) 语句 3

else if (条件表达式 m) 语句 n else 语句 m

这是由 if else 语句组成的嵌套,用来实现多方向条件分支,使用应注意 if 和 else 的配对使用,要是少了一个就会语法出错,记住 else 总是与最临近的 if 相配对。一般条件语句只会用作单一条件或少数量的分支,如果多数量的分支时则更多的会用到下一篇中的开 关语句。如果使用条件语句来编写超过 3 个以上的分支程序的话,会使程序变得不是那么清晰易读。

学习了条件语句,用多个条件语句能实现多方向条件分支,但是能发现使用过多的条件语句实现多方向分支会使条件语句嵌套过多,程序冗长,这样读起来也很不好读。这个时候使用开关语句同样能达到处理多分支选择的目的,又能使程序结构清晰。它的语法为下:

switch (表达式)

{

case 常量表达式 1: 语句 1; break;

case 常量表达式 2: 语句 2; break;

case 常量表达式 3: 语句 3; break;

case 常量表达式 n: 语句 n; break;

default: 语句

}

运行中 switch 后面的表达式的值将会做为条件,与 case 后面的各个常量表达式的值相 对比,如果相等时则执行 case 后面的语句,再执行 break(间断语句)语句,跳出 switch 语句。如果 case 后没有和条件相等的值时就执行 default 后的语句。当要求没有符合的条 件时不做任何处理,则能不写 default 语句。

在上面的章节中我们一直在用 printf 这个标准的 C 输出函数做字符的输出,使用它当然会很方便,但它的功能强大,所占用的存储空间自然也很大,要 1K 左右字节空间,如果 再加上 scanf 输入函数就要达到 2K 左右的字节,这样的话如果要求用 2K 存储空间的芯片时 就无法再使用这两个函数,例如 AT89C2051。在这些小项目中,通常我们只是要求简单的字符输入输出,这里以笔者发表在本人网站的一个简单的串行口应用实例为例,一来学习使用开 关语句的使用,二来简单了解 51 芯片串行口基本编程。这个实例是用 PC 串行口通过上位机程序 与由 AT89c51 组成的下位机相通信,实现用 PC 软件控制 AT89c51 芯片的 IO 口,这样也就可 以再通过相关电路实现对设备的控制。为了方便实验,在此所使用的硬件还是用回以上课程中做好的硬件,以串行口和 PC 连接,用 LED 查看实验的结果。原代码请到在笔者的网站 下载,上面有 单片机c语言 下位机源码、PC 上位机源码、电路图等资料。

代码中有多处使用开关语句的,使用它对不一样的条件做不一样的处理,如在 CSToOut 函数 中根据 CN[1]来选择输出到那个 IO 口,CN[1]=0 则把 CN[2]的值送到 P0,CN[1]=1 则送到 P1, 这样的写法比起用 if (CN[1]==0)这样的判断语句来的清晰明了。当然它们的效果没有太大 的差别(在不考虑编译后的代码执行效率的情况下)。

在这段代码主要的作用就是通过串行口和上位机软件进行通信,跟据上位机的命令字串, 对指定的 IO 端口进行读写。InitCom 函数,原型为 void InitCom(unsigned char BaudRate),其作用为初始化串行口。它的输入参数为一个字节,程序就是用这个参数做为开关语句的选择 参数。如调用 InitCom(6),函数就会把波特率设置为 9600。当然这段代码只使用了一种波特率,能用更高效率的语句去编写,这里就不多讨论了。

看到这里,你也许会问函数中的 SCON,TCON,TMOD,SCOM 等是代表什么?它们是特殊 功能寄存器。

SBUF 数据缓冲寄存器这是一个能直接寻址的串行口专用寄存器。有朋友这样问起 过“为何在串行口收发中,都只是使用到同一个寄存器 SBUF?而不是收发各用一个寄存器。” 实际上 SBUF 包含了两个独立的寄存器,一个是发送寄存,另一个是接收寄存器,但它们都共同使用同一个寻址地址-99H。CPU 在读 SBUF 时会指到接收寄存器,在写时会指到发送寄存器,而且接收寄存器是双缓冲寄存器,这样能避免接收中断没有及时的被响应,数据没有被取走,下一帧数据已到来,而造成的数据重叠问题。发送器则不需要用到双缓冲,一般情况下我们在写发送程序时也不必用到发送中断去外理发送数据。操作 SBUF 寄存器的方法 则很简单,只要把这个 99H 地址用关键字 sfr 定义为一个变量就能对其进行读写操作了,

如 sfr SBUF = 0x99;当然你也能用其它的名称。通常在标准的 reg51.h 或 at89x51.h 等头文件中已对其做了定义,只要用#include 引用就能了。

SCON 串行口控制寄存器 通常在芯片或设备中为了监视或控制接口状态,都会引用 到接口控制寄存器。SCON 就是 51 芯片的串行口控制寄存器。它的寻址地址是 98H,是一个 能位寻址的寄存器,作用就是监视和控制 51 芯片串行口的工作状态。51 芯片的串行口能 工作在几个不一样的工作模式下,其工作模式的设置就是使用 SCON 寄存器。它的各个位的具 体定义如下:

(MSB) (LSB) SM0 SM1 SM2 REN TB8 RB8 TI RI

表 8-1 串行口控制寄存器 SCON

SM0、SM1 为串行口工作模式设置位,这样两位能对应进行四种模式的设置。看表 8

-2 串行口工作模式设置。

SM0SM1模 式功能波特率

000同步移位寄存器fosc/12

0118 位 UART可变

1029 位 UARTfosc/32 或 fosc/64

1139 位 UART可变

表 8-2 串行口工作模式设置

在这里只说明最常用的模式 1,其它的模式也就一一略过,有兴趣的朋友能找相关的 硬件资料查看。表中的 fosc 代表振荡器的频率,也就是晶体震荡器的频率。UART 为(Universal Asynchronous Receiver)的英文缩写。

SM2 在模式 2、模式 3 中为多处理机通信使能位。在模式 0 中要求该位为 0。

REM 为允许接收位,REM 置 1 时串行口允许接收,置 0 时禁止接收。REM 是由软件置位或 清零。如果在一个电路中接收和发送引脚 P3.0,P3.1 都和上位机相连,在软件上有串行口中断处理程序,当要求在处理某个子程序时不允许串行口被上位机来的控制字符产生中断,那么可 以在这个子程序的开始处加入 REM=0 来禁止接收,在子程序结束处加入 REM=1 再次打开串行口 接收。大家也能用上面的实际源码加入 REM=0 来进行实验。

TB8 发送数据位 8,在模式 2 和 3 是要发送的第 9 位。该位能用软件根据需要置位或清除,通常这位在通信协议中做奇偶位,在多处理机通信中这一位则用于表示是地址帧还是 数据帧。

RB8 接收数据位 8,在模式 2 和 3 是已接收数据的第 9 位。该位可能是奇偶位,地址/ 数据标识位。在模式 0 中,RB8 为保留位没有被使用。在模式 1 中,当 SM2=0,RB8 是已接 收数据的停止位。

TI 发送中断标识位。在模式 0,发送完第 8 位数据时,由硬件置位。其它模式中则是在 发送停止位之初,由硬件置位。TI 置位后,申请中断,CPU 响应中断后,发送下一帧数据。 在任何模式下,TI 都必须由软件来清除,也就是说在数据写入到 SBUF 后,硬件发送数据,

中断响应(如中断打开),这个时候 TI=1,表明发送已完成,TI 不会由硬件清除,所以这个时候必须用软件对其清零。

RI 接收中断标识位。在模式 0,接收第 8 位结束时,由硬件置位。其它模式中则是在接收停止位的半中间,由硬件置位。RI=1,申请中断,要求 CPU 取走数据。但在模式 1 中,SM2=1 时,当未收到有效的停止位,则不会对 RI 置位。同样 RI 也必须要靠软件清除。

常用的串行口模式 1 是传输 10 个位的,1 位起始位为 0,8 位数据位,低位在先,1 位停止 位为 1。它的波特率是可变的,其速率是取决于定时器 1 或定时器 2 的定时值(溢出速率)。 AT89c51 和 AT89C2051 等 51 系列芯片只有两个定时器,定时器 0 和定时器 1,而定时器 2是 89C52 系列芯片才有的。

波特率 在使用串行口做通信时,一个很重要的参数就是波特率,只有上下位机的波特率一样时才能进行正常通信。波特率是指串行端口每秒内能传输的波特位数。有一些开始学习 的朋友认为波特率是指每秒传输的字节数,如标准 9600 会被误认为每秒种能传送 9600 个字节,而实际上它是指每秒能传送 9600 个二进位,而一个字节要 8 个二进位,如用串口模式 1 来传输那么加上起始位和停止位,每个数据字节就要占用 10 个二进位,9600 波特 率用模式 1 传输时,每秒传输的字节数是 9600÷10=960 字节。51 芯片的串行口工作模式 0 的波特率是固定的,为 fosc/12,以一个 12M 的晶体震荡器来计算,那么它的波特率能达到 1M。 模式 2 的波特率是固定在 fosc/64 或 fosc/32,具体用那一种就取决于 PCON 寄存器中的 SMOD 位,如 SMOD 为 0,波特率为 focs/64,SMOD 为 1,波特率为 focs/32。模式 1 和模式 3 的波 特率是可变的,取决于定时器 1 或 2(52 芯片)的溢出速率。那么我们怎么去计算这两个模 式的波特率设置时相关的寄存器的值呢?能用以下的公式去计算。

波特率=(2SMOD÷32)×定时器 1 溢出速率

上式中如设置了 PCON 寄存器中的 SMOD 位为 1 时就能把波特率提升 2 倍。通常会使用 定时器 1 工作在定时器工作模式 2 下,这个时候定时值中的 TL1 做为计数,TH1 做为自动重装值 , 这个定时模式下,定时器溢出后,TH1 的值会自动装载到 TL1,再次开始计数,这样能不用软件去干预,使得定时更准确。在这个定时模式 2 下定时器 1 溢出速率的计算公式如下:

溢出速率=(计数速率)/(256-TH1)

上式中的“计数速率”与所使用的晶体振荡器频率有关,在 51 芯片中定时器启动后会在每一个机器周期使定时寄存器 TH 的值增加一,一个机器周期等于十二个振荡周期,所以能得知 51 芯片的计数速率为晶体振荡器频率的 1/12,一个 12M 的晶体震荡器用在 51 芯片上, 那么 51 的计数速率就为 1M。通常用 11.0592M 晶体是为了得到标准的无误差的波特率,那 么为何呢?计算一下就知道了。如我们要得到 9600 的波特率,晶体震荡器为 11.0592M 和 12M,定 时器 1 为模式 2,SMOD 设为 1,分别看看那所要求的 TH1 为何值。代入公式:

11.0592M

9600=(2÷32)×((11.0592M/12)/(256-TH1))

TH1=250 //看看是不是和上面实例中的使用的数值一样?

12M

9600=(2÷32)×((12M/12)/(256-TH1)) TH1≈249.49

上面的计算能看出使用 12M 晶体的时候计算出来的 TH1 不为整数,而 TH1 的值只能取整数,这样它就会有一定的误差存在不能产生精确的 9600 波特率。当然一定的误差是能 在使用中被接受的,就算使用 11.0592M 的晶体振荡器也会因晶体本身所存在的误差使波特率产生误差,但晶体本身的误差对波特率的影响是十分之小的,能忽略不计。

==================================================================================================

循环语句是几乎每个程序都会用到的,它的作用就是用来实现需要反复进行多次的操 作。如一个 12M 的 51 芯片应用电路中要求实现 1 毫秒的延时,那么就要执行 1000 次空语句才能达到延时的目的(当然能使用定时器来做,这里就不讨论),如果是写 1000 条空语句那是多么麻烦的事情,再者就是要占用很多的存储空间。我们能知道这 1000 条空语句, 无非就是一条空语句重复执行 1000 次,因此我们就能用循环语句去写,这样不但使程序结构清晰明了,而且使其编译的效率大大的提高。在 C 语言中构成循环控制的语句有while,do-while,for 和 goto 语句。同样都是起到循环作用,但具体的作用和使用方法又大不一 样。我们具体来看看。

goto 语句

这个语句在很多高级语言中都会有,记得小时候用 BASIC 时就很喜欢用这个语句。它是 一个无条件的转向语句,只要执行到这个语句,程序指针就会跳转到 goto 后的标号所在的程序段。它的语法如下:

goto 语句标号; 其中的语句标号为一个带冒号的标识符。示例如下

void main(void)

{

unsigned char a;

start: a++;

if (a==10) goto end;

goto start;

end:;

}

上面一段程序只是说明一下 goto 的使用方法,实际编写很少使用这样的手法。这段程序的意思是在程序开始处用标识符“start:”标识,表示程序这是程序的开始,“end:”标识程序的结束,标识符的定义应遵循前面所讲的标识符定义原则,不能用 C 的关键字也不能和其它变 量和函数名相同,不然就会出错了。程序执行 a++,a 的值加 1,当 a 等于 10 时程序会跳到 end 标识处结束程序,不然跳回到 start 标识处继续 a++,直到 a 等于 10。上面的示例说明 goto 不但能无条件的转向,而且能和 if 语句构成一个循环结构,这些在 C 程序员的程序中都不太常见,常见的 goto 语句使用方法是用它来跳出多重循环,不过它只能从内层循环跳到外层循环,不能从外层循环跳到内层循环。在下面说到 for 循环语句时再略为提一提。 为何大多数 C 程序员都不喜欢用 goto 语句?那是因为过多的使用它时会程序结构不清晰,过多的跳转就使程序又回到了汇编的编程风格,使程序失去了 C 的模块化的优点。

while 语句

while 语句的意思很不难理解,在英语中它的意思是“当……的时候……”,在这里我们可以理解为“当条件为真的时候就执行后面的语句”,它的语法如下:

while (条件表达式) 语句;

使用 while 语句时要注意当条件表达式为真时,它才执行后面的语句,执行完后再次回到 while 执行条件判断,为真时重复执行语句,为假时退出循环体。当条件一开始就为假时, 那么 while 后面的循环体(语句或复合语句)将一次都不执行就退出循环。在调试程序时要注意 while 的判断条件不能为假而造成的死循环,调试时适当的在 while 处加入断点,也许会使你的调试工作更加顺利。当然有时会使用到死循环来等待中断或 IO 信号等,如在第一 篇时我们就用了 while(1)来不停的输出“Hello World!”。下面的例子是显示从 1 到 10 的累 加和,读者能修改一下 while 中的条件看看结果会如果,从而体会一下 while 的使用方法。

#include

#include

void main(void)

{

unsigned int I = 1;

unsigned int SUM = 0; //设初值

SCON = 0x50; //串行口方式 1,允许接收

TMOD = 0x20; //定时器 1 定时方式 2

TCON = 0x40; //设定时器 1 开始计数

TH1 = 0xE8; //11.0592MHz 1200 波特率

TL1 = 0xE8; TI = 1;

TR1 = 1; //启动定时器

while(I<=10)

{

SUM = I + SUM; //累加

printf ("%d SUM=%dn",I,SUM); //显示

I++;

}

while(1); //这句是为了不让程序完后,程序指针继续向下造成程序“跑飞”

}

//最后运行结果是 SUM=55;

do while 语句

do while 语句能说是 while 语句的补充,while 是先判断条件是否成立再执行循环体,

而 do while 则是先执行循环体,再根据条件判断是否要退出循环。这样就决定了循环体无论在任何条件下都会至少被执行一次。它的语法如下:

do 语句 while (条件表达式)

用 do while 怎么写上面那个例程呢?先想一想,再参考下面的程序。

#include

#include

void main(void)

{ unsigned int I = 1;

unsigned int SUM = 0; //设初值

SCON = 0x50; //串行口方式 1,允许接收 TMOD = 0x20; //定时器 1 定时方式 2

TCON = 0x40; //设定时器 1 开始计数

TH1 = 0xE8; //11.0592MHz 1200 波特率 TL1 = 0xE8;

TI = 1;

TR1 = 1; //启动定时器

do{

SUM = I + SUM; //累加

printf ("%d SUM=%dn",I,SUM); //显示 I++;

}while(I<=10);

while(1);

}在上面的程序看来 do while 语句和 while 语句似乎没有什么两样,但在实际的应用中要注

意任何 do while 的循环体一定会被执行一次。如把上面两个程序中 I 的初值设为 11,那么前一个程序不会得到显示结果,而后一个程序则会得到 SUM=11。

for 语句

在明确循环次数的情况下,for 语句比以上说的循环语句都要方便简单。它的语法如下:

for ([初值设定表达式];[循环条件表达式];[条件更新表达式]) 语句 中括号中的表达式是可选的,这样 for 语句的变化就会很多样了。for 语句的执行:

先代入初值,再判断条件是否为真,条件满足时执行循环体并更新条件,再判断条件是否为真…………直到条件为假时,退出循环。下面的例子所要实现的是和上二个例子一样的,对照着 看不难理解几个循环语句的差异。

#include

#include

void main(void)

{

unsigned int I;

unsigned int SUM = 0; //设初值

SCON = 0x50; //串行口方式 1,允许接收 TMOD = 0x20; //定时器 1 定时方式 2

TCON = 0x40; //设定时器 1 开始计数

TH1 = 0xE8; //11.0592MHz 1200 波特率 TL1 = 0xE8;

TI = 1;

TR1 = 1; //启动定时器

for (I=1; I<=10; I++) //这里能设初始值,所以变量定义时能不设

{

SUM = I + SUM; //累加

printf ("%d SUM=%dn",I,SUM); //显示

}

while(1);

}

如果我们把程序中的 for 改成 for(; I<=10; I++)这样条件的初值会变成当前 I 变量的

值。如果改成 for(;;)会怎么样呢?试试看。

continue 语句

continue 语句是用于中断的语句,通常使用在循环中,它的作用是结束本次循环,跳过循环体中没有执行的语句,跳转到下一次循环周期。语法为:continue;

continue 同时也是一个无条件跳转语句,但功能和前面说到的 break 语句有所不一样, continue 执行后不是跳出循环,而是跳到循环的开始并执行下一次的循环。在上面的例子 中的循环体加入 if (I==5) continue;看看什么结果?

return 语句

return 语句是返回语句,不属于循环语句,是要学习的最后一个语句所以一并写下了。 返回语句是用于结束函数的执行,返回到调用函数时的位置。语法有二种:

return (表达式);

return; 语法中因带有表达式,返回时先计算表达式,再返回表达式的值。不带表达式则返回的

值不确定。下面是一个同样是计算 1-10 的累加,所不一样是的用了函数的方式。

#include

#include

int Count(void); //声明函数

void main(void)

{

unsigned int temp;

SCON = 0x50; //串行口方式 1,允许接收 TMOD = 0x20; //定时器 1 定时方式 2

TCON = 0x40; //设定时器 1 开始计数

TH1 = 0xE8; //11.0592MHz 1200 波特率 TL1 = 0xE8;

TI = 1;TR1 = 1; //启动定时器

temp = Count();

printf ("1-10 SUM=%dn",temp); //显示

while(1);

}

int Count(void)

{

unsigned int I, SUM;

for (I=1; I<=10; I++)

{

SUM = I + SUM; //累加

}

return (SUM);

} 上一篇的最后一个例子中有用到函数,其实一直出现在例子中的 main()也算是一个函数,只不过它比较特殊,编译时以它做为程序的开始段。有了函数 C 语言就有了模块化的优点,一般功能较多的程序,会在编写程序时把每项单独的功能分成数个子程序模块,每个子程序就能用函数来实现。函数还能被反复的调用,因此一些常用的函数能做成函数库以供在编写程序时直接调用,从而更好的实现模块化的设计,大大提高编程工作的效率。

一.函数定义

通常 C 语言的编译器会自带标准的函数库,这些都是一些常用的函数,Keil uv 中也不例外。标准函数已由编译器软件商编写定义,使用者直接调用就能了,而无需定义。但是 标准的函数不足以满足使用者的特殊要求,因此 C 语言允许使用者根据需要编写特定功能的 函数,要调用它必须要先对其进行定义。定义的模式如下:

函数类型函数名称(形式参数表)

函数体

函数类型是说明所定义函数返回值的类型。返回值其实就是一个变量,只要按变量类型来定义函数类型就行了。如函数不需要返回值函数类型能写作“void”表示该函数没有返回值。注意的是函数体返回值的类型一定要和函数类型一致,不然会造成错误。函数名称的定义在遵循 C 语言变量命名规则的同时,不能在同一程序中定义同名的函数这将会造成编译错误(同一程序中是允许有同名变量的,因为变量有全局和局部变量之分)。形式参数是指调用函数时要传入到函数体内参与运算的变量,它能有一个、几个或没有,当不需要形式参数也就是无参函数,括号内能为空或写入“void”表示,但括号不能少。函数体中能包含有局部变量的定义和程序语句,如函数要返回运算值则要使用 return 语句进行返回。在函数的{}号中也能什么也不写,这就成了空函数,在一个程序项目中能写一些空函数,在以后的修改和升级中能方便的在这些空函数中进行功能扩充。

二.函数的调用

函数定义好以后,要被其它函数调用了才能被执行。C 语言的函数是能相互调用的,但在调用函数前,必须对函数的类型进行说明,就算是标准库函数也不例外。标准库函数的说明会被按功能分别写在不一样的头文件中,使用时只要在文件最前面用#include 预处理语句引入相应的头文件。如前面一直有使用的printf 函数说明就是放在文件名为 stdio.h 的头文件中。调用就是指一个函数体中引用另一个已定义的函数来实现所需要的功能,这个时候函数体称为主调用函数,函数体中所引用的函数称为被调用函数。一个函数体中能调用数个其它的函数,这些被调用的函数同样也能调用其它函数,也能嵌套调用。笔者本人认为 主函数只是相对于被调用函数而言。在 c51 语言中有一个函数是不能被其它函数所调用的, 它就是 main 主函数。

调用函数的一般形式如下:

函数名 (实际参数表) “函数名”就是指被调用的函数。实际参数表能为零或多个参数,多个参数时要用逗号隔开,每个参数的类型、位置应与函数定义时所的形式参数一一对应,它的作用就是把参数传到被调用函数中的形式参数,如果类型不对应就会产生一些错误。调用的函数是无参函数时不写参数,但不能省后面的括号。

在以前的一些例子我们也能看不一样的调用方式:

1.函数语句

如 printf ("Hello World!n"); 这是在 我们的第一个程序中出现的,它以 "HelloWorld!n"为参数调用 printf 这个库函数。在这里函数调用被看作了一条语句。

2.函数参数 “函数参数”这种方式是指被调用函数的返回值当作另一个被调用函数的实际参数,如 temp=StrToInt(CharB(16));CharB 的返回值作为 StrToInt 函数的实际参数传递。

3.函数表达式

而在上一篇的例子中有 temp = Count();这样一句,这个时候函数的调用作为一个运算对象出现在表达式中,能称为函数表达式。例子中 Count()返回一个 int 类型的返回 值直接赋值给 temp。注意的是这种调用方式要求被调用的函数能返回一个同类型的值, 不然会出现不可预料的错误。

前面说到调用函数前要对被调用的函数进行说明。标准库函数只要用#include 引入已写好说明的头文件,在程序就能直接调用函数了。如调用的是自定义的函数则要用如下形式编写函数类型说明类型标识符

函数的名称(形式参数表);

这样的说明方式是用在被调函数定义和主调函数是在同一文件中。你也能把这些写到文件名.h 的文件中用#include "文件名.h"引入。如果被调函数的定义和主调函数不是在同一文件中的,则要用如下的方式进行说明,说明被调函数的定义在同一项目的不一样文件之上,其实库函数的头文件也是如此说明库函数的,如果说明的函数也能称为外部函数。

extern 类型标识符 函数的名称(形式参数表);

函数的定义和说明是完全不一样的,在编译的角度上看函数的定义是把函数编译存放在ROM 的某一段地址上,而函数说明是告诉编译器要在程序中使用那些函数并确定函数的地址。如果在同一文件中被调函数的定义在主调函数之前,这个时候能不用说明函数类型。也就 是说在 main 函数之前定义的函数,在程序中就能不用写函数类型说明了。能在一个函数体调用另一个函数(嵌套调用),但不允许在一个函数定义中定义另一个函数。还要注意的是函数定义和说明中的“类型、形参表、名称”等都要相一致。

三.中断函数

中断服务函数是编写单片机应用程序不可缺少的。中断服务函数只有在中断源请求响应中断时才会被执行,这在处理突发事件和实时控制是十分有效的。例如:电路中一个按钮,要求按钮后 LED 点亮,这个按钮何时会被按下是不可预知的,为了要捕获这个按钮的事件,通常会有三种方法,一是用循环语句不断的对按钮进行查询,二是用定时中断在间隔时间内扫描按钮,三是用外部中断服务函数对按钮进行捕获。在这个应用中只有单一的按钮功能,那么第一种方式就能胜任了,程序也很简单,但是它会不停的在对按钮进行查询浪费了CPU 的时间。实际应用中一般都会还有其它的功能要求同时实现,这个时候能根据需要选用第 二或第三种方式,第三种方式占用的 CPU 时间最少,只有在有按钮事件发生时,中断服务函 数才会被执行,其余的时间则是执行其它的任务。

如果你学习过汇编语言的话,刚开始写汇编的中断应用程序时,你一定会为出入堆栈的 问题而困扰过。单片机c语言语言扩展了函数的定义使它能直接编写中断服务函数,你能不必考 虑出入堆栈的问题,从而提高了工作的效率。扩展的关键字是 interrupt,它是函数定义时的一个选项,只要在一个函数定义后面加上这个选项,那么这个函数就变成了中断服务函数。在后面还能加上一个选项 using,这个选项是指定选用 51 芯片内部 4 组工作寄存器中的那个组。开始学习者能不必去做工作寄存器设定,而由编译器自动选择,避免产生不必要的错误。定义中断服务函数时能用如下的形式。

函数类型 函数名 (形式参数) interrupt n [using n]

interrupt 关键字是不可缺少的,由它告诉编译器该函数是中断服务函数,并由后面的

n 指明所使用的中断号。n 的取值范围为 0-31,但具体的中断号要取决于芯片的型号,像 AT89c51 实际上就使用 0-4 号中断。每个中断号都对应一个中断向量,具体地址为 8n+3, 中断源响应后处理器会跳转到中断向量所处的地址执行程序,编译器会在这地址上产生一个无条件跳转语句,转到中断服务函数所在的地址执行程序。下表是 51 芯片的中断向量和中 断号。

中断号

中断源

中断向量

0

外部中断 0

0003H

1

定时器/计数器 0

000BH

2

外部中断 1

0013H

3

定时器/计数器 1

001BH

4

串行口

0023H

表 9-1 AT89c51 芯片中断号和中断向量

使用中断服务函数时应注意:中断函数不能直接调用中断函数;不能通过形参传速参数; 在中断函数中调用其它函数,两者所使用的寄存器组应相同。限于篇幅其它与函数相关的知识这里不能一一加以说明,如变量的传递、存储,局部变量、全部变量等,有兴趣的朋友可 以访问笔者的网站 阅读更多相关文章。

下面是简单的例子。首先要在前面做好的实验电路中加多一个按钮,接在 P3.2(12 引脚外 部中断 INT0)和地线之间。把编译好后的程序烧录到芯片后,当接在 P3.2 引脚的按钮接下 时,中断服务函数 Int0Demo 就会被执行,把 P3 当前的状态反映到 P1,如按钮接下后 P3.7(之前有在这脚装过一按钮)为低,这个时候 P1.7 上的 LED 就会熄灭。放开 P3.2 上的按钮后,

P1LED 状态保持先前按下 P3.2 时 P3 的状态。

#include

unsigned char P3State(void); //函数的说明,中断函数不用说明

void main(void)

{

IT0 = 0; //设外部中断 0 为低电平触发

EX0 = 1; //允许响应外部中断 0

EA = 1; //总中断开关

while(1);

}

//外部中断 0 演示,使用 2 号寄存器组

void Int0Demo(void) interrupt 0 using 2

{

unsigned int Temp; //定义局部变量

P1 = ~P3State(); //调用函数取得 p2 的状态反相后并赋给 P1

for (Temp=0; Temp<50; Temp++); //延时这里只是演示局部变量的使用

}

//用于返回 P3 的状态,演示函数的使用

unsigned char P3State(void)

{

unsigned char Temp;

Temp = P3; //读取 P3 的引脚状态并保存在变量 Temp 中,这样只有一句语句实在没必要做成函数,这里只是学习函数的基本使用方法

return Temp;

} 前面的文章中,都是介绍单个数据变量的使用,在“走马灯”等的例子中略有使用到数组,不难看出,数组不过就是同一类型变量的有序集合。形象的能这样去理解,就像一个学校在操场上排队,每一个级代表一个数据类型,每一个班级为一个数组,每一个学生就是数组中的一个数据。数据中的每个数据都能用唯一的下标来确定其位置,下标能是一维 或多维的。就如在学校的方队中要找一个学生,这个学生在 I 年级 H 班 X 组 Y 号的,那么 能把这个学生看做在 I 类型的 H 数组中(X,Y)下标位置中。数组和普通变量一样,要求先定义了才能使用,下面是定义一维或多维数组的方式:

数据类型

数组名

[常量表达式];

数据类型

数组名

[常量表达式 1]...... [常量表达式 N];

“数据类型”是指数组中的各数据单元的类型,每个数组中的数据单元只能是同一数据类型。“数组名”是整个数组的标识,命名方法和变量命名方法是一样的。在编译时系统会根据数组大小和类型为变量分配空间,数组名能说就是所分配空间的首地址的标识。“常量表达式”是表示数组的长度和维数,它必须用“[]”括起,括号里的数不能是变量只能是 常量。

unsigned int xcount [10]; //定义无符号整形数组,有 10 个数据单元

char inputstring [5]; //定义字符形数组,有 5 个数据单元

float outnum [10],[10];//定义浮点型数组,有 100 个数据单元

在 C 语言中数组的下标是从 0 开始的而不是从 1 开始,如一个具有 10 个数据单元的数组 count,它的下标就是从 count[0]到 count[9],引用单个元素就是数组名加下标,如 count[1] 就是引用 count 数组中的第 2 个元素,如果错用了 count[10]就会有错误出现了。还有一点要 注意的就是在程序中只能逐个引用数组中的元素,不能一次引用整个数组,但是字符型的数组就能一次引用整个数组。

数组也是能赋初值的。在上面介绍的定义方式只适用于定义在内存 DATA 存储器使 用的内存,有的时候我们需要把一些数据表存放在数组中,通常这些数据是不用在程序中改变数值的,这个时候就要把这些数据在程序编写时就赋给数组变量。因为 51 芯片的片内 RAM 很有限,通常会把 RAM 分给参与运算的变量或数组,而那些程序中不变数据则应存放在片 内的 CODE 存储区,以节省宝贵的 RAM。赋初值的方式如下:

数据类型 [存储器类型] 数组名 [常量表达式] = {常量表达式};

数据类型 [ 存储器类型] 数组名 [ 常量表达式 1]...... [ 常量表达式 N]={{ 常量表达 式}...{常量表达式 N}};

在定义并为数组赋初值时,开始学习的朋友一般会搞错初值个数和数组长度的关系,而致使编译出错。初值个数必须小于或等于数组长度,不指定数组长度则会在编译时由实际的初值 个数自动设置。

unsigned char LEDNUM[2]={12,35}; //一维数组赋初值

int Key[2][3]={{1,2,4},{2,2,1}}; //二维数组赋初值

unsigned char IOStr[]={3,5,2,5,3}; //没有指定数组长度,编译器自动设置

unsigned char code skydata[]={0x02,0x34,0x22,0x32,0x21,0x12}; //数据保存在 code 区

下面的一个简单例子是对数组中的数据进行排序,使用的是冒泡法,一来了解数组的使用,二来掌握基本的排序算法。冒泡排序算法是一种基本的排序算法,它每次顺序取数组中的两个数,并按需要按其大小排列,在下一次循环中则取下一次的一个数和数组中下一个数 进行排序,直到数组中的数据全部排序完成。

#include

#include

void taxisfun (int taxis2[])

{

unsigned char TempCycA,TempCycB,Temp;

for (TempCycA=0; TempCycA<=8; TempCycA++)

for (TempCycB=0; TempCycB<=8-TempCycA; TempCycB++)

{//TempCycB<8-TempCycA 比用 TempCycB<=8 少用很多循环

if (taxis2[TempCycB+1]>taxis2[TempCycB]) //当后一个数大于前一个 数

{

Temp = taxis2[TempCycB]; //前后 2 数交换

taxis2[TempCycB] = taxis2[TempCycB+1];

taxis2[TempCycB+1] = Temp; //因函数参数是数组名调用形参的变动影响实参

}

}

}

2015-11-12 13:59:12 来源:eefocus 关键字: 51系列单片机C编程程序语句void main(void)

{

int taxis[] = {113,5,22,12,32,233,1,21,129,3};

char Text1[] = {"source data:"}; //"源数据"

char Text2[] = {"sorted data:"}; //"排序后数据"

unsigned char TempCyc;

SCON = 0x50; //串行口方式 1,允许接收

TMOD = 0x20; //定时器 1 定时方式 2

TCON = 0x40; //设定时器 1 开始计数

TH1 = 0xE8; //11.0592MHz 1200 波特率

TL1 = 0xE8; TI = 1;

TR1 = 1; //启动定时器

printf("%sn",Text1); //字符数组的整体引用

for (TempCyc=0; TempCyc<10; TempCyc++)

printf("%d ",taxis[TempCyc]);

printf("n----------n");

taxisfun (taxis); //以实际参数数组名 taxis 做参数被函数调用

printf("%sn",Text2);

for (TempCyc=0; TempCyc<10; TempCyc++) //调用后 taxis 会被改变

printf("%d ",taxis[TempCyc]);

while(1);

}

例子中能看出,数组同样能作为函数的参数进行传递。数组做参数时是用数组名进行传递的,一个数组的数组名表示该数组的首地址,在用数组名作为函数的调用参数时,它的传递方式是采用了地址传递,就是将实际参数数组的首地址传递给函数中的形式参数数组,这个时候实际参数数组和形式参数数组实际上是使用了同一段内存单元,当形式参数数组在函数体中改变了元素的值,同时也会影响到实际参数数组,因为它们是存放在同一个地址的。 上面的例子同时还使用到字符数组。字符数组中每一个数据都是一个字符,这样一个一 维的字符数组就组成了一个字符串,在 C 语言中字符串是以字符数组来表达处理的。为了 能测定字符串的长度,C 语言中规定以‘o’来做为字符串的结束标识,编译时会自动在字符串的最后加入一个‘o’,那么要注意的是如果用一个数组要保存一个长度为 10 字节的字 符串则要求这个数组至少能保存 11 个元素。‘o’是转义字符,它的含义是空字符,它的 ASCII 码为 00H,也就是说当每一个字符串都是以数据 00H 结束的,在程序中操作字符数 据组时要注意这一点。字符数组除了能对数组中单个元素进行访问,还能访问整个数组,其实整个访问字符数组就是把数组名传到函数中,数组名是一个指向数据存放空间的地址指针,函数根据这个指针和‘/o’就能完整的操作这个字符数组。对于这一段所说的,能 参看下面一例 1602LCD 显示模块的驱动演示例子进行理解。这里要注意就是能用单个字

符数组元素来进行运算,但不能用整个数组来做运算,因为数组名是指针而不是数据。

#define LCM_RW P2_0 //定义引脚

#define LCM_RS P2_1

#define LCM_E P2_2

#define LCM_Data P1

#define Busy 0x80 //用于检测 LCM 状态字中的 Busy 标识

#include

void WriteDataLCM(unsigned char WDLCM);

void WriteCommandLCM(unsigned char WCLCM,BuysC);

unsigned char ReadDataLCM(void); unsigned char ReadStatusLCM(void); void LCMInit(void);

void DisplayOneChar(unsigned char X, unsigned char Y, unsigned char DData);

void DisplayListChar(unsigned char X, unsigned char Y, unsigned char code *DData);

void Delay5Ms(void);

void Delay400Ms(void);

unsigned char code cdle_net[] = {"www.51hei.com"};

unsigned char code email[] = {"pnzwzw@51hei.com"};

void main(void)

{

Delay400Ms(); //启动等待,等 LCM 讲入工作状态

LCMInit(); //LCM 初始化

Delay5Ms(); //延时片刻(可不要)

DisplayListChar(0, 0, cdle_net); DisplayListChar(0, 1, email); ReadDataLCM();//测试用句无意义 while(1);

}

//写数据

void WriteDataLCM(unsigned char WDLCM)

{

ReadStatusLCM(); //检测忙 LCM_Data = WDLCM; LCM_RS = 1;

LCM_RW = 0;

LCM_E = 0; //若晶体震荡器速度太高能在这后加小的延时

LCM_E = 0; //延时

LCM_E = 1;

}

//写指令

void WriteCommandLCM(unsigned char WCLCM,BuysC) //BuysC 为 0 时忽略忙检测

{

if (BuysC) ReadStatusLCM(); //根据需要检测忙

LCM_Data = WCLCM; LCM_RS = 0; LCM_RW = 0;

LCM_E = 0;

LCM_E = 0; LCM_E = 1;

}

//读数据

unsigned char ReadDataLCM(void)

{

LCM_RS = 1; LCM_RW = 1; LCM_E = 0; LCM_E = 0; LCM_E = 1; return(LCM_Data);

}

//读状态

unsigned char ReadStatusLCM(void)

{

LCM_Data = 0xFF; LCM_RS = 0; LCM_RW = 1; LCM_E = 0; LCM_E = 0; LCM_E = 1;

while (LCM_Data & Busy); //检测忙信号

return(LCM_Data);

}

void LCMInit(void) //LCM 初始化

{

LCM_Data = 0;

WriteCommandLCM(0x38,0); //三次显示模式设置,不检测忙信号

Delay5Ms(); WriteCommandLCM(0x38,0); Delay5Ms(); WriteCommandLCM(0x38,0); Delay5Ms();

WriteCommandLCM(0x38,1); //显示模式设置,开始要求每次检测忙信号

WriteCommandLCM(0x08,1); //关闭显示 WriteCommandLCM(0x01,1); //显示清屏 WriteCommandLCM(0x06,1); // 显示光标移动设置 WriteCommandLCM(0x0C,1); // 显示开及光标设置

}

//按指定位置显示一个字符

void DisplayOneChar(unsigned char X, unsigned char Y, unsigned char DData)

{

Y &= 0x1;

X &= 0xF; //限制 X 不能大于 15,Y 不能大于 1

if (Y) X |= 0x40; //当要显示第二行时地址码+0x40; X |= 0x80; //算出指令码

WriteCommandLCM(X, 0); //这里不检测忙信号,发送地址码

WriteDataLCM(DData);

}

//按指定位置显示一串字符

void DisplayListChar(unsigned char X, unsigned char Y, unsigned char code *DData)

{

unsigned char ListLength;

ListLength = 0; Y &= 0x1;

X &= 0xF; //限制 X 不能大于 15,Y 不能大于 1

while (DData[ListLength]>0x20) //若到达字串尾则退出

{

if (X <= 0xF) //X 坐标应小于 0xF

{

DisplayOneChar(X, Y, DData[ListLength]); //显示单个字符

ListLength++; X++;

}

}

}

//5ms 延时

void Delay5Ms(void)

{

unsigned int TempCyc = 5552;

while(TempCyc--);

}

//400ms 延时

void Delay400Ms(void)

{

unsigned char TempCycA = 5; unsigned int TempCycB; while(TempCycA--)

{

TempCycB=7269;

while(TempCycB--);

};

}

指针就是指变量或数据所在的存储区地址。如一个字符型的变量 STR 存放在内存单元DATA 区的 51H 这个地址中,那么 DATA 区的 51H 地址就是变量 STR 的指针。在 C 语言中指针是一个很重要的概念,正确有效的使用指针类型的数据,能更有效的表达复杂的数据结构,能更有效的使用数组或变量,能方便直接的处理内存或其它存储区。指针之所以能这么有效的操作数据,是因为无论程序的指令、常量、变量或特殊寄存器都要存放在内存单元或相应的存储区中,这些存储区是按字节来划分的,每一个存储单元都能用唯一的编号去读或写数据,这个编号就是常说的存储单元的地址,而读写这个编号的动作就叫做寻址,通过寻址就能访问到存储区中的任一个能访问的单元,而这个功能是变量或数组等 是不可能代替的。C 语言也因此引入了指针类型的数据类型,专门用来确定其他类型数据的地址。用一个变量来存放另一个变量的地址,那么用来存放变量地址的变量称为“指针变量”。 如用变量 STRIP 来存放文章开头的 STR 变量的地址 51H,变量 STRIP 就是指针变量。下面 用一个图表来说明变量的指针和指针变量两个不一样的概念。

2009330204556190.gif

变量的指针就是变量的地址,用取地址运算符‘&’取得赋给指针变量。&STR 就是把 变量 STR 的地址取得。用语句 STRIP = &STR 就能把所取得的 STR 指针存放在 STRIP 指 针变量中。STRIP 的值就变为 51H。可见指针变量的内容是另一个变量的地址,地址所属的变量称为指针变量所指向的变量。

要访问变量 STR 除了能用‘STR’这个变量名来访问之外,还能用变量地址来访 问。方法是先用&STR 取变量地址并赋于 STRIP 指针变量,然后就能用*STRIP 来对 STR 进行访问了。‘*’是指针运算符,用它能取得指针变量所指向的地址的值。在上图中指针 变量 STRIP 所指向的地址是 51H,而 51H 中的值是 40H,那么*STRIP 所得的值就是 40H。使用指针变量之前也和使用其它类型的变量那样要求先定义变量,而且形式也相类似,

一般的形式如下:

数据类型 [存储器类型] * 变量名;

unsigned char xdata *pi //指针会占用二字节,指针自身存放在编译器默认存储区,指向 xdata 存储区的 char 类型

unsigned char xdata * data pi; //除指针自身指定在 data 区,其它同上

int * pi; //定义为一般指针,指针自身存放在编译器默认存储区,占三个字节在定义形式中“数据类型”是指所定义的指针变量所指向的变量的类型。“存储器类型”是编译器编译时的一种扩展标识,它是可选的。在没有“存储器类型”选项时,则定义为一般指针,如有“存储器类型”选项时则定义为基于存储器的指针。限于 51 芯片的寻址范围,指针变量最大的值为 0xFFFF,这样就决定了一般指针在内存会占用 3 个字节,第一字节存放该指针存储器类型编码,后两个则存放该指针的高低位址。而基于存储器的指针因为不用识别存储器类型所以会占一或二个字节,idata,data,pdata 存储器指针占一个字节,code,xdata 则会占二个字节。由上可知,明确的定义指针,能节省存储器的开销,这在严格要求程序 体积的项目中很有用处。

指针的使用方法很多,限于篇幅以上只能对它做一些基础的介绍。下面用在讲述常量时的例程改动一下,用以说明指针的基本使用方法。

#include //预处理文件里面定义了特殊寄存器的名称如 P1 口定义为 P1

void main(void)

{

//定义花样数据,数据存放在片内 CODE 区中

unsigned char code design[]={0xFF,0xFE,0xFD,0xFB,0xF7,0xEF,0xDF,0xBF,0x7F,

0x7F,0xBF,0xDF,0xEF,0xF7,0xFB,0xFD,0xFE,0xFF,

0xFF,0xFE,0xFC,0xF8,0xF0,0xE0,0xC0,0x80,0x0,

0xE7,0xDB,0xBD,0x7E,0xFF};

unsigned int a; //定义循环用的变量

unsigned char b;

unsigned char code * dsi; //定义基于 CODE 区的指针

do{

dsi = &design[0]; //取得数组第一个单元的地址

for (b=0; b<32; b++)

{

}

}while(1);

}

for(a=0; a<30000; a++); //延时一段时间

P1 = *dsi; //从指针指向的地址取数据到 P1 口

dsi++; //指针加一,

为了能清楚的了解指针的工作原理,能使用 keil uv2 的软件仿真器查看各变量和存储器的

值。编译程序并执行,然后打开变量窗口,如图。用单步执行,就能查到到指针的变量。如图中所示的是程序中循环执行到第二次,这个时候指针 dsi 指向 c:0x0004 这个地址,这个地址 的值是 0xFE。在存储器窗口则能察看各地址单元的值。使用这种方法不但在学习时能帮助更好的了解语法或程序的工作,而且在实际使用中更能让你更快更准确的编写程序或解 决程序中的问题。返回搜狐,查看更多

责任编辑:

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值