动量轮自平衡自行车STM32
如何DIY一辆自平衡自行车?下面将制作内容分享给大家,欢迎讨论交流~。
目 录
3. 0.96寸OLED显示屏(四针、IIC通信、3.3V供电)
一、硬件篇(附淘宝链接,店铺不定,也可自行搜索购买)
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 输出高
}
三、链接
版权声明:本文为CSDN博主「_旺仔小菠萝」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_47820462/article/details/122797752
暂无评论