一.模拟舵机控制
网上不乏对此种舵机的介绍,比如下面这篇文章:
浅谈用单片机控制SG90舵机(原理+编程)
1.简介
SG90模拟舵机在市面上十分常见,价格也比较便宜。常用于航模,机器人或智能小车等。
如上图所示,一个舵机有三条线:VCC、GND和信号线。只要通过信号线给予规定的控制信号即可实现舵机码盘的转动。
2.控制信号
对于此种模拟舵机的控制是通过向信号线持续发送PWM信号,直到舵机转到指定位置(对于数字舵机只需发送一次目标角度的信号)。透过蓝色的舵机外壳可以看到里面有一块很小的电路板,它便是用来将PWM信号转换成舵机的实际转动。
一般PWM信号的周期为20ms(50Hz的频率),想要控制角度只需控制一个周期中高电平持续的时间。以180°舵机为例,对应关系如下:
一个周期中高电平持续时间 | 舵机保持的角度 |
---|---|
0.5ms | 0° |
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寄存器
p r e s c a l e v a l u e = r o u n d ( o s c _ c l o c k 4096 ∗ u p d a t e _ r a t e ) − 1 prescale ~value=round(\frac{osc\_clock}{4096*update\_rate})-1 prescale value=round(4096∗update_rateosc_clock)−1
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);
}
}