基于stm32的自平衡小车

26 篇文章 6 订阅
20 篇文章 2 订阅

引言

1、系统概述

1.1、设计任务

利用stm32做一辆自平衡小车

1.2、设计要求

利用IIC和MPU6050、OLED12864进行通信,使用pid算法到自平衡,熟练掌握PID算法

2、方案设计与论证

2.1、芯片选择方案

芯片可以选择stm32和arduino,基于学习目的,使用stm32

stm32是一个低功耗,高性能32位单片机,片内含4k Bytes ISP(In-system programmable)的可反复擦写1000次的Flash只读程序存储器。主要性能有:与MCS-51单片机产品兼容、全静态操作:0Hz~33Hz、 三级加密程序存储器、32个可编程I/O口线、三个16位定时器/计数器、八个中断源、全双工UART串行通道、掉电后中断可唤醒、看门狗定时器、双数据指针、掉电标识符、易编程。

2.2 、系统概述

本设计是一个具有自动调节平衡功能的两轮小车。由MPU6050、12864OLED显示屏、电机驱动块、电机、供电电路等模块组成。本项目研究一种使用单片机PID算法的自平衡方案。这种方案后续可以制作成为自平衡代步工具,自平衡自行车等等。

2.3、设计要求

  1. IIC通信
  2. PID算法的调节,要求抖动不得超过1cm’
  3. 小车可以正常达到自平衡
  4. oled显示偏航/俯仰/滚动角的数据

2.4、系统总体设计

利用stm32和MPU6050进行通信,实时获取mpu6050发送过来的数据,并且再oled上面显示,利用mpu6050的数据,结合PID算法,控制电机驱动块去控制电机的正方转,以达到自平衡的目的。

2.5、各功能模块程序实现原理分析

2.5.1、MPU6050模块的介绍

MPU6050内部整合了三轴MEMS陀螺仪、三轴MEMS加速度计以及一个可扩展的数字运动处理器DMP(Digital Motion Processor),而且还可以连接一个第三方数字传感器(如磁力计),这样的话,就可以通过IIC接口输出一个9轴信号(链接第三方数字传感器才可以输出九轴信号,否则只有六轴信号)。更加方便的是,有了DMP,可以结合InvenSense公司提供的运动处理资料库,实现姿态解算。通过自带的DMP,可以通过IIC接口输出9轴融合演算的数据,大大降低了运动处理运算对操作系统的负荷,同时也降低了开发难度。其实,简单一句话说,陀螺仪就是测角速度的,加速度传感器就是测角加速度的,二者数据通过算法就可以得到PITCH、YAW、ROLL角了。

陀螺仪知识点介绍:

陀螺仪是用高速回转体的动量矩敏感壳体相对惯性空间绕正交于自转轴的一个或二个轴的角运动检测装置。利用其他原理制成的角运动检测装置起同样功能的也称陀螺仪。
从力学的观点近似的分析陀螺的运动时,可以把它看成是一个刚体,刚体上有一个万向支点,而陀螺可以绕着这个支点作三个自由度的转动,所以陀螺的运动是属于刚体绕一个定点的转动运动。更确切地说,一个绕对称铀高速旋转的飞轮转子叫陀螺。将陀螺安装在框架装置上,使陀螺的自转轴有角转动的自由度,这种装置的总体叫做陀螺仪。
陀螺仪的原理就是,一个旋转物体的旋转轴所指的方向在不受外力影响时,是不会改变的。人们根据这个道理,用它来保持方向,制造出来的东西就叫陀螺仪。我们骑自行车其实也是利用了这个原理。轮子转得越快越不容易倒,因为车轴有一股保持水平的力量。陀螺仪在工作时要给它一个力,使它快速旋转起来,一般能达到每分钟几十万转,可以工作很长时间。然后用多种方法读取轴所指示的方向,并自动将数据信号传给控制系统。

2.5.2、OLED12864显示屏

OLED 屏幕作为一种新型的显示技术,其自身可以发光,亮度,对比度高,功耗低,在当下备受追捧。而在我们正常的显示调整参数过程中,我们越来越多的使用这种屏幕。我们使用的一般是分辨率为 128×64 ,屏幕尺寸为 0.96 寸。由于其较小的尺寸和比较高的分辨率,让它有着很好的显示效果和便携性。

2.5.3、LN298N电机驱动块

L298N是专用驱动集成电路,属于H桥集成电路,与L293D的差别是起输出电流增大,功率增强。其输出电流为2A,最高电流4A,最高工作电压50V,可以驱动感性负载,如大功率直流电机,步进电机,电磁阀等等,特别是其输入端可以与单片机直接相连,从而很方便地受单片机控制。当驱动直流电机时,可以直接控制步进电机,并可以实现电机的正转和反转,实现此功能只需要改变输入端的逻辑电平。为了避免电机对单片机的干扰,本模块加入光耦,进行光电隔离,从而使系统能够稳定可靠的工作。

3、单片机的选择及硬件设计介绍

3.1、单片机选择

出于实验目的选择STM32F103ZET6,性能足,完善功能强大,管脚也够,后续完善功能将改为STM32F103C8T6。

3.2、电路设计

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4、系统程序

4.1、主程序

4.1.1主程序设计如下

由mpu6050采集角度信息,并发送到STM32上,STM32进行PID的调节,反馈给电机驱动块,电机驱动块调整电机转动方向和速度,以达到调整角度的目的,电机驱动块和mpu6050闭环。

4.1.2主程序流程图

在这里插入图片描述

4.2、主程序代码

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "key.h"

#include "mpu6050.h"
#include "inv_mpu.h"
#include "inv_mpu_dmp_motion_driver.h" 

#include "oled.h"
#include "stdio.h"
#include <string.h>

#include "pid.h"
#include "motor.h"


	extern int Moto1,Moto2;  		
	float pitch,roll,yaw; 		//欧拉角
	short aacx,aacy,aacz;		//加速度传感器原始数据
	short gyrox,gyroy,gyroz;	//陀螺仪原始数据
	int temp1;					//温度
	u8 stop_flag=0;    										//小车停止标志位
	
void calculation(void);


//void pid_pingheng();

//串口1发送1个字符 
//c:要发送的字符
void usart1_send_char(u8 c)
{ 
	while(__HAL_UART_GET_FLAG(&UART1_Handler,UART_FLAG_TC)==RESET){};//循环发送,直到发送完毕   
	HAL_UART_Transmit(&UART1_Handler,&c,1,1000);	
} 



int main(void)
{	

    HAL_Init();                    	 	//初始化HAL库    
    Stm32_Clock_Init(RCC_PLL_MUL9);   	//设置时钟,72M
	delay_init(72);               		//初始化延时函数
	MiniBalance_PWM_Init(20000-1,72-1);   //初始化PWM	72m/72=1m	1m/20000=50hz 1/50hz=0.02s
	PID_Init();
	uart_init(115200);					//初始化串口	
	LED_Init();							//初始化LED	
	KEY_Init();							//初始化按键
	MPU_Init();							//初始化MPU6050
	for(int i=0;i<10;i++){
	OLED_Init();	
	delay_ms(20);
	}
	OLED_ColorTurn(0);//0正常显示,1 反色显示
	OLED_DisplayTurn(0);//0正常显示 1 屏幕翻转显示
	
	if(mpu_dmp_init()!=0) 	
		{
		printf(".");
		OLED_ShowString(50,20,"wait",12,1);
		OLED_Refresh();
} 		if(mpu_dmp_init()!=0)	OLED_ShowString(50,20,"   ",12,1);
	OLED_ShowString(0,0,"pitch",16,1);
	OLED_ShowString(0,16,"err",16,1);
	OLED_ShowString(0,32,"L_err",16,1);
	OLED_ShowString(0,48,"PID",16,1);

//			int i=0;
 	while(1)
	{
		while(mpu_dmp_get_data(&pitch,&roll,&yaw)!=0)
		{ 
//			temp=MPU_Get_Temperature();	//得到温度值
			MPU_Get_Accelerometer(&aacx,&aacy,&aacz);	//得到加速度传感器数据
			MPU_Get_Gyroscope(&gyrox,&gyroy,&gyroz);	//得到陀螺仪数据
			//mpu6050_send_data(aacx,aacy,aacz,gyrox,gyroy,gyroz);//用自定义帧发送加速度和陀螺仪原始数据
			//usart1_report_imu(aacx,aacy,aacz,gyrox,gyroy,gyroz,(int)(roll*100),(int)(pitch*100),(int)(yaw*10));
			LED0=!LED0;//LED闪烁
		}
//	printf("roll:%d pitch:%d yaw:%d \r\n",(int)(roll*100),(int)(pitch*100),(int)(yaw*10));
		
	calculation();	//整体计算PID,读取角度值等等
		
	if(pitch<0){OLED_ShowString(70,0,"-",16,1);OLED_ShowNum(90,0,-pitch,2,16,1);}
else			{OLED_ShowString(70,0," ",16,1);OLED_ShowNum(90,0,pitch,2,16,1);}
	if(err<0){OLED_ShowString(40,16,"-",16,1);OLED_ShowNum(60,16,-err,5,16,1);}
else			{OLED_ShowString(40,16," ",16,1);OLED_ShowNum(60,16,err,5,16,1);}
	if(last_err<0){OLED_ShowString(40,32,"-",16,1);OLED_ShowNum(60,32,-llast_err,5,16,1);}
else			{OLED_ShowString(40,32," ",16,1);OLED_ShowNum(60,32,llast_err,5,16,1);}
	if(temp1<0){OLED_ShowString(40,48,"-",16,1);OLED_ShowNum(60,48,-temp1,5,16,1);}
else			{OLED_ShowString(40,48," ",16,1);OLED_ShowNum(60,48,temp1,5,16,1);}		
		OLED_Refresh();
	delay_ms(50);



	}
} 


/**************************************************************************
函数功能:整体计算PID,读取角度值等等
入口参数:未知
返回  值:无
						本应该不写在这,但是因为各种原因只能放这
**************************************************************************/
void calculation(void)
{
if(mpu_dmp_get_data(&pitch,&roll,&yaw)==0)
		{ 

			pid.Pv = (int)pitch*100;										//角度*十倍
//			if(Turn_Off(pid.Pv)) stop_flag=1;						//检测是否小车异常,异常就停止
//			else stop_flag=0;
			
			if(pid.Pv<-3000||pid.Pv>3000)
			{	                                                 //===倾角大于40度关闭电机
				IN1=0;                                            
				IN2=0;
				IN3=0;
				IN4=0;
				stop_flag=1;	
			}else{stop_flag=0;}
			
			Moto1 = balance(pid.Pv) ;		//获得PWM输出值
			Moto2 =	balance(pid.Pv);
			temp1	=	balance(pid.Pv);
			
			Xianfu_Pwm();					//对PWM进行限幅
			Set_Pwm(Moto1,Moto2);			//设置PWM
		}
}

/*
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_9,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_11,GPIO_PIN_SET);

HAL_GPIO_WritePin(GPIOB,GPIO_PIN_12,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_13,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_14,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_RESET);

delay_ms(1000);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_12,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_13,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_14,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_SET);
delay_ms(1000);
*/



4.3、模块程序代码

电机驱动块:

#include "motor.h"

int Moto1,Moto2;  //电机PWM变量

void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

/**************************************************************************
函数功能:电机驱动输出IO初始化
入口参数:无
返回  值:无
**************************************************************************/	
void MiniBalance_Motor_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	
//	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //使能PB端口时钟
	  __HAL_RCC_GPIOB_CLK_ENABLE();						//使能PB端口时钟

	GPIO_InitStructure.Pin = GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15;	//端口配置
	GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;      //推挽输出
	GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;     //50M
	HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);					      //根据设定参数初始化GPIOB 
	
}
/**************************************************************************
函数功能:电机驱动PWM输出IO口初始化
入口参数:无
返回  值:无
**************************************************************************/	 

	TIM_HandleTypeDef htim1;					//tim1结构体初始化
void MiniBalance_PWM_Init(u16 arr,u16 psc)
{		 		
	GPIO_InitTypeDef	GPIO_InitStructure;		//引脚结构体初始化
	TIM_OC_InitTypeDef sConfigOC = {0};			//输出比较结构体初始化
	MiniBalance_Motor_Init();					
	
	__HAL_RCC_GPIOA_CLK_ENABLE();						//使能PA端口时钟
	__HAL_RCC_TIM1_CLK_ENABLE();						// 使能TIM1端口时钟 TIM1 TIM8在APB2

	//设置该引脚为复用输出功能,输出TIM1 CH1 CH4的PWM脉冲波形
	GPIO_InitStructure.Pin= GPIO_PIN_8|GPIO_PIN_11; //TIM_CH1 //TIM_CH4
	GPIO_InitStructure.Mode = GPIO_MODE_AF_PP;  //复用推挽输出
	GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
	HAL_GPIO_Init(GPIOA, &GPIO_InitStructure);		//根据设定参数初始化GPIOA
	
	htim1.Instance = TIM1;
	htim1.Init.Prescaler = psc; //设置用来作为TIMx时钟频率除数的预分频值  不分频
	htim1.Init.Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值	
	htim1.Init.ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
	htim1.Init.CounterMode = TIM_COUNTERMODE_UP;//TIM向上计数模式
	htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;	//disable自动重装载
  if (HAL_TIM_PWM_Init(&htim1) != HAL_OK)
  {
    Error_Handler();
	  
  }
  if (HAL_TIM_OC_Init(&htim1) != HAL_OK)
  {
    Error_Handler();
  }

  sConfigOC.OCMode = TIM_OCMODE_PWM1;		//选择定时器模式:TIM脉冲宽度调制模式1
  
  sConfigOC.Pulse = 0;						//设置待装入捕获比较寄存器的脉冲值
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;//输出极性:TIM输出比较极性高
  sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;	//指定互补输出极性。
  
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET;
  sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET;
  /**使能通道1**/
   if (HAL_TIM_OC_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1) != HAL_OK)
  {
    Error_Handler();
  }
  
  /**使能通道2**/

  if (HAL_TIM_OC_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_4) != HAL_OK)
  {
    Error_Handler();
  }
  sConfigOC.OCMode = TIM_OCMODE_TIMING; //清除模式配置 方便下一个通道使用
  
    HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);
	HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_4);
  
  __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 5000);
  __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, 5000);

}


/**************************************************************************
函数功能:获得整数绝对值函数
入口参数:整数
返回  值:绝对值
**************************************************************************/	 
int myabs(int a)
{ 		   
	int temp;
	if(a<0)  temp=-a;  
		else temp=a;
	return temp;
}
/**************************************************************************
函数功能:最后设置PWM函数,并检测是否关闭电机
入口参数:无
返回  值:无
**************************************************************************/	 
void Set_Pwm(int moto1,int moto2)
{
	if(stop_flag==0)
	{
    	if(moto1>0)			IN1=1,	IN2=0;
			else 	        IN1=0,	IN2=1;
			PWMA=myabs(moto1);
		if(moto2>0)			IN3=1,	IN4=0;
			else        	IN3=0,	IN4=1;
			PWMB=myabs(moto2);
	}
	
}
/**************************************************************************
函数功能:PWM限幅函数
入口参数:无
返回  值:无
**************************************************************************/	 
void Xianfu_Pwm(void)
{	
	  int Amplitude=10000;    //===PWM满幅是20000 限制在10000
    if(Moto1	<	-Amplitude) Moto1=-Amplitude;	
	if(Moto1	>	Amplitude)  Moto1=Amplitude;	
	if(Moto2	<	-Amplitude) Moto2=-Amplitude;	
	if(Moto2	>	Amplitude)  Moto2=Amplitude;		
	
}
/**************************************************************************
函数功能:电机异常关闭函数
入口参数:角度
返回  值:1:关闭,0:不关闭
**************************************************************************/	 
u8 Turn_Off(signed int angle)
{
	    u8 temp=0;
			if(angle<-300||angle>300)
			{	                                                 //===倾角大于40度关闭电机
				temp=1;                                            //===Flag_Stop置1关闭电机
				IN1=0;                                            
				IN2=0;
				IN3=0;
				IN4=0;
      }
			return temp;
}




PID.C

#include "pid.h"
#include "usart.h"
#include "motor.h"

 PID pid;
signed  int  err;
signed  int last_err;
signed  int llast_err;
/**************************************************************************
函数功能:PID数据初始化
入口参数:无
返回  值:无
**************************************************************************/
void PID_Init()
{
	          /*平衡PID环控制参数初始化*/
	
		pid.Sv =  0;		
		pid.Kp = 10;
		pid.Ki =  0.001;		
		pid.Kd = 0.05;  
}

/**************************************************************************
函数功能:以下三个函数,分别计算各个环的PID值,并返回
入口参数:未知
返回  值:无
**************************************************************************/

/*小车平衡环部分,微分+比例控制	微分变量为直接读取的加速度*/
extern short pitch;
int	leijia;		//累加
int balance(float Angle)
{  		int balance;
	if(stop_flag==0){
	//	Angle=pid.Pv;
		err=(0-Angle);
		leijia+=last_err;
		balance=pid.Kp*last_err+pid.Ki*(leijia)+pid.Kd*(last_err-llast_err);
		llast_err=last_err;
		last_err=err;
		
		printf("Angle:%.2f	balance:%d		pid.Kp:%d		Bias:%d  \r\n",Angle,balance,pid.Kp,err);

	}
	return balance;
}

OLED.C是使用中景园的代码:


#include "oled.h"
#include "stdlib.h"
#include "stdint.h"
#include "oledfont.h"  	 
#include "delay.h"

#define OLED_ADDRESS  0x78

uint8_t OLED_GRAM[144][8];

//反显函数
void OLED_ColorTurn(uint8_t i)
{
	if(i==0)
		{
			OLED_WR_Byte(0xA6,OLED_CMD);//正常显示
		}
	if(i==1)
		{
			OLED_WR_Byte(0xA7,OLED_CMD);//反色显示
		}
}

//屏幕旋转180度
void OLED_DisplayTurn(uint8_t i)
{
	if(i==0)
		{
			OLED_WR_Byte(0xC8,OLED_CMD);//正常显示
			OLED_WR_Byte(0xA1,OLED_CMD);
		}
	if(i==1)
		{
			OLED_WR_Byte(0xC0,OLED_CMD);//反转显示
			OLED_WR_Byte(0xA0,OLED_CMD);
		}
}

//延时
void IIC_delay(void)
{
	uint8_t t=3;
	while(t--);
}

//起始信号
void I2C_Start(void)
{
	OLED_SDA_Set();
	OLED_SCL_Set();
	IIC_delay();
	OLED_SDA_Clr();
	IIC_delay();
	OLED_SCL_Clr();
	IIC_delay();
}

//结束信号
void I2C_Stop(void)
{
	OLED_SDA_Clr();
	OLED_SCL_Set();
	IIC_delay();
	OLED_SDA_Set();
}

//等待信号响应
void I2C_WaitAck(void) //测数据信号的电平
{
	OLED_SDA_Set();
	IIC_delay();
	OLED_SCL_Set();
	IIC_delay();
	OLED_SCL_Clr();
	IIC_delay();
}

//写入一个字节
void Send_Byte(uint8_t dat)
{
	uint8_t i;
	for(i=0;i<8;i++)
	{
		if(dat&0x80)//将dat的8位从最高位依次写入
		{
			OLED_SDA_Set();
    }
		else
		{
			OLED_SDA_Clr();
    }
		IIC_delay();
		OLED_SCL_Set();
		IIC_delay();
		OLED_SCL_Clr();//将时钟信号设置为低电平
		dat<<=1;
  }
}

//发送一个字节
//mode:数据/命令标志 0,表示命令;1,表示数据;
void OLED_WR_Byte(uint8_t dat,uint8_t mode)
{
	I2C_Start();
	Send_Byte(0x78);
	I2C_WaitAck();
	if(mode){Send_Byte(0x40);}
  else{Send_Byte(0x00);}
	I2C_WaitAck();
	Send_Byte(dat);
	I2C_WaitAck();
	I2C_Stop();
}

//开启OLED显示 
void OLED_DisPlay_On(void)
{
	OLED_WR_Byte(0x8D,OLED_CMD);//电荷泵使能
	OLED_WR_Byte(0x14,OLED_CMD);//开启电荷泵
	OLED_WR_Byte(0xAF,OLED_CMD);//点亮屏幕
}

//关闭OLED显示 
void OLED_DisPlay_Off(void)
{
	OLED_WR_Byte(0x8D,OLED_CMD);//电荷泵使能
	OLED_WR_Byte(0x10,OLED_CMD);//关闭电荷泵
	OLED_WR_Byte(0xAE,OLED_CMD);//关闭屏幕
}

//更新显存到OLED	
void OLED_Refresh(void)
{
	uint8_t i,n;
	for(i=0;i<8;i++)
	{
		OLED_WR_Byte(0xb0+i,OLED_CMD); //设置行起始地址
		OLED_WR_Byte(0x00,OLED_CMD);   //设置低列起始地址
		OLED_WR_Byte(0x10,OLED_CMD);   //设置高列起始地址
		I2C_Start();
		Send_Byte(0x78);
		I2C_WaitAck();
		Send_Byte(0x40);
		I2C_WaitAck();
		for(n=0;n<128;n++)
		{
			Send_Byte(OLED_GRAM[n][i]);
			I2C_WaitAck();
		}
		I2C_Stop();
  }
}
//清屏函数
void OLED_Clear(void)
{
	uint8_t i,n;
	for(i=0;i<8;i++)
	{
	   for(n=0;n<128;n++)
			{
			 OLED_GRAM[n][i]=0;//清除所有数据
			}
  }
	OLED_Refresh();//更新显示
}

//画点 
//x:0~127
//y:0~63
//t:1 填充 0,清空	
void OLED_DrawPoint(uint8_t x,uint8_t y,uint8_t t)
{
	uint8_t i,m,n;
	i=y/8;
	m=y%8;
	n=1<<m;
	if(t){OLED_GRAM[x][i]|=n;}
	else
	{
		OLED_GRAM[x][i]=~OLED_GRAM[x][i];
		OLED_GRAM[x][i]|=n;
		OLED_GRAM[x][i]=~OLED_GRAM[x][i];
	}
}

//画线
//x1,y1:起点坐标
//x2,y2:结束坐标
void OLED_DrawLine(uint8_t x1,uint8_t y1,uint8_t x2,uint8_t y2,uint8_t mode)
{
	uint16_t t; 
	int xerr=0,yerr=0,delta_x,delta_y,distance;
	int incx,incy,uRow,uCol;
	delta_x=x2-x1; //计算坐标增量 
	delta_y=y2-y1;
	uRow=x1;//画线起点坐标
	uCol=y1;
	if(delta_x>0)incx=1; //设置单步方向 
	else if (delta_x==0)incx=0;//垂直线 
	else {incx=-1;delta_x=-delta_x;}
	if(delta_y>0)incy=1;
	else if (delta_y==0)incy=0;//水平线 
	else {incy=-1;delta_y=-delta_x;}
	if(delta_x>delta_y)distance=delta_x; //选取基本增量坐标轴 
	else distance=delta_y;
	for(t=0;t<distance+1;t++)
	{
		OLED_DrawPoint(uRow,uCol,mode);//画点
		xerr+=delta_x;
		yerr+=delta_y;
		if(xerr>distance)
		{
			xerr-=distance;
			uRow+=incx;
		}
		if(yerr>distance)
		{
			yerr-=distance;
			uCol+=incy;
		}
	}
}
//x,y:圆心坐标
//r:圆的半径
void OLED_DrawCircle(uint8_t x,uint8_t y,uint8_t r)
{
	int a, b,num;
    a = 0;
    b = r;
    while(2 * b * b >= r * r)      
    {
        OLED_DrawPoint(x + a, y - b,1);
        OLED_DrawPoint(x - a, y - b,1);
        OLED_DrawPoint(x - a, y + b,1);
        OLED_DrawPoint(x + a, y + b,1);
 
        OLED_DrawPoint(x + b, y + a,1);
        OLED_DrawPoint(x + b, y - a,1);
        OLED_DrawPoint(x - b, y - a,1);
        OLED_DrawPoint(x - b, y + a,1);
        
        a++;
        num = (a * a + b * b) - r*r;//计算画的点离圆心的距离
        if(num > 0)
        {
            b--;
            a--;
        }
    }
}



//在指定位置显示一个字符,包括部分字符
//x:0~127
//y:0~63
//size1:选择字体 6x8/6x12/8x16/12x24
//mode:0,反色显示;1,正常显示
void OLED_ShowChar(uint8_t x,uint8_t y,uint8_t chr,uint8_t size1,uint8_t mode)
{
	uint8_t i,m,temp,size2,chr1;
	uint8_t x0=x,y0=y;
	if(size1==8)size2=6;
	else size2=(size1/8+((size1%8)?1:0))*(size1/2);  //得到字体一个字符对应点阵集所占的字节数
	chr1=chr-' ';  //计算偏移后的值
	for(i=0;i<size2;i++)
	{
		if(size1==8)
			  {temp=asc2_0806[chr1][i];} //调用0806字体
		else if(size1==12)
        {temp=asc2_1206[chr1][i];} //调用1206字体
		else if(size1==16)
        {temp=asc2_1608[chr1][i];} //调用1608字体
		else if(size1==24)
        {temp=asc2_2412[chr1][i];} //调用2412字体
		else return;
		for(m=0;m<8;m++)
		{
			if(temp&0x01)OLED_DrawPoint(x,y,mode);
			else OLED_DrawPoint(x,y,!mode);
			temp>>=1;
			y++;
		}
		x++;
		if((size1!=8)&&((x-x0)==size1/2))
		{x=x0;y0=y0+8;}
		y=y0;
  }
}


//显示字符串
//x,y:起点坐标  
//size1:字体大小 
//*chr:字符串起始地址 
//mode:0,反色显示;1,正常显示
void OLED_ShowString(uint8_t x,uint8_t y,uint8_t *chr,uint8_t size1,uint8_t mode)
{
	while((*chr>=' ')&&(*chr<='~'))//判断是不是非法字符!
	{
		OLED_ShowChar(x,y,*chr,size1,mode);
		if(size1==8)x+=6;
		else x+=size1/2;
		chr++;
  }
}

//m^n
uint32_t OLED_Pow(uint8_t m,uint8_t n)
{
	uint32_t result=1;
	while(n--)
	{
	  result*=m;
	}
	return result;
}

//显示数字
//x,y :起点坐标
//num :要显示的数字
//len :数字的位数
//size:字体大小
//mode:0,反色显示;1,正常显示
void OLED_ShowNum(uint8_t x,uint8_t y,uint32_t num,uint8_t len,uint8_t size1,uint8_t mode)
{
	uint8_t t,temp,m=0;
	if(size1==8)m=2;
	for(t=0;t<len;t++)
	{
		temp=(num/OLED_Pow(10,len-t-1))%10;
			if(temp==0)
			{
				OLED_ShowChar(x+(size1/2+m)*t,y,'0',size1,mode);
      }
			else 
			{
			  OLED_ShowChar(x+(size1/2+m)*t,y,temp+'0',size1,mode);
			}
  }
}

//显示汉字
//x,y:起点坐标
//num:汉字对应的序号
//mode:0,反色显示;1,正常显示
void OLED_ShowChinese(uint8_t x,uint8_t y,uint8_t num,uint8_t size1,uint8_t mode)
{
	uint8_t m,temp;
	uint8_t x0=x,y0=y;
	uint16_t i,size3=(size1/8+((size1%8)?1:0))*size1;  //得到字体一个字符对应点阵集所占的字节数
	for(i=0;i<size3;i++)
	{
		if(size1==16)
				{temp=Hzk1[num][i];}//调用16*16字体
		else if(size1==24)
				{temp=Hzk2[num][i];}//调用24*24字体
		else if(size1==32)       
				{temp=Hzk3[num][i];}//调用32*32字体
		else if(size1==64)
				{temp=Hzk4[num][i];}//调用64*64字体
		else return;
		for(m=0;m<8;m++)
		{
			if(temp&0x01)OLED_DrawPoint(x,y,mode);
			else OLED_DrawPoint(x,y,!mode);
			temp>>=1;
			y++;
		}
		x++;
		if((x-x0)==size1)
		{x=x0;y0=y0+8;}
		y=y0;
	}
}

//num 显示汉字的个数
//space 每一遍显示的间隔
//mode:0,反色显示;1,正常显示
void OLED_ScrollDisplay(uint8_t num,uint8_t space,uint8_t mode)
{
	uint8_t i,n,t=0,m=0,r;
	while(1)
	{
		if(m==0)
		{
	    OLED_ShowChinese(128,24,t,16,mode); //写入一个汉字保存在OLED_GRAM[][]数组中
			t++;
		}
		if(t==num)
			{
				for(r=0;r<16*space;r++)      //显示间隔
				 {
					for(i=1;i<144;i++)
						{
							for(n=0;n<8;n++)
							{
								OLED_GRAM[i-1][n]=OLED_GRAM[i][n];
							}
						}
           OLED_Refresh();
				 }
        t=0;
      }
		m++;
		if(m==16){m=0;}
		for(i=1;i<144;i++)   //实现左移
		{
			for(n=0;n<8;n++)
			{
				OLED_GRAM[i-1][n]=OLED_GRAM[i][n];
			}
		}
		OLED_Refresh();
	}
}

//x,y:起点坐标
//sizex,sizey,图片长宽
//BMP[]:要写入的图片数组
//mode:0,反色显示;1,正常显示
void OLED_ShowPicture(uint8_t x,uint8_t y,uint8_t sizex,uint8_t sizey,uint8_t BMP[],uint8_t mode)
{
	uint16_t j=0;
	uint8_t i,n,temp,m;
	uint8_t x0=x,y0=y;
	sizey=sizey/8+((sizey%8)?1:0);
	for(n=0;n<sizey;n++)
	{
		 for(i=0;i<sizex;i++)
		 {
				temp=BMP[j];
				j++;
				for(m=0;m<8;m++)
				{
					if(temp&0x01)OLED_DrawPoint(x,y,mode);
					else OLED_DrawPoint(x,y,!mode);
					temp>>=1;
					y++;
				}
				x++;
				if((x-x0)==sizex)
				{
					x=x0;
					y0=y0+8;
				}
				y=y0;
     }
	 }
}
//OLED的初始化
void OLED_Init(void)
{
//	GPIO_InitTypeDef  GPIO_InitStructure;
// 	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG|RCC_APB2Periph_GPIOD, ENABLE);	 //使能A端口时钟
//	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;	 
// 	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; 		 //推挽输出
//	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度50MHz
// 	GPIO_Init(GPIOG, &GPIO_InitStructure);	  //初始化GPIOG12
// 	GPIO_SetBits(GPIOG,GPIO_Pin_12);
//	
//	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
// 	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; 		 //推挽输出
//	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度50MHz
// 	GPIO_Init(GPIOD, &GPIO_InitStructure);	  //初始化GPIOD1,5,15
// 	GPIO_SetBits(GPIOD,GPIO_Pin_5);
//	
//	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
// 	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 		 //推挽输出
//	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度50MHz
// 	GPIO_Init(GPIOD, &GPIO_InitStructure);	  //初始化GPIOD1,5,15
// 	GPIO_SetBits(GPIOD,GPIO_Pin_4);	
    MX_GPIO_Init();	
	
	OLED_RES_Clr();
	HAL_Delay(200);
	OLED_RES_Set();
	
	OLED_WR_Byte(0xAE,OLED_CMD);//--关闭oled面板
	OLED_WR_Byte(0x00,OLED_CMD);//---设置低列地址
	OLED_WR_Byte(0x10,OLED_CMD);//---设置高列地址
	OLED_WR_Byte(0x40,OLED_CMD);//--设置映射RAM显示起始线(0x00~0x3F)
	OLED_WR_Byte(0x81,OLED_CMD);//--设置对比度控制寄存器
	OLED_WR_Byte(0xCF,OLED_CMD);// 设置SEG输出电流亮度
	OLED_WR_Byte(0xA1,OLED_CMD);//--赛格/列映射    0xa0左右反置 0xa1正常
	OLED_WR_Byte(0xC8,OLED_CMD);//设置“COM/Row扫描方向”  0xc0上下反置 0xc8正常
	OLED_WR_Byte(0xA6,OLED_CMD);//--设置正常显示
	OLED_WR_Byte(0xA8,OLED_CMD);//--设置复用比例(1 ~ 64)
	OLED_WR_Byte(0x3f,OLED_CMD);//--1/64 duty
	OLED_WR_Byte(0xD3,OLED_CMD);//-设置显示偏移移位映射RAM计数器(0x00~0x3F)
	OLED_WR_Byte(0x00,OLED_CMD);//-not offset
	OLED_WR_Byte(0xd5,OLED_CMD);//--设置显示时钟分频比/振荡器频率
	OLED_WR_Byte(0x80,OLED_CMD);//--设置分割比率,设置时钟为100帧/秒
	OLED_WR_Byte(0xD9,OLED_CMD);//--设置pre-charge时期
	OLED_WR_Byte(0xF1,OLED_CMD);//设置预充电为15个时钟&放电为1个时钟
	OLED_WR_Byte(0xDA,OLED_CMD);//--设置com引脚硬件配置
	OLED_WR_Byte(0x12,OLED_CMD);
	OLED_WR_Byte(0xDB,OLED_CMD);//--设置vcomh
	OLED_WR_Byte(0x40,OLED_CMD);//设置VCOM取消选择级别
	OLED_WR_Byte(0x20,OLED_CMD);//-设置页面寻址模式(0x00/0x01/0x02)
	OLED_WR_Byte(0x02,OLED_CMD);//
	OLED_WR_Byte(0x8D,OLED_CMD);//--set Charge Pump enable/disable
	OLED_WR_Byte(0x14,OLED_CMD);//--set(0x10) disable
	OLED_WR_Byte(0xA4,OLED_CMD);// 禁用整个显示打开(0xa4/0xa5)
	OLED_WR_Byte(0xA6,OLED_CMD);// 禁用逆显示开关(0xa6/a7)
	OLED_Clear();
	OLED_WR_Byte(0xAF,OLED_CMD);
}


5、系统调试及分析

5.1、系统调试

需要调整PID参数和定时器CCR的参数,还是比较麻烦的

5.2、调试现象及分析

小车的pid会不稳定,会前后颠倒,倾斜角度不得大于30,否则GG

5.3、测试结果

心累
在这里插入图片描述
在这里插入图片描述

6、参考借鉴内容

https://www.bilibili.com/video/BV1AZ4y1V7wt?p=27&spm_id_from=pageDriver

https://blog.csdn.net/best_xiaolong/article/details/105153978

https://wenku.baidu.com/view/3e5fc765bb4cf7ec4bfed00b.html

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

创客阿蛋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值