文章目录
前言
最近在学习一个比较有意思的开源项目uHand2.0机械手掌,应导师要求,在此进行学习总结,以幻尔的开机源码来进行逐步的拆解,从而更进一步理解和运用。(部分图片、内容出自uHand2.0学习资料)
一、uHand2.0是什么?
详细的就不多说,可以自行上某宝搜索,这里提供一张图片给大家参考:
二、关于舵机
1.舵机的内部结构
舵机内部包括了一个小型直流马达、 一组变速齿轮组、 一个线性反馈电位器、 及一块控
制电路板。
其中高速转动的直流马达提供了舵机的原始动力, 带动减速齿轮组, 使之产生高扭力的
输出, 齿轮组的变速比愈大, 舵机的输出扭力也愈大, 也就是说能带动更大重量的负载(受
齿轮强度限制), 但输出的转速(响应速度) 也愈低。
2.舵机工作原理
舵机是一个典型闭环反馈系统, 其原理可由下图表示:
减速齿轮组由马达驱动, 其输出端带动一个线性的比例电位器作位置检测, 该电位器把
转角角度转换为一比例电压反馈给控制电路, 控制电路将其与输入的控制信号对应的角度作
比较, 并驱动马达正向或反向地转动, 使电位器反馈角度趋向于控制信号期望角度, 从而达
到使伺服马达精确定位的目的。
3.硬件接线及说明
标准的PWM舵机有三条控制线, 分别为: 电源、 地及控制。
电源线与地线用于提供内部的直流马达及控制线路所需的能源, 电压通常介于5V—8V
之间, 该电源应尽可能与处理系统的电源隔离(因为马达会产生噪音)。 甚至舵机在重负载
时也会拉低放大器的电压, 所以整个系统的电源供应的比例必须合理。
输入一个周期性的正向脉冲信号, 这个周期性脉冲信号的高电平时间通常在1ms—2ms
之间, 而低电平时间应在5ms到20ms之间。
三、程序分析及调试
1.多路电机动作实现
1.1程序部分
代码如下(示例):
pwm初始化
void InitPWM(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
InitTimer3();
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_8;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10| GPIO_Pin_11 | GPIO_Pin_12;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOD, &GPIO_InitStructure);
}
uHand由六路舵机进行驱动,其中五路舵机驱动手指的动作,剩下一路驱动手腕位置转向。
这里先对GPIO初始化,这里用的是PB8、PB9、PC10、PC11、PC12、PD2;在该初始化函数中调用InitTimer3(),对定时器3初始化。
定时器3中断初始化
void InitTimer3(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
RCC->APB1ENR|=1<<1;//TIM3时钟使能
TIM3->ARR=10000 - 1; //设定计数器自动重装值//刚好10ms
TIM3->PSC=72 - 1; //预分频器72,得到1Mhz的计数时钟
TIM3->DIER|=1<<0; //允许更新中断
// TIM3->DIER|=1<<6; //允许触发中断
TIM3->CR1|=0x01; //使能定时器3
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; //TIM3中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //先占优先级3级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //从优先级3级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
}
这里 T = (ARR+1)/(PSC+1)/72000000,舵机控制信号的分辨率最小是1us, 因此可以将定时器的时基配置为1us(预分频器72,得到1Mhz的计数时钟,配置时基1us)。
脉宽转换函数:
//将PWM脉宽转化成自动装载寄存器的值
void Timer3ARRValue(uint16 pwm)
{
TIM3->ARR = pwm + 1;
}
定时器中断函数:
//定时器3中断服务程序
void TIM3_IRQHandler(void)
{
static uint16 i = 1;
if(TIM3->SR&0X0001)//溢出中断
{
switch(i)
{
case 1:
// SERVO0 = 1; //PWM控制脚高电平
//给定时器0赋值,计数Pwm0Duty个脉冲后产生中断,下次中断会进入下一个case语句
Timer3ARRValue(ServoPwmDuty[0]);
break;
case 2:
// SERVO0 = 0; //PWM控制脚低电平
//此计数器赋值产生的中断表示下一个单元要进行任务的开始
Timer3ARRValue(2500-ServoPwmDuty[0]);
break;
case 3:
SERVO1 = 1;
Timer3ARRValue(ServoPwmDuty[1]);
break;
case 4:
SERVO1 = 0; //PWM控制脚低电平
Timer3ARRValue(2500-ServoPwmDuty[1]);
break;
case 5:
SERVO2 = 1;
Timer3ARRValue(ServoPwmDuty[2]);
break;
case 6:
SERVO2 = 0; //PWM控制脚低电平
Timer3ARRValue(2500-ServoPwmDuty[2]);
break;
case 7:
SERVO3 = 1;
Timer3ARRValue(ServoPwmDuty[3]);
break;
case 8:
SERVO3 = 0; //PWM控制脚低电平
Timer3ARRValue(2500-ServoPwmDuty[3]);
break;
case 9:
SERVO4 = 1;
Timer3ARRValue(ServoPwmDuty[4]);
break;
case 10:
SERVO4 = 0; //PWM控制脚低电平
Timer3ARRValue(2500-ServoPwmDuty[4]);
break;
case 11:
SERVO5 = 1;
Timer3ARRValue(ServoPwmDuty[5]);
break;
case 12:
SERVO5 = 0; //PWM控制脚低电平
Timer3ARRValue(2500-ServoPwmDuty[5]);
break;
case 13:
SERVO6 = 1;
Timer3ARRValue(ServoPwmDuty[6]);
break;
case 14:
SERVO6 = 0; //PWM控制脚低电平
Timer3ARRValue(2500-ServoPwmDuty[6]);
break;
case 15:
// SERVO7 = 1;
Timer3ARRValue(ServoPwmDuty[7]);
break;
case 16:
// SERVO7 = 0; //PWM控制脚低电平
Timer3ARRValue(2500-ServoPwmDuty[7]);
i = 0;
break;
}
i++;
}
TIM3->SR&=~(1<<0);//清除中断标志位
}
这里使用了switch语句,假设当第一次进入中断时,执行case1,把ServoPwmDuty[0]赋值到定时器,下一次计数ServoPwmDuty[0]个脉冲后产生中断,此时SERVO0 置高电平,第二次进入中断时此时把2500-ServoPwmDuty[0](即2500us的剩余时间)赋值到定时器SERVO1 置低电平,组合成完整pwm波,每进入一次i++,如此类推重复8次, 即可实现了20ms的周期;
因为手掌控制板上只有6路舵机接口, 所以将SERVO0和SERVO7注释掉。 这样在操
作数组时其数组的下标则为对应舵机号的舵机。
main函数:
#include "include.h"
extern uint16 ServoPwmDuty[8];
uint16 Duty1=2000;
uint16 Duty2=900;
uint8 i;
int main(void)
{
InitPWM();
SystemInit(); //系统时钟初始化为72M SYSCLK_FREQ_72MHz
InitDelay(72); //延时初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
InitLED();
InitKey();
Usart1_Init(9600);
LED = LED_ON;
while(1)
{
KeyNum=GetKeyNum();
if(KeyNum==1)
{
for(i=2;i<6;i++)
{
ServoPwmDutySet[i]=Duty1;
ServoPwmDutySet[1]=Duty2;
}
}
if(KeyNum==2)
{
for(i=2;i<6;i++)
{
ServoPwmDutySet[i]=1500;
ServoPwmDutySet[1]=1500;
}
}
if(KeyNum==3)
{
for(i=2;i<6;i++)
{
ServoPwmDutySet[i]=Duty2;
ServoPwmDutySet[1]=Duty1;
}
}
}
}
这里采用按键形式切换动作 ,利用for循环,将ServoPwmDutySet[i]数组快速赋值,然后计数器溢出后进入中断,将数组中的脉宽值装载到舵机,实现动作。
按键函数:
uint8 GetKeyNum(void)
{
if(KEY == 0)
{
DelayMs(100);
{
if(KEY == 0)
{
DelayMs(100);
KeyNum++;
if(KeyNum>=4)
{
KeyNum=1;
}
}
}
}
return KeyNum;
}
1.2调试现象
这里只展示了按键KeyNum为1时控制五指摊开的简单动作。可以看到手掌的展开速度非常快,与现实的手掌相比显得十分不自然。所以在接下来,将加入舵机速度控制的相关函数实现动作,使机械手掌效果与真实手掌更贴切。
2.多路舵机加入速度控制
舵机运动速度: 舵机的瞬时运动速度是由其内部的直流马达和变速齿轮组的配合决定
的, 在恒定的电压驱动下, 其数值为唯一。 对于数字PWM舵机, 其速度由其内部程序确定,
一般其平均运动速度可通过分段停顿的控制方式来改变。
例如: 把动作幅度为90° 的转动细分为128个停顿点, 通过控制每个停顿点的时间长短
来实现0° —90° 变化的平均速度。 对于多数舵机来说, 速度的单位由“度数/秒” 来决定。
1.1程序部分
ServoSetPluseAndTime设置舵机转动目标位置及转动时间函数:
void ServoSetPluseAndTime(uint8 id,uint16 p,uint16 time)
{
if(id >= 0 && id <= 7 && p >= 500 && p <= 2500) //0 gaile 1
{
if(time < 20)
time = 20;
if(time > 30000)
time = 30000;
if(id == 6)
{
if(p > 2500)
p = 2500;
else if(p < 500)
p = 500;
}
else
{
if(p > 2200)
p = 2200;
else if(p < 900)
p = 900;
}
ServoPwmDutySet[id] = p;
ServoTime = time;
ServoPwmDutyHaveChange = TRUE;
}
}
该函数的参数从左到右分别是舵机编号、舵机脉宽、停顿点时间,这里前面的if只是判断,对三个参数进行限制,然后将编号p、停顿点时间time进行传递,ServoPwmDutyHaveChange标志位置1进行下一步。
控制舵机转动函数:
void ServoPwmDutyCompare(void)//脉宽变化比较及速度控制
{
uint8 i;
static uint16 ServoPwmDutyIncTimes; //需要递增的次数
static bool ServoRunning = FALSE; //舵机正在以指定速度运动到指定的脉宽对应的位置
if(ServoPwmDutyHaveChange)//停止运动并且脉宽发生变化时才进行计算 ServoRunning == FALSE &&
{
ServoPwmDutyHaveChange = FALSE;
ServoPwmDutyIncTimes = ServoTime/20; //当每20ms调用一次ServoPwmDutyCompare()函数时用此句
for(i=0;i<8;i++)
{
//if(ServoPwmDuty[i] != ServoPwmDutySet[i])
{
if(ServoPwmDutySet[i] > ServoPwmDuty[i])
{
ServoPwmDutyInc[i] = ServoPwmDutySet[i] - ServoPwmDuty[i];
ServoPwmDutyInc[i] = -ServoPwmDutyInc[i];
}
else
{
ServoPwmDutyInc[i] = ServoPwmDuty[i] - ServoPwmDutySet[i];
}
ServoPwmDutyInc[i] /= ServoPwmDutyIncTimes;//每次递增的脉宽
}
}
ServoRunning = TRUE; //舵机开始动作
}
if(ServoRunning)
{
ServoPwmDutyIncTimes--;
for(i=0;i<8;i++)
{
if(ServoPwmDutyIncTimes == 0)
{ //最后一次递增就直接将设定值赋给当前值
ServoPwmDuty[i] = ServoPwmDutySet[i];
ServoRunning = FALSE; //到达设定位置,舵机停止运动
}
else
{
ServoPwmDuty[i] = ServoPwmDutySet[i] +
(signed short int)(ServoPwmDutyInc[i] * ServoPwmDutyIncTimes);
}
}
}
}
这里的ServoPwmDutyHaveChange标志位在上一步置1,使得当前可以进入该函数进行运算,ServoPwmDutyIncTimes表示要递增的次数,此变量由ServoTime / 20决定(20ms调用一次,所以这个变量相当于这段时间内变化多少次,也就是递增次数),至于为什么是20ms调用一次,如何实现,等下再看看下面这个函数TaskTimeHandle()。
接下来进入for循环,将ServoPwmDuty[i](表示当前脉宽)与ServoPwmDutySet[i](表示想要达到的脉宽)进行比较,如何将二者作差值赋值给ServoPwmDutyInc[i],此时再将其计算出负值,再次运算将每次递增的脉宽再次赋值到ServoPwmDutyInc[i] (ServoPwmDutyInc[i] /= ServoPwmDutyIncTimes),并将标志位置1进入下一步。
第二个for循环会将每次递增完之后的脉宽值重新存进ServoPwmDuty[i],进入中断接着会将舵
机的脉宽向目标脉宽靠近一步(这里解释为什么要负数,假设目标值是1900,而当前值是1500,每次进入for循环前递增次数ServoPwmDutyIncTimes--,那么原本1900减去二者运算会等于1500,但此时递增次数减了1,所以会比1500多1个递增次数的脉宽,从而进一步靠近目标值)。
函数TaskTimeHandle():
void TaskTimeHandle(void)
{
static uint32 time = 10;
static uint32 times = 0;
if(gSystemTickCount > time)
{
time += 10;
times++;
if(times % 2 == 0)//20ms
{
ServoPwmDutyCompare();
}
if(times % 50 == 0)//500ms
{
CheckBatteryVoltage();
}
}
}
我们会在主函数中调用TaskTimeHandle(),一方面是实现500ms进行adc电压读取(这部分先不讲,而time、times均在中断中进行计数),另一方面是实现20ms进入一次ServoPwmDutyCompare()函数(这里就实现上一步的20ms)。
main函数:
#include "include.h"
extern uint16 ServoPwmDuty[8];
extern uint16 ServoPwmDutySet[8];
uint16 Duty1=2000;
uint16 Duty2=900;
uint8 i;
int main(void)
{
InitPWM();
InitTimer2();//用于产生100us的定时中断
SystemInit(); //系统时钟初始化为72M SYSCLK_FREQ_72MHz
InitDelay(72); //延时初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
InitADC();
InitLED();
InitKey();
InitBuzzer();
LED = LED_ON;
while(1)
{
KeyNum=GetKeyNum();
if(KeyNum==1)
{
for(i=2;i<6;i++)
{
ServoPwmDutySet[i]=Duty1;
ServoPwmDutySet[1]=Duty2;
ServoSetPluseAndTime(i,Duty1,1000);
TaskTimeHandle();
}
}
if(KeyNum==2)
{
for(i=2;i<6;i++)
{
ServoPwmDutySet[i]=1500;
ServoPwmDutySet[1]=1500;
ServoSetPluseAndTime(i,1500,1000);
TaskTimeHandle();
}
}
if(KeyNum==3)
{
for(i=2;i<6;i++)
{
ServoPwmDutySet[i]=Duty2;
ServoPwmDutySet[1]=Duty1;
ServoSetPluseAndTime(i,Duty2,1000);
TaskTimeHandle();
}
}
}
}
这里主函数部分与之前相比,是在每一个动作中调用 TaskTimeHandle()、ServoSetPluseAndTime()(其中的参数传输分别是编号,脉宽,以及时间;这里的时间是决定递增次数的,并不是表示完成这个动作的时间)。
2.2调试现象
观察加入了舵机速度控制函数后的变化,显然相比之前,机械手掌的运动更加贴切真实手掌,更加生动。