概述
在这篇博客中,将介绍一个基于51单片机的舵机控制系统,其可以通过矩阵按键输入角度,舵机打到预定角度,并在数码管上显示。
硬件设计
这次的小系统电路都由洞洞板搭建,主要包括51单片机最小系统,矩阵按键,数码管驱动和舵机四个部分。Protues仿真电路图如下
-
数码管驱动
仿真测试程序数码管时可以直接用IO口驱动,但是实物搭建电路需要再接驱动电路,这次选择了74HC245芯片来驱动。这款芯片使用非常简单,在驱动显示屏,做隔离芯片时应用都非常多。它有A0-A7,B0-B7两组输入输出口,可以DIR管脚选择数据传输方向,在使用时如果不需要转换传输方向可以直接将此管脚接到芯片VCC或者GND。
-
矩阵按键
4*4的矩阵按键每一行每一列接在一个口上,通过行列扫描确定哪一个按键被按下,也就是导通。 -
舵机
舵机一般分为数字舵机和模拟电机。它们都由马达,减速齿轮组和控制电路组成。主要区别是数字电机有微处理器和晶振,能产生一周期为20ms,占空比为7.5%(即1.5ms高电平)的基准信号,通过输入与信号比较确定打角。而模拟电机则通过电位器产生的差分电压驱动电机正反转从而改变角度。这次选用的是SG90 180度的模拟舵机,价格低,带动一个激光头绰绰有余。通过单片机输出的周期为20ms的PWM波控制,在宽度从0.5ms到2.5ms,每一个占空比对应一个角度,示意图如下
程序设计
PWM波的产生
在51单片机中,没有可以直接调用产生PWM波的底层库,需要自己用定时器写。一般产生PWM波能用两个定时器写,每一个定时器只固定拉高或者拉低,靠时间差确定周期。也可以只用一个定时器写,通过改变装载值来反复改变电平。以下详细说明用一个定时器写的方式,另外一个定时器空余出来可以实现更多功能。
一个定时器产生PWM波需要两组装载值,分别控制高低电平时间。设置一个全局变量作为中断标志位确定用哪一组装载值,拉高还是拉低电平,每一次进入中断后改变标志位的值。舵机180度对应每周期高电平时间0.5到2.5ms,可调范围2ms,用12M晶振时也就是2000个机器周期长,得到装载值与角度关系系数为11.11,取11,再加0.5ms对应的500个机器周期,就能得到角度与高电平对应值的关系。
机器周期数=角度*11+500
*周期20ms减去高电平时间就能换算出低电平对应装载值。*如果想要更精确,系数和对应值可以用浮点型,在最后计算装载值时再强制转换成整型。
把这段程序单独贴出来
/*******************************************************
装载值计算函数
*******************************************************/
void zhuang(u8 jiaodu)
{
u16 zhuangza; //高电平对应机械周期数
zhuangza=jiaodu*11+500; //2000/180=11.11
H1= (65536-zhuangza)/256;
L1 = (65536-zhuangza)%256;
H2 = (45536+zhuangza)/256;//65535-(2000-高电平值)
L2 = (45536+zhuangza)%256;
}
/*******************************************************
定时器1中断服务函数
*******************************************************/
void Time1(void) interrupt 3
{
if(servoflag==0) //中断标志位
{
TH1 = H1;
TL1 = L1;
PWM=1;
servoflag=1;
}
else if(servoflag==1)
{
TH1 = H2;
TL1 = L2;
PWM=0;
servoflag=0;
}
}
矩阵按键功能与数码管显示参数传递
矩阵按键前10个值分别对应0到9。10为确定键,按下后在主函数中将按键读到的几个一位数转换成角度,传给装载值计算函数,改变PWM波占空比,舵机打到对应角度。另外设计了11,12,13三个键,按下后能使舵机直接打到程序预定好的角度。相应按键功能分配在主函数中。具体程序不再赘述。
为了使输入多位数时能正确显示,让数码管的每一位固定显示个、十、百位,对应数组的四个元素,初值不显示,输入低位后,接受按键值的元素移向高位。
完整程序
#include "reg52.h"
typedef unsigned int u16;
typedef unsigned char u8;
sbit LA=P2^4; //数码管位选
sbit LB=P2^1;
sbit LC=P2^2;
sbit LD=P2^3;
sbit PWM=P2^5; //PWM输出口
u8 code smgduan[17]={0x3f,0x06,0x9b,0x8f,0xA6,0xAd,0xBd,0x07,0xBf,
0xAf,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
//0到9,大于9不显示
u8 f=0;
static u8 jiaodu=0; //角度
u8 keyflag,servoflag; //按键标志位,中断标志位
u8 H1=0xFA,L1=0x24,H2=0xB7,L2=0xBC; //两组装载值
u16 zhuangzai;
u8 w[4]={16,16,16,16}; //按键输入值保存数组,初值16不显示
u8 KeyValue; //按键值
/*******************************************************
延时函数
*******************************************************/
void delay(u16 i)
{
while(i--);
}
/*******************************************************
定时器1初始化
*******************************************************/
void Timer1Init()
{
TMOD|=0X10;
TH1 = 0xFC;
TL1 = 0x18;
ET1=1;
EA=1;
TR1=1;
}
/******************************************************
装载值计算函数
*******************************************************/
void zhuang(u8 jiaodu)
{
u16 zhuangza;
zhuangza=jiaodu*11+500; //高电平对应值与角度换算
H1= (65536-zhuangza)/256; //H,L分别是装载值高、低8位
L1 = (65536-zhuangza)%256; //1是高电平装载值,2是低电平装载值
H2 = (45536+zhuangza)/256;
L2 = (45536+zhuangza)%256;
}
/*******************************************************
数码管动态扫描
*******************************************************/
void DigDisplay()
{
u8 i;
for(i=0;i<4;i++)
{
switch(i)
{
case(0):
LA=0;LB=1;LC=1;LD=1; P0=smgduan[w[0]];break;
case(1):
LA=1;LB=0;LC=1;LD=1; P0=smgduan[w[1]];break;
case(2):
LA=1;LB=1;LC=0;LD=1; P0=smgduan[w[2]];break;
case(3):
LA=1;LB=1;LC=1;LD=0; P0=smgduan[w[3]];break;
}
delay(10);
P0=0x00;
// delay(100); //不加亮度更高
}
}
/*******************************************************
矩阵按键扫描
*******************************************************/
void KeyDown(void)
{
char a=0;
P1=0x0f;
if(P1!=0x0f)
{
delay(1000);//消抖
if(P1!=0x0f)
{
keyflag=1;
//测试列
P1=0X0F;
switch(P1)
{
case(0X0e): KeyValue=0;break;
case(0X0d): KeyValue=1;break;
case(0X0b): KeyValue=2;break;
case(0X07): KeyValue=3;break;
}
//测试行
P1=0XF0;
switch(P1)
{
case(0Xe0): KeyValue=KeyValue;break;
case(0Xd0): KeyValue=KeyValue+4;break;
case(0Xb0): KeyValue=KeyValue+8;break;
case(0X70): KeyValue=KeyValue+12;break;
}
while((a<50)&&(P1!=0xf0)) //松手检测
{
delay(1000);
a++;
}
}
}
}
/*******************************************************
主函数
*******************************************************/
void main()
{
u8 i=0;
Timer1Init(); //定时器1初始化
while(1)
{
DigDisplay();
KeyDown();
if(keyflag==1)
{
w[i]=KeyValue;
i++;
keyflag=0;
}
if(KeyValue==10) //确定键
{
for(i=0;w[i]<10;i++)
jiaodu=jiaodu*10+w[i];
zhuang(jiaodu);
jiaodu=0;
}
if(KeyValue==12)
{
zhuang(87); //特定角度
}
if(KeyValue==13)
{
zhuang(97);
}
if(KeyValue==14)
{
zhuang(109);
}
}
}
/*******************************************************
定时器中断服务函数
*******************************************************/
void Time1(void) interrupt 3
{
if(servoflag==0)
{
TH1 = H1;
TL1 = L1;
PWM=1;
servoflag=1; //改变值,下次进入中断用另一组装载值
}
else if(servoflag==1)
{
TH1 = H2;
TL1 = L2;
PWM=0;
servoflag=0;
}
}
外加模块
为了增加系统的趣味性,可以在舵机上安装激光头,距离一定距离放置三个目标靶,将其角度写入程序,则可以通过特定按键打靶。还可以在目标靶上增加指示电路,是激光打到靶后指示灯亮,可以通过光敏电阻的简单电路实现。