赞
踩
动量轮自平衡自行车STM32
目 录
3. 0.96寸OLED显示屏(四针、IIC通信、3.3V供电)
(串口通信、用于接收小车运动指令)
使用教程链接:https://blog.csdn.net/weixin_44325419/article/details/110727911
该电机自带驱动和光电编码器。
该自平衡自行车中我们使用万宝至无刷伺服电机,内置驱动,支持正反转,PWM调速,并且带有100线编码器AB相双通道信号输出。
该电机接线图如上图所示,实际小车中的线的颜色可能与上图有所不符,大家要按照位置来判断而不是线的颜色。
1.)信号A相和信号B相为编码器脉冲输出端;
2.)正反转切换的线我们直接用单片机的引脚3.3V电平控制,是完全没有问题的;
3.)编码器供电接3.3V;
4.)PWM接单片机的PWM输出,启动运行我们接单片机IO口,在电机初始化时置为高电平;
5.)电源负极接GND,电源正极接12V。
视频中所使用,有点小贵,可以买便宜的。
将航模电池电压降至5V给单片机、舵机、蓝牙、超声波、电机编码器供电。
由于小车后轮是通过皮带传动,为减小摩擦,使后轮转动更加顺滑,需在后轮安装微型轴承。(轴承根据车轴尺寸购买)
尺寸如下:
点击文章结尾处B站链接三连加关注并留言(或邮箱)即可获取车架及转向结构3D打印模型文件
将上述功能模块集成在一块PCB电路板上(6.5x7.8cm),为方便焊接,电容电阻及三极管均为直插式元件。作者水平有限,PCB供大家参考,其中不足的地方可自行调整更改。
点击文章结尾处B站链接三连加关注并留言(或邮箱)即可获取PCB工程文件
点击文章结尾处B站链接三连加关注并留言(或邮箱)即可获取Keil源码文件
- #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);
- }
- }
点击文章结尾处B站链接三连加关注并留言(或邮箱)即可获取PID相关教程资料
该小车更够实现直立平衡需要用到两个闭环控制,即直立环(PD控制、负反馈),速度环(PI控制、正反馈),代码原理及调试过程与两轮平衡小车调试过程基本一致。
关于PID控制算法的学习,内容较多,不好详细展开,网上资源丰富,大家可自行学习。这里推荐一篇知乎文章:https://zhuanlan.zhihu.com/p/39573490
为避免小车在运行调试过程中受到超声波避障功能的干扰,可先将超声波避障功能关闭,超声波避障功能在定时器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;
- }
- }
- }
一般航模电池的电量是和电压相关的, 过放必然导致电池永久过放,电池损坏,所以我们有必要通过监控电池电压的变化, 近似表示电池的电量, 在电池电量比较低的情况下, 提醒我们充电,充电时间不超过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
一般淘宝购买的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位
- }
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 输出高
-
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。