基於STM32的避障小車
最近幾天的學習了STM32輸入捕獲輸入捕獲的相關知識,爲了鞏固自己學習的知識特意製作一輛有三個超聲波組成的4輪避障小車來加深對輸入捕獲的理解。
1.輸入捕獲簡介
輸入捕獲模式可以用來測量脈衝寬度或者測量頻率。 我們以測量脈寬爲例,用一個簡圖來說明輸入捕獲的原理,如圖 15.1.1 所示:
如圖 15.1.1 所示,就是輸入捕獲測量高電平脈寬的原理,假定定時器工作在向上計數模式,圖中 t1~t2 時間,就是我們需要測量的高電平時間。測量方法如下:首先設置定時器通道 x 爲上升沿捕獲,這樣, t1 時刻,就會捕獲到當前的 CNT 值,然後立即清零 CNT,並設置通道 x爲下降沿捕獲,這樣到 t2 時刻,又會發生捕獲事件,得到此時的 CNT 值,記爲 CCRx2。 這樣,根據定時器的計數頻率,我們就可以算出 t1~t2 的時間,從而得到高電平脈寬。
在 t1~t2 之間,可能產生 N 次定時器溢出,這就要求我們對定時器溢出,做處理,防止高電平太長,導致數據不準確。如圖 15.1.1所示,t1~t2之間,CNT計數的次數等於:N*ARR+CCRx2,有了這個計數次數,再乘以 CNT 的計數週期,即可得到 t2-t1 的時間長度,即高電平持續時間。輸入捕獲的原理。
STM32F1 的定時器,除了 TIM6 和 TIM7,其他定時器都有輸入捕獲功能。 STM32F1 的輸入捕獲,簡單的說就是通過檢測 TIMx_CHx 上的邊沿信號,在邊沿信號發生跳變(比如上升沿/下降沿)的時候,將當前定時器的值(TIMx_CNT)存放到對應的通道的捕獲/比較寄存器(TIMx_CCRx)裏面,完成一次捕獲。同時還可以配置捕獲時是否觸發中斷/DMA 等。
接下來,我們介紹我們需要用到的一些寄存器配置,需要用到的寄存器有: TIMx_ARR、TIMx_PSC、 TIMx_CCMR1、 TIMx_CCER、 TIMx_DIER、 TIMx_CR1、 TIMx_CCR1 ,我們這裏針對性的介紹這幾個寄存器的配置。
捕獲/比較模式寄存器 1: TIMx_CCMR1
該寄存器的各位描述如圖 15.1.2 所示:
當在輸入捕獲模式下使用的時候,對應圖 15.1.2 的第二行描述,從圖中可以看出,TIMx_CCMR1 明顯是針對 2 個通道的配置,低八位[7: 0]用於捕獲/比較通道 1 的控制,而高八位[15: 8]則用於捕獲/比較通道 2 的控制,因爲 TIMx 還有 CCMR2 這個寄存器,所以可以知道CCMR2 是用來控制通道 3 和通道 4。
捕獲/比較使能寄存器: TIMx_CCER
這裏我們以通道1爲例,具體的參考中文手冊。我們要用到這個寄存器的最低 2 位, CC1E 和 CC1P 位。這兩個位的描述如圖 15.1.4所示:
所以,要使能輸入捕獲,必須設置 CC1E=1,而 CC1P 則根據自己的需要來配置。
DMA/中斷使能寄存器: TIMx_DIER
我們需要用到中斷來處理捕獲數據,所以必須開啓通道 1 的捕獲比較中
斷,即 CC1IE 設置爲 1。
控制寄存器: TIMx_CR1
我們只用到了它的最低位,也就是用來使能定時器的。
捕獲/比較寄存器 1: TIMx_CCR1
該寄存器用來存儲捕獲發生時, TIMx_CNT的值,我們從 TIMx_CCR1 就可以讀出通道 1 捕獲發生時刻的 TIMx_CNT 值,通過兩次捕獲(一次上升沿捕獲,一次下降沿捕獲)的差值,就可以計算出高電平脈衝的寬度(注意,對於脈寬太長的情況,還要計算定時器溢出的次數)
注意以上寄存器的配置均以通道1爲例,具體的配置看中文參考書冊。
2、超聲波模塊簡介
這裏我們用的是HC-SR04模塊,此模塊性能穩定,測度距離精確,模塊高精度,盲區小。採用IO口TRIG觸發測距,給至少10us的高電平信號;模塊自動發送8個40khz的方波,自動檢測是否有信號返回;有信號返回,通過IO口ECHO輸出一個高電平,高電平持續的時間就是超聲波從發射到返回的時間。測試距離=(高電平時間*聲速(340M/S))/2;本模塊使用方法簡單,一個控制口發一個10US以上的高電平,就可以在接收口等待高電平輸出。一有輸出就可以開定時器計時,當此口變爲低電平時就可以讀定時器的值,此時就爲此次測距的時間,方可算出距離。如此不斷的週期測,即可以達到你移動測量的值。
3、小車設計思路
3個超聲波分別安裝在正前方左邊和右邊各一個,具體情況見下圖:
在主函數裏調用測距函數,分別計算出障礙物離小車前方、左邊、右邊的距離lenth1、lenth2、lenth3,如果前方距離大於20cm並且左邊和右邊的距離大於10釐米則前進(GO)負責判斷左邊和右邊的距離。如果左邊距離小於10cm(lenth2<10),則右轉(YOU)。如果右邊的距離小於10cm(lenth3<10),則左轉(ZUO)。其它情況下則是:後退——>延時500ms——>左拐——>延時200ms。值得注意的是:小車在超聲波測距時要保證小車處於靜止狀態爲了避免小車在測距時小車依舊在運動從而增加誤差。同時利用PWM調速儘量讓小車速度慢下來,來減小誤差。
4、程序代碼
1、超聲波測距初識化
這裏我們用的是定時2(TIM2)的通道2、3、4,在這段程序是對定時器2的4個通道進行初始化。在工程文件下新建一個HC6的文件,新建一個hc6.c和hc6.h文件。期中hc6.c代碼如下:
#include "hc6.h"
#include "sys.h"
void HCSR04_Init(u16 arr,u16 psc)
{
RCC->APB1ENR|=1<<0; //TIM2時鐘使能
RCC->APB2ENR|=1<<2; //使能PORTA時鐘
RCC->APB2ENR|=1<<3; //使能PORTB時鐘
RCC->APB2ENR|=1<<4; //使能PORTC時鐘
GPIOA->CRL&=0XFFFF0000;//PA0~3清除之前設置
GPIOA->CRL|=0X00008888;//PA0~3浮空輸入
GPIOA->ODR|=0<<0;
GPIOA->ODR|=0<<1; //PA1下拉
GPIOA->ODR|=0<<2;
GPIOA->ODR|=0<<3;
GPIOB->CRL&=0X000FFFFF;
GPIOB->CRL|=0X33300000;
GPIOB->ODR|=1<<7; //PB7 輸出高
GPIOB->ODR|=1<<6;
GPIOB->ODR|=1<<5;
TIM2->ARR=arr; //設定計數器自動重裝值
TIM2->PSC=psc; //預分頻器
TIM2->CCMR1|=1<<0; //CC1S=01 選擇輸入端IC1映射到TI1
TIM2->CCMR1|=1<<4; //IC1F=0001 配置濾波器 以Fck_int採樣,兩個事件後有效
TIM2->CCMR1|=0<<2; //IC1PS=00 配置輸入分頻,不分頻
TIM2->CCER|=0<<1; //CC1P=0 上升沿捕獲
TIM2->CCER|=1<<0; //CC1E=1 允許捕獲計數器的值到捕獲寄存器中
TIM2->CCMR1|=1<<8; //CC2S=01 選擇輸入端IC1映射到TI1
TIM2->CCMR1|=1<<12; //IC2F=0001 配置濾波器 以Fck_int採樣,兩個事件後有效
TIM2->CCMR1|=0<<10; //IC2PS=00 配置輸入分頻,不分頻
TIM2->CCER|=0<<5; //CC2P=0 上升沿捕獲
TIM2->CCER|=1<<4; //CC2E=1 允許捕獲計數器的值到捕獲寄存器中
TIM2->CCMR2|=1<<0; //CC3S=01 選擇輸入端IC1映射到TI1
TIM2->CCMR2|=1<<4; //IC3F=0001 配置濾波器 以Fck_int採樣,兩個事件後有效
TIM2->CCMR2|=0<<2; //IC3PS=00 配置輸入分頻,不分頻
TIM2->CCER|=0<<9; //CC3P=0 上升沿捕獲
TIM2->CCER|=1<<8; //CC3E=1 允許捕獲計數器的值到捕獲寄存器中
TIM2->CCMR2|=1<<8; //CC4S=01 選擇輸入端IC1映射到TI1
TIM2->CCMR2|=1<<12; //IC4F=0001 配置濾波器 以Fck_int採樣,兩個事件後有效
TIM2->CCMR2|=0<<10; //IC4PS=00 配置輸入分頻,不分頻
TIM2->CCER|=0<<13; //CC4P=0 上升沿捕獲
TIM2->CCER|=1<<12; //CC4E=1 允許捕獲計數器的值到捕獲寄存器中
TIM2->DIER|=1<<1;
TIM2->DIER|=1<<2; //允許捕獲中斷
TIM2->DIER|=1<<3; //允許捕獲中斷
TIM2->DIER|=1<<4; //允許捕獲中斷
TIM2->DIER|=1<<0; //允許更新中斷
TIM2->CR1|=0X01; //使能定時器2
MY_NVIC_Init(2,0,TIM2_IRQn,2);//搶佔2,子優先級0,組2
}
u8 TIM2CH1_CAPTURE_STA=0; //輸入捕獲狀態
u16 TIM2CH1_CAPTURE_VAL; //輸入捕獲值
u8 TIM2CH2_CAPTURE_STA=0; //輸入捕獲狀態
u16 TIM2CH2_CAPTURE_VAL; //輸入捕獲值
u8 TIM2CH3_CAPTURE_STA=0; //輸入捕獲狀態
u16 TIM2CH3_CAPTURE_VAL; //輸入捕獲值
u8 TIM2CH4_CAPTURE_STA=0; //輸入捕獲狀態
u16 TIM2CH4_CAPTURE_VAL; //輸入捕獲值
//定時器2中斷服務程序
void TIM2_IRQHandler(void)
{
u16 tsr;
tsr=TIM2->SR;
if((TIM2CH1_CAPTURE_STA&0X80)==0)//還未成功捕獲
{
if(tsr&0X01)//溢出
{
if(TIM2CH1_CAPTURE_STA&0X40)//已經捕獲到高電平了
{
if((TIM2CH1_CAPTURE_STA&0X3F)==0X3F)//高電平太長了
{
TIM2CH1_CAPTURE_STA|=0X80;//標記成功捕獲了一次
TIM2CH1_CAPTURE_VAL=0XFFFF;
}else TIM2CH1_CAPTURE_STA++;
}
}
if(tsr&0x02)//捕獲1發生捕獲事件
{
if(TIM2CH1_CAPTURE_STA&0X40) //捕獲到一個下降沿
{
TIM2CH1_CAPTURE_STA|=0X80; //標記成功捕獲到一次高電平脈寬
TIM2CH1_CAPTURE_VAL=TIM2->CCR1;//獲取當前的捕獲值
TIM2->CCER&=~(1<<1); //CC1P=0 設置爲上升沿捕獲
}else //還未開始,第一次捕獲上升沿
{
TIM2CH1_CAPTURE_VAL=0;
TIM2CH1_CAPTURE_STA=0X40; //標記捕獲到了上升沿
TIM2->CNT=0; //計數器清空
TIM2->CCER|=1<<1; //CC1P=1 設置爲下降沿捕獲
}
}
}
if((TIM2CH2_CAPTURE_STA&0X80)==0)//還未成功捕獲
{
if(tsr&0X01)//溢出
{
if(TIM2CH2_CAPTURE_STA&0X40)//已經捕獲到高電平了
{
if((TIM2CH2_CAPTURE_STA&0X3F)==0X3F)//高電平太長了
{
TIM2CH2_CAPTURE_STA|=0X80;//標記成功捕獲了一次
TIM2CH2_CAPTURE_VAL=0XFFFF;
}else TIM2CH2_CAPTURE_STA++;
}
}
if(tsr&0x04)//捕獲1發生捕獲事件
{
if(TIM2CH2_CAPTURE_STA&0X40) //捕獲到一個下降沿
{
TIM2CH2_CAPTURE_STA|=0X80; //標記成功捕獲到一次高電平脈寬
TIM2CH2_CAPTURE_VAL=TIM2->CCR2;//獲取當前的捕獲值
TIM2->CCER&=~(1<<5); //CC1P=0 設置爲上升沿捕獲
}else //還未開始,第一次捕獲上升沿
{
TIM2CH2_CAPTURE_VAL=0;
TIM2CH2_CAPTURE_STA=0X40; //標記捕獲到了上升沿
TIM2->CNT=0; //計數器清空
TIM2->CCER|=1<<5; //CC1P=1 設置爲下降沿捕獲
TIM2->CR1|=0x01;
}
}
}
if((TIM2CH3_CAPTURE_STA&0X80)==0)//還未成功捕獲
{
if(tsr&0X01)//溢出
{
if(TIM2CH3_CAPTURE_STA&0X40)//已經捕獲到高電平了
{
if((TIM2CH3_CAPTURE_STA&0X3F)==0X3F)//高電平太長了
{
TIM2CH3_CAPTURE_STA|=0X80;//標記成功捕獲了一次
TIM2CH3_CAPTURE_VAL=0XFFFF;
}else TIM2CH3_CAPTURE_STA++;
}
}
if(tsr&0x08)//捕獲1發生捕獲事件
{
if(TIM2CH3_CAPTURE_STA&0X40) //捕獲到一個下降沿
{
TIM2CH3_CAPTURE_STA|=0X80; //標記成功捕獲到一次高電平脈寬
TIM2CH3_CAPTURE_VAL=TIM2->CCR3;//獲取當前的捕獲值
TIM2->CCER&=~(1<<9); //CC1P=0 設置爲上升沿捕獲
}else //還未開始,第一次捕獲上升沿
{
TIM2CH3_CAPTURE_VAL=0;
TIM2CH3_CAPTURE_STA=0X40; //標記捕獲到了上升沿
TIM2->CNT=0;
TIM2CH3_CAPTURE_VAL=TIM2->CCR3; //計數器清空
TIM2->CCER|=1<<9; //CC1P=1 設置爲下降沿捕獲
TIM2->CR1|=0x01;
}
}
}
if((TIM2CH4_CAPTURE_STA&0X80)==0)//還未成功捕獲
{
if(tsr&0X01)//溢出
{
if(TIM2CH4_CAPTURE_STA&0X40)//已經捕獲到高電平了
{
if((TIM2CH4_CAPTURE_STA&0X3F)==0X3F)//高電平太長了
{
TIM2CH4_CAPTURE_STA|=0X80;//標記成功捕獲了一次
TIM2CH4_CAPTURE_VAL=0XFFFF;
}else TIM2CH4_CAPTURE_STA++;
}
}
if(tsr&0x10)//捕獲1發生捕獲事件
{
if(TIM2CH4_CAPTURE_STA&0X40) //捕獲到一個下降沿
{
TIM2CH4_CAPTURE_STA|=0X80; //標記成功捕獲到一次高電平脈寬
TIM2CH4_CAPTURE_VAL=TIM2->CCR4;//獲取當前的捕獲值
TIM2->CCER&=~(1<<13); //CC1P=0 設置爲上升沿捕獲
}else //還未開始,第一次捕獲上升沿
{
TIM2CH4_CAPTURE_VAL=0;
TIM2CH4_CAPTURE_STA=0X40; //標記捕獲到了上升沿
TIM2->CNT=0; //計數器清空
TIM2->CCER|=1<<13; //CC1P=1 設置爲下降沿捕獲
TIM2->CR1|=0x01;
}
}
}
TIM2->SR=0;//清除中斷標誌位
}
hc6.h代碼如下:
#ifndef __HC6_H
#define __HC6_H
#include "sys.h"
void HCSR04_Init(u16 arr,u16 psc);
#endif
2、電機模塊初始化
這裏我們用的是定時3和定時器1,在工程文件下新建一個MOTER的文件夾,新建一個moter.c和moter.h文件。期中moter.c代碼如下:
#include "moter.h"
#include "sys.h"
#include "delay.h"
void TIM_PWM1_Init(u16 arr,u16 psc)
{
//此部分需手動修改IO口設置
RCC->APB1ENR|=1<<1; //TIM3時鐘使能
RCC->APB2ENR|=1<<2; //GPIOA使能
RCC->APB2ENR|=1<<3; //GPIOB使能
RCC->APB2ENR|=1<<4;
GPIOA->CRL&=0X00FFFFFF;
GPIOA->CRL|=0XBB000000;
GPIOC->CRH&=0X000FFFFF;
GPIOC->CRH|=0X33300000;
GPIOB->CRL&=0XFFFFF000; //PB(0)是AIN1 PB(1)是AIN2
GPIOB->CRL|=0X00000333;
TIM3->ARR=arr; //設定計數器3自動重裝值
TIM3->PSC=psc; //預分頻器設置
TIM3->CCMR1|=6<<4; //CH1 PWM2模式
TIM3->CCMR1|=1<<3; //CH1預裝載使能
TIM3->CCMR1|=6<<12; //CH2 PWM2模式
TIM3->CCMR1|=1<<11; //CH2預裝載使能
TIM3->CCER|=1<<0; //OC1 輸出使能
TIM3->CCER|=1<<4; //OC1 輸出使能
TIM3->CR1=0x0080; //ARPE使能
TIM3->CR1|=0x01; //使能定時器3
RCC->APB2ENR|=1<<11; //TIM1定時器使能
GPIOA->CRH&=0XFFFF0FF0;
GPIOA->CRH|=0X0000B00B;
GPIOB->CRH&=0X0000FFFF;
GPIOB->CRH|=0X33330000;
TIM1->ARR=arr; //設定計數器自動重裝值
TIM1->PSC=psc; //預分頻器設置
TIM1->CCMR1|=6<<4; //CH1 PWM2模式
TIM1->CCMR1|=1<<3; //CH1預裝載使能
TIM1->CCMR2|=6<<12; //CH4 PWM2模式
TIM1->CCMR2|=1<<11; //CH4預裝載使能
TIM1->CCER|=1<<0; //OC1 輸出使能
TIM1->CCER|=1<<12; //OC4 輸出使能
TIM1->BDTR|=1<<15; //MOE 主輸出使能
TIM1->CR1=0x0080; //ARPE使能
TIM1->CR1|=0x01; //使能定時器1
STBY=1;
STBY1=1;
}
void GO(u16 a,u16 b)
{
AIN1=0;
AIN2=1;
BIN1=0;
BIN2=1;
AIN3=1;
AIN4=0;
BIN3=1;
BIN4=0;
TIM3->CCR1=a;//右上
TIM3->CCR2=b;//左上
TIM1->CCR1=a;//右下
TIM1->CCR4=b;//左下
}
void STOP(void)
{
AIN1=0;
AIN2=0;
BIN1=0;
BIN2=0;
AIN3=0;
AIN4=0;
BIN3=0;
BIN4=0;
}
void HOU(u16 a,u16 b)
{
AIN1=1;
AIN2=0;
BIN1=1;
BIN2=0;
AIN3=0;
AIN4=1;
BIN3=0;
BIN4=1;
TIM3->CCR1=a;//右上
TIM3->CCR2=b;//左上
TIM1->CCR1=a;//右下
TIM1->CCR4=b;//左下
}
void YOU(u16 a,u16 b)
{
AIN1=1;
AIN2=0;
BIN1=0;
BIN2=1;
AIN3=0;
AIN4=1;
BIN3=1;
BIN4=0;
TIM3->CCR1=a;//右上
TIM3->CCR2=b;//左上
TIM1->CCR1=a;//右下
TIM1->CCR4=b;//左下
}
void ZUO(u16 a,u16 b)
{
AIN1=0;
AIN2=1;
BIN1=1;
BIN2=0;
AIN3=1;
AIN4=0;
BIN3=0;
BIN4=1;
TIM3->CCR1=a;//右上
TIM3->CCR2=b;//左上
TIM1->CCR1=a;//右下
TIM1->CCR4=b;//左下
}
上面這段代碼主要包括定時器PWM的初始化,還有定義了5個函數。前後左右對應有兩個參數a和b,通過改變這兩個參數就可以控制電機的速度。同時也能保證左邊和右邊的兩個輪子的速度相同。
moter.h電機如下:
#ifndef __MOTER_H
#define __MOTER_H
#include "sys.h"
#define AIN1 PBout(0)
#define AIN2 PBout(1)
#define BIN1 PCout(13)
#define BIN2 PCout(14)
#define STBY PCout(15)
#define AIN3 PBout(15)
#define AIN4 PBout(14)
#define BIN3 PBout(13)
#define BIN4 PBout(12)
#define STBY1 PAout(12)
void TIM_PWM1_Init(u16 arr,u16 psc);
void STOP(void);
void GO(u16 a,u16 b);
void HOU(u16 a,u16 b);
void YOU(u16 a,u16 b);
void ZUO(u16 a,u16 b);
#endif
3.主函數如下:
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "hc6.h"
#include "moter.h"
#define Trig1 PBout(7) // PB7
#define Trig2 PBout(6)
#define Trig3 PBout(5)
extern u8 TIM2CH2_CAPTURE_STA; //輸入捕獲狀態
extern u16 TIM2CH2_CAPTURE_VAL; //輸入捕獲值
extern u8 TIM2CH3_CAPTURE_STA; //輸入捕獲狀態
extern u16 TIM2CH3_CAPTURE_VAL; //輸入捕獲值
extern u8 TIM2CH4_CAPTURE_STA; //輸入捕獲狀態
extern u16 TIM2CH4_CAPTURE_VAL; //輸入捕獲值
u32 DIS_Init(u8 *STA,u16 VAL)//定義計算距離函數
{
u32 temp;
u32 lenth;
if((*STA)&0X80)//成功捕獲到了一次高電平
{
temp=(*STA)&0X3F;
temp*=65536; //溢出時間總和
temp+=VAL; //得到總的高電平時間
lenth=temp*0.017; //計算長度
*STA=0; //開啓下一次捕獲
}
return lenth;
}
int main(void)
{
u32 lenth1;
u32 lenth2;
u32 lenth3;
Stm32_Clock_Init(9); //系統時鐘設置
delay_init(72); //延時初始化
uart_init(72,9600); //串口初始化
HCSR04_Init(0XFFFF,72-1);//以1Mhz的頻率計數
TIM_PWM1_Init(899,0);
while(1)
{
Trig1=1;
delay_us(20); //輸入一個20us的高電平
Trig1=0;
lenth1=DIS_Init(&TIM2CH2_CAPTURE_STA,TIM2CH2_CAPTURE_VAL);
delay_us(20);
Trig2=1;
delay_us(20); //輸入一個20us的高電平
Trig2=0;
lenth2=DIS_Init(&TIM2CH3_CAPTURE_STA,TIM2CH3_CAPTURE_VAL);
delay_us(20);
Trig3=1;
delay_us(20);
Trig3=0;
lenth3=DIS_Init(&TIM2CH4_CAPTURE_STA,TIM2CH4_CAPTURE_VAL);
if((lenth1>30)&&(lenth3>10)&&(lenth2>10))
{
GO(400,400);
}
else if(lenth3<10)
{
ZUO(300,300);
delay_ms(20);
}
else if(lenth2<10)
{
YOU(300,300);
delay_ms(20);
}
else
{
HOU();
delay_ms(500);
ZUO(300,300);
delay_ms(300);
}
STOP();
delay_ms(20);
}
}
主函數主要調用相關的文件和函數,同時定義了一個測距函數**u32 DIS_Init(u8 *STA,u16 VAL)**返回值是所測得的距離。同時每次調用以後要使得TIM2CHx_CAPTURE_STA的值爲0所以傳遞的是TIM2CHx_CAPTURE_STA的地址,同時定義函數的參數爲指針變量來使得爲0。在while(1)裏實現了小車的測距,同時根據相關的距離來判斷小車的運動狀態。