天天看點

基于stm32的自平衡小車引言1、系統概述2、方案設計與論證3、單片機的選擇及硬體設計介紹4、系統程式5、系統調試及分析6、參考借鑒内容

文章目錄

  • 引言
  • 1、系統概述
    • 1.1、設計任務
    • 1.2、設計要求
  • 2、方案設計與論證
    • 2.1、晶片選擇方案
    • 2.2 、系統概述
    • 2.3、設計要求
    • 2.4、系統總體設計
    • 2.5、各功能子產品程式實作原理分析
      • 2.5.1、MPU6050子產品的介紹
      • 2.5.2、OLED12864顯示屏
      • 2.5.3、LN298N電機驅動塊
  • 3、單片機的選擇及硬體設計介紹
    • 3.1、單片機選擇
    • 3.2、電路設計
  • 4、系統程式
    • 4.1、主程式
      • 4.1.1主程式設計如下
      • 4.1.2主程式流程圖
    • 4.2、主程式代碼
    • 4.3、子產品程式代碼
  • 5、系統調試及分析
    • 5.1、系統調試
    • 5.2、調試現象及分析
    • 5.3、測試結果
  • 6、參考借鑒内容

引言

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、電路設計

基于stm32的自平衡小車引言1、系統概述2、方案設計與論證3、單片機的選擇及硬體設計介紹4、系統程式5、系統調試及分析6、參考借鑒内容
基于stm32的自平衡小車引言1、系統概述2、方案設計與論證3、單片機的選擇及硬體設計介紹4、系統程式5、系統調試及分析6、參考借鑒内容
基于stm32的自平衡小車引言1、系統概述2、方案設計與論證3、單片機的選擇及硬體設計介紹4、系統程式5、系統調試及分析6、參考借鑒内容

4、系統程式

4.1、主程式

4.1.1主程式設計如下

由mpu6050采集角度資訊,并發送到STM32上,STM32進行PID的調節,回報給電機驅動塊,電機驅動塊調整電機轉動方向和速度,以達到調整角度的目的,電機驅動塊和mpu6050閉環。

4.1.2主程式流程圖

基于stm32的自平衡小車引言1、系統概述2、方案設計與論證3、單片機的選擇及硬體設計介紹4、系統程式5、系統調試及分析6、參考借鑒内容

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、測試結果

心累

基于stm32的自平衡小車引言1、系統概述2、方案設計與論證3、單片機的選擇及硬體設計介紹4、系統程式5、系統調試及分析6、參考借鑒内容
基于stm32的自平衡小車引言1、系統概述2、方案設計與論證3、單片機的選擇及硬體設計介紹4、系統程式5、系統調試及分析6、參考借鑒内容

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