文章目录
一、介绍
这次项目是一款由按键控制模式的机械臂,它能实现的功能:模式 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(); 即可。
1万+

被折叠的 条评论
为什么被折叠?



