书接上回的硬件篇STM32控制四自由度机械臂(SG90舵机)(硬件篇)(简单易复刻)-CSDN博客
此时硬件平台已经搭建完毕,软件总共设计了三种模式,分别为
模式1:摇杆&蓝牙模式,此模式下可用摇杆或手机操作机械臂
模式2:示教器模式,此模式下由电位器控制机械臂
模式3:执行记忆动作,此模式下机械臂重复数组/链表中存储的动作
三种模式的切换以及存储动作可由按键或者手机蓝牙切换。
代码使用了FREERTOS操作系统,接下来我会将代码分开解析,这样大家看完后再结合起来回顾整体,就会更加容易理解了,请大家不要着急,慢慢看下去,一定能够复刻成功。
先说**模式1(摇杆&蓝牙)**的代码,其中cmd_BLE是蓝牙的数据,adc_dma[0-3]是摇杆的数据。
cs
void check_sg_cmd()//摇杆&蓝牙模式控制舵机函数
{
check_A();//舵机A
check_B();//舵机B
check_C();//舵机C
check_D();//舵机D
}
//舵机A,夹爪 CH4_B11-D1;adc4_A3
void check_A()
{
if(Mode == 1)
{
if((cmd_BLE == 'c' || adc_dma[3] > 4000) && angle[3] < 90)//合
{
angle[3]++;
}
else if((cmd_BLE == 'o' || adc_dma[3] <1000) && angle[3] > 0)//开
{
angle[3]--;
}
}
}
//舵机B,上下 CH3_B10-D2;adc3_A2
void check_B()
{
if(Mode == 1)
{
if((cmd_BLE == 'u' || adc_dma[2] <1000) && angle[2] < 135)//上
{
angle[2]++;
}
else if((cmd_BLE == 'd' || adc_dma[2] > 4000) && angle[2] > 45)//下
{
angle[2]--;
}
}
}
//舵机C,前后 CH2_B3-D3;adc2_A1
void check_C()
{
if(Mode == 1)
{
if((cmd_BLE == 'f' || adc_dma[1] <1000) && angle[1] < 135)//前
{
angle[1]++;
}
else if((cmd_BLE == 'b' || adc_dma[1] > 4000) && angle[1] > 45)//后
{
angle[1]--;
}
}
}
//舵机D,底座 CH1_A15-D0;adc1_A0
void check_D()
{
if(Mode == 1)
{
if((cmd_BLE == 'l' || adc_dma[0] <1000) && angle[0] < 180)//左
{
angle[0]++;
}
else if((cmd_BLE == 'r' || adc_dma[0] > 4000) && angle[0] > 0)//右
{
angle[0]--;
}
}
}
再看模式2(示教器)的代码,先通过translate()获取舵机目标角度,再通过reach_target()控制舵机达到目标角度,还有什么不懂的代码里的注释也很详细。
cs
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]++;
}
}
}
void translate()//示教器获取舵机目标角度函数
{
//adc_dma[4-7]表示四个电位器,将电位器数据转换成目标角度
angle_target[3] = (uint8_t)((double)adc_dma[7] / 22.75)/2;
angle_target[2] = (uint8_t)((double)adc_dma[6] / 22.75);
angle_target[1] = (uint8_t)((double)adc_dma[5] / 22.75) - 10;
angle_target[0] = 180 - (uint8_t)((double)adc_dma[4] / 22.75);//180减只是改变一下电位器方向,不影响总体
//限幅
if(angle_target[1]<45) angle_target[1]=45;
else if(angle_target[1]>135) angle_target[1]=135;
if(angle_target[2]<45) angle_target[1]=45;
else if(angle_target[2]>135) angle_target[1]=135;
}
再看模式3(记忆动作)的代码,memory[i][j]数组里存储的就是记忆数据。
cs
void get_target()//获取记忆角度数据,这里用的是数组存储记忆数据
{
angle_target_flag = 0;
for(j=0;j<4;j++)
{
if(angle[j] == angle_target[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]++;
}
}
}
到这里上述说的三个模式的代码都在代码工程的PWM.c中可以找到。
接下来给大家下说一下freertos.c里面主要的三个任务 ,分别是舵机任务Start_check_angle() ,主要用于在不同的模式下控制舵机,在舵机任务我们也能看到上述提到了三个模式下的函数,蓝牙串口S任务tart_usart_show() ,主要用于打印信息到手机app上,这里并没有通过手机控制机械臂的函数,那个在串口函数里,后面会说,oled显示屏任务Start_OLED_Task(),和蓝牙串口任务类似,只不过它是打印信息到oled显示屏上。
cs
void Start_check_angle(void const * argument)//舵机任务
{
/* USER CODE BEGIN Start_check_angle */
//开启4路PWM
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_4);
/* Infinite loop */
for(;;)
{
if(Mode == 1)
{
//摇杆&蓝牙模式
check_sg_cmd();
}
else if(Mode == 2)
{
//示教器模式
translate();
reach_target();
}
else if(Mode == 3)
{
//执行记忆动作
get_target();
reach_target();
}
if_BLE_cmd();//蓝牙控制记忆动作模式
//输出PWM波控制舵机运动
__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_1, Angle(angle[0]));
__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_2, Angle(angle[1]));
__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_3, Angle(angle[2]));
__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_4, Angle(angle[3]));
osDelay(15);//通过调整此延时可以改变机械臂运行速度
}
/* USER CODE END Start_check_angle */
}
void Start_usart_show(void const * argument)//蓝牙串口任务
{
/* USER CODE BEGIN Start_usart_show */
/* Infinite loop */
for(;;)
{
printf("Angle = {%d, %d, %d, %d}\r\n",angle[0],angle[1],angle[2],angle[3]);
printf("adc_dma1 = {%d, %d, %d, %d}\r\n",adc_dma[0],adc_dma[1],adc_dma[2],adc_dma[3]);
printf("adc_dma2 = {%d, %d, %d, %d}\r\n",adc_dma[4],adc_dma[5],adc_dma[6],adc_dma[7]);
printf("\r\n");
osDelay(1000);
}
/* USER CODE END Start_usart_show */
}
void Start_OLED_Task(void const * argument)//oled显示屏任务
{
/* USER CODE BEGIN Start_OLED_Task */
Oled_Init();
Oled_Clear();
/* Infinite loop */
for(;;)
{
//串口数据的字符串拼装,speed是格子,每个格子1cm
sprintf(speedMes,"A: %d ",angle[0]);
sprintf(speedMes1,"B: %d ",angle[1]);
sprintf(speedMes2,"C: %d ",angle[2]);
sprintf(speedMes3,"D: %d ",angle[3]);
sprintf(speedMes4,"Mode %d ",Mode);
sprintf(speedMes5,"S %d ",i);
Oled_Show_Str(1,5,speedMes);
Oled_Show_Str(1,69,speedMes1);
Oled_Show_Str(2,5,speedMes2);
Oled_Show_Str(2,69,speedMes3);
Oled_Show_Str(4,0,speedMes4);
Oled_Show_Str(4,64,speedMes5);
osDelay(500);
}
/* USER CODE END Start_OLED_Task */
}
接下来给大家说一下有关蓝牙部分的相关代码以及蓝牙手机app的使用
首先是usart.c里的一段代码,这里最重要的部分就是通过蓝牙模式控制机械臂时候,切换模式只需发送M1,M2,M3就可切换相应模式,而执行相关动作时,必须在发送的指令前面加上A开头,例如Ao就是夹爪开。
cs
/*机械臂控制模式,默认为1
1:摇杆控制
2:示教器控制
3:执行记忆动作
*/
uint8_t Mode = 1;
/*蓝牙控制机械臂指令:
s/m 停/储存当前动作
l/r 左右
u/d 上下
f/b 前后
o/c 开合*/
uint8_t cmd_BLE = 's';
// 串口中断:接收完成回调函数,收到一个数据后,在这里处理
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 判断中断是由哪个串口触发的
if(huart->Instance == USART1)
{
// 判断接收是否完成(UART1_RX_STA bit15 位是否为1)
if((UART1_RX_STA & 0x8000) == 0)
{
// 如果已经收到了 0x0d (回车),
if(UART1_RX_STA & 0x4000)
{
// 则接着判断是否收到 0x0a (换行)
if(buf == 0x0a)
{
// 如果 0x0a 和 0x0d 都收到,则将 bit15 位置为1
UART1_RX_STA |= 0x8000;
//=======中断信息处理=======
//模式切换在手机蓝牙app中只需发送M1,M2,M3即可切换模式
if (!strcmp((const char *)UART1_RX_Buffer, "M1"))
{
Mode = 1;
printf("摇杆模式\r\n");
}
else if(!strcmp((const char *)UART1_RX_Buffer, "M2"))
{
Mode = 2;
printf("示教模式\r\n");
}
else if(!strcmp((const char *)UART1_RX_Buffer, "M3"))
{
Mode = 3;
printf("执行记忆动作\r\n");
}
//蓝牙指令的切换则必须以A开头,例:发送Ao就是夹爪开
else if(UART1_RX_Buffer[0] == 'A')
{
cmd_BLE = UART1_RX_Buffer[1];
}
else {
if(UART1_RX_Buffer[0] != '\0')
printf("指令发送错误:%s\r\n", UART1_RX_Buffer);
}
//==========================
memset(UART1_RX_Buffer, 0, strlen((const char *)UART1_RX_Buffer));
// 重新开始下一次接收
UART1_RX_STA = 0;
//==========================
}
else
// 否则认为接收错误,重新开始
UART1_RX_STA = 0;
}
else // 如果没有收到了 0x0d (回车)
{
//则先判断收到的这个字符是否是 0x0d (回车)
if(buf == 0x0d)
{
// 是的话则将 bit14 位置为1
UART1_RX_STA |= 0x4000;
}
else
{
// 否则将接收到的数据保存在缓存数组里
UART1_RX_Buffer[UART1_RX_STA & 0X3FFF] = buf;
UART1_RX_STA++;
// 如果接收数据大于UART1_REC_LEN(200字节),则重新开始接收
if(UART1_RX_STA > UART1_REC_LEN - 1)
UART1_RX_STA = 0;
}
}
}
// 重新开启中断
HAL_UART_Receive_IT(&huart1, &buf, 1);
}
}
然后是PWM.c中的if_BLE_cmd(),这里蓝牙控制记忆动作模式同样需要在发送指令前面加上一个A开头才可以。
cs
//蓝牙控制记忆动作模式
void if_BLE_cmd()
{
switch(cmd_BLE)
{
case 'm':
if(i < location_cnt)
{
for(j=0;j<4;j++)
{
memory[i][j] = angle[j];
}
printf("储存动作\r\n");
cmd_BLE = 's';
i++;
}
else
{
printf("动作已满\r\n");
cmd_BLE = 's';
}
break;
case 'g':
for(i=0;i < location_cnt;i++)
{
for(j=0;j<4;j++)
{
printf("%d ",memory[i][j]);
}
printf("\r\n");
if(memory[i][j] == '\0') break;
}
cmd_BLE = 's';
break;
case 'D':
for(i=0; i < location_cnt ;i++)
{
memset(memory[i],'\0',4);
}
i = 0;
printf("已清除动作");
cmd_BLE = 's';
break;
}
}
最后介绍一下HC蓝牙助手手机app(APP会在文末与代码一起开源分享给大家)的使用方法 ,话不多说,直接上图。

接下来再给大家介绍一下按键控制的代码,A0按键切换模式1(摇杆&蓝牙模式),A1按键切换模式2(示教器模式),B4按键切换模式3(执行记忆动作),B5按键用于存储动作。
cs
void Callback01(void const * argument)
{
/* USER CODE BEGIN Callback01 */
anti_shake = 0;
/* USER CODE END Callback01 */
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)//按键任务
{
switch(GPIO_Pin)
{
case GPIO_PIN_0://A0按键
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_11, GPIO_PIN_RESET);//led亮
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_SET);//led灭
Mode = 1;
printf("摇杆模式\r\n");
break;
case GPIO_PIN_1://A1按键
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_11, GPIO_PIN_SET);//led灭
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_RESET);//led亮
Mode = 2;
printf("示教模式\r\n");
break;
case GPIO_PIN_4://B4按键
if(anti_shake == 0)
{
anti_shake = 1;
Mode = 3;
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_11, GPIO_PIN_RESET);//led亮
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_RESET);//led亮
printf("执行记忆动作\r\n");
osTimerStart(myTimer01Handle,800);
}
break;
case GPIO_PIN_5://B5按键
if(anti_shake == 0)
{
//软件消抖
anti_shake = 1;
osTimerStart(myTimer01Handle,800);
//存储动作
if(i<location_cnt)
{
for(j=0;j<4;j++)
{
memory[i][j] = angle[j];
}
printf("储存动作\r\n");
i++;
}
else if(i>=9)
{
printf("动作已满\r\n");
}
}
break;
}
}
到此为止,核心代码基本上都介绍差不多了,还有一些例如i2c.c,OLED.c显示屏引脚初始化用,gpio.c按键以及led初始化使用,adc.c,dma.c摇杆以及电位器初始化使用,tim.c舵机pwm初始化使用,这些代码主要是初始化相关引脚,而相关引脚对应在硬件篇我已经介绍过了,当然对于这些代码感兴趣的也可以自己去代码工程中看。
最后再带大家简单看一下main.c中的代码:
cs
int main(void)
{
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init(); //GPIO初始化,四个按键和两个LED
MX_DMA_Init(); //DMA初始化
MX_ADC1_Init(); //ADC初始化,2个摇杆用and4个电位器用
MX_TIM2_Init(); //定时器PWM初始化,舵机用
MX_USART1_UART_Init();//串口初始化,蓝牙用
MX_I2C1_Init(); //I2C初始化,oled显示屏用
/* USER CODE BEGIN 2 */
printf("Start\r\n");//程序开始运行
HAL_ADC_Start_DMA(&hadc1,(uint32_t *)adc_dma,8); //开启ADC和DMA
HAL_Delay(500);
/* Call init function for freertos objects (in freertos.c) */
MX_FREERTOS_Init();//freertos任务初始化
/* Start scheduler */
osKernelStart();//freertos开启任务调度
while (1)
{
}
}
相信大家结合这篇软件篇代码解说来阅读工程代码看到这里就算不会freertos的人也已经复刻成功了,创造不易,感谢c友的一键三联,球球了,这个对我真的很重要!
工程文件和蓝牙资料的链接给大家放这里了!!!http://通过网盘分享的文件:stm32机械臂源代码.zip 链接: https://pan.baidu.com/s/1o1IBFdVu9Ybk9gXJIZTY8Q 提取码: 0531