基于STM32+FreeRTOS的四轴机械臂

【投稿赢 iPhone 17】「我的第一个开源项目」故事征集:用代码换C位出道! 10w+人浏览 1.7k人参与

一、介绍

这次项目是一款由按键控制模式的机械臂,它能实现的功能:模式 1,使用摇杆控制移动抓取;模式 2,使用电位器进行控制机械臂 ;按下按键 1,接着使用摇杆或电位器做动作,系统就将当前的角度值存进一个数组里,按键 2,将保存的角度值通过 OLED 打印出来,按键 3,删除所有保存的角度值;最后,模式 3,将保存的角度值展示出来,实现了简单的自动化。

在这里插入图片描述

二、硬件设计

2.1.外部零件

本次项目需要 3D 打印机械臂的结构,在某宝或某鱼购买即可,另外的硬件:

  • 两个摇杆模块
  • 一个 OLED 屏幕
  • 一个电源模块(也可以直接上电池)
  • 四个舵机
  • 四个电位器
  • 按键(按需求)
  • 面包板
  • 若干杜邦线与飞线
  • W25Q64 存储模块(可选)

2.2.电路连接

PA0 ~ PA3 引脚,分别是两个摇杆的 X 轴和 Y轴;PA4 ~ PA7 引脚,分别接四个电位器;PB4 ~ PB7 引脚,接四个舵机,使用定时器 3、4 输出 PWM 控制舵机;PA11、PA12、PB10、PB11 接四个按键:

在这里插入图片描述

另一块面包板一端接电源模块,为上面的两行提供稳定的电压和 GND,为多个设备提供电源,用杜邦线将最小系统板的 GND 接到有电源模块的 GND,这样共地形成回路才能正常运行。电源模块有两个插口,一个是圆形的插口和 USB 插口,哪一边插上了插口,哪一边就有电压,电压可以选择 3.3V 和 5V,舵机和摇杆需要较高电压才能正常工作,因此将黄色的跳线帽接到 5V 这边,如果电压不足,舵机会抽搐,摇杆的模拟值会一直现实最大,切记:电源模块也有正负极,接反了容易击穿降压芯片。

在这里插入图片描述

电位器和按键都在倒立摆项目的电路板上,通过查询倒立摆项目的代码,得知电位器和按键的引脚接在最小系统板的哪一个引脚上,用杜邦线将其引出来就是电位器和按键的引脚了,使用这两个模块也需要插电给其供电。

在这里插入图片描述

三、软件设计

3.1.STM32CubeMX 搭建工程

RCC:配置外部高速晶振

在这里插入图片描述

SYS:Debug 设置成 Serial Wire

在这里插入图片描述

配置时钟树,如下图:

在这里插入图片描述

将 PA0 ~ PA7 配置为 ADC1 的通道 0 ~ 7,开启连续模式和扫描模式:

在这里插入图片描述

打开 DMA 的通道,选择循环模式,连续不断的搬运数据,减少 CPU 的压力,让硬件之间进行数据搬运:

在这里插入图片描述

将 PB4 ~ PB7 引脚,配置为 TIM3 和 TIM4 的 PWM 输出,分频系数使用 72 - 1,重装载值使用 20000 - 1,这样搭配可以将频率设置成 50Hz = 20ms,对应舵机的周期要求,CRR 的 500 对应 0.5ms:0°;2500 对应 2.5ms:180°

在这里插入图片描述

在这里插入图片描述

3.2.摇杆控制模式

在 CubeMX 里配置好 ADC、DMA、TIM,就可以实现摇杆控制四个舵机,定义两个数组,一个用来保存 DMA 转换后的 AD 值;一个用来保存每个舵机的角度值,角度值初始为 90° 。编写一个函数,将角度值放到参数里,获取对应的PWM输出。每个舵机的转动角度,最好不要设置在 0° 或者 180°,当舵机转到边界值时会瞬间增长到 256 或者舵机抽搐,每个角度设计按实际需要设计:

uint16_t Joystick_AD_Value[8];		//摇杆模块和电位器模块
uint8_t Angle[4] = {45,90,90,90};

//根据输入0~180°获取对应PWM占空比参数
//这里要用16位的大小,8位最大的数值位255,这样导致舵机不运行
uint16_t Angle_Set(uint8_t pwm_pulse)
{
	return (pwm_pulse * 2000 / 180) + 500;	//0~180°,对应500~2500,对应0.5ms~2.5ms
}

下面是爪子抓取的函数,当摇杆往最小数值的方向拨,张开爪子,反之关闭,并设置死区,大于 0° ~ 120°:

void Clip(void)
{
	if(Joystick_AD_Value[0] == 0)
	{
		Angle[0] += 5;
		if(Angle[0] == 120)
		{
			Angle[0] = 120;
		}
	}
	else if(Joystick_AD_Value[0] == 4095)
	{
		Angle[0] -= 5;
		if(Angle[0] == 0)
		{
			Angle[0] = 0;
		}
	}
}

下面是机械臂转动的函数,当摇杆往最小数值的方向拨动,向左转动,反之向右转动:

void Spin(void)
{
	if(Joystick_AD_Value[1] <= 2000)
	{
		Angle[1] += 5;
		if(Angle[1] >= 170)
		{
			Angle[1] = 170;
		}
	}
	else if(Joystick_AD_Value[1] == 4095)
	{
		Angle[1] -= 5;
		if(Angle[1] <= 20)
		{
			Angle[1] = 20;
		}
	}
}

下面是机械臂大臂转动控制前后移动的函数,当摇杆往最小数值的方向拨动,向前转动,反之向后转动:

void FrBa(void)
{
	if(Joystick_AD_Value[2] <= 2000)
	{
		Angle[2] += 5;
		if(Angle[2] >= 160)
		{
			Angle[2] = 160;
		}
	}
	else if(Joystick_AD_Value[2] == 4095)
	{
		Angle[2] -= 5;
		if(Angle[2] <= 45)
		{
			Angle[2] = 45;
		}
	}
}

下面是机械臂小臂转动控制爪子上下移动的函数,当摇杆往最小数值的方向拨动,向上转动,反之向下转动:

void HiLo(void)
{
	if(Joystick_AD_Value[3] <= 2000)
	{
		Angle[3] += 5;
		if(Angle[3] >= 130)
		{
			Angle[3] = 130;
		}
	}
	else if(Joystick_AD_Value[3] == 4095)
	{
		Angle[3] -= 5;
		if(Angle[3] <= 40)
		{
			Angle[3] = 40;
		}
	}
}

在 main 函数里开启 DMA 转换、PWM 输出,并设置每个 TIM 通道的占空比:

int main(void)
{
  HAL_ADC_Start_DMA(&hadc1,(uint32_t *)adc_dma,8); 
  
  HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
  HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);
  HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1);
  HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_2);
  HAL_Delay(500);
  
  while (1)
  {
	Clip();
	Spin();
	FrBa();
	HiLo();
	
	__HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_1, Angle_Servo(Angle[0]));
	__HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_2, Angle_Servo(Angle[1]));
	__HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_1, Angle_Servo(Angle[2]));
	__HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_2, Angle_Servo(Angle[3]));
	
	HAL_Delay(20);
  }
}

3.3.示教器模式

编写按键 Key.c 的驱动代码:

#include "stm32f1xx_hal.h"
#include "gpio.h"

uint8_t KeyNum = 0;		//定义变量,默认键码值为0

uint8_t Key_GetNum(void)
{		
	if (HAL_GPIO_ReadPin(GPIOB, KEY3_Pin) == GPIO_PIN_RESET)			
	{
		HAL_Delay(20);											
		while (HAL_GPIO_ReadPin(GPIOB, KEY3_Pin) == GPIO_PIN_RESET);	
		HAL_Delay(20);											
		KeyNum = 1;												
	}	
	if (HAL_GPIO_ReadPin(GPIOB, KEY4_Pin) == GPIO_PIN_RESET)			
	{
		HAL_Delay(20);											
		while (HAL_GPIO_ReadPin(GPIOB, KEY4_Pin) == GPIO_PIN_RESET);	
		HAL_Delay(20);											
		KeyNum = 2;												
	}
	if (HAL_GPIO_ReadPin(GPIOA, KEY2_Pin) == GPIO_PIN_RESET)			
	{
		HAL_Delay(20);											
		while (HAL_GPIO_ReadPin(GPIOA, KEY2_Pin) == GPIO_PIN_RESET);	
		HAL_Delay(20);											
		KeyNum = 3;												
	}	
	if (HAL_GPIO_ReadPin(GPIOA, KEY1_Pin) == GPIO_PIN_RESET)			
	{
		HAL_Delay(20);											
		while (HAL_GPIO_ReadPin(GPIOA, KEY1_Pin) == GPIO_PIN_RESET);	
		HAL_Delay(20);											
		KeyNum = 4;												
	}
	return KeyNum;			//返回键码值,如果没有按键按下,所有if都不成立,则键码为默认值0
}

按下按键 1,进入示教模式,将获得到的角度值保存在数组里:


void translate(void)	//将四个电位器的模拟值0~4095映射到0~180
{						//把采集的模拟值转变为角度。即0~4095变为0~180,除以22.75即可。
	Angle[0] = (uint8_t)((double)AD_Value[4] / 22.75);
	Angle[1] = (uint8_t)((double)AD_Value[5] / 22.75);
	Angle[2] = (uint8_t)((double)AD_Value[6] / 22.75);
	Angle[3] = (uint8_t)((double)AD_Value[7] / 22.75);
}

定义一个变量来记录模式的转变:

//记录模式的转变
extern uint8_t Mode;

void Mode_Change()
{
	if(Mode == 1)//摇杆控制模式
	{
		Clip();
		Spin();
		FrBa();
		HiLo();
	}
	else if(Mode == 2)//电位器控制模式
	{
		translate();
	}
}

在模式 2 里面,进行保存当前角度,当按键 1 按下时,将后面所做的角度保存在memory里;按下按键 2 时,在 OLED 上打印保存的角度值;按下按键 3 时,删除所有保存的记录:

uint8_t memory[10][4];		//定义一个二维数据来保存4个舵机的角度,可以存10组动作
extern uint8_t KeyNum;
uint8_t i,j = 0;

void if_BLE_cmd(void)
{
	switch(KeyNum)
	{
		case 1:
			if(i < 10)
			{
				for(j=0;j<4;j++)
				{
					memory[i][j] = Angle[j];
				}
				OLED_Update();
				OLED_Printf(1, 1, OLED_6X8, "Save,OK");
				KeyNum = 0;
				i++;
			}
			else
			{
				OLED_Update();
				OLED_Printf(1, 1, OLED_6X8, "Full");
				KeyNum = 0;
			}
		break;
		case 2:
			for(i=0;i<10;i++)
			{
				for(j=0;j<4;j++)
				{
					OLED_Update();
					OLED_ShowNum(9, 9, memory[i][j] + 0x30, 4, OLED_6X8);
				}
				if(memory[i][j] == '\0')	break;
			}
		break;
		case 3:
			for(i=0;i<10;i++)
			{
				memset(memory[i],'\0',4);
			}
			i = 0;
			OLED_Update();
			OLED_Printf(1, 1, OLED_6X8, "Delete,OK");
			KeyNum = 0;
		break;
	}	
}

3.4.执行记录的动作

执行记录动作的函数也是在 pwm.c 里编写,第一个函数是:从保存角度的数组里,用当前角度值获取和执行;第二个函数是:角度值像向角度目标值靠近,用于简单防抖和执行记忆动作。


uint8_t memory[10][4];
uint8_t i,j = 0;
uint8_t angle_target_flag = 0;

void get_target()//从数组获得位置信息并转换位角度目标值
{
	angle_target_flag = 0;
	
	for(i=0;i<10;i++)
	{
		for(j=0;j<4;j++)
		{
			if(Angle[j] == memory[i][j])	
			{
				angle_target_flag++;
			}
		}
	}
	if(angle_target_flag == 4)
	{
		i++;
	}
	//动作执行完毕之后,就删除之前保存的动作
	for(j=0;j<4;j++)
	{
		if(memory[i][j] == '\0')
		{
			i = 0;
		}
		angle_target[j] = memory[i][j];
	}
}
void reach_target()
{
	for(j = 0;j <4;j++)
	{
		if(Angle[j] > angle_target[j])
		{
			Angle[j]--;
		}
		else if(Angle[j] < angle_target[j])
		{
			Angle[j]++;
		}
	}
}

四、添加FreeRTOS

4.3.创建FreeRTOS

在左边栏目的第三方软件里,选择 FREERTOS:

在这里插入图片描述

然后选择任务和队列的选项卡,添加需要的任务,本次实现只需要两个任务即可,一个用来 OLED 打印数据,一个用来实现机械臂的系统:

在这里插入图片描述
在这里插入图片描述

再创建一个定时器任务,用来输出警告,超过了一定时间,就会跳转到一个由用户设定的函数,进行错误报错:

在这里插入图片描述
在这里插入图片描述

添加了 FreeRTOS 之后,需要将裸机的系统时钟改为由 TIM1 来提供,任意一个空闲的时钟即可,因为 FreeRTOS 使用了外部硬件的时钟,如果都使用同一个时钟,容易产生冲突,导致时钟混乱,而 FreeRTOS 就是需要一个严格的时钟计时:

在这里插入图片描述

4.2.OLED

添加 OLED 驱动代码,可以使用 B 站江科大的驱动代码,移植的时候需要注意一些问题:

  • 在 MX 里添加引脚

在这里插入图片描述

在这里插入图片描述

  • 修改 OLED.c 文件代码,还需再魔术棒的 C / C++ 选项卡里,Misc Controls 填写--no-multibyte-chars,这样修改就可以正常使用
#include "stm32f1xx_hal.h"	//头文件包含需要修改成这样

void OLED_W_SCL(uint8_t BitValue)
{
	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, (GPIO_PinState)BitValue);	//置高低电平的函数也需要修改
}

void OLED_W_SDA(uint8_t BitValue)
{
	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, (GPIO_PinState)BitValue);
}

//注意看大小写
void OLED_GPIO_Init(void)
{
    __HAL_RCC_GPIOB_CLK_ENABLE();

	GPIO_InitTypeDef GPIO_InitStructure;
 	GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_OD;
	GPIO_InitStructure.Speed = GPIO_SPEED_HIGH;
	GPIO_InitStructure.Pin = GPIO_PIN_9;
 	HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
	GPIO_InitStructure.Pin = GPIO_PIN_8;
 	HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
}

freeRTOS.c 文件里,使用动态创建 OLED 的任务,将当前角度值和电位器的模拟值打印出来:

#define osPriorityNormal           	   1
#define Start_OLED_TASK_STACK_SIZE     128
TaskHandle_t OLED_TaskHandle ;
void Start_OLED_Task(void *pvParameters);

void freertos_demo(void)
{
    xTaskCreate((TaskFunction_t)    OLED_Task,
                (char*)             "Start_OLED_Task",
                (uint16_t)          Start_OLED_TASK_STACK_SIZE,
                (void*)             NULL,
                (UBaseType_t)       osPriorityNormal,
                (TaskHandle_t*)     &OLED_TaskHandle );
    vTaskStartScheduler();  		//开启任务调度器
}

void Start_OLED_Task(void const * argument)
{
	OLED_Printf(1, 1, OLED_6X8, "Num:");
	OLED_Printf(30, 1, OLED_6X8, "RP:");

  while(1)
  {
	OLED_ShowNum(1, 9, Angle[0], 4, OLED_6X8);
	OLED_ShowNum(1, 18, Angle[1], 4, OLED_6X8);
	OLED_ShowNum(1, 27, Angle[2], 4, OLED_6X8);
	OLED_ShowNum(1, 36, Angle[3], 4, OLED_6X8);	

	OLED_ShowNum(30, 9, AD_Value[4], 4, OLED_6X8);
	OLED_ShowNum(30, 18, AD_Value[5], 4, OLED_6X8);
	OLED_ShowNum(30, 27, AD_Value[6], 4, OLED_6X8);
	OLED_ShowNum(30, 36, AD_Value[7], 4, OLED_6X8);
	OLED_Update();
    osDelay(200);
  }
}

4.3.舵机的模式转换、控制与角度的保存删除

同样使用动态创建该任务:

#define osPriorityNormal					  1
#define Start_check_angle_TASK_STACK_SIZE     128
TaskHandle_t check_angleHandle ;
void Start_check_angle(void *pvParameters);

void freertos_demo(void)
{
    xTaskCreate((TaskFunction_t)    check_angle,
                (char*)             "Start_check_angle",
                (uint16_t)          Start_check_angle_TASK_STACK_SIZE,
                (void*)             NULL,
                (UBaseType_t)       osPriorityNormal,
                (TaskHandle_t*)     &check_angleHandle);
    vTaskStartScheduler();  		//开启任务调度器
}

void Start_Check_Angle(void const * argument)
{
	HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
	HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);
	HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1);
	HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_2);

  while(1)
  {
	  HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
	  if(Mode == 1)
	  {
		Joystick_Control();
	  }
	  else if(Mode == 2)
	  {
		translate();
		reach_target();
	  }
	  else if(Mode == 3)
	  {
		get_target();
		reach_target();
	  }
	  if_BLE_cmd();
	  
	  __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_1, Angle_Set(Angle[0]));
	  __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_2, Angle_Set(Angle[1]));
	  __HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_1, Angle_Set(Angle[2]));
	  __HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_2, Angle_Set(Angle[3]));

    osDelay(500);
  }
  /* USER CODE END Start_Check_Angle */
}

4.4.主函数

最后在主函数里调用freertos_demo(); 即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值