【STM32实战】机械臂快递分拣系统(一)——机械臂控制程序(下位机)编写
主要实现的是可以自动识别获取快递位置,机械臂可以抓取快递,以及根据自动识别快递上的条形码获得目的地点,机械臂可以将快递抓取并移动到目的地点,此外就是还可以通过上位机来控制机械臂,上位机可以是PC,也可以是另一部STM32,需要有图形化界面。
前言
近期回校上最后一门课,刚好是做机械臂有关的题目,所以写文记录一下。主要实现的是可以自动识别获取快递位置,机械臂可以抓取快递,以及根据自动识别快递上的条形码获得目的地点,机械臂可以将快递抓取并移动到目的地点,此外就是还可以通过上位机来控制机械臂,上位机可以是PC,也可以是另一部STM32,需要有图形化界面。
题目分析
自动识别获取快递位置以及识别快递上的条形码需要用到摄像头模块,需要解决STM32和摄像头模块之间的通信问题,以及快递形状。通过上位机来控制机械臂则需要使用到蓝牙或者WIFI模块,图形化界面可以使用物联网云平台来开发,这里用的是阿里云。如果使用另一部32进行控制,则还可在另一部32上做界面。
但购买的机械臂是已经组装完毕的,且上面带有STM32系统板,型号为STM32F103C8T6,只引出了串口3的接口,也就是说如果要连接摄像头模块,就无法与上位机进行通信。所以我打算将该机械臂作为一个下位机来接收命令,上位机采用另一部STM32来做,上位机来处理串口接收到的来自云平台或者摄像头的数据,然后算出各个舵机的PWM占空比重装载值,通过蓝牙模块发送到下位机机械臂,以此进行控制。
工程模板生成
这部分因人而异,如果是自己组装的机械臂,则PWM输出引脚设置上比较自由,但我这一款机械臂是已经组装完毕的,因此需要根据这一款机械臂的原理图来设置对应引脚,方便起见,这里采用STM32CubeMX来生成工程模板,机械臂上的STM32系统板引脚说明如下。
点击 ACCESS TO MCU SELECTOR
搜索并选择F103C8T6,如果你的主控是其他型号,就选择其他型号。
点击选择高速时钟为外部晶振。
如图所示设置时钟频率为72M。
设置串口1为 Asynchronous,这个串口在我这款机械臂的STM32系统板上接的是CH340,转成USB。
同样的方法设置串口3。
如果有Debug需求的话可以设置一下SW的调试方式,我刚创建这个工程模板的时候是有设置的,后来发现如果只是用来接收命令的话也没什么必要。
然后就是根据上面的引脚说明图设置PWM了,PB3在TIM2的CH2上。
PB4在TIM3的CH1上。
其他的引脚都在TIM4上,因为TIM4默认PWM通道就是这几个引脚,就不需要重映射。
然后设置一下TIM2,TIM3,TIM4的预分频系数71,重装载值19999,使能自动重装载即可。
然后设置一下定时器1,这个主要用来控制各任务运行周期。
然后就是在NVIC中使能一下中断。
然后设置好你要保存到路径和工程名字,IDE。
这里我习惯只复制用得到的库,以及生成额外的c和h文件。然后生成代码即可。
蓝牙模块的使用
蓝牙模块就是一种透传设备,串口给它发送什么,它就接收什么,然后它将接收到的数据通过某种协议发送到跟它配对的蓝牙上,这个蓝牙将这些数据解析,最后还原成最初的未经处理的数据,这就是透明传输意思。这跟使用杜邦线将两个模块串口的RT对接是一样的。
推荐购买的蓝牙模块是这种带有按键的,按下按键然后接到电脑(通电)即可进入AT模式,设置蓝牙配对。
配对方法也很简单,接CH340模块(需要注意RT对接),在按键按下的状态,将CH340接入电脑USB口,即可让蓝牙进入AT模式,一般此模式下蓝牙模块的波特率为38400,在串口工具上发送命令AT,勾选发送新行,即可看到窗口返回OK。
然后可以使用AT命令设置蓝牙名称AT+NAME=Bluetooth-Marster,然后点击发送,这里设置名称为Bluetooth-Marster,另一个蓝牙的名称可以设置为Bluetooth-Slave。
然后设置蓝牙配对码AT+PSWD=0425,然后点击发送,这里设置为0425,另一个蓝牙的配对码也要是这个,才能成功配对。
然后设置工作模式为主机模式,命令是AT+ROLE=1,然后点击发送,另一个蓝牙需要设置为从机模式,命令就是AT+ROLE=0
然后配置蓝牙串口参数AT+UART=115200,0,0,然后点击发送即可,两个蓝牙都要设置一样。
然后设置一下模式为无需地址绑定模式,这样只要配对码一样就能配对成功了。命令AT+CMODE=1,然后发送即可。
蓝牙模块主机这就配置完毕了,接下来配置从机,也就是上面所说的另一个蓝牙。
这里因为我的两个蓝牙不是一套的,所有前面报错。这个蓝牙设置配对码需要加上冒号。
然后设置为从机模式
设置波特率115200
设置任意地址绑定模式
然后就将两个CH340模块拔掉再插入电脑,即可在串口工具上测试蓝牙能否配对成功了。
需要注意的是现在的波特率就不是38400了。
这里发送111,可以看到另一个蓝牙成功接收到了,说明配对成功了。
蓝牙接收数据解析与机械臂控制
这里需要用到如下代码文件,将这些代码文件添加进工程即可。
uartconfig.c
/********************************************************************/
// 作用: STM32固件库串口配置
// 作者: FITQY
// 时间: 2022.08.29
/********************************************************************/
#include "uartconfig.h"
/********************************************************************
串口发送数据函数 Data_Transmit
各参数作用
DataTransmit *data: 选择要发送的串口发送数据结构体
UART_HandleTypeDef *huart, : 选择通过哪一个串口发送
********************************************************************/
void Data_Transmit(DataTransmit *data, UART_HandleTypeDef *huart)
{
HAL_UART_Transmit(huart, data -> transmit_data, data -> cnt , 0xFFFF);
}
/********************************************************************
串口打包发送数据函数 Data_Pack_Transmit
各参数作用
DataTransmit *data: 选择要发送的串口发送数据结构体
UART_HandleTypeDef *huart, : 选择通过哪一个串口发送
********************************************************************/
void Data_Pack_Transmit(DataTransmit *data, UART_HandleTypeDef *huart)
{
Data_Pack(data);
Data_Transmit(data,huart);
}
/********************************************************************
串口接收1个数据函数 Buffer_Receive
各参数作用
DataReceive *data: 选择通过哪一个串口接收数据结构体接收
UART_HandleTypeDef *huart, : 选择通过哪一个串口接收
********************************************************************/
uint8_t Buffer_Receive(DataReceive *data, UART_HandleTypeDef *huart)
{
HAL_UART_Receive_IT(huart, &data -> data, 1);
return data -> data;
}
uartconfig.h
#ifndef __UARTCONFIG_H
#define __UARTCONFIG_H
#include "stm32f1xx_hal.h"
#include "uartprotocol.h"
// 重命名方便定义
typedef signed char int8_t;
typedef short int int16_t;
typedef int int32_t;
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
// 串口发送相关函数
void Data_Transmit(DataTransmit *data, UART_HandleTypeDef *huart);
void Data_Pack_Transmit(DataTransmit *data, UART_HandleTypeDef *huart);
// 串口接收相关函数
uint8_t Buffer_Receive(DataReceive *data, UART_HandleTypeDef *huart);
#endif
uartprotocol.c
/********************************************************************/
// 作用: 串口发送与接收
// 作者: FITQY
// 时间: 2022.08.29
/********************************************************************/
#include "uartprotocol.h"
/********************************************************************
串口发送数据结构体初始化函数 Data_Transmit_Init
各参数作用
DataTransmit *data: 选择要初始化的串口发送数据结构体
head1: 帧头1
head2: 帧头2
length: 有效数据长度
********************************************************************/
void Data_Transmit_Init(DataTransmit *data, uint8_t head1, uint8_t head2, uint8_t length)
{
data -> head1 = head1;
data -> head2 = head2;
data -> length = length;
data -> cnt = length + 4;
for(uint8_t i = 0; i < length; i++)
{
data -> data[i] = 0;
}
for(uint8_t j = 0; j < length + 4; j++)
{
data -> transmit_data[j] = 0;
}
}
/********************************************************************
串口发送数据打包函数 Data_Pack
各参数作用
DataTransmit *data: 选择要打包的串口发送数据结构体
********************************************************************/
void Data_Pack(DataTransmit *data)
{
data -> transmit_data[0] = data -> head1;
data -> transmit_data[1] = data -> head2;
data -> transmit_data[2] = data -> length;
for(uint8_t i = 0; i < data -> length; i++)
{
data -> transmit_data[3+i] = data -> data[i];
}
uint8_t sum = 0;
for(uint8_t j = 0; j < data -> length + 3; j++)
{
sum = sum + data -> transmit_data[j];
}
data -> transmit_data[data -> length + 3] = sum;
}
// 至此串口数据发送函数定义结束 使用例如下
/*
DataTransmit data_transmit_uart; // 声明全局结构体 data_transmit_uart 这个要放在 main 函数外面
Data_Transmit_Init(&data_transmit_uart,0xAA,0xAA,1); // main函数中在 while 前 对结构体 data_transmit_uart 进行初始化 帧头都为 0xAA 有效数据长度为 1
data_transmit_uart.data[0] = 1; // 将发送的第一个数据赋值为 1 位置不限
// HAL库 使用方法
// 打包与发送分开
Data_Pack(&data_transmit_uart); // 对数据进行打包 每次要发送的数据改变的时候 都要重新打包 这个可放在 while 中
Data_Transmit(&data_transmit_uart,&huart1); // 将数据通过USART1 发送
// 打包与发送合并
Data_Pack_Transmit(&data_transmit_uart,&huart1); // 对数据进行打包发送
// 固件库 使用方法
// 打包与发送分开
Data_Pack(&data_transmit_uart); // 对数据进行打包 每次要发送的数据改变的时候 都要重新打包 这个可放在 while 中
Data_Transmit(&data_transmit_uart, USART1); // 将数据通过USART1 发送
// 打包与发送合并
Data_Pack_Transmit(&data_transmit_uart, USART1); // 对数据进行打包 并通过USART1 发送
*/
// 至此串口数据发送函数使用例结束
/********************************************************************
串口接收数据结构体初始化函数 Data_Receive_Init
各参数作用
DataReceive *data: 选择要初始化的串口接收数据结构体
head1: 帧头1
head2: 帧头2
********************************************************************/
void Data_Receive_Init(DataReceive *data, uint8_t head1, uint8_t head2)
{
data -> head1 = head1;
data -> head2 = head2;
data -> length = 0;
data -> cnt = 0;
data -> state = 0;
data -> i = 0;
data -> data = 0;
for(uint8_t j = 0; j < 50; j++)
{
data -> receive_data[j] = 0;
}
}
/********************************************************************
串口接收数据函数 Data_Receive
各参数作用
DataReceive *data: 选择要接收的串口接收数据结构体
buf: 接收数据
********************************************************************/
void Data_Receive(DataReceive *data, uint8_t buf)
{
if(data -> state == 0 && buf == data -> head1)
{
data -> state = 1;
data -> receive_data[0] = buf;
}
else if(data -> state == 1 && buf == data -> head2)
{
data -> state = 2;
data -> receive_data[1] = buf;
}
else if(data -> state == 2 && buf < 40)
{
data -> state = 3;
data -> length = buf;
data -> cnt = buf+4;
data -> receive_data[2] = buf;
}
else if(data -> state == 3 && data -> length > 0)
{
data -> length = data -> length - 1;
data -> receive_data[3 + data -> i] = buf;
data -> i = data -> i + 1;
if(data -> length == 0)
{
data -> state = 4;
}
}
else if(data -> state == 4)
{
data -> receive_data[3 + data -> i] = buf;
data -> state = 0;
data -> i = 0;
}
else
{
data -> state = 0;
data -> i = 0;
}
}
// 至此串口数据接收函数定义结束 使用例如下
/*
DataReceive data_receive_uart; // 声明全局结构体 data_receive_uart 这个要放在 main 函数外面
Data_Receive_Init(&data_receive_uart,0xAA,0xAA); // main函数中在 while 前 对结构体 data_receive_uart 进行初始化 设置帧头为 0xAA
// HAL库 使用方法
Buffer_Receive(&data_receive_uart,&huart1); // main的函数在 while 前 HAL库需要接收一次 使之可以进入中断回调
// 串口中断回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) // 中断回调函数中调用即可 函数 HAL_UART_RxCpltCallback 是一个弱定义 可以在 main.c 的 USER CODE BEGIN 4 中进行重写
{
if( huart == &huart1 ) // 检测到串口中断1
{
Data_Receive(&data_receive_uart,Buffer_Receive(&data_receive_uart,&huart1));
}
}
// 固件库 使用方法 直接将中断函数复制到 main.c 下面即可
// 串口1 中断函数
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) // 判断接收数据寄存器是否有数据
{
Data_Receive(&data_receive_uart, USART_ReceiveData(USART1)); // 从 串口1 接收1个数据
USART_ClearFlag(USART1,USART_IT_RXNE); // 清空中断标志 准备下一次接收
}
}
*/
// 至此串口数据接收函数使用例结束
// 以下为串口接收数据解析函数 此类函数可自行定义 增删改变量 方法不唯一
/********************************************************************
串口接收数据结构体初始化函数 Target_Init
各参数作用
TargetAttribute *target: 选择要初始化的结构体
********************************************************************/
void Target_Init(TargetProperty *target)
{
target -> servo1 = 0;
target -> servo2 = 0;
target -> servo3 = 0;
target -> servo4 = 0;
target -> servo5 = 0;
target -> servo6 = 0;
}
/********************************************************************
串口接收数据解析函数 Target_Parse
各参数作用
DataReceive *data: 选择被解析的结构体
TargetProperty *target: 选择解析完成保存的结构体
********************************************************************/
void Target_Parse(DataReceive *data, TargetProperty *target)
{
uint8_t sum = 0;
uint8_t i = 0;
while(i < data -> cnt - 1)
{
sum = sum + data -> receive_data[i];
i = i + 1;
}
if(sum == data -> receive_data[data -> cnt - 1])
{
target -> servo1 = data -> receive_data[3]*256 + data -> receive_data[4];
target -> servo2 = data -> receive_data[5]*256 + data -> receive_data[6];
target -> servo3 = data -> receive_data[7]*256 + data -> receive_data[8];
target -> servo4 = data -> receive_data[9]*256 + data -> receive_data[10];
target -> servo5 = data -> receive_data[11]*256 + data -> receive_data[12];
target -> servo6 = data -> receive_data[13]*256 + data -> receive_data[14];
}
}
// 至此数据解析函数定义结束 使用例如下
/*
TargetProperty target; // 声明全局结构体 target 这个要放在 main 函数外面
Target_Init(&target); // main函数中在 while 前 对结构体 target 进行初始化
Target_Parse(&data_receive_uart,&target); // 解析接收数据 可以放在 while 中 也可放在其他地方 不唯一
*/
// 解析函数可根据自身需求自由定义 方法不唯一
// 下位机
/* USER CODE BEGIN Includes */
//#include "uartconfig.h"
//#include "uartprotocol.h"
/* USER CODE BEGIN 0 */
//uint16_t task_num = 0; // 任务号
//uint16_t task_flag = 0; // 任务完成标志
//uint16_t task_max = 40; // 任务号最大值
//DataTransmit data_transmit_uart3; // 声明全局结构体 data_transmit_uart3 用于 串口发送
//DataReceive data_receive_uart3; // 声明全局结构体 data_receive_uart3 用于 串口接收
//TargetProperty tmaster; // 声明全局结构体 tmaster 用于 保存发给上位机的参数
//TargetProperty tslave; // 声明全局结构体 tslave 用于 保存上位机下发的参数
//void System_Init(void); // 声明系统初始化函数
//void Task_Run(void); // 声明任务运行函数
/* USER CODE BEGIN 2 */
// System_Init(); // 系统初始化
/* USER CODE BEGIN 3 */
// Task_Run(); // 任务运行
/* USER CODE BEGIN 4 */
系统初始化函数
//void System_Init(void)
//{
//
// Data_Transmit_Init(&data_transmit_uart3,0xFC,0xAA,12); // 串口3 发送结构体初始化 设置帧头FC AA 有效数据长度12
//
// Data_Receive_Init(&data_receive_uart3,0xFC,0xAA); // 串口3 接收结构体初始化 设置帧头 FC AA
//
// Target_Init(&tmaster); // 发送上位机参数初始化
//
// Target_Init(&tslave); // 上位机下发参数初始化
//
// tslave.servo1 = 1500; // 各舵机初始装载值设置
// tslave.servo2 = 1500;
// tslave.servo3 = 2100;
// tslave.servo4 = 1500;
// tslave.servo5 = 1500;
// tslave.servo6 = 1500;
//
data_receive_uart3.receive_data [3] = 1500/256; // RX TX 短接测试用部分 注释
data_receive_uart3.receive_data [4] = 1500%256;
data_receive_uart3.receive_data [5] = 1500/256;
data_receive_uart3.receive_data [6] = 1500%256;
data_receive_uart3.receive_data [7] = 2100/256;
data_receive_uart3.receive_data [8] = 2100%256;
data_receive_uart3.receive_data [9] = 1500/256;
data_receive_uart3.receive_data [10] = 1500%256;
data_receive_uart3.receive_data [11] = 1500/256;
data_receive_uart3.receive_data [12] = 1500%256;
data_receive_uart3.receive_data [13] = 1500/256;
data_receive_uart3.receive_data [14] = 1500%256;
//
// HAL_TIM_Base_Start_IT(&htim1); // 开启 定时器1 中断
//
// HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_2); // 开启 定时器2 PWM CH2
// HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_1); // 开启 定时器3 PWM CH1
// HAL_TIM_PWM_Start(&htim4,TIM_CHANNEL_1); // 开启 定时器4 PWM CH1
// HAL_TIM_PWM_Start(&htim4,TIM_CHANNEL_2); // 开启 定时器4 PWM CH2
// HAL_TIM_PWM_Start(&htim4,TIM_CHANNEL_3); // 开启 定时器4 PWM CH3
// HAL_TIM_PWM_Start(&htim4,TIM_CHANNEL_4); // 开启 定时器4 PWM CH4
//
// Buffer_Receive(&data_receive_uart3,&huart3); // 串口3 接收一次数据 使之可以正确触发中断
//
//}
定时器中断回调函数
//void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
//{
//
// if( htim == (&htim1) )
// {
//
// if(task_num < task_max) // 任务号如果没超过上限值
// {
//
// task_num = task_num + 1; // 任务切换
// task_flag = 1; // 任务完成标志 0完成 1未完成 每次只做一次任务
//
// }
// else
// {
//
// task_num = 0; // 重头开始执行任务
// task_flag = 1; // 任务完成标志 0完成 1未完成 每次只做一次任务
//
// }
//
// }
//
//}
运行任务函数
//void Task_Run(void)
//{
//
// if(task_num == 0 && task_flag == 1) // 任务1 各参数上发上位机
// {
//
// data_transmit_uart3.data[0] = tmaster.servo1/256;
// data_transmit_uart3.data[1] = tmaster.servo1%256;
//
// data_transmit_uart3.data[2] = tmaster.servo2/256;
// data_transmit_uart3.data[3] = tmaster.servo2%256;
//
// data_transmit_uart3.data[4] = tmaster.servo3/256;
// data_transmit_uart3.data[5] = tmaster.servo3%256;
//
// data_transmit_uart3.data[6] = tmaster.servo4/256;
// data_transmit_uart3.data[7] = tmaster.servo4%256;
//
// data_transmit_uart3.data[8] = tmaster.servo5/256;
// data_transmit_uart3.data[9] = tmaster.servo5%256;
//
// data_transmit_uart3.data[10] = tmaster.servo6/256;
// data_transmit_uart3.data[11] = tmaster.servo6%256;
//
// Data_Pack_Transmit(&data_transmit_uart3, &huart3);
//
// task_flag = 0;
//
// }
// else if(task_num == 10 && task_flag == 1) // 任务2 上位机数据解析
// {
//
// Target_Parse(&data_receive_uart3,&tslave);
//
// task_flag = 0;
//
// }
// else if(task_num == 20 && task_flag == 1) // 任务3 各舵机PWM占空比设置
// {
//
// __HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_2, tslave.servo1);
// __HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_3, tslave.servo2);
// __HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_4, tslave.servo3);
// __HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_1, tslave.servo4);
// __HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_2, tslave.servo5);
// __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_1, tslave.servo6);
//
// task_flag = 0;
//
// }
// else if(task_num == 30 && task_flag == 1) // 任务4 各舵机占空比重装载值赋值 准备上发上位机
// {
//
// tmaster.servo1 = tslave.servo1;
// tmaster.servo2 = tslave.servo2;
// tmaster.servo3 = tslave.servo3;
// tmaster.servo4 = tslave.servo4;
// tmaster.servo5 = tslave.servo5;
// tmaster.servo6 = tslave.servo6;
//
// task_flag = 0;
//
// }
//
//}
串口中断回调函数
//void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
//{
// if( huart == &huart3 ) // 检测到串口中断3 则接收1个数据 通过函数 Data_Receive 保存
// {
// Data_Receive(&data_receive_uart3,Buffer_Receive(&data_receive_uart3,&huart3));
// }
//}
uartprotocol.h
#ifndef __UARTPROTOCOL_H
#define __UARTPROTOCOL_H
// 重命名方便定义
typedef signed char int8_t;
typedef short int int16_t;
typedef int int32_t;
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
// 串口发送相关结构体
typedef struct
{
uint8_t head1; // 帧头1
uint8_t head2; // 帧头2
uint8_t length; // 有效数据长度
uint8_t cnt; // 总数据长度
uint8_t data[40]; // 有效数据数组
uint8_t transmit_data[50]; // 实际发送的数组 附带上帧头1 帧头2 有效数据长度位 校验位
}DataTransmit;
// 串口发送相关函数
void Data_Transmit_Init(DataTransmit *data, uint8_t head1, uint8_t head2, uint8_t length);
void Data_Pack(DataTransmit *data);
// 串口接收相关结构体
typedef struct
{
uint8_t head1; // 帧头1
uint8_t head2; // 帧头2
uint8_t length; // 有效数据长度
uint8_t cnt; // 总数据长度
uint8_t state; // 接收状态
uint8_t i; // 有效数据下标
uint8_t data; // 接收数据缓冲位
uint8_t receive_data[50]; // 实际接收的数组 附带上帧头1 帧头2 有效数据长度位 校验位
}DataReceive;
// 串口接收相关函数
void Data_Receive_Init(DataReceive *data, uint8_t head1, uint8_t head2);
void Data_Receive(DataReceive *data, uint8_t buf);
// 接收数据解析相关结构体
typedef struct
{
uint16_t servo1;
uint16_t servo2;
uint16_t servo3;
uint16_t servo4;
uint16_t servo5;
uint16_t servo6;
}TargetProperty;
// 接收数据解析相关函数
void Target_Init(TargetProperty *target);
void Target_Parse(DataReceive *data, TargetProperty *target);
#endif
然后在main.c中的这些位置添加如下代码即可,这部分逻辑简单,我也将注释都写在后面了,就不再解释了。
/* USER CODE BEGIN Includes */
#include "uartconfig.h"
#include "uartprotocol.h"
/* USER CODE BEGIN 0 */
uint16_t task_num = 0; // 任务号
uint16_t task_flag = 0; // 任务完成标志
uint16_t task_max = 40; // 任务号最大值
DataTransmit data_transmit_uart3; // 声明全局结构体 data_transmit_uart3 用于 串口发送
DataReceive data_receive_uart3; // 声明全局结构体 data_receive_uart3 用于 串口接收
TargetProperty tmaster; // 声明全局结构体 tmaster 用于 保存发给上位机的参数
TargetProperty tslave; // 声明全局结构体 tslave 用于 保存上位机下发的参数
void System_Init(void); // 声明系统初始化函数
void Task_Run(void); // 声明任务运行函数
/* USER CODE BEGIN 2 */
System_Init(); // 系统初始化
/* USER CODE BEGIN 3 */
Task_Run(); // 任务运行
/* USER CODE BEGIN 4 */
// 系统初始化函数
void System_Init(void)
{
Data_Transmit_Init(&data_transmit_uart3,0xFC,0xAA,12); // 串口3 发送结构体初始化 设置帧头FC AA 有效数据长度12
Data_Receive_Init(&data_receive_uart3,0xFC,0xAA); // 串口3 接收结构体初始化 设置帧头 FC AA
Target_Init(&tmaster); // 发送上位机参数初始化
Target_Init(&tslave); // 上位机下发参数初始化
tslave.servo1 = 1500; // 各舵机初始装载值设置
tslave.servo2 = 1500;
tslave.servo3 = 2100;
tslave.servo4 = 1500;
tslave.servo5 = 1500;
tslave.servo6 = 1500;
HAL_TIM_Base_Start_IT(&htim1); // 开启 定时器1 中断
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_2); // 开启 定时器2 PWM CH2
HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_1); // 开启 定时器3 PWM CH1
HAL_TIM_PWM_Start(&htim4,TIM_CHANNEL_1); // 开启 定时器4 PWM CH1
HAL_TIM_PWM_Start(&htim4,TIM_CHANNEL_2); // 开启 定时器4 PWM CH2
HAL_TIM_PWM_Start(&htim4,TIM_CHANNEL_3); // 开启 定时器4 PWM CH3
HAL_TIM_PWM_Start(&htim4,TIM_CHANNEL_4); // 开启 定时器4 PWM CH4
Buffer_Receive(&data_receive_uart3,&huart3); // 串口3 接收一次数据 使之可以正确触发中断
}
// 定时器中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if( htim == (&htim1) )
{
if(task_num < task_max) // 任务号如果没超过上限值
{
task_num = task_num + 1; // 任务切换
task_flag = 1; // 任务完成标志 0完成 1未完成 每次只做一次任务
}
else
{
task_num = 0; // 重头开始执行任务
task_flag = 1; // 任务完成标志 0完成 1未完成 每次只做一次任务
}
}
}
// 运行任务函数
void Task_Run(void)
{
if(task_num == 0 && task_flag == 1) // 任务1 各参数上发上位机
{
data_transmit_uart3.data[0] = tmaster.servo1/256;
data_transmit_uart3.data[1] = tmaster.servo1%256;
data_transmit_uart3.data[2] = tmaster.servo2/256;
data_transmit_uart3.data[3] = tmaster.servo2%256;
data_transmit_uart3.data[4] = tmaster.servo3/256;
data_transmit_uart3.data[5] = tmaster.servo3%256;
data_transmit_uart3.data[6] = tmaster.servo4/256;
data_transmit_uart3.data[7] = tmaster.servo4%256;
data_transmit_uart3.data[8] = tmaster.servo5/256;
data_transmit_uart3.data[9] = tmaster.servo5%256;
data_transmit_uart3.data[10] = tmaster.servo6/256;
data_transmit_uart3.data[11] = tmaster.servo6%256;
Data_Pack_Transmit(&data_transmit_uart3, &huart3);
task_flag = 0;
}
else if(task_num == 10 && task_flag == 1) // 任务2 上位机数据解析
{
Target_Parse(&data_receive_uart3,&tslave);
task_flag = 0;
}
else if(task_num == 20 && task_flag == 1) // 任务3 各舵机PWM占空比设置
{
__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_2, tslave.servo1);
__HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_3, tslave.servo2);
__HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_4, tslave.servo3);
__HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_1, tslave.servo4);
__HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_2, tslave.servo5);
__HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_1, tslave.servo6);
task_flag = 0;
}
else if(task_num == 30 && task_flag == 1) // 任务4 各舵机占空比重装载值赋值 准备上发上位机
{
tmaster.servo1 = tslave.servo1;
tmaster.servo2 = tslave.servo2;
tmaster.servo3 = tslave.servo3;
tmaster.servo4 = tslave.servo4;
tmaster.servo5 = tslave.servo5;
tmaster.servo6 = tslave.servo6;
task_flag = 0;
}
}
// 串口中断回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if( huart == &huart3 ) // 检测到串口中断3 则接收1个数据 通过函数 Data_Receive 保存
{
Data_Receive(&data_receive_uart3,Buffer_Receive(&data_receive_uart3,&huart3));
}
}
然后就可以编译烧入运行了,还是讲下运行函数。这里的task_num在定时器中断触发的时候会被加1,直到加到40然后清零,每一次触发定时器中断都会将task_flag置为1,在执行完任务后,task_flag将会置0,也就是说一段时间内不管执行几次Task_Run,里面的任务都只会执行一次。
task_num为0的时候,将接收到的数据发回给上位机,这一步可以没有,因为我是用这一步来判断发送是否出错,是一个回显的作用。
task_num为10的时候,将解析上位机发送过来的数据,
task_num为20的时候,将解析后获得的各舵机占空比重装载值装载,调节占空比。
task_num为30的时候,将解析后的数据赋值给发送到上位机的参数,等待下一个task_num为0的时候,将数据发送给上位机,就是完成回显。
// 运行任务函数
void Task_Run(void)
{
if(task_num == 0 && task_flag == 1) // 任务1 各参数上发上位机
{
data_transmit_uart3.data[0] = tmaster.servo1/256;
data_transmit_uart3.data[1] = tmaster.servo1%256;
data_transmit_uart3.data[2] = tmaster.servo2/256;
data_transmit_uart3.data[3] = tmaster.servo2%256;
data_transmit_uart3.data[4] = tmaster.servo3/256;
data_transmit_uart3.data[5] = tmaster.servo3%256;
data_transmit_uart3.data[6] = tmaster.servo4/256;
data_transmit_uart3.data[7] = tmaster.servo4%256;
data_transmit_uart3.data[8] = tmaster.servo5/256;
data_transmit_uart3.data[9] = tmaster.servo5%256;
data_transmit_uart3.data[10] = tmaster.servo6/256;
data_transmit_uart3.data[11] = tmaster.servo6%256;
Data_Pack_Transmit(&data_transmit_uart3, &huart3);
task_flag = 0;
}
else if(task_num == 10 && task_flag == 1) // 任务2 上位机数据解析
{
Target_Parse(&data_receive_uart3,&tslave);
task_flag = 0;
}
else if(task_num == 20 && task_flag == 1) // 任务3 各舵机PWM占空比设置
{
__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_2, tslave.servo1);
__HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_3, tslave.servo2);
__HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_4, tslave.servo3);
__HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_1, tslave.servo4);
__HAL_TIM_SetCompare(&htim4, TIM_CHANNEL_2, tslave.servo5);
__HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_1, tslave.servo6);
task_flag = 0;
}
else if(task_num == 30 && task_flag == 1) // 任务4 各舵机占空比重装载值赋值 准备上发上位机
{
tmaster.servo1 = tslave.servo1;
tmaster.servo2 = tslave.servo2;
tmaster.servo3 = tslave.servo3;
tmaster.servo4 = tslave.servo4;
tmaster.servo5 = tslave.servo5;
tmaster.servo6 = tslave.servo6;
task_flag = 0;
}
}
测试
给舵机模块上电,这里可以用串口助手进行测试,帧头是FC,AA,有效数据长度是12,对应十六进制是0C,1500对应的十六进制是05 DC,校验码是F8,如果蓝牙成功连接上的话,舵机将会直立,因为1500对应的度数是90度(这个跟舵机安装方式有关)
如果要调节1号舵机,也就是底盘,对应的序号是3和4,这里将3改成04,因此校验码就要减1,变成F7
可以看到舵机转了,再测试一下二号舵机,将5改成04,校验码改成F6。
到这里不难发现用串口调试助手的缺点是,每一次调节舵机占空比都需要计算对应十六进制的值,比如1500的十六进制是05 DC,04 DC对应的是1244,虽然差是256,但是05 DC 和 04 DC相加的结果只差1,所以校验码是F8改成F7,计算会比较繁琐,这时上位机的作用就来了,但上位机部分的代码还比较杂乱,我还没注释,因此我们下期再见啦!
更多推荐
所有评论(0)