RoboGame参赛总结-3

RoboGame参赛总结-3

驱动板简单介绍

我们选用的是最常用的以pca9685为基础的PWM驱动板,以IIC作为通信协议,如果使用Arduino的话,控制相当简单,但STM32因为缺乏相关库,所以驱动的编写有些复杂。
先了解一下原理:
机械臂的运动主要依靠舵机,也就是一种伺服电机,由PWM信号决定运行的角度,并且没有信号就罢工了,这么多的PWM波,依靠STM32本身来维持是很糟糕的,16路舵机驱动板很好的解决了这个问题。我们只需要通过IIC给出角度信号,产生和维持PWM信号的工作就交给驱动板了。
在这里插入图片描述
这里需要基于IIC的驱动函数,这个在网上很容易找到:

void IIC_Init(void);                //初始化IIC的IO口				 
void IIC_Start(void);				//发送IIC开始信号
void IIC_Stop(void);	  			//发送IIC停止信号
void IIC_Send_Byte(u8 txd);			//IIC发送一个字节
u8 IIC_Read_Byte(unsigned char ack);//IIC读取一个字节
u8 IIC_Wait_Ack(void); 				//IIC等待ACK信号
void IIC_Ack(void);					//IIC发送ACK信号
void IIC_NAck(void);				//IIC不发送ACK信号

驱动板函数编写

关键的问题在于如何方便快捷的调用舵机运动,使得main函数变得简洁便于更改维护,先介绍一些基本的对pca9685的操作:
我们需要一些宏定义:

#define PCA9685_SUBADR1 0x2 
#define PCA9685_SUBADR2 0x3 
#define PCA9685_SUBADR3 0x4 
#define pca_adrr 0x80
#define pca_mode1 0x0 
#define pca_pre 0xFE 
#define LED0_ON_L 0x6 
#define LED0_ON_H 0x7 
#define LED0_OFF_L 0x8 
#define LED0_OFF_H 0x9 
#define ALLLED_ON_L 0xFA 
#define ALLLED_ON_H 0xFB 
#define ALLLED_OFF_L 0xFC 
#define ALLLED_OFF_H 0xFD 

其实这些控制逻辑跟使用LCD屏幕是高度类似的,就是发送一些定义好的指令就可以了:

void pca_write(u8 adrr,u8 data)
{ 
        IIC_Start();
        
        IIC_Send_Byte(pca_adrr);
        IIC_Wait_Ack();
        
        IIC_Send_Byte(adrr);
        IIC_Wait_Ack();
        
        IIC_Send_Byte(data);
        IIC_Wait_Ack();
        
        IIC_Stop();
}

u8 pca_read(u8 adrr)
{
        u8 data;
        IIC_Start();
        
        IIC_Send_Byte(pca_adrr);
        IIC_Wait_Ack();
        
        IIC_Send_Byte(adrr);
        IIC_Wait_Ack();
        
        IIC_Start();
        
        IIC_Send_Byte(pca_adrr|0x01);
        IIC_Wait_Ack();
        
        data=IIC_Read_Byte(0);
        IIC_Stop();
        
        return data;
}


void pca_setfreq(float freq)
{
	u8 prescale,oldmode,newmode;
    double prescaleval;
    freq *= 0.92; 
    prescaleval = 25000000;
    prescaleval /= 4096;
    prescaleval /= freq;
    prescaleval -= 1;
    prescale =floor(prescaleval + 0.5f);

    oldmode = pca_read(pca_mode1);
        
    newmode = (oldmode&0x7F) | 0x10; 
        
    pca_write(pca_mode1, newmode); 
        
    pca_write(pca_pre, prescale);
        
    pca_write(pca_mode1, oldmode);
    delay_ms(2);
        
    pca_write(pca_mode1, oldmode | 0xa1); 
}

void pca_setpwm(u8 num, u32 on, u32 off)
{
	pca_write(LED0_ON_L+4*num,on);
    pca_write(LED0_ON_H+4*num,on>>8);
    pca_write(LED0_OFF_L+4*num,off);
    pca_write(LED0_OFF_H+4*num,off>>8);
}

在实际使用的时候,需要初始化IIC通道:

delay_init();
IIC_Init();
pca_write(pca_mode1, 0x00);
pca_setfreq(50);

但现在,我们能做的也仅仅是通过main函数给驱动板发送一个角度信息,然后舵机做相关动作:

pca_setpwm(ptr->engine_index, 0, ptr->NowAngle);

这是不符合实际需求的,这时我想到了运用中断,在中断中完成从某个角度运动到另一个角度的过程,保证平稳的同时,方便调速,还不会在main函数中占用大量时间,可以让main函数去调配其他任务,首先初始化一个定时器:

void TIM5_PWM_Init(u16 arr, u16 psc){
	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE); //使能 TIM5 时钟
	TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值
	TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值 
	TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
	TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
	TIM_ITConfig(TIM5, TIM_IT_Update, ENABLE);
	TIM_Cmd(TIM5, ENABLE);
	
	NVIC_InitStructure.NVIC_IRQChannel = TIM5_IRQn;                     //NVIC配置
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);
}

使用更新溢出中断就足够了,然后我们需要定义一个结构体来储存舵机的各种信息:

typedef struct ControlNode{
	u8 engine_index;
	int step;
	u16 NowAngle;
	u16 FinalAngle;
	struct ControlNode* next;
}ControlNode;

一个结构体对应一个舵机,包含了当前角度,转动步长,舵机编号,和目标角度。需要运行的舵机会新建一个对应拥有相应index的结构体,插入链表中:

extern ControlNode* ControlHead;

这是一个全局的指针,作为头指针被调用,用于检索所有需要运动的舵机,到达目标值之后,这个结构体会被清除:

void Sterengine_Scan(void){
	ControlNode* ptr, *pre;
	pre = ControlHead;
	ptr = ControlHead->next;
	while(ptr!=NULL){
		if(ptr->NowAngle==ptr->FinalAngle){
			StatusNode[ptr->engine_index].status = 0; //任务值空
			pre->next = ptr->next;
			free(ptr);
			pre = pre;
			ptr = pre->next;
			continue;
		} //清除结构体
		ptr->NowAngle = ptr->NowAngle+ptr->step;
		StatusNode[ptr->engine_index].NowAngle = ptr->NowAngle;
		pca_setpwm(ptr->engine_index, 0, ptr->NowAngle);
		pre = ptr;
		ptr = ptr->next;
	}
}

将上面这个函数放入定义好的中断服务中:

void TIM5_IRQHandler(void){
	if(TIM_GetITStatus(TIM5, TIM_IT_Update) != RESET){
		GetInf();
		Sterengine_Scan(); //舵机任务执行
		//Irsensor_Control();
		//Control_FindSideWay();
		TIM_ClearITPendingBit(TIM5, TIM_IT_Update);
	}
}

最后,我们需要一个函数用于装载需要运动的结构体,为了方便管理,将index设置为宏定义,这个index同在驱动板上的插线有关,具体可以查相关技术手册:

#define BehindShoulder 0
#define BehindArm 1
#define BehindPaw 2
#define FrontShoulder 3
#define FrontArm 4
#define FrontPaw 5
#define RightPaw 6
#define LeftPaw 7

我们的机器人共使用8个舵机,将角度信息作为数组存储起来,应为实际上待命的角度一般只有两个:

static u16 OpenAngleBuf[9]={480, 280, 395, 435, 250, 320, 400, 400, 400};
static u16 CloseAngleBuf[9]={80, 105, 260, 200, 110, 180, 250, 250, 250};

最关键的来了,在插入函数中,通过指令决定相关动作,并设置成为宏定义:

#define Open -1
#define Close 1
#define toR -1
#define toM 0
#define toL 1
#define Up 1
#define Down -1
#define Open1 1
#define Close1 -1

其实归根结底不过是调用上面那个数组的两种顺序罢了。

void Sterengine_Control(u8 index, int commond){
	u16 MidAngle;
	ControlNode* ptr;
	/*
	0:Behindshoulder
	1:BehindArm
	2:BehindPaw
	3:FrondShoulder
	4:FrontArm
	5:FrontPaw
	6:RightPaw
	7:LeftPaw
	*/
	Sterengine_Ready(index);
	ptr = (ControlNode*)malloc(sizeof(ControlNode));
	ptr->next = ControlHead->next;
	ControlHead->next = ptr;
	ptr->engine_index = index;
	StatusNode[index].status = 1; 
	ptr->NowAngle = StatusNode[index].NowAngle;
	//新建任务
	if(index == 0||index==3){ //检索到云台
		MidAngle = (OpenAngleBuf[index]+CloseAngleBuf[index])/2;
		switch(commond){
			case 0: ptr->FinalAngle = MidAngle; break;
			case 1: ptr->FinalAngle = OpenAngleBuf[index]; break;
			case -1: ptr->FinalAngle = CloseAngleBuf[index]; break;
		}
	}
	else{
		if(commond == 1){
			ptr->FinalAngle = OpenAngleBuf[index];
		}
		else{
			ptr->FinalAngle = CloseAngleBuf[index];
		}
	}
	//设置步长正负值
	if(ptr->FinalAngle-StatusNode[index].NowAngle>0){
		ptr->step = 1;
	}
	else{
		ptr->step = -1;
	}
}

也许注意到了在语句执行之前还有一个函数,这个函数的目的是如果这个舵机正在执行,也就是结构体还在链表中,那么需要等待完成再将新的任务结构体插入链表中,这样就避免了未知的冲突:

void Sterengine_Ready(u8 index){
	while(StatusNode[index].status!=0);
}

这里利用了一个新的结构体:

typedef struct MissionStatus{
	u8 status;
	u16 NowAngle;
}MissionStatus;

status为0表示空闲,1表示繁忙,当前角度是在中断中被写入的,其实可以通过IIC从pca9685上读入,但不是非常必要,但我们这样做的话,必须设置一个初始值,也就是一开始所有舵机角度需要到达我们预设的位置,相应的角度信息也必须写入内存,否则在执行的时候会带来混乱。
举个例子:
一开始认为的NowAngle是180,这是我们自己定义的,但舵机却未必是这样,当运行的时候,舵机可能收到一个和当前的物理角度相距甚远的指令,这种情况是要尽量避免的,所以有这样一个初始化函数:

void Grab_Init(void){
	//所有舵机信息结构体赋初值,舵机就位
	u8 index;
	u16 temp;
	if(ObjectSearchNode->mirror_direction == -1){
		pca_setpwm(BehindShoulder, 0, OpenAngleBuf[BehindShoulder]);
		StatusNode[BehindShoulder].NowAngle = OpenAngleBuf[BehindShoulder];
	}
	else if(ObjectSearchNode->mirror_direction == 1){
		pca_setpwm(BehindShoulder, 0, CloseAngleBuf[BehindShoulder]);
		StatusNode[BehindShoulder].NowAngle = CloseAngleBuf[BehindShoulder];
	}
	delay_ms(100);
	
	temp = (OpenAngleBuf[FrontShoulder] + CloseAngleBuf[FrontShoulder])/2;
	pca_setpwm(FrontShoulder, 0, temp);
	StatusNode[FrontShoulder].NowAngle = temp;
	delay_ms(100);
	
	temp = OpenAngleBuf[FrontArm];
	pca_setpwm(FrontArm, 0, temp);
	StatusNode[FrontArm].NowAngle = temp;
	delay_ms(100);
	
	temp = OpenAngleBuf[FrontPaw];
	pca_setpwm(FrontPaw, 0, temp);
	StatusNode[FrontPaw].NowAngle = temp;
	delay_ms(100);
	
	temp = CloseAngleBuf[BehindArm];
	pca_setpwm(BehindArm, 0, temp);
	StatusNode[BehindArm].NowAngle = temp;
	delay_ms(100);
	
	temp = CloseAngleBuf[BehindPaw];
	pca_setpwm(BehindPaw, 0, temp);
	StatusNode[BehindPaw].NowAngle = temp;
	delay_ms(100);
	
	temp = CloseAngleBuf[RightPaw];
	pca_setpwm(RightPaw, 0, temp);
	StatusNode[RightPaw].NowAngle = temp;
	delay_ms(100);
	
	temp = CloseAngleBuf[LeftPaw];
	pca_setpwm(LeftPaw, 0, temp);
	StatusNode[LeftPaw].NowAngle = temp;
	delay_ms(100);
	for(index=0; index<8; index++){
		StatusNode[index].status = 0; 
	}
}

这样,舵机就可以非常方便的发送指令使之完成相关动作了:

void FrontPlace(void){
	if(ObjectSearchNode->mirror_direction == 1){
		Sterengine_Control(FrontShoulder, toL);
	}
	else if(ObjectSearchNode->mirror_direction == -1){
		Sterengine_Control(FrontShoulder, toR);
	}
	delay_ms(300);
	Sterengine_Control(FrontArm, Down);
	Sterengine_Control(FrontPaw, Open);
}

这是一个实际使用例子。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
淘宝上卖的16路PWM舵机驱动模块的51单片机程序 部分程序如下 #include #include #include #include typedef unsigned char uchar; typedef unsigned int uint; sbit scl=P1^3; //时钟输入线 sbit sda=P1^4; //数据输入/输出端 sbit KEY1=P2^0; sbit KEY2=P2^1; #define PCA9685_adrr 0x80// 1+A5+A4+A3+A2+A1+A0+w/r //片选地址,将焊接点置1可改变地址, // 当IIC总 呱嫌 多片PCA9685或相同地址时才需焊接 // #define PCA9685_SUBADR1 0x2 // #define PCA9685_SUBADR2 0x3 // #define PCA9685_SUBADR3 0x4 #define PCA9685_MODE1 0x0 #define PCA9685_PRESCALE 0xFE #define LED0_ON_L 0x6 #define LED0_ON_H 0x7 #define LED0_OFF_L 0x8 #define LED0_OFF_H 0x9 // #define ALLLED_ON_L 0xFA // #define ALLLED_ON_H 0xFB // #define ALLLED_OFF_L 0xFC // #define ALLLED_OFF_H 0xFD #define SERVOMIN 115 // this is the 'minimum' pulse length count (out of 4096) #define SERVOMAX 590 // this is the 'maximum' pulse length count (out of 4096) #define SERVO000 130 //0度对应4096的脉宽计数值 #define SERVO180 520 //180度对应4096的脉宽计算值,四个值可根据不同舵机修改 /**********************函数的声明*********************************/ /*--------------------------------------------------------------- 毫秒延时函数 ----------------------------------------------------------------*/ void delayms(uint z) { uint x,y; for(x=z;x>0;x--) for(y=148;y>0;y--); } /*--------------------------------------------------------------- IIC总线所需的通用函数 ----------------------------------------------------------------*/ /*--------------------------------------------------------------- 微妙级别延时函数 大于4.7us ----------------------------------------------------------------*/ void delayus() { _nop_(); //在intrins.h文件里 _nop_(); _nop_(); _nop_(); _nop_(); } /*--------------------------------------------------------------- IIC总线初始化函数 ----------------------------------------------------------------*/ void init() { sda=1; //sda scl使用前总是被拉高 delayus(); scl=1; delayus(); } /*--------------------------------------------------------------- IIC总线启动信号函数 ----------------------------------------------------------------*/ void start() { sda=1; delayus(); scl=1; //scl拉高时 sda突然来个低电平 就启动了IIC总线 delayus(); sda=0; delayus(); scl=0; delayus(); } /*--------------------------------------------------------------- IIC总线停止信号函数 ----------------------------------------------------------------*/ void stop() { sda=0; delayus(); scl=1; //scl拉高时 sda突然来个高电平 就停止了IIC总线 delayus(); sda=1; delayus(); } /*--------------------------------------------------------------- IIC总线应答信号函数 ----------------------------------------------------------------*/ void ACK() { uchar i; scl=1; delayus(); while((sda=1)&&(i<255)) i++; scl=0; delayus(); } /*--------------------------------------------------------------- 写一个字节,无返回值,需输入一个字节值 ----------------------------------------------------------------*/ void write_byte(uchar byte) { uchar i,temp; temp=byte; for(i=0;i<8;i++) { temp=temp<<1; scl=0; delayus(); sda=CY; delayus(); scl=1; delayus(); } scl=0; delayus(); sda=1; delayus(); } /*--------------------------------------------------------------- 读一个字节函数,有返回值 ----------------------------------------------------------------*/ uchar read_byte() { uchar i,j,k; scl=0; delayus(); sda=1; delayus(); for(i=0;i<8;i++) { delayus(); scl=1; delayus(); if(sda==1) { j=1; } else j=0; k=(k<< 1)|j; scl=0; } delayus(); return k; } /*--------------------------------------------------------------- 有关PCA9685模块的函数 ----------------------------------------------------------------*/ /*--------------------------------------------------------------- 向PCA9685里写地址,数据 ----------------------------------------------------------------*/ void PCA9685_write(uchar address,uchar date) { start(); write_byte(PCA9685_adrr); //PCA9685的片选地址 ACK(); write_byte(address); //写地址控制字节 ACK(); write_byte(date); //写数据 ACK(); stop(); } /*--------------------------------------------------------------- 从PCA9685里的地址值中读数据(有返回值) ----------------------------------------------------------------*/ uchar PCA9685_read(uchar address) { uchar date; start(); write_byte(PCA9685_adrr); //PCA9685的片选地址 ACK(); write_byte(address); ACK(); start(); write_byte(PCA9685_adrr|0x01); //地址的第八位控制数据流方向,就是写或读 ACK(); date=read_byte(); stop(); return date; }

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值