一.模拟舵机控制

在这里插入图片描述
在这里插入图片描述
网上不乏对此种舵机的介绍,比如下面这篇文章:
浅谈用单片机控制SG90舵机(原理+编程)

1.简介

SG90模拟舵机在市面上十分常见,价格也比较便宜。常用于航模,机器人或智能小车等。
如上图所示,一个舵机有三条线:VCC、GND和信号线。只要通过信号线给予规定的控制信号即可实现舵机码盘的转动。

2.控制信号

对于此种模拟舵机的控制是通过向信号线持续发送PWM信号,直到舵机转到指定位置(对于数字舵机只需发送一次目标角度的信号)。透过蓝色的舵机外壳可以看到里面有一块很小的电路板,它便是用来将PWM信号转换成舵机的实际转动。

一般PWM信号的周期为20ms(50Hz的频率),想要控制角度只需控制一个周期中高电平持续的时间。以180°舵机为例,对应关系如下:

一个周期中高电平持续时间 舵机保持的角度
0.5ms
1ms 45°
1.5ms 90°
2ms 135°
2.5ms

180°

其他在0°~180°之间的角度通过上表类推即可。(但需要注意舵机的死区时间
需要理解注意的是:对于舵机而言,所有提到的角度均为绝对角度,而非相对角度。即每个角度所对应的是一个固定的位置,并而不是像步进电机那样相对当前位置转动一定角度。

二.PCA9685模块

也可参考下面这篇博客:
16路12位PWM控制器 PCA9685
后续的介绍借鉴了此篇文章。

1.简介

通过之前对舵机的介绍会发现,控制一个舵机需要占用三个引脚,其中一个为单片机的I/O脚。当所需使用的舵机数量较多时,十分占用引脚资源。此时PCA9685模块便可以发挥作用。

PCA9685芯片设计之初是用来控制多路LED灯,后来发现在控制多路舵机上也可发挥很大作用。因此网上所购买的PCA9685模块大都是以控制多路舵机而设计的。如下图所示:
在这里插入图片描述

PCA9685芯片内置了25MHz的晶振,同时也提供外部晶振输入引脚(但是模块中一般不引出此脚,只能使用内部晶振)

2.模块接口介绍:

★1.PCA9685模块的通信使用的是IIC协议,如上图模块左右两侧的SDA数据线和SCL时钟线

★2.OE是芯片的使能引脚,低电平时使能芯片。模块中已经下拉保持低电平,因此使用时可不连接。

★3. 上图中红色线连接的V+引脚是舵机的驱动电压,与PCA9685芯片本身无关。可通过左右两侧的排针接到最小系统板的标准3.3V/5V。当发现驱动电压不够,舵机无法转动或者扭力不够时,可通过上方的接线端子接入外接电源,如锂电池。但需要注意,接入的最大电压不能大于左上角电解电容的耐压值,一般为10V。

★4.绿色方框便是舵机的接口,一块PCA9685芯片最多可以控制16路舵机

3.模块器件地址

在这里插入图片描述

模块的器件地址构成如上。其中最高位固定为1,最低位为读/写控制位,A0~A5决定了其硬件地址,当采用多个此模块时可借此用于分别的控制。模块中对于A0~A5的设置可见上图的右上角紫色方框。6个引脚通过下拉电阻接地,因此默认全为低电平(即器件地址默认0x80。有些地方说是0x40是因为不考虑读写位,也是正确的),如果焊上框中上方的连接点,便会接到VCC,从而变为高电平。

4.相关寄存器

PCA9685的寄存器较多,使用也相对复杂,此处仅介绍控制舵机需要用到的。

①寄存器总览
寄存器地址 名称
00H MODE1
01H MODE2
02H SUBADR1
03H SUBADR2
04H SUBADR3
05H ALLCALLADR
06H LED0_ON_L
07H LED0_ON_H
08H LED0_OFF_L
09H LED0_OFF_H
··· ···
06H+4*X LEDX_ON_L
06H+4*X+1 LEDX_ON_H
06H+4*X+2 LEDX_OFF_L
06H+4*X+3 LEDX_OFF_H
FA ALL_LED_ON_L
FB ALL_LED_ON_H
FC ALL_LED_OFF_L
FD ALL_LED_OFF_H
FE PRE_SCALE
FF TestMode
②MODE1模式配置寄存器1
SFR name Address bit B7 B6 B5 B4 B3 B2 B1 B0
MODE1 00H name RESTART EXTCLK AI SLEEP SUB1 SUB2 SUB3 ALLCALL

RESTART:0—不复位,1—复位,完成复位后会自动清零;要在SLEEP置0后至少500us以后才能进行复位。

EXTCLK:0—使用内部时钟,1—使用外部时钟;修改此位时,需要先将SLEEP位置1。

AI:0—读写后寄存器地址不自动递增,1—读写后寄存器地址自动递增;一般设置自动递增。

SLEEP:0—退出SLEEP模式,1—进入SLEEP模式。设置EXTCLK位和PRE_SCALE寄存器前都需先进入SLEEP模式。

ALLCALL:0—不响应0x70通用IIC地址,1—相应0x70通用IIC地址。

③PRE_SCALE寄存器

osc_clock为选用的时钟频率,如使用内部25MHz时钟,即为25 000 000。
update_rate为PWM的频率,如前面说舵机PWM周期为20ms,则update_rate=50Hz。
4096是因为计数器是12位。

经过上式计算可发现,PRE_SCALE中所存的值实际是计数器ACK每自加1,需要的时钟脉冲个数。其实就是时钟分频后计数。和51或stm32的定时器原理类似。

④每个通道的四个寄存器
由之前的寄存器总览表中可看出:16个通道中,每个都有LEDX_ON_L、LEDX_ON_H、LEDX_OFF_L、LEDX_OFF_H 四个寄存器。

芯片中12位的计数器ACK,会根据PRE_SCALE设置的值进行计数。
当LEDX_ON_H[3:0]:LEDX_ON_L < ACK < LEDX_OFF_H[3:0]:LEDX_OFF_L时,输出高电平;
当LEDX_OFF_H[3:0]:LEDX_OFF_L < ACK < 4096时,输出低电平;

实际应用中有误差,需要校准,把update_rate乘0.915。

5.以stc15单片机为例代码

以上提到的一些要点均会体现在下方的代码中。

★需要注意的是:此模块同一时刻只能改变一个PWM输出,因此控制多个舵机时,只能依次控制,并不能实现多个同步控制。当然,如果使用上面寄存器表中给出的ALL_LED_ON/OFF_L/H四个寄存器,也可以实现所有舵机一起转动,但是转动角度只能是全部相同。

#include <stc15.h>           
#include <intrins.h>  
#include <stdio.h>
#include <math.h>

typedef  unsigned char  u8;        
typedef  unsigned int   u16;        

sbit SCL=P2^0;                   //时钟线
sbit SDA=P2^1;                   //数据线

#define DELAY_TIME 5			//延时时间
#define PCA9685_adrr 0x80	    //1+A5+A4+A3+A2+A1+A0+w/r 

#define PCA9685_SUBADR1 0x02
#define PCA9685_SUBADR2 0x03
#define PCA9685_SUBADR3 0x04

#define PCA9685_MODE1    0x00   //MODE1寄存器地址
#define PCA9685_PRESCALE 0xFE   //PRE_SCALE寄存器地址

#define LED0_ON_L  0x06			//通道0的四个控制寄存器
#define LED0_ON_H  0x07
#define LED0_OFF_L 0x08
#define LED0_OFF_H 0x09

#define ALLLED_ON_L  0xFA		//全部通道的四个控制寄存器
#define ALLLED_ON_H  0xFB		//舵机控制时一般不用
#define ALLLED_OFF_L 0xFC
#define ALLLED_OFF_H 0xFD

#define SERVO000  130 			//0度对应4096的脉宽计数值
#define SERVO180  520 			//180度对应4096的脉宽计算值
								//每个舵机都会有一定差异,需要实际测试。

/*-----------------------IIC协议-------------------------*/

/*********************************************************
函数功能:毫秒延时函数
*********************************************************/
void delayms(u16 z)
{
  u16 x,y;
  for(x=z;x>0;x--)
      for(y=148;y>0;y--);
}

/*********************************************************
函数功能:IIC微秒延时函数,晶振12M,指令周期1T
*********************************************************/
void IIC_Delay(unsigned char i)
{
    do
	{
		_nop_();_nop_();_nop_();_nop_();_nop_();
        _nop_();_nop_();_nop_();_nop_();_nop_();
        _nop_();_nop_();_nop_();_nop_();_nop_();
	}while(i--);      
}

/*********************************************************
函数功能:IIC启动
*********************************************************/
void IIC_Start(void)
{
	SDA = 1;
	SCL = 1;				
	IIC_Delay(DELAY_TIME);
	SDA = 0;
	IIC_Delay(DELAY_TIME);
	SCL = 0;	
}

/*********************************************************
函数功能:IIC停止
*********************************************************/
void IIC_Stop(void)
{
	SDA = 0;
	SCL = 1;				
	IIC_Delay(DELAY_TIME);
	SDA = 1;
}

/*********************************************************
函数功能:等待从机应答
*********************************************************/
bit IIC_WaitAck(void)
{ 
	SDA = 1;
	IIC_Delay(DELAY_TIME);
	SCL = 1;
	IIC_Delay(DELAY_TIME);
	if(SDA)    
	{   
		SCL = 0;
		IIC_Stop();
		return 0;
	}
	else  
	{ 
		SCL = 0;
		return 1;
	}
}

/*********************************************************
函数功能:IIC发送一个字节
*********************************************************/
void IIC_SendByte(unsigned char byt)
{
	unsigned char i;
	for(i=0;i<8;i++)
	{   
		if(byt&0x80) 
		{	
			SDA = 1;
		}
		else 
		{
			SDA = 0;
		}
		IIC_Delay(DELAY_TIME);
		SCL = 1;
		byt <<= 1;
		IIC_Delay(DELAY_TIME);
		SCL = 0;
	}
}

/*********************************************************
函数功能:IIC接收一个字节
*********************************************************/
unsigned char IIC_RecByte(void)
{
	unsigned char da;
	unsigned char i;
	
	for(i=0;i<8;i++)
	{   
		SCL = 1;
		IIC_Delay(DELAY_TIME);
		da <<= 1;
		if(SDA) 
		da |= 0x01;
		SCL = 0;
		IIC_Delay(DELAY_TIME);
	}
	return da;
}

/*-----------------------PCA9685模块相关函数-------------------------*/

/*********************************************************
函数功能:向PCA9685的一个地址写数据
*********************************************************/
void PCA9685_write(u8 address,u8 date)
{
	IIC_Start();
	IIC_SendByte(PCA9685_adrr);   //PCA9685的片选地址
	IIC_WaitAck();                          
	IIC_SendByte(address);  	  //写地址控制字节
	IIC_WaitAck();
	IIC_SendByte(date);           //写数据
	IIC_WaitAck();
	IIC_Stop();
}

/*********************************************************
函数功能:从PCA9685的一个地址读数据
*********************************************************/
u8 PCA9685_read(u8 address)
{
	u8 date;
	IIC_Start();
	IIC_SendByte(PCA9685_adrr); //PCA9685的片选地址
	IIC_WaitAck();
	IIC_SendByte(address);
	IIC_WaitAck();

	IIC_Start();
	IIC_SendByte(PCA9685_adrr|0x01);        //地址的第八位控制数据流方向,就是写或读
	IIC_WaitAck();
	date=IIC_RecByte();
	IIC_Stop();
	return date;
}

/*********************************************************
函数功能:PCA9685的MODE1寄存器清零
*********************************************************/
void reset(void) 
{
	PCA9685_write(PCA9685_MODE1,0x00);
}

/*********************************************************
函数功能:PCA9685频率修改
入口参数:freq-输出PWM频率
*********************************************************/
void setPWMFreq(float freq) 
{
	u16 prescale,oldmode,newmode;
	float prescaleval;
	
	freq *= 0.92;   							//纠正频率设置中的过冲,进行校准								
	prescaleval = 25000000;						//根据公式计算prescale的值
	prescaleval /= 4096;						//prescaleval=round(osc_cloc/4096/freq)-1;
	prescaleval /= freq;
	prescaleval -= 1;
	prescale = floor(prescaleval + 0.5);		
                
	oldmode = PCA9685_read(PCA9685_MODE1);		  //获得MODE1寄存器值
	newmode = (oldmode&0xEF) | 0x10; 			  //SLEEP位 置1
	PCA9685_write(PCA9685_MODE1, newmode);  	  //进入SLEEP模式
	PCA9685_write(PCA9685_PRESCALE, prescale);    //设置频率
	PCA9685_write(PCA9685_MODE1, oldmode);		  //退出SLEEP模式
	delayms(2);
	PCA9685_write(PCA9685_MODE1, oldmode | 0xa1); //RESTART、AI、ALLCALL三个位 置1
}

/*********************************************************
函数功能:改变通道PWM占空比
输入参数:num-使用的通道0~15
		  on-高电平开始时计数器ACK值
		  off-高电平结束时计数器ACK值
*********************************************************/
void setPWM(u16 num, u16 on, u16 off) 
{
	PCA9685_write(LED0_ON_L+4*num,on);		//LED0_ON_L保存on低8位
	PCA9685_write(LED0_ON_H+4*num,on>>8);	//LED0_ON_H保存on高4位
	PCA9685_write(LED0_OFF_L+4*num,off);
	PCA9685_write(LED0_OFF_H+4*num,off>>8);
 }
 
/*-----------------------主函数-------------------------*/
void main()
{
	reset();
	setPWMFreq(50);  //设置频率50Hz
	//以转动到60°位置为例:
	//60度对应的脉宽=0.5ms+(60/180)*(2.5ms-0.5ms)=1.1666ms
	//利用占空比=1.1666ms/20ms=off/4096,off=239,50hz对应周期20ms
	//setPWM(num,0,239);
	while(1) 
	{
		setPWM(0, 0, 239);
		setPWM(1, 0, SERVO000);
	}                
}