如何用51单片机控制舵机加减速

本文最后更新于:2020年10月12日 晚上

本文介绍使用如何51单片机控制舵机转动、加减速。

背景

这边文章是2016年上大学的时候写的,当时还不太懂做一个自己的网站,这篇文章写在了新浪博客上,排版阅读体验极差,重新把他整理处理出来,我的网站SEO还行,希望帮助更多人吧。

关于舵机,网络和书本上,很难找到详细的资料,我在2015年准备中国工程机器人暨国际公开赛的时候,做了一个采用舵机驱动的车型搬运机器人。由于当时知识有限,使用90系列51单片机,无法生成满足舵机精度要求的PWM,导致一个舵机的控制问题花费了近半年时间,并没有按照预期参加比赛(这个也真的不怪谁,不走个弯路)。

关于舵机的控制,我查询过一些资料,也请教过实验室的老师,但是结果都很不满意。舵机乱转,转速控制不精确,出现过各种问题。最终,硬是通过砸钱和砸时间(20块一个的PWM调制器不知道烧了多少- -!这个反正烧的不是我的,不心疼,但是90块一个舵机,加上60块一个的航模PWM舵机调制器还是砸了几百块进去的),和小伙伴通过实验,反复测试得到的结果,在这里分享出来,希望能帮助到感兴趣的铁子们。

舵机VS减速电机

我们知道,普通的减速电机也能转,舵机也能转,那这两个玩意儿有什么区别?

舵机,按照转动的方式,可以分为180°和360°连续旋转舵机。

  • 180°舵机,顾名思义,就是只能旋转180°的舵机,我们可以通过PWM,精确的控制舵机旋转的角度。舵机从0°转到180°就不能继续转了,只能往回去转。另外,180°舵机无法调速,就是说,他始终会以最快速度,朝着你设定的角度旋转,直到旋转到目标角度刹停。因此,180°舵机在航模(控制飞机机翼)、双足机器人等很多场景中都有使用。

  • 360°连续旋转舵机,顾名思义,就是可以朝着顺时针或者逆时针的方向,按照一定的速度一直转。可以通过PWM,控制舵机的旋转方向和速度,因此,也常常用于车型机器人的驱动上。360°舵机是无法控制旋转的角度的,只可以控制转和停、正转和反转以及转速。


    360°舵机

减速电机,其实类似于360°连续旋转舵机,也常用小车的动力,就通电就一直转呗,断电就停呗。一般在某宝上面买的什么寻迹机器人,都是使用的减速电机。


减速电机

减速电机和舵机的区别是:

  • 硬件上:由于舵机内置有电路,因此我们做机器人设计电路的时候,仅需要设计好阴阳极和PWM信号输入的接口,非常简单。而普通的减速电机在设计电路的时候,则需要考虑到驱动芯片,就比较复杂了。淘宝上有现成的l298n驱动模块,买回来可以直接用了。
  • 控制难度上,舵机对于PWM的精度要求非常高,而电机相对就不高。
  • 价格方面:通常来说,舵机的价格是普通减速电机的很多倍,就记得之前带一个中学的学生参加机器人比赛,结果土豪学校用的舵机是300块一个的磁传感舵机。一个机器人,四个轮子就1200出去了,真的有钱。
  • 转动效果上:减速电机转速高,扭矩小(说人话,如果车轻,跑的飞快,车重就跑不动)。舵机通常转速一般,扭矩大(说人话,车重车轻影响不大,始终能以一个固定的最大速度跑)。
  • 个人认为最核心的差别:舵机是带刹停的。什么意思?如果减速电机需要停止转动,那就断电呗。但是假如一个小车在斜坡上,那减速电机就很难让小车刹住,断电就滑下去了嘛。反观舵机就比较容易实现了,只需要给一个固定的PWM就能刹住了。因为舵机的停,实际上是通电的,舵机本身是有力量保持住停止的这个状态的。快速响应的拖刹,给定高电平1.5ms的pwm,舵机会立刻刹住以及其充沛的动力这一点是减速电机做不到的。

PWM

上一段中,我提到可以通过PWM,控制舵机转动。咱不做百度百科的搬运工,复制一堆看不懂的东西去解释概念。这里,只要知道,控制舵机,输出一个周期为20ms-30ms(最好20ms),高电平为0.5ms-2.5ms的信号即可,这个信号具体控制舵机的范围如下:

  • 对于180°舵机而言,PWM高电平0.5-2.5ms每一个值对应一个角度,比如0.5ms对应转动到0°的位置,那1.5ms就对应转动到90°的位置,那2.5ms就对应转动到180°的位置。
  • 对于360°舵机而言,PWM高电平0.5-1.5ms为正转区间,1.5-2.5ms为反转区间,1.5ms为舵机刹车点,调速的区间通常在1.5ms左右0.1ms宽度的范围内。举个例子,我使用的舵机,高电平0.5ms-1.37ms为以最大速度正转,正转的调速高电平区间为1.37ms-1.47ms(高电平越靠近1.5ms,舵机转速越慢),高电平1.53ms-2.5ms为以最大速度反转,反转调速高电平区间为:1.53-1.63ms(高电平越靠近1.5ms,舵机转速越慢),刹车高电平区间为:1.47-1.53ms。

PWM示意图

关于硬件上的设计,无非是保证供电的稳定和预留出PWM的接口,这里就不详细说明了,这里附上一张原理图,感兴趣的小伙伴可以研究一下。如果舵机反向电流很大,建议加上光耦隔离。


舵机控制原理图

STC90系列能控制360°舵机吗?

这里对360°舵机如果通过51单片机的程序进行控制做详细的说明。如何使用51单片机产生我们需要的PWM?用定时器嘛!当然,我一开始也是这么想的,看看下面这段程序:

#include <reg51.h>

unsigned int i=1;
int n;

void ste360(n){
  TMOD=0X01;
  TL0=0XF6; // 0.01ms
  TH0=0XFF;
  TR0=1;
  EA=1;
  ET0=1;
  P1=0X00; 
	while(1);
}

void _pwm() interrupt 1 {
  TH0=0xff; // 0.01ms
  TL0=0xF6;
	i++;

	if(i<=n){
		P1=0XFF; // 1.37<<1.47-1.54<<1.63
	} else P1=0x00;

	if(i>=2000){
		i=1;
	}

}

看起来程序好像没有什么问题,ste360(n)这个函数,n就可以控制pwm高电平的时间,我设定定时器为每0.01ms进入一次中断,所以,要想得到1.5ms高电平,是不是n填写150就行了?答案是否定的,虽然程序逻辑是没有问题的,计算出来确实是1.5ms,但是现实中,很有可能是高电平为3ms+,为什么?这个问题困惑了我挺久,我和一位学长也讨论过,他认为:编译过程中,会将我们写的c文件,编译成汇编文件,然后转成机器语言,这个过程中可能会有误差。而我觉得,主要是晶振本身是有误差,其次,这样反复的进入中断,细小的误差会被无限的放大。最后,可能STC90系列的单片机设计有缺陷?我也不确定。因为同样是使用c,stm32上pwm的精度高的离谱。所以,使用STC90系列单片机(所有只有8位定时器的51单片机)来控制舵机,在我看来,是完全不可行的(我在很长一段时间里,极力优化代码,尝试过各种思路,但是结果只能说有提升,但是远远达不到控制舵机的要求)。当然了,还是要有点朋克精神,铁子们也可以自己写点小程序,打在示波器上看看,STC90系列的单片机能不能生成满足精度要求的PWM。

STC12系列能控制360°舵机吗?

不能用定时器,那怎么办?stc12系列的51单片机是自带8位pwm波形发生器,是不是可以用12系列的单片机实现?看程序:

#include <reg51.h>

sfr CCON=0xD8;
//CF CR - - | - - CCF1 CCF0
//-----------------------------
//CF:PCA计数阵列溢出标志.计数值翻转时由硬件置位。
//CR:PCA计数阵列运行控制位。
//CCF1:PCA模块1中断标志。当出现匹配或者捕获时由硬件置位。
//CCF0:PCA模块0中断标志。当出现匹配或者捕获时由硬件置位。
           /
sfr CCAP0L=0xEA;//PCA模块0的捕捉/比较寄存器低8位
sfr CCAP0H=0xFA;//PCA模块0的捕捉/比较寄存器高8位

sfr CCAPM0=0xDA;//PCA模块0的工作模式寄存器
//---------------------------------------
//7   6   5     4     3   2    1    0
//- ECMn CAPPn CAPNn MATn T0Gn PWMn ECCFn
//----------------------------------------
//ECOMn:使能比较器,1时使能比较器功能
//CAPPn:正捕获,1时使能上升沿捕获
//CAPNn:负捕获,1时使能下降沿捕获
//MATn:匹配:1时,PCA计数器的值与模块的比较/捕获寄存器的值匹配将置位CCON寄存器中断标志位CCFn
//T0Gn:翻转,1时,工作在PCA告诉输出模式,PCA计数器的值与模块的比较/捕获寄存器的值匹配将是CEXn脚翻转
//PWMn:脉宽调节输出模式,1时,使能CEXn脚用做PWM输出
//EECFn:使能CCFn中断,使能寄存器CCON中的捕获/比较标志CCFn,用来产生中断
sfr PCA_PWM0=0xF2;//PCA模块0,PWM寄存器
//-----------------------------------
//        7 6 5 4   3 2   1    0   
//PCA_PWMn:- - - - | - - EPCnH EPCnL
//-----------------------------------
sfr CCAP1L=0xEB;
sfr CCAP1H=0xFB;
sfr CCAPM1=0xDB;
sfr PCA_PWM1=0xF3;//
sbit CR=0xDE;//因为只有能和8整除的才能位寻址,所以能些0xDE,看起来有冲突,实际上不会。
sfr AUXR1=0xA2;//PWM引脚位置 串口2位置 双DPTR选择 AD转换结果存放方式调整 SPI位置调整

void ini_T0(void)
{
    TMOD=0x02;//T0方式2
    TH0=0xb2; //12MH时
    TL0=0xb2;
    TR0=1;
}

//频率为50HZ 周期的1/50=0.02s,将0.02S分成256分:0.02/256=0.000078125S=0.078125ms
//0.078125为一份的时间 一共256份
//CCAP0L=223;CCAP0H=223;为2.5ms //243时约为1ms (0.9375)
//CCAP0L=249;CCAP0H=248;为0.5ms //236时为1.5ms (1.484375)
                                //230为2ms (2.03125)

void main(void)
{
   ini_T0();  //方式2,0.078125ms溢出,每溢出一次CL加1
    CMOD=0x04;//定时器0溢出率作为时钟输入
    //CIDL - - -  CPS2 CPS1 CPS0 ECF
    //--------------------------------
    //CIDL:计数阵列空闲控制,0时,空闲模式下PCA计数器继续工作;1时空闲模式PCA停止工作。
    //----------------------------------------------
    //CPS2 CPS1 CPS0: PCA计数脉冲选择
    //000:系统时钟,FOSC/12
    //001:系统时钟,FOSC/2
    //010:定时器0的溢出,可实现可调频率PWM输出
    //011:ECI/P3.4脚的外部时钟输入(最大速率FOSC/2)
    //100:系统时钟,FOSC
    //101:系统时钟/4,FOSC/4
    //110:系统时钟/6,FOSC/6
    //111:系统时钟/8,FOSC/8
    //-----------------------------------------------
    //ECF:PCA计数溢出中断使能:1时,使能寄存器CCON CF位的中断。0时禁止该功能。
    CL=0x00;//清零自由递增计数的16位定时器的值
    CH=0x00;//CH0为00  看结构图,CL前面是永远是0
  
    //CCAP0L=223;//装入比较初值
    //CCAP0H=223;
    PCA_PWM0=0x00; //EPC0H=0,EPC0L=0
    CCAPM0=0x42;//设置ECOM1=1,PWM1=1.
  
    //CCAP1L=223;
    //CCAP1H=223;
    PCA_PWM1=0x00;//EPC1H=0,EPC1L=0
    CCAPM1=0x42;//设置ECOM1=1,PWM1=1.
    CR=1;//CR=1,启动PCA阵列计数。  看151页,其中和CMOD的CIDL位有关,又和IDLE有关,看183页。
  
  
    //AUXR1=0xc0;//PWM0从P1.3切换到P4.2
       //PWM1从P1.4切换到P4.3
    while(1)
    {
    		//========================两舵机停止,中位调节用
        CCAP0L=236;//装入比较初值
        CCAP0H=236;
        CCAP1L=230;//装入比较初值
        CCAP1H=230;
        while(1);
    }
}

至于怎么调整pwm的高电平,修改上面CCAPXL和CCAPXH就可以了。是不是大功告成了呢?并没有!测试中我在示波器上发现,CCAPXH和CCAPXL的值每改变1,PWM高电平改变0.07ms左右。那么问题来了,在前面我提到过,360°舵机调速的区间,只有0.1ms(1.47-1.37=0.1),这个0.07,是可以让舵机实现正转反转,以及停止,但是没办法调速,循迹小车是需要控制车子两侧的轮子产生转速差来控制方向的,没法调速,就没法实现循迹。造成这个问题的原因是什么?8位的pwm模块,8位的pwm,上面程序中的注释中有这样一段:

//频率为50HZ 周期的1/50=0.02s,将0.02S分成256分:0.02/256=0.000078125S=0.078125ms

//0.078125为一份的时间 一共256份

很清楚了吧!频率50HZ就是周期为20ms的PWM,8位的计数器,最大值是256,精度只能到0.07ms左右。

51单片机还能控制360°舵机吗?

那么是不是51单片机控制不了360°舵机呢?当然还是可以控制的,关键要选择一款好芯片。我最终发现了STC15W4K32S4系列单片机,这个系列的芯片带6路15位的PWM波形发生器,也有16位的定时器,自动重装模式能够实现8~16位的PWM。真的挺服STC的产品线的,他还有一个STC15系列单片机,不带PWM波形发生器。我真以为它15打头的全系都不带PWM波形发生器。

具体的程序在这里我就不列出来了,STC15W4K32S4自带pwm波形发生器的示例代码在官方文档的1056页,自动重装模式实现8~16位PWM的示例代码在官方文档的991页。

//对于15位PWM波形发生器而言,频率为50HZ 周期的1/50=0.02s,将20ms分成32768份:20/32768≈0.0006ms

//0.0006为一份的时间 一共32768份

所以,15位的PWM波形发生器,精度足够生成能够控制舵机的PWM了。

舵机供电很重要

前面所述,360°舵机的控制基本上能通过STC15W4K32S4系列单片机或者其他带15、16位PWM波形生成器的单片机解决,其实180°舵机相似,程序都是一样的。但是,尤其需要注意一下供电的问题,舵机要求的电压为6v-7.2v,你也许会发现5v其实舵机也能转,但是,低电压会带来很多未知的问题,比如舵机乱转,曾经有一段时间,我陷入舵机乱转的怪圈,我以为是我程序的问题,但是示波器显示没问题,以至于我以为是舵机坏了。实际上,是电池的问题,电池用的时间长了,电压不足,让舵机乱转。所以,建议使用航模sss电池,这个电池是11.2V的,使用降压模块,降成7.2v,给舵机供电,降成5v给单片机和传感器供电就行了。

尾巴(听我一句劝环节)

其实,舵机的控制,很简单,保证硬件上供电的稳定,保证软件上,能输出稳定、精度够高的PWM就足够了。个人觉得,这里面最大的坑,莫过于我们学习单片机的过程中,资料太过老旧。我学习51单片机那会儿,教程中使用的STC89、90系列的单片机,速度慢、外设少、价格高。也许当你抱着学习的心态去对待,没有太大的问题。但我相信,能看到这里的我相信,大概率是都对机器人制作感兴趣,刚学单片机(51),然后舵机控制遇到了问题;大概率都是希望通过单片机这样一个工具,去实现自己的一些创意,去做一些小玩意儿出来的。这种情况下,一款老旧的芯片,会给你带来很多不必要的麻烦。

PWM这一个场景为例,就像文本所述,从STC90系列单片机到STC12系列单片机,单片机本身外设太差导致PWM的精度始终达不到要求。当我使用STC15W4K32S4让舵机稳定转起来的时候,心里面真的长舒一口气。当我学习STM32F103时,在示波器上看到无比精准的PWM时,除了吃惊,还有一点想笑,自己要是不学51单片机,直接用STM32,哪有这么多事?

但,听我一句劝吧:作为电子设计的初学者,选择一个性能合适的芯片真的很重要。注意,我这里说的是合适。比如本文提到的舵机控制的场景:

  • 我们可以使用51单片机(STC15W4K32S4)实现,前提是,你真的吃透51单片机,看得懂数据手册上不同寄存器是啥意思,STC15W4K32S4能输出精度够用的PWM。
  • 我们可以使用arduino实现,那我们就不需要懂寄存器什么的,我们照着各种教程的程序,改一改就能实现,arduino能输出精度很够用的PWM。
  • 我们可以使用STM32库开发实现,前提是,我们起码知道怎么调库函数,各种库函数是在干什么,STM32能输出精度完美的PWM。
  • 我们可以使用STM32 HAL开发实现,配置一下功能,自动生成工程,稍微改动代码,就能实现,STM32能输出精度完美的PWM。
  • 我们可以使用Mbed开发实现,前提是你英文水平还行,起码看得懂英文文档,Mbed支持的ARM芯片能输出精度完美的PWM。
  • 我们可以使用树莓派实现。前提是你稍微掌握一点linux的基本知识,学一点python,甚至用C语言都能实现,树莓派能输出精度完美的PWM。

跳出舒适圈,是一件很不容易的事儿,特别是抛弃自己学会的东西,去学一个新的东西。我自己在最初的学习过程中,十分畏惧新的东西,所以想在这里把自己的经验分享出来:

  • arduino上手真的比51单片机容易,做个简单的项目真的比51单片机好用。
  • STM32也没有想象中那么复杂,51单片机学习需要10天,STM32一个月也就会用了吧。
  • 树莓派没有什么上来就linux,听起来的那么复杂,linux和windows一样,就是个操作系统,我们在上面写程序跑就行了,python写代码比C语言简单多了。