(1)实验平台:普中51单片机开发板-A2&A3&A4
不论学习什么单片机, 最简单的外设莫过于 IO 口的高低电平控制, 本章将向大家介绍如何在创建好的工程模板上, 通过控制 51 单片机的 GPIO 使开发板上的 LED 灯点亮。 通过本章的学习, 让大家学会如何在程序中操作 51 单片机的 GPIO 口输出高低电平。 本章分为如下几部分内容:
[9.1 51 单片机 GPIO 介绍](#9.1 51 单片机 GPIO 介绍)
[9.1.1 GPIO 概念](#9.1.1 GPIO 概念)
[9.1.2 GPIO 结构框图与工作原理](#9.1.2 GPIO 结构框图与工作原理)
[9.1.2.1 P0 端口](#9.1.2.1 P0 端口)
[9.1.2.2 P1 端口](#9.1.2.2 P1 端口)
[9.1.2.3 P2 端口](#9.1.2.3 P2 端口)
[9.1.2.4 P3 端口](#9.1.2.4 P3 端口)
[9.2 LED 简介](#9.2 LED 简介)
[9.3 硬件设计](#9.3 硬件设计)
[9.4 软件设计](#9.4 软件设计)
[9.4.1 点亮第一个 LED](#9.4.1 点亮第一个 LED)
[9.4.2 LED 闪烁实验](#9.4.2 LED 闪烁实验)
[9.4.2.1 通过 KEIL 软件仿真查看延时时间](#9.4.2.1 通过 KEIL 软件仿真查看延时时间)
[9.4.3 LED 流水灯实验](#9.4.3 LED 流水灯实验)
[9.4.3.1 使用移位和循环实现](#9.4.3.1 使用移位和循环实现)
[9.4.3.2 使用左移_crol_、 右移_cror_函数](#9.4.3.2 使用左移_crol_、 右移_cror_函数)
[9.5 实验现象](#9.5 实验现象)
9.1 51 单片机 GPIO 介绍
9.1.1 GPIO 概念
GPIO(general purpose intput output) 是通用输入输出端口的简称, 可以通过软件来控制其输入和输出。 51 单片机芯片的 GPIO 引脚与外部设备连接起来, 从而实现与外部通讯、 控制以及数据采集的功能。 不过 GPIO 最简单的应用还属点亮 LED 灯了, 只需通过软件控制 GPIO 输出高低电平即可。 当然 GPIO还可以作为输入控制, 比如在引脚上接入一个按键, 通过电平的高低判断按键是否按下。
我们开发板上使用的 51 单片机型号是 STC89C52 或 STC89C516, 此芯片共有40 引脚, 芯片引脚图如下图所示:

那么是不是所有引脚都是 GPIO 呢? 当然不是, 51 单片机引脚可以分为这么几大类:
(1) 电源引脚: 引脚图中的 VCC、 GND 都属于电源引脚。
(2) 晶振引脚: 引脚图中的 XTAL1、 XTAL2 都属于晶振引脚。
(3) 复位引脚: 引脚图中的 RST/VPD 属于复位引脚, 不做其他功能使用。
(4) 下载引脚: 51 单片机的串口功能引脚(TXD、 RXD) 可以作为下载引脚使用。
(5) GPIO 引脚: 引脚图中带有 Px.x 等字样的均属于 GPIO 引脚。 从引脚图可以看出, GPIO 占用了芯片大部分的引脚, 共达 32 个, 分为了 4 组, P0、 P1、P2、 P3, 每组为 8 个 IO, 而且在 P3 组中每个 IO 都具备额外功能, 只要通过相应的寄存器设置即可配置对应的附加功能, 同一时刻, 每个引脚只能使用该引脚的一个功能。
对于这么多 GPIO 管脚, 我们怎么知道具体某个引脚有什么功能呢? 很简单, 可以查阅 STC89CXX 芯片数据手册获取信息, 数据手册在我们资料"\6--芯片资料\开发板芯片数据手册" 内, 里面有一个 STC89CXX 数据手册.pdf。 里面的第 23 页中就有介绍, 我们截取了一部分内容如下图所示:

从上图中我们可以获取引脚的名字和引脚功能等信息。 这个我们开发板芯片原理图内已经将引脚所有功能都标进去了, 所以后面也不需要查找具体引脚有什么功能, 直接看原理图即可。
9.1.2 GPIO 结构框图与工作原理
我们使用的 51 单片机 GPIO 分为 P0、 P1、 P2 和 P3 口, 下面分别来介绍其内部结构框图与工作原理。
9.1.2.1 P0 端口
P0 端口含有 8 位引脚, 下图为其中一个, 其它几个与之完全一致, 因此只需了解当中一个即可。 如下图所示:

由上图可见, P0 端口由锁存器、 输入缓冲器、 切换开关、 一个非门、 一个与非门及场效应管驱动电路构成。 再看图的最右边, 标号为 P0.x 引脚的图标, 也就是说 P0.x 引脚可以是 P0.0 到 P0.7 的任何一位, 即在 P0 口有 8 个与上图相同的电路组成。
下面, 我们先就组成 P0 口的每个单元部份跟大家介绍一下:
①输入缓冲器
在 P0 口中, 有两个三态的缓冲器, 在学数字电路时, 我们已知道, 三态门有三个状态, 即在输出端可以是高电平、 低电平, 同时还有一种就是高阻状态(或称为禁止状态) , 大家看上图, 上面一个是读锁存器的缓冲器, 也就是说, 要读取 D 锁存器输出端 Q 的数据, 那就得使读锁存器的这个缓冲器的三态控制端(上图中标号为'读锁存器' 端) 有效。 下面一个是读引脚的缓冲器, 要读取 P0.x引脚上的数据, 也要使标号为'读引脚' 的这个三态缓冲器的控制端有效, 引脚上的数据才会传输到我们单片机的内部数据总线上。
②D 锁存器
构成一个锁存器, 通常要用一个时序电路, 时序的单元电路在学数字电路时我们已知道, 一个触发器可以保存一位的二进制数(即具有保持功能) , 在 51单片机的 32 根 I/O 口线中都是用一个 D 触发器来构成锁存器的。 大家看上图中的 D 锁存器, D 端是数据输入端, CP(CLK) 是控制端(也就是时序控制信号输入端) , Q 是输出端, Q 非是反向输出端。
对于 D 触发器来讲, 当 D 输入端有一个输入信号, 如果这时控制端 CP 没有信号(也就是时序脉冲没有到来) , 这时输入端 D 的数据是无法传输到输出端 Q及反向输出端 Q 非的。 如果时序控制端 CP 的时序脉冲一旦到了, 这时 D 端输入的数据就会传输到 Q 及 Q 非端。 数据传送过来后, 当 CP 时序控制端的时序信号消失了, 这时, 输出端还会保持着上次输入端 D 的数据(即把上次的数据锁存起来了) 。 如果下一个时序控制脉冲信号来了, 这时 D 端的数据才再次传送到 Q端, 从而改变 Q 端的状态。
③多路开关
在 51 单片机中, 当内部的存储器够用(也就是不需要外扩展存储器时, 这里讲的存储器包括数据存储器及程序存储器) 时, P0 口可以作为通用的输入输出端口(即 I/O) 使用, 对于 8031(内部没有 ROM) 的单片机或者编写的程序超过了单片机内部的存储器容量, 需要外扩存储器时, P0 口就作为'地址/数据'总线使用。 那么这个多路选择开关就是用于选择是做为普通 I/O 口使用还是作为
'数据/地址' 总线使用的选择开关了。 大家看上图, 当多路开关与下面接通时, P0 口是作为普通的 I/O 口使用的, 当多路开关是与上面接通时, P0 口是作为'地址/数据' 总线使用的。
④场效应管输出驱动
从上图中可以看出, P0 口的输出是由两个 MOS 管组成的推拉式结构, 也就是说, 这两个 MOS 管一次只能导通一个, 当 V1 导通时, V2 就截止, 当 V2 导通时, V1 截止。
⑤与非门、 非门
这个在学习数字电路时也很好理解, 如果没有数字电路基础的用户, 可以百度查找与非门、 非门以及前面的 D 触发器详细了解, 这里就不再过多叙述。 当然如果搞不明白这些也不会影响后续我们学习 51 单片机编程, 大家也可以忽略。
前面我们已将 P0 口的各单元部件进行了一个详细的讲解, 下面我们就来研究一下 P0 口做为 I/O 口及地址/数据总线使用时的具体工作过程。
(1) 作为 I/O 端口输出使用时的工作原理
P0 口作为 I/O 端口使用时, 多路开关的控制信号为 0(低电平) , 看上图中的红线部份, 多路开关的控制信号同时与与非门的一个输入端是相接的, 我们知道与门的逻辑特点是"全 1 出 1, 有 0 出 0" 那么控制信号是 0 的话, 这时与门输出的也是一个 0(低电平) , 与门的输出是 0, V1 管就截止, 在多路控制开关的控制信号是 0(低电平) 时, 多路开关是与锁存器的 Q 非端相接的(即 P0 口作为 I/O 口线使用) 。
P0 口用作 I/O 口线, 其由数据总线向引脚输出(即输出状态 Output) 的工作过程: 当写锁存器信号 CP 有效, 数据总线的信号→锁存器的输入端→D 锁存器的反向输出 Q 非端→多路开关→V2 管的栅极→V2 的漏极到输出端 P0.X。 前面我们已讲了, 当多路开关的控制信号为低电平 0 时, 与门输出为低电平, V1 管是截止的, 所以作为输出口时, P0 是漏极开路输出, 类似于 OC 门, 当驱动上接电流负载时, 需要外接上拉电阻。
下图就是由内部数据总线向 P0 口输出数据的流程图(红色箭头) :

(2) 作为 I/O 端口输入使用时的工作原理
数据输入时(读 P0 口) 有两种情况:
1、 读引脚
读芯片引脚上的数据, 读引脚数时, 读引脚缓冲器打开(即三态缓冲器的控制端要有效) , 通过内部数据总线输入, 请看下图(红色箭头) 。

2、 读锁存器
通过打开读锁存器三态缓冲器读取锁存器输出端 Q 的状态, 请看下图(红色箭头)

因为现在 STC 51 单片机内存已经足够使用, 所以也用不到通过 P0 口外扩存储器, 对于 P0 口作为外扩存储器时的工作原理这里就不叙述, 如需了解的朋友可以上网百度。
9.1.2.2 P1 端口
P1 口的结构最简单, 用途也单一, 仅作为数据输入/输出端口使用。 输出的信息有锁存, 输入有读引脚和读锁存器之分。 P1 端口的一位结构见下图:

由图可见, P1 端口与 P0 端口的主要差别在于, P1 端口用内部上拉电阻 R 代替了 P0 端口的场效应管 V1, 并且输出的信息仅来自内部总线。 由内部总线输出的数据经锁存器反相和场效应管反相后, 锁存在端口线上, 所以, P1 端口是具有输出锁存的静态口。
由上图可见, 要正确地从引脚上读入外部信息, 必须先使场效应管关断, 以便由外部输入的信息确定引脚的状态。 为此, 在作引脚读入前, 必须先对该端口写入 l。 具有这种操作特点的输入/输出端口, 称为准双向 I/O 口。 8051 单片机的 P1、 P2、 P3 都是准双向口。 P0 端口由于输出有三态功能, 输入前, 端口线已处于高阻态, 无需先写入 l 后再作读操作。
P1 口的结构相对简单, 前面我们已详细的分析了 P0 口, 只要大家认真的分析了 P0 口的工作原理, P1 口我想大家都有能力去分析, 这里我就不多论述了。
单片机复位后, 各个端口已自动地被写入了 1, 此时, 可直接作输入操作。如果在应用端口的过程中, 已向 P1 一 P3 端口线输出过 0, 则再要输入时, 必须先写 1 后再读引脚, 才能得到正确的信息。 此外, 随输入指令的不同, P1 端口也有读锁存器与读引脚之分。
9.1.2.3 P2 端口
P2 端口的一位结构见下图:

由图可见, P2 端口在片内既有上拉电阻, 又有切换开关 MUX, 所以 P2 端口在功能上兼有 P0 端口和 P1 端口的特点。 这主要表现在输出功能上, 当切换开关向下接通时, 从内部总线输出的一位数据经反相器和场效应管反相后, 输出在端口引脚线上; 当多路开关向上时, 输出的一位地址信号也经反相器和场效应管反相后, 输出在端口引脚线上。
对于 8031 单片机必须外接程序存储器才能构成应用电路(或者我们的应用电路扩展了外部存储器) , 而 P2 端口就是用来周期性地输出从外存中取指令的地址(高 8 位地址), 因此, P2 端口的多路开关总是在进行切换, 分时地输出从内部总线来的数据和从地址信号线上来的地址。 因此 P2 端口是动态的 I/O 端口。输出数据虽被锁存, 但不是稳定地出现在端口线上。 其实, 这里输出的数据往往也是一种地址, 只不过是外部 RAM 的高 8 位地址。
P2 口既可作为 I/O 口使用, 也可作为地址总线使用, 通常主要用作 I/O 口使用, 地址总线使用不作分析。
P2 口的结构相对简单, 前面我们已详细的分析了 P0 和 P1 口, 只要大家认真的分析了它们的工作原理, P2 口我想大家都有能力去分析, 这里我就不多论述了。
9.1.2.4 P3 端口
P3 口是一个多功能口, 它除了可以作为 I/O 口外, 还具有第二功能, P3 端口的一位结构见下图:

由上图可见, P3 端口和 Pl 端口的结构相似, 区别仅在于 P3 端口的各端口线有两种功能选择。 当处于第一功能时, 第二输出功能线为 1, 此时, 内部总线信号经锁存器和场效应管输入/输出, 其作用与 P1 端口作用相同, 也是静态准双向 I/O 端口。 当处于第二功能时, 锁存器输出 1, 通过第二输出功能线输出特定的内含信号, 在输入方面, 即可以通过缓冲器读入引脚信号, 还可以通过替代输入功能读入片内的特定第二功能信号。 由于输出信号锁存并且有双重功能, 故P3 端口为静态双功能端口。 有关 P3 口第二功能, 在前面章节芯片管脚功能定义已经讲解过, 此处不再重复。
至此, 我们就把 51 单片机的 P0、 P1、 P2 和 P3 口内部结构及原理讲解完,可能有的朋友会很懵、 看不懂, 没关系, 这些都不会影响你编写单片机应用程序, 大家记住以下几点即可:
①P0 口是漏极开路, 要使其输出高电平, 必须外接上拉电阻, 通常选择4.7K~10K 阻值。
②P0、 P1、 P2 几乎都用作普通 I/O 口使用, 既可作为输入, 又可作为输出。
③P3 口既可用作普通 I/O 口, 又可作为第二功能使用, 比如串口、 外部中断、 计数器等。
9.2 LED 简介
LED 即发光二极管。 它具有单向导电性, 通过 5mA 左右电流即可发光, 电流越大, 其亮度越强, 但若电流过大, 会烧毁二极管, 一般我们控制在 3 mA-20mA之间, 通常我们会在 LED 管脚上串联一个电阻, 目的就是为了限制通过发光二极管的电流不要太大, 因此这些电阻又可以称为"限流电阻" 。 当发光二极管发光时, 测量它两端电压约为 1.7V, 这个电压又叫做发光二极管的"导通压降" 。下图左右分别为直插式发光二极管和贴片式发光二极管实物图。 发光二极管正极又称阳极, 负极又称阴极, 电流只能从阳极流向阴极。 直插式发光二极管长脚为阳极, 短脚为阴极。 仔细观察贴片式发光二极管正面的一端有彩色标记, 通常有标记的一端为阴极。


9.3 硬件设计
开发板上 LED 模块电路如下图所示:

在前面我们介绍过如何查看原理图, 相同网络标号表示它们是连接在一起的, 因此 D1-D8 连接到单片机的 P20-P27 口。 图中 LED 采用共阳接法, 即所有LED 阳极管脚接电源 VCC, 阴极管脚通过一个 470 欧的限流电阻接到 P2 口上。 根据前面 LED 的介绍我们知道, 要让 LED 发光即对应的阴极管脚应该为低电平, 若为高电平则熄灭。
如果要想 51 单片机控制 LED, 就必须通过单片机管脚在 P2 口上输出低电平。本章所要实现的功能是: 点亮 D1 指示灯。
9.4 软件设计
9.4.1 点亮第一个 LED
本章所要实现的功能是: 点亮 D1 指示灯, 即让 P2.0 管脚输出一个低电平。完成后可再控制 D1 指示灯闪烁, 即间隔一段时间点亮和熄灭 D1 指示灯。
我们直接复制前面创建好的工程模板, 在此模板基础上进行程序开发。 为了能够与开发攻略教程对应, 将复制过来的模板文件夹重新命名为"2-点亮第一个LED" 。 打开工程直接在 main.c 源文件内进行编程, main.c 内代码如下:
cpp
/**************************************************************************************
实验名称: 点亮第一个 LED
接线说明:
实验现象: 下载程序后"LED 模块" 的 D1 指示灯点亮
注意事项:
***************************************************************************************/
#include "reg52.h"
sbit LED1=P2^0; //将 P2.0 管脚定义为 LED1
/*******************************************************************************
* 函 数 名 : main
* 函数功能 : 主函数
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void main()
{
LED1=0; //LED1 端口设置为低电平
while(1)
{
}
}
main.c 文件内代码非常少也很简单, 首先将 51 单片机的头文件包含进来,然后使用 sbit 关键字来定义 P2.0 管脚, 定义好后即可使用 LED1 来替代 P2.0口的操作。 主函数功能非常简单, 首先让 LED1 为低电平, 即 P2.0 口输出为低电平, D1 指示灯即点亮, 然后进入 while 循环, 单片机此时一直在 while 内循环操作。 当然该条语句也可以放在 while 循环语句内, 同样会点亮 D1 指示灯。 在51 单片机程序开发中, main 函数内通常都会有一个 while 循环, 在循环体内不断执行我们所要实现的功能。 对于仅点亮 LED, 可以把控制语句放在 while 之前来执行。
至此, 整个程序就编写完成, 我们编译一下, 如下图所示

可以看到没有错误, 也没有警告。 从编译信息可以看出, 我们的代码占用FLASH 大小为: 19 字节, 所用的 SRAM 大小为: 9 个字节(9+0) 。 这里我们解释一下, 编译结果里面的几个数据的意义:
code: 表示程序所占用 FLASH 的大小。data: 数据储存器内部 RAM 占用大小。xdata: 数据储存器外部 RAM 占用大小。
有了这个就可以知道你当前使用的 flash 和 sram 大小了。 一定要注意的是程序的大小不是.hex 文件的大小, 而是编译后的 code 和 data 之和。
9.4.2 LED 闪烁实验
如果要实现 LED 闪烁, 只需循环让 D1 指示灯先亮一会后熄灭。 这里就有一个延时问题, 如何来产生延时呢? 我们知道单片机执行每条代码指令都是需要时间的, 在前面介绍 C 语言时讲解过循环语句, 因此只需编写一个循环函数, 让CPU 不干其它事, 专门在那循环运行即可实现延时功能。 依据人的肉眼余晖效应,延时时间不能太短, 否则就无法观察到 LED 闪烁。 打开"\4--实验程序\1--基础实验\3-LED 闪烁实验" 例程内延时函数如下:
cpp
/*******************************************************************************
* 函 数 名 : delay_10us
* 函数功能 : 延时函数, ten_us=1 时, 大约延时 10us
* 输 入 : ten_us
* 输 出 : 无
*******************************************************************************/
void delay_10us(u16 ten_us)
{
while(ten_us--);
}
上述代码即为延时函数, 通过 while 循环来实现。 函数入口有一个形式参数ten_us, 如果 ten_us 等于 1, 则 while 循环执行一次, 调用该函数延时时间大约 10us, 当然使用循环来实现延时, 这种延时是不精确的, 目前我们先得到个大概的时间即可。
细心的朋友可能会看到函数形参 ten_us 是 u16 类型的, 这个似乎不是 C 语言数据类型关键字, 这是我们重定义的数据类型, 如下:
cpp
typedef unsigned int u16; //对系统默认数据类型进行重命名
typedef unsigned char u8;
使用关键字 typedef 对系统默认数据类型 unsigned int 和 unsigned char重新命名, 主要是方便我们代码的书写和变量类型的查看。 u16 即代表该变量是16 位的无符号整型数据, u8 代表该变量是 8 位的无符号字符型数据。 有了这个就知道参数的传送范围, 不能超过形参定义的范围。
下面看下 main 函数代码, 如下:
cpp
void main()
{
while(1)
{
LED1=0; //点亮
delay_10us(50000); //大约延时 450ms
LED1=1; //熄灭
delay_10us(50000);
}
}
main 函数内实现功能很简单, 在 while 循环内不断间隔一定时间点亮 LED1和熄灭 LED1, 这样即可实现 D1 指示灯闪烁。 细心的朋友可能会问, 前面delay_10us 函数形参为 1 时大约是 10us, 那现在实参传输 50000, 不应该是 500ms吗, 为什么注释写的是 450ms 呢? 这里还得回到刚才话题, 使用循环来延时只是获得一个大概的时间, 并不能精确, 如需精确延时, 后期我们会学习定时器。 此处就不用纠结这个问题。
9.4.2.1 通过 KEIL 软件仿真查看延时时间
上述代码中我们传递实参是 50000, 得到的延时大约是 450ms, 如何来验证呢? 可以通过 KEIL 自带的软件仿真功能, 操作如下:
①打开实验工程, 点击魔术棒, 选择"Target" 选项卡, 在 Xtal(MHz)文本框中输入 12M, 该值表示开发板上实际使用外部晶振大小, 如果开发板上使用外部晶振是 11.0592M, 则修改为对应值。 然后点击 OK。

②点击仿真按钮, 进入仿真界面, 如下所示:

③点击 RST 按钮, 重新复位系统参数, 此时参数列表中 sec 则为 0, 然后在所要查看调试的代码数字前面用鼠标左键双击即可出现"红色块" , 我们称之为断点。 如果再次双击, 即可取消该断点。 当点击红色标记 8 运行的时候就能直接运行到我们设置的断点处。

④当点击红色标记 8 运行时, 可以看到黄色箭头直接定位到 36 行代码, 也就是我们刚才设置的第一个断点位置, 此时参数列表中 sec 时间是 0.00039s。

⑤再次点击红色标记 8 运行时, 此时黄色箭头指向第 37 行代码, 此时 sec为 0.450601s。

将现在这个时间减去上一步的 sec 时间就可以得到 delay_10us(50000)运行的实际时间了。 再次点击仿真按钮则可退出仿真界面, 回到程序编辑界面。
KEIL 软件的仿真功能非常强大, 里面有很多功能, 包括在仿真时使用单步调试观察变量参数等都有, 这里就不过多介绍, 如果对这个感兴趣的可以自行百度搜索 KEILC51 软件仿真的使用。
9.4.3 LED 流水灯实验
9.4.3.1 使用移位和循环实现
如果要实现 LED 流水灯, 只需循环让 D1-D8 指示灯逐个点亮。 同样本实验也需要延时, 这个在前面已介绍, 这里就不多说。 要实现循环点亮, 可以使用最容易理解的方法: 点亮 D1 且把 D2-D8 熄灭, 延时一段时间后再点亮 D2 且把 D1、D3-D8 熄灭, 延时一段时间后再点亮 D3 且把 D1-D2、 D4-D8 熄灭, 如此循环, 这样就可以很简单的实现 LED 流水灯实验, 当然我们不推荐此种方法。 在前面我们学习了 C 语言相关的基础知识, 里面有移位以及循环语句操作, 根据流水灯实现原理, 即 IO 口由低往高或者由高往低逐个输出低电平特点, 那么我们可以将移位操作以及循环结合进来。 实现代码如下:
cpp
#define LED_PORT P2 //使用宏定义 P2 端口
void main()
{
u8 i=0;
while(1)
{
for(i=0;i<8;i++)
{
LED_PORT=~(0x01<<i); //将 1 右移 i 位, 然后取反将结果赋值到 LED_PORT
delay_10us(50000);
}
}
}
进入 main 函数后首先定义一个变量 i, 然后进入 while 循环, 由于要实现 8个 LED 从 D1->D8 循环点亮, 因此可以使用 for 循环语句循环 8 次, 每循环一次,点亮的小灯向右移动一个, 而 D1-D8 是连接到 P2.0-P2.7 的, 因此输出的低电平要左移一位, 因此可以使用 LED_PORT=~(0x01<<i);语句实现。 0X01<<i 表示 i
增加 1 次, 0x01 中的 1 就移动多少位, 因为 1(高电平) 不会让 LED 点亮, 需要取反后变为低电平 0 才能点亮, 所以最后的结果需要取反后给 LED_PORT 口, 并且每次循环都要延时一段时间, 这样才能分辨出来 LED 在流水。
9.4.3.2 使用左移_crol_、 右移_cror_函数
除了使用 for 循环语句实现移位, KEIL C51 软件内还有对应的移位库函数,左移函数是_crol_(), 右移函数是_cror_(), 要使用这两个函数在我们的程序中必须包含 intrins.h 头文件。 这两个移位函数大家可以百度了解下, 其内部实现过程是看不到的, 该移位函数实现的移位功能就相当于一个队列内循环移动, 如果是左移, 那么最高位就被移到最低位了, 次高位变为最高位, 依次类推。 使用左移、 右移函数实现的流水灯操作代码如下:
cpp
#include "reg52.h"
#include "intrins.h"
void main()
{
u8 i=0;
LED_PORT=~0x01;
delay_10us(50000);
while(1)
{
for(i=0;i<7;i++) //将 led 左移一位
{
LED_PORT=_crol_(LED_PORT,1);
delay_10us(50000);
}
for(i=0;i<7;i++) //将 led 右移一位
{
LED_PORT=_cror_(LED_PORT,1);
delay_10us(50000);
}
}
}
进入 main 函数后首先定义一个变量 i, LED_PORT=~0x01, 因为 LED 是低电平点亮, 所以 0X01 取反后的结果是 0XFE, 对应二进制数为 1111 1110, 即最低位为 0, 因此最开始的 D1 指示灯会点亮, 然后进入 while 循环, 使用 for 循环、_crol_和_cror_移位函数实现 LED 左右流水显示。
细心的朋友可能会发现此处每个 for 循环只有 7 次, 为什么不是 8 次呢, 这是因为在进入 main 开始, 就已经将 LED_PORT 端口设置了一次状态, 即让 D1 点亮, 并且我们是想让 LED 从左至右依次点亮, 然后继续又从右至左依次点亮, 这样形成左右流水效果。 假如将循环次数改为 8 次, 我们列举下第一个 for 循环的 LED_PORT 端口状态值, 如下所示:
初始状态: LED_PORT=1111 1110
i=0: LED_PORT=1111 1101
i=1: LED_PORT=1111 1011
i=2: LED_PORT=1111 0111
i=3: LED_PORT=1110 1111
i=4: LED_PORT=1101 1111
i=5: LED_PORT=1011 1111
i=6: LED_PORT=0111 1111i=7: LED_PORT=1111 1110
通过上述列举, 可以非常清楚的了解端口 LED 状态, 所以此处应该将循环次数设置为 7 次, 即 i 最大等于 6。 这样到下一个循环右移时就可以从最高位开始往低位移动, 从而实现左右流水灯效果。
9.5 实验现象
使用 USB 线将开发板和电脑连接成功后(电脑能识别开发板上 CH340 串口) ,把编译后产生的.hex 文件烧入到芯片内(程序的烧入具体操作步骤可参考"第3 章 开发板功能及使用介绍" 的程序烧入(下载) 章节) , 下载"\4--实验程序\1--基础实验\2-点亮第一个 LED" 实验程序, 可以看到开发板上 LED 模块 D1指示灯点亮。
下载"\4--实验程序\1--基础实验\3-LED 闪烁实验" 实验程序, 可以看到开发板上 LED 模块 D1 指示灯闪烁。
下载"\4--实验程序\1--基础实验\4-LED 流水灯实验" 实验程序, 可以看到开发板上 LED 模块 D1-D8 指示灯左右循环流水。