概述
动量轮自平衡自行车STM32
如何DIY一辆自平衡自行车?下面将制作内容分享给大家,欢迎讨论交流~。
目 录
一、硬件篇(附淘宝链接,店铺不定,也可自行搜索购买)
1. STM32F103C8T6最小系统(小蓝板)
2. MPU6050姿态传感器(3.3V供电)
3. 0.96寸OLED显示屏(四针、IIC通信、3.3V供电)
4. HC-05蓝牙模块
5. 超声波测距模块
6. N20电机及驱动(电机选型:DC 12V A12型)
7. 无刷电机动量轮模组
8. 舵机
9. 3S航模电池(注意电池尺寸)
10. 稳压模块及开关
11. 轮子及轴承
12. 车架及转向结构(3D打印)
13. 电路PCB
二、软件篇
1. main.c
2. PID控制算法
3. TIM2中断
4. 电池电压检测
5. OLED显示
6. 代码阅读注意点
三、链接
一、硬件篇(附淘宝链接,店铺不定,也可自行搜索购买)
1. STM32F103C8T6最小系统(小蓝板)
2. MPU6050姿态传感器(3.3V供电)
3. 0.96寸OLED显示屏(四针、IIC通信、3.3V供电)
4. HC-05蓝牙模块
(串口通信、用于接收小车运动指令)
使用教程链接:https://blog.csdn.net/weixin_44325419/article/details/110727911
5. 超声波测距模块
6. N20电机及驱动(电机选型:DC 12V A12型)
7. 无刷电机动量轮模组
该电机自带驱动和光电编码器。
该自平衡自行车中我们使用万宝至无刷伺服电机,内置驱动,支持正反转,PWM调速,并且带有100线编码器AB相双通道信号输出。
该电机接线图如上图所示,实际小车中的线的颜色可能与上图有所不符,大家要按照位置来判断而不是线的颜色。
1.)信号A相和信号B相为编码器脉冲输出端;
2.)正反转切换的线我们直接用单片机的引脚3.3V电平控制,是完全没有问题的;
3.)编码器供电接3.3V;
4.)PWM接单片机的PWM输出,启动运行我们接单片机IO口,在电机初始化时置为高电平;
5.)电源负极接GND,电源正极接12V。
8. 舵机
视频中所使用,有点小贵,可以买便宜的。
9. 3S航模电池(注意电池尺寸)
10. 稳压模块及开关
将航模电池电压降至5V给单片机、舵机、蓝牙、超声波、电机编码器供电。
11. 轮子及轴承
由于小车后轮是通过皮带传动,为减小摩擦,使后轮转动更加顺滑,需在后轮安装微型轴承。(轴承根据车轴尺寸购买)
尺寸如下:
12. 车架及转向结构(3D打印)
点击文章结尾处B站链接三连加关注并留言(或邮箱)即可获取车架及转向结构3D打印模型文件
13. 电路PCB
将上述功能模块集成在一块PCB电路板上(6.5x7.8cm),为方便焊接,电容电阻及三极管均为直插式元件。作者水平有限,PCB供大家参考,其中不足的地方可自行调整更改。
点击文章结尾处B站链接三连加关注并留言(或邮箱)即可获取PCB工程文件
二、软件篇
点击文章结尾处B站链接三连加关注并留言(或邮箱)即可获取Keil源码文件
1. main.c
#include "sys.h"
float AdcValue; //电池电压数字量
float Pitch,Roll,Yaw; //角度
short aacx,aacy,aacz; //加速度传感器原始数据
short gyrox,gyroy,gyroz; //陀螺仪原始数据
int PWM1;
int PWM_MAX=6500,PWM_MIN=-6500; //PWM限幅变量
int Encoder_Motor; //编码器数据(速度)
int main(void)
{
NVIC_Config();
delay_init();
Led_Init();
Beep_Init();
Wave_SRD_Init();
uart3_init(9600);
OLED_Init(); //初始化OLED
OLED_Clear();
adc_Init();
MOTOR_1_Init();
MOTOR_2_Init();
PWM_Init_TIM3(7199,0);//定时器3初始化PWM 10KHZ,用于驱动动量轮电机
PWM_Init_TIM2(9999, 143);//定时器2初始化PWM 50HZ,用于驱动舵机
TIM_SetCompare1(TIM2, 790);//舵机复位
Init_TIM1(9998,7199);
Encoder_Init_TIM4(65535,0);
OLED_ShowString(25,4,"MPU6050...",16);
MPU_Init(); //MPU6050初始化
while(mpu_dmp_init())
{
OLED_ShowString(25,4,"MPU6050 Error",16);
}
OLED_ShowString(25,4,"MPU6050 OK!",16);
Beep=1;
delay_ms(400);
Beep=0;
MPU6050_EXTI_Init();
OLED_Clear();
OLED_ShowString(0,0,"Roll : C",16);
OLED_ShowString(0,3,"Speed: R ",16);
OLED_ShowString(0,6,"Power: V ",16);
while(1)
{
Wave_SRD_Strat();
AdcValue=11.09*(3.3*Get_adc_Average(ADC_Channel_4,10)/0x0fff); //ADC值范围为从0-2^12=4095(111111111111)一般情况下对应电压为0-3.3V
OLED_Showdecimal(55,0,Roll,9,16);
OLED_Showdecimal(55,3,Encoder_Motor*0.25,9,16);
OLED_Showdecimal(50,6,AdcValue,9,16);
}
}
2. PID控制算法
点击文章结尾处B站链接三连加关注并留言(或邮箱)即可获取PID相关教程资料
该小车更够实现直立平衡需要用到两个闭环控制,即直立环(PD控制、负反馈),速度环(PI控制、正反馈),代码原理及调试过程与两轮平衡小车调试过程基本一致。
关于PID控制算法的学习,内容较多,不好详细展开,网上资源丰富,大家可自行学习。这里推荐一篇知乎文章:https://zhuanlan.zhihu.com/p/39573490
3. TIM2中断
为避免小车在运行调试过程中受到超声波避障功能的干扰,可先将超声波避障功能关闭,超声波避障功能在定时器2中断服务函数中实现,所以将TIM2中断使能关闭即可。
//TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //使能TIM2中断,中断模式为更新中断:TIM_IT_Update
void TIM2_IRQHandler()
{
static int count=0;
if(TIM_GetITStatus(TIM2, TIM_IT_Update)==1) //当发生中断时状态寄存器(TIMx_SR)的bit0会被硬件置1
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //状态寄存器(TIMx_SR)的bit0置0
count++;
if(Distance<8) //与障碍物距离小于8cm时,蜂鸣器发出警报病后退
{
Beep=1,Led1=0,Led2=0;
Backward();
TIM_SetCompare1(TIM2, 790);//舵机复位
}
else Beep=0;
if(count==25) //TIM2溢出时间为20ms,20x25=500ms,即后退500ms后停止
{
Stopped();count=0;
}
}
}
4. 电池电压检测
一般航模电池的电量是和电压相关的, 过放必然导致电池永久过放,电池损坏,所以我们有必要通过监控电池电压的变化, 近似表示电池的电量, 在电池电量比较低的情况下, 提醒我们充电,充电时间不超过2个半小时,以免电池过充。长期储存时应确保单节电压在3.8V左右,并且每月充电一次。
3S 满电的时候是 12.6V, 过放时电压低于 9.6V。
2S 满电的时候是 8.4 V , 过放时电压低于 7.4V。
利用STM32内置ADC测量电池电压,ADC值范围为从0-2^12=4095(111111111111)一般情况下对应电压为0-3.3V,而3S航模电池电压为12V,直接测量将烧毁单片机,因此需要将电池分压,原理图如下:
简单分析可知, 电池电压经过电阻分压, 衰竭为原来的 1/11 之后, 送单片机 ADC检测,再将采集到的电压值乘以11即可得到电池的实际电压。(这里是乘以11.09,可根据实际情况进行微调)
#include "adc.h"
#include "delay.h"
//ADC初始化函数
void adc_Init()
{
GPIO_InitTypeDef GPIO_InitStructure; //定义一个引脚初始化的结构体
ADC_InitTypeDef ADC_InitStructure; //定义一个ADC初始化的结构体
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能CPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //使能TIM4时钟
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_4; //引脚0
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN; //引脚输入输出模式为模拟输入模式
GPIO_Init(GPIOA, &GPIO_InitStructure); //根据上面设置好的GPIO_InitStructure参数,初始化引脚GPIOA_PIN0
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M
ADC_DeInit(ADC1); //复位ADC1,将外设 ADC1 的全部寄存器重设为缺省值
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC工作模式:ADC1和ADC2工作在独立模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //是否为扫描(一组)模式:否:单通道模式
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //是否为连续转换模式,否:单次转换模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //转换由软件而不是外部触发启动
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC数据对齐模式:右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1; //顺序进行规则转换的ADC通道的数目
ADC_Init(ADC1, &ADC_InitStructure); //根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1); //使能复位校准
while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束
ADC_StartCalibration(ADC1); //开启AD校准
while(ADC_GetCalibrationStatus(ADC1)); //等待校准结束
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的ADC1的软件转换启动功能
}
//采集ADC值函数,输入参数为ADC通道
u16 Get_adc(u8 chn)
{
ADC_RegularChannelConfig(ADC1, chn, 1, ADC_SampleTime_239Cycles5 ); //ADC1,chn:ADC通道,第3个参数设置该通道的转换顺序(多通道模式下)
//采样时间为239.5周期=239.5/ADCCLOK,ADCCLOK=72/6MHZ(6代表ADC初始化时的分频系数)
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的ADC1的软件转换启动功能
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束
return ADC_GetConversionValue(ADC1); //返回最近一次ADC1规则组的转换结果
}
//采集多次ADC值求平均值函数,输入参数为ADC通道和采集次数
u16 Get_adc_Average(u8 chn, u8 times)
{
u32 temp_val=0;
u8 t;
for(t=0;t<times;t++)
{
temp_val+=Get_adc(chn);
delay_ms(5);
}
return temp_val/times;
}
AdcValue=11.09*(3.3*Get_adc_Average(ADC_Channel_4,10)/0x0fff); //ADC值范围为从0-2^12=4095(111111111111)一般情况下对应电压为0-3.3V
5. OLED显示
一般淘宝购买的OLED显示模块资料中,其显示函数中没有显示小数及显示正负的函数。因此在原有的显示函数中添加了如下函数,从而能够方便的实时显示小车的角度、动量轮转速、电池电压信息。
//显示9位字符,最高位正负,三位整数,第五位小数点,后四位小数部分
//x,y :起点坐标
//len :数字的位数
//size:字体大小
void OLED_Showdecimal(u8 x,u8 y,float num,u8 len,u8 size2)
{
u8 t,temp,len1,temp1;
float temp2;
u8 enshow=0;
if(num < 0)
{
OLED_ShowChar(x,y,'0'- 3,size2);
num =fabs(num);
}
else
OLED_ShowChar(x,y,' ',size2);//第一位显示符号
temp1 = (int)temp;
temp2 = num - temp1;
len1 = len - 6;//len1为整数部分位数,若显示数位需要扩展,修改该行
OLED_ShowChar(x + size2/2*4,y,'0'- 2,size2);//浮点数的第5位显示小数点
x = x + size2/2;
for(t=0;t<len1;t++)//整数部分的显示
{
temp=(int)((num/oled_pow(10,len1-t-1)))%10;
if(enshow==0&&t<(len1-1))
{
if(temp==0)
{
OLED_ShowChar(x+(size2/2)*t,y,' ',size2);
continue;
}else enshow=1;
}
OLED_ShowChar(x+(size2/2)*t,y,temp+'0',size2);
}
OLED_ShowChar(x+(size2/2)*4,y,((int)(temp2*10)%10) + '0',size2); //小数第一位
OLED_ShowChar(x+(size2/2)*5,y,((int)(temp2*100)%10) + '0',size2); //小数第2位
// OLED_ShowChar(x+(size2/2)*6,y,((int)(temp2*1000)%10) + '0',size2); //小数第3位
// OLED_ShowChar(x+(size2/2)*7,y,((int)(temp2*10000)%10) + '0',size2); //小数第4位
}
6. 代码阅读注意点
1.)所有头文件都包含在sys.h中,每个.h文件都包含sys.h,方便函数调用。
#ifndef __SYS_H
#define __SYS_H
#include "stm32f10x.h"
#include "adc.h"
#include "oled.h"
#include "led.h"
#include "beep.h"
#include "wave.h"
#include "control.h"
#include "exti.h"
#include "mpu6050.h"
#include "inv_mpu.h"
#include "inv_mpu_dmp_motion_driver.h"
#include "motor.h"
#include "pwm.h"
#include "encoder.h"
#include "usart.h"
#include "delay.h"
#include <math.h>
#include <stdlib.h>
2.)中断优先级分组配置在sys.c文件中
#include "sys.h"
/*
============================================================================================================================
NVIC_PriorityGroup | NVIC_IRQChannelPreemptionPriority | NVIC_IRQChannelSubPriority | Description
============================================================================================================================
NVIC_PriorityGroup_0 | 0 | 0-15 | 0 bits for pre-emption priority
| | | 4 bits for subpriority
----------------------------------------------------------------------------------------------------------------------------
NVIC_PriorityGroup_1 | 0-1 | 0-7 | 1 bits for pre-emption priority
| | | 3 bits for subpriority
----------------------------------------------------------------------------------------------------------------------------
NVIC_PriorityGroup_2 | 0-3 | 0-3 | 2 bits for pre-emption priority
| | | 2 bits for subpriority
----------------------------------------------------------------------------------------------------------------------------
NVIC_PriorityGroup_3 | 0-7 | 0-1 | 3 bits for pre-emption priority
| | | 1 bits for subpriority
----------------------------------------------------------------------------------------------------------------------------
NVIC_PriorityGroup_4 | 0-15 | 0 | 4 bits for pre-emption priority
| | | 0 bits for subpriority
============================================================================================================================
*/
void NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStruct_extiB5;
NVIC_InitTypeDef NVIC_InitStruct_extiA10;
NVIC_InitTypeDef NVIC_InitStruct_usart3;
NVIC_InitTypeDef NVIC_InitStruct_tim2;
NVIC_InitTypeDef NVIC_InitStruct_tim1;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//4级抢占,4级响应。
//外部中断PB5
NVIC_InitStruct_extiB5.NVIC_IRQChannel=EXTI9_5_IRQn;
NVIC_InitStruct_extiB5.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStruct_extiB5.NVIC_IRQChannelPreemptionPriority=0;
NVIC_InitStruct_extiB5.NVIC_IRQChannelSubPriority=0;
NVIC_Init(&NVIC_InitStruct_extiB5);
//USART3 NVIC 配置
NVIC_InitStruct_usart3.NVIC_IRQChannel = USART3_IRQn;
NVIC_InitStruct_usart3.NVIC_IRQChannelPreemptionPriority=1 ;//抢占优先级3
NVIC_InitStruct_usart3.NVIC_IRQChannelSubPriority = 0; //子优先级3
NVIC_InitStruct_usart3.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStruct_usart3); //根据指定的参数初始化VIC寄存器
// 定时器2中断
NVIC_InitStruct_tim2.NVIC_IRQChannel=TIM2_IRQn; //属于TIM2中断
NVIC_InitStruct_tim2.NVIC_IRQChannelCmd=ENABLE; //中断使能
NVIC_InitStruct_tim2.NVIC_IRQChannelPreemptionPriority=1; //抢占优先级为1级,值越小优先级越高,0级优先级最高
NVIC_InitStruct_tim2.NVIC_IRQChannelSubPriority=1; //响应优先级为1级,值越小优先级越高,0级优先级最高
NVIC_Init(&NVIC_InitStruct_tim2); //根据NVIC_InitStruct_tim1的参数初始化VIC寄存器,设置TIM2中断
//外部中断PA10
NVIC_InitStruct_extiA10.NVIC_IRQChannel=EXTI15_10_IRQn;
NVIC_InitStruct_extiA10.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStruct_extiA10.NVIC_IRQChannelPreemptionPriority=2;
NVIC_InitStruct_extiA10.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStruct_extiA10);
// 定时器1中断
NVIC_InitStruct_tim1.NVIC_IRQChannel=TIM1_UP_IRQn; //属于TIM1中断
NVIC_InitStruct_tim1.NVIC_IRQChannelCmd=ENABLE; //中断使能
NVIC_InitStruct_tim1.NVIC_IRQChannelPreemptionPriority=2; //抢占优先级为1级,值越小优先级越高,0级优先级最高
NVIC_InitStruct_tim1.NVIC_IRQChannelSubPriority=2; //响应优先级为1级,值越小优先级越高,0级优先级最高
NVIC_Init(&NVIC_InitStruct_tim1); //根据NVIC_InitStruct_tim1的参数初始化VIC寄存器,设置TIM2中断
}
3.)STM32F10x系列的MCU复位后,PA13/14/15 & PB3/4默认配置为JTAG功能。有时我们为了充分利用MCU I/O口的资源,会把这些端口设置为普通I/O口。
使用JLINK向STM32烧录程序时,需要使用6个芯片的引脚(以STM32F103C8T6为例),分别是PB4 / JNTRST,PB3 / JTDO,PA13 / JTMS,PA14 / JTCK,PA15 / JTDI,NRST。当芯片IO口资源比较紧张时,可选择SW模式烧录程序。SWD只需用到PA13 / JTMS,PA14 / JTCK两根线,NREST可以接可不接,剩下的PB4 / JNTRST,PB3 / JTDO和PA15 / JTDI就可以当然普通IO使用,但是这三个口当然普通IO使用时需要先进行如下配置。(这里MPU6050模块用到PB3和PB4引脚)
mpuiic.c
//初始化IIC
void MPU_IIC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_AFIO|RCC_APB2Periph_GPIOB,ENABLE); //打开PB口时钟和AFIO复用时钟
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE); //重映射
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_4; // 端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO口速度为50MHz
GPIO_Init(GPIOB, &GPIO_InitStructure); //根据设定参数初始化GPIO
GPIO_SetBits(GPIOB,GPIO_Pin_3|GPIO_Pin_4); //PB3,PB4 输出高
}
三、链接
bilibili_[动量轮自平衡自行车]STM32_PID(开源-含硬件资料)https://www.bilibili.com/video/BV1M3411J77m?share_source=copy_web
最后
以上就是怕黑棒棒糖为你收集整理的【动量轮自平衡自行车】STM32_PID(开源-含硬件资料)的全部内容,希望文章能够帮你解决【动量轮自平衡自行车】STM32_PID(开源-含硬件资料)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复