学完江科大STM32后,如何从零开始开发平衡车(第一部分)
#在学完江科大的课程之后,想巩固一下相关知识点并学习相关的算法#
在开发的过程中我遇到了非常多的麻烦,汲取了许多的教训,希望这篇文章对你有所帮助
目录
前期准备
几个进阶学习的模块
电路设计
编写程序框架
原理图绘制
设计PCB电路并打板组装
算法编写及调试
下面是我学习过的几个相关视频:
1.原理图和PCB绘制:【教程】零基础入门PCB设计-国一学长带你学立创EDA专业版 全程保姆级教学 中文字幕(持续更新中)_哔哩哔哩_bilibili
2.JDY-31蓝牙模式:
JDY-31蓝牙模块使用方法_哔哩哔哩_bilibili
3.STM32平衡车(材料购买可以看这里):
草履虫都能学会的STM32平衡小车教程(基础篇)_哔哩哔哩_bilibili
4.PID算法:
PID算法 – 从入门到实战!_哔哩哔哩_bilibili
5.PID调参:
1.直立环_哔哩哔哩_bilibili
【平衡小车PID】直立环+速度环完整调参过程 (开源)_哔哩哔哩_bilibili
6.陀螺仪算法(这个我学不会):
3.角度、角速度获取与滤波算法_哔哩哔哩_bilibili
互补滤波姿态估计、卡尔曼滤波姿态、高度估计_哔哩哔哩_bilibili
7.vofa(串口调试工具,我主要是用来看陀螺仪是否准确的):
STM32F4使用DMA串口通信连接VOFA软件进行电机调试和波形显示_哔哩哔哩_bilibili
8.MPU6050DMP库移植:
[小学生都会的]MPU6050DMP库移植(stm32标准库)_哔哩哔哩_bilibili
前期准备
在学习之前我们先要进行材料的住准备了解一下平衡车需要用到的模块,和其他零部件:
江科大套件自带:TB6612、STM32F103C8T6最小系统板、OLED、MPU6050
需另外购买:两个带编码器的电机(我用的是25GA370)、JDY-31(蓝牙模块)、12v电池、MP1584(12V转5V)、超声波模块(这个我买了,但是没用到)购买型号的细节或者其他购买的零件如亚克力板,螺丝,排母,开关等可以看上面的视频
一、几个模块的进阶学习
因为之后我们要进行程序框架的编写,所以我们以下要先学习一些新的内容
1.编码电机
编码电机比我们之前用过的电机多了四个引脚,其中两个引脚连接编码器的电源,另外两个引脚分别输出编码器的波形,跟我们之前学过的旋转编码器输出的波形是一样的
2.MPU6050姿态解算
我主要是通过积分角速度和读取加速度的分量来获取当前角度,这个姿态解算我并不能写出一个比较好的算法,老是有些偏移,想继续深入学习但是碰到了数学上的瓶颈,所以我选择了移植DMP库(上面有视频链接)
3.JDY-31(我买的是四角的)
这个主要是串口通讯,把USB转串口的代码搬过来修改一下即可,之后通过商家的进行简单的调试,这里不进行过多赘述。但是在手机调试蓝牙的时候老是找不到软件,所以在我给大家推荐一个比较好用的蓝牙调试软件,叫E调试(手机自带的应用商城应该都能找得到)
4.PID算法
先了解一下PID算法的运用场景和原理,得知为什么平衡车需要PID,学习PID算法的简单表达式
(具体可以观看上面的视频)
二、电路设计
学习完这些之后,我们要对几个模块进行组合(可以参考一下江科大的接线图,遇到冲突的时候要切换别的接口),在接口正确的前提下尽量使接线更为简单,减少之后PCB绘制的工作量。或者在程序写完之后,先让基本零部件跑起来,之后再对线路进行整改
三、编写程序框架
下面是我画的简易版流程图,希望对大家有所帮助,我的代码框架就是依据此图写出来的
这里要注意单片机资源的分配,如TIMER资源
下面我只呈现几个重要的函数,其他函数可以参考江科大,或者上面的视频
1.main.c引用的函数
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "oled.h"
#include "mpu6050.h"
#include "inv_mpu.h"
#include "timer.h" //中断TIM3
#include "Serial.h"
#include "Encoder.h" //译码器TIM2 TIM4
#include "motor.h" //PWM TIM1
#include "blancecar_pid.h"
其中Delay为了适配DMP做了一些修改,其中还进行了DMP库的移植(具体看上面的视频)
2.blancecar_pid.c PID算法(有一些float可以换成int16_t)
#include "stm32f10x.h" // Device header
float zl_Kp,zl_Kd; //直立环
float zl_error,zl_measure_last=0,zl_difference; //直立环:误差,测量,上次测量,两次测量的差
float sd_Kp,sd_Ki; //速度环
float sd_error,sd_error_last=0,sd_sum=0,sd_difference,sd_filt;//速度环:误差,上次误差,积分,两次测量的差,权重
float zx_Kp,zx_Kd; //转向环
float zx_error,zx_measure_last=0,zx_difference; //转向环:误差,测量,上次测量,两次测量的差
//经过测量陀螺仪roll中值为-3
void Limit(int16_t Date) //限制值的大小,必免积分过大影响PWM
{
if(sd_sum >= Date) sd_sum = Date;
if(sd_sum <= -Date) sd_sum = -Date;
}
/*直立环PD*/
int16_t Vertical_PID_value(float measure,float theoretic)
{
zl_error = measure - theoretic; //与目标的距离
zl_difference = zl_error-zl_measure_last; //与目标的距离 的变化量(微分)
zl_measure_last = zl_error; //赋值
return zl_Kp*zl_error + zl_Kd*zl_difference; //计算PD
}
/*速度环PI*/
int16_t Velocity_PID_value(int16_t measure,int16_t theoretic)
{
float a=0.3;
sd_error = measure - theoretic; //与目标的距离
sd_sum = sd_sum+sd_error*0.02; //积分
Limit(2000); //积分限幅
sd_filt = a*measure+(1-a)*sd_error_last; //大权重给到上一次速度,滤波
sd_error_last = sd_error; //赋值
return sd_Kp*sd_filt + sd_Ki*sd_sum; //计算PI
}
/*转向环PD*/
int16_t Trun_PID_value(float measure,float theoretic)
{
zx_error = measure - theoretic; //与目标的距离
zx_difference = measure - zx_measure_last; //与目标的距离 的变化量(微分)
zx_measure_last = measure; //赋值
return zx_Kp*zl_error + zx_Kd*zl_difference; //计算PD
}
3.motor.C 电机配置
#include "stm32f10x.h" // Device header
#include "PWM.h"
/*电机正反转引脚初始化*/
uint8_t Motor_Flag;//电机启动标志位
void Motor_Init(void)
{
Motor_Flag=1;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_IintStructure;
GPIO_IintStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_IintStructure.GPIO_Pin=GPIO_Pin_13 | GPIO_Pin_12 | GPIO_Pin_14 | GPIO_Pin_15;
GPIO_IintStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_IintStructure);
PWM_Init();
}
/*设置 电机1 PWM*/
void Motor_SetSpeed1(int16_t Speed)
{
if(Motor_Flag==1)
{
if(Speed>0)
{
GPIO_SetBits(GPIOB,GPIO_Pin_13);
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
PWM_SetCompare4(Speed);
}
else
{
GPIO_SetBits(GPIOB,GPIO_Pin_12);
GPIO_ResetBits(GPIOB,GPIO_Pin_13);
PWM_SetCompare4(-Speed);
}
}
}
/*设置 电机2 PWM*/
void Motor_SetSpeed2(int16_t Speed)
{
if(Motor_Flag==1)
{
if(Speed>0)
{
GPIO_SetBits(GPIOB,GPIO_Pin_14);
GPIO_ResetBits(GPIOB,GPIO_Pin_15);
PWM_SetCompare1(Speed);
}
else
{
GPIO_SetBits(GPIOB,GPIO_Pin_15);
GPIO_ResetBits(GPIOB,GPIO_Pin_14);
PWM_SetCompare1(-Speed);
}
}
}
/*电机关闭*/
void Motor_Reset()
{
GPIO_ResetBits(GPIOB,GPIO_Pin_14); //关闭引脚
GPIO_ResetBits(GPIOB,GPIO_Pin_15);
GPIO_ResetBits(GPIOB,GPIO_Pin_13);
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
Motor_Flag=0;//标志位置0
}
/*电机开启*/
void Motor_Set()
{
Motor_Flag=1;
}
/*PWM限幅,PWM赋值*/
void Motor_SetDoubleSpeed(int16_t limit,int16_t *pwm1,int16_t *pwm2)
{
if(*pwm1 >= limit) *pwm1 = limit;
if(*pwm1 <= -limit) *pwm1 = -limit;
if(*pwm2 <= -limit) *pwm2 = -limit;
if(*pwm2 >= limit) *pwm2 = limit;
Motor_SetSpeed2(*pwm2);
Motor_SetSpeed1(*pwm1);
}
4.encode.C 译码器读速度
#include "stm32f10x.h" // Device header
/*译码器初始化*/
void Encoder_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //初始化时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure; //初始化GPIO为上拉输入
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_1 |GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);//GPIOA
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6 |GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);//GPIOB
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //配置时基单元
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1; //时钟分割,外部引脚触发,进入边沿检测
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period=65536-1;
TIM_TimeBaseInitStructure.TIM_Prescaler=1-1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);//TIM2
TIM_InternalClockConfig(TIM4);
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1; //时钟分割,外部引脚触发,进入边沿检测
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period=65536-1;
TIM_TimeBaseInitStructure.TIM_Prescaler=1-1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM4,&TIM_TimeBaseInitStructure);//TIM4
/*TIM2测速*/
TIM_ICInitTypeDef TIM_ICInitStructure; //开启两通道边沿性检测
TIM_ICStructInit(&TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel=TIM_Channel_1;
TIM_ICInitStructure.TIM_ICPolarity=TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICFilter=0xf;
TIM_ICInit(TIM2,&TIM_ICInitStructure);//TIM2
TIM_ICStructInit(&TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel=TIM_Channel_2;
TIM_ICInitStructure.TIM_ICPolarity=TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICFilter=0xf;
TIM_ICInit(TIM2,&TIM_ICInitStructure);//TIM2
/*TIM4测速*/
TIM_ICStructInit(&TIM_ICInitStructure); //开启两通道边沿性检测
TIM_ICInitStructure.TIM_Channel=TIM_Channel_1;
TIM_ICInitStructure.TIM_ICPolarity=TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICFilter=0xf;
TIM_ICInit(TIM4,&TIM_ICInitStructure);//TIM4
TIM_ICStructInit(&TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel=TIM_Channel_2;
TIM_ICInitStructure.TIM_ICPolarity=TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICFilter=0xf;
TIM_ICInit(TIM4,&TIM_ICInitStructure);//TIM4
//两通道进入译码器接口
TIM_EncoderInterfaceConfig(TIM2,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);
TIM_Cmd(TIM2,ENABLE);//TIM2
TIM_EncoderInterfaceConfig(TIM4,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);
TIM_Cmd(TIM4,ENABLE);//TIM4
}
int16_t Speed1,Speed2;
/*获取电机速度*/
int16_t Encoder_Get_Speed1(void)
{
Speed1=TIM_GetCounter(TIM2);
TIM_SetCounter(TIM2,0);
return Speed1;
}
int16_t Encoder_Get_Speed2(void)
{
Speed2=TIM_GetCounter(TIM4);
TIM_SetCounter(TIM4,0);
return Speed2;
}
以上函数可能存在一些冗余,可以进行适当修改
四、绘制原理图
在绘制原理图之前最好先用面包板把基本电路连接出来,整理完了之后再进行原理图的绘制
以下是我画的原理图(上面有教程视频)
在画原理图的时候我们要对模块使用的引脚进行一些规划,使其在硬件方面发挥最佳的性能,也要考虑其在PCB布线时是否便利
注意:这个原理图有一些错误,对之后在PCB上的测试造成了许多麻烦,陀螺仪上的PB10和PB11应该对调,这两个引脚对应的是单片机上的SCL、SDA,通过这次教训,我明白了画原理图一定一定要检查其与实物的关系
五、设计PCB电路并打板焊机组装
在确保原理图的正确之后,我们就可以开始PCB的绘制(上面有教程)
打板(立创EDA白嫖)
关于焊接的话最好得去学一下,因为这个是我的初次焊接,用了我很长的时间
PCB设计错误:
1.因为我上面原理图设计的错误,导致我PCB设计的时候接线也接错了
2.我应用的MP1584封装与我买的实物大小不同,导致我在组装时发生了引脚浮空
3.除了接线错误之外,我还这个PCB布局还发生了错误,因为我为电机买的6PIN接口不是直针,而是有90°弯折的,所以我只有反向焊接才能保证电机接口的正确
4.并且,我的6PIN接口还阻碍了铜柱的组装(旁边有一个孔是用来给PCB固定的,但是被接口挡住了)
六、算法编写及调试
算法的代码在上面已经有相关的展示。在我看来,开发STM32平衡车最难的就是PID的参数调节,具体可以看上面的视频,这里不过多的赘述。调好参数后,平衡车就能实现基本的平衡功能
七、烧坏驱动模块
泪目
作者:v_for_van