初学51单片机基础知识与步进电机28BYJ-48详解

单片机的IO的三种状态分别是开漏、推挽、高阻态如图

准双向IO口前面的矩阵按键博文已经有所叙述。

开漏输出与准双向IO口的区别就是开漏输出把内部的上拉电阻去掉了。开漏输出如果要输出高电平,T2(相当于NPN三极管)mos管要关断,IO的电平要靠外部的上拉电阻才能拉成高电平。如果没有外部上拉电阻IO电平就是一个不确定态。

标准51单片机的P0口查数据手册默认就是开漏输出。

我们看到P1-P4是准双向口且是弱上拉,这个弱上拉的意思是上拉电阻比较大电流偏小比如50K

同理强上拉的意思就是上拉电阻较小电流偏大比如50R,甚至你不要这个电阻直接接电源就是强上拉。

(T1相当于PNP三极管,T2相当于NPN三极管)

强推挽输出就是有比较强的驱动能力,如果当内部输出一个高电平时,通过T1 mos管直接输出电流,没有电阻的限流,电流输出能力也比较大;如果内部输出一个低电平,那反向电流(T2导通IO口直接接地)也可以很大,强推挽的特点就是驱动能力强。

单片机IO还有一种状态叫高阻态。通常用来做输入引脚的时候,可以将IO口设置成高组态,高阻态引脚本身如果悬空,用万用表测量电压由可能是高也可能是低,它的状态完全取决于外部输入信号的电平,高阻态引脚对GND的等效电阻很大(理论上是无穷大,但又是有限值而非无穷大)。

上下拉电阻

       IO口设置为开漏输出高电平或者高阻态时,默认的电平就是不确定的,外部经过一个电阻接到VCC,也就是上拉电阻,那么相应的引脚就是高电平。同理经过一个电阻到GND,也就是下拉电阻,那么引脚就是一个低电平。

上拉电阻的应用

(1)OC/OD(Open Collector集电极开路,Open Drain漏极开路)门要输出高电平,必须外部加上拉电阻才能使用,其实就相当于单片机IO口的开漏输出

(2)加大普通IO口的驱动能力,标准51单片机的IO口的上拉电阻一般都是几十KΩ,STC89C52内部的上拉电阻是20K。以5V电平来说最大的输出电流是250微安,因此外部加个上拉电阻,可以形成和内部上拉电阻的并联结构,增大高电平时电流的输出能力。

(3)在电平转换电路中,起到限流电阻的作用

(4)单片机中未使用的引脚,比如总线引脚和引脚悬空时,容易受到电磁干扰而处于紊乱状态,虽然不会对程序照成什么影响,但通常会增加单片机的功耗,加上一个对VCC的上拉电阻或者一个对GND的下拉电阻,可以有效的抵抗电磁干扰。

上下拉电阻的选择

(1)从降低功耗的方面考虑应当足够大,因为电阻越大,电流越小。

(2)从确保足够的引脚驱动能力考虑应当足够小,电阻小了,电流才能大。

(3)在开漏输出时,过大的上拉电阻会导致信号上升沿变缓。如图

当然再理想的上升沿都不可能垂直向上,只能是近似,即上升的时间虽小但永远都达不到零。

综合考虑上下拉电阻的取值大多在1K~10KΩ之间。具体根据实际情况选择。

28BYJ-48型步进电机

步进电机分为反应式、永磁式和混合式三种。

(1)反应式步进电机:结构简单成本低,但是动态性能差效率低,发热大,可靠性难以保证,基本淘汰。

(2)永磁式步进电机:动态性能好、输出力矩较大、但误差相对来说大一些,因其价格低而广泛用于消费性产品。

(3)混合式步进电机:力矩大、动态性能好、步距角度小、精度高、但结构相对来说复杂,价格也相对较高,主要应用于工业。

28BYJ-48各参数的具体意思。

28——步进电机的有效最大外径是28毫米

B——表示是步进电机

Y——表示是永磁式

J——表示是减速型

48——表示四相八拍

然后是它的原理图。

        如果只看书,没拆过电机内部结构。估计对于教材书上的步距脚360°/(8*4) =11.25度怎么计算出来,有点纳闷甚至还要自己脑补。笔者的教材也没有具体的说明电机内部的机械结构,当然示意图仅在功能上确实是没有问题的,但是步进脚距这么重要数据就这么糊里糊涂的得出结果确实令人寒心,我不知道现在学生的教学质量怎么样,回顾笔者的求学经历,很多课程教材不说一塌糊涂更是男默女泪。对于山区的或者贫困的孩子他们大多只有一本教材没法看到实物,没法实践。这种弄出啦就是误人子弟。

       就以笔者来说,不怕各位笑话,在我小的时候关于变压器和通电螺线管的铜线它们就这么绕在一起,为什么电流还是沿着导线流的吗?电流的流向不应该四面八方360度的流的吗?它们都碰到一起了,他们不是导体吗?为什么老师一直说沿着导线流的?直到大学毕业我都没搞定这个问题?这种原则性问题搞不清楚,搞不通透可想而知我这门课学的稀巴烂,当然我现在知道了,因为外面有一层绝缘漆。它是漆包线它不是铜线,漆包线和铜线的区别他们在讲课的时候就这么理所当然的当作是一样的吗!?

当然各位可能会问你为什么不去问老师啊? 问的好!这是非常有意思的问题!就如胖猫最后选择自杀他如果现在还活着若干年以后估计也就笑笑觉得自己是SB,现在的我不能帮以前的我来做选择。

诸君共勉

闲话不再多说,关于步进电机的讲解笔者推荐几个视频。

什么是步进电机,步进电机的巧妙原理_哔哩哔哩_bilibili

51单片机第24讲-步进电机控制之28BYJ-48_哔哩哔哩_bilibili

步进电机28BYJ-48的结构、原理及控制(Arduino和ULN2003)_哔哩哔哩_bilibili

最好三个视频都看一下每个视频都有所得综合下,得到一个笔者觉得是正确的说法。当然笔者手上有电机但没拆开看是否真如笔者所说,各位自行决定。(声明:下面博文里面的图片来自视频

首先是它的定子结构

可以看到它的有很多小爪子,这些爪子都是导磁体,它的作用应该是类似铁芯的作用,一旦通电产生磁场,更多的磁力线就会往导磁体这边走。而且它是分两层的,上层线圈的分步16个爪极,上端8个下端8个爪极互相交错分布,同理下面的线包也是16个爪极。最后可知定子的爪极是32个。

用示意图表示就是

可以看到整个圆被分为了32段,可以清楚的看到它的步距是360/32 =11.25°,没有任何的花里胡哨的推理演算,它实际上就是这么朴实无华的看出来了。

然后就是它的接线,电机引出5根线除了一根电源线其他4根都是相线。通过示意图可知,每导通一相电,同一相的8个爪极都通电产生磁性,然后吸引中间的转子转动。可以这么理解但事实不是这样的简单。

先看一下它的线圈引线图

红线是正5V的电源接头,它也是两个线圈的中心抽头。1,3两个抽头是同一个线圈的,同理2, 4两个抽头是同一个线圈的。因为电源是在线圈中心抽头,如果同一线圈其他两个抽头分别接地的话它们产生的磁场是相反的。如图

也就是说对于该电机,如果A相电导通的话假设A相爪极是N极,那么同时C相的爪极就是S极,因为A、C相是同一个线圈的配合爪极。

如图

也就是只要有一相通电那么就有16个爪极有磁性。

同样的如果相邻两个相线通电的话比如AB相,那代表着上下线圈都有一半的绕组通了电,对于该电机来说就是A、B相爪极都是N极了C、D相爪极都是S极了,即36个爪极都有磁性了(应该是相应的爪极都有磁通线了)。

接着我们探讨下转子部分,转子是为圆柱形永久磁铁交替分布着N极和S级,极对数为8,即8个N极8个S级共16个磁极。且N极S极等极距。则可知每一磁极的极距为22.5°

如此定子和转子配合 如图示

现在来探讨下如果对步进电机每相通电步进电机的转动情况,假设A相通电A相是N极同时C相是S极则定子与转子配合如图,转子的带标记点的S磁极为观察磁极。

如果我们关闭A相导通B相则定子的磁极发生变化,AC相不在产生磁性,B相现在是N极同时D相是

S极。

可以看到带标记的转子S极从A相转到了B相这之间的步距是11.25°。以标记的转子S极来说它在转动过程(A转到B)中受到两股力的作用,一是B相的吸引力(向B靠近)二是D相的排斥力(推动离开A相向B靠近),当转到转距最小的位置时受力平衡转子将不再转动,此时转动的距离就是A相到B相的步距。

同样的C相通电,C相爪极是N极同时A相爪极是S极。

然后是D相

最后又回到A相

可以看到转子从起点A相经过4相通电后它又回到了A相,它转过的步距是 45度 

一步我们称为一节拍,刚才的操作有个名字叫

单向绕组通电4节拍,显然转一圈要32节拍。每步以及每节拍的步距就是360/32 =11.25 度。

再来讲解一种更优性能的工作模式,那就是在单四拍的每两个节拍之间再插入一个双绕组导通的中间节拍,组成8拍模式。即A -- AB -- B -- BC -- C -- CD -- D -- DA -- A   

A相通电

AB相通电

B相通电

可以看到A相到AB相的步距是之前的一半即11.25/2=5.625度,AB相到B相的步距也是之前的一半即5.625度。 依此类推可知8拍也只转了45度,若想转一圈它的节拍数是8x8=64节拍。这样一来不仅使转动的精度增加了一倍,新增加的中间节拍还增加了电机的整体扭力输出,使电机更“有劲”了。一般来说我们都采用这种8节拍工作模式控制步进电机。

统一下相线线颜色和相线爪极称呼。A(4)相橙色, B(3)相黄色 ,C(2)相粉色, D(1)相蓝色,                        

则它的控制顺序是

然后看一下该电机的其它参数;

可以看到启动频率是大于等于550,单位是P.P.S,即每秒脉冲数。电机保证在每秒给出550个步进脉冲的情况下可以正常启动。换算成单拍的持续时间就是1/550 = 1.8ms  按照逻辑来说启动频率是要大于550,那么持续时间取值应该是小于1.8ms。但这里的意思是大于1.8ms,我们就从实际出发,如果说它是要求的持续时间小于1.8ms,那如果把这个值取的非常的小无限趋于0,显然是不符和现实,笔者觉得这种书写方式可能是一种以前留下的写法,它本质应该是表达大于多少时间,但是在表示的时候又用了频率表示,可能是一种行业约定俗成。所以该数据是,P.P.S要小于等于550,启动时间要大于1.8ms。

本案在电机控制的时候使用了跳线

可以看到只要Q2导通,电机的相电线圈就导通接地,开始产生磁场。要想Q2导通MC0要输出低电平,而MC0就是P1.0端口。

根据实际硬件设计5p插头的2-5的电子线颜色是橙黄粉蓝即

根据之前的步进控制顺序它的步进控制编码是

BeatCode[8] = {   0xE,0xC,0xD,0x9,0xB,0x3,0x7,0x6    }; (A相)0xE = 1110  (AB相) 0xC=1100  (B相)0xD=1101依次类推。

上代码

# include<reg52.h>

unsigned long beats = 0;

void StartMotor(unsigned long angle);

void main()
{
    EA = 1;
	  TMOD = 0x01;
	  TH0 = 0xFB;   //定时2ms
	  TL0 = 0xCD;
	  ET0 = 1;
	  TR0 = 1;
	
	  StartMotor(360*25); //转25圈
	  while(1);
}


void StartMotor(unsigned long angle)
{
 //
	EA = 0;
	beats = (angle*4096)/360;//计算步数
	EA = 1;
}

void InterruptTimer0() interrupt 1
{
  unsigned char tmp;
	static unsigned char index = 0;
	unsigned char code BeatCode[8] = {
	  0xE,0xC,0xD,0x9,0xB,0x3,0x7,0x6
	};
	
	TH0 = 0xFB;
	TL0 = 0xCD;
	if(beats != 0)
	{
	  tmp = P1;
		tmp = tmp & 0xF0;//保持高位不动,低位清0
		tmp = tmp | BeatCode[index];//端口控制取值
		P1 = tmp;
		index++;
		index = index & 0x07;
		beats--;
	}
	else
	{
	  P1 = P1 |0x0F; //当步数为0,4个端口置1,遂停止
	}
	
	

}

步进电机转动_哔哩哔哩_bilibili电机转动示意

步进电机开始转动,25圈后它停止转动。前文说了该步进电机是个减速型步进电机减数比是

1:64,是因为里面有齿轮结构。如图

看过该步进电机教材应该都看过这个图,理论上转子转动64圈,电机轴才转动一圈,而转子转一圈要经过64拍,即4096拍步进电机转一圈。但是实际是不是这么理想的,根据齿轮的齿数计算是4076拍。即实际的转一圈的拍数是4076拍。因此程序计算拍数的算式要改动一下4096改成4076

beats = (angle*4096)/360;//计算步数,其实笔者也是倾向于这个式子的,但是事与愿违,笔者在测试的过程中发现4096才是正确的节拍,也就是是笔者手上的步进电机是准确的64:1不是近似值

笔者经过多次测量结果皆是如此。初始位置

4096拍(25圈)

4076拍(25圈)

可以看到4076拍少转了φ角度。

当然笔者不保证各位读者手上的步进电机是否如此,但是笔者手上的步进电机是如此的,倘若是电机误差,那误差的步数也太凑巧了。

当然也许是步进电机的工艺进步了,电机已经能准确做到64比1了。这是个不错的猜想,倘若各位手上有电机最好自己都去试一下,而不是套用这个结论。

然后我们探讨一下如果单项绕组通电能否驱动电机工作

这是控制驱动的数组为BeatCode[4] = {0xE,0xD,0XB,0x7};即单向绕组通电4节拍,经过笔者测试电机在震动,但是无法带动电机转轴转动。有可能是4拍驱动的扭力不够,无法通过齿轮组带动电机轴转动。

同时发散一下思维,如果把原先的8拍删掉一个中间拍数即变成7拍,电机是否还能够转动?

经过测试无法带动电机,这个其实也是显而易见的,4节拍的扭力不够,8节拍抽掉一节拍,相当与其中一节拍的扭力与4节拍的一样,当该拍的扭力不够电机转子就无法带动齿轮转动就会一直卡住不动,因此电机一直在震动,电机转轴却不动。

前一篇博文我们用矩阵按键实现了简单的数学运算,这篇我们用矩阵按键来控制电机的转动。

数字1-9控制电机转的圈数。 向上键控制正转,向下键控制反转,向左键固定正转90°,向右键固定反转90°,ESC停止转动,空格键一直转动。

#include<reg52.h>


sbit KEY_IN_1 = P2^4; 
sbit KEY_IN_2 = P2^5; 
sbit KEY_IN_3 = P2^6; 
sbit KEY_IN_4 = P2^7; 

sbit KEY_OUT_1 = P2^3;
sbit KEY_OUT_2 = P2^2;
sbit KEY_OUT_3 = P2^1;
sbit KEY_OUT_4 = P2^0;





unsigned char code KeyCodeMap[4][4] = { //矩阵按键编号到标准键盘键码的映射表
   { 0x31,0x32,0x33,0x26},             //数字键1,数字键2,数字键3,向上键
	 { 0x34,0x35,0x36,0x25},             //数字键4,数字键5,数字键6,向左键
	 { 0x37,0x38,0x39,0x28},             //数字键7,数字键8,数字键9,向下键
	 { 0x30,0x1B,0x0D,0x27}              //数字键0,ESC键,  回车键, 向右键
  };


unsigned char KeySta[4][4] = {                         //全部按键当前态
    {1,1,1,1},
		{1,1,1,1},
    {1,1,1,1},
    {1,1,1,1}
}; 
signed long beats = 0;
 bit run = 0;    //定义持续转动的标记

void KeyDriver();  //按键驱动函数声明

//void KeyAction(unsigned char keycode);
//void KeyScan();



void main()
{


   EA = 1;               //中断使能
	TMOD = 0x01;         //设置T0为模式1
	TH0 = 0XFC;         //定时1ms
	TL0 = 0x67;
	ET0 = 1;           //启动定时器0中断
	TR0 = 1;
	
	while(1)
	{
	
	 KeyDriver(); // 调用按键驱动函数
	}
        
}

/*步进电机启动函数,angle为需转过的角度 */	
void StartMotor(signed long angle)
{
  //
	EA = 0;
	beats = (angle * 4096)/360;
	EA = 1;

}

/*步进电机停止函数 */	
void StopMotor()
{
  EA = 0;
	beats = 0;
	EA = 1;
}

	
/*按键动作函数,根据键码执行相应的操作,keycode-按键键码 */	

void KeyAction(unsigned char keycode)		
{
  static bit dirMotor = 0;  //定义电机转动方向
	
	
	if((keycode >= 0x30) &&(keycode <= 0x39))
	{
	  if(dirMotor == 0)
			StartMotor(360*(keycode-0x30));
		else
			StartMotor(-360*(keycode - 0x30));
	}
	else if (keycode == 0x26)  //向上键,控制转动方向正转
	{
	  dirMotor = 0;
	}
	else if (keycode ==0x28)  //向下键,控制转动方向为反转
	{
	  dirMotor = 1;
	}
	else if (keycode ==0x25) //左键,固定正转90度
	{
	  StartMotor(90);
	}	
	else if (keycode ==0x27) //向右键,固定反转90度
	{
	  StartMotor(-90);
	}
	else if (keycode == 0x1B) //ESC 停止转动
	{
	 StopMotor();
     run = 0;
	}
	else if (keycode == 0x0D ) // 一直转动
		
	{
		if(dirMotor == 0)
	    StartMotor(90);
		else
			StartMotor(-90);
		run = 1; //为1就一直转
	}
	
}





/*按键驱动函数,检测按键动作,调度相应动作函数,需要在主循环中调用 */
	void KeyDriver()
	{
	unsigned char i,j ;
	static unsigned char PastSta[4][4] ={
	 {1,1,1,1},                               //按键值备份,保存前一次的值(前一稳态)
	 {1,1,1,1},
	 {1,1,1,1},
	 {1,1,1,1}
	}; 
  
			
     for(i=0; i<4; i++)              //循环检测4*4的矩阵按键,i是行 j是列
		   {
			   for(j=0; j<4; j++)
				    {
						 if(PastSta[i][j] != KeySta[i][j])  //检测按键动作
						  {
						   if(PastSta[i][j] != 0)          //按键前一稳态不是0即按键前一稳态是1即现稳态是0,即按住开关触发
							  {
							   KeyAction(KeyCodeMap[i][j]);       //调用按键动作函数
							  }
						   PastSta[i][j] = KeySta[i][j];  //把现稳态赋值给前态
						  }
						}
		    }
		}
		

/*按键动作扫描函数,需要定时中断调用,间隔1ms */	
void KeyScan()
{
  
	  unsigned char i;
		static unsigned char keyout = 0;          //矩阵按键扫描输出索引
		static unsigned char keybuf[4][4] = {    //矩阵按键扫描缓冲区,16个建初始都是0xff则说明全部处于弹起状态
		{0xFF,0xFF,0xFF,0xFF},
		{0xFF,0xFF,0xFF,0xFF},
		{0xFF,0xFF,0xFF,0xFF},
		{0xFF,0xFF,0xFF,0xFF}
		
		};
		
		//将一行4个按键值移入缓冲区
		keybuf[keyout][0] = (keybuf[keyout][0]<<1) | KEY_IN_1;
		keybuf[keyout][1] = (keybuf[keyout][1]<<1) | KEY_IN_2;
		keybuf[keyout][2] = (keybuf[keyout][2]<<1) | KEY_IN_3;
		keybuf[keyout][3] = (keybuf[keyout][3]<<1) | KEY_IN_4;
		//消抖后更新按键状态
		for(i = 0; i<4; i++)          //每行4个按键,所以循环4次
		{
		  if((keybuf[keyout][i] & 0x0F) ==0x00)
			{  //连续4次扫描值为0,即4*4ms内部都是按下的状态时,可以认为按键已稳定的按下
			  KeySta[keyout][i] = 0;
			}
		  else if((keybuf[keyout][i] & 0x0F) ==0x0F)
			{ //连续扫描4次扫描值为1,即4*4ms内部都是弹起状态,可认为按键已稳定的弹起
			  KeySta[keyout][i] = 1;
			}
			//else{}
		}
		keyout++;                                    //输出值索引递增
		keyout = keyout & 0x03;                      //索引值加到4即归零
		switch(keyout)                                //根据索引,释放当前输出引脚,拉低下次的输出引脚
		{
			case 0:KEY_OUT_4 = 1; KEY_OUT_1 = 0; break;  
			case 1:KEY_OUT_1 = 1; KEY_OUT_2 = 0; break;
			case 2:KEY_OUT_2 = 1; KEY_OUT_3 = 0; break;
			case 3:KEY_OUT_3 = 1; KEY_OUT_4 = 0; break;
			default : break;
		
		
		}
	
}

/*电机转动控制函数 */	
void TurnMotor()
{
  unsigned char tmp;
	static unsigned char index = 0;
	unsigned char code BeatCode[8] = {0xE,0xC,0xD,0x9,0xB,0x3,0x7,0x6};
	
	if(beats != 0)
	{
	  if(beats > 0)
		{
		  index++;
			index = index & 0x07; //用&操作实现到8归零
			beats--;
			if(run == 1)         //如果按下空格则节拍数不变的即一直转下去
			{
			beats++;
			}
		}
		else                    //节拍数小于0反转
		{
		  index--;
			index = index & 0x07; //用&操作同样实现-1时归7
			beats++;              //反转时节拍计数递增
			if(run == 1)
			{
			beats--;
			}
		}
		tmp = P1;
		tmp = tmp & 0xF0;
		tmp = tmp | BeatCode[index];
		P1 = tmp;
	}
	else
	{
	 P1 = P1 | 0x0F;
	}
}



/* T0中断服务函数,用于数码管显示扫描与按键扫描 */

void interruptTimer0() interrupt 1
{
	static bit div = 0;
	
  TH0 = 0xFC;    //重载初值
	TL0 = 0x67;
	KeyScan();   //执行按键扫描每1ms执行一次
	div = ~div; //用1个静态bit变量实现二分频,即定时2ms
	if(div == 1) 
	{
	 TurnMotor();  //每2ms执行一次函数控制
	}
}
	

矩阵按键控制步进电机转动_哔哩哔哩_bilibili

这里面几个要点解释一下

本案使用的51单片机,在操作数据时都是按8位即1个字节运行的,那么要操作多个字节(无论读还是写)就必须分多次进行了,程序中的beat的变量定义的是unsigned long型是4个字节,那么对它的赋值也要分4次完成,想象一下,假如在完成第一个字节的赋值后,恰好中断发生了,interrupt Timer0()函数得到执行,而这个函数可能会对beats进行减1操作,减法就有可能发生借位,借位就会改变其他的字节,但因为此时其他的字节还没有被赋入新值,于是错误发生了。

所以要避免这种错误的发生就得先暂时关闭中断。等赋值结束后再打开中断。 而如果使用的是char或者bit型变量的话,就不需要,因为它在CPU中是一次操作完成的,所以即使不关断中断,也不会发生错误。

但事实笔者在测试过程中还是发生了错误,但是不知错误怎么产生的,即按下1键,电机没有转1圈就停止了,而且是发生在刚开机的时候。当然不是每次都这样是偶尔,显然偶尔问题才难找。

运行出错_哔哩哔哩_bilibili

负数在C语言中是用补码储存的,-1的补码是1111 1111因此该&操作的结果是0x07本身。

梳理下程序工作流程:

结语:尽信书,则不如无书。也许有很多优秀的教材在它出现的那个年代是不错的指导,但是随着时代的变化,当初的研究对象可能都已经发生了变化,然而教材却一成不变。引用它的人不去验证,曾经的闪光点都可能变成最大的错误。

笔者这篇发了点牢骚,主要是教材上的内容突然看不懂了,看不懂就算了只要是对的就行,结果货不对板那笔者有点忍不了。

  • 28
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值