本章教你如何用单片机 AT89S52 的输入/输出接口来连接、测试和控制机器人伺服电机。
为此,你需要理解和掌握用单片机输入/输出接口控制伺服电机方向、速度和运行时间的相 关原理和编程技术。
C51单片机的输入/输出接口
控制机器人伺服电机以不同速度运动是通过让单片机的输入/输出(I/O)口输出不同的 脉冲序列来实现的。51 系列单片机有 4 个 8 位的并行 I/O 口:P0、P1、P2 和 P3。这 4 个接 口,既可以作为输入,也可以作为输出;可按 8 位处理,也可按位方式(1 位)使用。图 2-1 是单片机 AT89S52 的引脚定义图,这是一个标准的40 引脚双列直插式集成电路芯片。
图2-1 单片机AT89S52引脚I/O定义图
说到这里,你或许马上就会问,单片机如何知道它的引脚端口是作为输入还是输出呢?
这与单片机各I/O 接口的内部结构有关,而且每个 8 位并行 I/O 口的使用方式也不太一 样。后面的章节会根据机器人控制的需要逐步介绍它们的原理和使用方法。本章主要介绍如 何用P1 口来完成机器人伺服电机的控制。P1 口作为输出时,使用非常简单,可以直接对该 端口的位进行操作而不需额外设置,只需向该端口的各个位输出你想输出的高低电平信号即 可。
AT89S52引脚
如图2-1所示,AT89S52共有40根引脚,其中32根是I/O端口引脚。在这32根引脚
中,有29根具备两种用途(用圆括号写出),既可作为I/O端口,也可作为控制信号或地
址及数据线。
任务一 单灯闪烁控制
为了验证P1 口的输出电平是不是由你编写的程序输出的电平,可以采用一个非常简单 有效的办法,就是在你想验证的端口位接一个发光二极管。当你输出高电平时,发光二级管 灭;输出低电平时,发光二极管亮。
在本任务中,使用P1 端口的第一脚(记为 P1_0)来控制发光二极管以 1HZ 的频率不 断闪烁。
LED电路元件
1. 红色发光二极管2个 2. 470Ω电阻2个
LED电路搭建
如果你已经学习过《基础机器人制作》这本教材,你肯定对图2-2 所示电路很熟悉。按 照图2-2 上边所示电路,在智能机器人教学板的面包板上搭建起实际电路。实际搭建好的电 路参考图2-2 下边的照片。实际搭建电路时注意:
z 确认发光二极管的短针脚(阴极)插入通过电阻与 P1_0 相连 z 确认发光二极管的长针脚(阳极)插入“VCC”插口
图2-2 发光二极管与I/O脚P1_0的连接 例程:HighLowLed.c
z 接通板上的电源
z 输入、保存、下载并运行程序 HighLowLed.c(整个过程请参考第一章)
z 观察与 P1_0 连接的 LED 是否每隔一秒发光、关闭一次
#include<BoeBot.h>
#include<uart.h>
int main(void) {
uart_Init(); //初始化串口
while(1) 时函数:void delay_nms(unsigned int i)与 void delay_nus(unsigned int i)。
无符号整型数据 unsigned int
与第一章讲到的整型数据int 相比,无符号整型数据 unsigned int 只有一个区别:数据的 取值范围从-32768~+32767 变为 0~65535,也就是说它只能取非负整数。
delay_nms( )是毫秒级的延时,而 delay_nus( )是微秒级的延时。如果你想延时 1 秒钟,
可以使用语句 delay_nms(1000); 1 毫秒的延时则用 delay_nus(1000)来完成。
注意:上述的延时函数是在外部晶振为12MHZ的情况下设计的,如果外部晶振频率不
是12MHz,调用这两个函数所产生的真正延时就会发生变化。
晶振的作用
单片机要能工作,就必须有一个标准时钟信号,而晶振就是为单片机提供标准时钟信号。
uart_Init();串口初始化函数,在头文件 uart.h 中实现,具体内容将在后面章节讲解。
调用printf 是为了在程序执行前给调试终端发送一条提示信息,告诉你现在程序开始执 语句P1_0=1 完成,然后调用延时函数 delay_nms(500),让单片机微控制等待 500 毫秒,再 给P1_0 脚输出低电平,即 P1_0=0,然后再次调用延时函数 delay_nms(500)。这样就完成了 一次闪烁。在程序中,你没有看到P1_0 的定义,它已经在由 C 语言为 C51 开发的标准库中
while语句
while 语句的一般形式如下:
while(表达式) 循环体语句
当表达式为非0 值时,执行 while 语句中的内嵌语句,其特点是先判断表达式,后执行 语句。例程中直接用1 代替了表达式,因此总是非 0 值,所以循环永不结束,也就可以一直 让LED 灯闪烁。
注意:循环体语句如果包含一个以上的语句,就必须用花括号(“{ }”)括起来,以 复合语句的形式出现。如果不加花括号,则 while 语句的范围只到 while 后面的第一个分号 处。例如,本例中 while 语句中如果没有花括号,则 while 语句范只到“P1_0=1;”。
也可以不要循环体语句,如第一章例程中就直接用 while(1);程序将一直停在此处。
时序图简介
时序图反应的是高、低电压信号与时间的关系图。在图2-3 中,时间从左到右增长,高、
低电压信号随着时间在0V 或 5V 间变化。这个时序图显示的是刚才实验中的 1000ms 的高、
低电压信号片段。右边的省略号表示的是这些信号是重复出现的。
图2-3 程序 HighLowLed.c 的时序图 该你了——让另一个 LED 闪烁
让另一个连接到P1_1 管脚的 LED 闪烁是一件很容易的事情,把 P1_0 改为 P1_1,重新 运行程序即可。
参考下面的代码段修改程序:
uart_Init();
printf("The LED connected to P1_1 is blinking!");
while(1) {
P1_1=1; // P1_1 输出高电平 delay_nms(500); //延时 500ms P1_1=0; // P1_1 输出低电平 delay_nms(500); //延时 500ms }
运行修改后的程序,确定能让LED 闪烁。
你也可以让两个LED 同时闪烁。参考下面代码段修改程序:
uart_Init();
printf("The LEDs connected to P1_0 and P1_1 are blinking!\n ");
while(1) {
P1_0=1; // P1_0 输出高电平 P1_1=1; // P1_1 输出高电平 delay_nms(500); //延时 500ms P1_0=0; // P1_0 输出低电平 P1_1=0; // P1_0 输出低电平 delay_nms(500); //延时 500ms }
在进行下面的实验之前,你必须首先确认一下机器人两个伺服电机的控制线是否已经正 确的连接到了C51 单片机教学板的两个专用电机控制接口上。照图 2-7 所示的电机连接原理 图和实际接线图进行检查。如果没有正确连接,也请参照该图重新连接。从图2-7 可知,P1_0 引脚的控制输出用来控制右的伺服电机,而 P1_1 则用来控制左边的伺服电机。
显然这里对微控制器编程发给伺服电机的高、低电平信号必须具备更精确的时间。因为 单片机只有整数,没有小数,所以要生成伺服电机的控制信号,要求具有比delay_nms()
函数的时间更精确的函数,这就需要用另一个延时函数delay_nus(unsigned int n)。前面已经 介绍过,这个函数可以实现更小的延时,它的延时单位是微秒,即千分之一毫秒,参数 n 为延时微秒数。
看看下面的代码片断 while(1)
{
P1_0=1; //P1_0 输出高电平 delay_nus(1500); //延时 1.5ms P1_0=0; //P1_0 输出低电平 delay_nus(20000); //延时 20ms }
如果用这个代码段代替例程 HighLowLed.c 中相应程序片断,它是不是就会输出图 2-4 所示的脉冲信号?肯定是!如果你手边有个示波器,可以用示波器观察P1_0 脚输出的波形 是不是如图2-4 所示。此时,连接到该脚的机器人轮子是不是静止不动。如果它在慢慢转动,
就说明你的机器人伺服电机可能没有经过调整。
同样,用下面的程序片断代替例程HighLowLed.c 中相应程序片断,编译、连接下载执 行代码,观察连接到P1_0 脚的机器人轮子是不是顺时针全速旋转?
while(1) {
P1_0=1; //P1_0 输出高电平 delay_nus(1300); //延时 1.3ms P1_0=0; //P1_0 输出低电平 delay_nus(20000); //延时 20ms }
用下面的程序片断代替例程HighLowLed.c 中相应程序片断,编译、连接下载执行代码,
观察连接到P1_0 脚的机器人轮子是不是逆时针全速旋转?
图2-7 伺服电机与教学底板的连线原理图(左)和实际接线示意图(右)
{
P1_0=1; //P1_0 输出高电平 delay_nus(1700); //延时 1.7ms P1_0=0; //P1_0 输出低电平 delay_nus(20000); //延时 20ms }
z 输入、保存、下载并运行程序 BothServoClockwsie.c(整个过程请参考第一章)
z 观察机器人的运动行为
#include<BoeBot.h>
#include<uart.h>
int main(void) {
uart_Init(); //初始化串口
printf("The LEDs connected to P1_0 and P1_1 are blinking!\n ");
while(1)
for(表达式1;表达式2;表达式3) 语句
for(myCounter=1; myCounter<=10; myCounter++) {
myCounter ++的作用就相当于 myCounter = myCounter +1,只不过这样用起来更 简洁。这也是C 语言的特点,灵活简洁。
该你了――不同的初始值和终值以及计数步长
你可以修改表达式3 来使 myCounter 以不同步长计数,而不是按 9, 10, 11…来计,你可 以让它每次增加2(9, 11,13…)或增加 5 (10, 15, 20…)或任何你想要的步进,递增或递减都可 以。下面的例子是每次减3。
for(myCounter=21; myCounter>=9; myCounter=myCounter-3) {
delay_nus(1700)持续 1.7 ms,delay_nms(20)持续 20ms,其他语句的执行时间很少,可忽略。
那么for 循环整体执行一次的时间是:1.7 ms + 20 ms = 21.7ms,本循环执行 100 次,即就是 21.7ms 乘以 100,时间=100*21.7ms =100*0.0217 秒=2.17 秒。
假如你要让电机运行4.34 秒,for 循环必须执行上面两倍的时间。
z 输入、保存并运行程序 ControlServoRunTimes.c
z 验证是否与 P1_1 连接的电机逆时针旋转 2.17 秒,然后与 P1_0 连接的电机旋转 4.34 秒
#include<BoeBot.h>
#include<uart.h>
int main(void) {
int Counter;
uart_Init();
printf("Program Running!\n");
for(Counter=1;Counter<=100;Counter++)
20 ms – 中断持续时间
z 输入、保存并运行程序 BothServosThreeSeconds.c
#include<BoeBot.h>
#include<uart.h>
int main(void) {
int counter;
uart_Init();
printf("Program Running!\n");
for(counter=1;counter<=130;counter++) {
P1_1=0;
z 输入、保存、下载并运行程序 ControlServoWithComputer.c z 验证是否机器人各个轮子的转动是不是同你期望的运动一样
#include<BoeBot.h>
#include<uart.h>
int main(void) {
int Counter;
int PulseNumber,PulseDuration;
uart_Init();
printf("Program Running!\n");
printf(“Please input pulse number:\n”);
printf(“Please input pulse duration:\n”);
scanf(“%d”,&PulseDuration);
for(Counter=1;Counter<=PulseNumber;Counter++) {
P1_1=1;
delay_nus(PulseDuration);
P1_1=0;
delay_nms(20);
}
for(Counter=1;Counter<=PulseNumber;Counter++) {
P1_0=1;
delay_nus(PulseDuration);
P1_0=0;
delay_nms(20);
}
while(1);
}
ControlServoWithComputer.c 是如何工作的?
单片机通过串口从计算机读取输入的数据,需要用到格式输入函数:
scanf函数
scanf 函数与 printf 函数对应,在 C51 库的 stdio.h 中定义。下面是它的一般形式:
scanf(“格式控制字符串”,地址表列);
“格式控制字符串”的作用与 printf 函数相同,但不能显示非格式字符串,也就是不 能显示提示字符串。
地址表列中给出各变量的地址。地址是由地址运算符“&”后跟变量名组成的。如“&a”
表示变量 a 的地址。这个地址是编译系统在存储器中给变量 a 分配的地址,你不必关心具体 的地址是多少。
变量的值和变量的地址 这是两个不同的概念,例如:
a=123;
那么:a 为变量名,123 是变量的值,&a 则是变量 a 的地址。
scanf(“%d”,&PulseNumber);将会把你输入的十进制整数赋给变量PulseNumber。 程序运行过程(见图 2-8)如下:
图 2-8 例程运行过程
1. 首先输出“Program Running!”和“Please input pulse number:” 2. 程序处于等待状态,等待你输入数据
3. 将你输入数据给变量PulseNumber;
4. 输出“Please input pulse duration:” 5. 又处于等待状态
6. 将输入数据给变量PulseDuration 7. 电机运转
一次输入多个数据
当要求输入数据比较多时,上述方法是不是很麻烦?下面的代码可以让你一次输入两个 数据,两个数据之间用空格隔开:
printf(“Please input pulse number and pulse duration:\n”);
scanf(“%d %d”,&PulseNumber,&PulseDuration);
想一想,如果要输入三及以上数据,程序代码段该怎样写呢?
工程素质和技能归纳
1. C51 系列单片机的引脚定义和分布
2. 用 C51 单片机的 P1 端口的位输出控制单灯和双灯闪烁,时序图的概念,while 循
2. 用 C51 单片机的 P1 端口的位输出控制单灯和双灯闪烁,时序图的概念,while 循