明日机甲——DIY一套基于脑/肌电的“意念控制”机械臂 

(0)意念机甲与外骨骼

    小蘑菇非常喜欢影视剧中的机械外骨骼——让仿生机器人随着我们的想法来运动一定是十分炫酷的事情;例如下面的图就是我们团队打造的一台SSVEP脑控仿生机器人和脑控机械臂样机:

   本着干货就要共享的原则,从今天开始,小蘑菇将用一个月左右的时间,带着大家一起打造一套用脑电/肌电系统控制的人体外骨骼,来实现类似我们动图中的炫酷机器人(当然由于资金有限我们可能会将机器人替换为一个机械臂......)

(1)看完本教程你将收获什么

    本教程将按照以下知识点来进行更新:

    1. 挑选一个合适的机械臂——机械臂的硬件设计与控制【预计2-3章】

    2.让机械臂动起来——ROS中机械臂控制系统的搭建与“MoveIt”仿真【预计2-3章】

    3. 给机械臂注入灵魂——脑电(EEG)的采集,前处理与分析【预计3-4章】

    4. 给机械臂注入灵魂——肌电(EMG)的采集,前处理与分析【预计3-4章】

    5. 打造听话的机械臂——生物电分析与机械臂之间的通讯和编码【预计3-4章】

    6. 打造智能的机械臂——基于MoveIt与Python的人机融合系统设计【预计3-4章】

    当然,这个教程里可能涉及一些关于ROS,Python等基础知识,大家想了解的话可以翻回我之前的系列教程【查看之前的教程】

    下面闲话少说,我们正式开始我们的第一小节~


    第一节——机械臂的电路硬件与底层驱动程序设计

    为了给大家一个对硬件直观的印象,我们把这个教程所需要的一些关键硬件列出来了,左边是机械臂的部分,右边是人体EEG/EMG采集的基本硬件(后续我会逐个教程更新硬件的采购链接,总价格大概6000RMB)

   

1.1 机械臂的多自由度与多路舵机电路设计

    跟我们人类的手臂一样,每一台机械臂的基本控制单元都是关节(Joint),一个机械臂的关节数量越多,它的控制自由度(Degree of Freedom)越高。

    像我们这种比较低成本的机械臂,它的关节一般都是由舵机(Servo)来控制的;想让机械臂保持一定的姿态,就需要让它关节的舵机保持到一个角度,如下图就是一个2自由度机械臂在不同运动状态下的舵机角度:

    那么为了控制机械臂的实时姿态,我们就需要不断的调整机械臂的关节角度,而这个角度就通过放在关节上的舵机来调节。

    一般的舵机上都有三根线(如下图,图来自网络),其中中间那根红线一般都是舵机供电的正极;棕色是负极;黄色代表控制线(不同舵机颜色不一样,具体参考说明书)。由于舵机一般需要保持姿态,这期间就会耗费很大的电流,所以舵机的正负极之间的最大电流一般会达到3-5A,而控制线的电流则小的多,单片机IO口输出的电流就够用。

    驱动多路舵机的电路图设计也相对比较简单,如下图所示(本图源设计图来自Oskar魏),可以看出这是一个支持8路舵机的组合驱动,其中P5,P6,P7元器件分别接入8路电机的信号线,正极,负极。值得注意的是多路舵机在极端条件下的电流消耗是巨大的,为了保证我们的机械臂不会在关键时刻突然阳痿,周边电路支持的总电流起码要达到舵机数量*2A。

   

1.2 多路舵机控制的底层软件驱动支持

    我们之前的教程中也讲过,舵机的角度是通过脉宽电平(Pluse Width)控制的。 如果我们将脉宽输出的频率固定(20ms),然后调整占空比,让高电平的宽度在0.5(舵机逆时针转到头)到2.5(顺时针转到头)ms,就可以实现高电平脉宽的控制了。

  我们所采用的底层主控是STM32单片机,控制的基本原理是使用Timer的输出比较模式;具体的控制代码如下,当然我们也可以使用中断来控制,这样占用的Timer数量会少一些,但是控制的准确度会有所下降:

/*****************************初始化代码*********************************/
void Servo_Bundle1_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStructure;
	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
	TIM_OCInitTypeDef  TIM_OCInitStructure;
  // 输出比较通道1和2 GPIO 初始化
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
  GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_6 | GPIO_Pin_7;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	// 输出比较通道3和4 GPIO 初始化
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
  GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_0 | GPIO_Pin_1;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	
	// 开启定时器时钟,即内部时钟CK_INT=72M
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
 
/*--------------------时基结构体初始化-------------------------*/
	// 配置周期,这里配置为20ms
	
	// 自动重装载寄存器的值,累计TIM_Period+1个频率后产生一个更新或者中断
	TIM_TimeBaseStructure.TIM_Period = SERVO_PWM_RESOLUTION;	
	TIM_TimeBaseStructure.TIM_Prescaler = SERVO_TIM_PSC_APB1;
	TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;		
	TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;		
	TIM_TimeBaseStructure.TIM_RepetitionCounter=0;	
	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
 
	/*--------------------输出比较结构体初始化-------------------*/	
	
	
	// 配置为PWM模式1
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
	// 输出使能
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
	// 输出通道电平极性配置	
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
	
	// 输出比较通道 1
	TIM_OCInitStructure.TIM_Pulse = SERVO_DEFAULT_DUTY;
	TIM_OC1Init(TIM3, &TIM_OCInitStructure);
	TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable);
	
	// 输出比较通道 2
	TIM_OCInitStructure.TIM_Pulse = SERVO_DEFAULT_DUTY;
	TIM_OC2Init(TIM3, &TIM_OCInitStructure);
	TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable);
	
	// 输出比较通道 3
	TIM_OCInitStructure.TIM_Pulse = SERVO_DEFAULT_DUTY;
	TIM_OC3Init(TIM3, &TIM_OCInitStructure);
	TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable);
	
	// 输出比较通道 4
	TIM_OCInitStructure.TIM_Pulse = SERVO_DEFAULT_DUTY;
	TIM_OC4Init(TIM3, &TIM_OCInitStructure);
	TIM_OC4PreloadConfig(TIM3, TIM_OCPreload_Enable);
	
	// 使能计数器
	TIM_Cmd(TIM3, ENABLE);
}
/************************************控制代码*****************************************/
void Set_Servo_PWM(uint8_t channel, uint16_t pwm)
{
	TIM_TypeDef *tim;
	//第六路Servo连接的是雷达的控制端
	if(channel==6) return;
	if(channel > 8)
	{
		return;
	}
	
	// 如果PWM值不在区间[500,2500]内,则不处理
	if(pwm > 2500)
	{
		return;
	}
	else if(pwm < 500)
	{
		return;
	}
	
	if(channel > 4)
	{
		tim = TIM8;
		channel -= 4;
	}
	else
	{
		tim = TIM3;
	}
	
	switch(channel)
	{
		case 1: tim->CCR1 = pwm; break;
		case 2: tim->CCR2 = pwm; break;
		case 3: tim->CCR3 = pwm; break;
		case 4: tim->CCR4 = pwm; break;
		
		default:
			break;
	}
}

  1.3 让机械臂做程序化运动

    想让机械臂完整的做复杂动作,就需要不断的同时调整6路舵机的角度。实现这个目的的方法有很多,比如我们在Main函数中不断对其进行调整,或者在接收到数据的关键函数中对机械臂中某个舵机的某个角度进行调节。

    在下一节中,我们将会使用蓝牙串口,让机械臂硬件按照固定的流程进行运动,然后来完成整个机械臂硬件底层的搭建~

本章小结

    本章主要介绍了这个系列教程的章节安排,知识点节构;初步介绍了机械臂部分最底层控制单元——关节的硬件实现。

    下一节我们将基于一套完整的控制和机械臂通讯程序,实现编码信号对舵机的完整控制~

联系作者