使用3D打印材料制作全向轮 STM32作为主控使用HAL库 制作简单手柄并使用串口无线模块和主控进行通信
主要是第一次见到三轮全向底盘,进行全向移动对他的算法什么很感兴趣,如果能自己制作一辆全向轮底盘就好了
三轮全向小车模型
三轮全向的底盘网上售卖的店铺也很多也有着各种各样的选择不过价格有的也并不便宜所以打算自己弄一个(当然可能花费的更多但是也更有趣)
全向轮
网上在售的大多数以金属全向轮为主(中尺寸像是RoboMaster, RoboCount使用的),也有那种小一点的塑料轮子(金属轮子普遍不便宜)所以尝试自己画了个全向轮
这一版还有比较多的问题,比如留给棍子的空间太小了后面无法加装胶皮管
打印件一般摩擦力不是很大所以采用了小棍子外面包裹胶片软管的方式,效果杠杠的
3D打印并不是很好的选择一盘材料(1KG)中等一点的50左右 还有不到40的打印三个大概消耗了我半盘也并不是很便宜
SW软件中的装配图
实物图 (那些打印件是用偏口钳剪的,不然胶皮管会蹭到轮子)
螺丝螺母没买够只能找个长的替代了
底盘
三轮全向的地盘就好做一些了因为三个点确定一个面所以所有轮子不会悬空不加减震也没有关系,只要将三个轮子按照120°的角度固定就好
这里的电机模型是买了个电机找商家要的,这个电机挺好就是有的贵,后面测试没有用因为我就买了一个QAQ
硬件电路
主控选择
因为前段时间板子老烧所以手里没有什么可以使用的主控板子,一开始想使用ESP32的后来下载电路出了问题就没用,直接用了STM32F031C6T6的主控板 因为这个芯片很便宜做了5个开发板,这也是我第一次用立创EDA画的板子
不过这颗主控的定时器比较少没有办法使用AB编码器来测正反转速度
原理图(仅主控部分)
详细:https://oshwhub.com/watermelonnoodles/STM32F031C6T6_COR02
PCB
后面准备使用STM32F103ZET6正在画图中
电机驱动
使用直接购买的成品模块L298N
不是自己画的真是不好意思QAQ
下一版准备集成到开发板中可能使用双层板设计
电机选型
最开始在无刷和有刷直接选择,无刷还要进行驱动器的制作并且无刷电机也挺贵的后来直接选择了带有AB编码器的有刷电机,控起来也比较方便一些
一号电机什么都好就是太贵了一个大概就要50多
二号就比较中规中矩也挺好看的
三号也还可以不过他只有一相测速也就是说不能识别正反转
所以最后用了2号电机(因为都加了减速箱所以扭矩没太大问题)
所以电池电源也就定在了12V正好L298N上集成了宽电压转5V的降压
遥控端
大体思路是采用ADC手柄(XBOX那种)使用STM32F103C8T6进行采样过滤之后通过无线模块发送给主控端
原理图(主控引脚)因为在离线上还没有发布所以没有详细链接
PCB(之所以是方形是打算后面打印一个简单的外壳)
无线模块采用 汇承的HC-14 初学者套餐还是蛮优惠的 使用的是串口发送和接收
先在上位机设置好之后就可以装上了
接线
太晚了没法拍有时间或者有人需要在更新叭
软件调试
电机控制
HAL库配置
时钟树48MHz
开启 SYS — Debug Serial Wire
如果需要可以配置NVIC
PA0,1,2,3 不用管是用来测试步进电机的(没有用到)
定时器1 用来测速的ABC电机的A相接到Channel1,2,3上 B相没接 (后面没有用没法做闭环PID我只是拿来试试)
定时器PWM输出 三对 一共六个接口 用来控制电机的
定时器14(用来执行任务)开启NVIC
main.c的 /* USER CODE BEGIN 2 */ 下添加 完成PWM初始化
/* USER CODE BEGIN 2 */
/*电机代码*/
HAL_TIM_Base_Start(&htim2); //TIM2
HAL_TIM_Base_Start(&htim3); //TIM3
HAL_TIM_Base_Start_IT(&htim14);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); // TIM2_CH1(pwm)
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2); // TIM2_CH2(pwm)
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); // TIM3_CH1(pwm)
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2); // TIM3_CH2(pwm)
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3); // TIM3_CH3(pwm)
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4); // TIM3_CH4(pwm)
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
添加方法(这种只适用于测试这样写并不好 后面准备改一下创建一个电机的结构体)
可以添加到main方法上面/* USER CODE BEGIN 0 */区域
void Motor_set(int id,int speed){
switch (id){
case 3: // MotorC
if(speed > 0){
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, speed);
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 0);
}else{
speed = speed * -1;
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 0);
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, speed);
}
break;
case 1: // MotorA
if(speed > 0){
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, speed);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, 0);
}else{
speed = speed * -1;
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, speed);
}
break;
case 2: // MotorB
if(speed > 0){
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, speed);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_4, 0);
}else{
speed = speed * -1;
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, 0);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_4, speed);
}
break;
}
}
然后就可以判断那个电机是否都正常
遥控器处理
遥控器发送端 (是遥控器主板 并不是主控板)
HAL 配置
时钟树64MHz
开启 SYS — Debug Serial Wire
如果需要可以配置NVIC
主要使用的只有4个ADC以及串口
串口配置(刚发现竟然没用DMA 以后改进)开启NVIC
ADC配置 开启NVIC
定时器配置 开启NVIC
/* USER CODE BEGIN 2 */
HAL_ADC_Start_DMA(&hadc1,(uint32_t *)adc_buff,4);
adc_init();
HAL_UART_Transmit_DMA(&huart2, str, sizeof(str) - 1);
HAL_TIM_Base_Start_IT(&htim1);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
//测试用
HAL_Delay(50);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
全局变量
uint16_t adc_buff[4];
int lx = 0, ly = 0, rx = 0, ry = 0;
float adc_value[4];
uint8_t senda[10];
添加方法
// 用来上电做个校准不然偏差有点大
void adc_init(){
HAL_Delay(20);
Get_Data(adc_value,12);
HAL_Delay(20);
lx = 2000 - adc_value[0];
ly = 2000 - adc_value[1];
rx = 2000 - adc_value[2];
ry = 2000 - adc_value[3];
}
// 获取ADC摇杆数值并转化
static void Get_Data(float *temp,uint8_t times){
uint32_t temp_val[4]={0};
uint8_t t=0;
for(t=0;t<times;t++)
{
HAL_ADC_Start_DMA(&hadc1,(uint32_t*)&adc_buff,4);
temp_val[0]+=adc_buff[0];
temp_val[1]+=adc_buff[1];
temp_val[2]+=adc_buff[2];
temp_val[3]+=adc_buff[3];
// delay_ms(5);
}
//*3.3/4096
temp[0]=(float)temp_val[0]/times;
temp[1]=(float)temp_val[1]/times;
temp[2]=(float)temp_val[2]/times;
temp[3]=(float)temp_val[3]/times;
// 卡尔曼滤波
// temp[0] = KalmanFilter(temp[0]);
// temp[1] = KalmanFilter(temp[1]);
// temp[2] = KalmanFilter(temp[2]);
// temp[3] = KalmanFilter(temp[3]);
//四舍五入
temp[0] = (int)(temp[0]*1000+0.5)/1000.0 + lx;
temp[1] = (int)(temp[1]*1000+0.5)/1000.0 + ly;
temp[2] = (int)(temp[2]*1000+0.5)/1000.0 + rx;
temp[3] = (int)(temp[3]*1000+0.5)/1000.0 + ry;
// 设置5的死区
if(temp[0] <= 2005 && temp[0] >= 1995)
temp[0] = 2000;
if(temp[1] <= 2005 && temp[1] >= 1995)
temp[1] = 2000;
if(temp[2] <= 2005 && temp[2] >= 1995)
temp[2] = 2000;
if(temp[3] <= 2005 && temp[3] >= 1995)
temp[3] = 2000;
}
//发送数据
void sendData(){
// 数据位移
senda[0] = (uint16_t)adc_value[0] >> 8;
senda[1] = (uint16_t)adc_value[0];
senda[2] = (uint16_t)adc_value[1] >> 8;
senda[3] = (uint16_t)adc_value[1];
senda[4] = (uint16_t)adc_value[2] >> 8;
senda[5] = (uint16_t)adc_value[2];
senda[6] = (uint16_t)adc_value[3] >> 8;
senda[7] = (uint16_t)adc_value[3];
// 按钮
if(HAL_GPIO_ReadPin(GPIOB, KEY_B1_Pin) == 1){
senda[8] = 1;
}else{
senda[8] = 0;
}
if(HAL_GPIO_ReadPin(GPIOB, KEY_B2_Pin) == 1){
senda[8] += 2;
}else{
senda[8] += 0;
}
if(HAL_GPIO_ReadPin(GPIOB, KEY_B3_Pin) == 1){
senda[8] += 4;
}else{
senda[8] += 0;
}
}
// 定时器回调函数 如果没有进看看有没有开中断
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef* htim){
if(htim == &htim1){
Get_Data(adc_value,12);
sendData();
HAL_UART_Transmit_DMA(&huart2, senda, sizeof(senda) - 1);
}
}
遥控器接收(在主控板上的配置新增)
HAL配置
全局变量
#define USART_DMA_TX_BUFFER_MAXIMUM 9 // DMA缓冲区大小
#define USART_DMA_RX_BUFFER_MAXIMUM 9 // DMA缓冲区大小
int MotorOutput = 0;
uint8_t usart1_rx_buffer[USART_DMA_RX_BUFFER_MAXIMUM]; //串口1的DMA接收缓冲区
uint8_t usart1_tx_buffer[USART_DMA_TX_BUFFER_MAXIMUM]; //串口1的DMA发送缓冲区
int x1 = 2000,x2 = 2000,y1 = 2000,y2 = 2000;
int count = 0;
添加方法
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart==&huart1)
{
memcpy(usart1_tx_buffer,usart1_rx_buffer,9);//由于是异步的,发送和接收的buffer一定要分开。主程序不要随便修改这两个buffer的数据
HAL_UART_Receive_DMA(huart,usart1_rx_buffer,9);//继续监听新的数据,Normal模式下发送和监听都是一次性的,
//这里可以根据需求动态调整下一次接收的帧长
x1 = usart1_rx_buffer[0] << 8 | usart1_rx_buffer[1];
y1 = usart1_rx_buffer[2] << 8 | usart1_rx_buffer[3];
x2 = usart1_rx_buffer[4] << 8 | usart1_rx_buffer[5];
y2 = usart1_rx_buffer[6] << 8 | usart1_rx_buffer[7];
MotorOutput = -(x2-2000)/2;
}
}
底盘解算
对于全向轮的运动学结算网上有很多教程和例子讲的很好可以看看
三轮全向地盘要先确定轮子的序号和旋转方向 可以是如下
A转向C C转向B B转向A 为正向 以此为基础
添加方法 传入车的X方向 Y方向 Z方向(自转)的速度解析每个轮的速度
void Speed_Moto_Control(float vx,float vy,float vz)
{
MotorB.set_speed = (-VX_VALUE*vx + VY_VALUE*vy + L_value*vz);
MotorC.set_speed = (-VX_VALUE*vx - VY_VALUE*vy + L_value*vz);
MotorA.set_speed = (vx + L_value*vz);
}
效果视频
审核还没通过
QAQ
计划任务
改进代码 更换主控 实现速度的闭环控制
添加陀螺仪使其可以自转前进